Field-level access control with Pundit
--pundit
If you enable Pundit, your controllers will look for a Policy that matches the name of the thing being built.
Realtime Field-level Access
Hot Glue gives you automatic field level access control if you create *_able?
methods for each field you'd like to control on your Pundit policy.
Take note this is not what Pundit recommends in their documentation for field-level access control. Instead, it has been implemented this way to provide field-by-field user feedback to the user shown in red just like Rails validation errors are currently shown. (Remember, we're talking about an access control failure, which may involve conditions that look like what you'd typically see in Rails validations).
You can refine on a field-by-field and role-by-role basis, which users have access to edit which fields on the Hot Glue layout. For everyone else, they see the field as viewable only.
A user inputting a field they don't have access to edit is only hypothetical for Hot Glue, because the interface correctly shows the field non-editable for them anyway. But, if the interface was edited or simply hacked (your web user can add hidden fields using the DevTools to try to set the values they don't have access to), the backend policy will prevent the hacking attempt by applying the access control in the update?
method of the policy. You can set the object errors here when catching for access control errors, as you see in the example below.
The *_able?
method should return true if the field can be edited and false if the field should appear as viewable only (non-editable). No distinction is made between the create/edit contexts. You may check if the record is new using new_record?
. To remove the field completely, either remove it from your --include
list or add it to your --exclude
list, whichever you are using.
The *_able?
method is used by Hot Glue only on the new and edit actions, but you must incorporate it into the policy's update?
method as in the example, which will extend other operations since Hot Glue will use the policy to authorize the action
Add one *_able?
method to the policy for each field you want to allow field-level access control.
Replace *
with the name of the field you want under access control. Remember to include _id
for foreign keys. You do not need it for any field you don't want under access control.
When the method returns true, the field will be displayed to the user (and allowed) for editing.
When the method returns false, the field will be displayed as read-only (viewable) to the user.
Important: These special fields determine only display behavior (new and edit), not create and update.
For create & update field-level access control, you must also implement the update?
method on the Policy. Notice how in the example policy below, the update?
method uses the name_able?
method when it is checking if the name field can be updated, tying the feature together.
You can set Pundit to be enabled globally on the whole project for every build in config/hot_glue.yml
(then you can leave off the --pundit
flag from the scaffold command)
:pundit_default:
(all builds in that project will use Pundit)
Here's an example ThingPolicy
that would allow editing the name field only if:
• the current user is an admin
• the sent_at date is nil (meaning it has not been sent yet)
For your policies, copy the initialize
method of both the outer class (ThingPolicy
) and the inner class (Scope
) exactly as shown below.
If you argue with this implementation and think that there should be a way to make a field invisible based on access control — for example, if you have two different user contexts (roles) with different concerns and you want to use the same Hot Glue controller — instead you should simply make different Hot Glue controllers for the different purposes, hiding or showing the fields applicable to the users in their contexts. A singular application-wide policy is used to enforce consistency across the app, but each area of the app (namespaced Hot Glue controller) has a different user context interaction and, therefore, which fields to include or exclude get defined based on that.
class ThingPolicy < ApplicationPolicy
def initialize(user, record)
@user = user
@record = record
end
def name_able?
@record.sent_at.nil?
end
def update?
if !@user.is_admin?
return false
elsif record.name_changed? && !name_able?
record.errors.add(:name, "cannot be changed.")
return false
else
return true
end
end
class Scope < Scope
attr_reader :user, :scope
def initialize(user, scope)
@user = user
@scope = scope
end
end
end
Because Hot Glue detects the *_able?
methods at build time, if you add them to your policy, you will have to rebuild your scaffold.
When mixing the show only, update show only, and Pundit features, notice that the show only and update show only will act to override whatever the policy might say, so fields specified in those settings will be shown as viewable (non-editable) even when the policy may allow it.
Remember, the show only list is specified using --show-only
and the update show only list is specified using --update-show-only
.
'Viewable' means it displays as view-only (not editable) even on the form. In this context, 'viewable' means 'read-only'. It does not mean 'visible'.
That's because when the field is not viewable, then it is editable or inputable. This may seem counter-intuitive for a standard interpretation of the word 'viewable,' but consider that Hot Glue has been carefully designed this way. If you do not want the field to appear at all, remove it using the exclude list or don't specify it in your include list.
If the field is being built, Hot Glue assumes your users want to see or edit it. Other special cases are beyond the scope of Hot Glue but can easily be added using direct customization of the code.
Without Pundit:
on new screen | on edit screen | |
---|---|---|
for a field on the show only list | viewable | viewable |
for a field on the update show only list | inputable | viewable |
for all other fields | inputable | inputable |
With Pundit:
on new screen | on edit screen | |
---|---|---|
for a field on the show only list | viewable | viewable |
for a field on the update show only list | check policy | viewable |
for all other fields | check policy | check policy |