zooniverse/Panoptes — pull request #132 by edpaget (2014-08-01 15:59:21+00:00)
Title: Control control - Better Permissions and Roles

**THIS IS DEFINITELY NOT READY YET** I've still got to a bunch of testing and and revise the rest of the existing code to use this. This will close #69, when it's merged.

I want to give a quick high-level overview about what I'm doing to get a bit of feedback, even if it's "Ed this is crazy why would we ever do things that way". I'll also add this to the wiki for documentation if we merge this.

This PR introduces two new namespaces `ControlControl` and `RoleControl` (they're separated so eventually I can turn them into gems).

### `ControlControl`
This is a generic library for determining if one object is allowed to take actions on another object. It has two main modules `Resource` and `Actor`

The `Resource` Module defines the class method `can(action, filter=nil, &block)`, it defines an action type an actor object can take on a Resource (something abstract like `:edit` or `:read`), and then either takes a symbol name of an instance method, or a proc that is `instance_eval`ed. It also creates an instance method called `can_#{ action }?` that runs the supplied method or proc against an actor object to determine if it's allowed to take an action.

You can call `can` with the same action multiple times to build a chain of filters. An example resource class could look like.

```ruby
class Thing
extends ControlControl::Resource

attr_accessible :favorite_thing

can :read, { |actor| actor.is_admin? }
can :read, :has_permissions
can :edit, :has_permissions

def has_permissions(actor)
actor.has_grail?
end
end
```

There is also a small extension of `Resource` called `ActAs` for objects that are both resources and actors that adds a `can_as` class method that acts similarly to `can`. It address #69.

Instances of this class will have methods `can_read?` and `can_edit?`. `can_read?` will be true if the actor object `has_grail?` or `is_admin?`.

The `Actor` module is included adds a single method to a class it's included in. `do_to_resource(resource, action, as: nil, &block)`. This method simply tests the actor object against the `can_#{ action }?` method on the supplied resource object, and either raises a `ControlControl::AccessDenied` exception or executes the supplied proc with actor and resource as its arguments.

An example:

```ruby
Class ActingThing
include ControlControl::Actor

def has_grail?
true
end
end

thing = Thing.new
at = ActingThing.new

at.do_to_resource(thing, :edit) do |actor, resource|
resource.favorite_thing = actor
end
```

It also includes an optional keyword argument `as` that accepts another object that extends the `ActAs` method. When that argument is used the calling object is tested against the `can_#{ action }_as?` method of the `as` object. If that test sucess it, then does a `as_object.do_to_resource(resource, action, &block)` call. If that test fails, a `ControlControl::AccessDenied` exception is raise.

### `RoleControl`
RoleControl includes `Enrolled`, `Controlled`, and `VisibilityControlled` that sit on top of ControlControl and implement logic for accessing by user roles.

`Enrolled` is implemented on top of Actor and adds a `roles_for(klass, association)` class method. It's used with the instance method `roles_query_for` that produces an ActiveRecord::Relation based on the supplied association for an instance of the given named class.

I feel like that was a bad explaination so here's an example:

```ruby
class User < ActiveRecord::Base

has_many :user_project_preferecences

roles_for Project, :user_project_preferences
end

p = project.create!
p.save!
u = User.create!
u.save!
UserProjectPreference.create(user: u, project: p, roles: ['collaborator']).save!
u.roles_query_for(p) #=> ActiveRecord::Relation<[UserProjectPreference(project_id: 1, roles: ['collaborator'])]>
```

`Controlled` is implemented on top of `Resource` and adds a `can_by_role(action, *permitted_roles) method. Basically you give a can action name as in `Resource` and then a list of permitted roles. When you call `can_#{ action }?` it compares the supplied roles against the result of `enrolled_actor.roles_query_for(controlled_resource)`.

Example: (assuming the above User is still defined).

```ruby
class Project < ActiveRecord::Base
extend RoleControl::Controlled

can_by_role :edit, :collaborator, :translator
end

p = Project.new(name: "blad!")
p.save!
u = User.new(name: "grrr!")
u.save!
upp = UserProjectPreferences.new(user: u, project: p, roles: ['collaborator'])
upp.save!

u.do_to_resource(p, :edit) do |user, project|
project.favorite_user = u
project.save!
end
```

`VisibilityControlled` is a further extenion of `Controlled` that allows the permitted roles to be serialized to the Database instead of declared in code. Right now is only implemented as in a `can_read?` predicate, and a `visible_to` class_method that returns a scope of objects that are visible to an actor.

This does change how visibility is handled from how it was previously. Instead of having a visibility state, we're instead going to let users decide what roles a project will be visible to at a given time. (The state bit could still be faked in the front end if we want to keep similar functionality).

Let me know if there any other questions about how stuff works in this. I know there are still a few more TODOs before this is ready to merge. Off the top of my head I can think of:

- [ ] Specs for `RoleControl` and `ControlControl`
- [ ] Conditional inclusion of ownership test in `VisibilityControl
- [ ] Conversion of Controllers to use this.
- [ ] Constrain and Validate the names of assignable roles.

And I'm sure there will be more. Also apologies for the numerous spelling errors that are undoubtedly in here.