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