Slide 1

Slide 1 text

A DENIAL! A DENIAL! Vladimir Dementyev @evilmartians or why I’ve built yet another authorization framework?

Slide 2

Slide 2 text

…AND HOW

Slide 3

Slide 3 text

palkan_tula palkan Seattle.rb 2019 3 @palkan @palkan_tula Vladimir Dementyev 919 706 391 496

Slide 4

Slide 4 text

palkan_tula palkan Seattle.rb 2019 4

Slide 5

Slide 5 text

No content

Slide 6

Slide 6 text

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

Slide 7

Slide 7 text

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

Slide 8

Slide 8 text

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

Slide 9

Slide 9 text

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

Slide 10

Slide 10 text

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

Slide 11

Slide 11 text

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

Slide 12

Slide 12 text

palkan_tula palkan Seattle.rb 2019 AUTHORIZATION 12 Authentication System constraints Data model constraints (validations)

Slide 13

Slide 13 text

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)

Slide 14

Slide 14 text

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

Slide 15

Slide 15 text

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

Slide 16

Slide 16 text

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

Slide 17

Slide 17 text

palkan_tula palkan Seattle.rb 2019 SOLUTIONS and their problems 17

Slide 18

Slide 18 text

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

Slide 19

Slide 19 text

palkan_tula palkan Seattle.rb 2019 CANCAN(-CAN) 19 Ability class can? and authorize! helper authorize! :destroy, item # raises can? :destroy, item # predicate

Slide 20

Slide 20 text

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

Slide 21

Slide 21 text

palkan_tula palkan Seattle.rb 2019 21 Ability

Slide 22

Slide 22 text

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

Slide 23

Slide 23 text

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

Slide 24

Slide 24 text

palkan_tula palkan Seattle.rb 2019 POLICY IS PORO *Seattle.rb Slack 24

Slide 25

Slide 25 text

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

Slide 26

Slide 26 text

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

Slide 27

Slide 27 text

palkan_tula palkan Seattle.rb 2019 THE EVOLUTION 27 Start with CanCan Migrate to Pundit Write your own framework

Slide 28

Slide 28 text

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

Slide 29

Slide 29 text

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

Slide 30

Slide 30 text

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

Slide 31

Slide 31 text

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

Slide 32

Slide 32 text

palkan_tula palkan Seattle.rb 2019 ACTION POLICY 32

Slide 33

Slide 33 text

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

Slide 34

Slide 34 text

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

Slide 35

Slide 35 text

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

Slide 36

Slide 36 text

palkan_tula palkan Seattle.rb 2019 IT’S PUNDIT, ISN’T IT? https://github.com/palkan/action_policy/issues 36

Slide 37

Slide 37 text

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

Slide 38

Slide 38 text

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

Slide 39

Slide 39 text

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

Slide 40

Slide 40 text

palkan_tula palkan Seattle.rb 2019 COMPONENTS 40 Behaviour authorize! allowed_to? authorized_scope Policy rules pre-checks scopes definitions

Slide 41

Slide 41 text

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

Slide 42

Slide 42 text

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

Slide 43

Slide 43 text

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

Slide 44

Slide 44 text

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

Slide 45

Slide 45 text

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)

Slide 46

Slide 46 text

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)

Slide 47

Slide 47 text

palkan_tula palkan Seattle.rb 2019 MODULARITY 47 Build your own features set

Slide 48

Slide 48 text

palkan_tula palkan Seattle.rb 2019 POLICIES & RULES 48

Slide 49

Slide 49 text

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

Slide 50

Slide 50 text

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

Slide 51

Slide 51 text

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

Slide 52

Slide 52 text

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

Slide 53

Slide 53 text

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

Slide 54

Slide 54 text

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

Slide 55

Slide 55 text

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

Slide 56

Slide 56 text

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

Slide 57

Slide 57 text

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

Slide 58

Slide 58 text

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

Slide 59

Slide 59 text

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

Slide 60

Slide 60 text

palkan_tula palkan Seattle.rb 2019 WHY CUSTOMIZE? 60 Boilerplate Performance Testability Flexibility, pt. 2

Slide 61

Slide 61 text

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

Slide 62

Slide 62 text

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

Slide 63

Slide 63 text

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

Slide 64

Slide 64 text

ONE METHOD, DIFFERENT DATA?

Slide 65

Slide 65 text

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

Slide 66

Slide 66 text

palkan_tula palkan Seattle.rb 2019 APPLICATION POLICY 66 class ApplicationPolicy # ... include ActionPolicy ::Policy ::Scoping end add scoping API

Slide 67

Slide 67 text

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

Slide 68

Slide 68 text

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

Slide 69

Slide 69 text

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

Slide 70

Slide 70 text

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

Slide 71

Slide 71 text

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

Slide 72

Slide 72 text

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

Slide 73

Slide 73 text

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#=== ?

Slide 74

Slide 74 text

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#=== ?

Slide 75

Slide 75 text

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

Slide 76

Slide 76 text

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

Slide 77

Slide 77 text

palkan_tula palkan Seattle.rb 2019 77 CACHE LAYERS

Slide 78

Slide 78 text

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

Slide 79

Slide 79 text

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

Slide 80

Slide 80 text

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

Slide 81

Slide 81 text

INVALIDATION?

Slide 82

Slide 82 text

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

Slide 83

Slide 83 text

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

Slide 84

Slide 84 text

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

Slide 85

Slide 85 text

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

Slide 86

Slide 86 text

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

Slide 87

Slide 87 text

palkan_tula palkan Seattle.rb 2019 WHAT TO TEST 87 Test that the required authorization has been performed

Slide 88

Slide 88 text

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

Slide 89

Slide 89 text

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

Slide 90

Slide 90 text

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)

Slide 91

Slide 91 text

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

Slide 92

Slide 92 text

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) }

Slide 93

Slide 93 text

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) }

Slide 94

Slide 94 text

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

Slide 95

Slide 95 text

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)

Slide 96

Slide 96 text

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

Slide 97

Slide 97 text

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

Slide 98

Slide 98 text

palkan_tula palkan Seattle.rb 2019 BETTER ERRORS 98

Slide 99

Slide 99 text

palkan_tula palkan Seattle.rb 2019 BETTER ERRORS 99

Slide 100

Slide 100 text

DEBUGGING

Slide 101

Slide 101 text

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

Slide 102

Slide 102 text

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

Slide 103

Slide 103 text

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 )

Slide 104

Slide 104 text

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 )

Slide 105

Slide 105 text

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 )

Slide 106

Slide 106 text

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 )

Slide 107

Slide 107 text

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

Slide 108

Slide 108 text

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”)

Slide 109

Slide 109 text

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)

Slide 110

Slide 110 text

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

Slide 111

Slide 111 text

DO YOU STILL THINK IT’S JUST LIKE PUNDIT?

Slide 112

Slide 112 text

FAILURE REASONS and some GraphQL

Slide 113

Slide 113 text

palkan_tula palkan Seattle.rb 2019 REASONS 113 Provide insights on why authorization attempt has been rejected Generate actionable/meaningful errors

Slide 114

Slide 114 text

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

Slide 115

Slide 115 text

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

Slide 116

Slide 116 text

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

Slide 117

Slide 117 text

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.

Slide 118

Slide 118 text

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

Slide 119

Slide 119 text

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

Slide 120

Slide 120 text

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

Slide 121

Slide 121 text

WHEN DO I NEED IT?

Slide 122

Slide 122 text

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

Slide 123

Slide 123 text

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

Slide 124

Slide 124 text

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

Slide 125

Slide 125 text

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

Slide 126

Slide 126 text

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

Slide 127

Slide 127 text

palkan_tula palkan Seattle.rb 2019 AUTHORIZATION RESULT 127

Slide 128

Slide 128 text

palkan_tula palkan Seattle.rb 2019 FAILURE REASONS 128

Slide 129

Slide 129 text

palkan_tula palkan Seattle.rb 2019 GRAPHQL CASE 129

Slide 130

Slide 130 text

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"

Slide 131

Slide 131 text

palkan_tula palkan Seattle.rb 2019 GRAPHQL CASE 131

Slide 132

Slide 132 text

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

Slide 133

Slide 133 text

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

Slide 134

Slide 134 text

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"

Slide 135

Slide 135 text

palkan_tula palkan Seattle.rb 2019 github.com/palkan/action_policy http://actionpolicy.evilmartians.io ACTION POLICY 135

Slide 136

Slide 136 text

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

Slide 137

Slide 137 text

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

Slide 138

Slide 138 text

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