Wondering what the best approach would be for managing user roles using acl9?
Here's the need:
Admins should be able to update a user's role.
Here are the issues:
How can we list all subject roles? :admin, :manager
To keep the api endpoints RESTful, I would like to pass the role param in the user_controller's update method ideally.
How can I authorize just for the role property so that, the owner of the user object can still modify their first_name, last_name fields, but not their role field? Only admins are allowed.
I can check for params manually outside of the access_control block:
def secure_params
if current_user.has_role?(:admin)
params.require(:user).permit(:first_name, :last_name, :role)
else
params.require(:user).permit(:first_name, :last_name)
end
end
but thought I would check and see if there is a cleaner solution.
Lastly, is it possible and wise to use something like RoleTypes.ADMIN, RoleTypes.MANAGER instead of :admin? If so, what's the best way to do this and is that Class accessible throughout a rails app?
How can we list all subject roles? :admin, :manager
The Role model is just a normal model, so you can just query this like you would anything else:
Role.uniq.pluck :name
To keep the api endpoints RESTful, I would like to pass the role param in the user_controller's update method ideally.
Generally, you're much better off using a separate controller for performing administration, and put it in the Admin:: namespace, and use namespace :admin do routes, etc..
Then in that controller you can use a normal access_control block to make sure no one else can get in:
class Admin::UsersController < ApplicationController
access_control do
allow :admin
end
# ...
end
So then, yeah, you can set/update a role in a number of ways, but seeing as a user can have many roles it's probably best not to have a single :role param, but rather to use:
class User < ActiveRecord::Base
acts_as_authorization_subject
accepts_nested_attributes_for :roles
end
So then in your controller you would have:
def user_params
params.require(:user).permit(:first_name, :last_name, roles_attributes: [:name])
end
Then in your form you can do something like:
= form_for :user do |f|
= f.text_field :first_name
= f.text_field :last_name
= f.fields_for :roles do |fr|
= fr.text_field :name
Note that I'm assuming you have just simple roles here, not roles on an object, if you have object roles then you'll need something a bit trickier to capture authorizable_id and authorizable_type for each role, and some way to select the object on which to act.
How can I authorize just for the role property so that, the owner of the user object can still modify their first_name, last_name fields, but not their role field? Only admins are allowed.
Hopefully you're already answering this one yourself now - by using different controllers for the admin interface and the user's own interface.
Lastly, is it possible and wise to use something like RoleTypes.ADMIN, RoleTypes.MANAGER instead of :admin?
No, the symbols are simpler and better, although it's quite common to do something like:
class Role < ActiveRecord::Base
def self.permitted_roles
%i/admin manager foo bar wee/
end
end
So that you can then use Role.permitted_roles to construct an array of checkboxes, or a dropdown, or something like that.
Edit: Totally missed the acl9 bit in the first sentence - sorry for that!
That being said, my answer is still decently applicable; I would have a helper method or use acl9's has_role? method and alter the view based on this. Ie:
>> <% if #user.has_role?(:foo, nil) %>
>> <p>
>> <%= f.label :usertype %><br />
>> <%= f.check_box :usertype %>
>> </p>
>> <% end %>
I would use a helper method to actually set the user role using the .has_role! method ie: user.has_role! :some_role, [optional scope], where user is the user being assigned the role.
For more info, check out this (from the acl9 docs):
https://github.com/be9/acl9/wiki/Tutorial%3A-securing-a-controller
Ok, so if what I think you're saying is correct, you want a way to have content and/or profile information editable by users but user roles editable by an admin.
First off, I'm going to assume role is a part of your user schema (if its not, I suggest you consider moving it there). If so, I would suggest adding a method to your user model may_edit?(content) that returns a boolean depending on the user role or id. Ie:
def may_edit?(content)
item.user.id == self.id
end
Where the content is found via a find_by(id) in the controller. You could then use an if in the controller to dictate which content will appear on the show:
<% if current_user.role == admin %>
<%= some button or link or dropdown render %>
<% end %>
<% if current_user.may_edit?(content) %>
<%= some button or link or dropdown render %>
<% end %>
Or something along those lines. You can also write an if..else as a separate helper method or in the may_edit? method (I'd suggest making it/them model methods). The controller ideally shouldn't be the one deciding who can and can't perform this kind of thing. If you're set on doing it in the controller, you might want to look into :before_filter at the top of your controller, ie:
:before_filter admin?, except [:foo, :bar]
I'm not sure which user id you want to pass in the params, but either the editor/admin's or the original poster's ids can be found - the current_user will be logged in session as session[:user_id] and the original poster can be found via the content being edited, since it belongs to a user and can be found via a ``User.find_by(content.user_id).
Finally, are you asking how to see a list of all user roles? If so, you can use a dropdown menu, but you'll have to either hardcode them somewhere in your project or iterate through every user and aggregate their roles, although the latter is far from ideal and very inefficient.
Hope this helps, and good luck!
Related
This question is kind of hard to ask, but basically, I have a Class model and a User model, each Class table has a token, and so does each User one. After the user submits a sign up form, how would I set the value of the users class_id in the create action? I've tried <%= f.hidden_field :app_id, :value => App.find_by_token(params[:key]) %>, but this doesn't work. Sorry for the long and confusing question, will be glad to answer more. Thanks in advance for any answers
It sounds as though you have a "relationship" where a User belongs to a Class and a Class could have many users. If that is the case then you should use rails Associations to make it easy for yourself. This would involve adding a 'has_many :users' to your Class model and a 'belongs_to :class' call to your User model. You would then just use the rails helpers to 'build' the object and save it with the association in the corresponding controllers.
The manual way to do it would be as follows from your controller:
def create
#This would involve you sending the proper class id as a hidden form field with the form field attribute named 'class_id'. You may need to add 'attr_accessor :class_id' to your User model.
if user.create(user_params)
blahblahblah
else
sorry blah blah
end
end
private
def user_params
params.require(:user).permit(:name, :email, :class_id, :etc)
end
I have the following line of code
<% map = options_for_select(User.all.map {|u| [u.first_name+" "+u.last_name, u.id]}) %>
which grabs the first and last name of a user and submits its ID in a form. Now I have added a few users and they are not in alphabetical order. How can I sort this map by first name?
You can also use order to get the rows already ordered from the database:
<% map = options_for_select(User.all.order(:first_name).map {|u| [u.first_name+" "+u.last_name, u.id]}) %>
May be...
<% map = options_for_select(User.all.map {|u| [u.first_name+" "+u.last_name, u.id]}.sort) %>
You should never use queries on the views. You should use views only for presentation, and all the logic on the Models and or the Controllers.
Also, respecting Fat Models, Skinny Controllers Best Practice:
In practice, this can require a range of different types of refactoring, but it all comes down to one idea: by moving any logic that isn’t about the response (for example, setting a flash message, or choosing whether to redirect or render a view) to the model (instead of the controller), not only have you promoted reuse where possible but you’ve also made it possible to test your code outside of the context of a request.
Finally, in this case it's best to use a scope to reuse it later.
Use a scope on User model, and have a name method:
class User < ActiveRecord::Base
scope :order_by_name, ->(first_name, last_name) { order("#{first_name} ASC, #{ last_name} ASC") }
def name
"#{first_name} #{last_name}"
end
end
If you're calling your line from users/index, create an instance variable to load the users collection like this:
class UsersController < ApplicationController
def index
#users = User.order_by_name
end
end
Then you would call on the view using options_from_collection_for_select like this:
<% map = options_from_collection_for_select(#users, :id, :name) %>
I recommend you to do both the CONCAT and ORDER in the Database for performance reasons
User.select("CONCAT(u.first_name, ' ', u.last_name), u.id").order("u.first_name")
This User.all.map could be a bottleneck of your application
I am using acts_as_commentable and am curious if anyone has any good ideas on how to allow for anonymous and registered users to post comments? Meaning, if a registered user is authenticated, I want the comment to be marked with their name, etc. But I also want an anonymous user to be able to comment and have a name and email address recorded. I am using Devise for authentication.
I have an idea on how to make this work but it feels a little hacky to me. Wondering if anyone has any thoughts.
I don't know your plugin, but if you use this one (https://github.com/jackdempsey/acts_as_commentable), it seems very basic...
The Comment model has a relation to a user which is not mandatory.
So in your new comment form, I would just add two text_field_tags if the user is not logged (text_field_tag :first_name, text_field_tag :last_name).
And I'd just write the create action for comments like this :
def create
#comment = Comment.new(:commentable => #your_object, :user => current_user, :first_name => params[:first_name], :last_name => params[:last_name])
...
end
if the user is not logged, current_user will be nil and that won't cause any problem.
You can write an helper method to display the name for a comment depending it has a user or not like this...
# Displays the user's login if any or else the first name and last name
def displayed_name(comment)
comment.user ? comment.user.login : "#{comment.first_name} #{comment.last_name}"
end
I have 2 equal-access models: Users and Categories
Each of these should have the standard-actions: index, new, create, edit, update and destroy
But where do I integrate the associations, when I want to create an association between this two models?
Do I have to write 2 times nearly the same code:
class UsersController << ApplicationController
# blabla
def addCategory
User.find(params[:id]).categories << Category.find(params[:user_id])
end
end
class CategoriessController << ApplicationController
# blabla
def addUser
Category.find(params[:id]).users << User.find(params[:user_id])
end
end
Or should I create a new Controller, named UsersCategoriesController?
Whats the best practice here? The above example doens't look very DRY.... And a new controller is a little bit too much, I think?
Thanks!
EDIT:
I need to have both of these associations-adding-functions, because f.e.
#on the
show_category_path(1)
# I want to see all assigned users (with possibility to assign new users)
and
#on the
show_user_path(1)
#I want to see all assigned categories (with possibility to assign new categories)
EDIT:
I'm taking about a HBTM relationship.
If you have a situation where you need to do this with has_and_belongs_to_many, you could take the approach you are currently using, or you could build this into your existing update actions.
When you add a habtm relationship, you will get an additional method on your classes...
class User < ActiveRecord::Base
has_and_belongs_to_many :categories
end
With this, you can do this:
user = User.find(params[:id])
user.category_ids = [1,3,4,7,10]
user.save
The categories with those ids will be set. If you name your form fields appropriately, the update can take care of this for you if you want to use checkboxes or multiselect controls.
If you need to add them one at a time, then the methods you've built in your original post are reasonable enough. If you think the repetition you have is a code smell, you are correct - this is why you should use the approach I outlined in my previous answer - an additional model and an additional controller.
You didn't mention if you are using has_and_belongs_to_many or if you are using has_many :through. I recommend has_many :through, which forces you to use an actual model for the join, something like UserCategory or Categorization something like that. Then you just make a new controller to handle creation of that.
You will want to pass the user and category as parameters to the create action of this controller.
Your form...
<% form_tag categorizations_path(:category_id => #category.id), :method => :post do %>
<%=text_field_tag "user_id" %>
<%=submit_tag "Add user" %>
<% end %>
Your controller...
class CategorizationsController < ApplicationController
def create
if Categorization.add_user_to_category(params[:user_id], params[:category_id])
...
end
end
then your categorization class...
class Categorization
belongs_to :user
belongs_to :category
def self.add_user_to_category(user_id, category_id)
# might want to validate that this user and category exist somehow
Categorization.new(:user_id => user_id, :category_id => category_id)
Categorization.save
end
end
The problem comes in when you want to send the users back, but that's not terribly hard - detect where they came from and send them back there. Or put the return page into a hidden field on your form.
Hope that helps.
I'd like to create a user registration form where the user ticks some boxes that do not connect to the model.
For example, there might be a 'terms & conditions' box that I don't want to have a boolean field in the User model saying 'ticked Terms & Conditions'. Instead I want to create a record somewhere else (like a transaction) that recorded the date/time they accepted the T&Cs.
Another example might be some preference they indicated that I'll use later and hold in the session for now, like 'remember me'.
I can mix these types of fields with the regular form helper. How could I do either one of the examples above when using formtastic? It kind of sticks to have to mix traditional rails tags with lovely clean formtastic code.
You can create any number of virtual attributes in your model that do not necessarily need to be tied to a database column. Adding attr_accessor :terms_and_conditions to your user model will make this 'field' available to formtastic -- even though it's not a database field. You can validate it like any other field or create your own setter method to create a record elsewhere if that's what you need.
I'm inclined to disagree with the approach to use attr_accessors for action-specific entry elements. If Ts&Cs need to be recorded then that makes sense, but sometimes you need data that really is unrelated to the model and is only related to the specific action at hand, such as 'perform some heavyweight operation when executing the action'.
Lets say you have a sign-up form, and you're not using OAuth, and you have an option to specify twitter username and password on sign up. This is fine:
<%= form.input :twitter_username %>
<%= form.input :twitter_password, :as => :password %>
But this bit below confuses me -- its like formtastic in this case is actually taking away what is already there. Is there a way of adding params[:your-object] while still getting formastic to do all its lovely layout stuff?
How about:
class User < ActiveRecord::Base
...
#I don't want this here. Its only for UserController#create.
#attr_accessor :tweet_when_signed_up
...
end
and:
<%= form.input :tweet_when_signed_up, :as => :checkbox, :param_only => true %>
param_only is my made-up suggestion. It says 'this isn't even a transient property. Its just for this action.
class UserController < ActionController::Base
...
def create
if params[:tweet_when_signed_up] # haven't done this yet -- == 1 or !.nil?
Tweeter.tweet( ... )
end
#user = User.create( params[:user] )
end
The ability to do this is probably there -- does anyone know how to do effectively what I think is a good idea above?
thanks!
Instead I want to create a record
somewhere else (like a transaction)
that recorded the date/time they
accepted the T&Cs.
Use the attr_accessor that bensie describes to integrate the field with formtastic.
Formtastic is about view logic, while the relationship are more model logic. Don't worry about creating the related record in the form. Instead, use callbacks like before_update and after_save in the model to ensure the related record has been created.