Rolify scope roles to many objects and different classes Rails 6 - ruby-on-rails

I am trying to "extend" Rolify functionality to have some global roles such as 'Admin', 'Member', 'Guest', etc... and to be able to set up different "scopes" for each user who have a specific role.
For example, in my app i have this admin role, which is a "super role" meaning it grants access to basically everything. But i also want to be able to "scope" this role for another User, the scope will be, for example 'he will have access to all users, but only if they are from countries A, B, C and from cities X, Y, Z'. I know rolify supports different roles with different scopes, but what i want is to manage "global roles" with different scopes only for different users.
I thought about doing something like a 'Scope' model that belongs to a Role and to a User, in which i would have HABTM relationships with countries and cities, and then use that for authorization (I'm using CanCanCan). But i ran into many issues when working on this approach. It was something like:
class Scope
belongs_to :user
belongs_to :role
has_and_belongs_to_many :countries
has_and_belongs_to_many :cities
end
One of the issues i ran into was that i need to grant the role at the same time i create a scope, and if a user is 'revoked' of a role, the scope which belongs to the user and the role, needs to be destroyed. This last part i found particularly hard since 'Scope' is not related to 'users_roles' table.
Anyone has any idea on a better approach to this problem? I'm having a hard time figuring out the right way to have a role that has custom scopes for each user (basically i need something in the middle of the user and the role to define what is the user's scope with that role).
Appreciate any help I can get!

If you want to create something of your own has_and_belongs_to_many is not the answer (hint: it's almost never the right answer). Using HABTM is the akilles heel of Rolify as its assocations look like this:
class User
has_and_belongs_to_many :roles
end
class Role
has_and_belongs_to_many :users
belongs_to :resource,
polymorphic: true,
optional: true
end
This doesn't let you you query the users_roles table directly or add additional columns or logic. Fixing it has been an open issue since 2013. There are workarounds but Rolify may not be the right tool for the job here anyways.
If you want to roll your own you want to use has_many through: to setup an actual join model so you can query the join table directly and add assocations, additional columns and logic to it.
class User
has_many :user_roles
has_many :roles, through: :user_roles
end
class UserRole
belongs_to :user
belongs_to :role
belongs_to :resource,
polymorphic: true,
optional: true
validates_uniqueness_of :user_id,
scope: [:role_id, :resource_id, :resource_type]
end
class Role
validates :name, presence: true,
uniqueness: true
has_many :user_roles
has_many :roles, through: :user_roles
end
This moves the resource scoping from being per role to being per user.
While you could add additional join tables between the user_roles table and the "scoped" resources its not strictly necissary unless you want to avoid polymorphic asssocations.

Related

Rolify make association to a user with a specific role

I want to make an association where a user has many deals and a deal belongs to a user as well as another with a role ('associate').
I am using the rolify gem to do this.
Like this:
# user.rb
has_many :deals
# deal.rb
belongs_to :user
belongs_to :associate # User with role 'associate or admin'
The first belongs to could be whatever user, doesn't matter if the user is any role it should still work, the second belongs to however should most definitely be an associate or an admin
Do you think I should use rolify for this? or should I not and just make a different model for each role?
Update
My current solution won't work because the user as an associate would need two associations, the has many deals and the has_many :client_deals. I'm not sure in the naming still.
Update 2
Max's solution works great!
This is not where you want to use Rolify's tables. Rolify creates a one-to-one assocation between roles and resources through the roles tables. Roles then have a many-to-many assocation through the users_roles table to users.
Which means it works great for cases where the association is one-to-many or many-to-many but Rolify really can't guarantee that there will ever only be one user with a particular role due to the lack of database constraints.
Even if you add validations or other application level constraints that still leaves the potential for race conditions that could be a double click away.
Instead you want to just create separate one-to-one assocations:
class Deal < ApplicationRecord
belongs_to :creator,
class_name: 'User',
inverse_of: :created_deals
belongs_to :associate,
class_name: 'User',
inverse_of: :deals_as_associate
validates :must_have_associate_role!
private
def must_have_associate_role!
# this could just as well be two separate roles...
errors.add(:associate, '^ user must be an associate!') unless associate.has_role?(:associate)
end
end
class User < ApplicationRecord
has_many :created_deals,
class_name: 'Deal'
foreign_key: :creator_id,
inverse_of: :creator
has_many :deals_as_associate,
class_name: 'Deal'
foreign_key: :associate_id,
inverse_of: :associate
end
Two models can really have an unlimited number of associations between them as long as the name of each assocation is unique and you configure it correctly with the class_name and foreign_key options.
Since this uses a single foreign key this means that ere can ever only be one and you're safeguarded against race conditions.
For associate you can use the following
belongs_to :associate, -> { includes(:roles).where(roles: {name: ['associate', 'admin'] }) }, class_name: 'User', foreign_key: 'user_id'

Inheriting an association via another model in Rails 5

I have a pretty standard User/Role setup going on (a user HABTM roles, a role HABTM users). I'm using CanCanCan for authorisation, and the role you have defines what you can do around the application. This part is all working fine, but now I want to be able to have users inherit roles as part of having a subscription to different memberships.
Here are the models concerned:
class User < ApplicationRecord
has_and_belongs_to_many :roles
has_one :membership_subscription
has_one :membership, through: :membership_subscription
end
class Role < ApplicationRecord
has_and_belongs_to_many :users
end
class MembershipSubscription < ApplicationRecord
belongs_to :user
belongs_to :membership
end
class Membership < ApplicationRecord
has_many :membership_subscriptions
has_many :users, through: :membership_subscriptions
end
I was thinking that I might be able to just add a has_many: roles association to the Membership, and then say that the user has_many roles through their subscription to the Membership, as well as the current HABTM association that allows roles to be assigned directly.
This way you can directly attach roles to users like you can now (as some roles are additive, and not related to the membership subscription/type at all) but also users will automatically inherit roles (and lose them again) as their membership subs come and go.
Does that make sense? I guess the other option would be to use callbacks in the model to deal with creating/deleting role associations but it doesn't seem as elegant.
Any advice greatly appreciated!
Okay so I think this is a valid answer:
First, update the models so that there is an associations between the Memberships and the Roles:
class Role < ApplicationRecord
has_and_belongs_to_many :users
has_and_belongs_to_many :memberships
end
class Membership < ApplicationRecord
has_many :membership_subscriptions
has_many :users, through: :membership_subscriptions
has_and_belongs_to_many :roles
end
Next, create a method in the user model that can be used to retrieve both directly assigned roles and inherited roles:
def combined_roles
if self.membership == nil
self.roles
else
self.roles + self.membership.roles
end
end
Then wherever you need to check a role, call that method instead of the usual user.roles
Not sure if that's a naive way of doing things, but seems to work okay. Comments still welcome if there's a better way
EDIT:
This allows a user to have the same role multiple times - it can be assigned directly or inherited. Modify the combined_roles method like so so that it strips out duplicates:
def combined_roles
if self.membership == nil
self.roles
else
(self.roles + self.membership.roles).uniq
end
end

Uncertain about model association

I'm working on a project that allows a user to create a company. If a user creates a company, the user will be the admin.
However I would also like that user to then be able to invite users to sign up. So that all the users will belong to that company.
So my question is that the company would technically I guess belong_to the admin. However the company also has many users.
What would be the right association setup for this?
You can have both a belongs_to and has_many relation to User from your Company model at the same time. Set an alias to separate them like this:
# app/models/company.rb
belongs_to :admin, :class_name => 'User'
has_many :users
Now you can access the admin user through #company.admin and the users through #company.users.
class Company
belongs_to :admin, :class_name => 'User', foreign_key: :admin_id
has_many :users
end
This should work. When creating the Company model, give
admin_id = #current_user.id
#current_user provided you are using Devise for authentication management.
Cheers,
Akhil

Rails "Sometimes Has One" Relationships

This is probably a newbie question, but I can't seem to think of a good solution. I have a company table that has_many users through groups that can also be administrators of the company (enabling them to edit the company but only one company per user).
What's the best way to set this up in Rails?
I can't add an admin field to the user table, because it wouldn't discriminate which company he/she is administrating. But if I do a company_id field, what would that relationship look like in Rails (since it's a sort of somtimes_has_one relationship!). I could leave it without a relationship, but that doesn't seem proper...
Thanks in advance for any help!
From what I understand, you have a user which might belong to a company, and if it does, it might actually administer it.
You could setup Group to have for example, company_id, user_id and an admin field (this way you get to know which users belong to which company, and if they also administrate that company)
For a user to belong to just one company you could add a validation for uniqueness per two columns (company_id and user_id)
You could get one company's administrators by doing
class Company < ActiveRecord::Base
has_many :groups
has_many :users, through: :groups
has_many :administrators, through: :groups, source: :user, conditions: ["groups.admin = ?", true]
end
and call company.administrators or company.users for all users
You could also do something like
class User < ActiveRecord::Base
has_one :group
has_one :company, through: :group
has_one :administered_company, through: :group, source: :company, conditions: ["groups.admin = ?", true]
end
so you can call user.company or user.administered_company
and so on...

Is there a way to have three way habtm associations in rails / activerecord?

Often three (or more) way associations are needed for habtm associations. For instance a permission model with roles.
A particular area of functionality has many users which can access many areas.
The permissions on the area are setup via roles (habtm)
The user/roles association is also habtm
The permissions (read, write, delete, etc) are habtm towards roles.
How would that be best done with rails/activerecord?
I'm not sure if you are just using Role based user permissions as an example, or if that is actually your goal.
Nested habtm relationships are easy in Rails, though I would highly recommend Nested has_many :through, just set them up as you would imagine:
class Permission < ActiveRecord::Base
end
#this table must have permission_id and role_id
class PermissionAssignment < ActiveRecord::Base
belongs_to :permission
belongs_to :role
end
class Role < ActiveRecord::Base
has_many :users, :through => :role_assignments
has_many :permissions, :through => :permission_assignments
end
#this table must have user_id and role_id
class RoleAssignment < ActiveRecord::Base
belongs_to :role
belongs_to :user
end
class User < ActiveRecord::Base
has_many :roles, :through => :role_assignments
end
This question about rails and RBAC (role-based authentication control) has a bunch of useful samples that provide the answer to this question and also sample implementations of your example.

Resources