What Is the Purpose of Rolify

What is the purpose of Rolify?

My answer, garnishing the question from this reddit post:

Authentication is establishing a User is who they claim to be.

Authorization is establishing that a User can perform a given action, be it reading or writing, after they've established their identity.

Roles are just common patterns of authorization across users: this User can be authorized as such, that User can be authorized like this instead.

The ingredient you're missing here is Permissions: a relationship between an established Role and some controller action.

Roles themselves make no promises about what action a User can perform. And remember--authorization is all about actions. Roles generalize what kind of User you're dealing with. They exist to keep you from having to query every User for a giant laundry list of Permissions. They declare: this User is a Role! Of course they have Permission to do that!

There are many types of Permission. You can store them in a database if you want your sufficiently authorized Users to be able to edit them, along with your Roles if those too ought to be configurable. Or, if your User's Roles are sufficiently static, you can manage Permissions in advance with Ruby code:

  • When I want to have configurable Roles and Permissions, i.e. for a client application you're handing off to someone at completion of contract, I implement a User :has_many Roles and a Role :has_many Permissions with my own custom models, and then add a before_filter :authorize hook into my ApplicationController, and write an authorize method on it that knows how to martial these expectations, or render a 403 page for those people who insist upon manually entering urls to things they hope expose actions to things they oughtn't have access to.

  • When I want to just have configurable Roles, I use Ryan Bates' CanCan gem.

  • When I want to have predetermined Roles and Permissions, I use Rolify in conjunction with Nathan Long's Authority, to get delightfully flexible Class-based Permissions via Authorizer classes.

Both Roles and Permissions can be either class-based or instance-based, depending on your use-case. You can, say, with the abilities of rolify you've just discovered, decide that Users may only act as a Role in certain, instance-based circumstances. Or, general Roles of User may only be able to execute an action given the object they are trying to action is of a certain type.

To explore the permutation of these, assuming a blog application, following the formula

a User who is a/an Role class/instance can action a/an/all/any/that (class/instance) Permission:

  • Role class and Permission class:

    A User who is an Admin can delete any Post.

  • Role class and Permission instance:

    A User who is an Admin can edit all Posts that they approved to be published

    This would be easier if published posts had an approved_by field pointing to a User id. (Use a state machine gem for this sort of situation.

  • Role instance and Permission class:

    A User who is an Author of a Post can comment on any Post

    Note that this sort of situation is rare, which is why there are no gems I've mentioned above to handle this situation, except for perhaps the ability to manage predetermined circumstances like Rolify and Authority in conjunction; or, if you must pass this decision on to your client, your own custom solution.

  • Role instance and Permission instance:

    A User who is an Author of a Post can edit that Post.

TL;DR:

  • Rolify is just for roles: grouping Users by Permission: access to a controller action. You have yet to decide how you are going to manage Permissions.

I hope this helps your understanding of Rolify's position in the grand scheme of authentication and authorization!

Defining Roles with Rolify

What is completely baffling me is that all the documents for the Rolify gem use the console to define roles.

Rolify documentation does not use the console to define roles - it demonstrates how roles can be added in pure ruby. This is amazingly powerful because you can
define roles whereever you can run ruby. You are not restricted to a static list of roles defined in some configuration file or in some database table.


The first question you need to ask is when do the roles get created ?

The most common use cases fall into two groups:

1. Roles are static.

Roles are created once by the application developer, support staff or company executives during application installation/deployment and during the lifetime of the running application these pre-created roles are assigned to different users.

The most common use cases of such roles includes modelling designations of a people in a company (developer, manager, support etc.), or modelling priorly known responsibilities (editor, administrator, viewer etc.)

If your roles do fall into such use cases, next thing you have to decide - whose responsibility is it to create and modify the roles. There are typically two possibilities:

1.1. Application developer himself is the person who has to add/remove/modify roles. In such cases it is best to rely on seed data or Rails migrations.

Advantage of migrations is that you can rollback the data easily if need be. This migration is additional to migration generated by rolify generators which create the schema of roles related tables for you (refer to the diagram below).

Such a migration might look like:

db/migrate/20151204083556_create_application_roles.rb

class CreateApplicationRoles < ActiveRecord::Migration
def up
['admin', 'support', 'editor'].each do |role_name|
Role.create! name: role_name
end
end
def down
Role.where(name: ['admin', 'support', 'editor']).destroy_all
end

end

Some people rightly consider it an antipattern to have schema changes and data changes both managed by migrations. data-migrate is a gem that allows you to separate the data centric migrations from your schema migrations.

In this case and all the other cases below actual assignment of roles will happen based on user actions or application events through add_role or remove_role methods provided by rolify.This will happen during the course of the lifecycle of running application and not during application installation.

1.2 The task of adding/removing/modifying roles is done by a support team or technical executives. In such cases it would be required to provide an administrative interface for managing roles.

In this case you will have a rails controller to manage the roles. The create action will be used for creating role, show action will be there to present the role etc. These actions will have accompanying views that will provide a graphical user interface to end user to manage the roles.

2. Roles are dynamic

This category covers use cases where roles are treated more like categories or tags and can be created/modified/deleted by end users. For example a librarian can assign some role/category to a particular genre of books.

This case is similar to 1.2 because you have to handle creating/deleting/updating roles through rails controllers.


Next part is how the information is structured in your tables.

Rolify expects a specific schema (customizable to certain extent) but the expected schema is flexible enough to handle all the above use cases.

Rolify tables

What's the point of Rolify and CanCan?

CanCan is used for managing authorization from the application standpoint is what lets you restrict X controller/action to X user.

When you want to dive into a deeper fine grained of control you use Rolify. Rolify, goes beyond the simple

if user.role == :super_admin
# do something pretty cool stuff
elsif user.role == :admin
# do some more awesome stuff

by allowing you to add roles to resources. Let's say you have a forum application, where you want an user to be able to have a moderator role on the Gaming Board. You would use rolify to by

user = User.find(2)
user.add_role :moderator, Forum.where(type: 'Gaming')

Rolify also let's you do this to a class by using the class itself instead of an instance (in case you want an user to be a moderator of all the boards)

user = User.find(2)
user.add_role :moderator, Forum

After that it lets you easily query the resources/class to find out who was access to what. On top of helping you manage the roles scope.

Rolify and getting a list of User with specific access to a resource

Try this.

has_many :users, through: :roles, class_name: 'User', source: :users

This should only add those that are using the roles model (skipping those on the class). You could also try something more explicit with conditions see this issue/PR:

https://github.com/RolifyCommunity/rolify/pull/181

Rolify scope roles to many objects and different classes Rails 6

If you want to create something of your own has_and_belongs_to_many is not the answer (hint: it's almost never the right answer). Using HABTM is the akilles heel of Rolify as its assocations look like this:

class User
has_and_belongs_to_many :roles
end

class Role
has_and_belongs_to_many :users
belongs_to :resource,
polymorphic: true,
optional: true
end

This doesn't let you you query the users_roles table directly or add additional columns or logic. Fixing it has been an open issue since 2013. There are workarounds but Rolify may not be the right tool for the job here anyways.

If you want to roll your own you want to use has_many through: to setup an actual join model so you can query the join table directly and add assocations, additional columns and logic to it.

class User
has_many :user_roles
has_many :roles, through: :user_roles
end

class UserRole
belongs_to :user
belongs_to :role
belongs_to :resource,
polymorphic: true,
optional: true
validates_uniqueness_of :user_id,
scope: [:role_id, :resource_id, :resource_type]
end

class Role
validates :name, presence: true,
uniqueness: true
has_many :user_roles
has_many :roles, through: :user_roles
end

This moves the resource scoping from being per role to being per user.

While you could add additional join tables between the user_roles table and the "scoped" resources its not strictly necissary unless you want to avoid polymorphic asssocations.

Rolify make association to a user with a specific role

This is not where you want to use Rolify's tables. Rolify creates a one-to-one assocation between roles and resources through the roles tables. Roles then have a many-to-many assocation through the users_roles table to users.

Which means it works great for cases where the association is one-to-many or many-to-many but Rolify really can't guarantee that there will ever only be one user with a particular role due to the lack of database constraints.

Diagram

Even if you add validations or other application level constraints that still leaves the potential for race conditions that could be a double click away.

Instead you want to just create separate one-to-one assocations:

class Deal < ApplicationRecord
belongs_to :creator,
class_name: 'User',
inverse_of: :created_deals
belongs_to :associate,
class_name: 'User',
inverse_of: :deals_as_associate

validates :must_have_associate_role!

private
def must_have_associate_role!
# this could just as well be two separate roles...
errors.add(:associate, '^ user must be an associate!') unless associate.has_role?(:associate)
end
end

class User < ApplicationRecord
has_many :created_deals,
class_name: 'Deal'
foreign_key: :creator_id,
inverse_of: :creator
has_many :deals_as_associate,
class_name: 'Deal'
foreign_key: :associate_id,
inverse_of: :associate
end

Two models can really have an unlimited number of associations between them as long as the name of each assocation is unique and you configure it correctly with the class_name and foreign_key options.

Since this uses a single foreign key this means that ere can ever only be one and you're safeguarded against race conditions.



Related Topics



Leave a reply



Submit