I got stuck on the following issue - I suspect there is a simple solution to it, but I just can't figure it out.
I am using Role Model Gem with Rails 4.
All works well, and the assigned roles are stored perfectly well in the roles_mask attribute (integer) of the user model as internal bitmask.
Now, I would like that Admins can assign as well as remove Roles from users via a FORM (view). I am not a Rails Ninja, so there might be a trick to do this.
According to the Doc, I can do the following:
# role assignment
>> u.roles = [:admin] # ['admin'] works as well
=> [:admin]
# adding roles (remove via delete or re-assign)
>> u.roles << :manager
=> [:admin, :manager]
So that is understood.
And my approach was to query for all valid roles in the form:
# get all valid roles that have been declared
>> User.valid_roles
=> [:admin, :manager, :author]
Then list them as checkbox. Once the form gets submitted I assign / remove roles.
The question:
Is that the right approach, does this even work, and if so how?
Since I had the problem not once I figured out a way - here is the solution:
More details here.
Many roles per user
I assume in this answer that you have roles in form of symbols (below I modified this solution in a way that it works with symbols) - so to work with Pundit in case you are using it.
# in models/user.rb
ROLES = [:admin, :manager, :general, :custom_role, :another_custome_role, :banned]
It is possible to assign multiple roles to a user and store it into a single integer column using a bitmask. First add a roles_mask integer column to your users table.
rails generate migration add_roles_mask_to_users roles_mask:integer
rake db:migrate
Next you'll need to add the following code to the User model for getting and setting the list of roles a user belongs to. This will perform the necessary bitwise operations to translate an array of roles into the integer field.
# in models/user.rb
def roles=(roles)
self.roles_mask = (roles & ROLES).map { |r| 2**ROLES.index(r) }.inject(0, :+)
end
def roles
ROLES.reject do |r|
((roles_mask.to_i || 0) & 2**ROLES.index(r)).zero?
end
end
Here a tiny modification to make it work with Rolify & Pundit
def roles=(roles)
self.roles_mask = (roles.map { |x| x.to_sym } & ROLES).map { |r| 2**ROLES.index(r) }.inject(0, :+)
end
If you're using devise without strong parameters, don't forget to add attr_accessible :roles to you user model.
If you're using devise with strong_parameters, either as a gem in a Rails 3 app, or as is built-in in Rails 4, dont forget to add the roles to the permitted list in the controller
class ApplicationController < ActionController::Base
before_filter :configure_permitted_parameters, if: :devise_controller?
protected
def configure_permitted_parameters
devise_parameter_sanitizer.for(:sign_up) {|u| u.permit(:email, :password, :password_confirmation, roles: [])}
end
end
See the section on strong parameters in the Devise documentation.
You can use checkboxes in the view for setting these roles.
<% for role in User::ROLES %>
<%= check_box_tag "user[roles][#{role}]", role, #user.roles.include?(role), {:name => "user[roles][]"}%>
<%= label_tag "user_roles_#{role}", role.humanize %><br />
<% end %>
<%= hidden_field_tag "user[roles][]", "" %>
Finally, you can then add a convenient way to check the user's roles in the Ability class.
# in models/user.rb
def is?(role)
roles.include?(role.to_s)
end
# in models/ability.rb
can :manage, :all if user.is? :admin
See Custom Actions for a way to restrict which users can assign roles to other users.
This functionality has also been extracted into a little gem called role_model (code & howto).
If you do not like this bitmask solution, see Separate Role Model for an alternative way to handle this.
Related
I've currently got a user object but to avoid redundancy, I'd like to wrap it into a presenter object called MerchantUser/ProviderUser. However, with ActiveAdmin, I'm a little confused on how to do this. I've tried using before_create to change the user into the corresponding presenters but in index...do, I'm still seeing that user.class is equal to User and not the wrapper classes that I've defined.
I've looked into scoping_collection but unfortunately that only works on collections and not individual objects?
ActiveAdmin.register User, as: "Companies" do # rubocop:disable Metrics/BlockLength
before_create do |user|
if user.merchant?
user = MerchantUser.new(user)
else
user = ProviderUser.new(user)
end
end
actions :all, except: [:destroy]
permit_params :name, :email, contract_attributes: [:id, :flat_rate, :percentage]
filter :role, as: :select
index do # rubocop:disable Metrics/BlockLength
column :name do |user|
user.name <---I want it so I can just do this without the if/else blocks like below.
end
column :role
column :contact_phone
column :email
column :website do |user|
if user.merchant?
user.company.website
else
user.provider.website
end
end
column :flat_rate do |user|
money_without_cents_and_with_symbol(user.contract.flat_rate)
end
column :percentage do |user|
number_to_percentage(user.contract.percentage, precision: 0)
end
actions
end
Have you looked into Active Admin's support for decorators? This page is quite comprehensive. The best way to implement them depends on how your decorator/presenter object is implemented.
Link summary: use decorate_with or look into using this gem for PORO support
Are you sure you want/need a presenter here? You can register the same Rails model multiple times as ActiveAdmin resources with different names and customizations (filters, index page, forms, etc). You can also use Rails STI or just subclass Rails models, perhaps with different Rails default_scope and then register the subclasses.
I recently created a small rails app using Rails Composer with devise.
I then followed the guide to an admin attribute to the users table using this guide: https://github.com/plataformatec/devise/wiki/How-To:-Add-an-Admin-Role.
I'm struggling with this one thing:
in app/views/layouts/_nav_links_for_auth.html.erb, I have:
<% if user_signed_in? %>
<% if current_user.try(:admin?) %>
<li> <%= link_to 'Users', users_path %></li>
<% end %>
<% end %>
I have set the attribute to true for the user I'm logged in as - however, the statement if current_user.try(:admin?) above doesn't return true. I've doubled checked in the sqlite development database that the attribute is actually set to true, and it is.
So, I'm stuck. As best I can figure out, it doesn't know what actually is an "admin". In my user model, I have this (which was there by default):
class User < ActiveRecord::Base
enum role: [:user, :vip, :admin]
after_initialize :set_default_role, :if => :new_record?
...
end
I'd love some help with getting to this work. What do I need to do?
Why do you have an enum if you have an explicit admin attribute for your User model?
If you're using enum, you'd have a role attribute, which will give you methods such as admin?, or you need to have an explicit admin attribute - which you should have if you have fired the migration that you posted.
To clear up any potential confusion, enum is used if you have strict criteria for a column, and only wish your users to have certain values assigned to it. Roles is a great example:
#app/models/user.rb
class User < ActiveRecord::Base
enum role: [:admin, :user] #-> requires "role" column which will store either :admin (0) or :user (1) values
end
In your case, you have several issues.
Firstly, boolean columns create appropriate methods in your model for that column...
Having admin as a boolean column will give you #user.admin? method.
<%= content_tag :li, link_to('Users', users_path) if user_signed_in? && current_user.admin? %>
This is what you should use if you have the migration as you stated from the Devise link (IE you have an admin boolean column)
--
The second problem you have is that you're getting confused in your model.
IF you wanted to have a default role, and have potential roles defined with an enum, you'd want to use the following:
#app/models/user.rb
class User < ActiveRecord::Base
enum role: [:user, :vip, :admin]
# remove after_init callback
end
You'd set a default role by setting the role default value in your db:
$ rails g migration SetDefaultRole
#db/migrate/set_default_role_______.rb
class SetDefaultRole < ActiveRecord::Migration
def change
add_column :users, :role, :integer, default: 1
end
end
$ rails db:migrate #-> if you're using Rails 5
$ rake db:migrate #-> Rails 4+
You could use either the role enum or the admin attribute but not both.
The problem in your case is that the admin? method being called is the one that is defined by enum (which checks the role) rather than admin? method provided by activerecord for boolean columns.
So though your admin column is true, your role is not being set to admin (for reasons we can not investigate because your set_default_role implementation is not available).
I recommend adopting a role based system, investigating why the correct role is not being set and then getting rid of the admin column.
I would like to use rails new dynamic attr_accessible feature. However each of my user has many roles (i am using declarative authorization). So i have the following in my model:
class Student < ActiveRecord::Base
attr_accessible :first_name, :as=> :admin
end
and i pass this in my controller:
#student.update_attributes(params[:student], :as => user_roles)
user_roles is an array of symbols:
user_roles = [:admin, :employee]
I would like my model to check if one of the symbols in the array matches with the declared attr_accessible. Therefore I avoid any duplication.
For example, given that user_roles =[:admin, :employee]. This works:
#student.update_attributes(params[:student], :as => user_roles.first)
but it is useless if I can only verify one role or symbol because all my users have many roles.
Any help would be greatly appreciated
***************UPDATE************************
You can download an example app here:
https://github.com/jalagrange/roles_test_app
There are 2 examples in this app: Students in which y cannot update any attributes, despite the fact that 'user_roles = [:admin, :student]'; And People in which I can change only the first name because i am using "user_roles.first" in the controller update action. Hope this helps. Im sure somebody else must have this issue.
You can monkey-patch ActiveModel's mass assignment module as follows:
# in config/initializers/mass_assignment_security.rb
module ActiveModel::MassAssignmentSecurity::ClassMethods
def accessible_attributes(roles = :default)
whitelist = ActiveModel::MassAssignmentSecurity::WhiteList.new
Array.wrap(roles).inject(whitelist) do |allowed_attrs, role|
allowed_attrs + accessible_attributes_configs[role].to_a
end
end
end
That way, you can pass an array as the :as option to update_attributes
Note that this probably breaks if accessible_attrs_configs contains a BlackList (from using attr_protected)
I have a Rails 3 App that has needs some user defined settings. I would like to use this https://github.com/ledermann/rails-settings plugin. I have it working in the rails console. But I am having trouble getting working in a form. Do I use fields_for & attr_accessible? If so I am having no luck.
I need to add settings for two Models:
For example, settings that are specific to a User,
user = User.find(123)
user.settings.color = :red
user.settings.color
# => :red
user.settings.all
# => { "color" => :red }
(The above works fine for me in the console.)
but I need to administer them through a standard web form. I'd love to know how others are handling this.
Thanks.
What I did is add dynamic setters/getters to my User class as such
class User < ActiveRecord::Base
has_settings
def self.settings_attr_accessor(*args)
args.each do |method_name|
eval "
def #{method_name}
self.settings.send(:#{method_name})
end
def #{method_name}=(value)
self.settings.send(:#{method_name}=, value)
end
"
end
end
settings_attr_accessor :color, :currency, :time_zone
end
With that, you can use "color" just like any other attribute of your User model.
Also it's very simple to add more settings, just add them to the list
I'm using cancan as my authorization engine.
I already have the roles in user:
ROLES = %w[admin normal author corp]
I also have methods to add and check roles:
#cancan
def roles=(roles)
self.roles_mask = (roles & ROLES).map { |r| 2**ROLES.index(r) }.sum
end
def roles
ROLES.reject do |r|
((roles_mask || 0) & 2**ROLES.index(r)).zero?
end
end
def is?(role)
roles.include?(role.to_s)
end
And I have # roles_mask :integer in the User model.
However, I want to have a after_save :add_normal_role which assigns the normal role to the user.
Basically, I'm not being able (don't know) how to assign roles to each user.
This is what I have, which doesn't work:
private
def add_normal_role
self.roles=(ROLES[1])
end
Thanks
You should try using a before_create callback ensuring the user has a normal role.
The problem with your current callback is since it's an after_save, your modifications aren't saved by default. (Saving in an after_save callback is a bad idea leading to infinite loops...) You could also use a before_save callback (with the same code you already have), which will also work.
However, since you really only need to add a normal role when the object is created (and not on every update), a before_create is better suited.