How can I enforce scoping on singular resources using Pundit? - ruby-on-rails

I have a few singular resources in my app, e.g.:
# routes.rb
MySite::Application.routes.draw do
resource :thing
end
# things_controller.rb
class ThingsController < ApplicationController
def edit
load_thing
end
def update
load_thing
if #thing.update_attributes(thing_params)
...
else
...
end
end
private
def load_thing
#thing ||= current_user.thing
end
def thing_params
params.require(:thing).permit(...)
end
end
I'm wondering how to enforce policy scoping using Pundit (before_action :verify_policy_scoped has been set in ApplicationController).
I'm not sure how to form my policy scope for singular resources, i.e.:
# thing_policy.rb
class ThingPolicy < ApplicationPolicy
Scope < Scope
def resolve
# What to do here...
# scope => ?
end
end
end
# things_controller.rb
def load_thing
# ...and what to do here
#thing ||= policy_scope(...)
end
According to Pundit's docs:
...the method resolve...should return some kind of result which can be
iterated over.
However, with singular resources, this iterability clause isn't really valid and there is no AR-style scope as such... just a single record.
Anyone have any suggestions for how to go about this?

Scoping is generally performed on the index action to restrict the base set of records the user can access before filtering is applied. For individual records, you should authorize them instead.
In any case, you are already scoping #thing to current_user
Would suggest to limit before_action :verify_policy_scoped to only: [:index]

Related

Rails 5, pundit authorization

Pundit works well, if action has resources like:
class Admin::PagesController << ApplicationController
def index
#pages = Page.all
end
end
How to authorise method without any resources in action?
class Admin::DashboardController << ApplicationController
def index
end
end
I hav file policies/admin/dashboard_policy.rb
class Admin::DashboardPolicy < ApplicationPolicy
class Scope < Scope
def resolve
scope.all
end
end
end
This file was generated by command:
rails g pundit:policy Admin/Dashboard
File views/admin/index.html.slim has only static text. Nothing more.
How to authorise action without any resources?
Regards
Sssebaaa
To authorize without a scope or model instance call authorize with a symbol or array of symbols (when namespaced):
class Admin::DashboardController << ApplicationController
def index
authorize [:admin, :dashboard]
end
end
This will call the #index? method on the policy class:
class Admin::DashboardPolicy < ApplicationPolicy
def index?
user.admin?
end
end
You can also remove the scope completely from your policy.
If you don't have any callbacks checking that the policy is scoped, as pundit doc suggests, like
class ApplictationController < ActionController::Base
include Pundit
after_action :verify_policy_scoped, only: :index
end
You don't have anything to do.
However if you do have a callback, you can just skip it in your controller action like this:
class Admin::DashboardController << ApplicationController
skip_after_action :verify_policy_scoped, only: [:index]
def index
end
end

Pundit headless policy for nested resource

How can I authorize an action from a controller without a model based on another model object?
Let's say I have a model called Server and I have a nested controller called config_files_controller which doesn't have a corresponding model.
I want to be able to authorize the actions from the config_files_controller based on a Server object and current user and the policies defined for Server.
In routes.rb I have:
resources :servers do
resources :config_files do
collection do
get 'load_file'
end
end
end
The config_files_controller.rb looks something like this:
class ConfigFilesController < ApplicationController
before_filter :authenticate_user!
before_filter :load_server
def index
# displays file names
end
def load_file
# gets file content
end
private
def load_server
#server = Server.find(params[:server_id])
authorize :config_file, "#{action_name}?"
end
end
In configuration_file_policy.rb I would like to have something like this:
class ConfigurationFilePolicy < Struct.new(:user, :configuration_file, :server)
def index?
ServerPolicy.new(user, server).show?
end
def load_file?
ServerPolicy.new(user, server).update?
end
end
I'm probably missing something or I'm just not seeing the obvious solution. Any suggestion would be appreciated!
Thanks!
Your controller sets #server object, and Server seems to be a model. Hence it should be sufficient to authorize that. (No need for ConfigurationFilePolicy.)
config_files_controller.rb
...
def index
authorize #server, :show?
# displays file names
...
end
def load_file
authorize #server, :update?
# gets file content
...
end
https://github.com/elabs/pundit#policies

Multitenant scoping using Pundit

I'm using Pundit for authorization and I want to make use of its scoping mechanisms for multi-tenancy (driven by hostname).
I've been doing this manually to date by virtue of:
class ApplicationController < ActionController::Base
# Returns a single Client record
def current_client
#current_client ||= Client.by_host(request.host)
end
end
And then in my controllers doing things like:
class PostsController < ApplicationController
def index
#posts = current_client.posts
end
end
Pretty standard fare, really.
I like the simplicity of Pundit's verify_policy_scoped filter for ensuring absolutely every action has been scoped to the correct Client. To me, it really is worthy of a 500 error if scoping has not been officially performed.
Given a Pundit policy scope:
class PostPolicy < ApplicationPolicy
class Scope < Scope
def resolve
# have access to #scope => Post class
# have access to #user => User object or nil
end
end
end
Now, Pundit seems to want me to filter Posts by user, e.g.:
def resolve
scope.where(user_id: user.id)
end
However, in this scenario I actually want to filter by current_client.posts as the default case. I'm not sure how to use Pundit scopes in this situation but my feeling is it needs to look something like:
def resolve
current_client.posts
end
But current_client is naturally not going to be available in the Pundit scope.
One solution could be to pass current_client.posts to policy_scope:
def index
#posts = policy_scope(current_client.posts)
end
But I feel this decentralizes my tenancy scoping destroys the purpose of using Pundit for this task.
Any ideas? Or am I driving Pundit beyond what it was designed for?
The most "Pundit-complient" way to deal with this problem would be to create a scope in your Post model:
Class Post < ActiveRecord::Base
scope :from_user, -> (user) do
user.posts
end
end
Then, you will be able to use it in your policy, where user is filled with the current_user from your controller:
class PostPolicy < ApplicationPolicy
class Scope
attr_reader :user, :scope
def initialize(user, scope)
#user = user
#scope = scope
end
def resolve
scope.from_user(user)
end
end
end
If you are returning an ActiveRecord::Relation from the scope, you can stop reading from here.
If your scope returns an array
The default ApplicationPolicy implement the method show using a where:
source.
So if your scope does not return an AR::Relation but an array, one work-around could be to override this show method:
class PostPolicy < ApplicationPolicy
class Scope
# same content than above
end
def show?
post = scope.find do |post_in_scope|
post_in_scope.id == post.id
end
post.present?
end
end
Whatever your implementation is, you just need to use the PostPolicy from your controller the "Pundit-way":
class PostsController < ApplicationController
def index
#posts = policy_scope(Post)
end
def show
#post = Post.find(params[:id])
authorize #post
end
end

Rails simple access control

I am aware that several gems are made to handle authorization in Rails. But is it really worth it to use these gems for simple access controls ?
I only have a few "roles" in my application, and I feel that a powerful gem would be useless and even slow down the response time.
I have already implemented a solution, but then I took some security classes (:p) and I realized my model was wrong ("Allow by default, then restrict" instead of "Deny by default, then allow").
Now how can I simply implement a "deny by default, allow on specific cases" ?
Basically I'd like to put at the very top of my ApplicationController
class ApplicationController < ApplicationController::Base
before_filter :deny_access
And at the very top of my other controllers :
class some_controller < ApplicationController
before_filter :allow_access_to_[entity/user]
These allow_access_to_ before_filters should do something like skip_before_filter
def allow_access_to_[...]
skip_before_filter(:deny_access) if condition
end
But this doesn't work, because these allow_access before filters are not evaluated before the deny_access before_filter
Any workaround, better solution for this custom implementation of access control ?
EDIT
Many non-RESTful actions
I need per-action access control
undefined method 'skip_before_filter' for #<MyController... why ?
My before_filters can get tricky
before_action :find_project, except: [:index, :new, :create]
before_action(except: [:show, :index, :new, :create]) do |c|
c.restrict_access_to_manager(#project.manager)
end
I would really advise using a proper battle tested gem for authentication & authorisation instead of rolling your own. These gems have enormous test suites and aren't really all that hard to setup.
I've recently implemented an action based authorization using roles with Pundit & Devise
Devise is changeable as long as the gem you are using provides a current_user method if you don't want to further configure pundit.
# app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
include Pundit
rescue_from Pundit::NotAuthorizedError, with: :rescue_unauthorized
# Lock actions untill authorization is performed
before_action :authorize_user
# Fallback when not authorized
def rescue_unauthorized(exception)
policy_name = exception.policy.class.to_s.underscore
flash[:notice] = t(
"#{policy_name}.#{exception.query}",
scope: "pundit",
default: :default
)
redirect_to(request.referrer || root_path)
end
end
# app/models/user.rb
class User < ActiveRecord::Base
has_many :roles, through: :memberships
def authorized?(action)
claim = String(action)
roles.pluck(:claim).any? { |role_claim| role_claim == claim }
end
end
# app/policies/user_policy.rb => maps to user_controller#actions
class UserPolicy < ApplicationPolicy
class Scope < Scope
attr_reader :user, :scope
# user is automagically set to current_user
def initialize(user, scope)
#user = user
#scope = scope
end
def resolve
scope.all
end
end
def index?
# If user has a role which has the claim :view_users
# Allow this user to use the user#index action
#user.authorized? :view_users
end
def new?
#user.authorized? :new_users
end
def edit?
#user.authorized? :edit_users
end
def create?
new?
end
def update?
edit?
end
def destroy?
#user.authorized? :destroy_users
end
end
Long story short:
If you configure pundit to force authorization on each request which is described in detail on the github page, the controller evaluates a policy based on the used controller.
UserController -> UserPolicy
Actions get defined with a question mark, even non restful routes.
def index?
# authorization is done inside the method.
# true = authorization succes
# false = authorization failure
end
This is my solution to action based authorization hope it helps you out.
Optimisations & feedback are welcome !
Rolling your own implementation isn't necessarily bad as long as you're committed to it.
It won't get tested and maintained by the community so you must be willing to maintain it yourself in the long run, and if it compromises security you need to be really sure of what you're doing and take extra care. If you have that covered and the existing alternatives don't really fit your needs, making your own isn't such a bad idea. And generally it's an incredibly good learning experience.
I rolled my own with ActionAccess and I couldn't be happier with the results.
Locked by default aproach:
class ApplicationController < ActionController::Base
lock_access
# ...
end
Per-action access control:
class ArticlesController < ApplicationController
let :admins, :all
let :editors, [:index, :show, :edit, :update]
let :all, [:index, :show]
def index
# ...
end
# ...
end
Really lighweight implementation.
I encourage you not to use it but to check out the source code, it has a fare share of comments and should be a good source of inspiration. ControllerAdditions might be a good place to start.
ActionAccess follows a different approach internally, but you can refactor your answer to mimic it's API with something like this:
module AccessControl
extend ActiveSupport::Concern
included do
before_filter :lock_access
end
module ClassMethods
def lock_access
unless #authorized
# Redirect user...
end
end
def allow_manager_to(actions = [])
prepend_before_action only: actions do
#authorized = true if current_user_is_a_manager?
end
end
end
end
class ApplicationController < ActionController::Base
include AccessControl # Locked by default
# ...
end
class ProjectController < ApplicationController
allow_managers_to [:edit, :update] # Per-action access control
# ...
end
Take this example as pseudo-code, I haven't tested it.
Hope this helps.
I didn't like my previous solution using prepend_before_action, here is a nice implementation using ActionController callbacks
module AccessControl
extend ActiveSupport::Concern
class UnauthorizedException < Exception
end
class_methods do
define_method :access_control do |*names, &blk|
_insert_callbacks(names, blk) do |name, options|
set_callback(:access_control, :before, name, options)
end
end
end
included do
define_callbacks :access_control
before_action :deny_by_default
around_action :perform_if_access_granted
def perform_if_access_granted
run_callbacks :access_control do
if #access_denied and not #access_authorized
#request_authentication = true unless user_signed_in?
render(
file: File.join(Rails.root, 'app/views/errors/403.html'),
status: 403,
layout: 'error')
else
yield
end
end
end
def deny_by_default
#access_denied ||= true
end
def allow_access
#access_authorized = true
end
end
end
Then you can add your own allow_access_to_x methods (for example in the same AccessControl concern) :
def allow_access_to_participants_of(project)
return unless user_signed_in?
allow_access if current_user.in?(project.executants)
end
Use it in your controllers the following way (don't forget to include AccessControl in your ApplicationController
class ProjectsController < ApplicationController
access_control(only: [:show, :edit, :update]) do
set_project
allow_access_to_participants_of(#project)
allow_access_to_project_managers
end
def index; ...; end;
def show; ...; end;
def edit; ...; end;
def update; ...; end;
def set_project
#project = Project.find(params[:project_id])
end
end
EDIT : Outdated answer, I have a friendlier implementation that involves using an access_control block
Going with evanbikes suggestion, for now I'll be using prepend_before action. I find it quite simple & flexible, but if I ever realize it's not good enough I will try other things.
Also if you find security issues/other problems with the solution below, please comment and/or downvote. I don't like leaving bad examples in SO.
class ApplicationController < ApplicationController::Base
include AccessControl
before_filter :access_denied
...
My Access Control module
module AccessControl
extend ActiveSupport::Concern
included do
def access_denied(message: nil)
unless #authorized
flash.alert = 'Unauthorized access'
flash.info = "Authorized entities : #{#authorized_entities.join(', '}" if #authorized_entities
render 'static_pages/home', :status => :unauthorized
end
end
def allow_access_to_managers
(#authorized_entities ||= []) << "Project managers"
#authorized = true if manager_logged_in?
end
...
How I use the AC in controllers :
class ProjectController < ApplicationController
# In reverse because `prepend_` is LIFO
prepend_before_action(except: [:show, :index, :new, :create]) do |c|
c.allow_access_to_manager(#manager.administrateur)
end
prepend_before_action :find_manager, except: [:index, :new, :create]

Is there any way to access the parent object in a Cancan nested resource ability?

I have a nested resource for which I'm using Cancan to do authorization. I need to be able to access the parent object in order to be able to authorize the :index action of the child (since no child instance is passed for an :index action).
# memberships_controller.rb
class MembershipsController < ApplicationController
...
load_and_authorize_resource :org
load_and_authorize_resource :membership, through: :org
..
end
ability.rb
can [:read, :write], Membership do |membership|
membership.org.has_member? user
end
This doesn't work for the :index action
Unfortunately the index action doesn't have any membership instance associated with it and so you can't work your way back up to check permissions.
In order to check the permissions, I need to interrogate the parent object (the org) and ask it whether the current user is a member e.g.
# ability.rb
...
can :index, Membership, org: { self.has_member? user }
Cancan almost lets me do this...
Cancan states that you can access the parent's attributes using the following mechanism:
https://github.com/ryanb/cancan/wiki/Nested-Resources#wiki-accessing-parent-in-ability
# in Ability
can :manage, Task, :project => { :user_id => user.id }
However this just works by comparing attributes which doesn't work for my case.
How can I access the parent object itself though?
Is there any way to access the parent object itself within the permissions?
I recently faced the same problem and ended up with the following (assuming you have Org model):
class MembershipsController < ApplicationController
before_action :set_org, only: [:index, :new, :create] # if shallow nesting is enabled (see link at the bottom)
before_action :authorize_org, only: :index
load_and_authorize_resource except: :index
# GET orgs/1/memberships
def index
#memberships = #org.memberships
end
# ...
private
def set_org
#org = Org.find(params[:org_id])
end
def authorize_org
authorize! :access_memberships, #org
end
end
ability.rb:
can :access_memberships, Org do |org|
org.has_member? user
end
Useful links
https://github.com/ryanb/cancan/issues/301
http://guides.rubyonrails.org/routing.html#shallow-nesting
Can't you do something like this?
class Ability
include CanCan::Ability
def initialize(user)
user ||= User.new # guest user (not logged in)
can :index, Membership, org: {id: user.memberships.map(&:org_id)}
end
end

Resources