I know how to create an admin role/user : https://github.com/plataformatec/devise/wiki/How-To:-Add-an-Admin-role
What I am wondering though is if there are any advantages or disadvantages to the two options to consider when deciding between them. Can anyone supply any insight on this please?
Let me muddle the water a bit. I prefer to this via a Role table and a join table UserRole. This way I can define more than one role without adding another column/table to db.
class User
has_many :user_roles
has_many :roles, :through => :user_roles
def role?(role)
role_names.include? role
end
def role_names
#role_names ||= self.roles.map(&:name)
end
def role=(role)
self.roles << Role.find_or_create_by_name(role)
end
end
class UserRole
# integer: user_id, integer: role_id
belongs_to :user
belongs_to :role
end
class Role
# string: name
has_many :user_roles
has_many :users, :through => :user_roles
end
It really depends on what you wish to do with your admin role. The first option, I would say is a bit secure as the admin role is a unique model in itself.
The second option is straightforward and would help you get going with the least effort. However, if your users figure out the boolean variable and a way to set it, any user can become an admin and access areas you don't want them to.
Related
I am trying to achieve role-based authorization in Rails.
What we require:
Roles should be dynamic, we should able to create, edit, or delete roles.
Permissions also should be dynamic.
Findings:
We can't use the pundit gem because its policies are static and we can't make it dynamic.
We can use the cancan gem and we can use it dynamically but I didn't get how it can be done? And how it works with `database?
It's my first project on the authorization part. We have rails as the back-end and vue.js as the front end. Whatever roles are there, on the database all data should be empty at first. We'll use the seed to create a super-admin role and give all permissions. Super-admin will create roles, edit roles, destroy roles, and also add permissions, edit and destroy permissions eventually.
If there is any other helpful method then please let me know.
Thanks.
Pundit vs CanCanCan
Your conclusions about CanCanCan and Pundit are just nonsense. Neither of them are "static" or "dynamic" and they have pretty much the same features. The architecture and design philosophy are radically different though.
CanCanCan (originally CanCan) is written as a DSL which was the hottest thing since pre-sliced bread back when Ryan Bates created CanCan 10 years ago. It scales down really well and is easy to learn but gets really ugly as soon as you reach any level of complexity. If anything doing "dynamic authorization" in CanCanCan is going to be a nightmare due its architecture. The ability class in CanCanCan is the god of all god objects.
Pundit is just Object Oriented Programming. In pundit your policies are just classes that take a user and resource as initializer arguments and respond to methods like show?, create? etc. Pundit is harder to understand initially but since its just OOP you can tailor it however you want. And since your authentication logic is stored in separate objects it scales up to complexity far better and adheres to the SOLID principles.
How do I setup a dynamic roles system?
This is you standard role system ala Rolify:
class User < ApplicationRecord
has_many :user_roles
has_many :roles, through: :user_roles
def has_role?(role, resource = nil)
roles.where({ name: role, resource: resource }.compact).exists?
end
def add_role(role, resource = nil)
role = Role.find_or_create_by!({ name: role, resource: resource }.compact)
roles << role
end
end
# rails g model user_roles user:belongs_to role:belongs_to
class UserRole < ApplicationRecord
belongs_to :user
belongs_to :role
end
# rails g model role name:string resource:belongs_to:polymorphic
class Role < ApplicationRecord
belongs_to :resource, polymorphic: true, optional: true
has_many :user_roles
has_many :users, through: :user_roles
end
You can then scope roles to resources:
class Forum < ApplicationRecord
has_many :roles, as: :resource
end
Rolify lets you go a step further and just defines roles with a class as the resource. Like for example user.add_role(:admin, Forum) which makes the user an admin on all forums.
How do I create a permissions system?
A simple RBAC system could be built as:
class Role < ApplicationRecord
has_many :role_permissions
has_many :permissions, through: :role_permissions
def has_permission?(permission)
permissions.where(name: permission).exists?
end
end
# rails g model permission name:string
class Permission < ApplicationRecord
end
# rails g model role_permission role:belongs_to permission:belongs_to
class RolePermission < ApplicationRecord
belongs_to :role
belongs_to :permission
end
So for example you could grant "destroy" to "moderators" on Forum.find(1) by:
role = Role.find_by!(name: 'moderator', resource: Forum.find(1))
role.permissions.create!(name: 'destroy')
role.has_permission?('destroy') # true
Although I doubt its really going to be this simple in reality.
If I understand your requirements correctly, you should be able to use Pundit to achieve this.
From what I understand,
Users have roles
Users can be assigned and unassigned roles at runtime
Permissions are given to either roles or users directly.
Permissions can be updated at runtime
So you can have something like,
class User
has_many :user_role_mappings
has_many :roles, through: :user_role_mappings
has_many :permissions, through: :roles
...
end
class UserRoleMapping
belongs_to :user
belongs_to :role
end
class Role
has_many :role_permission_mappings
has_many :permissions, through :role_permission_mappings
...
def has_permission?(permission)
permissions.where(name: permission).exists?
end
end
class RolePermissionMapping
belongs_to :role
belongs_to :permission
end
class Permission
...
end
And in your policy, you can check if any of the user's roles has the required permission.
class PostPolicy < ApplicationPolicy
def update?
user.roles.any? { |role| role.has_permission?('update_post') }
end
end
Edit:
Using the mapping tables, you can update the permissions for a role, and the roles for a user from an admin dashboard.
You can achieve dynamic roles with pundit. pundit let's you define plain Ruby objects with methods that are called to determine if a user has permission to perform an action. For example:
class PostPolicy < ApplicationPolicy
def update?
user.has_role('admin')
end
end
If you want a user to have multiple roles you could set up a has_and_belongs_to_many: :roles association on your User model.
See: https://guides.rubyonrails.org/association_basics.html#the-has-and-belongs-to-many-association
I am using CanCan and have been researching how to get started. However, it seems that most of the tutorials aren't very specific, and don't suit my own needs. I'm building a social network where users can create projects and add other users to their projects, allowing those users to moderate that project.
I currently have a Role model with a string attribute, and a User model from devise. Where do I go from here?
I have seen this post, but it doesn't fully explain how to set up the roles and the relationship between the Role model and the ability.rb file from CanCan.
If you need me to be more specific, please say so! I'm not the greatest Rails developer ;)
Edit
I have seen the railscast on this, and it doesn't have a separate Role model which I would like to have. I have tried using Rolify, but people have said it is too complicated and that it's possible to do it in a more simple way. I also ran into some complications so I'd like to just use my own Role model.
Edit
I'm currently using rolify and the roles are working. I found my solution at: https://github.com/EppO/rolify/wiki/Tutorial
If your User-Role stuff is looking similar to following:
class User < ActiveRecord::Base
has_many :user_roles
has_many :roles, :through => :user_roles
# user model has for example following attributes:
# username, email, password, ...
end
class Role < ActiveRecord::Base
has_many :user_roles
has_many :users, :through => :user_roles
# role model has for example following attributes:
# name (e.g. Role.first.name => "admin" or "editor" or "whatever"
end
class UserRole < ActiveRecord::Base
belongs_to :user
belongs_to :role
end
you can do following:
First, extend your User model with a few helper methods or something similar:
class User < ActiveRecord::Base
def is_admin?
is_type?("admin")
end
def is_editor?
is_type?("editor")
end
def is_whatever?
is_type?("whatever")
end
private
def is_type? type
self.roles.map(&:name).include?(type) ? true : false # will return true if the param type is included in the user´s role´s names.
end
end
Second, extend your ability class:
class Ability
include CanCan::Ability
def initialize(user)
if user
can :manage, :all if user.is_admin?
can :create, Project if user.is_editor?
can :read, Project if user.is_whatever?
# .. and so on..
# you can work with your different roles on base of the given user instance.
end
end
end
Alternatively you could remove your User-Roles has-many-through associations and replace it with the easy-roles gem - very useful. It is available on github: https://github.com/platform45/easy_roles
Now you should have an idea how you could work with cancan, roles and all the stuff together :-).
I'm trying to apply a rule to an ability class
Each user in my application (except admins, but we'll ignore that) belongs_to a practice, defined in another model.
The practice may want to be suspended from access for one reason or another.
I want to say in my ability model
if user.practice.suspended?
can :read, Client, :practice_id => user.practice_id
else
can :manage, CLient, :practice_id => user.practice_id
etc....
But for some reason, whilst I can use
user.practice_id
in the ability model
I can't use
user.practice.<attribute>
Any ideas for a way around that?
models/user.rb
belongs_to :practice
models/practice.rb
A practice has many users.
The only user who won't have a practice are those with role :admin
has_many :users
I think you have the relationships backwards.
It should be that "User has_many (or has_one) practice" and "Practice belongs_to user". This would signify that the foreign_key (in this case user_id) is actually in the practice table instead of the user table.
Try this:
User.rb
has_one :practice
Practice.rb
belongs_to :user
Create a migration to add user_id to practice
add_column :practices, :user_id, :integer
Then you can call practice from user like so:
user.practice.x
First, thanks for taking the time to read. I'm new to Rails and have been stuck on this one for many hours.
In my Rails 3.2 app, I have three models: User, Organization, and Membership (the last is a join model between User and Organization).
When a user creates an organization, he/she should become a member upon create. So, in my Organization model, I've included a before_create callback that builds a Membership. The problem is that while the Membership builds when the new Organization is created, the user_id on the Membership object is set to "nil.," and therefore the current user is not a member.
Hardcoding in the user_id attribute in the callback actually does correctly build the membership, i.e. (:user_id => "1"), but in general asking the Organization model to be aware of current user state seems like bad MVC practice.
What's the proper way to set the current user ID on the new Membership? It seems like my associations should handle that, but I might be wrong.
Here are my models — I'm leaving out some validation lines for readability's sake. Thanks so much in advance.
user.rb
class User < ActiveRecord::Base
has_many :memberships
has_many :organizations, :through => :memberships
end
membership.rb
class Membership < ActiveRecord::Base
belongs_to :user
belongs_to :organization
end
organization.rb
class Organization < ActiveRecord::Base
has_many :memberships
has_many :users, :through => :memberships
accepts_nested_attributes_for :memberships, :allow_destroy => true
...
before_create :add_membership
protected
def add_membership
self.memberships.build
end
end
You are right in the fact that allowing your model to magically know about the current user is bad MVC practice. So you have to somehow pass the current user id during creation. you can do this in many ways ; for example in the controller :
def create
#organization = Organization.new( params[:organization] ) do |org|
org.memberships.build( user_id: current_user.id )
end
# save, etc.
end
Doing this in the controller is fine, but it would be better if your business logic would reflect the fact that a user creating an organization should automatically belong to it. You could override new and / or create on Organization (or create your own method if you fear overrides) :
def new( params = {}, options = {} )
creator = options.delete( :creator )
super( params, options ) do |org|
org.memberships.build( user_id: creator.id ) if creator
yield org if block_given?
end
end
passing the user is easy now :
def create
#organization = Organization.new(params[:organization], creator: current_user)
end
If you don't like this approach, or if you don't want to override new or create a specific factory method, you can also make something similar to nested_attributes :
attr_accessible :creator_id
def creator_id=( user_id )
memberships.build user_id: user_id
end
then in your view :
f.hidden_field :creator_id, current_user.id
optional :
with first approach, for additional clarity / ease of use, you can also create a method on User :
def new_organization( params = {}, options = {}, &block )
Organization.new( params, options.merge(creator: self), &block )
end
... ok, Organization is hardcoded here (bad !) but your workflow is now quite understandable :
def create
# we know at first glance that the user is responsible for the organization
# creation, and that there must be specific logic associated to this
#organization = current_user.new_organization( params[:organization] )
# etc
end
with a little more thinking, it should be possible to avoid hardcoding Organization into User (using an association extension for instance)
EDIT
To be able to setup a validation on membership's organization presence, you need to do this :
class Organization < ActiveRecord::Base
has_many :memberships, inverse_of: :organization
end
class Membership < ActiveRecord::Base
belongs_to :organization, inverse_of: :memberships
validates :organization, presence: true
end
Let's explain this :
inverse_of sets up your associations to be bidirectional. By default, associations are one-way, which means that when you do organization.memberships.first.organization, rails tries to load the organisation again because it does not know how to "climb back" the association. When using inverse_of, rails knows it does not have to reload the organization.
validates MUST be setup on organization and NOT on organization_id. This way the validator knows we're "climbing back" the association, it knows that organization is a "parent" record and that it's in the process of being saved - so it does not complain.
I have a number of users who have different roles in relation to a numbers of posts. These roles are owner, editor, viewer, none. Each user may only have one role for a post. I have represented this as a has many through relationship in rails as follows:
class User < ActiveRecord::Base
has_many :roles
has_many :posts, :through => :roles
end
class Post < ActiveRecord::Base
has_many :roles
has_many :users, through => :roles
end
class Role < ActiveRecord::Base
attr_accessor :role
belongs_to :users
belongs_to :posts
end
Where the role attribute is used to indicate which type of role the user has on the post.
When setting a new role I cannot simply use the << operator as it wont set the role attribute. What is the preferred way of handling this situation? How can I enforce that there is only one role per user / post combination and enforce this in my Role creation logic?
You can check in the creation of roles for the User , if he already has a role assigned in which case you can skip assigning this role.
unless user.roles.present?
user.roles.create
end
I understand that you want to make sure that no user will have more than one role for a certain post. If this is what you want to achieve then you just need to add uniquness validation to your Role mode
validates :user_id, uniqueness: {scope: :post_id, message: 'User can have one role per post'}
this will ensure that the combination of user_id and post_id will be unique, you can see more on rails guide on validation with scope.