Skip to content

Authorization Models

markrebec edited this page Apr 10, 2013 · 1 revision

Authorization models are any of the subject, role, permission or resource/context models that are used by the authorization system (take a look at the Getting Started section for a brief explanation of each). They are configured using the acts_as_authorization_* methods - such as acts_as_authorization_subject for authorization subjects.

When a model "acts as an authorization object," it inherits some behaviors specific to the type of object it's emulating. For example with subjects, this provides them the ability to have roles and permissions assigned to them, or check if they has_role?(:admin). Roles and permissions are a bit simpler, and are provided with some methods to help them behave as what they are, check them against subjects, etc. And finally resources/contexts, if you choose to define them, are given some shortcut methods like allowed?(user, role) which essentially are just wrappers to check whether the user possesses the role for the provided resource (within the provided context).

###Subjects Zuul authorization subjects provide a few methods to make it easy to assign, remove and verify roles and permissions.

TODO: add a table with a list of methods like has_role?, assign_role, unassign_role, etc.

###Roles Authorization roles are provided with methods to assign, remove and verify permissions against the role. This in turn grants those permissions to any subjects who possess the role.

TODO: add a table with a list of methods like has_permission?, assign_permission, etc.

###Permissions Authorization permissions do not have any useful public methods available, since all the management is handled against the subjects or roles to which they are assigned. They mostly just have a few associations and internal methods defined to facilitate their use by the other authorization objects.

###Resources & Contexts Defining resources (also used as contexts) is not required. You can use subjects, roles and permissions to authorize against any model without having to modify the resource model in any way. However, you have the option of configuring your resource models to acts_as_authorization_context, which will provide a few shortcut methods for you (which are all just wrappers for the methods defined on the subject being authorized).

TODO: add a table with the resource/context methods - allowed?, allowed_to?

###Configuring class names Zuul comes with some default class names that are used when generating and configuring authorization models, but you can use any class names you'd like. These can be defined globally (see the Configuration section of this readme) to DRY up your code, or when setting up a model to acts_as_authorization_*. If you set them up globally they'll be used in place of the defaults unless you override them.

When configuring an authorization model, you don't need to provide the name of the model you're configuring. So if you're configuring a Soldier as an authorization subject, you don't need to provide the subject class name. You also don't need to worry about table names or foreign keys, as zuul will ask your models for that information rather than trying to inflect the provided class names.

Here is an example using some custom classes:

# this is our subject, a chef who will be assigned cuisines
class Chef < ActiveRecord::Base
  # you can set classes as strings, symbols or actual class constants
  acts_as_authorization_subject :role_class => :cuisine, :permission_class => Ingredient
end

# cuisine might be things like 'seafood', 'bbq', etc.
class Cuisine < ActiveRecord::Base
  acts_as_authorization_role :subject_class => :chef, :permission_class => :ingredient
end

# a cuisine would probably have ingredients assigned to it - like 'fish' and 'scallops' for seafood
# but a chef might also have an ingredient like 'fish' in their repertoire without specializing in seafood
class Ingredient < ActiveRecord::Base
  acts_as_authorization_permission :subject_class => :chef, :role_class => :cuisine
end

If you only provide the core class names, as above, zuul will fill in the blanks for the association models automatically. So in the above example, chefs and cuisines will be linked together using the ChefCuisine model.

You can override each association model class name as well if you need to. Let's say you wanted to link Chef and Cuisine with a model called Specialty. Just provide the :role_subject_class as well:

class Chef < ActiveRecord::Base
  acts_as_authorization_subject :role_class => :cuisine, :role_subject_class => :specialty, :permission_class => :ingredient
end

class Cuisine < ActiveRecord::Base
  acts_as_authorization_subject :subject_class => :chef, :role_subject_class => :specialty, :permission_class => :ingredient
end

The config options for the three association classes are :role_subject_class, :permission_role_class and :permission_subject_class.

###The Context Chain All operations involving roles and permissions within zuul utilize "the context chain," which dictates what roles and permissions are allowed to be used within what context. There are three types of contexts - global, class level and object/instance level - and they are organized in a hierarchy which defines how they may be used within the chain. Basically they cascade in order, from global to class to instance level:

Roles and permissions defined within the global context may also be assigned at the class or instance level. The global context is sometimes also referred to as a 'nil context', since it is defined by leaving the context data blank.

Those defined at the class level may only be used within a class level context that matches it's own, or any instance level contexts with a matching class. The class level context is defined by providing the class name to which the context applies. For example, you might assign the role :admin to a subject within a context of BlogPost, and use that to allow subjects to have admin access to all blog posts.

If defined at the instance level, the role or permission may only be used within that context. The instance level context is defined by providing the class name and the primary key (id) of the record to which the context applies. This allows you to assign the permission :edit_thread to a role or individual subject within the context type of DiscussionForum and the id of a specific forum, which you can then use to allow editing of threads within just the specified forum.

When dealing with contexts in zuul, one important thing to keep in mind is that there are two types of contexts in action. There is the context within wich a role or permissions is defined and the context within which a role or permission is assigned, and the former generally dictates the latter. This means that any role or permission defined within a context (including the nil/global context) may only be used within it's own context or other valid contexts further down the chain.

So for example, if you define the role :admin within a global context, you may assign that role to subjects within any context. However, if you define that :admin role within the context of DiscussionForum you may not assign that role within a global context or any other class context, but you may assign it within the same context or within an instance level context for an instance of that same class.

It is also important to understand that zuul will prefer the closest contextual match to the one provided, and uses the context chain both for the lookup of the role or permission and when checking whether it is assigned to a subject. This mostly comes into play when you want to force the use of a particular context and not bubble up the context chain.

But why would you want to define and assign a role in two separate contexts, or even define contexts in the first place? It depends, you may not need contexts at all. If that's the case you can just use the default global context without even thinking about it (just don't specify contexts), and you can probably skip this section entirely. However, you may want to define and assign roles separately for specific types of resources, or even define a single set of roles at the global level and then assign them contextually.

Maybe you want to define one :moderator role to be used for your discussion forums and a separate :moderator role only for use with a video submissions system. You could create two separate roles, both with the moderator slug but each defined within their own context, and then you could assign those to the users who are moderators of the appropriate section of your site. Alternately you might want to define a single global :moderator role, and then just assign that role to various subjects within the appropriate context (discussion forums, video submissions system, etc.).

Let's look at a more in-depth example with multiple contexts at work using a company that provides a self publishing app for users to upload, create, edit and publish ebooks. In this example, we'll assume we have the following models: User, Role, Permission, Publisher, Series and Book. The user, role and permission models are self explanatory, and the others should be fairly obvious. A publisher is the organization under which series and books are organized, a series represent a collection of books, and a book is a book. Multiple users may be linked to a publisher, each with varying abilities based on their roles and permissions.

In this scenario, you might have a set of roles defined with a global context, like the following:

admin = Role.create(:slug => 'admin', :level => 100)
manager = Role.create(:slug => 'manager', :level => 70)
employee = Role.create(:slug => 'employee', :level => 60)

You would then assign those roles, also within a global context, to the user accounts for employees of the company and either allow or deny them various abilities around the site based on those roles. Engineers and officers could have the 'admin' role and be able to change settings that control the applications behavior, 'managers' might be allowed to pull reports for various departments, and an 'employee' might be given some other abilities beyond those of normal users.

You can assign roles within a global context like this:

user = User.find(1)
user.assign_role(:admin)

And you can check for roles using the has_role? method:

user.has_role?(:admin)  # => true

These same roles could also be used within a context to give users special abilities. For example, you might assign the role of 'admin' within the context of the Publisher class to some of your employee users:

bob = User.find(1)
bob.assign_role(:admin, Publisher)   # this would make the user an admin for publishers, but not a global admin

Then you could allow any users possessing that global 'admin' role, but assigned within the Publisher context, the ability to edit, update, destroy, etc. for any and all individual publishers. This user would not possess the global admin abilities mentioned above (unless you also separately assign the admin role within a global context). You'd check if a user possesses the role by passing the context in as well:

bob.has_role?(:admin)                 # => false (bob is a publisher admin, but not a global admin)
bob.has_role?(:admin, Publisher)      # => true
user.has_role?(:admin, Publisher)     # => true (because this user has the global admin role and it bubbles up the chain)

By default zuul does not force the use of the provided context, but instead bubbles up the chain when looking for roles/permissions. In this example, if a user possesses the global admin role assigned within a global context, they would also be allowed the same abilities as the Publisher admin.

If you were to tell zuul to force the context though, it would not look up the chain and the user would be required to possess the admin role within the specific context provided. If you wanted only users who possessed the admin role within the Publisher context and did not want to include global admins, you can force the context like this:

# passing true for force_context
user.has_role?(:admin, Publisher, true)   # => false (this user does not possess the role within the context)

However right now this same check would also fail for bob, even though he has the admin role assigned within the Publisher context. The reason is because forcing the context requires the role or permissions to be both defined and assigned within the force context:

bob.has_role?(:admin, Publisher, true)    # => false (this also fails because by default it wants the role to be DEFINED AND ASSIGNED within the forced context)

The catch here is that forcing the context for the role check will also force the context for the lookup of the role via the slug. Since our admin role is defined in the global context, this lookup will fail before we even check the role against the subject (even though it is assigned to bob within the forced Publisher context). In this case, as far as has_role? is concerned the publisher admin role doesn't even exist (because it's not bubbling up). In order for this to work using a slug, the admin role would have to be both defined and assigned within the Publisher context.

You can however work around this by passing a role or permission object (instead of a slug) to pretty much all the authorization methods. So for our previous example to work using the global roles defined in our example and assigned within the Publisher context, we'd actually want to do this:

global_admin = Role.find(1)   # this would be our global admin role
bob.has_role?(global_admin, Publisher, true)    # => true (because the role is provided so there is no lookup by slug, and it is assigned within the context being forced)
user.has_role?(global_admin, Publisher, true)   # => false (this still doesn't match because the user is assigned in the global context and the match doesn't bubble when forced)

Otherwise you can break out your role definitions by context and create a separate publisher admin role, in which case the slug lookup and the match will work:

publisher_admin = Role.create(:slug => 'admin', :level => 100, :context => Publisher)
bob.assign_role(:admin, Publisher)
bob.has_role?(:admin, Publisher, true)  # => true

A few more examples should help clarify:

# this is our global admin role
global_admin = Role.create(:slug => 'admin', :level => 100)
# this is our publisher admin role
publisher_admin = Role.create(:slug => 'admin', :level => 100, :context => Publisher)

user = User.find(1)

user.assign_role(:admin)                        # this will use the global context, so it will find the global_admin role and assign it within the global context
user.has_role?(:admin)                          # => true
user.has_role?(:admin, Publisher)               # => true (it bubbles up the context chain for the lookup and the match)
user.has_role?(:admin, Publisher, true)         # => false (since we're forcing context, it finds the publisher_admin role and can't make a match)
user.remove_role(:admin)

user.assign_role(:admin, Publisher)             # this will find the publisher_admin role and assign it within the Publisher context
user.has_role?(:admin)                          # => false (the user is not a global admin)
user.has_role?(:admin, Publisher)               # => true
user.has_role?(:admin, Publisher, true)         # => true (the role is defined and assigned within the forced context, so it matches)
user.remove_role(:admin, Publisher)

# use the global role, but assign it within the Publisher context, and force the context for the match
user.assign_role(global_admin, Publisher)       # this assigns the global_admin role within the assigned context of Publisher
user.has_role?(:admin)                          # => false (the global_admin role is looked up, but the user is not assigned the role in a global context)
user.has_role?(:admin, Publisher)               # => false (even though we're not forcing context here, because a Publisher admin role exists, that one is preferred over the global admin role in the lookup)
user.has_role?(global_admin, Publisher)         # => true (using the actual role object means the context is only used to match the role since it doesn't have to be looked up)
user.has_role?(:admin, Publisher, true)         # => false (the Publisher admin role is preferred in the lookup and no match is made)
user.has_role?(global_admin, Publisher, true)   # => true (since we provide the role object there is no lookup, and since the role we provided is assigned within the force context of Publisher, there is a match)

Now, to get back to our publishing example.

A user may also be assigned one of these global roles within the context of a specific publisher. Part of the requirements for your application might be for a new user to either create or join a publisher organization when they first sign up. When creating that publisher organization the user should be assigned as an 'admin' just for that publisher. Publisher admins would have the ability to invite or remove users as collaborators for their publisher, edit any of the publisher profile information or series or book content, etc. but not edit any other publisher's info/content.

user = User.find(1)
publisher = Publisher.find(1)
user.assign_role(:admin, publisher)   # or just user.assign_role(:admin, Publisher.find(1))

And now you can update your code to allow these publisher admins to do various things within the context of the publisher organization to which they belong. As with the class level context above, unless you choose to force the context the role check will bubble up the context chain, so any Publisher context admin users or any global admin users will be allowed these same abilities.

As mentioned, publisher admin users would be able to invite other users to join their organization and assign them roles. In that case, you could also reuse the 'manager' and 'employee' global roles within the individual publisher context. The publisher admin could assign those roles to other users within the publisher organization, and they would be allowed different abilities within the publisher context (maybe employees can only edit books, but managers can delete and create new series).

So now we've defined a few global roles, and we're using those roles in a few different contexts, but we've decided we want to introduce an 'editor' role for publishers. The editor role should be just below admins and just above managers. We don't have any use for the editor role outside the publisher context however, so instead of defining it at the global level, we'll define it at the Publisher level. That way we can use it to give users the editor role within publisher contexts, but it's not unnecessarily polluting our global or other class level context space.

editor = Role.create(:slug => 'editor', :level => 80, :context => Publisher)
user = User.find(1)
user.assign_role(:editor, Publisher.find(1))  # even though it COULD be used at the class level, here we're using the role to make the user an editor for just his own publisher instance

Because of the context chain, you can define as simple or complex of a set of roles and permissions as you want. If you want to keep one global set of roles and assign them within a context (or no context) you can. If you want to create a separate set of roles for each resource in your application, you can do that too. It's entirely up to you how to structure your abilities and how you'd like to mix-n-match everything.

###Scoping With the context chain outlined above zuul is already extremely flexible. But in order to add another level of customizability (and to keep unnecessary methods from polluting the ActiveRecord namespace), authorization scopes have been implemented as well. Each of the authorizaton objects is scoped to a provided namespace (the default is :default), which allows the object to be used in more than one scope. You may provide a named scope when defining the model acts_as_authorization_* which you may then use to access authorization methods within that scope. If a :default scope does not yet exist, your named scope will be aliased to :default.

Let's use an example of a web based role playing game. Let's say you have a website with multiple components - an HTML5 role playing game you're building, user profiles and discussion forums for users to discuss bugs, strategies, etc. In our example, we'll assume you have your default User model and Role and Permission models that you use outside of the game component to define roles like :forum_user to grant access to the forums, or :banned to prevent users from even logging into the webiste. Your User model would probably be defined with something like the following:

class User < ActiveRecord::Base
  acts_as_authorization_subject # this uses the defaults, which point to the Role and Permission classes
  
  # the rest of your model stuff would be here
end

Now what you'd like to do, is allow those same User objects to be assigned a Level and Skill for the game component. You plan on assigning skills to a level, and then assigning levels to each user as they progress through the game. Then various activities in the game will be based on whether or not a user possesses certain skills or is of a certain level. You can create a new scoped authorization subject for User that uses those new classes:

class User < ActiveRecord::Base
  acts_as_authorization_subject # this uses the defaults, which point to the Role and Permission classes
  acts_as_authorization_subject :scope => :character, :role_class => :level, :permission_class => :skill
  
  # the rest of your model stuff would be here
end

class Level < ActiveRecord::Base
  acts_as_authorization_role :permission_class => :skill  # the default subject model is User, so no need to specify it
  # you don't need to scope this to :character unless you want to, and since there is no other scope on this model, :character would still be aliased to :default
end

class Skill < ActiveRecord::Base
  acts_as_authorization_permission :role_class => :level    # the default subject model is User, so no need to specify it
  # you don't need to scope this to :character unless you want to, and since there is no other scope on this model, :character would still be aliased to :default
end

This would add an authorization scope of :character to User objects that can be used (with the auth_scope method) instead of the default:

user = User.find(1)
user.auth_scope(:character) do
  has_role_or_higher?(:level_10)  # check if the user is level 10 or greater
  has_permission?(:dual_wield)    # check if the user has the :dual_wield skill
end

user.has_role?(:forum_user)  # this uses the default scope to check for the :forum_user role

You might then even want to use contextual permissions within that scope so you can do things like assign the :dual_wield skill within the context of a weapon (swords, maces, axes, etc.).

There are plans to implement some dynamic aliasing, to allow for methods like has_level? to be aliased to has_role?, but for now you have to use the "role" and "permission" methods.

Clone this wiki locally