chaps-io/access-granted

How to setup access_policy.rb when roles are not part of a user model?

marekdlugos opened this issue · 3 comments

I'm pretty new to Rails, however, I work on the app where I have this trio: users, wikis, wiki_users. Many users can be assigned to many wikis and vice versa.

I struggle with connecting my access_policy.rb file with Rails. How could I let the gem know that it should look for user, assigned to a specific wiki with a specific role on that specific wiki?

# wiki_user.rb
  belongs_to :user
  belongs_to :wiki

  enum role: { owner: 1, administrator: 2, moderator: 3, contributor: 4, reader: 5 }
# user.rb
...
  has_many :wiki_users, dependent: :destroy
  has_many :wikis, through: :wiki_users
...
  def wiki_user_role(role)
    wiki = Wiki.find_by_subdomain request.subdomain
    wiki.users.include?(self) && wiki.wiki_users.find_by_user_id(self.id).role == role
  end
# project.rb
...
  has_many :wiki_users, dependent: :destroy
  has_many :users, through: :wiki_users
...

My project_users table contains columns id (of the relationship), user_id, project_id, and role. My access_policy.rb file looks like this so far, with some effort to make it work in role :reader block. However, it all feels just like a workaround and I am wondering whether this scenario can't be handeled in easier manner?

  def configure

    # ROLES
    # Owner — a person who handles payments, e.g. owner of the company
    # Administrator — administrator who does not care ab payments only ab the wiki itself
    # Moderator — can create and edit other people articles
    # Contributor — can only create and edit his own articles
    # Reader (guest) — only the permission to read

    role :owner, proc { |user| user.present? } do
      can [:edit, :update, :destroy], Article
      can [:edit, :update, :destroy], Comment
      can [:edit, :update, :destroy], User
      can [:edit, :update, :destroy], Wiki

    end

    role :administrator, proc { |user| user.present? } do
      can [:edit, :update, :destroy], Article
      can [:edit, :update, :destroy], Comment
      can [:edit, :update, :destroy], User

    end

    role :moderator, proc { |user| user.present? } do
      can [:edit, :update, :destroy], Article
      can [:edit, :update, :destroy], Comment

    end

    role :contributor, proc { |user| user.present? } do
      can [:edit, :create, :destroy], Article do |article, user|
        article.user_id == user.id
      end

      can [:edit, :create, :destroy], Comment do |comment, user|
        comment.user_id == user.id
      end
    end

    role :reader, proc { |user| user.present? && user.wiki_user_role("reader") } do
      can :read, Article do |article, user|
        article.wiki.users.include? user
      end

      can :read, User do |selected_user_and_wiki, user|
        selected_user = selected_user_and_wiki.first
        wiki = selected_user_and_wiki.second

        wiki.users.include?(selected_user) && wiki.users.include?(user)
      end

      can [:edit, :destroy], User do |edited_user, user|
        edited_user == user
      end
    end

  end

Would it help to just use the wiki_user_role(role) method you defined on User, in your policies?

Like @jrochkind said, you should check for specific role inside can block, because those are no longer static roles

Many to many relationships with this are tough, and by default was extremely non-DRY.

In my case, my User model has rights to perform actions on other users, projects, etc. (about 12 different classes), with a bunch of conditional cases. So, extracting it to a clean pattern was challenging.

So, what i did was create a Concern that I included into the AccessGranted::Role a class that helps me make things easier to query. It gives me nice methods for own_record?(user) etc.

I thought about forking and letting you pass a helper method, but that seemed like overkill. It seems to be working now, but I thought I'd share what I did in case future version want to make it all easier.

Here's the important part/example of the mixin. No point in pasting 100s of lines.

module PolicyBase
  extend ActiveSupport::Concern

  def user_manager
    @user_manager ||= ::UserAccess.new(user: user)
  end

  def admin_role?(role)
    admin_roles.include?(role)
  end

  def admin_roles
    %w[admin owner]
  end

# SNIP about 200 lines of code here
end

My UserAccess class has a single method that wraps other "relational check" methods using a case statement to find the right way to extract the relational roles, so I can make the language in the "can" statement super simple.

# This is a method in UserAccess
 # Checks the role of the current_user for another object
  # Returns a nil if the role_for cannot be found
  def role_for(klass_instance)
    case klass_instance
    when Company
      company_role_for(klass_instance)
    when Project
      project_role_for(klass_instance)
    when Team
      team_role_for(klass_instance)
    when Accessor
      project_role_for(klass_instance.project)
    when Relationship
      company_role_for(klass_instance.company)
    when User
      user_role_for(klass_instance)
    else
      nil
    end

Then, I mixed the PolicyBase into the class, as I said.. and then added it to the top of the policy file.

class AccessGranted::Role
  include PolicyBase
end

class UserRights
  include AccessGranted::Policy

  def configure
    role :admin, proc { |user| user.admin? } do
      can :manage, Accessor        # Access to a project directly
      can :manage, Company         # owner or rights
      can :manage, Invite          # their invites, only for ones that they have rights to
      can :manage, Project         # can we manage this projects?
      can :manage, Relationship    # Access in a company
      can :manage, User            # the user themself
     # SNIP and others
    end

    role :manager, proc { |user| user.manager? } do
      can :manage, Accessor do |accessor, user|
        own_accessor?(accessor) || manageable_role?(user_manager.role_for(accessor))
      end
    end
  # SNIP a bunch of lines here 
end 

So you can see that the can statement super clean, and readable/manageable by a simple label (reader, user, manager, admin, owner). So, doing all this DRY'd up the thing pretty nicely.

It's a great gem and does really simplify doing this by hand. Thanks for the work! Please let me know if there will be major unintended consequences to what I did. :-)