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

52cc8a838bf44a589d2572833b2dd1b9?s=128

Vlad Dem

April 17, 2018
Tweet

Transcript

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

    Rails
  2. @palkan @palkan_tula Vladimir Dementyev

  3. ! Moscow ✈ # New York Pittsburgh

  4. None
  5. https://evilmartians.com

  6. https://evilmartians.com

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

  9. Part 1 THEORY…

  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
  11. Authentication AUTHORIZATION

  12. AUTHENTICATION Who are you?

  13. AUTHORIZATION Am I allowed to do that?

  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
  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
  16. AUTHENTICATION rodauth warden authlogic clearance devise doorkeeper sorcery

  17. AUTHORIZATION cancancan rolify consul allowy walruz action_access pundit the_role trust

    eaco declarative_aithorization SimonSays canable kan
  18. None
  19. Authentication System constraints AUTHORIZATION

  20. EXAMPLE class ReposController < AppController def create if current_account.available_repos.zero? head

    :payment_required end # ... end end constraint check
  21. Authentication System constraints Data model constraints (validations) AUTHORIZATION

  22. LINES OF DEFENCE authentication authorization constraints validations

  23. How to grant/revoke access? Authorization model (roles, permission, accesses) How

    to verify access? Authorization layer (policies, rules) AUTHORIZATION
  24. FORMAL MODELS DAC MAC RBAC ABAC

  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)
  26. user.can?(:read, @resource) # => is transformed to @resource.security_level <= user.security_clearance

    MANDATORY (MAC) Security clearance vs. security level
  27. ROLE-BASED (RBAC) https://github.com/the-teacher/the_role

  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
  29. ABAC https://csrc.nist.gov/projects/abac/

  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)
  31. … AND PRACTICE FOR ALL Part 2

  32. IN RAILS

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

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

    resolvers
  35. TOOLS CanCan(-Can) Pundit

  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
  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
  38. Ability

  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
  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
  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
  42. WHY CUSTOMIZE? http://gemcheck.evilmartians.io Boilerplate Performance Testing Flexibility

  43. FRAMEWORK OF A DREAM Part 3

  44. THE QUESTION Why does Rails not provide an authorization solution

    out-of-the-box?
  45. THE ANSWER? gem "action_policy"

  46. Pundit, re-visited Born in Hell in production Pre-release published today

    ACTION POLICY https://github.com/palkan/action_policy
  47. ACTION POLICY

  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
  49. class ProductsController < ApplicationController def index # non-raising predicate method

    if allowed_to?(:create?) @tags = current_account.tags end end end ACTION POLICY
  50. Let’s talk about BOILERplate

  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
  52. BOILERPLATE # With ActionPolicy class ApplicationPolicy < ActionPolicy ::Base pre_check

    :allow_admins private def allow_admins allow! if user.admin? end end
  53. BOILERPLATE # With ActionPolicy class CoursePolicy < ApplicationPolicy def show?

    manager? || owner? || assigned? end def update? assigned? end def destroy? manager? || owner? end end
  54. BOILERPLATE # With ActionPolicy class UserPolicy < ApplicationPolicy skip_pre_check :allow_admins,

    only: :destroy? def destroy? (admin? || !record.admin?) && manage? end end
  55. BOILERPLATE # With Pundit class ProductsController < ApplicationController def create

    authorize Product end end
  56. BOILERPLATE # With ActionPolicy class ProductsController < ApplicationController def create

    # target class is inferred from controller authorize! end end CONVENTION OVER CONFIGURATION
  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
  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
  59. PERFORMANCE © https://www.railsspeed.com

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

  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”
  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
  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
  64. None
  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
  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
  67. None
  68. TESTABILITY How to test authorization?

  69. 100% COVERAGE

  70. 100% CODE COVERAGE

  71. 100% BUSINESS LOGIC COVERAGE

  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
  73. CASE: TESTING CONTROLLERS 0 15 30 45 60 % of

    tests Other Access
  74. CASE: TESTING CONTROLLERS 0 1 2 3 4 5 time

    to run (minutes) Other Access
  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
  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
  77. MORE FEATURES

  78. MORE FEATURES Scopes

  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
  80. SCOPES class CoursesController < ApplicationController def index # default scope

    @courses = scoped(Course.all) # named scope @courses = scoped(Course.all, :own) end end
  81. SCOPES

  82. MORE FEATURES Scopes Namespaces

  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
  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
  85. MORE FEATURES Scopes Namespaces i18n

  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
  87. MORE FEATURES Scopes Namespaces i18n Failure reasons

  88. REASONS class ApplicantPolicy < ApplicationPolicy def show? allowed_to?(:view?) && allowed_to?(:show?,

    stage) end def view? user.has_permission?(:view_applicants) end end
  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
  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
  91. MORE FEATURES authorize! everywhere

  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!
  93. MORE FEATURES authorize! everywhere Authorization contexts

  94. CONTEXTS class ApplicationPolicy < ActionPolicy ::Base authorize :account end class

    ApplicationController < ActionController ::Base authorize :account, through: :current_account end
  95. github.com/palkan/action_policy http://actionpolicy.evilmartians.io ACTION POLICY

  96. http://actionpolicy.evilmartians.io

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