Skip to content

Instantly share code, notes, and snippets.

@atz
Last active January 5, 2017 01:03
Show Gist options
  • Save atz/ccf685c547927e4749cf3a1feead190b to your computer and use it in GitHub Desktop.
Save atz/ccf685c547927e4749cf3a1feead190b to your computer and use it in GitHub Desktop.
Sufia/CurationConcerns CanCan notes

Context

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.

Use Case

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.

Attempts

The generated application has a conventional Ability class where most of the permissions are defined. This is the location of the initial implementation attempts.

Delegation aka "Shared Definitions"

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.

CuractionConcerns convention

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.

Problem:

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.

Metaprogramming 1

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.

Metaprogramming 2

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.

A note on :create vs. :new

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.

Restrictions

To summarize:

  • cannot use curation_concern_type= (or load_and_authorize_resource) per request,
  • cannot use Hash conditions or any block ability definition with :create, and
  • cannot call authorized_models or can? in can.

Conclusion: take it out of Ability and just check permissions in the controller actions. Avoid CanCan and CuractionConcerns convenience methods.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment