Devise/CanCan - Restricted Content - ruby-on-rails

Building a rails B2B application that will have various users. I'm pretty clear on restricting access for internal staff using Devise and CanCan but I want to be able to give suppliers and customers their own login as well. Customer will be fairly simple, however, I want to ensure the supplier (label) login enables them to view and amend their own product and sales data only.
The model is roughly:
User (as setup by Devise)
Label [has_many releases]
Release [belongs_to label / has_many products]
Product [belongs_release / has_many tracks]
Track [belongs_product]
I'm guessing I could add in a label_id field on the user model and associate that way but I need internal users (and customers) to have access to view all label data. I also need to allow a label to have many users.
Would it simply be a case of defining a 'label' role via Cancan that enforces the use of a label_id in the the views? If that's the correct approach how do I then lock down the content to that label_id in my controllers/views? Role based if statements?
Thanks in advance!

What you'd first have to do is define some CanCan roles, like supplier, customer and staff, and then create an interstitial controller to handle the forking:
class CheckingController < ApplicationController
def index
#path = case user.role
when 'supplier'
supplier_path
when 'customer'
customer_path
when 'staff'
staff_path
else
admin_sign_in_path #or whatever
end
redirect_to #path
end
end
Then in your routes.rb file you can send users to either the root or index action of your controller by first sending them to CheckingController#index which will redirect based on your CanCan roles.

Related

Rails Cancancan authorization of models in many-to-many relation

I have User and Role models in many-to-many relations via UserRoleAssoc in Ruby-on-Rails.
I need a page (web interface) from which a user can add/delete roles associated with a user, where ordinary users but administrators can edit the roles for themselves only.
My question is how to implement the scheme, particularly authorization.
Here are the models of User and Role (just the standard many-to-many):
class User < ApplicationRecord
has_many :user_role_assocs, dependent: :destroy
has_many :roles, through: :user_role_assocs
end
class Role < ApplicationRecord
has_many :user_role_assocs
has_many :users, through: :user_role_assocs
end
class UserRoleAssoc < ApplicationRecord
belongs_to :user
belongs_to :role
end
According to DHH's principle (cf. "How DHH Organizes His Rails Controllers" by Jerome Dalbert), such actions should be implemented as if a controller, say, ManageUserRolesController, does one or more of the CRUD actions. In this case, ManageUserRolesController either or both of create and delete multiple records on UserRoleAssoc.
Since the web user interface should enable one to manage a list of roles (with a select box) in one go from a URL, I made the create method of ManageUserRolesController does both, receiving User-ID (user) and an Array of Role-IDs (roles) in params (I'm open to suggestions, though!). routes.rb is as follows:
resources :manage_user_role, only: [:create] # index may be provided, too.
Now, to restrict a user to add/delete roles to any other users, I would like to write in models/ability.rb something like, along with a Controller:
# models/ability.rb`
can :create, ManageUserRoles, :PARAMS => {user: user} # "PARAMS" is invalid!! Any alternative ideas?
can :manage, ManageUserRoles if user.administrator?
# controllers/manage_user_roles_controller.rb
class ManageUserRolesController < ApplicationController
load_and_authorize_resource
end
It seems possible to achieve it in the way described in an answer to "Passing params to CanCan in RoR" and CanCan wiki, though I think the model corresponding to the controller has to be defined to point the non-standard table, in models/manage_user_role.rb
class ManageUserRole < ApplicationRecord
self.table_name = 'user_role_assocs'
end
But this seems quite awkward…
What is the 'Rails' way (Version 6+) to implement authorization of many-to-many models? To be specific, what is a good interface to add/delete multiple roles to a user with some constraint?
Note that the route doesn't have to be like the sample code above; the route can be set so that a user-ID is passed as a part of the path like /manage_user_role/:user_id instead of via params, as long as authorization works.
Here is an answer, a solution I have used in the end.
Background
Many-to-many relation is by definition complex and I do not think there are any simple solutions that fit all cases. Certainly, Ability in CanCanCan does not support it in default (unless you do some complicated hacks, such as the way the OP wanted to avoid, as mentioned in the Question).
In this particular case of question, however, the situation which the OP wants to deal with is a constraint based on the user ID, which is basically a one-to-many (or has_many) relation, namely one-user having many roles. Then, it can actually fit in the standard way as Cancancan/Ability works.
General speaking, there are three ways to deal with the OP's case of many-to-many relation between users and roles (i.e., each user can have many roles and a role may belong to many users):
handling it as in the User (Controller) model,
handling it as in the Role (Controller) model,
or UserRoleAssoc (Controller), that is, a model associated with the join table between User and Role (n.b., this Controller is not created by default and so you must create it manually if you use it).
Let me discuss below which one of the three best fits the purpose with Cancancan authorization.
How Cancancan authorizes with Ability and what would fit this case best
For the default CRUD actions, Cancancan deals with a can statement as follows (in my understanding); this is basically a brief summary with regard to this case of the official reference of Cancancan:
for the action index, only the information Cancancan has is the User, the Model Class (with/without scopes), in addition to the action type index. So, basically, Cancancan does not and cannot do much. Importantly, a Ruby block associated with the can statement, if any, is not called.
if the (primary) ID of the model is given in the path, namely for the actions of show, edit, update, destroy, Cancancan retrieves the model from the DB and it is fed to the algorithm you provide with the can statement, including a Ruby block, if given.
In the OP's case, a user should not be authorized to handle the roles of any other users but of her/himself. Then, the judgement must be based on the two user-IDs, i.e., the one of current_user and the one given in the path/route. For Rails to pick up the latter from the path automatically, the route must be set accordingly.
Then, because the "ID" is a User-ID, the most natural solution to deal with this case is to use UsersController (case 1 in the description above); then the ID included in the default route is interpreted as User#id by Rails and Cancancan. By contrast, if you adopt case 2, the default ID in the path will be interpreted as Role#id, which does not work well with this case. As for case 3 (which was mentioned in the question), UserRoleAssoc#id is just a random number given to an association and has nothing to do with User#id or Role#id. Therefore, it does not fit this case, either.
Solution
As explained above, the action of the Controller must be selected carefully so that Cancancan correctly sets the User based on the given ID in the path.
The OP mentions create and delete (destroy) for the Controller. It is technically true in this case that the required actions are either or both of to create and delete new associations between a User and Roles. However, in Rails' default routing, create does not take the ID parameter (of course not, given the ID is given in creation by the DB!). Therefore, the action name of create is not really appropriate in this case. update would be most appropriate. In the natural language, we interpret it such that a user's (Role-association) status will be update-d with this action of a Controller. The default HTTP method for update is PATCH/PUT, which fits the meaning of the operation, too.
Finally, here is the solution I have found to work (with Rails 6.1):
routes.rb
resources :manage_user_roles, only: [:update]
# => Route: manage_user_role PATCH /manage_user_roles/:id(.:format) manage_user_roles#update
manage_user_roles_controller.rb
class ManageUserRolesController < ApplicationController
load_and_authorize_resource :user
# This means as far as authorization is concerned,
# the model and controller are User and UsersController.
my_params = params.permit('add_role_11', 'del_role_11', 'add_role_12', 'del_role_12')
end
View (to submit the data)
This can be in show.html.erb of User or whatever.
<%= form_with(method: :patch, url: manage_user_role_path(#user)) do |form| %>
Form components follow...
app/models/ability.rb
def initialize(user)
if user.present?
can :update, User, id: user.id
end
end
A key take is, I think, simplifying the case. Though many-to-many relations are inherently complex, you probably better deal with each case in smaller and more simple fragments. Then, they may fit in the existing scheme without too much hustle.

Who handle the roles of a User in Rails?

I'm making a Rails 6 application where I'm using Devise for authentication, Pundit for authorization and I added Active-Admin because I need a dashboard where admin users manage the content of the app.
Other than admin, I have a couple of more roles president, manager, guest. An admin can be president or manager.
I'm little confuse on what to use to implement the roles, with devise? pundit? I do it by hand?
Is it better to unite the User and the AdminUser model active-admin created? Because this way UserAdmin users can't log in to the application, only to the dashboard and that is not what I want.
I have seen tutorials where people add an admin:boolean column to the users, should I do something like that?
Is it better to unite the User and the AdminUser model active-admin created? Because this way UserAdmin users can't log in to the application, only to the dashboard and that is not what I want.
That depends more on your business logic. It may be a good idea to keep your users and admin_users tables separated; The users table will probably need to have a lot of associations with other tables, that will not be necessarily needed by admin_users, right?
I'm little confuse on what to use to implement the roles, with devise? pundit? I do it by hand?
You may define a role column in your admin_users table, and use that column in pundit policies, for example:
class ResourcePolicy
# ...
# ...
# ...
def update?
user.admin? || user.president?
end
end
in AdminUser, you can do the following:
class AdminUser < ActiveRecord::Base
def admin?
role == 'admin'
end
def president?
role == 'president'
end
end
There are many other ways to implement that, and they all depend on what you need to achieve.

Rails two registration forms with two user types

I have a rails project where I have one user model with two different roles one is for a client and the other for business, currently am playing around with both enum roles and rolify.
I am using devise as my authentication system and I have two registration forms for each one but what I want to achieve is to assign the role to the user after the create action so to give him the ability afterwards to create a client profile or a business profile and more ..., I don't want to give the user the ability to choose from a select what type he wants.
So I am bit confused I used this method before :
after_initialize :set_client, :if => :new_record?
def set_client
self.kind ||= :client
end
I know this will give every record a client role, so I wanted to do it on the controllers and I hear its bad practice.
So am thinking to pass a hidden field in the form with the value of the role.
Any suggestions ?

How to create and organize an admin to have access to features other than Users in Rails

Rails newbie: I currently have a basic app where Customers(users) have many points (customer model and points model). And I want an admin user (new model) to have the ability to add points to the customer.
-The customer enters their phone number (#index route).
-If the customer is not found, they will be brought to a signup page (#new/#create route).
-If customer is found in the database, their profile will show (#show route).
Now on this page, I want to be able to have an admin passcode, which once entered, gives access to adding points. I also want to keep track of which admin user, gives which customer points on a different page.
How would the schema look like with the admin user? How would I give it access certain access to features like adding points. (I assume I'm going to have to create a helper method for checking if admin is logged in, and keep track of that somehow, maybe with sessions?)
class Customer < ActiveRecord::Base
has_many :points
end
class Point < ActiveRecord::Base
belongs_to :customer
end
class Admin < ActiveRecord::Base
#??? (my best guess is has_many :points, has_many :customers)
end
www.loyalty-app.herokuapp.com
I think the best way is to not have a special admin class and just make admins a type of customer (or user). Then use a gem like cancan or access granted (I prefer access granted) to handle what the different types of users can do.

Using CanCan to assign permissions to roles

I have a rails application where I have set up a table: users another table: roles and finally a join table: user_roles where a user may have many roles, but a role belong_to_and_has_many :users
This has allowed me to create new roles and then, assuming thee user is an admin, on the user edit page, switch their role.
This is great, how ever currently no role has capabilities. What I was thinking was doing:
role_permissions table
permissions: has_and_belongs_to_many :roles
Setting up a set of checkboxes on the roles edit page to assign a set of capabilities
to said role, that can then be applied to said user, that can then be used by capybara to determine if a user has the appropriate action or not.
While you can create roles, you cannot create capabillities. so you would have a predetermined list of capabilities. Also some roles, such as administrator or member could not be destroyed or edited. (already done.)
I can set up the table and the relationship to do this, what I cannot figure out how to do is to integrate this concept with cancan. Because can can does something like:
can? :destroy #project
If I assign, say:
Role: Editor (String name)
Capabilities: Read, Write, Destroy, Update, Preview (These are just string names)
How could I then say:
can? user.role? Editor read Post - seudo code.
First of all, for capabilities, if it's a fixed list of capabilities you're working with, you're probably better off with having a number of booleans on the roles table, e.g. can_create_projects, can_create_users, etc., which encode the abilities of each role.
Then your CanCan Ability class might have something like the following,
class Ability
include CanCan::Ability
def initialize(user)
can(:create, Project) do |project|
user.roles.any?(&:can_create_projects)
end
end
end

Resources