pundit 2.0.0 → 2.1.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6b973703e2b1653c804d138fb24c204807e41885a81f5b172656d032d7c2a75a
4
- data.tar.gz: 01211cab222a4c16f274e041c074b2d2a1763a4943ab26b6dba938272297a48d
3
+ metadata.gz: def6c710d9f9d1705ca43cdad9364bb34ce5d878b8a3a9bf26d336802e40efeb
4
+ data.tar.gz: 0d6618cb61dfef8ae18f811a73b3f059d9945f8def7f9b2f794ae095b3a0f0cf
5
5
  SHA512:
6
- metadata.gz: 812528978ec4e8d3322af071c3ebd5b31f4123be449d3fe9bfea1e1fd2845704e0fee308d4cdd6e787636987b1d7a03527f8eae5fe5968483a1f4c5f751b40ef
7
- data.tar.gz: c433160a559102336b9a268ec1311a47e8f54e427ad8618b048634435259612a92d9b9187fefb6d7cc4a1ce5576a37f3e51b7adad1e3773d21c2bb6e9827c26f
6
+ metadata.gz: 7105cd0a84469071de9211e19f7e7a31f4ea8b5283d7b1ed007b6533805de18d105c09128a9936144612f3daa8fadfdf0698f7448d8db94e47bb459efaa11c4b
7
+ data.tar.gz: 3805e2664f21f30c66e9a9a05606f0d0d18b1607137e794948c7907bd33c6d6e64a45b9abb97d2f1a21df0d46497ff708679442807e152fa167abc461c8c0abe
data/.gitignore CHANGED
@@ -2,6 +2,7 @@
2
2
  *.rbc
3
3
  .bundle
4
4
  .config
5
+ .coverage
5
6
  .yardoc
6
7
  Gemfile.lock
7
8
  InstalledFiles
data/.rubocop.yml CHANGED
@@ -1,10 +1,7 @@
1
1
  AllCops:
2
- DisplayCopNames: true
3
- TargetRubyVersion: 2.1
2
+ TargetRubyVersion: 2.6
4
3
  Exclude:
5
- - "gemfiles/**/*"
6
- - "vendor/**/*"
7
- - "lib/generators/**/*"
4
+ - "lib/generators/**/templates/**/*"
8
5
 
9
6
  Metrics/BlockLength:
10
7
  Exclude:
@@ -30,33 +27,9 @@ Metrics/CyclomaticComplexity:
30
27
  Metrics/PerceivedComplexity:
31
28
  Enabled: false
32
29
 
33
- Style/StructInheritance:
34
- Enabled: false
35
-
36
30
  Layout/AlignParameters:
37
31
  EnforcedStyle: with_fixed_indentation
38
32
 
39
- Style/StringLiterals:
40
- EnforcedStyle: double_quotes
41
-
42
- Style/StringLiteralsInInterpolation:
43
- EnforcedStyle: double_quotes
44
-
45
- Layout/ClosingParenthesisIndentation:
46
- Enabled: false
47
-
48
- Style/OneLineConditional:
49
- Enabled: false
50
-
51
- Style/AndOr:
52
- Enabled: false
53
-
54
- Style/Not:
55
- Enabled: false
56
-
57
- Documentation:
58
- Enabled: false # TODO: Enable again once we have more docs
59
-
60
33
  Layout/CaseIndentation:
61
34
  EnforcedStyle: case
62
35
  SupportedStyles:
@@ -64,40 +37,31 @@ Layout/CaseIndentation:
64
37
  - end
65
38
  IndentOneStep: true
66
39
 
40
+ Layout/EndAlignment:
41
+ EnforcedStyleAlignWith: variable
42
+
67
43
  Style/PercentLiteralDelimiters:
68
44
  PreferredDelimiters:
69
45
  '%w': "[]"
70
46
  '%W': "[]"
71
47
 
72
- Layout/AccessModifierIndentation:
73
- EnforcedStyle: outdent
74
-
75
- Style/SignalException:
76
- Enabled: false
77
-
78
- Layout/IndentationWidth:
79
- Enabled: false
80
-
81
- Style/TrivialAccessors:
82
- ExactNameMatch: true
83
-
84
- Layout/EndAlignment:
85
- EnforcedStyleAlignWith: variable
86
-
87
- Layout/DefEndAlignment:
88
- Enabled: false
48
+ Style/StringLiterals:
49
+ EnforcedStyle: double_quotes
89
50
 
90
- Lint/HandleExceptions:
91
- Enabled: false
51
+ Style/StringLiteralsInInterpolation:
52
+ EnforcedStyle: double_quotes
92
53
 
93
- Style/SpecialGlobalVars:
54
+ Style/StructInheritance:
94
55
  Enabled: false
95
56
 
96
- Style/TrivialAccessors:
57
+ Style/AndOr:
97
58
  Enabled: false
98
59
 
99
- Layout/IndentHash:
60
+ Style/Not:
100
61
  Enabled: false
101
62
 
102
63
  Style/DoubleNegation:
103
64
  Enabled: false
65
+
66
+ Documentation:
67
+ Enabled: false # TODO: Enable again once we have more docs
data/.travis.yml CHANGED
@@ -1,21 +1,25 @@
1
1
  language: ruby
2
- sudo: false
3
- before_install:
4
- - gem update --system
5
- - gem install bundler
2
+ dist: focal
6
3
 
7
4
  matrix:
8
5
  include:
9
- - rvm: 2.5.1
6
+ - name: "RuboCop lint on pre-installed Ruby version"
7
+ rvm: 2.7.1 # Pre-installed Ruby version
8
+ before_install:
9
+ - gem install bundler
10
10
  script: bundle exec rake rubocop # ONLY lint once, first
11
- - rvm: 2.1
12
- - rvm: 2.2.8
13
- - rvm: 2.3.5
14
- - rvm: 2.4.2
15
- - rvm: 2.5.1
16
- - rvm: jruby-9.1.8.0
17
- env:
18
- - JRUBY_OPTS="--debug"
19
- - rvm: jruby-9.2.0.0
11
+ - rvm: 2.6.7
12
+ before_script:
13
+ - curl -L https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64 > ./cc-test-reporter
14
+ - chmod +x ./cc-test-reporter
15
+ - ./cc-test-reporter before-build
16
+ after_script:
17
+ - ./cc-test-reporter after-build --exit-code $TRAVIS_TEST_RESULT
18
+ - rvm: 2.7.3
19
+ - rvm: 3.0.1
20
+ - rvm: jruby-9.2.17.0
20
21
  env:
21
22
  - JRUBY_OPTS="--debug"
23
+ - rvm: truffleruby-head
24
+ allow_failures:
25
+ - rvm: truffleruby-head
data/CHANGELOG.md CHANGED
@@ -1,21 +1,74 @@
1
1
  # Pundit
2
2
 
3
+ ## Unreleased
4
+
5
+ ## 2.1.1 (2021-08-13)
6
+
7
+ Friday 13th-release!
8
+
9
+ Careful! The bugfix below (#626) could break existing code. If you rely on the
10
+ return value for `authorize` and namespaced policies you might need to do some
11
+ changes.
12
+
13
+ ### Fixed
14
+
15
+ - `.authorize` and `#authorize` return the instance, even for namespaced
16
+ policies (#626)
17
+
18
+ ### Changed
19
+
20
+ - Generate application scope with `protected` attr_readers. (#616)
21
+
22
+ ### Removed
23
+
24
+ - Dropped support for Ruby end-of-life versions: 2.1 and 2.2. (#604)
25
+ - Dropped support for Ruby end-of-life versions: 2.3 (#633)
26
+ - Dropped support for Ruby end-of-life versions: 2.4, 2.5 and JRuby 9.1 (#676)
27
+ - Dropped support for RSpec 2 (#615)
28
+
29
+ ## 2.1.0 (2019-08-14)
30
+
31
+ ### Fixed
32
+
33
+ - Avoid name clashes with the Error class. (#590)
34
+
35
+ ### Changed
36
+
37
+ - Return a safer default NotAuthorizedError message. (#583)
38
+
39
+ ## 2.0.1 (2019-01-18)
40
+
41
+ ### Breaking changes
42
+
43
+ None
44
+
45
+ ### Other changes
46
+
47
+ - Improve exception handling for `#policy_scope` and `#policy_scope!`. (#550)
48
+ - Add `:policy` metadata to RSpec template. (#566)
49
+
3
50
  ## 2.0.0 (2018-07-21)
4
51
 
5
52
  No changes since beta1
6
53
 
7
54
  ## 2.0.0.beta1 (2018-07-04)
8
55
 
56
+ ### Breaking changes
57
+
58
+ - Only pass last element of "namespace array" to policy and scope. (#529)
59
+ - Raise `InvalidConstructorError` if a policy or policy scope with an invalid constructor is called. (#462)
60
+ - Return passed object from `#authorize` method to make chaining possible. (#385)
61
+
62
+ ### Other changes
63
+
9
64
  - Add `policy_class` option to `authorize` to be able to override the policy. (#441)
10
65
  - Add `policy_scope_class` option to `authorize` to be able to override the policy scope. (#441)
11
66
  - Fix `param_key` issue when passed an array. (#529)
12
- - Only pass last element of "namespace array" to policy and scope. (#529)
13
67
  - Allow specification of a `NilClassPolicy`. (#525)
14
68
  - Make sure `policy_class` override is called when passed an array. (#475)
15
- - Raise `InvalidConstructorError` if a policy or policy scope with an invalid constructor is called. (#462)
69
+
16
70
  - Use `action_name` instead of `params[:action]`. (#419)
17
71
  - Add `pundit_params_for` method to make it easy to customize params fetching. (#502)
18
- - Return passed object from `#authorize` method to make chaining possible. (#385)
19
72
 
20
73
  ## 1.1.0 (2016-01-14)
21
74
 
data/Gemfile CHANGED
@@ -1,16 +1,7 @@
1
+ # frozen_string_literal: true
2
+
1
3
  source "https://rubygems.org"
2
4
 
3
5
  ruby RUBY_VERSION
4
6
 
5
7
  gemspec
6
-
7
- group :development, :test do
8
- gem "actionpack"
9
- gem "activemodel"
10
- gem "bundler"
11
- gem "pry"
12
- gem "rake"
13
- gem "rspec"
14
- gem "rubocop"
15
- gem "yard"
16
- end
data/LICENSE.txt CHANGED
@@ -1,4 +1,4 @@
1
- Copyright (c) 2012 Jonas Nicklas, Elabs AB
1
+ Copyright (c) 2019 Jonas Nicklas, Varvet AB
2
2
 
3
3
  MIT License
4
4
 
data/README.md CHANGED
@@ -7,7 +7,7 @@
7
7
 
8
8
  Pundit provides a set of helpers which guide you in leveraging regular Ruby
9
9
  classes and object oriented design patterns to build a simple, robust and
10
- scaleable authorization system.
10
+ scalable authorization system.
11
11
 
12
12
  Links:
13
13
 
@@ -31,7 +31,6 @@ Include Pundit in your application controller:
31
31
  ``` ruby
32
32
  class ApplicationController < ActionController::Base
33
33
  include Pundit
34
- protect_from_forgery
35
34
  end
36
35
  ```
37
36
 
@@ -61,7 +60,7 @@ class PostPolicy
61
60
  end
62
61
 
63
62
  def update?
64
- user.admin? or not post.published?
63
+ user.admin? || !post.published?
65
64
  end
66
65
  end
67
66
  ```
@@ -165,13 +164,18 @@ def admin_list
165
164
  end
166
165
  ```
167
166
 
168
- `authorize` returns the object passed to it, so you can chain it like this:
167
+ `authorize` returns the instance passed to it, so you can chain it like this:
169
168
 
170
169
  Controller:
171
170
  ```ruby
172
171
  def show
173
172
  @user = authorize User.find(params[:id])
174
173
  end
174
+
175
+ # return the record even for namespaced policies
176
+ def show
177
+ @user = authorize [:admin, User.find(params[:id])]
178
+ end
175
179
  ```
176
180
 
177
181
  You can easily get a hold of an instance of the policy through the `policy`
@@ -195,6 +199,10 @@ class DashboardPolicy < Struct.new(:user, :dashboard)
195
199
  end
196
200
  ```
197
201
 
202
+ Note that the headless policy still needs to accept two arguments. The
203
+ second argument will just be the symbol `:dashboard` in this case which
204
+ is what is passed as the record to `authorize` below.
205
+
198
206
  ```ruby
199
207
  # In controllers
200
208
  authorize :dashboard, :show?
@@ -216,8 +224,6 @@ define a class called a policy scope. It can look something like this:
216
224
  ``` ruby
217
225
  class PostPolicy < ApplicationPolicy
218
226
  class Scope
219
- attr_reader :user, :scope
220
-
221
227
  def initialize(user, scope)
222
228
  @user = user
223
229
  @scope = scope
@@ -230,6 +236,10 @@ class PostPolicy < ApplicationPolicy
230
236
  scope.where(published: true)
231
237
  end
232
238
  end
239
+
240
+ private
241
+
242
+ attr_reader :user, :scope
233
243
  end
234
244
 
235
245
  def update?
@@ -292,13 +302,11 @@ def index
292
302
  end
293
303
  ```
294
304
 
295
- Just as with your policy, this will automatically infer that you want to use
296
- the `PostPolicy::Scope` class, it will instantiate this class and call
297
- `resolve` on the instance. In this case it is a shortcut for doing:
305
+ In this case it is a shortcut for doing:
298
306
 
299
307
  ``` ruby
300
308
  def index
301
- @posts = PostPolicy::Scope.new(current_user, Post).resolve
309
+ @publications = PublicationPolicy::Scope.new(current_user, Post).resolve
302
310
  end
303
311
  ```
304
312
 
@@ -387,6 +395,16 @@ class Post
387
395
  end
388
396
  ```
389
397
 
398
+ Alternatively, you can declare an instance method:
399
+
400
+ ``` ruby
401
+ class Post
402
+ def policy_class
403
+ PostablePolicy
404
+ end
405
+ end
406
+ ```
407
+
390
408
  ## Just plain old Ruby
391
409
 
392
410
  As you can see, Pundit doesn't do anything you couldn't have easily done
@@ -472,7 +490,6 @@ method in every controller.
472
490
 
473
491
  ```ruby
474
492
  class ApplicationController < ActionController::Base
475
- protect_from_forgery
476
493
  include Pundit
477
494
 
478
495
  rescue_from Pundit::NotAuthorizedError, with: :user_not_authorized
@@ -593,8 +610,7 @@ class Admin::PostController < AdminController
593
610
  end
594
611
 
595
612
  def show
596
- post = Post.find(params[:id])
597
- authorize(post)
613
+ post = authorize Post.find(params[:id])
598
614
  end
599
615
  end
600
616
  ```
@@ -637,9 +653,8 @@ end
637
653
 
638
654
  ## Strong parameters
639
655
 
640
- In Rails 4 (or Rails 3.2 with the
641
- [strong_parameters](https://github.com/rails/strong_parameters) gem),
642
- mass-assignment protection is handled in the controller. With Pundit you can
656
+ In Rails,
657
+ mass-assignment protection is handled in the controller. With Pundit you can
643
658
  control which attributes a user has access to update via your policies. You can
644
659
  set up a `permitted_attributes` method in your policy like this:
645
660
 
@@ -778,9 +793,14 @@ Pundit does not provide a DSL for testing scopes. Just test it like a regular Ru
778
793
  - [RailsApps Example Application: Pundit and Devise](https://github.com/RailsApps/rails-devise-pundit)
779
794
  - [Migrating to Pundit from CanCan](http://blog.carbonfive.com/2013/10/21/migrating-to-pundit-from-cancan/)
780
795
  - [Testing Pundit Policies with RSpec](http://thunderboltlabs.com/blog/2013/03/27/testing-pundit-policies-with-rspec/)
796
+ - [Testing Pundit with Minitest](https://github.com/varvet/pundit/issues/204#issuecomment-60166450)
781
797
  - [Using Pundit outside of a Rails controller](https://github.com/varvet/pundit/pull/136)
782
798
  - [Straightforward Rails Authorization with Pundit](http://www.sitepoint.com/straightforward-rails-authorization-with-pundit/)
783
799
 
800
+ ## Other implementations
801
+
802
+ - [Flask-Pundit](https://github.com/anurag90x/flask-pundit) (Python) is a [Flask](http://flask.pocoo.org/) extension "heavily inspired by" Pundit
803
+
784
804
  # License
785
805
 
786
806
  Licensed under the MIT license, see the separate LICENSE.txt file.
data/Rakefile CHANGED
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "rubygems"
2
4
  require "bundler/gem_tasks"
3
5
  require "rspec/core/rake_task"
@@ -1,10 +1,12 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Pundit
2
4
  module Generators
3
5
  class InstallGenerator < ::Rails::Generators::Base
4
- source_root File.expand_path('templates', __dir__)
6
+ source_root File.expand_path("templates", __dir__)
5
7
 
6
8
  def copy_application_policy
7
- template 'application_policy.rb', 'app/policies/application_policy.rb'
9
+ template "application_policy.rb", "app/policies/application_policy.rb"
8
10
  end
9
11
  end
10
12
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  class ApplicationPolicy
2
4
  attr_reader :user, :record
3
5
 
@@ -35,8 +37,6 @@ class ApplicationPolicy
35
37
  end
36
38
 
37
39
  class Scope
38
- attr_reader :user, :scope
39
-
40
40
  def initialize(user, scope)
41
41
  @user = user
42
42
  @scope = scope
@@ -45,5 +45,9 @@ class ApplicationPolicy
45
45
  def resolve
46
46
  scope.all
47
47
  end
48
+
49
+ private
50
+
51
+ attr_reader :user, :scope
48
52
  end
49
53
  end
@@ -1,10 +1,12 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Pundit
2
4
  module Generators
3
5
  class PolicyGenerator < ::Rails::Generators::NamedBase
4
- source_root File.expand_path('templates', __dir__)
6
+ source_root File.expand_path("templates", __dir__)
5
7
 
6
8
  def create_policy
7
- template 'policy.rb', File.join('app/policies', class_path, "#{file_name}_policy.rb")
9
+ template "policy.rb", File.join("app/policies", class_path, "#{file_name}_policy.rb")
8
10
  end
9
11
 
10
12
  hook_for :test_framework
@@ -1,10 +1,12 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Rspec
2
4
  module Generators
3
5
  class PolicyGenerator < ::Rails::Generators::NamedBase
4
- source_root File.expand_path('templates', __dir__)
6
+ source_root File.expand_path("templates", __dir__)
5
7
 
6
8
  def create_policy_spec
7
- template 'policy_spec.rb', File.join('spec/policies', class_path, "#{file_name}_policy_spec.rb")
9
+ template "policy_spec.rb", File.join("spec/policies", class_path, "#{file_name}_policy_spec.rb")
8
10
  end
9
11
  end
10
12
  end
@@ -1,6 +1,6 @@
1
1
  require '<%= File.exists?('spec/rails_helper.rb') ? 'rails_helper' : 'spec_helper' %>'
2
2
 
3
- RSpec.describe <%= class_name %>Policy do
3
+ RSpec.describe <%= class_name %>Policy, type: :policy do
4
4
  let(:user) { User.new }
5
5
 
6
6
  subject { described_class }
@@ -1,10 +1,12 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module TestUnit
2
4
  module Generators
3
5
  class PolicyGenerator < ::Rails::Generators::NamedBase
4
- source_root File.expand_path('templates', __dir__)
6
+ source_root File.expand_path("templates", __dir__)
5
7
 
6
8
  def create_policy_test
7
- template 'policy_test.rb', File.join('test/policies', class_path, "#{file_name}_policy_test.rb")
9
+ template "policy_test.rb", File.join("test/policies", class_path, "#{file_name}_policy_test.rb")
8
10
  end
9
11
  end
10
12
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Pundit
2
4
  # Finds policy and scope classes for given object.
3
5
  # @api public
@@ -66,7 +68,7 @@ module Pundit
66
68
  end
67
69
  end
68
70
 
69
- private
71
+ private
70
72
 
71
73
  def find(subject)
72
74
  if subject.is_a?(Array)
data/lib/pundit/rspec.rb CHANGED
@@ -1,4 +1,4 @@
1
- require "active_support/core_ext/array/conversions"
1
+ # frozen_string_literal: true
2
2
 
3
3
  module Pundit
4
4
  module RSpec
@@ -72,17 +72,9 @@ module Pundit
72
72
  end
73
73
 
74
74
  RSpec.configure do |config|
75
- if RSpec::Core::Version::STRING.split(".").first.to_i >= 3
76
- config.include(
77
- Pundit::RSpec::PolicyExampleGroup,
78
- type: :policy,
79
- file_path: %r{spec/policies}
80
- )
81
- else
82
- config.include(
83
- Pundit::RSpec::PolicyExampleGroup,
84
- type: :policy,
85
- example_group: { file_path: %r{spec/policies} }
86
- )
87
- end
75
+ config.include(
76
+ Pundit::RSpec::PolicyExampleGroup,
77
+ type: :policy,
78
+ file_path: %r{spec/policies}
79
+ )
88
80
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Pundit
4
- VERSION = "2.0.0".freeze
4
+ VERSION = "2.1.1"
5
5
  end
data/lib/pundit.rb CHANGED
@@ -8,16 +8,18 @@ require "active_support/core_ext/object/blank"
8
8
  require "active_support/core_ext/module/introspection"
9
9
  require "active_support/dependencies/autoload"
10
10
 
11
+ # @api private
12
+ # To avoid name clashes with common Error naming when mixing in Pundit,
13
+ # keep it here with compact class style definition.
14
+ class Pundit::Error < StandardError; end # rubocop:disable Style/ClassAndModuleChildren
15
+
11
16
  # @api public
12
17
  module Pundit
13
- SUFFIX = "Policy".freeze
18
+ SUFFIX = "Policy"
14
19
 
15
20
  # @api private
16
21
  module Generators; end
17
22
 
18
- # @api private
19
- class Error < StandardError; end
20
-
21
23
  # Error that will be raised when authorization has failed
22
24
  class NotAuthorizedError < Error
23
25
  attr_reader :query, :record, :policy
@@ -30,7 +32,7 @@ module Pundit
30
32
  @record = options[:record]
31
33
  @policy = options[:policy]
32
34
 
33
- message = options.fetch(:message) { "not allowed to #{query} this #{record.inspect}" }
35
+ message = options.fetch(:message) { "not allowed to #{query} this #{record.class}" }
34
36
  end
35
37
 
36
38
  super(message)
@@ -69,7 +71,7 @@ module Pundit
69
71
 
70
72
  raise NotAuthorizedError, query: query, record: record, policy: policy unless policy.public_send(query)
71
73
 
72
- record
74
+ record.is_a?(Array) ? record.last : record
73
75
  end
74
76
 
75
77
  # Retrieves the policy scope for the given record.
@@ -80,10 +82,16 @@ module Pundit
80
82
  # @raise [InvalidConstructorError] if the policy constructor called incorrectly
81
83
  # @return [Scope{#resolve}, nil] instance of scope class which can resolve to a scope
82
84
  def policy_scope(user, scope)
83
- policy_scope = PolicyFinder.new(scope).scope
84
- policy_scope.new(user, pundit_model(scope)).resolve if policy_scope
85
- rescue ArgumentError
86
- raise InvalidConstructorError, "Invalid #<#{policy_scope}> constructor is called"
85
+ policy_scope_class = PolicyFinder.new(scope).scope
86
+ return unless policy_scope_class
87
+
88
+ begin
89
+ policy_scope = policy_scope_class.new(user, pundit_model(scope))
90
+ rescue ArgumentError
91
+ raise InvalidConstructorError, "Invalid #<#{policy_scope_class}> constructor is called"
92
+ end
93
+
94
+ policy_scope.resolve
87
95
  end
88
96
 
89
97
  # Retrieves the policy scope for the given record.
@@ -95,10 +103,16 @@ module Pundit
95
103
  # @raise [InvalidConstructorError] if the policy constructor called incorrectly
96
104
  # @return [Scope{#resolve}] instance of scope class which can resolve to a scope
97
105
  def policy_scope!(user, scope)
98
- policy_scope = PolicyFinder.new(scope).scope!
99
- policy_scope.new(user, pundit_model(scope)).resolve
100
- rescue ArgumentError
101
- raise InvalidConstructorError, "Invalid #<#{policy_scope}> constructor is called"
106
+ policy_scope_class = PolicyFinder.new(scope).scope!
107
+ return unless policy_scope_class
108
+
109
+ begin
110
+ policy_scope = policy_scope_class.new(user, pundit_model(scope))
111
+ rescue ArgumentError
112
+ raise InvalidConstructorError, "Invalid #<#{policy_scope_class}> constructor is called"
113
+ end
114
+
115
+ policy_scope.resolve
102
116
  end
103
117
 
104
118
  # Retrieves the policy for the given record.
@@ -110,7 +124,7 @@ module Pundit
110
124
  # @return [Object, nil] instance of policy class with query methods
111
125
  def policy(user, record)
112
126
  policy = PolicyFinder.new(record).policy
113
- policy.new(user, pundit_model(record)) if policy
127
+ policy&.new(user, pundit_model(record))
114
128
  rescue ArgumentError
115
129
  raise InvalidConstructorError, "Invalid #<#{policy}> constructor is called"
116
130
  end
@@ -130,7 +144,7 @@ module Pundit
130
144
  raise InvalidConstructorError, "Invalid #<#{policy}> constructor is called"
131
145
  end
132
146
 
133
- private
147
+ private
134
148
 
135
149
  def pundit_model(record)
136
150
  record.is_a?(Array) ? record.last : record
@@ -153,7 +167,7 @@ module Pundit
153
167
  end
154
168
  end
155
169
 
156
- protected
170
+ protected
157
171
 
158
172
  # @return [Boolean] whether authorization has been performed, i.e. whether
159
173
  # one {#authorize} or {#skip_authorization} has been called
@@ -208,7 +222,7 @@ protected
208
222
 
209
223
  raise NotAuthorizedError, query: query, record: record, policy: policy unless policy.public_send(query)
210
224
 
211
- record
225
+ record.is_a?(Array) ? record.last : record
212
226
  end
213
227
 
214
228
  # Allow this action not to perform authorization.
@@ -303,7 +317,7 @@ protected
303
317
  current_user
304
318
  end
305
319
 
306
- private
320
+ private
307
321
 
308
322
  def pundit_policy_scope(scope)
309
323
  policy_scopes[scope] ||= Pundit.policy_scope!(pundit_user, scope)
data/pundit.gemspec CHANGED
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  lib = File.expand_path("lib", __dir__)
2
4
  $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
3
5
  require "pundit/version"
@@ -5,17 +7,26 @@ require "pundit/version"
5
7
  Gem::Specification.new do |gem|
6
8
  gem.name = "pundit"
7
9
  gem.version = Pundit::VERSION
8
- gem.authors = ["Jonas Nicklas", "Elabs AB"]
10
+ gem.authors = ["Jonas Nicklas", "Varvet AB"]
9
11
  gem.email = ["jonas.nicklas@gmail.com", "dev@elabs.se"]
10
12
  gem.description = "Object oriented authorization for Rails applications"
11
13
  gem.summary = "OO authorization for Rails"
12
14
  gem.homepage = "https://github.com/varvet/pundit"
13
15
  gem.license = "MIT"
14
16
 
15
- gem.files = `git ls-files`.split($/)
17
+ gem.files = `git ls-files`.split($INPUT_RECORD_SEPARATOR)
16
18
  gem.executables = gem.files.grep(%r{^bin/}).map { |f| File.basename(f) }
17
19
  gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
18
20
  gem.require_paths = ["lib"]
19
21
 
20
22
  gem.add_dependency "activesupport", ">= 3.0.0"
23
+ gem.add_development_dependency "actionpack", ">= 3.0.0"
24
+ gem.add_development_dependency "activemodel", ">= 3.0.0"
25
+ gem.add_development_dependency "bundler"
26
+ gem.add_development_dependency "pry"
27
+ gem.add_development_dependency "rake"
28
+ gem.add_development_dependency "rspec", ">= 3.0.0"
29
+ gem.add_development_dependency "rubocop", "0.74.0"
30
+ gem.add_development_dependency "simplecov", ">= 0.17.0"
31
+ gem.add_development_dependency "yard"
21
32
  end
@@ -1,6 +1,8 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "spec_helper"
2
4
 
3
- describe PostPolicy do
5
+ RSpec.describe PostPolicy do
4
6
  let(:user) { double }
5
7
  let(:own_post) { double(user: user) }
6
8
  let(:other_post) { double(user: double) }
@@ -1,6 +1,8 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "spec_helper"
2
4
 
3
- describe Pundit::PolicyFinder do
5
+ RSpec.describe Pundit::PolicyFinder do
4
6
  let(:user) { double }
5
7
  let(:post) { Post.new(user) }
6
8
  let(:comment) { CommentFourFiveSix.new }
@@ -22,37 +24,100 @@ describe Pundit::PolicyFinder do
22
24
  end
23
25
 
24
26
  describe "#policy" do
25
- subject { described_class.new(post) }
27
+ context "with an instance" do
28
+ it "returns the associated policy" do
29
+ object = described_class.new(post)
30
+
31
+ expect(object.policy).to eq PostPolicy
32
+ end
33
+ end
34
+
35
+ context "with an array of symbols" do
36
+ it "returns the associated namespaced policy" do
37
+ object = described_class.new(%i[project post])
38
+
39
+ expect(object.policy).to eq Project::PostPolicy
40
+ end
41
+ end
26
42
 
27
- it "returns a policy" do
28
- expect(subject.policy).to eq PostPolicy
43
+ context "with an array of a symbol and an instance" do
44
+ it "returns the associated namespaced policy" do
45
+ object = described_class.new([:project, post])
46
+
47
+ expect(object.policy).to eq Project::PostPolicy
48
+ end
29
49
  end
30
50
 
31
- context "with a string" do
32
- it "returns a policy" do
33
- allow(subject).to receive(:find).and_return "PostPolicy"
34
- expect(subject.policy).to eq PostPolicy
51
+ context "with an array of a symbol and a class with a specified policy class" do
52
+ it "returns the associated namespaced policy" do
53
+ object = described_class.new([:project, Customer::Post])
54
+
55
+ expect(object.policy).to eq Project::PostPolicy
56
+ end
57
+ end
58
+
59
+ context "with an array of a symbol and a class with a specified model name" do
60
+ it "returns the associated namespaced policy" do
61
+ object = described_class.new([:project, CommentsRelation])
62
+
63
+ expect(object.policy).to eq Project::CommentPolicy
35
64
  end
36
65
  end
37
66
 
38
67
  context "with a class" do
39
- it "returns a policy" do
40
- allow(subject).to receive(:find).and_return PostPolicy
41
- expect(subject.policy).to eq PostPolicy
68
+ it "returns the associated policy" do
69
+ object = described_class.new(Post)
70
+
71
+ expect(object.policy).to eq PostPolicy
72
+ end
73
+ end
74
+
75
+ context "with a class which has a specified policy class" do
76
+ it "returns the associated policy" do
77
+ object = described_class.new(Customer::Post)
78
+
79
+ expect(object.policy).to eq PostPolicy
80
+ end
81
+ end
82
+
83
+ context "with an instance which has a specified policy class" do
84
+ it "returns the associated policy" do
85
+ object = described_class.new(Customer::Post.new(user))
86
+
87
+ expect(object.policy).to eq PostPolicy
88
+ end
89
+ end
90
+
91
+ context "with a class which has a specified model name" do
92
+ it "returns the associated policy" do
93
+ object = described_class.new(CommentsRelation)
94
+
95
+ expect(object.policy).to eq CommentPolicy
96
+ end
97
+ end
98
+
99
+ context "with an instance which has a specified policy class" do
100
+ it "returns the associated policy" do
101
+ object = described_class.new(CommentsRelation.new)
102
+
103
+ expect(object.policy).to eq CommentPolicy
42
104
  end
43
105
  end
44
106
 
45
107
  context "with nil" do
46
- it "returns nil" do
47
- allow(subject).to receive(:find).and_return nil
48
- expect(subject.policy).to eq nil
108
+ it "returns a NilClassPolicy" do
109
+ object = described_class.new(nil)
110
+
111
+ expect(object.policy).to eq NilClassPolicy
49
112
  end
50
113
  end
51
114
 
52
- context "with a string that can't be constantized" do
115
+ context "with a class that doesn't have an associated policy" do
53
116
  it "returns nil" do
54
- allow(subject).to receive(:find).and_return "FooPolicy"
55
- expect(subject.policy).to eq nil
117
+ class Foo; end
118
+ object = described_class.new(Foo)
119
+
120
+ expect(object.policy).to eq nil
56
121
  end
57
122
  end
58
123
  end
data/spec/pundit_spec.rb CHANGED
@@ -1,6 +1,8 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "spec_helper"
2
4
 
3
- describe Pundit do
5
+ RSpec.describe Pundit do
4
6
  let(:user) { double }
5
7
  let(:post) { Post.new(user) }
6
8
  let(:customer_post) { Customer::Post.new(user) }
@@ -16,12 +18,33 @@ describe Pundit do
16
18
  let(:tag_four_five_six) { ProjectOneTwoThree::TagFourFiveSix.new(user) }
17
19
  let(:avatar_four_five_six) { ProjectOneTwoThree::AvatarFourFiveSix.new }
18
20
  let(:wiki) { Wiki.new }
21
+ let(:thread) { Thread.new }
19
22
 
20
23
  describe ".authorize" do
21
24
  it "infers the policy and authorizes based on it" do
22
25
  expect(Pundit.authorize(user, post, :update?)).to be_truthy
23
26
  end
24
27
 
28
+ it "returns the record on successful authorization" do
29
+ expect(Pundit.authorize(user, post, :update?)).to eq(post)
30
+ end
31
+
32
+ it "returns the record when passed record with namespace " do
33
+ expect(Pundit.authorize(user, [:project, comment], :update?)).to eq(comment)
34
+ end
35
+
36
+ it "returns the record when passed record with nested namespace " do
37
+ expect(Pundit.authorize(user, [:project, :admin, comment], :update?)).to eq(comment)
38
+ end
39
+
40
+ it "returns the policy name symbol when passed record with headless policy" do
41
+ expect(Pundit.authorize(user, :publication, :create?)).to eq(:publication)
42
+ end
43
+
44
+ it "returns the class when passed record not a particular instance" do
45
+ expect(Pundit.authorize(user, Post, :show?)).to eq(Post)
46
+ end
47
+
25
48
  it "can be given a different policy class" do
26
49
  expect(Pundit.authorize(user, post, :create?, policy_class: PublicationPolicy)).to be_truthy
27
50
  end
@@ -35,7 +58,7 @@ describe Pundit do
35
58
  # rubocop:disable Style/MultilineBlockChain
36
59
  expect do
37
60
  Pundit.authorize(user, post, :destroy?)
38
- end.to raise_error(Pundit::NotAuthorizedError, "not allowed to destroy? this #<Post>") do |error|
61
+ end.to raise_error(Pundit::NotAuthorizedError, "not allowed to destroy? this Post") do |error|
39
62
  expect(error.query).to eq :destroy?
40
63
  expect(error.record).to eq post
41
64
  expect(error.policy).to eq Pundit.policy(user, post)
@@ -88,6 +111,12 @@ describe Pundit do
88
111
  Pundit.policy_scope(user, Wiki)
89
112
  end.to raise_error(Pundit::InvalidConstructorError, "Invalid #<WikiPolicy::Scope> constructor is called")
90
113
  end
114
+
115
+ it "raises an original error with a policy scope that contains error" do
116
+ expect do
117
+ Pundit.policy_scope(user, Thread)
118
+ end.to raise_error(ArgumentError)
119
+ end
91
120
  end
92
121
 
93
122
  describe ".policy_scope!" do
@@ -401,7 +430,23 @@ describe Pundit do
401
430
  end
402
431
 
403
432
  it "returns the record on successful authorization" do
404
- expect(controller.authorize(post)).to be(post)
433
+ expect(controller.authorize(post)).to eq(post)
434
+ end
435
+
436
+ it "returns the record when passed record with namespace " do
437
+ expect(controller.authorize([:project, comment], :update?)).to eq(comment)
438
+ end
439
+
440
+ it "returns the record when passed record with nested namespace " do
441
+ expect(controller.authorize([:project, :admin, comment], :update?)).to eq(comment)
442
+ end
443
+
444
+ it "returns the policy name symbol when passed record with headless policy" do
445
+ expect(controller.authorize(:publication, :create?)).to eq(:publication)
446
+ end
447
+
448
+ it "returns the class when passed record not a particular instance" do
449
+ expect(controller.authorize(Post, :show?)).to eq(Post)
405
450
  end
406
451
 
407
452
  it "can be given a different permission to check" do
@@ -511,11 +556,13 @@ describe Pundit do
511
556
 
512
557
  describe "#permitted_attributes" do
513
558
  it "checks policy for permitted attributes" do
514
- params = ActionController::Parameters.new(post: {
515
- title: "Hello",
516
- votes: 5,
517
- admin: true
518
- })
559
+ params = ActionController::Parameters.new(
560
+ post: {
561
+ title: "Hello",
562
+ votes: 5,
563
+ admin: true
564
+ }
565
+ )
519
566
 
520
567
  action = "update"
521
568
 
@@ -527,11 +574,13 @@ describe Pundit do
527
574
  end
528
575
 
529
576
  it "checks policy for permitted attributes for record of a ActiveModel type" do
530
- params = ActionController::Parameters.new(customer_post: {
531
- title: "Hello",
532
- votes: 5,
533
- admin: true
534
- })
577
+ params = ActionController::Parameters.new(
578
+ customer_post: {
579
+ title: "Hello",
580
+ votes: 5,
581
+ admin: true
582
+ }
583
+ )
535
584
 
536
585
  action = "update"
537
586
 
@@ -547,24 +596,28 @@ describe Pundit do
547
596
 
548
597
  describe "#permitted_attributes_for_action" do
549
598
  it "is checked if it is defined in the policy" do
550
- params = ActionController::Parameters.new(post: {
551
- title: "Hello",
552
- body: "blah",
553
- votes: 5,
554
- admin: true
555
- })
599
+ params = ActionController::Parameters.new(
600
+ post: {
601
+ title: "Hello",
602
+ body: "blah",
603
+ votes: 5,
604
+ admin: true
605
+ }
606
+ )
556
607
 
557
608
  action = "revise"
558
609
  expect(Controller.new(user, action, params).permitted_attributes(post).to_h).to eq("body" => "blah")
559
610
  end
560
611
 
561
612
  it "can be explicitly set" do
562
- params = ActionController::Parameters.new(post: {
563
- title: "Hello",
564
- body: "blah",
565
- votes: 5,
566
- admin: true
567
- })
613
+ params = ActionController::Parameters.new(
614
+ post: {
615
+ title: "Hello",
616
+ body: "blah",
617
+ votes: 5,
618
+ admin: true
619
+ }
620
+ )
568
621
 
569
622
  action = "update"
570
623
  expect(Controller.new(user, action, params).permitted_attributes(post, :revise).to_h).to eq("body" => "blah")
data/spec/spec_helper.rb CHANGED
@@ -1,3 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "simplecov"
4
+ SimpleCov.start do
5
+ add_filter "/spec/"
6
+ end
7
+
1
8
  require "pundit"
2
9
  require "pundit/rspec"
3
10
 
@@ -9,22 +16,6 @@ require "active_support/core_ext"
9
16
  require "active_model/naming"
10
17
  require "action_controller/metal/strong_parameters"
11
18
 
12
- I18n.enforce_available_locales = false
13
-
14
- module PunditSpecHelper
15
- extend RSpec::Matchers::DSL
16
-
17
- matcher :be_truthy do
18
- match do |actual|
19
- actual
20
- end
21
- end
22
- end
23
-
24
- RSpec.configure do |config|
25
- config.include PunditSpecHelper
26
- end
27
-
28
19
  class PostPolicy < Struct.new(:user, :post)
29
20
  class Scope < Struct.new(:user, :scope)
30
21
  def resolve
@@ -84,10 +75,6 @@ module Customer
84
75
  def self.policy_class
85
76
  PostPolicy
86
77
  end
87
-
88
- def policy_class
89
- self.class.policy_class
90
- end
91
78
  end
92
79
  end
93
80
 
@@ -135,7 +122,7 @@ class CommentsRelation
135
122
  @empty
136
123
  end
137
124
 
138
- def model_name
125
+ def self.model_name
139
126
  Comment.model_name
140
127
  end
141
128
  end
@@ -172,6 +159,10 @@ class CriteriaPolicy < Struct.new(:user, :criteria); end
172
159
 
173
160
  module Project
174
161
  class CommentPolicy < Struct.new(:user, :comment)
162
+ def update?
163
+ true
164
+ end
165
+
175
166
  class Scope < Struct.new(:user, :scope)
176
167
  def resolve
177
168
  scope
@@ -188,6 +179,14 @@ module Project
188
179
  end
189
180
  end
190
181
  end
182
+
183
+ module Admin
184
+ class CommentPolicy < Struct.new(:user, :comment)
185
+ def update?
186
+ true
187
+ end
188
+ end
189
+ end
191
190
  end
192
191
 
193
192
  class DenierPolicy < Struct.new(:user, :record)
@@ -199,9 +198,9 @@ end
199
198
  class Controller
200
199
  include Pundit
201
200
  # Mark protected methods public so they may be called in test
202
- # rubocop:disable Layout/AccessModifierIndentation, Style/AccessModifierDeclarations
201
+ # rubocop:disable Style/AccessModifierDeclarations
203
202
  public(*Pundit.protected_instance_methods)
204
- # rubocop:enable Layout/AccessModifierIndentation, Style/AccessModifierDeclarations
203
+ # rubocop:enable Style/AccessModifierDeclarations
205
204
 
206
205
  attr_reader :current_user, :action_name, :params
207
206
 
@@ -236,6 +235,18 @@ class WikiPolicy
236
235
  end
237
236
  end
238
237
 
238
+ class Thread
239
+ def self.all; end
240
+ end
241
+ class ThreadPolicy < Struct.new(:user, :thread)
242
+ class Scope < Struct.new(:user, :scope)
243
+ def resolve
244
+ # deliberate wrong useage of the method
245
+ scope.all(:unvalid, :parameters)
246
+ end
247
+ end
248
+ end
249
+
239
250
  class PostFourFiveSix < Struct.new(:user); end
240
251
 
241
252
  class CommentFourFiveSix; extend ActiveModel::Naming; end
metadata CHANGED
@@ -1,15 +1,15 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: pundit
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.0.0
4
+ version: 2.1.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jonas Nicklas
8
- - Elabs AB
8
+ - Varvet AB
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2018-07-21 00:00:00.000000000 Z
12
+ date: 2021-08-13 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: activesupport
@@ -25,6 +25,132 @@ dependencies:
25
25
  - - ">="
26
26
  - !ruby/object:Gem::Version
27
27
  version: 3.0.0
28
+ - !ruby/object:Gem::Dependency
29
+ name: actionpack
30
+ requirement: !ruby/object:Gem::Requirement
31
+ requirements:
32
+ - - ">="
33
+ - !ruby/object:Gem::Version
34
+ version: 3.0.0
35
+ type: :development
36
+ prerelease: false
37
+ version_requirements: !ruby/object:Gem::Requirement
38
+ requirements:
39
+ - - ">="
40
+ - !ruby/object:Gem::Version
41
+ version: 3.0.0
42
+ - !ruby/object:Gem::Dependency
43
+ name: activemodel
44
+ requirement: !ruby/object:Gem::Requirement
45
+ requirements:
46
+ - - ">="
47
+ - !ruby/object:Gem::Version
48
+ version: 3.0.0
49
+ type: :development
50
+ prerelease: false
51
+ version_requirements: !ruby/object:Gem::Requirement
52
+ requirements:
53
+ - - ">="
54
+ - !ruby/object:Gem::Version
55
+ version: 3.0.0
56
+ - !ruby/object:Gem::Dependency
57
+ name: bundler
58
+ requirement: !ruby/object:Gem::Requirement
59
+ requirements:
60
+ - - ">="
61
+ - !ruby/object:Gem::Version
62
+ version: '0'
63
+ type: :development
64
+ prerelease: false
65
+ version_requirements: !ruby/object:Gem::Requirement
66
+ requirements:
67
+ - - ">="
68
+ - !ruby/object:Gem::Version
69
+ version: '0'
70
+ - !ruby/object:Gem::Dependency
71
+ name: pry
72
+ requirement: !ruby/object:Gem::Requirement
73
+ requirements:
74
+ - - ">="
75
+ - !ruby/object:Gem::Version
76
+ version: '0'
77
+ type: :development
78
+ prerelease: false
79
+ version_requirements: !ruby/object:Gem::Requirement
80
+ requirements:
81
+ - - ">="
82
+ - !ruby/object:Gem::Version
83
+ version: '0'
84
+ - !ruby/object:Gem::Dependency
85
+ name: rake
86
+ requirement: !ruby/object:Gem::Requirement
87
+ requirements:
88
+ - - ">="
89
+ - !ruby/object:Gem::Version
90
+ version: '0'
91
+ type: :development
92
+ prerelease: false
93
+ version_requirements: !ruby/object:Gem::Requirement
94
+ requirements:
95
+ - - ">="
96
+ - !ruby/object:Gem::Version
97
+ version: '0'
98
+ - !ruby/object:Gem::Dependency
99
+ name: rspec
100
+ requirement: !ruby/object:Gem::Requirement
101
+ requirements:
102
+ - - ">="
103
+ - !ruby/object:Gem::Version
104
+ version: 3.0.0
105
+ type: :development
106
+ prerelease: false
107
+ version_requirements: !ruby/object:Gem::Requirement
108
+ requirements:
109
+ - - ">="
110
+ - !ruby/object:Gem::Version
111
+ version: 3.0.0
112
+ - !ruby/object:Gem::Dependency
113
+ name: rubocop
114
+ requirement: !ruby/object:Gem::Requirement
115
+ requirements:
116
+ - - '='
117
+ - !ruby/object:Gem::Version
118
+ version: 0.74.0
119
+ type: :development
120
+ prerelease: false
121
+ version_requirements: !ruby/object:Gem::Requirement
122
+ requirements:
123
+ - - '='
124
+ - !ruby/object:Gem::Version
125
+ version: 0.74.0
126
+ - !ruby/object:Gem::Dependency
127
+ name: simplecov
128
+ requirement: !ruby/object:Gem::Requirement
129
+ requirements:
130
+ - - ">="
131
+ - !ruby/object:Gem::Version
132
+ version: 0.17.0
133
+ type: :development
134
+ prerelease: false
135
+ version_requirements: !ruby/object:Gem::Requirement
136
+ requirements:
137
+ - - ">="
138
+ - !ruby/object:Gem::Version
139
+ version: 0.17.0
140
+ - !ruby/object:Gem::Dependency
141
+ name: yard
142
+ requirement: !ruby/object:Gem::Requirement
143
+ requirements:
144
+ - - ">="
145
+ - !ruby/object:Gem::Version
146
+ version: '0'
147
+ type: :development
148
+ prerelease: false
149
+ version_requirements: !ruby/object:Gem::Requirement
150
+ requirements:
151
+ - - ">="
152
+ - !ruby/object:Gem::Version
153
+ version: '0'
28
154
  description: Object oriented authorization for Rails applications
29
155
  email:
30
156
  - jonas.nicklas@gmail.com
@@ -82,8 +208,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
82
208
  - !ruby/object:Gem::Version
83
209
  version: '0'
84
210
  requirements: []
85
- rubyforge_project:
86
- rubygems_version: 2.7.6
211
+ rubygems_version: 3.2.25
87
212
  signing_key:
88
213
  specification_version: 4
89
214
  summary: OO authorization for Rails