Last Updated: February 23, 2017
·
1.158K
· mroach_

simple_form: Dynamically disabled inputs based on Pundit policy

Screen Shot 2017-02-23 at 10.59.05.png

I use Pundit for managing authorisation in Rails apps. There's generally a 1:1 ratio of models to policy classes with an inheritance strategy that makes implementing authorisation quite clean. It allows you to control who can create, update, and delete which kinds of or which specific records.

I wanted to take this one step further and get into attribute-specific authorisation. Take for example my setups where I have Person models and then a person may have an associated User. This allows for cleaner separation of concerns with person info an user account info. When an admin creates a new User they'll pick the person its associated with. But once it's setup, it should not be allowed to change the Person relation. This is how I've achieved this both on the backend (strong params) and the front-end (simple_form)

In my ApplicationPolicy base class for Pundit policies I setup a few methods to make this easier:

class ApplicationPolicy
  ...

  # Default permitted attributes for create and update
  def permitted_attributes
    # slug is generated by friendly_id and should never be updatable
    # id and timestamp fields are never directly updatable
    record.attributes.keys.map(&:to_sym) - %i(id slug created_at updated_at deleted_at)
  end

  def permitted_create_attributes
    permitted_attributes
  end

  def permitted_update_attributes
    permitted_attributes
  end
end

Then, in the policy for a Users, we build on this to get more more granular.

class UserPolicy < ApplicationPolicy
  def permitted_attributes
    # fields maintained by devise are not directly updatable
    super - %i(encrypted_password reset_password_token reset_password_sent_at)
  end

  def permitted_update_attributes
    # for update, it's not allowed to change the person or email (username)
    permitted_attributes - %i(person person_id email)
  end
end

Now I'll use these lists of attributes in two places. The first is in my controller in place of the usual list of strong parameters.

protected

def permitted_params
  policy(@user).send("permitted_#{action_name}_attributes")
end

Now this is great and gives us exactly the list of writable attributes we want, but now we want that reflected in the UI so fields are greyed-out.

To do this I've had to monkey-patch the SimpleForm::Inputs::Base class a bit. Here's how:

# lib/simple_form_extensions/disabled_extensions.rb

module SimpleForm
  module DisabledExtensions
    private

    # I didn't name this method, so don't yell at me for its name!
    # rubocop:disable Style/PredicateName
    def has_disabled?
      super || disabled_by_policy?
    end

    def disabled_by_policy?
      # permit attributes by name or relation name. e.g. person or person_id
      !(permitted_attributes.include?(attribute_name) || permitted_attributes.include?(reflection_or_attribute_name))
    end

    def permitted_attributes
      # load the Pundit policy for the object
      policy = template.controller.policy(object)

      # map action names
      action = { edit: :update, new: :create }[template.controller.action_name.to_sym]

      # Get the list of permitted attributes as an array of attribute names
      # For attributes that permit array values, the entry is not a name, but
      # a hash with the key as the attribute name and a value of []
      # So for such cases, pluck the hash key as the name
      @permitted_attributes ||= policy.send("permitted_#{action}_attributes".to_sym)
                                    .map { |a| a.is_a?(Hash) ? a.keys.first : a }
    end
  end
end

SimpleForm::Inputs::Base.send :prepend, SimpleForm::DisabledExtensions

Then at the top of config/initializers/simple_form.rb

require 'simple_form_extensions/disabled_extensions'

Restart your server and now the UI automatically disables fields that are not updatable as per your Pundit policy!