chaps-io / access-granted

Multi-role and whitelist based authorization gem for Rails (and not only Rails!)
MIT License
773 stars 41 forks source link

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

Open marekdlugos opened 5 years ago

marekdlugos commented 5 years ago

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
jrochkind commented 5 years ago

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

pokonski commented 5 years ago

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

wflanagan commented 4 years ago

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. :-)