[Seattle.rb 2019] A denial!

[Seattle.rb 2019] A denial!

...Or why I’ve built yet another authorization framework?

http://actionpolicy.evilmartians.io/

52cc8a838bf44a589d2572833b2dd1b9?s=128

Vlad Dem

April 02, 2019
Tweet

Transcript

  1. A DENIAL! A DENIAL! Vladimir Dementyev @evilmartians or why I’ve

    built yet another authorization framework?
  2. …AND HOW

  3. palkan_tula palkan Seattle.rb 2019 3 @palkan @palkan_tula Vladimir Dementyev 919

    706 391 496
  4. palkan_tula palkan Seattle.rb 2019 4

  5. None
  6. palkan_tula palkan Seattle.rb 2019 evl.ms/blog 6

  7. palkan_tula palkan Seattle.rb 2019 DEFINITIONS or what is this talk

    about 7
  8. palkan_tula palkan Seattle.rb 2019 BORING DEFINITION 8 The act of

    giving someone official permission to do something https://dictionary.cambridge.org/dictionary/english/authorization
  9. palkan_tula palkan Seattle.rb 2019 BY EXAMPLE 9 class ItemsController <

    ApplicationController def destroy item = Item.find(params[:id]) if current_user.admin? || current_user.moderator? || item.owner_id == current_user.id item.destroy! head :ok end else head :forbidden end end
  10. palkan_tula palkan Seattle.rb 2019 class ItemsController < ApplicationController def destroy

    item = Item.find(params[:id]) if current_user.admin? || current_user.moderator? || item.owner_id == current_user.id item.destroy! head :ok end else head :forbidden end end authorization BY EXAMPLE 10
  11. palkan_tula palkan Seattle.rb 2019 class ItemsController < ApplicationController def destroy

    item = Item.find(params[:id]) if current_user.admin? || current_user.moderator? || item.owner_id == current_user.id item.destroy! head :ok end else head :forbidden end end Is user allowed to do that? authorization BY EXAMPLE 11
  12. palkan_tula palkan Seattle.rb 2019 AUTHORIZATION 12 Authentication System constraints Data

    model constraints (validations)
  13. palkan_tula palkan Seattle.rb 2019 AUTHORIZATION 13 How to grant/revoke access?

    Authorization model (roles, permission, accesses) How to verify access? Authorization layer (policies, rules)
  14. palkan_tula palkan Seattle.rb 2019 AUTHORIZATION 14 How to grant/revoke access?

    Authorization model (roles, permission, accesses) How to verify access? Authorization layer (policies, rules) plan for today
  15. palkan_tula palkan Seattle.rb 2019 class ItemsController < ApplicationController def destroy

    item = Item.find(params[:id]) if current_user.can?(:destroy, item) item.destroy! head :ok end else head :forbidden end end authorization layer in action BY EXAMPLE 15
  16. palkan_tula palkan Seattle.rb 2019 class ItemsController < ApplicationController def destroy

    item = Item.find(params[:id]) - if current_user.admin? || - current_user.moderator? || - item.owner_id == current_user.id + if current_user.can?(:destroy, item) item.destroy! head :ok end else head :forbidden end end BY EXAMPLE 16
  17. palkan_tula palkan Seattle.rb 2019 SOLUTIONS and their problems 17

  18. palkan_tula palkan Seattle.rb 2019 AUTHORIZATION 18 cancancan rolify consul allowy

    walruz action_access pundit the_role trust eaco declarative_aithorization SimonSays canable kan
  19. palkan_tula palkan Seattle.rb 2019 CANCAN(-CAN) 19 Ability class can? and

    authorize! helper authorize! :destroy, item # raises can? :destroy, item # predicate
  20. palkan_tula palkan Seattle.rb 2019 CANCAN 20 class Ability include CanCan

    ::Ability def user_abilities can :create, [Question, Answer] can :update, [Question, Answer], user_id: user.id can :destroy, [Question, Answer], user_id: user.id can :destroy, Attachment, attachable: { user_id: user.id } can [:vote_up, :vote_down], [Question, Answer] do |resource| resource.user_id != user.id end end end
  21. palkan_tula palkan Seattle.rb 2019 21 Ability

  22. palkan_tula palkan Seattle.rb 2019 PUNDIT 22 Policy classes are plain

    old Ruby classes authorize and policy helpers authorize item, :destroy? # raises policy(item).destroy? # predicate
  23. palkan_tula palkan Seattle.rb 2019 PUNDIT 23 class QuestionPolicy def index?

    true end def create? true end def update? user.admin? || (user.id == target.user_id) end end
  24. palkan_tula palkan Seattle.rb 2019 POLICY IS PORO *Seattle.rb Slack 24

  25. palkan_tula palkan Seattle.rb 2019 PUNDIT 25 You’re on your own

    with your policy classes authorize and policy helpers authorize item, :destroy? # raises policy(item).destroy? # predicate
  26. palkan_tula palkan Seattle.rb 2019 THE EVOLUTION 26 Start with CanCan

    Migrate to Pundit Customize Pundit Customize Pundit…
  27. palkan_tula palkan Seattle.rb 2019 THE EVOLUTION 27 Start with CanCan

    Migrate to Pundit Write your own framework
  28. palkan_tula palkan Seattle.rb 2019 WHY CUSTOMIZE? 28 Boilerplate Performance Testability

    Flexibility
  29. palkan_tula palkan Seattle.rb 2019 RE-INVENTING THE WHEEL OR …? 29

  30. palkan_tula palkan Seattle.rb 2019 30 gem “action_policy” SMELLS LIKE RAILS

    SPIRIT
  31. palkan_tula palkan Seattle.rb 2019 RAILSCONF 2018 http://bit.ly/action-policy-rc2018 31

  32. palkan_tula palkan Seattle.rb 2019 ACTION POLICY 32

  33. palkan_tula palkan Seattle.rb 2019 class ProductsController < ApplicationController before_action :load_product,

    except: [:index, :new, :create] def load_product @product = current_account.products.find(params[:id]) # auto-infer policy and rule authorize! @product # explicit rule and policy authorize! @product, to: :manage?, with: SpecialProductPolicy end end ACTION POLICY 33
  34. palkan_tula palkan Seattle.rb 2019 class ProductsController < ApplicationController def index

    # non-raising predicate method if allowed_to?(:create?) @tags = current_account.tags end # scoping data @products = authorized_scope(current_account.products) end end ACTION POLICY 34
  35. palkan_tula palkan Seattle.rb 2019 class ProductPolicy < ApplicationPolicy relation_scope do

    |rel| next rel if user.manager? rel.where(owner_id: user.id) end def create? user.manager? end def show? record.account_id == user.account_id end end ACTION POLICY 35
  36. palkan_tula palkan Seattle.rb 2019 IT’S PUNDIT, ISN’T IT? https://github.com/palkan/action_policy/issues 36

  37. palkan_tula palkan Seattle.rb 2019 WHY CUSTOMIZE? 37 Boilerplate Performance Testability

    Flexibility
  38. palkan_tula palkan Seattle.rb 2019 Action Policy has flexible architecture 38

  39. palkan_tula palkan Seattle.rb 2019 Action Policy has flexible architecture 39

  40. palkan_tula palkan Seattle.rb 2019 COMPONENTS 40 Behaviour authorize! allowed_to? authorized_scope

    Policy rules pre-checks scopes definitions
  41. palkan_tula palkan Seattle.rb 2019 BEHAVIOURS 41 Controllers (and views) Channels

    (Action Cable) GraphQL mutations and types
  42. palkan_tula palkan Seattle.rb 2019 BEHAVIOURS 42 Controllers (and views) Channels

    (Action Cable) GraphQL mutations and types Anything
  43. palkan_tula palkan Seattle.rb 2019 BEHAVIOUR 43 class PostUpdateAction include ActionPolicy

    ::Behaviour authorize :user attr_reader :user def initialize(user) @user = user end def call(post, params) authorize! post, to: :update? post.update!(params) end end
  44. palkan_tula palkan Seattle.rb 2019 BEHAVIOUR 44 class PostUpdateAction include ActionPolicy

    ::Behaviour authorize :user attr_reader :user def initialize(user) @user = user end def call(post, params) authorize! post, to: :update? post.update!(params) end end connect authorization layer
  45. palkan_tula palkan Seattle.rb 2019 BEHAVIOUR 45 class PostUpdateAction include ActionPolicy

    ::Behaviour authorize :user attr_reader :user def initialize(user) @user = user end def call(post, params) authorize! post, to: :update? post.update!(params) end end configure authorization context (what to pass to a policy)
  46. palkan_tula palkan Seattle.rb 2019 BEHAVIOUR 46 class PostUpdateAction include ActionPolicy

    ::Behaviour authorize :user attr_reader :user def initialize(user) @user = user end def call(post, params) authorize! post, to: :update? post.update!(params) end end apply authorization (resolve policy, resolve context, invoke rule)
  47. palkan_tula palkan Seattle.rb 2019 MODULARITY 47 Build your own features

    set
  48. palkan_tula palkan Seattle.rb 2019 POLICIES & RULES 48

  49. palkan_tula palkan Seattle.rb 2019 APPLICATION POLICY 49 class ApplicationPolicy include

    ActionPolicy ::Policy ::Core end implements the API required to work with behaviours
  50. palkan_tula palkan Seattle.rb 2019 APPLICATION POLICY 50 class ApplicationPolicy include

    ActionPolicy ::Policy ::Core include ActionPolicy ::Policy ::Authorization authorize :user end adds API to define the required context
  51. palkan_tula palkan Seattle.rb 2019 APPLICATION POLICY 51 class ApplicationPolicy include

    ActionPolicy ::Policy ::Core include ActionPolicy ::Policy ::Authorization authorize :user end requires behaviours to provide the “user” context, and adds the attr reader
  52. palkan_tula palkan Seattle.rb 2019 WHY CUSTOMIZE? 52 Boilerplate Performance Testability

    Flexibility
  53. palkan_tula palkan Seattle.rb 2019 BOILERPLATE 53 class RepoPolicy < ApplicationPolicy

    def update? user.admin? || (user.id == record.user_id) end alias edit? update? alias destroy? update? alias publish? update? alias unpublish? update? end
  54. palkan_tula palkan Seattle.rb 2019 APPLICATION POLICY 54 class ApplicationPolicy include

    ActionPolicy ::Policy ::Core include ActionPolicy ::Policy ::Authorization include ActionPolicy ::Policy ::Aliases authorize :user end
  55. palkan_tula palkan Seattle.rb 2019 BOILERPLATE 55 class RepoPolicy < ApplicationPolicy

    # manage? is a default (fallback) rule default_rule :manage? def manage? user.admin? || (user.id == record.user_id) end end
  56. palkan_tula palkan Seattle.rb 2019 BOILERPLATE 56 class RepoPolicy < ApplicationPolicy

    # or specify explicit aliases alias_rule :update?, :destroy?, :edit?, to: :manage? def manage? user.admin? || (user.id == record.user_id) end end
  57. palkan_tula palkan Seattle.rb 2019 BOILERPLATE 57 class CoursePolicy < ApplicationPolicy

    def show? admin? || manager? || owner? || assigned? end def update? admin? || assigned? end def destroy? admin? || manager? || owner? end end
  58. palkan_tula palkan Seattle.rb 2019 BOILERPLATE 58 class CoursePolicy < ApplicationPolicy

    def show? admin? || manager? || owner? || assigned? end def update? admin? || assigned? end def destroy? admin? || manager? || owner? end end
  59. palkan_tula palkan Seattle.rb 2019 APPLICATION POLICY 59 class ApplicationPolicy #

    ... include ActionPolicy ::Policy ::PreCheck pre_check :allow_admins def allow_admins allow! if user.admin? end end
  60. palkan_tula palkan Seattle.rb 2019 WHY CUSTOMIZE? 60 Boilerplate Performance Testability

    Flexibility, pt. 2
  61. palkan_tula palkan Seattle.rb 2019 SCOPES 61 class ProductsController < ApplicationController

    def index @products = authorized_scope(current_account.products) end def create @product = Product.new( authorized_scope(params.require(:product)) ) # ... end end
  62. palkan_tula palkan Seattle.rb 2019 SCOPES 62 class ProductsController < ApplicationController

    def index @products = authorized_scope(current_account.products) end def create @product = Product.new( authorized_scope(params.require(:product) ) # … end end Use scopes to “filter” collections according to the user’s permissions
  63. palkan_tula palkan Seattle.rb 2019 SCOPES 63 class ProductsController < ApplicationController

    def index @products = authorized_scope(current_account.products) end def create @product = Product.new( authorized_scope(params.require(:product) ) # ... end end Use scopes to permit parameters
  64. ONE METHOD, DIFFERENT DATA?

  65. palkan_tula palkan Seattle.rb 2019 ACTION POLICY SCOPES 65 General-purpose micro-framework

    Use for any kind of data (different scope types) Define matchers to automatically infer the scope type
  66. palkan_tula palkan Seattle.rb 2019 APPLICATION POLICY 66 class ApplicationPolicy #

    ... include ActionPolicy ::Policy ::Scoping end add scoping API
  67. palkan_tula palkan Seattle.rb 2019 SCOPES 67 class CoursePolicy < ApplicationPolicy

    scope_for :relation do |scope| if user.manager? scope.where(account: account) else scope.where(account: account).assigned(user) end end scope_for :relation, :own do |scope| next target.none if user.listener? scope.where(account: account, user: user) end end
  68. palkan_tula palkan Seattle.rb 2019 SCOPES 68 class CoursePolicy < ApplicationPolicy

    scope_for :relation do |scope| if user.manager? scope.where(account: account) else scope.where(account: account).assigned(user) end end scope_for :relation, :own do |scope| next target.none if user.listener? scope.where(account: account, user: user) end end
  69. palkan_tula palkan Seattle.rb 2019 SCOPES 69 class CoursePolicy < ApplicationPolicy

    scope_for :relation do |scope| if user.manager? scope.where(account: account) else scope.where(account: account).assigned(user) end end scope_for :relation, :own do |scope| next target.none if user.listener? scope.where(account: account, user: user) end end
  70. palkan_tula palkan Seattle.rb 2019 SCOPE MATCHERS 70 class ApplicationPolicy scope_matcher

    :action_controller_params, ActionController ::Parameters scope_matcher :active_record_relation, ActiveRecord ::Relation scope_matcher :active_record_relation, ->(target) { target < ActiveRecord ::Base } end
  71. palkan_tula palkan Seattle.rb 2019 SCOPE MATCHERS 71 def lookup_type_from_target(target) self.class.scope_matchers.detect

    do |(_type, matcher)| matcher === target end&.first end
  72. palkan_tula palkan Seattle.rb 2019 SCOPE MATCHERS 72 def lookup_type_from_target(target) self.class.scope_matchers.detect

    do |(_type, matcher)| matcher === target end&.first end hash of type to Module or Proc
  73. palkan_tula palkan Seattle.rb 2019 SCOPE MATCHERS 73 def lookup_type_from_target(target) self.class.scope_matchers.detect

    do |(_type, matcher)| matcher === target end&.first end Proc#=== ?
  74. palkan_tula palkan Seattle.rb 2019 SCOPE MATCHERS 74 def lookup_type_from_target(target) self.class.scope_matchers.detect

    do |(_type, matcher)| matcher === target end&.first end Proc#=== ?
  75. palkan_tula palkan Seattle.rb 2019 WHY CUSTOMIZE? 75 Boilerplate Performance Testability

    Flexibility
  76. palkan_tula palkan Seattle.rb 2019 “HEAVIER THAN HEAVEN” 76 class StagePolicy

    < ApplicationPolicy def show? full_access? || user.stage_permissions.where( stage_id: record.id ).exists? end def full_access? # slow SQL query we have no time to refactor end end
  77. palkan_tula palkan Seattle.rb 2019 77 CACHE LAYERS

  78. palkan_tula palkan Seattle.rb 2019 APPLICATION POLICY 78 class ApplicationPolicy #

    ... include ActionPolicy ::Policy ::Cache end cache the rule application result in the cache store
  79. palkan_tula palkan Seattle.rb 2019 CACHE STORE 79 # application.rb config.action_policy.cache_store

    = :redis_cache_store # or without Rails ActionPolicy.cache_store = MyCacheStore.new
  80. palkan_tula palkan Seattle.rb 2019 CACHE 80 class StagePolicy < ApplicationPolicy

    cache :show? def show? full_access? || user.stage_permissions.where( stage_id: record.id ).exists? end def full_access? # complex SQL end end use the cache store for this rule
  81. INVALIDATION?

  82. palkan_tula palkan Seattle.rb 2019 INVALIDATION 82 class Access < ApplicationRecord

    belongs_to :resource belongs_to :user after_commit :cleanup_policy_cache, on: [:create, :destroy] def cleanup_policy_cache ActionPolicy.cache_store.delete_matched( “policy_cache/ #{user_id}/“ \ “ #{ResourcePolicy.name}/ #{resource_id} /*” ) end end
  83. palkan_tula palkan Seattle.rb 2019 INVALIDATION 83 class Access < ApplicationRecord

    belongs_to :resource belongs_to :user after_commit :cleanup_policy_cache, on: [:create, :destroy] def cleanup_policy_cache ActionPolicy.cache_store.delete_matched( “policy_cache/ #{user_id}/“ \ “ #{ResourcePolicy.name}/ #{resource_id} /*” ) end end
  84. palkan_tula palkan Seattle.rb 2019 INVALIDATION 84 class Access < ApplicationRecord

    belongs_to :resource belongs_to :user after_commit :cleanup_policy_cache, on: [:create, :destroy] def cleanup_policy_cache ActionPolicy.cache_store.delete_matched( “policy_cache/ #{user_id}/“ \ “ #{ResourcePolicy.name}/ #{resource_id} /*” ) end end
  85. palkan_tula palkan Seattle.rb 2019 INVALIDATION 85 class User def policy_cache_key

    "user :: #{id} :: #{role_id}" end end class Resource def policy_cache_key " #{resource.class.name} :: #{id} :: #{access_updated_at}" end end
  86. palkan_tula palkan Seattle.rb 2019 WHY CUSTOMIZE? 86 Boilerplate Performance Testability

    Flexibility
  87. palkan_tula palkan Seattle.rb 2019 WHAT TO TEST 87 Test that

    the required authorization has been performed
  88. palkan_tula palkan Seattle.rb 2019 BEFORE ACTION POLICY 88 describe “GET

    #index” do subject { get :index, params: {account_slug: account.slug} } include_examples "account access" include_examples "permission access", "view_applicants" end 45% of controllers test – authorization tests
  89. palkan_tula palkan Seattle.rb 2019 ACTION POLICY 89 describe “GET #index”

    do subject { get :index, params: {account_slug: account.slug} } it "is authorized" do expect { subject } .to be_authorized_to(:index?).with(ApplicantPolicy) end end
  90. palkan_tula palkan Seattle.rb 2019 ACTION POLICY 90 describe “GET #index”

    do subject { get :index, params: {account_slug: account.slug} } it "is authorized" do expect { subject } .to be_authorized_to(:index?).with(ApplicantPolicy) end end Action Policy tracks all calls to `authorize!` in test env, then checks for matching authorization (no mocks/stubs)
  91. palkan_tula palkan Seattle.rb 2019 WHAT TO TEST 91 Test that

    the required authorization has been performed Test that the required scoping has been applied
  92. palkan_tula palkan Seattle.rb 2019 ACTION POLICY 92 @posts = authorized_scope(Post.all)

    expect { subject }.to have_authorized_scope(:relation) .with_policy(PostPolicy) .with_target { |target| expect(target).to eq(Post.all) }
  93. palkan_tula palkan Seattle.rb 2019 ACTION POLICY 93 @posts = authorized_scope(Post.all)

    expect { subject }.to have_authorized_scope(:relation) .with_policy(PostPolicy) .with_target { |target| expect(target).to eq(Post.all) }
  94. palkan_tula palkan Seattle.rb 2019 ACTION POLICY MINITEST 94 include ActionPolicy

    ::TestHelper def test_authorization_performed assert_authorized_to( :index?, Applicant, with: ApplicantPolicy) do get :index, params: {account_slug: account.slug} end assert_have_authorized_scope( type: :active_record_relation, with: PostPolicy) do get :index end.with_target do |target| assert_equal Post.all, target end end
  95. palkan_tula palkan Seattle.rb 2019 WHAT TO TEST 95 Test that

    the required authorization has been performed Test that the required scoping has been applied Test the authorization rules (policy classes)
  96. palkan_tula palkan Seattle.rb 2019 ONE YEAR AGO 96 1 describe

    “#show?" do 2 let(:record) { build_stubbed(:post, :active, global: true) } 3 let(:policy) { described_class.new(post, user: user) } 4 subject { policy.apply(:show?) } 5 it “succeeds” { is_expected.to eq true } 6 context "when non-active" do 7 before { record.active = false } 8 it "fails" { is_expected.to eq false } 9 end 10 context "when not for my city" do 11 before { post.city = build_stubbed :city } 12 it "fails" { is_expected.to eq false } 13 context "when manager" do 14 let(:user) { build_stubbed(:manager) } 15 it "succeeds" { is_expected.to eq true } 16 end 17 end 18 end 19 end
  97. palkan_tula palkan Seattle.rb 2019 TODAY 97 1 describe_rule :show? do

    2 let(:record) { build_stubbed(:post, :active, global: true) } 3 succeed "when active and global" 4 failed "when non-active" do 5 before { record.active = false } 6 end 7 failed "when not for my city" do 8 before { record.city = build_stubbed :city } 9 succeed "when user is a manager" do 10 let(:user) { build_stubbed(:manager) } 11 end 12 end 13 end 13 LOC vs 19 LOC
  98. palkan_tula palkan Seattle.rb 2019 BETTER ERRORS 98

  99. palkan_tula palkan Seattle.rb 2019 BETTER ERRORS 99

  100. DEBUGGING

  101. palkan_tula palkan Seattle.rb 2019 WHY DOES IT FAIL? 101 def

    rsvp? rsvp_opened? && show? && (seats_available? || rsvp_to_pack?) end
  102. palkan_tula palkan Seattle.rb 2019 WHY DOES IT FAIL? 102 def

    rsvp? binding.pry rsvp_opened? && show? && (seats_available? || rsvp_to_pack?) end
  103. palkan_tula palkan Seattle.rb 2019 WHY DOES IT FAIL? 103 pry>

    pp :rsvp? EventPolicy#rsvp? ↳ rsvp_opened? # => true AND show? # => false AND ( seats_available? # => true OR rsvp_to_pack? # => true )
  104. palkan_tula palkan Seattle.rb 2019 WHY DOES IT FAIL? 104 pry>

    pp :rsvp? EventPolicy#rsvp? ↳ rsvp_opened? # => true AND show? # => false AND ( seats_available? # => true OR rsvp_to_pack? # => true )
  105. palkan_tula palkan Seattle.rb 2019 WHY DOES IT FAIL? 105 pry>

    pp :show? EventPolicy#show? ↳ manager? # => false OR owner? # => false OR ( !record.draft? # => false AND invited? # => true )
  106. palkan_tula palkan Seattle.rb 2019 WHY DOES IT FAIL? 106 pry>

    pp :show? EventPolicy#show? ↳ manager? # => false OR owner? # => false OR ( !record.draft? # => false AND invited? # => true )
  107. palkan_tula palkan Seattle.rb 2019 HOW DOES IT WORK? 107 def

    print_method(object, method_name) ast = object.method(method_name).source.then(&Unparser.method(:parse)) # outer node is a method definition itself body = ast.children[2] Visitor.new(object).collect(body) end
  108. palkan_tula palkan Seattle.rb 2019 HOW DOES IT WORK? 108 def

    print_method(object, method_name) ast = object.method(method_name).source.then(&Unparser.method(:parse)) # outer node is a method definition itself body = ast.children[2] Visitor.new(object).collect(body) end gem “method_source” (required by “pry” or “railties”)
  109. palkan_tula palkan Seattle.rb 2019 HOW DOES IT WORK? 109 def

    print_method(object, method_name) ast = object.method(method_name).source.then(&Unparser.method(:parse)) # outer node is a method definition itself body = ast.children[2] Visitor.new(object).collect(body) end gem “unparser” (wrapper over “parser”, provides AST to code functionality)
  110. palkan_tula palkan Seattle.rb 2019 HOW DOES IT WORK? 110 def

    print_method(object, method_name) ast = object.method(method_name).source.then(&Unparser.method(:parse)) # outer node is a method definition itself body = ast.children[2] Visitor.new(object).collect(body) end
  111. DO YOU STILL THINK IT’S JUST LIKE PUNDIT?

  112. FAILURE REASONS and some GraphQL

  113. palkan_tula palkan Seattle.rb 2019 REASONS 113 Provide insights on why

    authorization attempt has been rejected Generate actionable/meaningful errors
  114. palkan_tula palkan Seattle.rb 2019 REASONS 114 class ApplicantPolicy < ApplicationPolicy

    def show? user.has_permission?(:view_applicants) && allowed_to?(:show?, stage) end end
  115. palkan_tula palkan Seattle.rb 2019 REASONS 115 class ApplicantPolicy < ApplicationPolicy

    def show? user.has_permission?(:view_applicants) && allowed_to?(:show?, stage) end end Two possible rejection reasons
  116. palkan_tula palkan Seattle.rb 2019 REASONS 116 class ApplicantPolicy < ApplicationPolicy

    def show? user.has_permission?(:view_applicants) && allowed_to?(:show?, stage) end end User has no required permission
  117. palkan_tula palkan Seattle.rb 2019 REASONS 117 class ApplicantPolicy < ApplicationPolicy

    def show? user.has_permission?(:view_applicants) && allowed_to?(:show?, stage) end end User doesn’t have an access to the stage. The allowed_to? method tracks the failed checks.
  118. palkan_tula palkan Seattle.rb 2019 REASONS 118 # in controller rescue_from

    ActionPolicy ::Unauthorized do |ex| # either p exception.reasons.details # => { stage: [:show?] } # or nothing (if the first check failed) p exception.reasons.details # => {} – that’s not good end
  119. palkan_tula palkan Seattle.rb 2019 REASONS 119 class ApplicantPolicy < ApplicationPolicy

    def show? allowed_to?(:view?) && allowed_to?(:show?, stage) end def view? user.has_permission?(:view_applicants) end end
  120. palkan_tula palkan Seattle.rb 2019 REASONS 120 # in controller rescue_from

    ActionPolicy ::Unauthorized do |ex| # either p exception.reasons.details # => { stage: [:show?] } # or p exception.reasons.details # => { applicant: [:view?] } end
  121. WHEN DO I NEED IT?

  122. palkan_tula palkan Seattle.rb 2019 GRAPHQL CASE 122 Clients want to

    know which actions are allowed to user Clients want to show different error messages depending on the rejection reason
  123. palkan_tula palkan Seattle.rb 2019 GRAPHQL CASE 123 def rsvp? check?(:no_rsvp_manager?)

    && check?(:rsvp_opened?) && show? && check?(:seats_available?) && allowed_to?(:rsvp_to_pack?) end
  124. palkan_tula palkan Seattle.rb 2019 GRAPHQL CASE 124 An alias for

    allowed_to? (i.e. also tracks the failures) def rsvp? check?(:no_rsvp_manager?) && check?(:rsvp_opened?) && show? && check?(:seats_available?) && allowed_to?(:rsvp_to_pack?) end
  125. palkan_tula palkan Seattle.rb 2019 GRAPHQL CASE 125 module Types class

    Event < Base include ActionPolicy ::Behaviour expose_authorization_rules :rsvp? # ... end end
  126. palkan_tula palkan Seattle.rb 2019 GRAPHQL CASE 126 module Types class

    Event < Base field :can_rsvp, Types ::AuthorizationResult, null: false def can_rsvp policy = policy_for(record: object) policy.apply(rule) policy.result end end end
  127. palkan_tula palkan Seattle.rb 2019 AUTHORIZATION RESULT 127

  128. palkan_tula palkan Seattle.rb 2019 FAILURE REASONS 128

  129. palkan_tula palkan Seattle.rb 2019 GRAPHQL CASE 129

  130. palkan_tula palkan Seattle.rb 2019 I18N 130 en: action_policy: policy: event:

    rsvp_opened ?: "RSVP has been closed for the event" seats_available ?: "This event is sold out"
  131. palkan_tula palkan Seattle.rb 2019 GRAPHQL CASE 131

  132. palkan_tula palkan Seattle.rb 2019 GRAPHQL CASE 132 def no_events_from_pack? user.

    rsvp_events. where(pack_id: record.pack_id). all?(&:grace_period_started?) || begin details[:name] = record.pack.name false end end
  133. palkan_tula palkan Seattle.rb 2019 GRAPHQL CASE 133 def no_events_from_pack? user.

    rsvp_events. where(pack_id: record.pack_id). all?(&:grace_period_started?) || begin details[:name] = record.pack.name false end end Additional details metadata
  134. palkan_tula palkan Seattle.rb 2019 GRAPHQL CASE 134 en: action_policy: policy:

    event: rsvp_to_pack ?: | "You've already RSVP'd to an event “ \ “in the %{name} series, “ \ "so you cannot RSVP to this event in advance"
  135. palkan_tula palkan Seattle.rb 2019 github.com/palkan/action_policy http://actionpolicy.evilmartians.io ACTION POLICY 135

  136. palkan_tula palkan Seattle.rb 2019 136 http://actionpolicy.evilmartians.io

  137. palkan_tula palkan Seattle.rb 2019 137 gem “action_policy-graphql” COMING SOON

  138. THANK YOU! Vladimir Dementyev @evilmartians evilmartians.com @palkan @palkan_tula