I have a user model and lots of other models in my project, to create a RBAC system I implemented role and permission. User has_and_belongs_to_many roles and Role has_and_belongs_to_many permissions.
class Permission
include Mongoid::Document
field :ability, type: String
has_and_belongs_to_many :roles
belongs_to :permission_for, polymorphic: true, dependent: :destroy
index({ability: 1,permission_for_id: 1},unique: true)
end
class Role
include Mongoid::Document
field :name, type: String
has_and_belongs_to_many :permissions
has_and_belongs_to_many :users
belongs_to :role_for, polymorphic: true
index({name: 1,role_for_id: 1},unique: true)
end
and in User model I have :
Class User
include Mongoid::Document
.
.
.
def able?(scope,model,action)
# There must be something to load and check permissions
end
end
Role defined in a scope (role_for) and Permission defined in Models in Role's scope (project is scope and task is model in that scope) with permission_for.
In User model I need to get data from database and check if user is able to do that action, in large amount of data it take too long. able? function I've implemented is simple, it just load every user's role and every role's permission and then check if permission's ability is equal to action and then return true or false!
Is there any gem or code do something like that? and if there's not, could you give me advise on how to implement role and permission in such way, with much less database load?
Many many tahnks
Edit 1
Ok, I've created this relation and managed to use it in my models, performance in normal use is ok, but when I want to get huge amount data it's very slow. I need to cache roles and permissions in scope model, how can I do such thing? Is there any plugin for rails can do that for me?
Our product is near soft-launch and I implemented that solution but with minor tweaks :
How I've done RBAC :
Project <--- Role <---> Permissions ---> Task
^ (ability)
|
|
V
User (able?)
This schema is very simplified version of final implementation but the concept is same
User.able?(model,ability) returns true if union of all permissions of user's related roles witch are related to model has a permission with ability.
permission >> View Edit Delete
V Role --------------------------------
1 true false false
2 true true false
3 false true false
--------------------------------
result true true false
I case of user has role 1,2,3 then user can view,edit but can't delete
To solve Performance Issue and Database Hit used russian doll caching, for each role I cache Hash representation of permissions :
role.get_permissions
# returns {modelID => ['view'], model2ID => ['view','edit']}
And then merge this all this hashes for user and again cache that new hash.
In each call of able? method of User class I get that hash (from cache or if changed generate new from database) and Job Done :)
Our worst problem about this caching was cache expiry. So we decided to add new functionality to our ORM (MongoID)
Adding or removing permissions from role will update an attribute in role model (without updating it's timestamps)
For role-user on add/remove/edit role we do so and also for project-role relation.
But for task-permission we've done nothing, because permissions will never change (ability and ID is important).
For role-permission relation update won't trigger update_permission on role.
Hope this help for anybody reach this point.
Related
So I'll go straight to the point.
I'm using the ActiveAdmin gem, so I have the AdminUser model in my app... now I got a requirement from my client where a "super admin" must be able to control the permissions of other administrators.
So, for example, if I have the resources: Message, Client and Country, the "super admin" should be able to assign to an AdminUser the task of managing messages, to another one the task of managing clients and to another one the task to managing countries.
For this I was thinking about adding several boolean attributes to the admin_users table. For example, a boolean attribute called "super_admin" would be used to determine if this AdminUser can change the permissions of other AdminUsers, another attribute called message would be used to determine if this AdminUser has control (can edit, read, delete, etc.) over the messages, another attribute called country would be used to determine if this AdminUser has control (can edit, read, delete, etc.) over the countries and so on...
What's the problem? I can't access to current_admin_user in models, so I can't do something like this:
ActiveAdmin.register Message do
if (current_admin_user.message)
permit_params Commune.attribute_names.map(&:to_sym)
end
end
So what can I do? I must build this functionality!
edit
I found this gem that adds roles to active admin https://github.com/yhirano55/active_admin_role
why it needs to be in model ? Code looks like it should be placed in controller, permit_params...
I would use pundit. I can see can can was updated 5 years ago. They are similiar.
pundit repo: https://github.com/varvet/pundit
It uses policies, so you create policy for every model.
class PostPolicy < ApplicationPolicy
def update?
user.admin? or not record.published?
end
end
where you can check your flags on update or create or show or anything...
In action you use something like this authorize #post, :update?
Quote from their doc
In this case, you can imagine that authorize would have done something like this:
unless PostPolicy.new(current_user, #post).update?
raise Pundit::NotAuthorizedError, "not allowed to update? this #{#post.inspect}"
end
Hope it will help
P.S It you need more complex solution. I would create Role model, where I could specify model, read, write permissions. I would link it with my user to has_many roles, and in my policy do something like this:
class PostPolicy < ApplicationPolicy
def get_role
user.roles.where(model: "Post").first
end
def update?
user.get_role.write? or not record.published?
end
end
Or maybe there is better way to use it somehow in policy model...
Has anyone tried to add user role switching using the authlogic/declarative authorisation gems?
My requirement is that a user can have many roles (e.g. Author, Teacher) and can switch role within the application.
I have a two ideas of how I might approach this:
Adding another boolean attribute (active), to the user_roles join
table
Copying the switched role_id into the users table and working
off that
I read the declarative authorisation readme and can't see anything that appears to be built in. Any ideas would be appreciated
Just looked back into this today and the solution is easy enough. I went with adding a boolean attribute to my many-to-many user_roles join to avoid duplication. The join now has the following attributes:
id | user_id | role_id | active
The role_symbols method in my user model, which is used to hook in the authorization_rules.rb DSL now looks like:
def role_symbols
user_roles.where(:active => true).map do |user_role|
user_role.role.name.underscore.to_sym
end
end
Now a users role sets itself to which ever role has active true in the user_roles table.
User switching is easy too (from the user model)
def self.set_active_role(user_id, role_id)
UserRole.where(:user_id => user_id).update_all(:active => false)
activate_role = UserRole.where(:user_id => user_id, :role_id => role_id).first
activate_role.update_attributes(:active => true)
end
Thought it might help someone in the future
User has_many constructusers, the latter being a join table for a has_many :through relationship to Construct. For the application purposes, the boolean roles are defined in the join table (constructusers.manager, constructusers.operator, etc.), while admin is a user attribute.
So when it comes time to define the policies on the actions the following throws a no method error for 'manager', while a relationship is recognised ActiveRecord::Relation:0x000001035c4370
def show?
user.admin? or user.constructusers.manager?
end
if the relationship (I assume the proper one) is correct, why is there no recognition of the boolean attribute?
As per comment below, for the simple reason that is plural. Thus filtering requires:
Constructuser.where(['construct_id = ? and user_id = ?', params[:id], current_user]).first
...which is running in the controller and impacts the view. Nonetheless, for proper Pundit handling, this needs to be factored out... still de application_controller in a before filter to set that attribute. However a before_filter :set_constructuser_manager with that find condition, with nil case handling, still has no impact when stating the policy
def show?
set_constructuser_manager?
end
Update: as per comment below. Pundit class private method
def contractorconstruct
#contructs = Construct.where(['constructusers.user_id = ?', user]).joins(:users).all
#contractorconstruct ||= Contractor.where(['construct_id IN (?)', #contructs]).first
end
and action rule
|| contractorconstruct?
returns no method error.
manager? will be a method on an instance of Constructuser, not on the relation. Think about what you are asking, "Is this constructusers a manager?" - it makes no sense. How would the computer know what constructuser you are talking about?
If a user has_many constructusers, in order to use manager? you need to find the instance you are concerned about. If this is in the ConstructPolicy, then you need to find the specific constructuser that links user to the construct that you are authorizing, then check if that single constructuser is manager?.
If you are in the Construct controller, you'll have something like
class ConstructsController
before_action :set_construct
def show
authorize #construct
# ...
end
# ...
end
In your policy then, user will be the current user and record will be #construct.
class ConstructPolicy
def show?
user.admin? || constructuser.manage?
end
private
def constructuser
#constructuser ||= Constructuser.find_by(user_id: user, construct_id: record)
end
end
In my omniauth & omniauth-identity based app I came across the follwoing issue:
There are three tables that manage Userdata:
# Stores User data _not_ authentication data
class User
include Mongoid::Document
has_many :identities
field :email, type: String
end
# Stores Authentication data (i.e. Facebook)
class Identity
include Mongoid::Document
belongs_to :user
end
# Stores Authentication data for omniauth-identity
class LocalIdentity < Identity
include OmniAuth::Identity::Models::Mongoid
field :email, type: String
end
The issue is now that I have duplicated the email-field in LocalIdentity. So whenever a user changes his email-address in the User model I need to synchronize the email address. This seems trivial at first but can get real messy as soon as there are more fields involved etc.
So my question is: Is there a way to remove the redundance between User and LocalIdentity?
While pondering about a solution i came to the following conclusions:
Multi Inheritance isn't supported so LocalIdentity < Identity, User won't work
Having a LocalIdentity embedded in every User and
write-trough the values won't work b/c validations wouldn't work
I am trying to figure out how to redirect users on certain URL based on their role, after they log to the Ruby/Rails3 application.
So far, I have used authlogic gem for authentification and cancan gem for role setting.
Roles are just like this (defined in app/models/user.rb):
class User < ActiveRecord::Base
acts_as_authentic
ROLES = %w[admin customer demo]
end
Now there is app/controllers/user_session_controller.rb which is taking care of logins.
I would like to make something like this:
for r in User.role
if r == "admin"
redirect_to admins_url
else
redirect_to users_url
end
end
This is not working because of the following error:
"undefined method `role' for #<Class:0xb5bb6e88>"
Is there a simple or elegant way how to redirect users to the certain URLs according to their roles?
(Roles are defined in mysql column 'role' in the users table.)
The for r in User.role is confusing. Are you trying to access the array of ROLES defined on the class or are you trying to access the role value of the current user?
If you are trying to access the array of ROLES, then use User::ROLES.
Using authlogic, one typically defines the current_user in the application_controller. So the role of the current user can be found using current_user.role
So your code could look something like
if current_user.role == "admin"
redirect_to admins_url
else
redirect_to users_url
end
You should definitely check out CanCan. It is a pretty logical way to manage user roles and abilities.