Upgrade to Pro — share decks privately, control downloads, hide ads and more …

[Seattle.rb 2019] A denial!

[Seattle.rb 2019] A denial!

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

http://actionpolicy.evilmartians.io/

Vladimir Dementyev

April 02, 2019
Tweet

More Decks by Vladimir Dementyev

Other Decks in Programming

Transcript

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

    View Slide

  2. …AND HOW

    View Slide

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

    View Slide

  4. palkan_tula
    palkan Seattle.rb 2019
    4

    View Slide

  5. View Slide

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

    View Slide

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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

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

    View Slide

  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)

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  17. palkan_tula
    palkan Seattle.rb 2019
    SOLUTIONS
    and their problems
    17

    View Slide

  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

    View Slide

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

    View Slide

  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

    View Slide

  21. palkan_tula
    palkan Seattle.rb 2019
    21
    Ability

    View Slide

  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

    View Slide

  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

    View Slide

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

    View Slide

  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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  32. palkan_tula
    palkan Seattle.rb 2019
    ACTION POLICY
    32

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  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

    View Slide

  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

    View Slide

  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)

    View Slide

  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)

    View Slide

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

    View Slide

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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  64. ONE METHOD,
    DIFFERENT DATA?

    View Slide

  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

    View Slide

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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  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

    View Slide

  77. palkan_tula
    palkan Seattle.rb 2019
    77
    CACHE LAYERS

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  81. INVALIDATION?

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

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

    View Slide

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

    View Slide

  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

    View Slide

  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

    View Slide

  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)

    View Slide

  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

    View Slide

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

    View Slide

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

    View Slide

  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

    View Slide

  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)

    View Slide

  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

    View Slide

  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

    View Slide

  98. palkan_tula
    palkan Seattle.rb 2019
    BETTER ERRORS
    98

    View Slide

  99. palkan_tula
    palkan Seattle.rb 2019
    BETTER ERRORS
    99

    View Slide

  100. DEBUGGING

    View Slide

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

    View Slide

  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

    View Slide

  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
    )

    View Slide

  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
    )

    View Slide

  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
    )

    View Slide

  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
    )

    View Slide

  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

    View Slide

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

    View Slide

  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)

    View Slide

  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

    View Slide

  111. DO YOU STILL THINK
    IT’S JUST LIKE PUNDIT?

    View Slide

  112. FAILURE REASONS
    and some GraphQL

    View Slide

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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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.

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  121. WHEN DO
    I NEED IT?

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

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

    View Slide

  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

    View Slide

  127. palkan_tula
    palkan Seattle.rb 2019
    AUTHORIZATION RESULT
    127

    View Slide

  128. palkan_tula
    palkan Seattle.rb 2019
    FAILURE REASONS
    128

    View Slide

  129. palkan_tula
    palkan Seattle.rb 2019
    GRAPHQL CASE
    129

    View Slide

  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"

    View Slide

  131. palkan_tula
    palkan Seattle.rb 2019
    GRAPHQL CASE
    131

    View Slide

  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

    View Slide

  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

    View Slide

  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"

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide