How do I pass extra context to Pundit scopes? - ruby-on-rails

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?

Related

ActiveAdmin checking create authorization

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

Rails Pundit how the policies are worknig?

I have a web application in ruby on rails with devise as the authentication and pundit as the authorization.
I have a model user with an integer role attribute with values 0, 1, 2, for visitor, vip, and admin respectively. I also have a scaffold, say Page that I want just vip and admin to have access to and not visitor users.
In page_policy.rb I have
def index?
current_user.vip? or current_user.admin?
end
and in pages_controller.rb I have a line authorize current_user.
Although I have given access to vip but it is available just for admin user. Where have I been wrong with the code?
Thank you
I assume you have properly set up your predicate methods vip? and admin? on the User model, and these work as expected? Can you check the result of calling each of these methods on your current user?
Pundit actually passes the result of current_user to your policy initializer, and you can access that via the user method inside your policy methods. Also I would be careful of using the or operator in this context (see http://www.virtuouscode.com/2010/08/02/using-and-and-or-in-ruby/).
So I would try:
def index?
user.vip? || user.admin?
end
Also, pundit expects you to pass the resource you are checking to the authorize method, not the user object. If you don't have an instance to pass, you can pass the class:
authorize Page

spree customizing the create user sessions action

I added inheritance to my Spree::User model class with STI. I have a :type column which can be (Spree::Guest, Spree::Writer, or Spree::Reader).
In my authentication in the admin side I want to authenticate only writer and reader. What would be the best option to solve this issue?
I tried to override the create action to something like:
def create
authenticate_spree_user!
if spree_user_signed_in? && (spree_current_user.role?(:writer) || spree_current_user.role?(:reader))
respond_to do |format|
format.html {
flash[:success] = Spree.t(:logged_in_succesfully)
redirect_back_or_default(after_sign_in_path_for(spree_current_user))
}
format.js {
user = resource.record
render :json => {:ship_address => user.ship_address, :bill_address => user.bill_address}.to_json
}
end
else
flash.now[:error] = t('devise.failure.invalid')
render :new
end
end
In this case when trying to authenticate with user of type :guest, it redirects to the new action with invalid failure message (ok) but somehow the user get authenticated (nok).
I don't think that is a good way to solve that, controller should be just a controller. I'd rather go that way:
Spree uses cancancan (or cancan in older branches) for authorization and that's how Spree implements that. I don't know why you want that STI solution - I would simply create new custom Spree::Role for that but as I said I don't know why you chose STI way - that should work fine too.
Anyway, you can either just add a decorator for that ability file with additional checks for something like user.is_a? Spree::Guest and so on or register new abilities via register_ability - something like this.
Most important part of third link (or in case it goes off):
# create a file under app/models (or lib/) to define your abilities (in this example I protect only the HostAppCoolPage model):
Spree::Ability.register_ability MyAppAbility
class MyAppAbility
include CanCan::Ability
def initialize(user)
if user.has_role?('admin')
can manage, :host_app_cool_pages
end
end
end
Personally I would go with decorator option (code seems a bit unclear but is cleaner when it comes to determine what can be managed by who - remember about abilities precedence) but it is up to you. If you have any specific questions feel free to ask, I will help if I will be able to.
Edit: so if you want to disable authentication for some users maybe just leverage existing Devise methods? Something like this(in your user model):
def active_for_authentication?
super && self.am_i_not_a_guest? # check here if user is a Guest or not
end
def inactive_message
self.am_i_not_a_guest? ? Spree.t('devise.failure.invalid') : super # just make sure you get proper messages if you are using that module in your app
end

what is the "rails way" for enabling an admin to create privileges for an existing user?

I'm writing a ruby on rails website for the first time. I have a User model and a Manager model. The user has_one Manager and a Manager belongs_to a User. The Manager model contains more info and flags regarding privileges. I want to allow an admin while viewing a User (show) to be able to make him a manager.
This is what I wrote (probably wrong):
In the view: <%= link_to 'Make Manager', new_manager_path(:id => #user.id) %>
In the controller:
def new
#user = User.find(params[:id])
#manager = #user.build_manager
end
resulting in a managers/new?id=X Url.
I would separate roles and permissions from the User class. Here's why:
Managers are users too. They share the same characteristics of Users: Email address, first name, last name, password, etc...
What if a manager also has a higher level manager? You'll have create a ManagerManager class, and that's terrible. You might end up with a ManagerManagerManager.
You could use inheritance, but that would still be wrong. Managers are users except for their title and permissions, so extract these domains into their own classes. Then use an authorisation library to isolate permissions.
You can use Pundit or CanCan. I prefer Pundit because it's better maintained, and separates permissions into their own classes.
Once you have done that, allowing a manager to change a normal user to a manager becomes trivial and easy to test:
class UserPolicy
attr_reader :user, :other_user
def initialize(user, other_user)
#user = user
#other_user = other_user
end
def make_manager?
user.manager?
end
end
In your user class you can have something like:
def manager?
title == 'manager?'
# or
# roles.include?('manager')
# Or whatever way you choose to implement this
end
Now you can always rely on this policy, wherever you are in the application, to make a decision whether the current user can change another user's role. So, in your view, you can do something like this:
- if policy(#user).make_manager?
= link_to "Make Manager", make_manager_path(#user)
Then, in the controller you would fetch the current user, and the user being acted upon, use the same policy to otherwise the action, and run the necessary updates. Something like:
def make_manager
user = User.find(params[:id])
authorize #user, :make_manager?
user.update(role: 'manager')
# or better, extract the method to the user class
# user.make_manager!
end
So you can now see the advantage of taking this approach.

Ruby on Rails security vulnerability with user enumeration via id

With Ruby on Rails, my models are being created with increasing unique ids. For example, the first user has a user id of 1, the second 2, the third 3.
This is not good from a security perspective because if someone can snoop on the user id of the last created user (perhaps by creating a new user), they can infer your growth rate. They can also easily guess user ids.
Is there a good way to use random ids instead?
What have people done about this? Google search doesn't reveal much of anything.
I do not consider exposing user IDs to public as a security flaw, there should be other mechanisms for security. Maybe it is a "marketing security flaw" when visitors find out you do not have that million users they promise ;-)
Anyway:
To avoid IDs in urls at all you can use the user's login in all places. Make sure the login does not contain some special characters (./\#? etc.), that cause problems in routes (use a whitelist regex). Also login names may not be changed later, that can cause trouble if you have hard links/search engine entries to your pages.
Example calls are /users/Jeff and /users/Jeff/edit instead of /users/522047 and /users/522047/edit.
In your user class you need to override the to_param to use the login for routes instead of the user's id. This way there is no need to replace anything in your routes file nor in helpers like link_to #user.
class User < ActiveRecord::Base
def to_param
self.login
end
end
Then in every controller replace User.find by User.find_by_login:
class UsersController < ApplicationController
def show
#user = User.find_by_login(params[:id])
end
end
Or use a before_filter to replace the params before. For other controllers with nested resources use params[:user_id]:
class UsersController < ApplicationController
before_filter :get_id_from_login
def show
#user = User.find(params[:id])
end
private
# As users are not called by +id+ but by +login+ here is a function
# that converts a params[:id] containing an alphanumeric login to a
# params[:id] with a numeric id
def get_id_from_login
user = User.find_by_login(params[:id])
params[:id] = user.id unless user.nil?
end
end
Even if you would generate random INTEGER id it also can be compromted very easy. You should generate a random token for each user like MD5 or SHA1 ("asd342gdfg4534dfgdf"), then it would help you. And you should link to user profile with this random hash.
Note, this is not actually the hash concept, it just a random string.
Another way is to link to user with their nick, for example.
However, my guess is knowing the users ID or users count or users growth rate is not a vulnerability itself!
Add a field called random_id or whatever you want to your User model. Then when creating a user, place this code in your UsersController:
def create
...
user.random_id = User.generate_random_id
user.save
end
And place this code in your User class:
# random_id will contain capital letters and numbers only
def self.generate_random_id(size = 8)
alphanumerics = ('0'..'9').to_a + ('A'..'Z').to_a
key = (0..size).map {alphanumerics[Kernel.rand(36)]}.join
# if random_id exists in database, regenerate key
key = generate_random_id(size) if User.find_by_random_id(key)
# output the key
return key
end
If you need lowercase letters too, add them to alphanumerics and make sure you get the correct random number from the kernel, i.e. Kernel.rand(62).
Also be sure to modify your routes and other controllers to utilize the random_id instead of the default id.
You need to add a proper authorization layer to prevent un-authorized access.
Let us say you you display the user information in show action of the Users controller and the code is as shown below:
class UsersController < ActionController::Base
before_filter :require_user
def show
#user = User.find(params[:id])
end
end
This implementation is vulnerable to id guessing. You can easily fix it by ensuring that show action always shows the information of the logged in user:
def show
#user = current_user
end
Now regardless of what id is given in the URL you will display the current users profile.
Let us say that we want to allow account admin and account owner to access the show action:
def show
#user = current_user.has_role?(:admin) ? User.find(params[:id]) : current_user
end
OTH authorization logic is better implemented using a gem like CanCan.

Resources