pragma-policy 0.1.0 → 2.0.0

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