pragma-policy 0.1.0 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 3558b9e18dec8f44921f49f1390ac27ec81d0633
4
- data.tar.gz: ae90ff0b36bbd2ba93c66af2f33f540727517140
3
+ metadata.gz: 92f9cd653b6f54508780a68f79ccaa83c923780c
4
+ data.tar.gz: 12813e8504acfcade966bf450a4bd0c3f76db4be
5
5
  SHA512:
6
- metadata.gz: e3dfe25ea37762ef45e30d474a5176d38f558d528f0352b3cc342a4f18c1508e9c8fe8f6605b67082ef5a8fa06f352bad19a6df420b8d42b2f5d8aa2acfdbeac
7
- data.tar.gz: 3d15e2cceb67dc0236f0d9204d3938f0e248946688d07346e4f19bb572ae68b2767987fd1c34a8210659e681f1ee2e29d748164d57a46b487f813cf546d1f6bd
6
+ metadata.gz: 45f33ffbbc876b4a386841e819e8ce85d315039bad4d5644e999d527c88fa97153217f689c99dfb987481a0b6f1483775c7e0bb75ab568a8af4f0fe58caddcea
7
+ data.tar.gz: f7cd7b953b894075e7fe636c314509c3d8d9972b865312aa7afc758cce36180eda3ed1f30bb710e7d8ea763521e7a9cda0ee401f9786483940b8d5c9c0d740e2
@@ -15,6 +15,7 @@ AllCops:
15
15
  - 'config/**/*'
16
16
  - '**/Rakefile'
17
17
  - '**/Gemfile'
18
+ - 'pragma-policy.gemspec'
18
19
 
19
20
  RSpec/DescribeClass:
20
21
  Exclude:
@@ -24,26 +25,26 @@ Style/BlockDelimiters:
24
25
  Exclude:
25
26
  - 'spec/**/*'
26
27
 
27
- Style/AlignParameters:
28
+ Layout/AlignParameters:
28
29
  EnforcedStyle: with_fixed_indentation
29
30
 
30
- Style/ClosingParenthesisIndentation:
31
+ Layout/ClosingParenthesisIndentation:
31
32
  Enabled: false
32
33
 
33
34
  Metrics/LineLength:
34
35
  Max: 100
35
36
  AllowURI: true
36
37
 
37
- Style/FirstParameterIndentation:
38
+ Layout/FirstParameterIndentation:
38
39
  Enabled: false
39
40
 
40
- Style/MultilineMethodCallIndentation:
41
+ Layout/MultilineMethodCallIndentation:
41
42
  EnforcedStyle: indented
42
43
 
43
- Style/IndentArray:
44
+ Layout/IndentArray:
44
45
  EnforcedStyle: consistent
45
46
 
46
- Style/IndentHash:
47
+ Layout/IndentHash:
47
48
  EnforcedStyle: consistent
48
49
 
49
50
  Style/SignalException:
@@ -53,7 +54,7 @@ Style/BracesAroundHashParameters:
53
54
  EnforcedStyle: context_dependent
54
55
 
55
56
  Lint/EndAlignment:
56
- AlignWith: variable
57
+ EnforcedStyleAlignWith: variable
57
58
  AutoCorrect: true
58
59
 
59
60
  Style/AndOr:
@@ -68,7 +69,7 @@ RSpec/NamedSubject:
68
69
  RSpec/ExampleLength:
69
70
  Enabled: false
70
71
 
71
- Style/MultilineMethodCallBraceLayout:
72
+ Layout/MultilineMethodCallBraceLayout:
72
73
  Enabled: false
73
74
 
74
75
  Metrics/MethodLength:
@@ -82,3 +83,6 @@ Metrics/PerceivedComplexity:
82
83
 
83
84
  Metrics/CyclomaticComplexity:
84
85
  Enabled: false
86
+
87
+ Metrics/BlockLength:
88
+ Enabled: false
data/README.md CHANGED
@@ -7,7 +7,7 @@
7
7
 
8
8
  Policies provide fine-grained access control for your API resources.
9
9
 
10
- They are built on top of [Reform](https://github.com/apotonick/reform).
10
+ They are built on top of the [Pundit](https://github.com/elabs/pundit) gem.
11
11
 
12
12
  ## Installation
13
13
 
@@ -36,7 +36,7 @@ To create a policy, simply inherit from `Pragma::Policy::Base`:
36
36
  ```ruby
37
37
  module API
38
38
  module V1
39
- module Post
39
+ module Article
40
40
  class Policy < Pragma::Policy::Base
41
41
  end
42
42
  end
@@ -44,29 +44,31 @@ module API
44
44
  end
45
45
  ```
46
46
 
47
- By default, the policy does not return any objects and forbids all operations.
47
+ By default, the policy does not return any objects when scoping and forbids all operations.
48
48
 
49
49
  You can start customizing your policy by defining a scope and operation predicates:
50
50
 
51
51
  ```ruby
52
52
  module API
53
53
  module V1
54
- module Post
54
+ module Article
55
55
  class Policy < Pragma::Policy::Base
56
- def self.accessible_by(user:, scope:)
57
- scope.where('published = ? OR author_id = ?', true, user.id)
56
+ class Scope < Pragma::Policy::Base::Scope
57
+ def resolve
58
+ scope.where('published = ? OR author_id = ?', true, user.id)
59
+ end
58
60
  end
59
61
 
60
62
  def show?
61
- resource.published? || resource.author_id == user.id
63
+ record.published? || record.author_id == user.id
62
64
  end
63
65
 
64
66
  def update?
65
- resource.author_id == user.id
67
+ record.author_id == user.id
66
68
  end
67
69
 
68
70
  def destroy?
69
- resource.author_id == user.id
71
+ record.author_id == user.id
70
72
  end
71
73
  end
72
74
  end
@@ -76,103 +78,31 @@ end
76
78
 
77
79
  You are ready to use your policy!
78
80
 
79
- ### Retrieving records
81
+ ### Retrieving Records
80
82
 
81
83
  To retrieve all the records accessible by a user, use the `.accessible_by` class method:
82
84
 
83
85
  ```ruby
84
- posts = API::V1::Post::Policy.accessible_by(user: user, scope: Post.all)
86
+ posts = API::V1::Article::Policy::Scope.new(user, Article.all).resolve
85
87
  ```
86
88
 
87
- ### Authorizing operations
89
+ ### Authorizing Operations
88
90
 
89
91
  To authorize an operation, first instantiate the policy, then use the predicate methods:
90
92
 
91
93
  ```ruby
92
- policy = API::V1::Post::Policy.new(user: user, resource: post)
94
+ policy = API::V1::Article::Policy.new(user, post)
93
95
  fail 'You cannot update this post!' unless policy.update?
94
96
  ```
95
97
 
96
98
  Since raising when the operation is forbidden is so common, we provide bang methods a shorthand
97
- syntax. `Pragma::Policy::ForbiddenError` is raised if the predicate method returns `false`:
99
+ syntax. `Pragma::Policy::NotAuthorizedError` is raised if the predicate method returns `false`:
98
100
 
99
101
  ```ruby
100
- policy = API::V1::Post::Policy.new(user: user, resource: post)
102
+ policy = API::V1::Article::Policy.new(user, post)
101
103
  policy.update! # raises if the user cannot update the post
102
104
  ```
103
105
 
104
- ### Attribute-level authorization
105
-
106
- In some cases, you'll want to prevent a user from updating a certain attribute. You can do that with
107
- the `#authorize_attr` method:
108
-
109
- ```ruby
110
- module API
111
- module V1
112
- module Post
113
- class Policy < Pragma::Policy::Base
114
- def update?
115
- # admins can do whatever they want
116
- return true if user.admin?
117
-
118
- (
119
- resource.author_id == user.id &&
120
- # regular users cannot change the 'featured' attribute
121
- authorize_attr(:featured)
122
- )
123
- end
124
- end
125
- end
126
- end
127
- end
128
- ```
129
-
130
- You can also allow specific values for an enumerated attribute:
131
-
132
- ```ruby
133
- module API
134
- module V1
135
- module Post
136
- class Policy < Pragma::Policy::Base
137
- def update?
138
- # admins can do whatever they want
139
- return true if user.admin?
140
-
141
- (
142
- resource.author_id == user.id &&
143
- # regular users can only set status to 'draft' or 'published'
144
- authorize_attr(:status, only: ['draft', 'published'])
145
- )
146
- end
147
- end
148
- end
149
- end
150
- end
151
- ```
152
-
153
- Or you can invert the condition and specify the forbidden attributes:
154
-
155
- ```ruby
156
- module API
157
- module V1
158
- module Post
159
- class Policy < Pragma::Policy::Base
160
- def update?
161
- # admins can do whatever they want
162
- return true if user.admin?
163
-
164
- (
165
- resource.author_id == user.id &&
166
- # regular users cannot set the status to 'rejected'
167
- authorize_attr(:status, except: ['rejected'])
168
- )
169
- end
170
- end
171
- end
172
- end
173
- end
174
- ```
175
-
176
106
  ## Contributing
177
107
 
178
108
  Bug reports and pull requests are welcome on GitHub at https://github.com/pragmarb/pragma-policy.
@@ -1,7 +1,10 @@
1
1
  # frozen_string_literal: true
2
+
3
+ require 'pundit'
4
+
2
5
  require 'pragma/policy/version'
3
6
  require 'pragma/policy/base'
4
- require 'pragma/policy/attribute_authorizer'
7
+ require 'pragma/policy/errors'
5
8
 
6
9
  module Pragma
7
10
  # Fine-grained access control for your API resources.
@@ -1,41 +1,61 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  module Pragma
3
4
  module Policy
4
- # This is the base policy class that all your resource-specific policies should inherit from.
5
+ # This is the base policy class that all your record-specific policies should inherit from.
5
6
  #
6
7
  # A policy provides predicate methods for determining whether a user can perform a specific
7
- # action on a resource.
8
+ # action on a record.
8
9
  #
9
10
  # @author Alessandro Desantis
10
11
  #
11
12
  # @abstract Subclass and implement action methods to create a policy.
12
13
  class Base
13
- # @!attribute [r] user
14
- # @return [Object] the user operating on the resource
15
- #
16
- # @!attribute [r] resource
17
- # @return [Object] the resource being operated on
18
- attr_reader :user, :resource
14
+ # Authorizes AR scopes and other relations by only returning the records accessible by the
15
+ # current user. Used, for instance, in index operations.
16
+ #
17
+ # @author Alessandro Desantis
18
+ class Scope
19
+ # @!attribute [r] user
20
+ # @return [Object] the user accessing the records
21
+ #
22
+ # @!attribute [r] scope
23
+ # @return [Object] the relation to use as a base
24
+ attr_reader :user, :scope
19
25
 
20
- # Returns the records accessible by the given user.
21
- #
22
- # @param user [Object] the user accessing the records
23
- # @param relation [Object] the relation to use as a base
24
- #
25
- # @return [Object]
26
- #
27
- # @abstract Override to implement retrieving the accessible records
28
- def self.accessible_by(user, relation:) # rubocop:disable Lint/UnusedMethodArgument
29
- fail NotImplementedError
26
+ # Initializes the scope.
27
+ #
28
+ # @param user [Object] the user accessing the records
29
+ # @param scope [Object] the relation to use as a base
30
+ def initialize(user, scope)
31
+ @user = user
32
+ @scope = scope
33
+ end
34
+
35
+ # Returns the records accessible by the given user.
36
+ #
37
+ # @return [Object]
38
+ #
39
+ # @abstract Override to implement retrieving the accessible records
40
+ def resolve
41
+ fail NotImplementedError
42
+ end
30
43
  end
31
44
 
45
+ # @!attribute [r] user
46
+ # @return [Object] the user operating on the record
47
+ #
48
+ # @!attribute [r] record
49
+ # @return [Object] the record being operated on
50
+ attr_reader :user, :record
51
+
32
52
  # Initializes the policy.
33
53
  #
34
- # @param user [Object] the user operating on the resource
35
- # @param resource [Object] the resource being operated on
36
- def initialize(user:, resource:)
54
+ # @param user [Object] the user operating on the record
55
+ # @param record [Object] the record being operated on
56
+ def initialize(user, record)
37
57
  @user = user
38
- @resource = resource
58
+ @record = record
39
59
  end
40
60
 
41
61
  # Returns whether the policy responds to the provided missing method.
@@ -71,67 +91,18 @@ module Pragma
71
91
  # @raise [ForbiddenError] if the user is not authorized to perform the action
72
92
  def authorize(action)
73
93
  unless respond_to?("#{action}?")
74
- fail(
75
- ArgumentError,
76
- "'#{action}' is not a valid action for this policy."
77
- )
94
+ fail(ArgumentError, "'#{action}' is not a valid action for this policy.")
78
95
  end
79
96
 
80
97
  return if send("#{action}?")
81
98
 
82
99
  fail(
83
- ForbiddenError,
100
+ NotAuthorizedError,
84
101
  user: user,
85
102
  action: action,
86
- resource: resource
103
+ record: record
87
104
  )
88
105
  end
89
-
90
- protected
91
-
92
- # Authorizes a resource attribute.
93
- #
94
- # @param attribute [Symbol] the name of the attribute
95
- # @param options [Hash] options (see {AttributeAuthorizer#authorize} for allowed options)
96
- #
97
- # @return [Boolean] whether the attribute's value is allowed
98
- def authorize_attr(attribute, options = {})
99
- AttributeAuthorizer.new(
100
- resource: resource,
101
- attribute: attribute
102
- ).authorize(options)
103
- end
104
- end
105
-
106
- # This error is raised when a user attempts to perform an unauthorized operation on a
107
- # resource.
108
- #
109
- # @author Alessandro Desantis
110
- class ForbiddenError < StandardError
111
- MESSAGE = "User is not authorized to perform the '%{action}' action on this resource."
112
-
113
- # @!attribtue [r] user
114
- # @return [Object] the user operating on the resource
115
- #
116
- # @!attribute [r] action
117
- # @return [Symbol] the attempted action
118
- #
119
- # @!attribute [r] resource
120
- # @return [Object] the resource being operated on
121
- attr_reader :user, :action, :resource
122
-
123
- # Initializes the error.
124
- #
125
- # @param user [Object] the user operating on the resource
126
- # @param action [Symbol] the attempted action
127
- # @param resource [Object] the resource being operated on
128
- def initialize(user:, action:, resource:)
129
- @user = user
130
- @action = action.to_sym
131
- @resource = resource
132
-
133
- super MESSAGE.gsub('%{action}', action.to_s)
134
- end
135
106
  end
136
107
  end
137
108
  end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pragma
4
+ module Policy
5
+ class NotAuthorizedError < Pundit::NotAuthorizedError
6
+ end
7
+ end
8
+ end
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  module Pragma
3
4
  module Policy
4
- VERSION = '0.1.0'
5
+ VERSION = '2.0.0'
5
6
  end
6
7
  end
@@ -1,29 +1,32 @@
1
- # coding: utf-8
1
+ # frozen_string_literal: true
2
+
2
3
  lib = File.expand_path('../lib', __FILE__)
3
4
  $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
5
  require 'pragma/policy/version'
5
6
 
6
7
  Gem::Specification.new do |spec|
7
- spec.name = "pragma-policy"
8
+ spec.name = 'pragma-policy'
8
9
  spec.version = Pragma::Policy::VERSION
9
- spec.authors = ["Alessandro Desantis"]
10
- spec.email = ["desa.alessandro@gmail.com"]
10
+ spec.authors = ['Alessandro Desantis']
11
+ spec.email = ['desa.alessandro@gmail.com']
11
12
 
12
13
  spec.summary = 'Fine-grained access control for your API resources.'
13
- spec.homepage = "https://github.com/pragmarb/pragma-policy"
14
- spec.license = "MIT"
14
+ spec.homepage = 'https://github.com/pragmarb/pragma-policy'
15
+ spec.license = 'MIT'
15
16
 
16
17
  spec.files = `git ls-files -z`.split("\x0").reject do |f|
17
18
  f.match(%r{^(test|spec|features)/})
18
19
  end
19
- spec.bindir = "exe"
20
+ spec.bindir = 'exe'
20
21
  spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
21
- spec.require_paths = ["lib"]
22
+ spec.require_paths = ['lib']
23
+
24
+ spec.add_dependency 'pundit', '~> 1.1'
22
25
 
23
- spec.add_development_dependency "bundler"
24
- spec.add_development_dependency "rake"
25
- spec.add_development_dependency "rspec"
26
- spec.add_development_dependency "rubocop"
27
- spec.add_development_dependency "rubocop-rspec"
28
- spec.add_development_dependency "coveralls"
26
+ spec.add_development_dependency 'bundler'
27
+ spec.add_development_dependency 'rake'
28
+ spec.add_development_dependency 'rspec'
29
+ spec.add_development_dependency 'rubocop'
30
+ spec.add_development_dependency 'rubocop-rspec'
31
+ spec.add_development_dependency 'coveralls'
29
32
  end
metadata CHANGED
@@ -1,15 +1,29 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: pragma-policy
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 2.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Alessandro Desantis
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2016-12-26 00:00:00.000000000 Z
11
+ date: 2017-09-27 00:00:00.000000000 Z
12
12
  dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: pundit
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.1'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.1'
13
27
  - !ruby/object:Gem::Dependency
14
28
  name: bundler
15
29
  requirement: !ruby/object:Gem::Requirement
@@ -112,8 +126,8 @@ files:
112
126
  - bin/console
113
127
  - bin/setup
114
128
  - lib/pragma/policy.rb
115
- - lib/pragma/policy/attribute_authorizer.rb
116
129
  - lib/pragma/policy/base.rb
130
+ - lib/pragma/policy/errors.rb
117
131
  - lib/pragma/policy/version.rb
118
132
  - pragma-policy.gemspec
119
133
  homepage: https://github.com/pragmarb/pragma-policy
@@ -136,7 +150,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
136
150
  version: '0'
137
151
  requirements: []
138
152
  rubyforge_project:
139
- rubygems_version: 2.5.2
153
+ rubygems_version: 2.6.13
140
154
  signing_key:
141
155
  specification_version: 4
142
156
  summary: Fine-grained access control for your API resources.
@@ -1,142 +0,0 @@
1
- # frozen_string_literal: true
2
- module Pragma
3
- module Policy
4
- # The attribute authorizer provides attribute-level authorization for resource updates.
5
- #
6
- # It allows you to specify whether a resource attribute can be changed and, if you want, what
7
- # values should be allowed.
8
- #
9
- # If you want, you can subclass this base authorizer to avoid repeating code.
10
- #
11
- # @author Alessandro Desantis
12
- class AttributeAuthorizer
13
- # @!attribute [r] resource
14
- # @return [ActiveRecord::Base|Reform::Form] the resource being authorized
15
- #
16
- # @!attribute [r] attribute
17
- # @return [Symbol] the attribute being authorized
18
- attr_reader :resource, :attribute
19
-
20
- # Initializes the authorizer.
21
- #
22
- # @param resource [ActiveRecord::Base|Reform::Form] the resource being authorized
23
- # @param attribute [Symbol] the attribute being authorized
24
- #
25
- # @raise [UnknownEngineError] if the resource is not based on Reform or ActiveRecord
26
- def initialize(resource:, attribute:)
27
- @resource = resource
28
- @attribute = attribute
29
-
30
- validate_resource
31
- end
32
-
33
- # Returns the old value of the attribute (if any).
34
- #
35
- # For Reform, this retrieves the current value of the attribute from the model. For
36
- # ActiveRecord, uses the +<attribute>_was+ method.
37
- #
38
- # @return [Object|NilClass]
39
- def old_value
40
- case resource_engine
41
- when :reform
42
- resource.model.send(attribute)
43
- when :active_record
44
- resource.send("#{attribute}_was")
45
- end
46
- end
47
-
48
- # Returns the new (i.e. current) value of the attribute.
49
- #
50
- # Simply sends the attribute name to the resource.
51
- #
52
- # @return [Object]
53
- def new_value
54
- resource.send(attribute)
55
- end
56
-
57
- # Returns whether the attribute has changed, by comparing the new and the old value.
58
- #
59
- # @return [Boolean]
60
- def changed?
61
- old_value != new_value
62
- end
63
-
64
- # Returns the engine used for the resource being authorized (Reform or ActiveRecord).
65
- #
66
- # @return [Symbol] +:reform+ or +:active_record+
67
- #
68
- # @raise [UnknownEngineError] if the engine cannot be detected
69
- def resource_engine
70
- if defined?(Reform::Form) && resource.is_a?(Reform::Form)
71
- :reform
72
- elsif defined?(ActiveRecord::Base) && resource.is_a?(ActiveRecord::Base)
73
- :active_record
74
- else
75
- fail UnknownEngineError(resource: resource, attribute: attribute)
76
- end
77
- end
78
-
79
- # Ensures that the attribute was changed according to the provided options.
80
- #
81
- # When neither +only+ nor +except+ are passed, simply ensures that the attribute was not
82
- # changed.
83
- #
84
- # When +only+ is passed and is not empty, ensures that the value is part of the given array.
85
- #
86
- # When +except+ is passed and not empty, also ensures that the value is NOT part of the given
87
- # array.
88
- #
89
- # @param options [Hash] a hash of options
90
- #
91
- # @option options [Array<String>] :only an optional list of allowed values
92
- # @option options [Array<String>] :except an optional list of forbidden values
93
- #
94
- # @return [Boolean] whether the attribute has an authorized value
95
- def authorize(options = {})
96
- options[:only] = ([options[:only]] || []).flatten.map(&:to_s).reject(&:empty?)
97
- options[:except] = ([options[:except]] || []).flatten.map(&:to_s).reject(&:empty?)
98
-
99
- if options[:only].any? && options[:except].any?
100
- fail(
101
- ArgumentError,
102
- 'The :only and :except options cannot be used at the same time.'
103
- )
104
- end
105
-
106
- return true unless changed?
107
-
108
- if options[:only].any?
109
- options[:only].include?(new_value.to_s)
110
- elsif options[:except].any?
111
- !options[:except].include?(new_value.to_s)
112
- end || false
113
- end
114
-
115
- private
116
-
117
- def validate_resource
118
- resource_engine
119
- end
120
-
121
- # This error when the engine behind a resource cannot be detected for attribute authorization.
122
- #
123
- # @author Alessanro Desantis
124
- class UnknownEngineError < StandardError
125
- MESSAGE = 'Attribute authorization only works with Reform forms and ActiveRecord models.'
126
-
127
- # @!attribute [r] resource
128
- # @return [Object] the resource
129
- attr_reader :resource
130
-
131
- # Initializes the error.
132
- #
133
- # @param resource [Object] the resource
134
- def initialize(resource:)
135
- @resource = resource
136
-
137
- super MESSAGE
138
- end
139
- end
140
- end
141
- end
142
- end