Authorization_policy_cancan_pundit_action_policy
Cancan
use cancancan and define all your actions in app/models/ability. If you want to use load_resource, you should separately define: index (with hash), show/edit/update/destroy (with block or hash), new/create (with hash). For nested resources just write parent association
Note that following next cannot
rule will override a previous can
rule, so
it is enough to set can :manage, :all
and than write what cannot :destroy,
Project
Define abilities https://github.com/CanCanCommunity/cancancan/wiki/defining-abilities
can :read, Article # autorize! :read, Article will return true
can :crud, Article # authorize! :read/:create/:update/:destroy, Article will return true
# https://github.com/CanCanCommunity/cancancan/wiki/Action-Aliases
aliase_action :index, :show, to: :read | :new, to: :create | :edit. to: :update
can :manage, Article # authorize! :any_action, Article will return true
can [:update, :destroy], [Post, Comment] # array notation
Hash of conditions
# can read only posts that belongs to user
can :read, Post, user_id: user.id
Defining with a block is evaluated only when instance is passed (for example not
in index action when class is used, ie if you call can? :update, Project
it
will return true)
can :update, Project do |project|
false
end
Defining with a block without other arguments can be used for defining Abilities and roles in database https://github.com/CanCanCommunity/cancancan/wiki/Abilities-in-Database
rails g model Permission user_id:integer name:string subject_class:string subject_id:integer action:string description:text
# app/models/ability.rb
class Ability
include CanCan::Ability
can do |action, subject_class, subject|
# action: :read, subject_class: User, subject: 1 or nil
end
end
Example that only admins can change, for example company_id, with can_change_user_company_id
# in models/ability.rb
can :can_change_user_company_id, User if user.admin?
<!-- users/_form.html.erb -->
<% if can? :can_change_user_company_id, @user %>
<!-- role checkboxes go here -->
<% end %>
# users_controller.rb
def update
authorize! :can_change_user_company_id, @user if params[:user][:company_id]
# ...
end
Pundit
https://github.com/varvet/pundit
Install with gem and include in controller
# Gemfile
gem 'pundit'
# app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
include Pundit
end
# generate default app/policies/application_policy.rb
rails g pundit:install
Sample ruby poro class
class PostPolicy
attr_reader :user, :post
def initialize(user, post)
@user = user
@post = post
end
def update?
user.admin? or not post.published?
end
end
Pundit will assume that:
- class has the same name as model + suffix
Policy
- first argument is a
current_user
(called where it was invoked) and stored inuser
. - second argument is object and stored in
record
(if you use generated ApplicationPolicy). - define methods like method name plus ?
When using authorize
you can set first argument as a class authorize Post
or
a symbol for headless policy. Second argument could be action name: authorize
@post, :destroy?
. To specify Policy class you can use authorize @post,
policy_class: PostPolicy
In controller authorize @post
inside def update
action will instantiate
policy with current_user
and call method with ?
at the end, something like
unless PostPolicy.new(current_user, @post).update?
raise Pundit::NotAuthorizedError, "not allowed to update? this #{@post.inspect}"
end
In view, you can use if policy(@post).update?
method to check if current_user
is authorized.
You can rescue from not authorized requests
https://github.com/varvet/pundit#rescuing-a-denied-authorization-in-rails
def new
authorize @user
rescue Pundit::NotAuthorizedError => e
flash.now[:alert] = e.message
render :error
end
end
Headless policies
Headless policies, if you do not have corresponding model.
# app/policies/dashboard_policy.rb
class DashboardPolicy < Struct.new(:user, :dashboard)
# ...
end
and use like
<% if policy(:dashboard).show? %>
<%= link_to 'Dashboard', dashboard_path %>
<% end %>
Scopes
To define scope on a model for which current_user have access, punding assumes:
- class has name
Scope
and is nested under the policy class - first argument is user and second argument is a scope
class PostPolicy < ApplicationPolicy
class Scope < Scope
def resolve
if user.admin?
scope.all
else
scope.where(published: true)
end
end
end
def update?
user.admin? or not record.published?
end
end
and you can use like
def index
# @posts = PostPolicy::Scope.new(current_user, Post).resolve
@posts = policy_scope(Post)
end
def show
@post = policy_scope(Post).find(params[:id])
end
To check if authorize
is called you can use verify_authorized
so it raises
error if authorize
is not called.
# app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
include Pundit
after_action :verify_authorized
end
Also you can check if user exists in ApplicationPolicy (so you not need to check in other policies).
class ApplicationPolicy
def initialize(user, record)
raise Pundit::NotAuthorizedError, "must be logged in" unless user
@user = user
@record = record
end
class Scope
attr_reader :user, :scope
def initialize(user, scope)
raise Pundit::NotAuthorizedError, "must be logged in" unless user
@user = user
@scope = scope
end
end
end
Use alias after method are defined
class MyPolicy < ApplicationPolicy
def create?
end
alias new? create?
end
Action Policy
https://github.com/palkan/action_policy is similar to pundit
Generate application policy
rails generate action_policy:install
rails generate action_policy:policy post
Use in controller (policy class is infered from model name, method infers from action name and current_user becomes user).
# You can check in before action
def _set_post
authorize! @post
end
# or with specific method
authorize! @post, to: :update?
# or with specific policy
authorize! @post, with: CustomPostPolicy
# bang will raise error if not allowed
rescue_from ActionPolicy::Unauthorized do |ex|
# Exception object contains the following information
ex.policy #=> policy class, e.g. UserPolicy
ex.rule #=> applied rule, e.g. :show?
end
# make sure we call authorize! during the action, use this after_action
verify_authorized
or use in view
<% if allowed_to? :edit?, post %>
For non models, services you need to pass any object and policy class
<% if allowed_to? :index?, Object, with: TranslationsPolicy %>
<%= link_to 'Translations', translations_path %>
<% end %>
Define policy using user
and record
instance methods. Remember to use ?
as
suffix for method names.
class CommentPolicy < ApplicationPolicy
# https://actionpolicy.evilmartians.io/#/authorization_context
# If you have some pages without user, you can define that with allow_nil: true
# do not define on ApplicationPolicy since you need to check user.nil? in
# every method... For view, you can check current_user before policy check
authorize :user, allow_nil: true
# you can call other policies, synonim as `check?`
def update?
user.admin? || allowed_to?(:update?, record.post)
end
# pass fast or fail fast with `allow` and `deny`
def show?
allow! if user.admin?
user.public?
end
# alias synonims so you do not need to repeat dry, NOTE that is will call if
# method exists, so alias is used only when method missing
# By default, ActionPolicy::Base adds one alias: alias_rule :new?, to: :create?.
alias_rule :edit?, :destroy?, to: :update?
# default rule is :manage (not matching anything, defaults are index? create?
# https://github.com/palkan/action_policy/blob/master/docs/custom_policy.md
def manage?
true
end
end