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

[RailsConf 2018] Access Denied: the missing guide to authorization in Rails

[RailsConf 2018] Access Denied: the missing guide to authorization in Rails

Video: https://www.youtube.com/watch?v=NVwx0DARDis

http://actionpolicy.evilmartians.io
https://github.com/palkan/action_policy
https://twitter.com/palkan_tula

Rails brings us a lot of useful tools out-of-the-box, but there are missing parts too. For example, for such essential tasks as authorization we are on our own. Even if we choose a trending OSS solution, we still have to care about the way to keep our code maintainable, efficient, and, of course, bug-less.

Working on Rails projects, I've noticed some common patterns in designing access systems as well as useful code techniques I'd like to share with you in this talk.

Vladimir Dementyev

April 17, 2018
Tweet

More Decks by Vladimir Dementyev

Other Decks in Programming

Transcript

  1. ACCESS DENIED
    Vladimir
    Dementyev
    The missing guide to
    authorization in Rails

    View Slide

  2. @palkan
    @palkan_tula
    Vladimir Dementyev

    View Slide

  3. ! Moscow

    # New York

    Pittsburgh

    View Slide

  4. View Slide

  5. https://evilmartians.com

    View Slide

  6. https://evilmartians.com

    View Slide

  7. View Slide

  8. Brooklyn, NY
    https://evilmartians.com

    View Slide

  9. Part 1
    THEORY…

    View Slide

  10. AUTHORIZATION
    Official permission for something to happen,
    or the act of giving someone official
    permission to do something
    https://dictionary.cambridge.org/dictionary/english/authorization

    View Slide

  11. Authentication
    AUTHORIZATION

    View Slide

  12. AUTHENTICATION
    Who are you?

    View Slide

  13. AUTHORIZATION
    Am I allowed to do that?

    View Slide

  14. EXAMPLE
    class AppController < ActionController ::Base
    before_action :authenticate_user!
    def authenticate_user!
    @current_user = User.find_by(
    token: params[:token]
    ) || head :unauthorized
    end
    end
    authentication

    View Slide

  15. EXAMPLE
    class UsersController < ApplicationController
    def destroy
    item = Item.find(params[:id])
    if item.owner_id == current_user.id
    item.destroy!
    head :ok
    end else head :forbidden
    end
    end
    authorization

    View Slide

  16. AUTHENTICATION
    rodauth
    warden authlogic
    clearance
    devise
    doorkeeper
    sorcery

    View Slide

  17. AUTHORIZATION
    cancancan
    rolify
    consul
    allowy
    walruz
    action_access
    pundit
    the_role
    trust
    eaco
    declarative_aithorization
    SimonSays
    canable
    kan

    View Slide

  18. View Slide

  19. Authentication
    System constraints
    AUTHORIZATION

    View Slide

  20. EXAMPLE
    class ReposController < AppController
    def create
    if current_account.available_repos.zero?
    head :payment_required
    end
    # ...
    end
    end
    constraint check

    View Slide

  21. Authentication
    System constraints
    Data model constraints
    (validations)
    AUTHORIZATION

    View Slide

  22. LINES
    OF
    DEFENCE
    authentication
    authorization
    constraints
    validations

    View Slide

  23. How to grant/revoke access?
    Authorization model (roles, permission, accesses)
    How to verify access?
    Authorization layer (policies, rules)
    AUTHORIZATION

    View Slide

  24. FORMAL
    MODELS
    DAC
    MAC
    RBAC
    ABAC

    View Slide

  25. User Permission Resource
    1 0,* 0,* 1
    @resource.permissions.create!(user: user, activity: :read)
    user.can?(:read, @resource)
    # => is transformed to
    @resource.permissions.exists?(user: user, activity: :read)
    DISCRETIONARY (DAC)

    View Slide

  26. user.can?(:read, @resource)
    # => is transformed to
    @resource.security_level <= user.security_clearance
    MANDATORY (MAC)
    Security clearance vs. security level

    View Slide

  27. ROLE-BASED (RBAC)
    https://github.com/the-teacher/the_role

    View Slide

  28. ATTRIBUTE-BASED
    [
    {
    "resource": "TestApp ::Book",
    "action": ["read"],
    "description": "Allow owners to read their books",
    "effect": "allow",
    "conditions": [
    {
    "equal": {
    "resource ::owner ::id": ["user ::id"]
    }
    }
    ]
    }
    ]
    https://github.com/TheClimateCorporation/iron_hide

    View Slide

  29. ABAC
    https://csrc.nist.gov/projects/abac/

    View Slide

  30. How to grant/revoke access?
    Authorization model (roles, permission, accesses)
    How to verify access?
    Authorization layer (policies, rules)
    AUTHORIZATION
    How to verify access?
    Authorization layer (policies, rules)

    View Slide

  31. … AND
    PRACTICE
    FOR ALL
    Part 2

    View Slide

  32. IN RAILS

    View Slide

  33. IN RAILS
    Controllers
    Views (templates, serializers)
    Channels (Action Cable)

    View Slide

  34. IN RAILS
    Controllers
    Views (templates, serializers)
    Channels (Action Cable)
    GraphQL resolvers

    View Slide

  35. TOOLS
    CanCan(-Can)
    Pundit

    View Slide

  36. WHAT PEOPLE SAY
    ✅ Easy to use
    ✅ Simple, readable config
    ✅ Great community & docs
    CanCanCan Pundit
    ✅ Pure OOP
    ✅ No magic
    ✅ Easy to test
    * “Authorization in Rails” survey bit.ly/rails-auth

    View Slide

  37. CANCAN
    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

  38. Ability

    View Slide

  39. PUNDIT
    class QuestionPolicy < ApplicationPolicy
    def index?
    true
    end
    def create?
    true
    end
    def update?
    user.admin? || (user.id == target.user_id)
    end
    alias edit? update?
    alias destroy? update?
    end

    View Slide

  40. WHAT PEOPLE SAY
    ❌ Framework-specific
    ❌ A lot of magic
    CanCanCan Pundit
    ❌ A lot of duplication
    ❌ No namespaces support
    * “Authorization in Rails” survey bit.ly/rails-auth

    View Slide

  41. THE EVOLUTION
    Start with CanCan
    Realize that it becomes more difficult
    to maintain abilities as project grows
    Migrate to Pundit
    Customize it again and again

    View Slide

  42. WHY CUSTOMIZE?
    http://gemcheck.evilmartians.io
    Boilerplate
    Performance
    Testing
    Flexibility

    View Slide

  43. FRAMEWORK
    OF A DREAM
    Part 3

    View Slide

  44. THE QUESTION
    Why does Rails not provide an
    authorization solution out-of-the-box?

    View Slide

  45. THE ANSWER?
    gem "action_policy"

    View Slide

  46. Pundit, re-visited
    Born in Hell in production
    Pre-release published today
    ACTION POLICY
    https://github.com/palkan/action_policy

    View Slide

  47. ACTION POLICY

    View Slide

  48. 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

    View Slide

  49. class ProductsController < ApplicationController
    def index
    # non-raising predicate method
    if allowed_to?(:create?)
    @tags = current_account.tags
    end
    end
    end
    ACTION POLICY

    View Slide

  50. Let’s talk about
    BOILERplate

    View Slide

  51. BOILERPLATE
    # With Pundit
    class CoursePolicy < ApplicationPolicy
    def show?
    admin? || manager? || owner? || assigned?
    end
    def update?
    admin? || assigned?
    end
    def destroy?
    admin? || manager? || owner?
    end
    end

    View Slide

  52. BOILERPLATE
    # With ActionPolicy
    class ApplicationPolicy < ActionPolicy ::Base
    pre_check :allow_admins
    private
    def allow_admins
    allow! if user.admin?
    end
    end

    View Slide

  53. BOILERPLATE
    # With ActionPolicy
    class CoursePolicy < ApplicationPolicy
    def show?
    manager? || owner? || assigned?
    end
    def update?
    assigned?
    end
    def destroy?
    manager? || owner?
    end
    end

    View Slide

  54. BOILERPLATE
    # With ActionPolicy
    class UserPolicy < ApplicationPolicy
    skip_pre_check :allow_admins, only: :destroy?
    def destroy?
    (admin? || !record.admin?) && manage?
    end
    end

    View Slide

  55. BOILERPLATE
    # With Pundit
    class ProductsController < ApplicationController
    def create
    authorize Product
    end
    end

    View Slide

  56. BOILERPLATE
    # With ActionPolicy
    class ProductsController < ApplicationController
    def create
    # target class is inferred from controller
    authorize!
    end
    end
    CONVENTION
    OVER
    CONFIGURATION

    View Slide

  57. BOILERPLATE
    # With ActionPolicy
    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

  58. BOILERPLATE
    # With ActionPolicy
    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

  59. PERFORMANCE
    © https://www.railsspeed.com

    View Slide

  60. PERFORMANCE
    How to measure?
    What to measure?
    How to fix?

    View Slide

  61. ActiveSupport ::Notifications
    “action_policy.authorize”
    # fires “action_policy.authorize" and
    “action_policy.apply” events
    authorize! @product
    # fires “action_policy.apply” event
    allowed_to?(:edit?, @product)
    “action_policy.apply”

    View Slide

  62. ActiveSupport ::Notifications
    “action_policy.apply”
    ActiveSupport ::Notifications.subscribe(“action_policy.apply") do
    |event, started, finished, _, data|
    timing = ((finished - started) * 1000).to_i
    $stdout.puts "authorize ##{event[:policy]} ##{event[:rule]}={timing}ms"
    end

    View Slide

  63. N+1 AUTHORIZATION
    14 Mar 2018 14:08:30.722241 <190>1 2018-03-14T11:08:30.349156+00:00 app web.1 - - [1bb8d912-308a-4966-b436-a407ef34bac2] Started GET "/test_account/openings/public-funnel/dashboard" for 185.13.112.107 at 2018-03-14 11:08:30 +0000
    14 Mar 2018 14:08:30.722186 <190>1 2018-03-14T11:08:30.358367+00:00 app web.1 - - [1bb8d912-308a-4966-b436-a407ef34bac2] [U#6] Processing by DashboardsController#show as HTML
    14 Mar 2018 14:08:30.722216 <190>1 2018-03-14T11:08:30.358391+00:00 app web.1 - - [1bb8d912-308a-4966-b436-a407ef34bac2] [U#6] Parameters: {"account_slug"=>"test_account", "opening_id"=>"public-funnel"}
    14 Mar 2018 14:08:30.722174 <190>1 2018-03-14T11:08:30.368142+00:00 app web.1 - - [1bb8d912-308a-4966-b436-a407ef34bac2] [U#6] measure#action_policy.scope.hit=0ms
    14 Mar 2018 14:08:30.722175 <190>1 2018-03-14T11:08:30.399348+00:00 app web.1 - - [1bb8d912-308a-4966-b436-a407ef34bac2] [U#6] measure#action_policy.scope.miss=6ms
    14 Mar 2018 14:08:30.722176 <190>1 2018-03-14T11:08:30.407044+00:00 app web.1 - - [1bb8d912-308a-4966-b436-a407ef34bac2] [U#6] measure#action_policy.scope.miss=21ms
    14 Mar 2018 14:08:30.722174 <190>1 2018-03-14T11:08:30.441693+00:00 app web.1 - - [1bb8d912-308a-4966-b436-a407ef34bac2] [U#6] measure#action_policy.scope.hit=0ms
    14 Mar 2018 14:08:30.722175 <190>1 2018-03-14T11:08:30.471756+00:00 app web.1 - - [1bb8d912-308a-4966-b436-a407ef34bac2] [U#6] measure#action_policy.check.miss=2ms
    14 Mar 2018 14:08:30.722174 <190>1 2018-03-14T11:08:30.474080+00:00 app web.1 - - [1bb8d912-308a-4966-b436-a407ef34bac2] [U#6] measure#action_policy.check.hit=0ms
    14 Mar 2018 14:08:30.722175 <190>1 2018-03-14T11:08:30.504566+00:00 app web.1 - - [1bb8d912-308a-4966-b436-a407ef34bac2] [U#6] measure#action_policy.check.miss=1ms
    14 Mar 2018 14:08:30.722175 <190>1 2018-03-14T11:08:30.505893+00:00 app web.1 - - [1bb8d912-308a-4966-b436-a407ef34bac2] [U#6] measure#action_policy.check.miss=1ms
    14 Mar 2018 14:08:30.722175 <190>1 2018-03-14T11:08:30.507222+00:00 app web.1 - - [1bb8d912-308a-4966-b436-a407ef34bac2] [U#6] measure#action_policy.check.miss=1ms
    14 Mar 2018 14:08:30.722175 <190>1 2018-03-14T11:08:30.508522+00:00 app web.1 - - [1bb8d912-308a-4966-b436-a407ef34bac2] [U#6] measure#action_policy.check.miss=1ms
    14 Mar 2018 14:08:30.722175 <190>1 2018-03-14T11:08:30.509846+00:00 app web.1 - - [1bb8d912-308a-4966-b436-a407ef34bac2] [U#6] measure#action_policy.check.miss=1ms
    14 Mar 2018 14:08:30.722175 <190>1 2018-03-14T11:08:30.511189+00:00 app web.1 - - [1bb8d912-308a-4966-b436-a407ef34bac2] [U#6] measure#action_policy.check.miss=1ms
    14 Mar 2018 14:08:30.796175 <190>1 2018-03-14T11:08:30.512857+00:00 app web.1 - - [1bb8d912-308a-4966-b436-a407ef34bac2] [U#6] measure#action_policy.check.miss=1ms
    14 Mar 2018 14:08:30.796174 <190>1 2018-03-14T11:08:30.523828+00:00 app web.1 - - [1bb8d912-308a-4966-b436-a407ef34bac2] [U#6] measure#action_policy.check.hit=0ms
    14 Mar 2018 14:08:30.796174 <190>1 2018-03-14T11:08:30.527719+00:00 app web.1 - - [1bb8d912-308a-4966-b436-a407ef34bac2] [U#6] measure#action_policy.check.hit=0ms
    14 Mar 2018 14:08:30.796174 <190>1 2018-03-14T11:08:30.529970+00:00 app web.1 - - [1bb8d912-308a-4966-b436-a407ef34bac2] [U#6] measure#action_policy.check.hit=0ms
    14 Mar 2018 14:08:31.295174 <190>1 2018-03-14T11:08:30.928734+00:00 app web.1 - - [1bb8d912-308a-4966-b436-a407ef34bac2] [U#6] measure#action_policy.check.hit=0ms
    14 Mar 2018 14:08:31.295174 <190>1 2018-03-14T11:08:31.039388+00:00 app web.1 - - [1bb8d912-308a-4966-b436-a407ef34bac2] [U#6] measure#action_policy.check.hit=0ms
    14 Mar 2018 14:08:31.295174 <190>1 2018-03-14T11:08:31.040101+00:00 app web.1 - - [1bb8d912-308a-4966-b436-a407ef34bac2] [U#6] measure#action_policy.check.hit=0ms
    14 Mar 2018 14:08:31.295174 <190>1 2018-03-14T11:08:31.137807+00:00 app web.1 - - [1bb8d912-308a-4966-b436-a407ef34bac2] [U#6] measure#action_policy.check.hit=0ms
    14 Mar 2018 14:08:31.295174 <190>1 2018-03-14T11:08:31.138677+00:00 app web.1 - - [1bb8d912-308a-4966-b436-a407ef34bac2] [U#6] measure#action_policy.check.hit=0ms
    14 Mar 2018 14:08:31.295174 <190>1 2018-03-14T11:08:31.139952+00:00 app web.1 - - [1bb8d912-308a-4966-b436-a407ef34bac2] [U#6] measure#action_policy.check.hit=0ms
    14 Mar 2018 14:08:31.295174 <190>1 2018-03-14T11:08:31.140577+00:00 app web.1 - - [1bb8d912-308a-4966-b436-a407ef34bac2] [U#6] measure#action_policy.check.hit=0ms
    14 Mar 2018 14:08:31.295174 <190>1 2018-03-14T11:08:31.141658+00:00 app web.1 - - [1bb8d912-308a-4966-b436-a407ef34bac2] [U#6] measure#action_policy.check.hit=0ms
    14 Mar 2018 14:08:31.295174 <190>1 2018-03-14T11:08:31.142342+00:00 app web.1 - - [1bb8d912-308a-4966-b436-a407ef34bac2] [U#6] measure#action_policy.check.hit=0ms
    14 Mar 2018 14:08:31.295174 <190>1 2018-03-14T11:08:31.143228+00:00 app web.1 - - [1bb8d912-308a-4966-b436-a407ef34bac2] [U#6] measure#action_policy.check.hit=0ms
    14 Mar 2018 14:08:31.295174 <190>1 2018-03-14T11:08:31.143837+00:00 app web.1 - - [1bb8d912-308a-4966-b436-a407ef34bac2] [U#6] measure#action_policy.check.hit=0ms
    14 Mar 2018 14:08:31.295174 <190>1 2018-03-14T11:08:31.144657+00:00 app web.1 - - [1bb8d912-308a-4966-b436-a407ef34bac2] [U#6] measure#action_policy.check.hit=0ms
    14 Mar 2018 14:08:31.295174 <190>1 2018-03-14T11:08:31.145339+00:00 app web.1 - - [1bb8d912-308a-4966-b436-a407ef34bac2] [U#6] measure#action_policy.check.hit=0ms
    14 Mar 2018 14:08:31.295174 <190>1 2018-03-14T11:08:31.146206+00:00 app web.1 - - [1bb8d912-308a-4966-b436-a407ef34bac2] [U#6] measure#action_policy.check.hit=0ms
    14 Mar 2018 14:08:31.295174 <190>1 2018-03-14T11:08:31.146852+00:00 app web.1 - - [1bb8d912-308a-4966-b436-a407ef34bac2] [U#6] measure#action_policy.check.hit=0ms
    14 Mar 2018 14:08:31.295174 <190>1 2018-03-14T11:08:31.147671+00:00 app web.1 - - [1bb8d912-308a-4966-b436-a407ef34bac2] [U#6] measure#action_policy.check.hit=0ms
    14 Mar 2018 14:08:31.295174 <190>1 2018-03-14T11:08:31.148303+00:00 app web.1 - - [1bb8d912-308a-4966-b436-a407ef34bac2] [U#6] measure#action_policy.check.hit=0ms
    14 Mar 2018 14:08:31.295174 <190>1 2018-03-14T11:08:31.149119+00:00 app web.1 - - [1bb8d912-308a-4966-b436-a407ef34bac2] [U#6] measure#action_policy.check.hit=0ms
    14 Mar 2018 14:08:31.295174 <190>1 2018-03-14T11:08:31.149849+00:00 app web.1 - - [1bb8d912-308a-4966-b436-a407ef34bac2] [U#6] measure#action_policy.check.hit=0ms
    14 Mar 2018 14:08:31.295174 <190>1 2018-03-14T11:08:31.150702+00:00 app web.1 - - [1bb8d912-308a-4966-b436-a407ef34bac2] [U#6] measure#action_policy.check.hit=0ms
    14 Mar 2018 14:08:31.295174 <190>1 2018-03-14T11:08:31.151359+00:00 app web.1 - - [1bb8d912-308a-4966-b436-a407ef34bac2] [U#6] measure#action_policy.check.hit=0ms
    14 Mar 2018 14:08:31.295174 <190>1 2018-03-14T11:08:31.152254+00:00 app web.1 - - [1bb8d912-308a-4966-b436-a407ef34bac2] [U#6] measure#action_policy.check.hit=0ms
    14 Mar 2018 14:08:31.295174 <190>1 2018-03-14T11:08:31.152879+00:00 app web.1 - - [1bb8d912-308a-4966-b436-a407ef34bac2] [U#6] measure#action_policy.check.hit=0ms
    14 Mar 2018 14:08:31.295174 <190>1 2018-03-14T11:08:31.153761+00:00 app web.1 - - [1bb8d912-308a-4966-b436-a407ef34bac2] [U#6] measure#action_policy.check.hit=0ms
    14 Mar 2018 14:08:31.295174 <190>1 2018-03-14T11:08:31.154353+00:00 app web.1 - - [1bb8d912-308a-4966-b436-a407ef34bac2] [U#6] measure#action_policy.check.hit=0ms
    14 Mar 2018 14:08:31.370174 <190>1 2018-03-14T11:08:31.155193+00:00 app web.1 - - [1bb8d912-308a-4966-b436-a407ef34bac2] [U#6] measure#action_policy.check.hit=0ms
    14 Mar 2018 14:08:31.370174 <190>1 2018-03-14T11:08:31.155826+00:00 app web.1 - - [1bb8d912-308a-4966-b436-a407ef34bac2] [U#6] measure#action_policy.check.hit=0ms
    14 Mar 2018 14:08:31.370174 <190>1 2018-03-14T11:08:31.156625+00:00 app web.1 - - [1bb8d912-308a-4966-b436-a407ef34bac2] [U#6] measure#action_policy.check.hit=0ms
    14 Mar 2018 14:08:31.370174 <190>1 2018-03-14T11:08:31.157286+00:00 app web.1 - - [1bb8d912-308a-4966-b436-a407ef34bac2] [U#6] measure#action_policy.check.hit=0ms
    45 policy checks per request

    View Slide

  64. View Slide

  65. CACHE STORE
    # application.rb
    config.action_policy.cache_store = :redis_cache_store
    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

    View Slide

  66. ActiveSupport ::Notifications
    “action_policy.apply”
    ActiveSupport ::Notifications.subscribe("action_policy.apply") do
    |event, started, finished, _, data|
    measurement = " #{event}. #{(data[:cached] ? "hit" : "miss")}"
    timing = ((finished - started) * 1000).to_i
    $stdout.puts "measure ##{measurement}= #{timing}ms"
    end

    View Slide

  67. View Slide

  68. TESTABILITY
    How to test authorization?

    View Slide

  69. 100%
    COVERAGE

    View Slide

  70. 100%
    CODE
    COVERAGE

    View Slide

  71. 100%
    BUSINESS LOGIC
    COVERAGE

    View Slide

  72. THE SURVEY
    Where do you test authorization logic?
    % of answers
    0 17,5 35 52,5 70
    requests/controllers tests unit tests (e.g. policy tests) system tests other
    * “Authorization in Rails” survey bit.ly/rails-auth

    View Slide

  73. CASE: TESTING CONTROLLERS
    0
    15
    30
    45
    60
    % of tests
    Other Access

    View Slide

  74. CASE: TESTING CONTROLLERS
    0
    1
    2
    3
    4
    5
    time to run (minutes)
    Other Access

    View Slide

  75. ACTION POLICY
    # in controller/request specs
    subject { patch :update, id: product.id }
    it "is authorized" do
    expect { subject }
    .to be_authorized_to(:manage?, product)
    .with(ProductPolicy)
    end

    View Slide

  76. # PORO unit tests
    describe StagePolicy do
    let(:user) { build_stubbed :user }
    let(:record) { build_stubbed :stage }
    let(:policy) { described_class.new(record, user: user) }
    describe '#show?' do
    subject { policy.show? }
    it { is_expected.to be true}
    end
    end
    ACTION POLICY

    View Slide

  77. MORE
    FEATURES

    View Slide

  78. MORE FEATURES
    Scopes

    View Slide

  79. SCOPES
    class CoursePolicy < ApplicationPolicy
    default_scope do
    if user.manager?
    scope.where(account: account)
    else
    scope.where(account: account).assigned(user)
    end
    end
    # Named scope
    scope :own do
    next target.none if user.listener?
    scope.where(account: account, user: user)
    end
    end

    View Slide

  80. SCOPES
    class CoursesController < ApplicationController
    def index
    # default scope
    @courses = scoped(Course.all)
    # named scope
    @courses = scoped(Course.all, :own)
    end
    end

    View Slide

  81. SCOPES

    View Slide

  82. MORE FEATURES
    Scopes
    Namespaces

    View Slide

  83. NAMESPACES
    module Admin
    class ProductsController < ApplicationController
    def show
    @product = Product.find(params[:id])
    # Tries to use load Admin ::ProductPolicy first
    # and then fallbacks to ::ProductPolicy
    authorize! @product
    end
    end
    end

    View Slide

  84. NAMESPACES
    class PittsburghersController < ApplicationController
    def authorization_namespace
    return ::Penguins if current_user.hockey_fan?
    return ::Pirates if current_user.baseball_fan?
    super
    end
    end

    View Slide

  85. MORE FEATURES
    Scopes
    Namespaces
    i18n

    View Slide

  86. I18N
    # in locales
    en:
    action_policy:
    course:
    take: "You are not allowed to view this course"
    # in controller
    rescue_from ActionPolicy ::Unauthorized do |ex|
    p ex.message
    # => "You are not allowed to view this course”
    end

    View Slide

  87. MORE FEATURES
    Scopes
    Namespaces
    i18n
    Failure reasons

    View Slide

  88. REASONS
    class ApplicantPolicy < ApplicationPolicy
    def show?
    allowed_to?(:view?) &&
    allowed_to?(:show?, stage)
    end
    def view?
    user.has_permission?(:view_applicants)
    end
    end

    View Slide

  89. REASONS
    # in controller
    rescue_from ActionPolicy ::Unauthorized do
    |exception|
    # either
    p exception.reasons.messages # => { stage: [:show] }
    # or
    p exception.reasons.messages # => { applicant: [:view] }
    end

    View Slide

  90. REASONS & I18N
    # in locales
    en:
    action_policy:
    applicant:
    view: "You don’t have permission to view applicants"
    # in controller
    rescue_from ActionPolicy ::Unauthorized do
    |exception|
    p exception.reasons.full_messages
    # => ["You don’t have permission to view applicants"]
    end

    View Slide

  91. MORE FEATURES
    authorize! everywhere

    View Slide

  92. class PostUpdateAction
    include ActionPolicy ::Behaviour
    # provide authorization subject (performer)
    authorize :user
    attr_reader :user
    def initialize(user)
    @user = user
    end
    def call(post, params)
    authorize! post, to: :update?
    post.update!(params)
    end
    end
    authorize!

    View Slide

  93. MORE FEATURES
    authorize! everywhere
    Authorization contexts

    View Slide

  94. CONTEXTS
    class ApplicationPolicy < ActionPolicy ::Base
    authorize :account
    end
    class ApplicationController < ActionController ::Base
    authorize :account, through: :current_account
    end

    View Slide

  95. github.com/palkan/action_policy
    http://actionpolicy.evilmartians.io
    ACTION POLICY

    View Slide

  96. http://actionpolicy.evilmartians.io

    View Slide

  97. THANK
    YOU!
    Vladimir Dementyev
    evilmartians.com
    @palkan
    @palkan_tula
    Brooklyn, NY

    View Slide