This is what I get going on the recipe show page:
My Controller looks like that:
class RecipesController < ApplicationController
skip_before_action :authenticate_user!, only: [:index, :show]
def index
if params[:query].present?
#recipes = policy_scope(Recipe).search_by_title_and_description(params[:query]).order(created_at: :desc)
else
#recipes = policy_scope(Recipe).order(created_at: :desc)
end
end
def show
#recipe = Recipe.find(params[:id])
#recipes = Recipe.first(5)
end
end
My policy.rb:
class RecipePolicy < ApplicationPolicy
class Scope < Scope
def resolve
scope.all
end
def index?
false
end
def show?
false
end
end
end
And this is the Error message when adding a 'authorize #recipe' to the show action:
I need Pundit Authorization for the comments for each recipe but not for the recipe show-action itself. What do I do wrong? Thanks for helping !!
authenticate_user! (which you haven't shown/explained to us, but is probably a method from devise or similar?) is presumably to do with signing in -- that's authentication, not authorization, and is therefore outside the scope of what Pundit tries to solve.
Authentication is all about checking "are you signed in?". If this check fails, then the server responds with a 401 status.
Authorization is about checking "are you allowed to perform this action (potentially as a guest)?". If this check fails, then the server responds with a 403 status
Now presumably, you have also added some code like this in your application:
class ApplicationController < ActionController::Base
include Pundit
after_action :verify_authorized, except: :index # !!!!!
end
This after_action check is a safety net; it's there to ensure that you never forget to authorize an endpoint -- as that would allow the action to be performable by any user, by default! The presence of this check is what's causing your error above.
So. With that explained, let's look how you can implement this.
Should RecipesController#show be accessible by guests, or only by logged-in users?
If, and only if, the answer is "guests" then add this:
skip_before_action :authenticate_user!, only: :show
Assuming you've already performed any necessary authentication, you want to let any user see any recipe. How can you implement that?
Option 1 (recommended):
class RecipePolicy < ApplicationPolicy
class Scope < Scope
def resolve
scope.all
end
end ## WARNING!! NOTICE THAT THE `Scope` CLASS ENDS HERE!!!
def show?
true # !!!!
end
end
class RecipesController < ApplicationController
# ...
def show
#recipe = Recipe.find(params[:id])
authorize(#recipe) # !!!
# ...
end
end
Option 2 (also works, but worse practice as it means you can't rely on unit tests for the policy class):
class RecipesController < ApplicationController
def show
skip_authorization # !!!
#recipe = Recipe.find(params[:id])
# ...
end
end
I have this view called Intranet where only authenticated "devise clients" can access.
class IntranetController < ApplicationController
before_action :authenticate_client!
def index
end
end
On the other side, I also have other "devise admin", this devise admin requires to access the same view. How can I handle this situation?
Try this:
class IntranetController < ApplicationController
before_action :authenticate_all!
def index
end
def authenticate_all!
if admin_signed_in?
true
else
authenticate_client!
end
end
end
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]
I working on an app with user authorization. It has a List and User classes. The authentication was built with Ryan Bates http://railscasts.com/episodes/270-authentication-in-rails-3-1
I'm not sure about authorization process. I read about cancan gem. But i could not understand.
I want to achieve this:
User only able to view/edit/delete his own list.
User only able to view/edit/delete his own profile(user class).
I don't implement user level right now. No guess or admin.
How to use before_filter method in list and User controller with current_user instance?
Since you are defining current_user in the application controller, this is easy. You can use before_filter like this in the Users controller:
class ItemsController < ApplicationController
before_filter :check_if_owner, :only => [:edit, :update, :show, :destroy]
def check_if_owner
unless current_user.admin? # check whether the user is admin, preferably by a method in the model
unless # check whether the current user is the owner of the item (or whether it is his account) like 'current_user.id == params[:id].to_i'
flash[:notice] = "You dont have permission to modify this item"
redirect_to # some path
return
end
end
end
###
end
You should add a similar method to UsersController to check if it is his profile, he is editing.
Also, have a look at Devise which is the recommended plugin for authentication purposes.
For this I'd not use devise. It's way to much for this simple use.
I'd make a seperate controller for the public views and always refere to current_user
Remember to make routes for the actions in the PublicController
class PublicController < ApplicationController
before_filter :login_required?
def list
#list = current_user.list
end
def user
#user = current_user
end
def user_delete
#user = current_user
# do your magic
end
def user_update
#user = current_user
# do your magic
end
# and so on...
end
I'd like to control the permit method with something like this
class SomethingController < ApplicationController
permit :somerole
end
where ':somerole' is a field in the database linked to a controller and an action. Something that an user with priviledge can administer and change.
Some Idea?
this is just for example i have
class Admin::AdminController < ApplicationController
before_filter :login_required
before_filter :only_moderator_and_above
layout 'admin'
def only_moderator_and_above
unless current_user.has_admin_access?
flash[:notice] = CustomMessages.admin_permission_alert
redirect_to '/'
end
end
end