Skip to content
Jason King edited this page Jun 24, 2015 · 3 revisions

Role control subsystem has been lifted from Rails authorization plugin, but undergone some modifications.

It's based on two tables in the database. First, role table, which stores pairs [role_name, object] where object is a polymorphic model instance or a class. Second, join table, which joins users and roles.

To use this subsystem, you should define a Role model.

Role model

class Role < ActiveRecord::Base
  acts_as_authorization_role
end

The structure of roles table is as follows:

create_table "roles", :force => true do |t|
  t.string   :name
  t.string   :authorizable_type
  t.integer  :authorizable_id
  t.timestamps null: false
end

add_index :roles, [:authorizable_type, :authorizable_id]

Note that you will almost never use the Role class directly.

Subject model

class User < ActiveRecord::Base
  acts_as_authorization_subject  :association_name => :roles
end

You won't need any specific columns in the users table, but there should be a join table:

create_table "roles_users", :id => false, :force => true do |t|
  t.references  :user
  t.references  :role
end

add_index :roles_users, :user_id
add_index :roles_users, :role_id

Object model

Place acts_as_authorization_object call inside any model you want to act as such.

class Foo < ActiveRecord::Base
  acts_as_authorization_object
end

class Bar < ActiveRecord::Base
  acts_as_authorization_object
end

Interface

Subject model

A call of acts_as_authorization_subject defines following methods on the model:

subject.has_role?(role, object = nil)      # Returns `true` of `false` (has or has not)
subject.has_role!(role, object = nil)      # Assigns a `role` for the `object` to the `subject`
subject.has_no_role!(role, object = nil)   # Unassigns a role from the `subject`
subject.has_roles_for?(object)             # Does the `subject` has any roles for `object`? (`true` of `false`)
subject.has_role_for?(object)              # Same as `has_roles_for?`
subject.roles_for(object)                  # Returns an array of `Role` instances, corresponding to `subject`'s roles on object
subject.has_no_roles_for!(object)          # Unassign any `subject`'s roles for a given `object`
subject.has_no_roles!                      # Unassign all roles from `subject`

Object model

A call of acts_as_authorization_object defines following methods on the model:

object.accepts_role?(role_name, subject)       # alias for `subject.has_role?(role_name, object)`
object.accepts_role!(role_name, subject)       # alias for `subject.has_role!(role_name, object)`
object.accepts_no_role!(role_name, subject)    # alias for `subject.has_no_role!(role_name, object)`
object.accepts_roles_by?(subject)              # alias for `subject.has_roles_for?(object)`
object.accepts_role_by?(subject)               # alias for `accepts_roles_by?`
object.accepts_roles_by(subject)               # alias for `subject.roles_for(object)`

TODO - add the accepted_roles stuff in here.

TODO - also add the object.users and object.users :manager interface.

Custom class names

You may want to deviate from default User and Role class names. That can easily be done with arguments to acts_as_....

Say, you have Account and AccountRole:

class Account < ActiveRecord::Base
  acts_as_authorization_subject :role_class_name => 'AccountRole'
end

class AccountRole < ActiveRecord::Base
  acts_as_authorization_role :subject_class_name => 'Account'
end

class FooBar < ActiveRecord::Base
  acts_as_authorization_object :role_class_name => 'AccountRole', :subject_class_name => 'Account'
end

Or... you can put the following in your config/initializers/acl9.rb to affect all classes:

Acl9.configure do |c|
  c.default_role_class_name = 'AccountRole'
  c.default_subject_class_name = 'Account'
end 

...and then in your models you can drop the explicit settings:

class Account < ActiveRecord::Base
  acts_as_authorization_subject
end

class AccountRole < ActiveRecord::Base
  acts_as_authorization_role
end

class FooBar < ActiveRecord::Base
  acts_as_authorization_object
end

Note that you'll need to change your database structure appropriately:

create_table "account_roles", :force => true do |t|
  t.string   :name,              :limit => 40
  t.string   :authorizable_type, :limit => 40
  t.integer  :authorizable_id
  t.timestamps null: false
end

create_table "account_roles_accounts", :id => false, :force => true do |t|
  t.references  :account
  t.references  :account_role
end

Examples

user = User.create!
user.has_role? 'admin'              # => false

user.has_role! :admin

user.has_role? :admin               # => true

user now has global role admin. Note that you can specify role name either as a string or as a symbol.

foo = Foo.create!

user.has_role? 'admin', foo         # => false

user.has_role! :manager, foo

user.has_role? :manager, foo        # => true
foo.accepts_role? :manager, user    # => true

user.has_roles_for? foo             # => true

You can see here that global and object roles are distinguished from each other. User with global role admin isn't automatically admin of foo.

However,

user.has_role? :manager             # => true

That is, if you have an object role, it means that you have a global role with the same name too! In other words, you are manager if you manage at least one foo (or a bar...).

bar = Bar.create!

user.has_role! :manager, bar
user.has_no_role! :manager, foo

user.has_role? :manager, foo        # => false
user.has_role? :manager             # => true

Our user is no more manager of foo, but has become a manager of bar.

user.has_no_roles!

user.has_role? :manager             # => false
user.has_role? :admin               # => false
user.role_objects                   # => []

At this time user has no roles in the system.

Coming up with your own role implementation

The described role system with its 2 tables (not counting the users table!) might be an overkill for many cases. If all you want is global roles without any scope, you'd better off implementing it by hand.

The access control subsystem of Acl9 uses only subject.has_role? method, so there's no need to implement anything else except for own convenience.

For example, if each your user can have only one global role, just add role column to your User class:

class User < ActiveRecord::Base
  def has_role?(role_name, obj=nil)
    self.role == role_name
  end

  def has_role!(role_name, obj=nil)
    self.role = role_name
    save!
  end
end

If you need to assign multiple roles to your users, you can use serialize with role array or a special solution like preference_fu.