To me it seems that ActiveAdmin should check the create authorization mainly in 2 cases:
The UI needs to show the "create new Ticket" button: here, it is useful to check wether the current_user has permission to create a generic Ticket.
Cancancan syntax looks like the following:
user.can?(:create, Ticket)
ActiveAdmin needs to understand if the resource can actually be stored in the db after a form submission: here it is useful to check wether the current user can store that ticket with the values just "typed in" using the ticket form.
Cancancan syntax looks like the following:
user.can?(:create, Ticket.new({author_id: user.id, some: "x", other: "y", values: "z"}))
That's it! So why would ActiveAdmin check the following right before showing the generated "create form" for the user?
user.can?(:create, Ticket.new({author_id: nil, some: nil, other: nil, values: nil}))
What if the current user has only permission to create tickets where author_id = own_user_id?
The authorization would fail even before seeing the form.
I can't explain why ActiveAdmin was written that way, but I can show you how I've solved a similar problem.
First, you will need to grant your user the ability to create the desired record under all conditions:
# app/models/ability.rb
...
can :create, Ticket
...
This will get your past ActiveAdmin's can? check and allow the user to see the form. But we need to make sure the author_id belongs to the current user. To do this, you can use the before_create callback to set the proper author_id before saving:
# app/admin/ticket.rb
ActiveAdmin.register Ticket do
...
before_create do |ticket|
ticket.author_id = own_user_id
end
...
end
The above assumes you have a helper method or a variable called own_user_id that is available to the ActiveAdmin module and returns the proper user id. If you were using Devise, you might substitute current_user.id for own_user_id.
I'll admit, this is a not the cleanest solution, but it works. I have implemented something similar in my own projects.
I did override the Data Access class as follows, in order to have it working.
I am:
disabling authorization that i feel is done in the wrong time
forced validation before the authorization before saving a resource
ActiveAdmin::ResourceController::DataAccess.module_eval do
def build_resource
get_resource_ivar || begin
resource = build_new_resource
resource = apply_decorations(resource)
run_build_callbacks resource
# this authorization check is the one we don't need anymore
# authorize_resource! resource
set_resource_ivar resource
end
end
end
ActiveAdmin::ResourceController::DataAccess.module_eval do
def save_resource(object)
run_save_callbacks object do
return false unless object.validate # added it
authorize_resource! resource # added it
object.save(validate: false) # disabled validation since i do it 2 lines up
end
end
end
Related
I'm using CanCanCan for authorization. An admin can manage all, so they don't have per-user-id rules. The result is that they can't create items for self by default. It looks like I need to add a bunch of extra plumbing to make create in my controllers work the same for admins as it does for general users. The reason seems to be Ability#attributes_for doesn't provide the admin user with the user_id attribute.
How are other people getting around this? Are you writing code to specifically handle the admin use case in your view or controller?
Relevant parts of the Ability class
if user.admin?
can manage, :all
else
can manage, Purchase, user_id: user.id
end
Example interaction
2.6.2 :012 > Ability.new(User.find(3)).attributes_for(:create, Purchase)
=> {:user_id=>3}
2.6.2 :013 > Ability.new(User.find(4)).attributes_for(:create, Purchase)
=> {}
User 3 is general_user, User 4 is an admin
In the controller
# relying on load_and_authorize_resource
def create
puts #purchase.user_id # => nil for admin, 3 for general user
# have to add this for admin use case
#purchase.user = current_user
...
end
I wouldn't rely on the Ability object to assign user_ids to your new objects. I think it's better to explicitly write it in the controller. It seems more clear what's happening. So basically just initiate a new object in the create method and don't rely on load_and_auhtorize_resource
def create
#purchase = current_user.purchases.new(purchase_attributes)
end
If the user wasn't authorized, CanCanCan would already have interfered.
ps. I have been a CanCanCan user for years, but recently moved to Pundit. I think the way it was designed is way better and clearer than CanCanCan. Check it out if you have the time!
There's a bug in CanCanCan. Defining the ability can(:manage) needs an id. Creating a record doesn't. So the ability file doesn't allow you to create bc it's looking only for record that it can find with an id. if you define can :create, Purchase above where you define the :manage you should be good :D
if user.admin?
can :manage, :all
else
can :create, Purchase
can :manage, Purchase, user_id: user.id
end
I have a system where a User can be associated with many Portals, however a user's permissions may differ between portals.
For example, a user might be able to see unpublished posts in one portal, but not in another portal.
For methods like show?, I can grab the portal off the record.
def show?
portal = record.portal
# logic to check whether, for this particular portal,
# this user has permission to view this record
end
However that solution doesn't work for policy scopes.
Is there any way I can, say, pass in the portal to policy_scope method in the controller?
One solution I've seen around the place is to set a (temporary) attribute against the user, so that policy methods can use it. e.g.
# model
class User < ActiveRecord::Base
attr_accessor :current_portal
...
end
# controller
posts = portal.posts
current_user.current_portal = current_portal
policy_scope posts
# policy Scope
def resolve
portal = user.current_portal
# logic to scope these records by user's portal permissions
end
However this seems like a workaround, and I can definitely think of other scenarios where I'd like to be able to give authorisation logic more context as well, and I don't want this workaround to become a bad habit.
Does anyone have any suggestions?
Pundit authorization classes are plain old ruby classes. Using pundit authorize method you can pass an object to be authorized and an optional query. When you for example use authorize method as
authorize #product, :update?
the authorize method will call the update? method on the ProductPolicy class. If no query passed, controller action name + ? mark will be set as the query. Take a look at Pundit authorize method definition.
So for passing extra parameters to Pundit authorize method, you should override authorize method with an extra argument:
module Pundit
def authorize(record, query=nil, options=nil)
query ||= params[:action].to_s + "?"
#_pundit_policy_authorized = true
policy = policy(record)
if options.nil?
raise NotAuthorizedError, query: query, record: record, policy: policy unless policy.public_send(query)
else
raise NotAuthorizedError, query: query, record: record, policy: policy unless policy.public_send(query, options)
end
end
end
Then in your policy class you could use this extra param
class ProductPolicy
...
def update?(options)
...
end
def new?
...
end
...
end
As you can see, you can have policy methods that accept an extra options argument.
Then you can use authorize method in your controllers in one of these ways:
authorize #product
authorize #product, nil, { owner: User.find(1) }
authorize #product, :some_method?, { owner: User.find(1) }
authorize #product, :some_method?
So, I am somewhat new to rails and devise, so I apologize in advance if this is a basic question. I couldn't find any information on this anywhere, and I searched thoroughly. This also makes me wonder if Devise is the right tool for this, but here we go:
I have an app where devise user authentication works great, I got it, implemented it correctly and it works.
In my app, users can belong to a group, and this group has a password that a user must enter to 'join' the group.
I successfully added devise :database_authenticatable to my model, and when I create it an encrypted password is created.
My problem, is that I cannot authenticate this! I have a form where the user joins the group, searching for their group, then entering the password for it.
This is what I tried:
def join
#home = Home.find_for_authentication(params[:_id]) # method i found that devise uses
if #home.valid_password?(params[:password]);
render :json => {success: true}
else
render :json => {success: false, message: "Invalid password"}
end
end
This gives me the error: can't dup NilClass
on this line: #home = Home.find_for_authentication(params[:_id])
What is the problem?
The problem will be here:
Home.find_for_authentication(params[:_id])
I've never used database_authenticatable before (will research it, thanks!), so I checked the Devise docs for you
The method they recommend:
User.find(1).valid_password?('password123') # returns true/false
--
Object?
The method you've used has a doc:
Find first record based on conditions given (ie by the sign in form).
This method is always called during an authentication process but it
may be wrapped as well. For instance, database authenticatable
provides a find_for_database_authentication that wraps a call to
this method. This allows you to customize both database
authenticatable or the whole authenticate stack by customize
find_for_authentication.
Overwrite to add customized conditions, create a join, or maybe use a
namedscope to filter records while authenticating
The actual code looks like this:
def self.find_for_authentication(tainted_conditions)
find_first_by_auth_conditions(tainted_conditions)
end
Looking at this code, it seems to me passing a single param is not going to cut it. You'll either need an object (hence User.find([id])), or you'll need to send a series of params to the method
I then found this:
class User
def self.authenticate(username, password)
user = User.find_for_authentication(:username => username)
user.valid_password?(password) ? user : nil
end
end
I would recommend doing this:
#home = Home.find_for_authentication(id: params[:_id])
...
My program is trying to create some groups automatically, with a prefix of 'automated_group', it won't show up when loading all groups, can't be edited, and some more stuff. But I have to limit users from doing it.
But if I make a validate function, it won't let my app do it and group.save returns false. Even when updating other attributes, it won't let me save it, cause the name won't validate.
Is there any other way? Sometimes use validation, or maybe check who's changing the value?
Thanks in advance
You can use a permission system like cancan (https://github.com/ryanb/cancan). Then you can define someting like this:
can :manage, Group, automated_group: false
I've found the half of the answer in skip certain validation method in Model
attr_accessor :automated_creation
validate :check_automated_name, unless: :automated_creation
def check_automated_name
#...
end
and in my controller:
def get_automated_group
name = "automated_group_#{product.id}"
group = Group.find(name: name).first
group Group.new(automated_creation: true) unless group.blank?
returrn group
end
When updating:
I'll check in check_automated_name function that it any change on name has happened with:
group.name_changed?
so any thing else can be updated except 'name', which the only way of creation is from rails itself.
I am using activeadmin and need to figure out how to require a scope to only show records pertaining to the current user.
I also have other scopes that users can select, but those scopes need to be "pre-scoped" so to speak so that only records belonging to that user are available at any given time.
I hope this makes sense. I'm fairly new to all of this, so i'm not real sure where to start. Thanks in advance for any help.
Did you try scoping with scope_to :current_user ?
AA has some examples with docs . Here they are
http://activeadmin.info/docs/2-resource-customization.html#scoping_the_queries
current_user is helper method to get currently logged in user (currend_admin_user is default I think)
code from AA initializer
# This setting changes the method which Active Admin calls
# to return the currently logged in user.
config.current_user_method = :current_user
If you had some kind of metod in your model that use your logged in user you can do something like that
controller do
def scoped_collection
Post.some_method(current_user)
#or for example Post.select(current_user.visible_posts_columns) ... etc
end
end