Is it considered bad practice (or un-RESTful) to create nested routes for resources that otherwise have no association? For example I have:
resources :foos do
resources :bars
end
But I have no business logic elsewhere in my database or application that associates :foos with :bars.
The reason I want to do this: Many of my routes are created as resources nested under my :groups resource. I do that so that I can always grab a group_id param and always show a layout that matches the group the user is currently "in". I'm comfortable with this when the resource belongs_to the group:
/groups/1/comments/1
But when some other comment does not belong_to the group (group1) and I want to look at it through layout that is "branded" as group1, my impulse is to route it like this:
/groups/1/comments/2
Is this ok to do, maybe I'm overthinking this?
I maintain an app with similar requirements. I do something roughly along the lines of:
class User
has_and_belongs_to_many :groups
belongs_to :active_group, class_name 'Group'
def active_group
return super unless super.nil?
group = groups.first
update_columns(active_group_id: group.id)
group
end
end
The User class validates that they are assigned to one or more groups and the active_group method is overriden to provide a default if it is a first login. With this approach you will need to provide an action to set the active group so the user can switch groups (presumably this is a requirement).
This assumes that you have some kind of authentication in place to know the current user. If restricting access to groups is not a concern, you can forego the habtm relationship and substitute groups.first with Group.first.
If you don't have/want/need authentication, you could just drop a active_group_id in the session cookie. But I would definitely consider it bad practice to nest unrelated resources.
Related
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.
I am working on a Ruby on Rails application that has two kinds of "Roles".
One will be where a user has multiple roles, such as "Admin", "Team Lead", etc. these will be defined in the seeds file and not generated by the user.
The other will be generated by a User and assigned to other users, such as "Chef", "Waiter", etc.
They are both similar in that they only have a name column. The former will be used with an authorization tool such as CanCanCan. I currently plan to allow users and roles to have many of the other using a has_many :through relationship.
class Role
has_many :user_roles
has_many :users, through: :user_roles
end
class User
has_many :user_roles
has_many :roles, through: :user_roles
end
class UserRole
belongs_to :user
belongs_to :role
end
My questions are:
Should the latter ("Chef", "Waiter", etc) be put in the same table? Separate tables?
Should I use some kind of inheritance?
What's a good practice for this situation?
I plan to use the term "Role" in the user interface for the latter, showing what "Roles" a user has. The former I guess is more about what privileges they have within the application.
If you go away from the technical side of roles and authentication, and try to describe the "things" from a more business oriented approach you make the distinction clearer for yourself.
What I understand is: You have a definition for a user of your application that is used to describe what authorization this user has, e.g. an "admin" has more rights than an "editor" or "community manager".
You also want these users of your application to be able to create names that are associated with (other?) users. Theses names have nothing to do with authorization, as I understood.
Maybe these names are more like tags, that people can assign?
I would keep both separated, as it shouldn't be able for a user to create a role, or modify existing roles, that could grant them access to admin features.
If you want to look at a tagging gem, I could recommend this one: https://github.com/mbleigh/acts-as-taggable-on I used this for several years and while it has its drawbacks, it's reliable.
I'd suggest having a look at rolify before rolling your own solution, as it will have solved some things that you'll have to reimplement / discover later (e.g. role queries, avoiding N+1 queries etc). It also integrates well with can?, and should work well for the authorisation part of your question.
Whilst it's not impossible to allow users to create new roles (by throwing an input form on top of Role.create), this starts to get messy, as you need to track which ones are for authorisation and which ones informative (and user created).
Since the two groups of things are for different purposes, I wholeheartedly agree with this other answer that it's cleaner to separate the user-generated entities, and look to implement them as tags. You may display all the "roles" together in certain views, but that doesn't mean that it makes sense to store them within a single table.
Side-note: if you do end up rolling your own solution, consider using HABTM here. The join table will still be created, but you won't have to manage the join table model. E.g.
has_and_belongs_to_many :users, join_table: :users_roles
Since you only have a limited number of roles, you could use a bitmask and store directly on the user model as a simple integer.
See this Railscasts for more information. That would be the most efficient, database and association wise, way to do this although perhaps not the simplest to understand. The only real restriction is that you can't alter the array of values you check against, only append to it.
Good luck!
I would create one more model, Permission, where you can create a list of all the permissions you want to manage under any given role.
Then have a many to many between Permissions and Roles.
From a UserRole instance then you will be able to list the permissions, and in the future you could add additional permissions to roles buy just running inserts in a couple of tables
Roles: Onwer, Chef, Cook, Waiter
Permission: can_cook, can_buy_wine, can_manage, can_charge_custome
Owner: can_manage, can_buy_wine, can_charge_customer
Chef: can_cook, can_manage
Waiter: can_charge_customer
Is would be a good start and you can evolve the role functionality to whatever your needs are without an external dependency.
Also, You can go just using Users table and adding role column as integer and give them a role code in default 0 integer.
#app/helpers/roles_helper.rb
module RolesHelper
#roles = {
'Default' => 0,
'Waiter' => 10,
'Chef' => 20,
'Superadmin' => 30
}
class << self
def list_roles
#roles.map{|k,v| [k,v] }
end
def code(str)
return #roles[str]
end
def value(id)
return #roles.key(id)
end
end
def require_default_users
unless current_user && current_user.role >= RolesHelper.code('Waiter')
redirect_to root_url(host: request.domain)
end
end
def require_superadmin_users
unless current_user && current_user.role >= RolesHelper.code('Superadmin')
redirect_to courses_path
end
end
end
access in controllers
sample:
class Admin::AdminController < ApplicationController
include RolesHelper
def sample_action_method
require_default_users #if non admin user redirect ...
puts "All Roles: #{RolesHelper.list_roles}"
puts "Value: #{RolesHelper.value(30)}"
puts "Code: #{RolesHelper.code('Superuser')}"
end
end
I am trying to learn how to use scopes in my Rails 5 app.
I have asked a background question here.
have models in my Rails 5 app for User, Proposal and Potential.
Users create Proposals which they themselves and others can then create comments on.
The associations between models are:
User
has_many :proposals, dependent: :destroy
has_many :potentials
Proposal
belongs_to :user
has_many :potentials, inverse_of: :proposal
accepts_nested_attributes_for :potentials, reject_if: :all_blank, allow_destroy: true
Potential
belongs_to :proposal, inverse_of: :potentials
belongs_to :user
In my routes file, I have two resources for potentials. I'm not sure if I've gone off piste with this bit- I cant find an example of how to do this otherwise. I have both:
resources :potentials
as well as:
resources :proposals do
resources :potentials
Objective:
When the user who made the proposal tries to edit it, I only want that user to be able to edit the potentials that they created themselves.
The reason I have two routes set up for potentials is that the nested resource has a nested form fields inside the proposal form, so that the proposal creator can make a potential in that way. Any other user that sees the proposal and makes a potential does it via a separate form.
Any user (including the proposal creator, can edit the potential via that separate form), and the proposal creator can also edit any of its own proposals by the nested form in the proposal form.
At the moment, whenever I edit the proposal form (even when I don't edit the potential nested fields), all of the potentials are updated to insert the proposal creator's user id overriding the actual potential creator's user id.
Solution
I am trying to limit the edit action in the proposals controller, so that it only allows the proposal /potentials to be edited if they have the user_id == the proposal.user_id.
For this purpose, I have written scopes in my proposal.rb
scope :owner_potentials, ->{ where(user_id: potential.user_id ) }
scope :third_party_potentials, ->{ where(user_id: != potential.user_id) }
The solution in the post i liked above was to try using a scope. Since scopes are meant to work on the class, rather than an instance, I'm stuck in trying to figure out how to adapt them so that I can use the scope to search for all the compliant potentials (i.e. potentials where potential.user_id == proposal.user_id). That means Im not searching the Proposal class, Im searching the specific proposal.
This post suggested defining Event.all inside the relevant controller action, but then how would I limit that so it only applied to the specific potentials edit line? I have other lines in my edit action which should not be tested on the Proposal table, but just the instance. If this were able to work, I imagine I would then need to rewrite my scope to try to exclude all the other proposals.
Is there a way to use an edit action in a controller with a scope, on a specific instance?
I would suggest scopes like this:
scope :owner_potentials, -> (user_id) { where(user_id: user_id) }
scope :third_party_potentials, -> (user_id) { where.not(user_id: user_id) }
When calling these scopes you just need to pass current user's id.
Scopes define queries for the AR class they are defined in. You say you have written owner_potentials and third_party_potentials scopes in proposal.rb. But if these scopes are meant to return a collection of potentials, then these should be defined in the Potential class. If you need to access these scopes from a proposal record, you can chain scopes to associations, e.g.
class Potential
scope :owner_potentials, -> (user) { where(user: user) }
scope :third_party_potentials, -> (user) { where.not(user: user) }
end
...
class ProposalsController # Proposals::PotentialsController..? imo Proposals::PotentialsController#edit sounds like an endpoint for editing exactly one potential record and nothing else, which doesn't sound like what you want. Your call on how to structure the controller/routes though.
def edit
#proposal = ... # Whatever your logic is to find the proposal
#proposal.potentials.owner_potentials(current_user) # do something with the user's potentials
#proposal.potentials.third_party_potentials(current_user) # do something with the potentials the user doesn't own
end
end
You can see here how you chain an association (.potentials) to a scope (.owner_potentials).
Also, if you have an association, you can treat that association as a field in a where method, a la where(user: user) instead of where(user_id: user.id).
Last thing to note is that you probably want to change the name of the scopes with this refactor.
potentials.owner_potentials(user) is a bit redundant. Maybe something like potentials.owned_by(user) ?
In my rails v4 app, users belong to a single group.
class User < ActiveRecord::Base
belongs_to :group
...
Each group can have many projects,
class Group < ActiveRecord::Base
has_many :projects
has_many :users
...
Each project can have many experiments, in a one to many relationship.
In my routes, I have:
resources :projects do
resources :experiments do
...
end
end
What I'd like to do is only allow users to access projects and experiments if the project has the same group_id as the user (i.e. if user enters a project id parameter in the projects#show route for a project outside of their group, it will not be displayed). Is there a clean way to implement this without having to do multiple checks in the view?
Take a look at building a custom constraint based on Group membership:
http://guides.rubyonrails.org/routing.html#advanced-constraints
Extremely simple example (obviously, you'll need to match your project design):
class GroupConstraint
def initialize
#project = Project.find(params[:id])
#user = current_user
end
def matches?(request)
#user.groups.include?(#project.group)
end
end
Then in your routes:
resources :projects, constraints: GroupConstraint.new do
resources :experiments do
...
end
end
This is authorization problem, so, as for me, it's better to define what users can and can not see using any auhtorization library, not using routes or something like that. Because you will, for example, definitely want to find out should you display link on given group in views, which groups is available for current user and so on.
Take a look on cancancan for example: https://github.com/CanCanCommunity/cancancan.
I need to find an elegant solution to scoped finds based on access control rules. Essentially I have the following setup:
Users
Customers
AccessControl - Defines which user has access to another users data
Users need to be able to access not just their own customers but also shared customers of other users.
Obviously something like a simple association will not work:
has_many :customers
and neither will this:
has_many :customers, :conditions => 'user_id in (1,2,3,4,5)'
because the association uses with_scope and the added condition is an AND condition not an OR condition.
I also tried overriding the find and method_missing methods with the association extension like this:
has_many :customers do
def find(*args)
#get the user_id and retrieve access conditions based on the id
#do a find based on the access conditions and passed args
end
def method_missing(*args)
#get the user_id and retrieve access conditions based on the id
#do a find based on the access conditions and passed args
end
end
but the issue is that I don't have access to the user object / parent object inside the extension methods and it just does not work as planned.
I also tried default_scope but as posted here before you can't pass a block to a default scope.
Anyhow, I know that data segmentation and data access controls have been done before using rails and am wondering if somebody found an elegant way to do it.
UPDATE:
The AccessControl table has the following layout
user_id
shared_user_id
The customer table has this structure:
id
account_id
user_id
first_name
last_name
Assuming the the following data would be in the AccessControl table:
1 1
1 3
1 4
2 2
2 13
and so on...
And the account_id for user 1 is 13 I need to be able to retrieve customers that can be best described with the following sql statement:
select * from customers where (account_id = 13 and user_id = null) or (user_id in (1,3,4))
Sorry if I've completely missed the point here but I'm not 100% sure of what you want to do. Is AccessControl a relationship between User and Customer? If so looks like you just need to setup a many-to-many relationship.
class User
has_and_belongs_to_many :customers
# or this if you need to store meta data in the join table
has_many :customers
has_many :access_controls
has_many :accessible_customers, through => :access_controls
end