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'
Related
I'm trying to create a hypermedia api in rails. I'd like to serialize my payloads with active_model_serializers using the json_api adapter. But it doesn't seem trivial to serialize links conditionaly.
It's kind of a blog application where users can follow other users. So when I serialize a User resource, say for UserA, I want to have a link with rel :follow if current_user is not following UserA and a link with rel :unfollow if current_user is already following UserA.
This seems like an extremely trivial use case when creating a hypermedia api. Does anyone know if there's any good way of doing this with active_model_serializers?
I currently wrote something like this (and include it in all serializers):
def self.link(rel, &block)
serializer = self
super do
user = scope
next unless serializer.can?(user, rel, #object)
instance_eval(&block)
end
end
# And in serializer (just as usual):
link :self do
api_user_path(object.id)
end
It does work. But it just don't feel right. And I wouldn't be surprised if future changes to active_model_serializers screw things up for me.
If someone else is looking for a solution to this here is what I did. I added the gem Pundit and made the Policy classes in charge of link serialization (as well as the usual authorization) by adding methods called "link_#{rel}". I created a base serializer like this:
module Api
class BaseSerializer < ActiveModel::Serializer
include Pundit
def self.link(rel, &block)
unless block_given?
Rails.logger.warn "Link without block (rel '#{rel}'), no authorization check"
return super
end
method = "link_#{rel}"
# We need to let the super class handle the evaluation since
# we don't have the object here in the class method. This block
# will be evalutated with instance_eval in the adapter (which has
# the object to be serialized)
super do
policy_class = PolicyFinder.new(object).policy
unless policy_class
Rails.logger.warn "Could not find policy class for #{object.class}."
next
end
user = scope
policy = policy_class.new(user, object)
unless policy.respond_to?(method)
Rails.logger.warn "Serialization of #{object.class} infers link with rel '#{rel}'. " \
"But no method '#{method}' in #{policy.class}."
next
end
next unless policy.public_send(method)
instance_eval(&block)
end
end
end
end
Then other serializers inherit from BaseSerializer, like:
module Api
class UserSerializer < BaseSerializer
type 'user'
attributes :name,
:email,
:followers_count,
:following_count,
:created_at,
:updated_at
link :self do
api_user_url(object)
end
link :edit do
api_user_url(object)
end
link :follow do
follow_api_user_url(object)
end
link :unfollow do
unfollow_api_user_url(object)
end
end
end
So the Policies are just like normal Pundit Policies with some added methods for each link that should be serialized (or not).
class ApplicationPolicy
attr_reader :user, :record
def initialize(user, record)
#user = user
#record = record
end
def link_self
true
end
end
module Api
class UserPolicy < ApplicationPolicy
alias current_user user
alias user record
def link_edit
current_user && current_user.id == user.id
end
# show follow link if user is not current_user and
# current_user is not already following user
def link_follow
current_user && current_user.id != user.id && !current_user.following?(user)
end
# show follow link if user is not current_user and
# current_user is following user
def link_unfollow
current_user && current_user.id != user.id && current_user.following?(user)
end
end
end
while doing admin work, i'd like to disable user logins --
is there some way to use devise for this -- I don't THINK this
is suitable for rolify -- because this is a temporary disablement --
thanks in advance for any help,
rick
Back-End
If you wanted to create a "maintenance" mode, you'll be best doing something like this:
#app/models/user.rb
class User < ActiveRecord::Base
end
#app/models/admin.rb
class Admin < User
def maintainance!
self.toggle! :maintainance
end
end
This will need a maintenance column in the users table, and you'll have to add a type column in the users table, too.
You could get away with keeping this in the User model, however, you'd need some conditions to determine whether the user is an admin. Since you didn't specify how you're differentiating, above is how we do it.
--
You'd be able to call it like this:
#app/controllers/users_controller.rb
class SettingsController < ApplicationController
before_action :authenticate_user!
def maintenance
current_user.maintenance! #-> toggles so you'll just be able to call this as you need.
end
end
#config/routes.rb
resources :settings, only: [] do
put :maintenance #-> url.com/settings/maintenance (considering current_user present)
end
This will allow you to set the "maintenance" mode through your user settings area. If you don't have one, you'll be able to use the above code to get it working.
Front-End
With the backend in place, you'll be able to then manage the front-end.
To do this, you'll need a helper to determine if any user has set the "maintenance" mode...
#app/helpers/application_helper.rb
class ApplicationHelper
def maintenance_mode?
Admin.exists? maintenance: true
end
end
This will allow you to use this helper to determine whether you should allow Devise to accept logins or not:
#app/views/devise/sessions/new.html.erb
<% unless maintenance_mode? %>
... devise form ...
<% end %>
The helper will execute a DB request, but keeping it in the devise areas only (IE it's not "site wide") should make it okay.
#app/controllers/devise/sessions_controller.rb
class SessionsController < Devise::SessionsController
before_action :check_maintenance
private
def check_maintenance
redirect_to root_path, notice: "Sorry, maintenance mode is in effect; no logins." if maintenance_mode?
end
end
This will prevent any controller-based actions from firing.
Finally, if you want to get rid of any logged-in users, you'll need to do something quirky, like resetting the sessions or something similar:
How can I reset all devise sessions so every user has to login again?
Devise force signout
Here's what I'd do:
1. Create a method for your User model. It could be something like active, or able_to_login.
2. Set this attribute to :boolean.
3. Use rails console. Use the console to set the active method to true or false, enabling or disabling your users to access your application:
user = User.all
user.each do |u|
u.active = false # or
u.able_to_login = false
u.save
end
I don't think this is the best method, but it should work without installing another gem or heavy code.
In your /models/user.rb add this method
def active_for_authentication?
super && is_admin?
end
def is_admin?
# returns true if user is admin
end
This is the "Devise way" of doing this :)
So I apologize for how noobish these questions may seem. I'm new to rails and as a first task I also brought in Neo4J as it seemed like the best fit if I grow the project.
I'll explain the flow of actions then show some example code. I'm trying to add in step 3-5 now.
User logs in via FB
The first login creates a user node. If the user exist, it simply retrieves that user+node
After the user node is created, the koala gem is used to access the FB Graph API
Retrieves friendlist of each friend using the app.
Go through each friend and add a two way friendship relationship between the two users
As 3-5 only needs to happen when the user first joins, I thought I could do this in a method associated with after_save callback. There is a flaw to this logic though as I will need to update the user at some point with additional attributes and it will call after_save again. Can I prevent this from occurring with update?
SessionsController for reference
def create
user = User.from_omniauth(env["omniauth.auth"])
session[:user_id] = user.id
redirect_to root_url
end
def destroy
session.delete(:user_id)
redirect_to root_path
end
So in my user.rb I have something like this
has_many :both, :friendships
after_save :check_friends
def self.from_omniauth(auth)
#user = User.where(auth.slice(:provider, :uid)).first
unless #user
#user = User.new
# assign a bunch of attributes to #user
#user.save!
end
return #user
end
def facebook
#facebook ||= Koala::Facebook::API.new(oauth_token)
block_given? ? yield(#facebook) : #facebook
rescue Koala::Facebook::APIError => e
logger.info e.to_s
nil
end
def friends_count
facebook { |fb| fb.get_connection("me", "friends", summary: {}) }
end
def check_friends(friendships)
facebook.get_connection("me", "friends").each do |friend|
friend_id = friend["id"]
friend_node = User.where(friend_id)
Friendship.create_friendship(user,friend_node)
return true
end
end
friendship.rb
from_class User
to_class User
type 'friendship'
def self.create_friendship(user,friend_node)
friendship = Friendship.create(from_node: user, to_node: friend_node)
end
I'm not sure if I'm on the right track with how to create a relationship node. As I just created #user, how do I incorporate that into my check_friends method and retrieve the user and friend node so properly so I can link the two together.
Right now it doesn't know that user and friend_user are nodes
If you see other bad code practice, please let me know!
In advance: Thanks for the help #subvertallchris. I'm sure you will be answering lots of my questions like this one.
This is a really great question! I think that you're on the right track but there are a few things you can change.
First, you need to adjust that has_many method. Your associations always need to terminate at a node, not ActiveRel classes, so you need to rewrite that as something like this:
has_many :both, :friends, model_class: 'User', rel_class: 'Friendship'
You'll run into some problems otherwise.
You may want to consider renaming your relationship type in the interest of Neo4j stylistic consistency. I have a lot of bad examples out there, so sorry if I gave you bad ideas. FRIENDS_WITH would be a better relationship name.
As for handling your big problem, there's a lot you can do here.
EDIT! Crap, I forgot the most important part! Ditch that after_save callback and make the load existing/create new user behavior two methods.
class SessionsController < ApplicationController
def create
user = User.from_omniauth(env["omniauth.auth"])
#user = user.nil? ? User.create_from_omniauth(env["omniauth.auth"]) : user
session[:user_id] = #user.id
redirect_to root_url
end
def destroy
session.delete(:user_id)
redirect_to root_path
end
end
class User
include Neo4j::ActiveNode
# lots of other properties
has_many :both, :friends, model_class: 'User', rel_class: 'Friendship'
def self.from_omniauth(auth)
User.where(auth.slice(:provider, :uid)).limit(1).first
end
def self.create_from_omniauth(auth)
user = User.new
# assign a bunch of attributes to user
if user.save!
user.check_friends
else
# raise an error -- your user was neither found nor created
end
user
end
# more stuff
end
That'll solve your problem with getting it started. You may want to wrap the whole thing in a transaction, so read about that in the wiki.
But we're not done. Let's look at your original check_friends:
def check_friends(friendships)
facebook.get_connection("me", "friends").each do |friend|
friend_id = friend["id"]
friend_node = User.where(friend_id)
Friendship.create_friendship(user,friend_node)
return true
end
end
You're not actually passing it an argument, so get rid of that. Also, if you know you're only looking for a single node, use find_by. I'm going to assume there's a facebook_id property on each user.
def check_friends
facebook.get_connection("me", "friends").each do |friend|
friend_node = User.find_by(facebook_id: friend["id"])
Friendship.create_friendship(user,friend_node) unless friend_node.blank?
end
end
The create_friendship method should should return true or false, so just make that the last statement of the method does that and you can return whatever it returns. That's as easy as this:
def self.create_friendship(user, friend_node)
Friendship.new(from_node: user, to_node: friend_node).save
end
create does not return true or false, it returns the resultant object, so chaining save to your new object will get you what you want. You don't need to set a variable there unless you plan on using it more within the method.
At this point, you can easily add an after_create callback to your ActiveRel model that will do something on from_node, which is always the User you just created. You can update the user's properties however you need to from there. Controlling this sort of behavior is exactly why ActiveRel exists.
I'd probably rework it a bit more, still. Start by moving your facebook stuff into a module. It'll keep your User model cleaner and more focused.
# models/concerns/facebook.rb
module Facebook
extend ActiveSupport::Concern
def facebook
#facebook ||= Koala::Facebook::API.new(oauth_token)
block_given? ? yield(#facebook) : #facebook
rescue Koala::Facebook::APIError => e
logger.info e.to_s
nil
end
def friends_count
facebook { |fb| fb.get_connection("me", "friends", summary: {}) }
end
end
# now back in User...
class User
include Neo4j::ActiveNode
include Facebook
# more code...
end
It's really easy for your models to become these messy grab bags. A lot of blogs will encourage this. Fight the urge!
This should be a good start. Let me know if you have any questions or if I screwed anything up, there's a lot of code and it's possible I may need to clarify or tweak some of it. Hope it helps, though.
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
I have a Post model with a :published attribute (boolean) and a User model with a role attribute (string). There are three roles: ROLES = %w[admin publisher author]
I don't want users whose role is author to be capable of setting, or editing, the :published field on the Post model.
I'm using CanCan (and RailsAdmin gem) and my simplified Ability.rb file looks like this:
class Ability
include CanCan::Ability
def initialize(user)
user ||= User.new
if user.role? :admin
can :manage, :all
elsif user.role? :publisher
can :manage, Post
elsif user.role? :author
# I want to prevent these guys from setting the :published attribute
end
end
end
Anyone got any tips for doing this sort of thing?
So far it is not possible. But according to this: https://github.com/ryanb/cancan/issues/326 this feature should be in cancan 2.0.
Update: you can see this on CanCan 2.0 branch here: https://github.com/ryanb/cancan/tree/2.0 in section "Resource Attributes"
Check out this post: How do I use CanCan with rails admin to check for ownership
It shows how to make a field not visible based off a users role.
UPDATE
I was able to set options in rails admin with this code:
config.model User do
edit do
configure :organization do
visible do
bindings[:view]._current_user.max_role_name != 'admin' ? false : true
end
end
configure :organization_id, :hidden do
visible do
true if bindings[:view]._current_user.max_role_name != 'admin'
end
default_value do
bindings[:view]._current_user.organization_id if bindings[:view]._current_user.max_role_name != 'admin'
end
end
include_all_fields
end
end
This configuration will hide the organization field if the logged in user is not an admin. It will then show an organization_id field ( set to type='hidden' ) and set the default value.
Hope this helps someone.
Until CanCan 2.0 comes out, I've solved this by creating a subclass of the model with restricted accessibility, something like:
class AuthorPost < Post
attr_protected :published
end
And then give authors access to AuthorPosts: can :manage => AuthorPost
Then in your controller, you can set the resource you want in a before_filter:
before_filter :set_resource
...
private
def set_resource
if current_user and current_user.author?
#resource = AuthorPost
else
#resource = Post
end
params[:post] ||= params[:author_post]
end
One last caveat: you won't be able to use load_and_authorize_resource in that controller. You'll have to do that manually, as detailed here: https://github.com/ryanb/cancan/wiki/Controller-Authorization-Example
You'll need to replace Project with #resource.
I'm on the fence as to whether this is more or less effective than the method described in the railscast. For my purposes, it left the original model totally intact, so my other code wasn't affected--and just allowed me to give some users fewer editable fields.
There is a way, I did something like this in my project. But CanCan is not entirely the answer. What you need to do is make attr_accessible in your model dynamic based on user role, so if you're an admin, then you're allowed to update the published field. If not, then giving the field a new value simply won't take when the model saves.
Railscasts comes to the rescue once again: http://railscasts.com/episodes/237-dynamic-attr-accessible
Following getting the backend part of that implemented, then you can do something about the frontend form by wrapping the publish field in the View with a roles check or something to show or hide the field based on the user. Rough example of my implementation...
<% if current_user.roles.where(:name => ['Administrator','Editor']).present? %>
<%= f.label :display_name %>
<%= f.text_field :display_name %>
<% end %>