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
Related
How would I provide pundit authorization for a dashboard controller which provides data from various models?
My DashboardsController looks like this:
class DashboardsController < ApplicationController
before_action :authenticate_user!
before_action :set_user
before_action :set_business
after_action :verify_authorized
def index
end
private
def set_user
#user = current_user
end
def set_business
#business = current_user.business
end
end
How would I authorize for both #user and #business within my DashboardsPolicy?
I would argue that trying to get access to a dashboard is not a policy based on a resource named dashboard, but simply a special method in the business policy.
Therefore, I would add this to the BusinessPolicy as a method dashboard.
# in your controller
authorize #business, :dashboard?
# and the business_policy
class BusinessPolicy < ApplicationPolicy
def dashboard?
# condition depending on a `user` (current_user) and a record (business)
user.admin? || user.business == record
end
end
Or it might be even simpler. If someone is allowed to see the dashboard when she is allowed to show the business, then just re-use BusinessPolicy#show? in your controller:
authorize #business, show?
Pundit expects a current user and a model object to be passed to it. In this case I think what you would want is a DashboardsPolicy class, and you would authorize it like:
def index
authorize(#business)
end
From the README:
Pundit will call the current_user method to retrieve what to send into
this argument
The authorize method automatically infers that Post will have a
matching PostPolicy class, and instantiates this class, handing in the
current user and the given record
There is also a specific section in the README regarding headless policies that uses the Dashboard as the example action: https://github.com/varvet/pundit#headless-policies
You can also create a plain ruby object that takes two entities and use that as your object to authorize:
class UserBusiness
def initialize(user, business)
end
...other methods here
end
#model = UserBusiness.new(user, business)
authorize(#model)
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]
I am trying to use Pundit to authenticate access to some static views that require no database interaction:
class StaticController < ApplicationController
include Pundit
authorize :splash, :home?
def home end
end
Below is my static policy. The home? policy always returns true, so I should be able to access the home view.
class StaticPolicy < Struct.new(:user, :static)
def initialize(user, resource)
#user = user
#resource = resource
end
def home?
true
end
end
Instead I get this:
undefined method `authorize' for StaticController:Class
Pundit works perfectly if I'm authorizing a model:
def forums_index
#forums = Forum.all
authorize #forums
end
However, if I try to use the authorize method outside of an action that doesn't make use of a model I get:
undefined method `authorize' for StaticController:Class
Well, AFAIK you'll always have to authorize against either an object or a class, while CanCan already "load_and_authorize_resource", when using Pundit you already know that you have to load and authorize something yourself (sorry if I'm being too obvious here).
That said and considering that your view doesn't have DB interation, it seems to me that the best solution for your case is make some custom authorization against your user, something like
class StaticPolicy < Struct.new(:user, :static)
def initialize(user, resource)
#user = user
#resource = resource
end
def home?
authorize #user, :admin # or suppress the second parameter and let the Policy use the 'home?' method
true
end
end
and in your UserPolicy something like
class UserPolicy < ApplicationPolicy
def admin # or def home?, it's up to you
user.admin?
end
end
I didn't test it, but that's the main idea, does it make any sense? Is it clear?
Please give it a try and post any impressions, hope it helps :)
I'm using pundit for access control in the admin section of my app. I have a dashboards controller that looks like this:
class Admin::DashboardsController < AdminController
def index
#total_revenue = Order.total_revenue
authorize :dashboards, :index?
end
...
end
and a policy that looks like this:
class DashboardPolicy < Struct.new(:user, :dashboard)
def index?
true
end
end
When I try to access /admin/dashboards/ I get a Pundit::NotDefinedError, unable to find policy SymbolPolicy for dashboards
I've also tried namespacing the policy and got the same error.
jizak's answer did not work for me, I found following solution for headless name-spaced policies, the trick being with the [:admin, :policy] first argument.
class Admin::HomeController < AdminController
def dashboard
authorize [:admin, :home], :dashboard?
end
end
And then for the policy:
Admin::HomePolicy < AdminPolicy
def dashboard?
return false unless user && user.admin?
true
end
end
I have such headless policy:
app/policies/admin/statistic_policy.rb
class Admin::StatisticPolicy < Struct.new(:user, :statistic)
def show?
user.admin?
end
end
app/controllers/admin/statistics_controller.rb
class Admin::StatisticsController < Admin::ApplicationController
def show
per_today Time.zone.now
authorize :statistics, :show?
end
...
end
and it works for me.
Try to update gem, because these changes are new (https://github.com/elabs/pundit/issues/77).
Delete your Gemfile.lock from the project and do 'bundle install'.
I recently had the same issue. The problem that I faced was that the controller was without a model.
Remember that Pundit is a model based authorization, not so much of controller based.
Before creating an Admin class (in the models), I was getting the same error as you were. Also, take note of the authorization statement on my dashboard action in the controller.
controllers/admin_controller.rb
class AdminController < ApplicationController
after_action :verify_authorized
def dashboard
authorize Admin, :dashboard?
end
end
models/admin.rb
class Admin
def self.policy_class
AdminPolicy
end
end
policies/admin_policy
class AdminPolicy < Struct.new(:user, :admin)
def dashboard?
user.admin?
end
end
I managed to use Pundit on namespaced controller actions regardless of the model by using this:
In my /private/scrapers_controller.rb I have
module Private
class ScrapersController < Private::PrivateApplicationController
# Pundit authorizations
before_action { authorize [:private, :scrapers] }
def index
end
...
And then in policies/private/scrapers_policy.rb
class Private::ScrapersPolicy < ApplicationPolicy
def index?
return true if user.has_role?(:super_admin)
return false
end
end
This will disallow visiting scrapers#index or any other action within the controller to any user who's not an :super_admin
To disallow only index explicitly you can use:
before_action { authorize [:private, :scrapers], :index? }
Check yout pundit version. You may need to run 'bundle update pundit', because headless policies were merged to master quite recently and before that you had need to install pundit from github: 'elabs/pundit' to use them.
Described issue
Merged headless policies
if you just want to render landing page for controller dashboard#index for example, where no need to authorize users you can just skip authorization like
dashboard_controller.rb
class DashboardController < ApplicationController
def index
skip_policy_scope
end
end
And therefore you don't have to create DashboardPolicy at all.
In my Ruby on Rails app, I've got:
class AdminController < ApplicationController
def create
if request.post? and params[:role_data]
parse_role_data(params[:role_data])
end
end
end
and also
module AdminHelper
def parse_role_data(roledata)
...
end
end
Yet I get an error saying parse_role_data is not defined. What am I doing wrong?
Helpers are mostly used for complex output-related tasks, like making a HTML table for calendar out of a list of dates. Anything related to the business rules like parsing a file should go in the associated model, a possible example below:
class Admin < ActiveRecord::Base
def self.parse_role_data(roledata)
...
end
end
#Call in your controller like this
Admin.parse_role_data(roledata)
Also look into using (RESTful routes or the :conditions option)[http://api.rubyonrails.org/classes/ActionController/Routing.html] when making routes, instead of checking for request.post? in your controller.
Shouldn't you be accessing the parse_role_data through the AdminHelper?
Update 1: check this
http://www.johnyerhot.com/2008/01/10/rails-using-helpers-in-you-controller/
From the looks of if you're trying to create a UI for adding roles to users. I'm going to assume you have a UsersController already, so I would suggest adding a Role model and a RolesController. In your routes.rb you'd do something like:
map.resources :users do |u|
u.resources :roles
end
This will allow you to have a route like:
/users/3/roles
In your RolesController you'd do something like:
def create
#user = User.find_by_username(params[:user_id])
#role = #user.roles.build(params[:role])
if #role.valid?
#role.save!
redirect_to #user
else
render :action => 'new'
end
end
This will take the role params data from the form displayed in the new action and create a new role model for this user. Hopefully this is a good starting point for you.