Convinient way to implement user roles in my app - ruby-on-rails

Thanks for reading!
I'm currently working on my new app and searching for the best way to implement next feature
By scenario I need to implement "As a user a have role in the location"
WHAT I HAVE DONE:
Scenario:
When user adds new location to the profile
one of the requred fields is "role". That could be "guest", "manager" or "seller". What's the best way to accomplish his in the model side?
I accomplished this with has_many_through assosiation
CONTROLLER:
def create
#location = Location.new(location_params)
#location.profiles << current_user.profile
#set user role
current_user.profile.profile_location_throughs.where(location_id: location.id).set_role(params[:location][:role])
respond_to do |format|
if #location.save
....
end
end
end
MODELS:
class Profile < ActiveRecord::Base do
has_many :profile_location_throughs
has_many :locations, through: :profile_location_throughs
end
class Location < ActiveRecord::Base do
has_many :profile_location_throughs
has_many :locations, through: :profile_location_throughs
end
class ProfileLocationThrough < ActiveRecord::Base
# with boolean fields: manager, seller, guest
belongs_to :location
belongs_to :profile
def set_role(role)
case role
when "guest"
self.guest = true
when "seller"
self.seller = true
when "manager"
self.manager = true
end
end
end
=====
QUESTION:
Could you suggest more beatiful way to implement his feature?

There are several ways to do role based authorization.
The simplest way is by adding a enum to the users themselves:
class Profile < ApplicationRecord
enum role: [:guest, :seller, :manager]
end
This is pretty limited though as it only allows "global" roles.
If you want resource scoped roles you need a join table.
class Profile < ApplicationRecord
has_many :roles
has_many :locations, through: :roles
def has_role?(role, location = nil)
self.roles.exists?( { name: role, location: location}.compact )
end
def add_role(role, location)
self.roles.create!( { name: role, location: location } )
end
end
class Role < ApplicationRecord
belongs_to :profile
belongs_to :location
end
class Location < ApplicationRecord
has_many :roles
has_many :profiles, through: :roles
end
In this example we are simply using a string for the roles.name column. You could also use an enum if the kinds of roles are limited. If you want to use the same Role model (no pun intended) to scope roles on different kinds of resources you can use a polymorphic belongs_to relationship.
class Role < ApplicationRecord
belongs_to :profile
belongs_to :resource, polymorphic: true
end
class Location < ApplicationRecord
has_many :roles, as: :resource
has_many :profiles, through: :roles
end
class OtherThing < ApplicationRecord
has_many :roles, as: :resource
has_many :profiles, through: :roles
end
Note that roles are just one part of an authentication solution. You would combine this with a authorization lib such as Pundit or CanCanCan which defines the rules about what role gets to do what and enforces those rules.
Rolify - Role management library with resource scoping

Related

How can I implement a has_many :through association two ways?

I have a property model and a user model.
A user with the role of 'admin', which is represented by a column on the users table, can have many properties.
A user with a role of 'guest' can also belong to a property, which gives them access to that property.
How should I do this in Rails?
authorizations table -> user_id, property_id
class Authorization < ActiveRecord::Base
belongs_to :user
belongs_to :property
end
class User < ActiveRecord::Base
has_many :authorizations
has_many :properties, through: :authorizations
end
class Property < ActiveRecord::Base
has_many :authorizations
has_many :users, through: :authorizations
end
then you can do User.find(id).properties
First, you need a has_many :through association between your models User and Property. So, create a new table properties_users with columns user_id and propety_id. And do following changes to the models:
class PropertiesUser < ActiveRecord::Base
belongs_to :user
belongs_to :property
end
class User < ActiveRecord::Base
has_many :properties_users
has_many :properties, through: :properties_users
end
class Property < ActiveRecord::Base
has_many :properties_users
has_many :users, through: :properties_users
end
Now, we need to make sure that a guest user does not have more than one property. For that we can add a validation to model PropertiesUser like below:
class PropertiesUser < ActiveRecord::Base
validate :validate_property_count_for_guest
private
def validate_property_count_for_guest
return unless user && user.guest?
if user.properties.not(id: self.id).count >= 1
self.errors.add(:base, 'guest user cannot have more than one properties')
end
end
end
class User < ActiveRecord::Base
def guest?
# return `true` if user is guest
end
end
Finally, to access a guest user's property, define a dedicated method in model User:
class User < ActiveRecord::Base
def property
# Raise error if `property` is called on non-guest users
raise 'user has multiple properties' unless guest?
properties.first
end
end
Now, you can fetch a guest user's property by running:
user = User.first
user.guest?
=> true
user.property
=> <#Property 1> # A record of Property

The Rails way to validate a web of classes

My app has many interrelationships like:
# Company
has_many :programs
has_many :projects
has_many :users
# Project
has_many :users
has_many :programs
belongs_to :company
# User
belongs_to :project
has_many :programs
belongs_to :company
# Program
belongs_to :project
belongs_to :user
belongs_to :company
Every program must belong to a project and user, BOTH OF WHICH belong to current_user.company.
Approach 1 - controller upon create/update
#program = Program.new(program_params)
#program.company = current_user.company
#allowed_projects = current_user.company.projects
unless #allowed_projects.include? #program.project
raise Exception
end
Approach 2 - model-based validation
before_save :ensure_all_allowed
def ensure_all_allowed
current_user = ???
self.company_id = current_user.company_id
# Then a similar validation to above for self.project_id
end
I feel these are both awkward and not 'the Rails way'.
I assume Approach 2 is the better method because it'll save all this awkward controller code and hold better to the MVC standard.
How can I validate these items correctly?
It's actually somewhat problematic to access current user in a model. It's not impossible, but it requires an around_action that will load the current user in the model class in a thread safe way.
Better would be to assign the current user in the controller
#program.user = current_user
#program.company = #program.user.company
Then do the validation in the model
validate :project_must_be_allowed
def project_must_be_allowed
unless company.projects include project
errors.add(:project, "Project is not valid for company.")
end
end
However, it would be a more normalized setup if you did through relationships
class Company
has_many :users
has_many :projects, through: :users
That way your 'projects' table doesn't need a company_id
You could still do the validation as I described but you'd have to add one method to the model...
def company
user.company
end
or more simply...
delegate :company, to: :user
Since Program has a belongs to relation to both user a and project you can setup some simple validations without worrying about the current_user. This is desirable from a MVC standpoint models should not be aware of the session or the request.
class Program < ActiveRecord::Base
# ...
validates_presence_of :user, :company, :project
# the unless conditions are there avoid the program blowing
# up with nil errors - but the presence validation above covers
# those scenarios
validate :user_must_belong_to_company,
unless: -> { company.nil? || user.nil? }
validate :project_must_belong_to_company,
unless: -> { company.nil? || project.nil? }
def user_must_belong_to_company
unless self.company == self.user.company
errors.add(:user, "must belong to same company as user.")
end
end
def project_must_belong_to_company
unless self.company == self.project.company
errors.add(:company, "must belong to same company as project.")
end
end
end
But I'm thinking that this is just a symtom of some bad relation design choices.
What you probably need is a series of many to many relations - it does not seem very realistic that a project can only have one user or a program either for that part.
class Company
has_many :users
has_many :projects
has_many :assignments, through :projects
has_many :programs, through :projects
end
class User
belongs_to :company
has_many :projects, through: :assignments
end
class Project
has_many :assignments, class_name: 'ProjectAssignment'
has_many :users, through: :assignments
belongs_to :company
end
# you can really call this whatever floats you boat
class ProjectAssignment
belongs_to :user
belongs_to :project
end
class Program
belongs_to :project
has_one :company, through: :project
has_many :assignments, class_name: 'ProgramAssignment'
has_many :users, through: :assignments
end
# you can really call this whatever floats you boat
class ProgramAssignment
belongs_to :user
belongs_to :program
end
That would automatically eliminate the problem with the company since it gets it through a parent relation.
The second problem that a user should not be able to create programs in a project he / she is not a member of sounds like something which should instead be handled on the authorization level - not in a validation.
Pundit example:
class ProgramPolicy < ApplicationPolicy
# ...
def create?
record.project.users.include?(user)
end
end
CanCanCan example:
class Ability
include CanCan::Ability
def initialize(user)
user ||= User.new # guest user (not logged in)
can :create, Program do |p|
p.project.users.include?(user)
end
end
end

ActiveRecord polymorphic association with unique constraint

I have a site that allows users to log in via multiple services (LinkedIn, Email, Twitter, etc..).
I have the below structure set up to model a User and their multiple identities. Basically a user can have multiple identieis, but only one of a given type (e.g. can't have 2 Twitter identiteis).
I decided to set it up as a polymorphic relationship, as drawn below. Basically there's a middle table identities that maps a User entry to multiple *_identity tables.
The associations are as follows (shown only for LinkedInIdentity, but can be extrapolated)
# /app/models/user.rb
class User < ActiveRecord::Base
has_many :identities
has_one :linkedin_identity, through: :identity, source: :identity, source_type: "LinkedinIdentity"
...
end
# /app/models/identity
class Identity < ActiveRecord::Base
belongs_to :user
belongs_to :identity, polymorphic: true
...
end
# /app/models/linkedin_identity.rb
class LinkedinIdentity < ActiveRecord::Base
has_one :identity, as: :identity
has_one :user, through: :identity
...
end
The problem I'm running into is with the User model. Since it can have multiple identities, I use has_many :identities. However, for a given identity type (e.g. LinkedIn), I used has_one :linkedin_identity ....
The problem is that the has_one statement is through: :identity, and there's no singular association called :identity. There's only a plural :identities
> User.first.linkedin_identity
ActiveRecord::HasManyThroughAssociationNotFoundError: Could not find the association :identity in model User
Any way around this?
I would do it like so - i've changed the relationship name between Identity and the others to external_identity, since saying identity.identity is just confusing, especially when you don't get an Identity record back. I'd also put a uniqueness validation on Identity, which will prevent the creation of a second identity of the same type for any user.
class User < ActiveRecord::Base
has_many :identities
has_one :linkedin_identity, through: :identity, source: :identity, source_type: "LinkedinIdentity"
end
# /app/models/identity
class Identity < ActiveRecord::Base
#fields: user_id, external_identity_id
belongs_to :user
belongs_to :external_identity, polymorphic: true
validates_uniqueness_of :external_identity_type, :scope => :user_id
...
end
# /app/models/linkedin_identity.rb
class LinkedinIdentity < ActiveRecord::Base
# Force the table name to be singular
self.table_name = "linkedin_identity"
has_one :identity
has_one :user, through: :identity
...
end
EDIT - rather than make the association for linkedin_identity, you could always just have a getter and setter method.
#User
def linkedin_identity
(identity = self.identities.where(external_identity_type: "LinkedinIdentity").includes(:external_identity)) && identity.external_identity
end
def linkedin_identity_id
(li = self.linkedin_identity) && li.id
end
def linkedin_identity=(linkedin_identity)
self.identities.build(external_identity: linkedin_identity)
end
def linkedin_identity_id=(li_id)
self.identities.build(external_identity_id: li_id)
end
EDIT2 - refactored the above to be more form-friendly: you can use the linkedin_identity_id= method as a "virtual attribute", eg if you have a form field like "user[linkedin_identity_id]", with the id of a LinkedinIdentity, you can then do #user.update_attributes(params[:user]) in the controller in the usual way.
Here is an idea that has worked wonderfully over here for such as case. (My case is a tad diffferent since all identites are in the same table, subclasses of the same base type).
class EmailIdentity < ActiveRecord::Base
def self.unique_for_user
false
end
def self.to_relation
'emails'
end
end
class LinkedinIdentity < ActiveRecord::Base
def self.unique_for_user
true
end
def self.to_relation
'linkedin'
end
end
class User < ActiveRecord::Base
has_many :identities do
[LinkedinIdentity EmailIdentity].each do |klass|
define_method klass.to_relation do
res = proxy_association.select{ |identity| identity.is_a? klass }
res = res.first if klass.unique_for_user
res
end
end
end
end
You can then
#user.identities.emails
#user.identities.linkedin

Can a 3-way relationship be modeled this way in Rails?

A User can have many roles, but only one role per Brand.
Class User < AR::Base
has_and_belongs_to_many :roles, :join_table => "user_brand_roles"
has_and_belongs_to_many :brands, :join_table => "user_brand_roles"
end
The problem with this setup is, how do I check the brand and the role at the same time?
Or would I better off with a BrandRole model where different roles can be set up for each Brand, and then be able to assign a user to a BrandRole?
Class User < AR::Base
has_many :user_brand_roles
has_many :brand_roles, :through => :user_brand_roles
end
Class BrandRole < AR::Base
belongs_to :brand
belongs_to :role
end
Class UserBrandRole < AR::Base
belongs_to :brand_role
belongs_to :user
end
This way I could do a find on the brand for the user:
br = current_user.brand_roles.where(:brand_id => #brand.id).includes(:brand_role)
if br.blank? or br.role != ADMIN
# reject access, redirect
end
This is a new application and I'm trying to learn from past mistakes and stick to the Rails Way. Am I making any bad assumptions or design decisions here?
Assuming Roles,Brands are reference tables. You can have a single association table Responsibilities with columns user_id, role_id, brand_id.
Then you can define
Class User < AR::Base
has_many : responsibilities
has_many :roles, :through => responsibilities
has_many :brands,:through => responsibilities
end
Class Responsibility < AR::Base
belongs_to :user
has_one :role
has_one :brand
end
The you can define
Class User < AR::Base
def has_access?(brand)
responsibility = responsibilities.where(:brand => brand)
responsibility and responsibility.role == ADMIN
end
end
[Not sure if Responsibility is the term used in your domain, but use a domain term instead of calling it as user_brand_role]
This is a conceptual thing. If BrandRole is an entity for your application, then your approach should work. If BrandRole is not an entity by itself in your app, then maybe you can create a UserBrandRole model:
class User < AR::Base
has_many :user_brand_roles
end
class Brand < AR::Base
has_many :user_brand_roles
end
class Role < AR::Base
has_many :user_brand_roles
end
class UserBrandRole < AR::Base
belongs_to :user
belongs_to :brand
belongs_to :role
validates_uniqueness_of :role_id, :scope => [:user_id, :brand_id]
end

has_many :through default values

I have a need to design a system to track users memberships to groups with varying roles (currently three).
class Group < ActiveRecord::Base
has_many :memberships
has_many :users, :through => :memberships
end
class Role < ActiveRecord::Base
has_many :memberships
has_many :users, :through => :memberships
end
class Membership < ActiveRecord::Base
belongs_to :user
belongs_to :role
belongs_to :group
end
class User < ActiveRecord::Base
has_many :memberships
has_many :groups, :through => :memberships
end
Ideally what I want is to simply set
#group.users << #user
and have the membership have the correct role. I can use :conditions to select data that has been manually inserted as such :
:conditions => ["memberships.role_id= ? ", Grouprole.find_by_name('user')]
But when creating the membership to the group the role_id is not being set.
Is there a way to do this as at present I have a somewhat repetitive piece of code for each user role in my Group model.
UPDATED
It should be noted what id ideally like to achieved is something similar to
#group.admins << #user
#group.moderators << #user
This would create the membership to the group and set the membership role (role_id ) appropriately.
You can always add triggers in your Membership model to handle assignments like this as they are created. For instance:
class Membership < ActiveRecord::Base
before_save :assign_default_role
protected
def assign_default_role
self.role = Role.find_by_name('user')
end
end
This is just an adaptation of your example.

Resources