How to get ActiveAdmin to work with Strong Parameters? - ruby-on-rails

Update: this question was asked before there was a solution for it already in ActiveAdmin. As Joseph states, the ActiveAdmin documentation now contains this information, but the answers here are provided for those working with older versions of ActiveAdmin.
When the strong_parameters 0.1.4 is used with ActiveAdmin 0.5.0 in Rails 3.2.8, if the model you are using is using StrongParameters by including:
include ::ActiveModel::ForbiddenAttributesProtection
then you get the following error in the log if you try to create/edit a record:
ActiveModel::ForbiddenAttributes (ActiveModel::ForbiddenAttributes)

Update to the latest inherited_resources gem and do this in your controller block:
ActiveAdmin.register Blog do
#...
controller do
#...
def permitted_params
params.permit(:blog => [:name, :description])
# params.permit! # allow all parameters
end
end
end

The documentation now clearly states how to go about Setting up Strong Parameters in Rails 4. See:
https://github.com/gregbell/active_admin/blob/master/docs/2-resource-customization.md#setting-up-strong-parameters

The accepted answer did not work for me with resources defined in an engine, so I tracked down the original resource_params in inherited_resources/lib/inherited_resources/base_helpers.rb and came up with this solution which closer mimics that code, and which works with engines:
In config/initializers/active_admin.rb:
ActiveAdmin::ResourceController.class_eval do
# Allow ActiveAdmin admins to freely mass-assign when using strong_parameters
def resource_params
[(params[resource_request_name] || params[resource_instance_name]).try(:permit!) || {}]
end
end

in your config/initializers/active_admin.rb
config.before_filter do
params.permit!
end

Update: See #Brendon-Muir's answer for latest way to do this. The following information was correct previously, so I'll leave it here in case it helps others with an older version of ActiveAdmin.
A patch had been proposed in a google group thread:
https://groups.google.com/forum/?fromgroups=#!topic/activeadmin/XD3W9QNbB8I
Then was being put together here:
https://github.com/gregbell/active_admin/issues/1731
But for now, the least invasive way to add strong parameters support to ActiveAdmin in your app is to redefine resource_params in your controller block, either via the "permit all params" method, which is less secure:
controller do
def resource_params
return [] if request.get?
[ params[active_admin_config.resource_class.name.underscore.to_sym].permit! ]
end
end
or the more secure explicit way:
controller do
def resource_params
return [] if request.get?
[ params.require(:name_of_model).permit(:each,:param,:goes,:here,:if,:you,:want) ]
end
end
See Active Admin docs on modifying controllers:
http://activeadmin.info/docs/8-custom-actions.html#modify_the_controller

You can also use permit_params as follows:
ActiveAdmin.register Resource do
permit_params do
%i(first_name last_name)
end
index pagination_total: false do
column :id
column :first_name
column :last_name
actions
end
end

Related

Allowing additional parameters for admin accounts

I have a number of models with attributes that ordinary users should not be able to change, but admins should. For example (though this is not my problem domain), normal users should not be able to change a Post's user_id, but administrators should be allowed to do so.
Handling this at the view level is simple enough—I can show or not show fields depending on whether the user is an administrator—but I'm not sure how to handle it in the controller's strong parameter handling. The only solution I can come up with (and the solution offered previously) is to Repeat Yourself, something you try to Don't in Rails:
def post_params
if admin?
params.require(:post).permit(:title, :text, :date, :user_id)
else
params.require(:post).permit(:title, :text, :date)
end
end
Is there a better way to handle this?
I don't see anything wrong with your current implementation. That being said, if you wanted to re-use these attribute permissions in a different controller (e.g. an Api::PostsConrtoller), one way to DRY it up would be to extract the code into it's own class. This is the approach Ryan Bates used in the Railscast about Strong Parameters (note: requires Pro account).
# app/models/permitted_params.rb
class PermittedParams < Struct.new(:params, :user)
def post
if user && user.admin?
params.require(:post).permit(:title, :text, :date, :user_id)
else
params.require(:post).permit(:title, :text, :date)
end
end
end
You can then instantiate this class from within the ApplicationController
# app/controllers/application_controller.rb
def permitted_params
#permitted_params ||= PermittedParams.new(params, current_user)
end
and then use it in any controller where you need that permission logic without duplicating the logic.
# app/controllers/posts_controller.rb
def update
#post = Post.find(params[:id])
if #post.update_attributes(permitted_params.post)
...
else
...
end
end
What's really nice about this solution is that you can also use it to DRY-up your views by slightly modifying the PermittedParams class.
# app/models/permitted_params.rb
class PermittedParams < Struct.new(:params, :user)
def post
params.require(:post).permit(*post_attributes)
end
def post_attributes
if user && user.admin?
[:title, :text, :date, :user_id]
else
[:title, :text, :date]
end
end
end
and exposing the permitted_params method as a view helper.
# app/controllers/application_controller.rb
def permitted_params
#permitted_params ||= PermittedParams.new(params, current_user)
end
helper_method :permitted_params
Finally, use it within your view to show/hide the form fields.
# app/views/posts/edit.html.erb
<% if permitted_params.post_attributes.include? :user_id %>
# show the user_id field
<% end %>
You don't need to go for two different params list just to prevent bad users.
Rather you should consider using cancan Gem.
Keep using the single params list uniformly(irrespective of User role).
Upon that to restrict/protect your actions (based on user role) , try using cancan gem.
Define your individual roles authorizations in the ability.rb file.
A sample ability file will look like :
#ability.rb
class Ability
include CanCan::Ability
def initialize(user)
user ||= User.new
if user.is?(:Administrator) # Access for Admin user
can :access, :all
elsif user.is?(:"Finance Manager") #Access for Finance Manager
can :access, [:subjects, :researchsubjects, :visits, :sessions]
elsif user.is?(:"Research Director") # Access for Research Director
else
#do something
end
end
end
Please check, cancan gem for more info.
I wasn't really happy with any of the previous answers. What I really wanted to do was this:
params.require(:post).permit(:title, :text, :date, user_id: admin?)
So I decided to make that possible. First I tried to patch the Rails core, but they weren't interested in accepting the patch:
This API is delicate, there are several directions in which it could evolve and it can get easily out of hand, inconsistent, or create expectations for extending the extensions... We prefer by now to keep it as it is.
In general, we prefer use cases not directly supported by the API to be addressed by regular programming. In this case, it would be
pkeys = [:title, :body]
pkeys << :author_id if admin?
params.require(:post).permit(*pkeys)
or something in that line (you already knew that was possible of course, but just to illustrate the point with an example).
So I turned it into a gem instead, and I'll have it for as long as I want to keep it. It's a shame this feature won't be part of every future Rails app I write, but at least I'll be able to get it from my Gemfile.
gem 'rails_conditional_params'

ActiveAdmin with friendly id

I am using friendly_id in my rails 4 application with slug. Now I am using active_admin gem.
Problem:
When I click on show link from active admin for Group resource, It is throwing the following exception:
ActiveRecord::RecordNotFound at /admin/groups/username20-s-group-1
I guess, I need to override some of the active_admin default functions?
There are cases, when application has quit a few resources, hence in order to keep it DRY there is a nice solution requiring few lines of code for whole application - simply override activeadmin's resource controller.
Create config/initializers/active_admin_monkey_patching.rb file with the following content:
ActiveAdmin::ResourceController.class_eval do
def find_resource
finder = resource_class.is_a?(FriendlyId) ? :slug : :id
scoped_collection.find_by(finder => params[:id])
end
end
Do not forget to restart the server.
A better approach to #AndreyDeineko's is to override ActiveAdmin::ResourceController's find_resource method in config/initialisers/active_admin.rb and leverage the methods provided by FriendlyId (5.x at this point):
In config/initialisers/active_admin.rb:
ActiveAdmin.setup do |config|
# == Friendly Id addon
ActiveAdmin::ResourceController.class_eval do
def find_resource
if resource_class.is_a?(FriendlyId)
scoped_collection.friendly.find(params[:id])
else
scoped_collection.find(params[:id])
end
end
end
# initial config
end
This looks much cleaner to me, than putting it in the application controller, as it is related to the configuration of Active Admin.
Found solution for the problem:
In your app/admin/[ResourceName.rb] add:
# app/admin/group.rb
# find record with slug(friendly_id)
controller do
def find_resource
begin
scoped_collection.where(slug: params[:id]).first!
rescue ActiveRecord::RecordNotFound
scoped_collection.find(params[:id])
end
end
end
This solved my problem.
class User < ActiveRecord::Base
extend FriendlyId
friendly_id :username, :use => [:slugged, :finders]
IMHO, it's suboptimal to completely override the find_resource as most of the answers suggest. Better to prepend a module and call super preserving normal behaviour when FriendlyId is not in use. For reference you can check how this method is currently (as of writing) implemented, it is not simply scoped_collection.find(params[:id]) as one might think:
https://github.com/activeadmin/activeadmin/blob/b45b1fb05af9a7f6c5e2be94f61cf4a5f60ff3bb/lib/active_admin/resource_controller/data_access.rb#L104
module ActiveAdminFriendlyIdScoping
def find_resource
if resource_class.is_a? FriendlyId
scoped_collection.friendly.find params[:id]
# Or potentially even
# scoped_collection.friendly.send method_for_find, params[:id]
# Or you could do something like this
# raise "Using FriendlyId, find method configuration ignored" if method_for_find != :find
else
super
end
end
end
ActiveAdmin.setup do |config|
#...
Rails.application.config.to_prepare do
ActiveAdmin::ResourceController.prepend(ActiveAdminFriendlyIdScoping)
end
end
If you've tried some of the other answers here and gotten
uninitialized constant InheritedResources::Base (NameError)
Then you might consider monkey patching FriendlyId rather than ActiveAdmin. Create a new initializer file config/initializers/friendly_id_monkey_patch.rb containing this:
module FriendlyIdModelMonkeyPatch
def to_param
if caller.to_s.include? 'active_admin'
id&.to_s
else
super
end
end
end
module FriendlyId::Model
prepend FriendlyIdModelMonkeyPatch
end
Now all of your FriendlyId models will revert to using their ID in ActiveAdmin and their slug everywhere else.
See also this answer, which does the same thing but for only one model (rather than monkey patching for all FriendlyId models)

Forbidden Attributes Error in Rails 4 when encountering a situation where one would have used attr_accessible in earlier versions of Rails

With the recent upgrade to Rails 4, updating attributes using code resembling the below does not work, I get a ActiveModel::ForbiddenAttributes error:
#user.update_attributes(params[:user], :as => :admin)
Where User has the following attr_accessible line in the model:
attr_accessible :role_ids, :as =>admin
# or any attribute other than :role_ids contained within :user
How do you accomplish the same task in Rails 4?
Rails 4 now has features from the strong_parameters gem built in by default.
One no longer has to make calls :as => :admin, nor do you need the attr_accessible :user_attribute, :as => admin in your model. The reason for this is that, by default, rails apps now have 'security' for every attribute on models. You have to permit the attribute you want to access / modify.
All you need to do now is call permit during update_attributes:
#user.update_attributes(params[:user], permit[:user_attribute])
or, to be more precise:
#user.update_attributes(params[:user].permit(:role_ids))
This single line, however, allows any user to modify the permitted role. You have to remember to only allow access to this action by an administrator or any other desired role through another filter such as the following:
authorize! :update, #user, :message => 'Not authorized as an administrator.'
. . . which would work if you're using Devise and CanCan for authentication and authorization.
If you create a new Rails 4 site you'll notice that generated controllers now include a private method which you use to receive your sanitised params. This is a nice idiom, and looks something like this:
private
def user_params
params.require(:user).permit(:username, :email, :password)
end
The old way of allowing mass assignment was to use something like:
attr_accessible :username, :email, :password
on your model to mark certain parameters as accessible.
Upgrading
To upgrade you have several options. Your best solution would be to refactor your controllers with a params method. This might be more work than you have time for right now though.
Protected_attributes gem
The alternative would be to use the protected_attributes gem which reinstates the attr_accessible method. This makes for a slightly smoother upgrade path with one major caveat.
Major Caveat
In Rails 3 any model without an attr_accessible call allowed all attributes though.
In Rails 4 with the protected_attributes gem this behaviour is reversed. Any model without an attr_accessible call has all attributes restricted. You must now declare attr_accessible on all your models. This means, if you haven't been using attr_accessible, you'll need to add this to all your models, which may be as much work as just creating a params method.
https://github.com/rails/protected_attributes
This problem might also be caused by the Cancan gem
Just add to application_controller.rb
before_filter do
resource = controller_name.singularize.to_sym
method = "#{resource}_params"
params[resource] &&= send(method) if respond_to?(method, true)
end
Works without any further modifications of code
got it from here: https://github.com/ryanb/cancan/issues/835#issuecomment-18663815
Don't forget to add your new user_params method to the controller action:
def create
#user = User.new(user_params)
#user.save
redirect_to 'wherever'
end
def create
#user = User.create(user_params)
....
end
def update
#user = User.find(params[:id])
if #user.update_attributes(blog_params)
redirect_to home_path, notice: "Your profile has been successfully updated."
else
render action: "edit"
end
end
private
def user_params
params.require(:user).permit(:name, :age, :others)
end

How do I make some attributes accessible only to Users that have a specific role?

In my User model, I have this:
attr_accessible :role_ids, :as => :admin
But that doesn't seem to work.
I want this particular attribute to be only accessible if the current_user.can?(:edit, Role) - i.e. only users with a role of admin or superadmin should be able to access those attributes.
How do I do this?
Edit 1: I am using Devise, CanCan & Rolify.
Like I said, we think the best way to restrict attributes to certain roles is to use the strong parameters gem that DHH introduced recently. This will also be part of Rails4 thankfully.
Even if it doesn't fit, it's a good idea to start integrating the principles as it will make your Rails 3 to 4 upgrade easier.
If you have a Railscasts Pro membership, Ryan Bates has made another fantastic tutorial on it.
In brief, here's what's recommended in that Railscast.
After installing the gem, remove attr_accessible from your model.
Add this to an initializer:
ActiveRecord::Base.send(:include, ActiveModel::ForbiddenAttributesProtection)
Alter your update action in your controller::
def update
#topic = Topic.find(params[:id])
if #topic.update_attributes(topic_params)
redirect_to topics_url, notice: "Updated topic."
else
render :edit
end
end
Create a private method in your controller:
def topic_params
if current_user && current_user.admin?
params[:topic].permit(:name, :sticky)
else
params[:topic].permit(:name)
end
end
In your situation, like we do, you'd have to change the topic_params method to use your roles.
There's a few more suggestions in the RailsCast and it's really worth the $9! (I'm in no way affiliated with the site, it's just proven invaluable to us)
Let me know if this helps.
in my user model
class User
attr_accessible :nickname, as: :admin
end
in my rails console
User.first.update_attributes!({nickname: "uschi"})
# WARNING: Can't mass-assign protected attributes: nickname
User.first.update_attributes!({nickname: "uschi"}, {as: :admin})
# true
UPDATE
maybe i am to stupid to get your problem, but just do a
if current_user.can?(:edit, Role)
user.update_attributes!({nickname: "uschi"}, {as: :admin})
else
first.update_attributes!({nickname: "uschi"})
end
and there are probably smarter ways of doing this...

ActiveRecord::ReadOnlyRecord when using ActiveAdmin and Friendly_id

I started using ActiveAdmin recently in a project and almost everything works great but I'm having a problem when using it in combination with the friendly_id gem. I'm getting ActiveRecord::ReadOnlyRecord thrown for my forms [i believe] because of the friendly_id attribute whose ID is readonly:
{"utf8"=>"✓",
"_method"=>"put",
"authenticity_token"=>"Rc5PmUYZt3BiLvfPQr8iCPPXlbfgjoe/n+NhCwXazNs=",
"space"=>{"name"=>"The Kosmonaut",
"address"=>"8 Sichovykh Striltsiv 24",
"email"=>"info#somedomain.com"},
"commit"=>"Update Space",
"id"=>"the-kosmonaut"} <--- culprit
I'm guessing the last line is the culprit as it's a readonly attribute, it's not in my form but rather in the PATH
http://localhost:5000/manage/spaces/the-kosmonaut/edit
How can I fix this from trying to update the ID?
Form from in ActiveAdmin looks like this:
form do |f|
f.inputs "Details" do
f.input :name
f.input :address
f.input :email
f.input :phone
f.input :website
end
f.inputs "Content" do
f.input :description
f.input :blurb
end
f.buttons
end
UPDATE: This doesn't work either so it's not the friendly_id?
I tried using #watson's suggestion which should have worked but still got the same error ;-(
{"utf8"=>"✓",
"_method"=>"put",
"authenticity_token"=>"Rc5PmUYZt3BiLvfPQr8iCPPXlbfgjoe/n+NhCwXazNs=",
"space"=>{"name"=>"The Kosmonaut 23"},
"commit"=>"Update Space",
"id"=>"6933"}
http://localhost:5000/manage/spaces/6933/edit
When I check the record in the console with record.readonly? it returns false
UPDATE UPDATE: removing the scope_to fixes the problem.
scope_to :current_user, :unless => proc{ current_user.admin? }
Only problem is I need the scope_to to prevent users from seeing records they do not own. My guess is (as I'm assuming scope_to normally works with has_many) that my HABTM association causes some weirdness? Ie Users <-- HABTM --> Spaces?
If you only want friendly ID's in the front end and don't care about them inside Active Admin, you can revert the effects of the friendly_id gem for your Active Admin controllers.
I don't know exactly how friendly_id overrides the to_param method, but if it's doing it the normal way, re-overriding it inside all of your Active Admin controllers should fix it, e.g.:
ActiveAdmin.register Foobar do
before_filter do
Foobar.class_eval do
def to_param
id.to_s
end
end
end
end
Even better you could create a before filter in the base Active Admin controller ActiveAdmin::ResourceController so that it is automatically inherited into all your Active Admin controllers.
First add the filter to the config/initializers/active_admin.rb setup:
ActiveAdmin.setup do |config|
# ...
config.before_filter :revert_friendly_id
end
The open up ActiveAdmin::ResourceController and add a revert_friendly_id method, E.g. by adding the following to the end of config/initializers/active_admin.rb:
ActiveAdmin::ResourceController.class_eval do
protected
def revert_friendly_id
model_name = self.class.name.match(/::(.*)Controller$/)[1].singularize
# Will throw a NameError if the class does not exist
Module.const_get model_name
eval(model_name).class_eval do
def to_param
id.to_s
end
end
rescue NameError
end
end
Update: I just updated the last code example to handle controllers with no related model (e.g. the Active Admin Dashboard controller)
Update 2: I just tried using the above hack together with the friendly_id gem and it seems to work just fine :)
Update 3: Cleaned up the code to use the standard way of adding Active Admin before filters to the base controller
You can customize the resource retrieval according to http://activeadmin.info/docs/2-resource-customization.html#customizing_resource_retrieval. Note that you want to use the find_resource method instead of resource, or you won't be able to create new records.
(Check https://github.com/gregbell/active_admin/blob/master/lib/active_admin/resource_controller/data_access.rb for more details)
In your ActiveAdmin resource class write:
controller do
def find_resource
scoped_collection.where(slug: params[:id]).first!
end
end
You can also overwrite the behavior for all resources by modyfing the ResourceController in the active_admin.rb initializer.
ActiveAdmin::ResourceController.class_eval do
def find_resource
if scoped_collection.is_a? FriendlyId
scoped_collection.where(slug: params[:id]).first!
else
scoped_collection.where(id: params[:id]).first!
end
end
end
Hope that helps!
Side note: When creating new records through the admin interface make sure you don't include the slug field in the form, or FriendlyId will not generate the slugs. (I believe that's for version 5+ only)
This method works for me. add this code in app/admin/model_name.rb
ActiveAdmin.register model_name do
controller do
defaults finder: :find_by_slug
end
end
To add to Denny's solution, a more "friendly" solution would be to use friendly_id's finders.
controller do
def find_resource
scoped_collection.friendly.find_by_friendly_id(params[:id])
end
end
Source
Here is my solution based on #Thomas solution
ActiveAdmin.setup do |config|
# ...
config.before_filter :revert_friendly_id, :if => -> { !devise_controller? && resource_controller? }
end
# override #to_param method defined in model in order to make AA generate
# routes like /admin/page/:id/edit
ActiveAdmin::BaseController.class_eval do
protected
def resource_controller?
self.class.superclass.name == "ActiveAdmin::ResourceController"
end
def revert_friendly_id
model_name = self.class.name.match(/::(.*)Controller$/)[1].singularize
# Will throw a NameError if the class does not exist
Module.const_get model_name
eval(model_name).class_eval do
def to_param
id.to_s
end
end
rescue NameError
end
end

Resources