Search code examples
ruby-on-railsrolify

Rolify scope roles to many objects and different classes Rails 6


I am trying to "extend" Rolify functionality to have some global roles such as 'Admin', 'Member', 'Guest', etc... and to be able to set up different "scopes" for each user who have a specific role.

For example, in my app i have this admin role, which is a "super role" meaning it grants access to basically everything. But i also want to be able to "scope" this role for another User, the scope will be, for example 'he will have access to all users, but only if they are from countries A, B, C and from cities X, Y, Z'. I know rolify supports different roles with different scopes, but what i want is to manage "global roles" with different scopes only for different users.

I thought about doing something like a 'Scope' model that belongs to a Role and to a User, in which i would have HABTM relationships with countries and cities, and then use that for authorization (I'm using CanCanCan). But i ran into many issues when working on this approach. It was something like:

class Scope
  belongs_to :user
  belongs_to :role

  has_and_belongs_to_many :countries
  has_and_belongs_to_many :cities
end

One of the issues i ran into was that i need to grant the role at the same time i create a scope, and if a user is 'revoked' of a role, the scope which belongs to the user and the role, needs to be destroyed. This last part i found particularly hard since 'Scope' is not related to 'users_roles' table.

Anyone has any idea on a better approach to this problem? I'm having a hard time figuring out the right way to have a role that has custom scopes for each user (basically i need something in the middle of the user and the role to define what is the user's scope with that role).

Appreciate any help I can get!


Solution

  • 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.