pragma-policy 0.1.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 +7 -0
- data/.gitignore +10 -0
- data/.rspec +3 -0
- data/.rubocop.yml +84 -0
- data/.travis.yml +5 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +21 -0
- data/README.md +182 -0
- data/Rakefile +6 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/lib/pragma/policy.rb +12 -0
- data/lib/pragma/policy/attribute_authorizer.rb +142 -0
- data/lib/pragma/policy/base.rb +137 -0
- data/lib/pragma/policy/version.rb +6 -0
- data/pragma-policy.gemspec +29 -0
- metadata +143 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 3558b9e18dec8f44921f49f1390ac27ec81d0633
|
4
|
+
data.tar.gz: ae90ff0b36bbd2ba93c66af2f33f540727517140
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: e3dfe25ea37762ef45e30d474a5176d38f558d528f0352b3cc342a4f18c1508e9c8fe8f6605b67082ef5a8fa06f352bad19a6df420b8d42b2f5d8aa2acfdbeac
|
7
|
+
data.tar.gz: 3d15e2cceb67dc0236f0d9204d3938f0e248946688d07346e4f19bb572ae68b2767987fd1c34a8210659e681f1ee2e29d748164d57a46b487f813cf546d1f6bd
|
data/.gitignore
ADDED
data/.rspec
ADDED
data/.rubocop.yml
ADDED
@@ -0,0 +1,84 @@
|
|
1
|
+
require: rubocop-rspec
|
2
|
+
|
3
|
+
AllCops:
|
4
|
+
TargetRubyVersion: 2.3
|
5
|
+
Include:
|
6
|
+
- '**/Gemfile'
|
7
|
+
- '**/Rakefile'
|
8
|
+
Exclude:
|
9
|
+
- 'bin/*'
|
10
|
+
- 'db/**/*'
|
11
|
+
- 'vendor/bundle/**/*'
|
12
|
+
- 'spec/spec_helper.rb'
|
13
|
+
- 'spec/rails_helper.rb'
|
14
|
+
- 'spec/support/**/*'
|
15
|
+
- 'config/**/*'
|
16
|
+
- '**/Rakefile'
|
17
|
+
- '**/Gemfile'
|
18
|
+
|
19
|
+
RSpec/DescribeClass:
|
20
|
+
Exclude:
|
21
|
+
- 'spec/requests/**/*'
|
22
|
+
|
23
|
+
Style/BlockDelimiters:
|
24
|
+
Exclude:
|
25
|
+
- 'spec/**/*'
|
26
|
+
|
27
|
+
Style/AlignParameters:
|
28
|
+
EnforcedStyle: with_fixed_indentation
|
29
|
+
|
30
|
+
Style/ClosingParenthesisIndentation:
|
31
|
+
Enabled: false
|
32
|
+
|
33
|
+
Metrics/LineLength:
|
34
|
+
Max: 100
|
35
|
+
AllowURI: true
|
36
|
+
|
37
|
+
Style/FirstParameterIndentation:
|
38
|
+
Enabled: false
|
39
|
+
|
40
|
+
Style/MultilineMethodCallIndentation:
|
41
|
+
EnforcedStyle: indented
|
42
|
+
|
43
|
+
Style/IndentArray:
|
44
|
+
EnforcedStyle: consistent
|
45
|
+
|
46
|
+
Style/IndentHash:
|
47
|
+
EnforcedStyle: consistent
|
48
|
+
|
49
|
+
Style/SignalException:
|
50
|
+
EnforcedStyle: semantic
|
51
|
+
|
52
|
+
Style/BracesAroundHashParameters:
|
53
|
+
EnforcedStyle: context_dependent
|
54
|
+
|
55
|
+
Lint/EndAlignment:
|
56
|
+
AlignWith: variable
|
57
|
+
AutoCorrect: true
|
58
|
+
|
59
|
+
Style/AndOr:
|
60
|
+
EnforcedStyle: conditionals
|
61
|
+
|
62
|
+
Style/MultilineBlockChain:
|
63
|
+
Enabled: false
|
64
|
+
|
65
|
+
RSpec/NamedSubject:
|
66
|
+
Enabled: false
|
67
|
+
|
68
|
+
RSpec/ExampleLength:
|
69
|
+
Enabled: false
|
70
|
+
|
71
|
+
Style/MultilineMethodCallBraceLayout:
|
72
|
+
Enabled: false
|
73
|
+
|
74
|
+
Metrics/MethodLength:
|
75
|
+
Enabled: false
|
76
|
+
|
77
|
+
Metrics/AbcSize:
|
78
|
+
Enabled: false
|
79
|
+
|
80
|
+
Metrics/PerceivedComplexity:
|
81
|
+
Enabled: false
|
82
|
+
|
83
|
+
Metrics/CyclomaticComplexity:
|
84
|
+
Enabled: false
|
data/.travis.yml
ADDED
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2016 Alessandro Desantis
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
13
|
+
all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,182 @@
|
|
1
|
+
# Pragma::Policy
|
2
|
+
|
3
|
+
[](https://travis-ci.org/pragmarb/pragma-policy)
|
4
|
+
[](https://gemnasium.com/github.com/pragmarb/pragma-policy)
|
5
|
+
[](https://codeclimate.com/github/pragmarb/pragma-policy)
|
6
|
+
[](https://coveralls.io/github/pragmarb/pragma-policy)
|
7
|
+
|
8
|
+
Policies provide fine-grained access control for your API resources.
|
9
|
+
|
10
|
+
They are built on top of [Reform](https://github.com/apotonick/reform).
|
11
|
+
|
12
|
+
## Installation
|
13
|
+
|
14
|
+
Add this line to your application's Gemfile:
|
15
|
+
|
16
|
+
```ruby
|
17
|
+
gem 'pragma-policy'
|
18
|
+
```
|
19
|
+
|
20
|
+
And then execute:
|
21
|
+
|
22
|
+
```console
|
23
|
+
$ bundle
|
24
|
+
```
|
25
|
+
|
26
|
+
Or install it yourself as:
|
27
|
+
|
28
|
+
```console
|
29
|
+
$ gem install pragma-policy
|
30
|
+
```
|
31
|
+
|
32
|
+
## Usage
|
33
|
+
|
34
|
+
To create a policy, simply inherit from `Pragma::Policy::Base`:
|
35
|
+
|
36
|
+
```ruby
|
37
|
+
module API
|
38
|
+
module V1
|
39
|
+
module Post
|
40
|
+
class Policy < Pragma::Policy::Base
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
```
|
46
|
+
|
47
|
+
By default, the policy does not return any objects and forbids all operations.
|
48
|
+
|
49
|
+
You can start customizing your policy by defining a scope and operation predicates:
|
50
|
+
|
51
|
+
```ruby
|
52
|
+
module API
|
53
|
+
module V1
|
54
|
+
module Post
|
55
|
+
class Policy < Pragma::Policy::Base
|
56
|
+
def self.accessible_by(user:, scope:)
|
57
|
+
scope.where('published = ? OR author_id = ?', true, user.id)
|
58
|
+
end
|
59
|
+
|
60
|
+
def show?
|
61
|
+
resource.published? || resource.author_id == user.id
|
62
|
+
end
|
63
|
+
|
64
|
+
def update?
|
65
|
+
resource.author_id == user.id
|
66
|
+
end
|
67
|
+
|
68
|
+
def destroy?
|
69
|
+
resource.author_id == user.id
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
```
|
76
|
+
|
77
|
+
You are ready to use your policy!
|
78
|
+
|
79
|
+
### Retrieving records
|
80
|
+
|
81
|
+
To retrieve all the records accessible by a user, use the `.accessible_by` class method:
|
82
|
+
|
83
|
+
```ruby
|
84
|
+
posts = API::V1::Post::Policy.accessible_by(user: user, scope: Post.all)
|
85
|
+
```
|
86
|
+
|
87
|
+
### Authorizing operations
|
88
|
+
|
89
|
+
To authorize an operation, first instantiate the policy, then use the predicate methods:
|
90
|
+
|
91
|
+
```ruby
|
92
|
+
policy = API::V1::Post::Policy.new(user: user, resource: post)
|
93
|
+
fail 'You cannot update this post!' unless policy.update?
|
94
|
+
```
|
95
|
+
|
96
|
+
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`:
|
98
|
+
|
99
|
+
```ruby
|
100
|
+
policy = API::V1::Post::Policy.new(user: user, resource: post)
|
101
|
+
policy.update! # raises if the user cannot update the post
|
102
|
+
```
|
103
|
+
|
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
|
+
## Contributing
|
177
|
+
|
178
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/pragmarb/pragma-policy.
|
179
|
+
|
180
|
+
## License
|
181
|
+
|
182
|
+
The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
|
data/Rakefile
ADDED
data/bin/console
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require "bundler/setup"
|
4
|
+
require "pragma/policy"
|
5
|
+
|
6
|
+
# You can add fixtures and/or initialization code here to make experimenting
|
7
|
+
# with your gem easier. You can also use a different console, if you like.
|
8
|
+
|
9
|
+
# (If you use this, don't forget to add pry to your Gemfile!)
|
10
|
+
# require "pry"
|
11
|
+
# Pry.start
|
12
|
+
|
13
|
+
require "irb"
|
14
|
+
IRB.start
|
data/bin/setup
ADDED
@@ -0,0 +1,12 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require 'pragma/policy/version'
|
3
|
+
require 'pragma/policy/base'
|
4
|
+
require 'pragma/policy/attribute_authorizer'
|
5
|
+
|
6
|
+
module Pragma
|
7
|
+
# Fine-grained access control for your API resources.
|
8
|
+
#
|
9
|
+
# @author Alessandro Desantis
|
10
|
+
module Policy
|
11
|
+
end
|
12
|
+
end
|
@@ -0,0 +1,142 @@
|
|
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
|
@@ -0,0 +1,137 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
module Pragma
|
3
|
+
module Policy
|
4
|
+
# This is the base policy class that all your resource-specific policies should inherit from.
|
5
|
+
#
|
6
|
+
# A policy provides predicate methods for determining whether a user can perform a specific
|
7
|
+
# action on a resource.
|
8
|
+
#
|
9
|
+
# @author Alessandro Desantis
|
10
|
+
#
|
11
|
+
# @abstract Subclass and implement action methods to create a policy.
|
12
|
+
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
|
19
|
+
|
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
|
30
|
+
end
|
31
|
+
|
32
|
+
# Initializes the policy.
|
33
|
+
#
|
34
|
+
# @param user [Object] the user operating on the resource
|
35
|
+
# @param resource [Object] the resource being operated on
|
36
|
+
def initialize(user:, resource:)
|
37
|
+
@user = user
|
38
|
+
@resource = resource
|
39
|
+
end
|
40
|
+
|
41
|
+
# Returns whether the policy responds to the provided missing method.
|
42
|
+
#
|
43
|
+
# Supports bang forms of predicates (+create!+, +update!+ etc.).
|
44
|
+
#
|
45
|
+
# @param method_name [String] the method name
|
46
|
+
# @param include_private [Boolean] whether to consider private methods
|
47
|
+
#
|
48
|
+
# @return [Boolean]
|
49
|
+
def respond_to_missing?(method_name, include_private = false)
|
50
|
+
return super unless method_name[-1] == '!'
|
51
|
+
respond_to?("#{method_name[0..-2]}?", include_private) || super
|
52
|
+
end
|
53
|
+
|
54
|
+
# Provides bang form of predicates (+create!+, +update!+ etc.).
|
55
|
+
#
|
56
|
+
# @param method_name [String] the method name
|
57
|
+
# @param *args [Array<Object>] the method arguments
|
58
|
+
#
|
59
|
+
# @return [Object]
|
60
|
+
def method_missing(method_name, *args, &block)
|
61
|
+
return super unless method_name[-1] == '!'
|
62
|
+
authorize method_name[0..-2], *args
|
63
|
+
end
|
64
|
+
|
65
|
+
# Authorizes the user to perform the given action. If not authorized, raises a
|
66
|
+
# {ForbiddenError}.
|
67
|
+
#
|
68
|
+
# @param action [Symbol] the action to authorize
|
69
|
+
#
|
70
|
+
# @raise [ArgumentError] if the action is not defined in this policy
|
71
|
+
# @raise [ForbiddenError] if the user is not authorized to perform the action
|
72
|
+
def authorize(action)
|
73
|
+
unless respond_to?("#{action}?")
|
74
|
+
fail(
|
75
|
+
ArgumentError,
|
76
|
+
"'#{action}' is not a valid action for this policy."
|
77
|
+
)
|
78
|
+
end
|
79
|
+
|
80
|
+
return if send("#{action}?")
|
81
|
+
|
82
|
+
fail(
|
83
|
+
ForbiddenError,
|
84
|
+
user: user,
|
85
|
+
action: action,
|
86
|
+
resource: resource
|
87
|
+
)
|
88
|
+
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
|
+
end
|
136
|
+
end
|
137
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'pragma/policy/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "pragma-policy"
|
8
|
+
spec.version = Pragma::Policy::VERSION
|
9
|
+
spec.authors = ["Alessandro Desantis"]
|
10
|
+
spec.email = ["desa.alessandro@gmail.com"]
|
11
|
+
|
12
|
+
spec.summary = 'Fine-grained access control for your API resources.'
|
13
|
+
spec.homepage = "https://github.com/pragmarb/pragma-policy"
|
14
|
+
spec.license = "MIT"
|
15
|
+
|
16
|
+
spec.files = `git ls-files -z`.split("\x0").reject do |f|
|
17
|
+
f.match(%r{^(test|spec|features)/})
|
18
|
+
end
|
19
|
+
spec.bindir = "exe"
|
20
|
+
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
21
|
+
spec.require_paths = ["lib"]
|
22
|
+
|
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"
|
29
|
+
end
|
metadata
ADDED
@@ -0,0 +1,143 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: pragma-policy
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Alessandro Desantis
|
8
|
+
autorequire:
|
9
|
+
bindir: exe
|
10
|
+
cert_chain: []
|
11
|
+
date: 2016-12-26 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: bundler
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '0'
|
20
|
+
type: :development
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: rake
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ">="
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: rspec
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ">="
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: rubocop
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - ">="
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - ">="
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '0'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: rubocop-rspec
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - ">="
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '0'
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - ">="
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '0'
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: coveralls
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - ">="
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '0'
|
90
|
+
type: :development
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - ">="
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '0'
|
97
|
+
description:
|
98
|
+
email:
|
99
|
+
- desa.alessandro@gmail.com
|
100
|
+
executables: []
|
101
|
+
extensions: []
|
102
|
+
extra_rdoc_files: []
|
103
|
+
files:
|
104
|
+
- ".gitignore"
|
105
|
+
- ".rspec"
|
106
|
+
- ".rubocop.yml"
|
107
|
+
- ".travis.yml"
|
108
|
+
- Gemfile
|
109
|
+
- LICENSE.txt
|
110
|
+
- README.md
|
111
|
+
- Rakefile
|
112
|
+
- bin/console
|
113
|
+
- bin/setup
|
114
|
+
- lib/pragma/policy.rb
|
115
|
+
- lib/pragma/policy/attribute_authorizer.rb
|
116
|
+
- lib/pragma/policy/base.rb
|
117
|
+
- lib/pragma/policy/version.rb
|
118
|
+
- pragma-policy.gemspec
|
119
|
+
homepage: https://github.com/pragmarb/pragma-policy
|
120
|
+
licenses:
|
121
|
+
- MIT
|
122
|
+
metadata: {}
|
123
|
+
post_install_message:
|
124
|
+
rdoc_options: []
|
125
|
+
require_paths:
|
126
|
+
- lib
|
127
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
128
|
+
requirements:
|
129
|
+
- - ">="
|
130
|
+
- !ruby/object:Gem::Version
|
131
|
+
version: '0'
|
132
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
133
|
+
requirements:
|
134
|
+
- - ">="
|
135
|
+
- !ruby/object:Gem::Version
|
136
|
+
version: '0'
|
137
|
+
requirements: []
|
138
|
+
rubyforge_project:
|
139
|
+
rubygems_version: 2.5.2
|
140
|
+
signing_key:
|
141
|
+
specification_version: 4
|
142
|
+
summary: Fine-grained access control for your API resources.
|
143
|
+
test_files: []
|