The context is an application based on a Rails engine (actually, nested Rails engines: Sufia, CurationConcerns, Blacklight, et al. -- a matryoshka of engines) using CanCan permissions.
The use case is permissioning the batch creation of items of different types. The types are already modeled and have established permissions. Ideally we would have a single batch model with an awareness of what item type it is batching, dynamically delegating permissions to that model. I.E., if you can create one object of a given type, you can create a batch of them.
The trick for the engine code is: we don't know how many item types will exist ahead of time. They are generated.
The engine provides convenience methods and conventions for applying permissions in controllers for the cases where a single controller maps to a single item type. They are not suited for our purpose here, since the same batch controller will receive a batch and the permissions must be dynamically evaluated based on item type of that given batch.
The generated application has a conventional Ability
class where most of the permissions are defined. This is the location of the initial implementation attempts.
Permissions delegation is an existing CanCan feature, as documented here.
Example:
can :update, Task do |task|
can?(:update, task.project)
end
For our action that might be:
# batch permissions depend on the kind of objects being made
can :create, BatchUploadItem do |batch|
batch.payload_concern.nil? ? false : can?(:create, batch.payload_concern)
end
Failure: never gets called because The block is only evaluated when an actual instance object is present. It is not evaluated when checking permissions on the class (such as in the index action).
Conclusion: cannot use any block logic with :create
.
Idea: reuse the same uploads controller while following the convention to call curation_concern_type=
, but instead of statically, do it dynamically based on item type.
def curation_concern_type=(curation_concern_type)
load_and_authorize_resource class: curation_concern_type, instance_name: :curation_concern, except: [:show, :file_manager, :inspect_work]
self._curation_concern_type = curation_concern_type
end
Failure: load_and_authorize_resource
litters the controller with (before_filter
) side-effects and those persist for the entire session. Therefore calling curation_concern_type=
dynamically is impermissable: the old filters are not removed, they are aggregated with new ones. Performing surgery to effectively un-load_and_authorize_resource
is complex, brittle and dangerous.
CurationConcerns.config.registered_curation_concern_types.each do |model|
can :create, BatchUploadItem, payload_concern: model if can?(:create, model)
end
Failure: can
calling can?
causes infinite recursion or just fails from timing issues.
At runtime the set of models available to a Sufia user is retrieved via:
CurationConcerns::QuickClassificationQuery.new(@current_user).authorized_models
Unfortunately, that method looks like this:
def authorized_models
normalized_model_names.select { |klass| user.can?(:create, klass) }
end
Failure: calling authorized_models
in Ability
's definition also causes infinite recursion.
In Rails routing, GET /resources/new
is for accessing the form (via the #new
method) for creating a new resource, while POST /resources
is that form's payoff, an attempt to persist a new object (via the #create
method).
CanCan reasonably applies the :create
permissions to a Rails Resource Controller's #new
method, presumably under the premise that there is no point allowing the user to see the form if it can only fail when submitted.
So while we specify can :create MyClassName
, in a user's workflow they would see the effect first when attempting to access the new
form and method. Or, in debugging, more likely you will detect a failure of your intended permissions at that phase.
To summarize:
- cannot use
curation_concern_type=
(orload_and_authorize_resource
) per request, - cannot use Hash conditions or any block ability definition with
:create
, and - cannot call
authorized_models
orcan?
incan
.
Conclusion: take it out of Ability
and just check permissions in the controller actions. Avoid CanCan and CuractionConcerns convenience methods.