Rails 3.1 attr_accessible verification receives an array of roles - ruby-on-rails

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)

Related

Rails ActiveAdmin modify resource object

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.

Rails: Validate uniqueness of admin_name based off of user_name column in a different table

I have two tables: admin_users and users. I want all the created names to be unique (a regular user can't create a name already taken by an admin user and vice-versa). I'm having trouble writing a validates_uniqueness_of validation that is able to analyze information in a different table. I think I have to use a scope of some sort, but I tried all sorts of combinations and couldn't get it to work. To be clear: I'm looking for the correct code to replace the question marks below.
validates_uniqueness_of :user_name, :scope => #???Look in admin users
#table and check to make that this name is not taken.
Any help would be very much appreciated. Thanks!
You can create a custom validator for this.
class UserNameValidator < ActiveModel::Validator
def validate(record)
if AdminUser.exists?(user_name: record.user_name)
record.errors[:base] << "An admin user have this username!"
end
end
end
class User < ActiveRecord::Base
validates_with UserNameValidator
end

Where and how to assign a user :admin role for attr_accessible in rails 3.1?

In Rails guide in http://edgeguides.rubyonrails.org/security.html, section 6.1, it introduces a role for attr_accessible with :as option,
attr_accessible :name, :is_admin, :as => :admin
My question is, if a user log in, where and how can I assign the user to :admin role so she/he gets the right to mass assign with attr_accessible? Also can I define my own role such as group_to_update? If it does, what should go into the definition of group_to_update?
Thanks.
You are using some technical terminology in vague ways that is making your understanding of this process muddled, so I'm going to clear up this terminology first.
where and how can I assign the user to :admin role
The 'role' used in the :as parameter to attr_accessible is not a user role. It is an attribute role. It means that attribute is protected from overwriting unless that role is specified in the statement that sets the attribute. So, this system is independent of any user system. Your application doesn't even need to have users to have roles in mass assignment.
can I define my own role such as group_to_update
Roles are not really "defined" in any formal sense at all. In any place that a role is expected, simply use any symbol/string (e.g. :group_to_update) as the role. No need to specify it anywhere else ahead of time.
Here's how it works. Normally, during mass assignment of a hash to model attributes, all of the model's attributes are used as keys to the assigned hash. So if you have a Barn model and barn instance of it, with three attributes horse, cat, and rabbit, then this:
barn.attributes = params
Is essentially the same as doing:
barn.horse = params[:horse]
barn.cat = params[:cat]
barn.rabbit = params[:rabbit]
Now, if you set any attr_accessible on the barn model, only the attributes you set there will be updated when you use mass assignment. Example:
class Barn < ActiveRecord::Base
attr_accessible :cat
attr_accessible :rabbit
end
Then this:
barn.attributes = params
Will only do this:
barn.cat = params[:cat]
barn.rabbit = params[:rabbit]
Because only 'cat' and 'rabbit' are set to accessible ('horse' is not). Now consider setting an attribute role like this:
class Barn < ActiveRecord::Base
attr_accessible :cat
attr_accessible :rabbit, :as => :banana
end
First, note that the the role can by anything you want as long as it is a symbol/string. In this case, I made the role :banana. Now, when you set a role on an attr_accessible attribute, it normally does not got assigned. This:
barn.attributes = params
Will now only do this:
barn.cat = params[:cat]
But you can assign attributes using a specific role by using the assign_attributes method. So you can do:
barn.assign_attributes(params, :as => :banana)
This will assign all normally-protected params as well as all params protected under the role :banana:
barn.cat = params[:cat]
barn.rabbit = params[:rabbit]
So consider a longer example with more attributes:
class Barn < ActiveRecord::Base
attr_accessible :cat
attr_accessible :rabbit, :as => :banana
attr_accessible :horse, :as => :banana
attr_accessible :cow, :as => :happiness
end
Then you can use those roles when assigning attributes. This:
barn.assign_attributes(params, :as => :banana)
Corresponds to:
barn.cat = params[:cat]
barn.rabbit = params[:rabbit]
barn.horse = params[:horse]
And this:
barn.assign_attributes(params, :as => :happiness)
Corresponds to:
barn.cat = params[:cat]
barn.cow = params[:cow]
Now, if you choose to, you can make user roles (e.g. a "role" column on your User model) correspond to attribute roles on any model. So you could do something like this:
barn.assign_attributes(params, :as => user.role)
If this user's role happens to be banana, then (using our last model example) it will set attributes on barn for cat, rabbit, and horse. But this is just one way to use attribute roles. It is entirely up to you if you want to use them a different way.
This is to protect against mass assignment as your link explains.
In rails (for updating) this only affects the update_attributes call. You can still use update_attribute or admin= methods to assign the admin variable.
User.first.update_attributes(:name => "Gazler", :admin => true) #this will not work
User.first.update_attribute(:admin, true) #This will work
#This will also work
user = User.first
user.admin = true
user.save
You might want to take a look at using a gem for your permissions. Cancan is probably the most common.
Look at the assign_attributes method.
In short, it enables you to asign the attributes only when you also pass the role. The docs have very nice and easily understandable code examples. In a way, it works like kind of a filter, or guard.

How do I create custom "association methods" in Rails 3?

I've read this article, but it's for Rails 1.x.
I'd really like to create my own association methods:
user = User.find(1)
# Example of a normal association method
user.replies.create(:body => 'very informative. plz check out my site.')
# My association method
user.replies.find_by_spamminess(:likelihood => :very)
In Rails 3, what's the proper way of doing this?
The Rails 3 way of doing things is often to not use find methods, but rather scopes, which delays the actual database call until you start iterating over the collection.
Guessing at your first example, I would do:
in class Reply ...
scope :spaminess, lambda {|s| where(:likelyhood => s) }
and then using it:
spammy_messages = user.replies.spaminess(:very)
or to use it in a view
spammy_messages.each do |reply|
....
end
I think I found it!
If you search for "association extensions" the Rails API page for ActiveRecord::Assications, you'll see that this is the syntax (copied from that link):
class Account < ActiveRecord::Base
has_many :people do
def find_or_create_by_name(name)
first_name, last_name = name.split(" ", 2)
find_or_create_by_first_name_and_last_name(first_name, last_name)
end
end
end

MongoMapper has_many association

I have problem with mongomapper associations. I have one class names User and other named Model. User has many models but...
user = User.first
=> <User ...
user.models
=> []
Model.find_by_user_id(user.id.to_s)
=> <Model ...
Model.find_by_user_id(user.id.to_s).user == user
=> true
Class code (simplified):
class User
include MongoMapper::Document
# some keys definition
many :models
end
class Model
include MongoMapper::Document
# some keys definitions
belongs_to :user
end
What I am doing wrong?
It appears that MM no longer uses String format for the FK column, so
Model.find_by_user_id(user.id.to_s)
should be
Model.find_by_user_id(user.id)
Furthermore, the datatype of the Model.user_id column should be set to
key :user_id, Mongo::ObjectID
When I ran into this problem, I had to delete and recreate my collection to get it to work- in other words I used to have user_id as a String, but it would only "take" when I switched it when I rebuilt my database. Luckily I am working with test data so that was easy enough.
What kind of errors or exceptions are you getting? The code you posted looks fine.
ah, this is poorly documented in the mm docs. You need to do this here:
class User
include MongoMapper::Document
# some keys definition
many :models, :in => :model_ids
end
class Model
include MongoMapper::Document
# some keys definitions
# no belongs_to necessary here
end
You can then add models to your user via:
# use an existing object
u = User.create ...
m = Model.create ...
# and add the model to the user
u.models << m
# don't forget to save
u.save
# you can then check if it worked like so:
# u.model_ids => [ BSON::ID 'your user id']
Hope that helped.

Resources