pundit_roles 0.1.2
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/.travis.yml +5 -0
- data/Gemfile +17 -0
- data/LICENSE.txt +21 -0
- data/README.md +329 -0
- data/Rakefile +2 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/lib/pundit_roles/policy/base.rb +148 -0
- data/lib/pundit_roles/policy/policy_defaults/defaults.rb +78 -0
- data/lib/pundit_roles/policy/role/base.rb +169 -0
- data/lib/pundit_roles/policy/role.rb +99 -0
- data/lib/pundit_roles/pundit.rb +34 -0
- data/lib/pundit_roles/version.rb +3 -0
- data/lib/pundit_roles.rb +13 -0
- data/pundit_roles.gemspec +26 -0
- metadata +74 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: f9a596e75302e60e5ac89fbd28e1d411f1e80e85
|
4
|
+
data.tar.gz: 70ef89deb9dbc45c8c3c3a3386f858662a1480ab
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 8f1e0eea9562fa2add0c1c9faebba5cd4a8ccb1441dde646c58eabd76193691da924eb3775cd251ce6658dfe90f5a1eb879f767944032aa5f4b75d3f4cebcf83
|
7
|
+
data.tar.gz: 2dcef9da90cad672759c3d315b1259b1a999bd0445fc5a4139d272aef0b1e78ce9fc445fe9ab9260329c773af0a5ecc0924c14af11768dd92000736df00b8558
|
data/.gitignore
ADDED
data/.travis.yml
ADDED
data/Gemfile
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
source 'https://rubygems.org'
|
2
|
+
|
3
|
+
# Specify your gem's dependencies in pundit_roles.gemspec
|
4
|
+
gemspec
|
5
|
+
|
6
|
+
gem 'actionpack'
|
7
|
+
gem 'activemodel'
|
8
|
+
gem 'pundit', '~> 1.1.0'
|
9
|
+
|
10
|
+
group :development, :test do
|
11
|
+
gem "actionpack"
|
12
|
+
gem "activemodel"
|
13
|
+
gem "bundler"
|
14
|
+
gem "pry"
|
15
|
+
gem "rake"
|
16
|
+
gem "rspec"
|
17
|
+
end
|
data/LICENSE.txt
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2017 StairwayB
|
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,329 @@
|
|
1
|
+
# PunditRoles
|
2
|
+
|
3
|
+
PunditRoles is a helper gem which works on top of [Pundit](https://github.com/elabs/pundit)
|
4
|
+
(if you are not familiar with Pundit, it is recommended you read it's documentation before continuing).
|
5
|
+
It allows you to extend Pundit's authorization system to include attributes and associations.
|
6
|
+
|
7
|
+
If you are already using Pundit, this should not conflict with any of Pundit's existing functionality.
|
8
|
+
You may use Pundit's features as well as the features from this gem interchangeably.
|
9
|
+
|
10
|
+
Please note that this gem is not affiliated with Pundit or it's creators, but it very much
|
11
|
+
appreciates the work that they did with their great authorization system.
|
12
|
+
|
13
|
+
* **Important** This is still early in it's development and is **NOT** considered production
|
14
|
+
ready. Consider what is here as a prototype for what will, in the future, be a reliable gem.
|
15
|
+
As of yet, bugs and unforeseen issues may be present. If you happen to find any, please feel free
|
16
|
+
to raise an issue.
|
17
|
+
|
18
|
+
## Installation
|
19
|
+
|
20
|
+
Add this line to your application's Gemfile:
|
21
|
+
|
22
|
+
```ruby
|
23
|
+
gem 'pundit_roles'
|
24
|
+
```
|
25
|
+
|
26
|
+
Add PunditRoles to your ApplicationController(Pundit is included in PunditRoles,
|
27
|
+
so no need to add both)
|
28
|
+
```ruby
|
29
|
+
class ApplicationController < ActionController::Base
|
30
|
+
include PunditRoles
|
31
|
+
end
|
32
|
+
```
|
33
|
+
|
34
|
+
And inherit your ApplicationPolicy from Policy::Base
|
35
|
+
```ruby
|
36
|
+
class ApplicationPolicy < Policy::Base
|
37
|
+
# def show?
|
38
|
+
# [...]
|
39
|
+
# end
|
40
|
+
end
|
41
|
+
```
|
42
|
+
|
43
|
+
## Roles
|
44
|
+
|
45
|
+
PunditRoles operates around the notion of _**roles**_. Each role needs to be defined at the Policy level
|
46
|
+
and provided with a conditional method that determines whether the `@user`(`current_user` in the context of a Policy)
|
47
|
+
falls into this role. Additionally, each role can have a set of permitted
|
48
|
+
_**attributes**_ and _**associations**_(from here on collectively referred to as **_options_**)
|
49
|
+
defined for it. A basic example for a UserPolicy would be:
|
50
|
+
```ruby
|
51
|
+
role :user, authorize_with: :logged_in_user
|
52
|
+
permitted_for :user,
|
53
|
+
attributes: {
|
54
|
+
show: %i(username name avatar is_confirmed created_at)
|
55
|
+
},
|
56
|
+
associations: {
|
57
|
+
show: %i(posts followers following)
|
58
|
+
}
|
59
|
+
|
60
|
+
role :correct_user, authorize_with: :correct_user
|
61
|
+
permitted_for :correct_user,
|
62
|
+
attributes: {
|
63
|
+
show: %i(email phone_number confirmed_at updated_at sign_in_count),
|
64
|
+
update: %i(username email password password_confirmation current_password name avatar)
|
65
|
+
},
|
66
|
+
associations: {
|
67
|
+
show: %i(settings),
|
68
|
+
save: %i(settings)
|
69
|
+
}
|
70
|
+
```
|
71
|
+
|
72
|
+
This assumes that there are two methods defined in the UserPolicy called `logged_in_user?` and
|
73
|
+
`correct_user?`. More on that later.
|
74
|
+
|
75
|
+
And then in you query method, you simply say:
|
76
|
+
```ruby
|
77
|
+
def show?
|
78
|
+
%i(user correct_user)
|
79
|
+
end
|
80
|
+
|
81
|
+
def update?
|
82
|
+
%i(correct_user)
|
83
|
+
end
|
84
|
+
```
|
85
|
+
Or you may use the `allow` helper method:
|
86
|
+
```ruby
|
87
|
+
def show?
|
88
|
+
allow :user, :correct_user
|
89
|
+
end
|
90
|
+
```
|
91
|
+
|
92
|
+
Finally, in your controller you call Pundit's `authorize` method and pass it's return value
|
93
|
+
to a variable:
|
94
|
+
```ruby
|
95
|
+
class UserController < ApplicationController
|
96
|
+
def show
|
97
|
+
@user = User.find(params[:id])
|
98
|
+
permitted = authorize @user
|
99
|
+
# [...]
|
100
|
+
end
|
101
|
+
end
|
102
|
+
```
|
103
|
+
|
104
|
+
The `authorize` method will return a hash of permitted attributes and associations for the corresponding action that the
|
105
|
+
user has access to. What you do with that is your business. Accessors for each segment look like this:
|
106
|
+
```ruby
|
107
|
+
permitted[:attributes][:show]
|
108
|
+
permitted[:attributes][:create]
|
109
|
+
|
110
|
+
permitted[:associations][:show]
|
111
|
+
permitted[:associations][:update]
|
112
|
+
```
|
113
|
+
|
114
|
+
If the user does not fall into any roles permitted by a query, the `authorize` method will raise `Pundit::NotAuthorizedError`
|
115
|
+
|
116
|
+
### Defining roles
|
117
|
+
|
118
|
+
Roles are defined with the `role` method. It receives the name of the role as it's first argument and the
|
119
|
+
options for the role as it's second. The required option is the `authorize_with` attribute, which is the method
|
120
|
+
that validates the role. The validation method must be passed as a symbol without the question mark, and declared
|
121
|
+
as a method with a question mark.
|
122
|
+
|
123
|
+
Currently there are no more options, but some, like database permissions, are planned for future updates.
|
124
|
+
|
125
|
+
```ruby
|
126
|
+
role :user, authorize_with: :logged_in_user
|
127
|
+
|
128
|
+
def logged_in_user?
|
129
|
+
@user.present?
|
130
|
+
end
|
131
|
+
```
|
132
|
+
|
133
|
+
### Users with multiple roles
|
134
|
+
|
135
|
+
You may have noticed that in the first example `correct_user` has fewer permitted options
|
136
|
+
defined than `user`. That is because PunditRoles does not treat roles as exclusionary.
|
137
|
+
Users may have a single role or they may have multiple roles, within the context of the model they are trying to access.
|
138
|
+
In the previous example, a `correct_user`, meaning a `user` trying to access it's own model, is naturally
|
139
|
+
also a regular `user`, so it will have access to all options a regular `user` has access to plus the
|
140
|
+
options that a `correct_user` has access to.
|
141
|
+
|
142
|
+
Take this example, to better illustrate what is happening:
|
143
|
+
|
144
|
+
```ruby
|
145
|
+
role :user, authorize_with: :logged_in_user
|
146
|
+
permitted_for :user,
|
147
|
+
attributes: {
|
148
|
+
show: %i(username name avatar)
|
149
|
+
}
|
150
|
+
|
151
|
+
role :correct_user, authorize_with: :correct_user
|
152
|
+
permitted_for :correct_user,
|
153
|
+
attributes: {
|
154
|
+
show: %i(email phone_number)
|
155
|
+
}
|
156
|
+
|
157
|
+
role :admin, authorize_with: :admin
|
158
|
+
permitted_for :admin,
|
159
|
+
attributes: {
|
160
|
+
show: %i(email is_admin)
|
161
|
+
}
|
162
|
+
```
|
163
|
+
|
164
|
+
Here, a user which fulfills the `admin` condition trying to access it's own model, would receive the
|
165
|
+
options of all three roles, meaning the `permitted[:attributes][:show]` would look like:
|
166
|
+
```ruby
|
167
|
+
[:username, :name, :avatar, :email, :phone_number, :is_admin]
|
168
|
+
```
|
169
|
+
Notice that there are no duplicates. This is because whenever a user tries to access an action,
|
170
|
+
PunditRoles will evaluate whether the user falls into the roles permitted to perform said action,
|
171
|
+
and if they do, it will uniquely merge the options hashes of all of these.
|
172
|
+
|
173
|
+
If the user is an `admin`, but is not a `correct_user`, it will not receive the `phone_number` attribute,
|
174
|
+
because that is unique to `correct_user` and vice versa.
|
175
|
+
|
176
|
+
At present, there is no way to prevent merging of roles. Such a feature may be coming in a future update.
|
177
|
+
|
178
|
+
### Inheritance and the default Guest role
|
179
|
+
|
180
|
+
One thing to watch out for is that roles are inherited but options are not.
|
181
|
+
This means that you may declare commonly used roles(whose validations are
|
182
|
+
independent of the `@record` of the Policy) in the ApplicationPolicy, and may reuse them
|
183
|
+
further down the line. You may also overwrite roles defined in a parent class(these will not affect those in the parent).
|
184
|
+
|
185
|
+
However, it is important to declare the options with the `permitted_for` method for each role that you permit
|
186
|
+
in your Policy, otherwise the role will return an empty hash.
|
187
|
+
|
188
|
+
With that in mind, PunditRoles comes with a default `:guest` role, which simply checks if
|
189
|
+
the user is nil. If you wish to permit guest users for a particular action, simply define the
|
190
|
+
options for it and allow it in your query method.
|
191
|
+
|
192
|
+
```ruby
|
193
|
+
class UserPolicy < ApplicationPolicy
|
194
|
+
permitted_for :guest,
|
195
|
+
attributes: {
|
196
|
+
show: %i(username first_name last_name avatar),
|
197
|
+
create: %i(username email password password_confirmation first_name last_name avatar)
|
198
|
+
},
|
199
|
+
associations: {}
|
200
|
+
|
201
|
+
def show?
|
202
|
+
allow :guest
|
203
|
+
end
|
204
|
+
|
205
|
+
def create?
|
206
|
+
allow :guest
|
207
|
+
end
|
208
|
+
|
209
|
+
end
|
210
|
+
```
|
211
|
+
|
212
|
+
* **Important!** The `:guest` role is exclusionary by default, meaning it cannot be merged
|
213
|
+
with other roles. It is also the first role that is evaluated, and if the user is a `:guest`, it will return the guest
|
214
|
+
attributes if `:guest` is allowed, or raise `PunditNotAuthorized` if not.
|
215
|
+
Do **NOT** overwrite the `:guest` role, that can lead to unexpected side effects, and if you wish to allow guest, use
|
216
|
+
the existing role and not a custom one.
|
217
|
+
|
218
|
+
### Explicit declaration of options
|
219
|
+
|
220
|
+
Options are declared with the `permitted_for` method, which receives the role as it's first argument,
|
221
|
+
and the options as it's second.
|
222
|
+
|
223
|
+
Valid options for the `permitted_for` method are `:attributes` and `:associations`.
|
224
|
+
Within these, valid options are `:show`,`:create`,`:update` and `:save` or the implicit options.
|
225
|
+
|
226
|
+
### Implicit declaration of options
|
227
|
+
|
228
|
+
PunditRoles provides a set of helpers to be able to implicitly declare the options of a role.
|
229
|
+
|
230
|
+
---
|
231
|
+
|
232
|
+
Although this is a possibility, it is _highly recommended_ that you explicitly declare
|
233
|
+
attributes for each role, to avoid any issues further in development, like say, an extra
|
234
|
+
attribute that is added to a model later down the line.
|
235
|
+
|
236
|
+
---
|
237
|
+
* **show_all**
|
238
|
+
|
239
|
+
Will be able to view all non-restricted options.
|
240
|
+
|
241
|
+
```ruby
|
242
|
+
role :admin, authorize_with: :admin
|
243
|
+
permitted_for :admin,
|
244
|
+
attributes: :show_all,
|
245
|
+
associations: :show_all
|
246
|
+
```
|
247
|
+
* **create_all, update_all, save_all**
|
248
|
+
|
249
|
+
Will be able to create, update or save all non-restricted attributes. These options also
|
250
|
+
imply that the role will be able to `show_all` options.
|
251
|
+
```ruby
|
252
|
+
role :admin, authorize_with: :admin
|
253
|
+
permitted_for :admin,
|
254
|
+
attributes: :save_all,
|
255
|
+
associations: :update_all
|
256
|
+
```
|
257
|
+
|
258
|
+
* **all**
|
259
|
+
|
260
|
+
Declare on a per-action basis whether the role has access to all options.
|
261
|
+
```ruby
|
262
|
+
role :admin, authorize_with: :admin
|
263
|
+
permitted_for :admin,
|
264
|
+
attributes: {
|
265
|
+
show: :all,
|
266
|
+
save: %i(name username email)
|
267
|
+
},
|
268
|
+
associations: {
|
269
|
+
show: :all
|
270
|
+
}
|
271
|
+
```
|
272
|
+
|
273
|
+
* **all_minus**
|
274
|
+
|
275
|
+
Can be used to allow all attributes, except those declared.
|
276
|
+
```ruby
|
277
|
+
role :admin, authorize_with: :admin
|
278
|
+
permitted_for :admin,
|
279
|
+
attributes: {
|
280
|
+
show: [:all_minus, :password_digest]
|
281
|
+
}
|
282
|
+
```
|
283
|
+
The `:admin` role will now be able to view all attributes, except `password_digest`.
|
284
|
+
|
285
|
+
### Restricted options
|
286
|
+
|
287
|
+
PunditRoles allows you to define restricted options which will be removed when declaring
|
288
|
+
implicitly. By default, only the `:id`, `:created_at`, `:updated_at` attributes are restricted
|
289
|
+
for `create`,`update` and `save` actions. You may overwrite this behaviour on a per-policy basis:
|
290
|
+
```ruby
|
291
|
+
private
|
292
|
+
|
293
|
+
def restricted_show_attributes
|
294
|
+
[:attr_one, :attr_two]
|
295
|
+
end
|
296
|
+
```
|
297
|
+
Or if you want to add to it, instead of overwriting, use `super`:
|
298
|
+
```ruby
|
299
|
+
private
|
300
|
+
|
301
|
+
def restricted_create_attributes
|
302
|
+
super + [:attr_one, :attr_two]
|
303
|
+
end
|
304
|
+
```
|
305
|
+
|
306
|
+
There are 8 `restricted_#{action}_#{option_type}` methods in total, where `option_type` refers
|
307
|
+
to either `attributes` or `associations` and `action` refers to `show`, `create`, `update` or `save`.
|
308
|
+
|
309
|
+
## Planned updates
|
310
|
+
|
311
|
+
Support for Pundit's scope method should be added in the near future, along with authorizing associations,
|
312
|
+
generators, and rspec helpers. And once the test suite is finished for this gem, it should be production
|
313
|
+
ready.
|
314
|
+
|
315
|
+
## Development
|
316
|
+
|
317
|
+
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake 'spec'` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
318
|
+
|
319
|
+
To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
|
320
|
+
|
321
|
+
## Contributing
|
322
|
+
|
323
|
+
Bug reports are welcome on GitHub at [StairwayB](https://github.com/StairwayB/pundit_roles).
|
324
|
+
|
325
|
+
|
326
|
+
## License
|
327
|
+
|
328
|
+
The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
|
329
|
+
|
data/Rakefile
ADDED
data/bin/console
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require "bundler/setup"
|
4
|
+
require "pundit_roles"
|
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(__FILE__)
|
data/bin/setup
ADDED
@@ -0,0 +1,148 @@
|
|
1
|
+
require_relative 'role'
|
2
|
+
require_relative 'policy_defaults/defaults'
|
3
|
+
|
4
|
+
|
5
|
+
module Policy
|
6
|
+
|
7
|
+
# Base policy class to be extended by all other policies, authorizes users based on roles they fall into,
|
8
|
+
# return a uniquely merged hash of permitted attributes and associations of each role the @user has.
|
9
|
+
#
|
10
|
+
# @param user [Object] the user that initiated the action
|
11
|
+
# @param record [Object] the object we're checking permissions of
|
12
|
+
class Base
|
13
|
+
extend Role
|
14
|
+
|
15
|
+
include PolicyDefaults::Defaults
|
16
|
+
|
17
|
+
role :guest, authorize_with: :user_guest
|
18
|
+
|
19
|
+
attr_reader :user, :record
|
20
|
+
|
21
|
+
def initialize(user, record)
|
22
|
+
@user = user
|
23
|
+
@record = record
|
24
|
+
end
|
25
|
+
|
26
|
+
# Is here
|
27
|
+
def scope
|
28
|
+
Pundit.authorize_scope!(user, record.class, fields)
|
29
|
+
end
|
30
|
+
|
31
|
+
# Retrieves the permitted roles for the current @query, checks if @user is one or more of these roles
|
32
|
+
# and return a hash of attributes and associations that the @user has access to.
|
33
|
+
#
|
34
|
+
# @param query [Symbol, String] the predicate method to check on the policy (e.g. `:show?`)
|
35
|
+
def resolve_query(query)
|
36
|
+
permitted_roles = public_send(query)
|
37
|
+
if permitted_roles.is_a? TrueClass or permitted_roles.is_a? FalseClass
|
38
|
+
return permitted_roles
|
39
|
+
end
|
40
|
+
|
41
|
+
permissions_hash = self.class.permissions_hash
|
42
|
+
|
43
|
+
# Always checks if the @user is a :guest first. :guest users cannot only have the one :guest role
|
44
|
+
guest = self.class::Guest.new(self, permissions_hash[:guest])
|
45
|
+
if guest.test_condition
|
46
|
+
if permitted_roles.include? :guest
|
47
|
+
return guest.permitted
|
48
|
+
else
|
49
|
+
return false
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
current_roles = determine_current_roles(permitted_roles, permissions_hash)
|
54
|
+
|
55
|
+
unless current_roles.present?
|
56
|
+
return false
|
57
|
+
end
|
58
|
+
|
59
|
+
if current_roles.length == 1
|
60
|
+
current_roles.values[0][:roles] = current_roles.keys[0]
|
61
|
+
return current_roles.values[0]
|
62
|
+
end
|
63
|
+
|
64
|
+
return unique_merge(current_roles)
|
65
|
+
end
|
66
|
+
|
67
|
+
private
|
68
|
+
|
69
|
+
# Build a hash of the roles that the user fulfills and the roles' attributes and associations,
|
70
|
+
# based on the test_condition of the role.
|
71
|
+
#
|
72
|
+
# @param permitted_roles [Hash] roles returned by the query
|
73
|
+
# @param permissions_hash [Hash] unrefined hash of options defined by all permitted_for methods
|
74
|
+
def determine_current_roles(permitted_roles, permissions_hash)
|
75
|
+
current_roles = {}
|
76
|
+
|
77
|
+
permitted_roles.each do |permitted_role|
|
78
|
+
if permitted_role == :guest
|
79
|
+
next
|
80
|
+
end
|
81
|
+
|
82
|
+
begin
|
83
|
+
current_role = {class: "#{self.class}::#{permitted_role.to_s.classify}".constantize}
|
84
|
+
current_role_obj = current_role[:class].new(self, permissions_hash[permitted_role])
|
85
|
+
if current_role_obj.test_condition
|
86
|
+
current_roles[permitted_role] = current_role_obj.permitted
|
87
|
+
end
|
88
|
+
rescue NoMethodError =>e
|
89
|
+
raise NoMethodError, "Could not find test condition, needs to be defined as 'test_condition?' and passed to the role as 'authorize_with: :test_condition' => #{e.message}"
|
90
|
+
rescue NameError => e
|
91
|
+
raise NameError, "#{current_role[:role]} not defined => #{e.message} "
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
return current_roles
|
96
|
+
end
|
97
|
+
|
98
|
+
# Uniquely merge the options of all roles that the user fulfills
|
99
|
+
#
|
100
|
+
# @param roles [Hash] roles and options that the user fulfills
|
101
|
+
def unique_merge(roles)
|
102
|
+
merged_hash = {attributes: {}, associations: {}, roles: []}
|
103
|
+
|
104
|
+
roles.each do |role, option|
|
105
|
+
unless option.present?
|
106
|
+
next
|
107
|
+
end
|
108
|
+
merged_hash[:roles] << role
|
109
|
+
option.each do |type, actions|
|
110
|
+
unless actions.present?
|
111
|
+
next
|
112
|
+
end
|
113
|
+
actions.each do |key, value|
|
114
|
+
unless merged_hash[type][key]
|
115
|
+
merged_hash[type][key] = []
|
116
|
+
end
|
117
|
+
merged_hash[type][key] |= value
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
return merged_hash
|
123
|
+
end
|
124
|
+
|
125
|
+
# Helper method to be able to define allow: :guest, :user, etc. in the query methods
|
126
|
+
#
|
127
|
+
# @param *roles [Array] an array of permitted roles for a particular action
|
128
|
+
def allow(*roles)
|
129
|
+
return roles
|
130
|
+
end
|
131
|
+
|
132
|
+
# Scope class from Pundit, to be used for limiting scopes. Unchanged from Pundit,
|
133
|
+
# possible implementation forthcoming in a future update
|
134
|
+
class Scope
|
135
|
+
attr_reader :user, :scope
|
136
|
+
|
137
|
+
def initialize(user, scope)
|
138
|
+
@user = user
|
139
|
+
@scope = scope
|
140
|
+
end
|
141
|
+
|
142
|
+
def resolve
|
143
|
+
scope
|
144
|
+
end
|
145
|
+
|
146
|
+
end
|
147
|
+
end
|
148
|
+
end
|
@@ -0,0 +1,78 @@
|
|
1
|
+
module PolicyDefaults
|
2
|
+
module Defaults
|
3
|
+
# default index? method
|
4
|
+
def index?
|
5
|
+
false
|
6
|
+
end
|
7
|
+
|
8
|
+
# default show? method
|
9
|
+
def show?
|
10
|
+
false
|
11
|
+
end
|
12
|
+
|
13
|
+
# default create? method
|
14
|
+
def create?
|
15
|
+
false
|
16
|
+
end
|
17
|
+
|
18
|
+
# default update? method
|
19
|
+
def update?
|
20
|
+
false
|
21
|
+
end
|
22
|
+
|
23
|
+
# default destroy? method
|
24
|
+
def destroy?
|
25
|
+
false
|
26
|
+
end
|
27
|
+
|
28
|
+
# default authorization method
|
29
|
+
def default_authorization?
|
30
|
+
return false
|
31
|
+
end
|
32
|
+
|
33
|
+
# @authorize_with method for :guest role
|
34
|
+
def user_guest?
|
35
|
+
@user.nil?
|
36
|
+
end
|
37
|
+
|
38
|
+
# restricted attributes for show
|
39
|
+
def restricted_show_attributes
|
40
|
+
[]
|
41
|
+
end
|
42
|
+
|
43
|
+
# restricted attributes for save
|
44
|
+
def restricted_save_attributes
|
45
|
+
[:id, :created_at, :updated_at]
|
46
|
+
end
|
47
|
+
|
48
|
+
# restricted attributes for create
|
49
|
+
def restricted_create_attributes
|
50
|
+
[:id, :created_at, :updated_at]
|
51
|
+
end
|
52
|
+
|
53
|
+
# restricted attributes for update
|
54
|
+
def restricted_update_attributes
|
55
|
+
[:id, :created_at, :updated_at]
|
56
|
+
end
|
57
|
+
|
58
|
+
# restricted associations for show
|
59
|
+
def restricted_show_associations
|
60
|
+
[]
|
61
|
+
end
|
62
|
+
|
63
|
+
# restricted associations for save
|
64
|
+
def restricted_save_associations
|
65
|
+
[]
|
66
|
+
end
|
67
|
+
|
68
|
+
# restricted associations for create
|
69
|
+
def restricted_create_associations
|
70
|
+
[]
|
71
|
+
end
|
72
|
+
|
73
|
+
# restricted associations for update
|
74
|
+
def restricted_update_associations
|
75
|
+
[]
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
@@ -0,0 +1,169 @@
|
|
1
|
+
module Role
|
2
|
+
# Base class that all roles inherit, stores role options in class instance variables
|
3
|
+
# and creates a hash of attributes and associations from the options defined in permitted_for methods
|
4
|
+
#
|
5
|
+
# @param authorize_with [Symbol, String] class instance attribute which stores the method that is used to
|
6
|
+
# authorize users
|
7
|
+
# @param disable_merge [TrueClass, FalseClass] unused as of yet
|
8
|
+
# @param policy [Object] instance variable used to store a reference to the policy which instantiated the class
|
9
|
+
# @param permission_options [Hash] unrefined hash of options to be refined by the permitted method
|
10
|
+
class Base
|
11
|
+
|
12
|
+
# Class instance variable accessors
|
13
|
+
class << self
|
14
|
+
attr_accessor :authorize_with, :disable_merge
|
15
|
+
end
|
16
|
+
|
17
|
+
@authorize_with = :default_authorization
|
18
|
+
@disable_merge = nil
|
19
|
+
|
20
|
+
attr_reader :policy
|
21
|
+
|
22
|
+
def initialize(policy, permission_options)
|
23
|
+
@policy = policy
|
24
|
+
@permission_options = permission_options
|
25
|
+
freeze
|
26
|
+
end
|
27
|
+
|
28
|
+
# Helper instance method to retrieve the class instance variable @authorize_with
|
29
|
+
def authorize_with
|
30
|
+
return self.class.authorize_with
|
31
|
+
end
|
32
|
+
|
33
|
+
# Send the method to the policy to check if user falls into this role
|
34
|
+
def test_condition
|
35
|
+
@policy.send(authorize_with)
|
36
|
+
end
|
37
|
+
|
38
|
+
# Returns a refined hash of attributes and associations this user has access to
|
39
|
+
def permitted
|
40
|
+
if not @permission_options
|
41
|
+
permitted = {attributes: {},
|
42
|
+
associations: {}}
|
43
|
+
|
44
|
+
elsif @permission_options.is_a? Symbol
|
45
|
+
permitted = {attributes: handle_default_options(@permission_options, 'attributes'),
|
46
|
+
associations: handle_default_options(@permission_options, 'associations')}
|
47
|
+
else
|
48
|
+
permitted = {attributes: init_options(@permission_options[:attributes], 'attributes'),
|
49
|
+
associations: init_options(@permission_options[:associations], 'associations')}
|
50
|
+
end
|
51
|
+
|
52
|
+
return permitted
|
53
|
+
end
|
54
|
+
|
55
|
+
private
|
56
|
+
|
57
|
+
# Build hash of options when options are explicitly declared as a Hash
|
58
|
+
#
|
59
|
+
# @param options [Hash] unrefined hash containing either attributes or associations
|
60
|
+
# @param type [String] the type of option to be built, can be 'attributes' or 'associations'
|
61
|
+
def init_options(options, type)
|
62
|
+
unless options.present?
|
63
|
+
return {}
|
64
|
+
end
|
65
|
+
|
66
|
+
if options.is_a? Symbol
|
67
|
+
return handle_default_options(options, type)
|
68
|
+
end
|
69
|
+
|
70
|
+
raise ArgumentError, "Permitted #{type}, if declared, must be declared as a Hash or Symbol, expected something along the lines of
|
71
|
+
{show: [:id, :name], create: [:name], update: :all} or :all, got #{options}" unless options.is_a? Hash
|
72
|
+
|
73
|
+
parsed_options = {}
|
74
|
+
options.each do |key, value|
|
75
|
+
raise ArgumentError, "Expected Symbol or Array, for #{key} attribute, got #{value} of kind #{value.class}" unless _permitted_value_types value
|
76
|
+
|
77
|
+
if value.is_a? Symbol and value == :all
|
78
|
+
parsed_options[key] = send("get_all_#{type}")
|
79
|
+
next
|
80
|
+
end
|
81
|
+
|
82
|
+
if value.is_a? Array
|
83
|
+
case value.first
|
84
|
+
when :all_minus
|
85
|
+
parsed_options[key] = send("get_all_#{type}") - (value - [value.first])
|
86
|
+
else
|
87
|
+
parsed_options[key] = value
|
88
|
+
end
|
89
|
+
next
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
return parsed_options
|
94
|
+
end
|
95
|
+
|
96
|
+
# Build hash of options when options are implicitly declared as a Symbol, ex: :show_all
|
97
|
+
#
|
98
|
+
# @param option [Symbol] unrefined hash containing either attributes or associations
|
99
|
+
# @param type [String] the type of option to be built, can be 'attributes' or 'associations'
|
100
|
+
def handle_default_options(option, type)
|
101
|
+
raise ArgumentError, "Permitted options for implicit permission declaration are #{_allowed_access_options},
|
102
|
+
got #{option} instead" unless _allowed_access_options.include? option
|
103
|
+
parsed_options = {}
|
104
|
+
case option
|
105
|
+
when :show_all
|
106
|
+
parsed_options[:show] = send("get_all_#{type}")
|
107
|
+
else
|
108
|
+
of_type = option.to_s.gsub('_all', '').to_sym
|
109
|
+
parsed_options[:show] = send("get_all_#{type}")
|
110
|
+
parsed_options[of_type] = send("get_all_#{type}")
|
111
|
+
end
|
112
|
+
|
113
|
+
return remove_restricted(parsed_options, type)
|
114
|
+
end
|
115
|
+
|
116
|
+
# Remove restricted attributes declared in the @policy restricted_#{key}_#{type} methods,
|
117
|
+
# ex: restricted_show_attributes
|
118
|
+
#
|
119
|
+
# @param obj [Hash] refined hash containing either attributes or associations
|
120
|
+
# @param type [String] the type of option to be built, can be 'attributes' or 'associations'
|
121
|
+
def remove_restricted(obj, type)
|
122
|
+
permitted_obj_values = {}
|
123
|
+
|
124
|
+
obj.each do |key, value|
|
125
|
+
restricted = @policy.send("restricted_#{key}_#{type}")
|
126
|
+
permitted_obj_values[key] = restricted.present? ? value - restricted : value
|
127
|
+
end
|
128
|
+
|
129
|
+
return permitted_obj_values
|
130
|
+
end
|
131
|
+
|
132
|
+
# Returns all attributes of a record or scope defined in the @policy
|
133
|
+
def get_all_attributes
|
134
|
+
begin
|
135
|
+
@policy.record.class.column_names.map(&:to_sym)
|
136
|
+
rescue NoMethodError
|
137
|
+
begin
|
138
|
+
@policy.scope.column_names.map(&:to_sym)
|
139
|
+
rescue NoMethodError
|
140
|
+
raise NoMethodError, "#{@policy} does not have a record or scope defined(or scope is not an ActiveRecord::Association), this is a problem."
|
141
|
+
end
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
# Returns all associations of a record or scope defined in the @policy
|
146
|
+
def get_all_associations
|
147
|
+
begin
|
148
|
+
@policy.record.class.reflect_on_all_associations.map(&:name)
|
149
|
+
rescue NoMethodError
|
150
|
+
begin
|
151
|
+
@policy.scope.reflect_on_all_associations.map(&:name)
|
152
|
+
rescue NoMethodError
|
153
|
+
raise NoMethodError, "#{@policy} does not have a record or scope defined(or scope is not an ActiveRecord::Association), this is a problem."
|
154
|
+
end
|
155
|
+
end
|
156
|
+
end
|
157
|
+
|
158
|
+
# allowed options for implicit declaration
|
159
|
+
def _allowed_access_options
|
160
|
+
[:show_all, :save_all, :create_all, :update_all]
|
161
|
+
end
|
162
|
+
|
163
|
+
# allowed options for explicit declaration
|
164
|
+
def _permitted_value_types(value)
|
165
|
+
value.is_a? Symbol or value.is_a? Array
|
166
|
+
end
|
167
|
+
|
168
|
+
end
|
169
|
+
end
|
@@ -0,0 +1,99 @@
|
|
1
|
+
require_relative 'role/base'
|
2
|
+
|
3
|
+
# Module which handles all class-level methods-and-instance variables. Add the ability for a class to define roles
|
4
|
+
# as dynamically generated classes and permitted options for those roles as class instance variables on the @policy.
|
5
|
+
#
|
6
|
+
# @param permission_hash [Hash] hash containing the unrefined attributes and association options
|
7
|
+
# @param scope_hash [Hash] unused as of yet
|
8
|
+
module Role
|
9
|
+
attr_accessor :permissions_hash, :scope_hash
|
10
|
+
@permissions_hash = {}
|
11
|
+
@scope_hash = {}
|
12
|
+
|
13
|
+
# Method to define a role with the opts used for those roles, checks if all is kosher and calls the the method
|
14
|
+
# to create the role
|
15
|
+
#
|
16
|
+
# @param role [Symbol, String] the role name
|
17
|
+
# @param opts [Hash] options for the role
|
18
|
+
def role(role, opts)
|
19
|
+
options = opts.slice(*_role_default_keys)
|
20
|
+
|
21
|
+
raise ArgumentError, 'You need to supply :authorize_with' unless options.slice(*_required_attributes).present?
|
22
|
+
|
23
|
+
unless role.is_a? Symbol or role.is_a? String
|
24
|
+
raise ArgumentError, "Expected Symbol or String for role, got #{role.class}"
|
25
|
+
end
|
26
|
+
|
27
|
+
create_role(role, self, options)
|
28
|
+
end
|
29
|
+
|
30
|
+
# Dynamically generates a class with the options and sets the constant on the @policy
|
31
|
+
#
|
32
|
+
# @param role [Symbol, String] the name of the role
|
33
|
+
# @param policy [Object] the reference to the policy to set the constant on, should be passed a 'self' reference
|
34
|
+
# @param opts [Hash] options for the role
|
35
|
+
def create_role(role, policy, opts)
|
36
|
+
begin
|
37
|
+
policy.const_set role.to_s.classify, Class.new(Role::Base) {
|
38
|
+
@authorize_with = "#{opts[:authorize_with]}?"
|
39
|
+
@disable_merge = opts[:disable_merge]
|
40
|
+
}
|
41
|
+
rescue NameError => e
|
42
|
+
raise ArgumentError, "Something went wrong, possible NameError with #{policy} or #{role} => #{e.message}"
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
# Saves the unrefined options into the @permission_hash class instance variable
|
47
|
+
#
|
48
|
+
# @param role [Symbol, String] the name of the role to which the opts are associated
|
49
|
+
# @param opts [Hash] the hash of options
|
50
|
+
def permitted_for(role, opts)
|
51
|
+
options = opts.slice(*_permitted_for_keys)
|
52
|
+
|
53
|
+
@permissions_hash = {} if @permissions_hash.nil?
|
54
|
+
@permissions_hash[role] = options
|
55
|
+
end
|
56
|
+
|
57
|
+
# Helper method to declare attributes directly
|
58
|
+
#
|
59
|
+
# @param role [Symbol, String] the name of the role to which the opts are associated
|
60
|
+
# @param attr [Hash] the hash of attributes
|
61
|
+
def permitted_attr_for(role, attr)
|
62
|
+
options = attr.slice(*_permitted_opt_for_keys)
|
63
|
+
|
64
|
+
@permissions_hash = {} if @permissions_hash.nil?
|
65
|
+
@permissions_hash[role] = {:attributes => options}
|
66
|
+
end
|
67
|
+
|
68
|
+
# Helper method to declare associations directly
|
69
|
+
#
|
70
|
+
# @param role [Symbol, String] the name of the role to which the opts are associated
|
71
|
+
# @param assoc [Hash] the hash of associations
|
72
|
+
def permitted_assoc_for(role, assoc)
|
73
|
+
options = assoc.slice(*_permitted_opt_for_keys)
|
74
|
+
|
75
|
+
@permissions_hash = {} if @permissions_hash.nil?
|
76
|
+
@permissions_hash[role] = {:associations => options}
|
77
|
+
end
|
78
|
+
|
79
|
+
# default options for role declaration
|
80
|
+
private def _role_default_keys
|
81
|
+
[:authorize_with, :disable_merge]
|
82
|
+
end
|
83
|
+
|
84
|
+
# required options for role declaration
|
85
|
+
private def _required_attributes
|
86
|
+
[:authorize_with]
|
87
|
+
end
|
88
|
+
|
89
|
+
# permitted options for permitted_for declaration
|
90
|
+
private def _permitted_for_keys
|
91
|
+
[:attributes, :associations]
|
92
|
+
end
|
93
|
+
|
94
|
+
# permitted options for permitted_assoc_for and permitted_attr_for declaration
|
95
|
+
private def _permitted_opt_for_keys
|
96
|
+
[:show, :create, :update, :save]
|
97
|
+
end
|
98
|
+
|
99
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
module PunditOverwrite
|
2
|
+
|
3
|
+
# Overwrite for Pundit's default authorization, to be able to use PunditRoles. Does not conflict with existing
|
4
|
+
# Pundit implementations
|
5
|
+
#
|
6
|
+
# @param record [Object] the object we're checking permissions of
|
7
|
+
# @param query [Symbol, String] the predicate method to check on the policy (e.g. `:show?`).
|
8
|
+
# If omitted then this defaults to the Rails controller action name.
|
9
|
+
# @raise [NotAuthorizedError] if the given query method returned false
|
10
|
+
# @return [Object] Always returns the passed object record
|
11
|
+
def authorize(record, query = nil)
|
12
|
+
query ||= params[:action].to_s + '?'
|
13
|
+
|
14
|
+
@_pundit_policy_authorized = true
|
15
|
+
|
16
|
+
policy = policy(record)
|
17
|
+
|
18
|
+
permitted_records = policy.resolve_query(query)
|
19
|
+
|
20
|
+
unless permitted_records
|
21
|
+
raise Pundit::NotAuthorizedError, query: query, record: record, policy: policy
|
22
|
+
end
|
23
|
+
|
24
|
+
if permitted_records.is_a? TrueClass
|
25
|
+
return record
|
26
|
+
end
|
27
|
+
|
28
|
+
return permitted_records
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
module Pundit
|
33
|
+
prepend PunditOverwrite
|
34
|
+
end
|
data/lib/pundit_roles.rb
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
require 'active_support/core_ext/hash/slice'
|
2
|
+
require 'active_support/core_ext/array/extract_options'
|
3
|
+
require 'active_support/core_ext/string/inflections'
|
4
|
+
require 'active_support/core_ext/object/blank'
|
5
|
+
|
6
|
+
require 'pundit_roles/version'
|
7
|
+
require 'pundit_roles/pundit'
|
8
|
+
require 'pundit_roles/policy/base'
|
9
|
+
require 'pundit'
|
10
|
+
|
11
|
+
module PunditRoles
|
12
|
+
include Pundit
|
13
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'pundit_roles/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "pundit_roles"
|
8
|
+
spec.version = PunditRoles::VERSION
|
9
|
+
spec.authors = ["Daniel Balogh"]
|
10
|
+
spec.email = ["danielferencbalogh@gmail.com"]
|
11
|
+
|
12
|
+
spec.summary = %q{Extends Pundit with roles, which allow attribute and association level authorizations}
|
13
|
+
spec.description = %q{Extends Pundit with roles}
|
14
|
+
spec.homepage = "https://github.com/StairwayB/pundit_roles"
|
15
|
+
spec.license = "MIT"
|
16
|
+
|
17
|
+
spec.files = `git ls-files -z`.split("\x0").reject do |f|
|
18
|
+
f.match(%r{^(test|spec|features)/})
|
19
|
+
end
|
20
|
+
spec.bindir = "exe"
|
21
|
+
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
22
|
+
spec.require_paths = ["lib"]
|
23
|
+
|
24
|
+
spec.required_ruby_version = '>= 2.3.1'
|
25
|
+
spec.add_dependency "activesupport", ">= 3.0.0"
|
26
|
+
end
|
metadata
ADDED
@@ -0,0 +1,74 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: pundit_roles
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.2
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Daniel Balogh
|
8
|
+
autorequire:
|
9
|
+
bindir: exe
|
10
|
+
cert_chain: []
|
11
|
+
date: 2017-10-24 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: activesupport
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: 3.0.0
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: 3.0.0
|
27
|
+
description: Extends Pundit with roles
|
28
|
+
email:
|
29
|
+
- danielferencbalogh@gmail.com
|
30
|
+
executables: []
|
31
|
+
extensions: []
|
32
|
+
extra_rdoc_files: []
|
33
|
+
files:
|
34
|
+
- ".gitignore"
|
35
|
+
- ".travis.yml"
|
36
|
+
- Gemfile
|
37
|
+
- LICENSE.txt
|
38
|
+
- README.md
|
39
|
+
- Rakefile
|
40
|
+
- bin/console
|
41
|
+
- bin/setup
|
42
|
+
- lib/pundit_roles.rb
|
43
|
+
- lib/pundit_roles/policy/base.rb
|
44
|
+
- lib/pundit_roles/policy/policy_defaults/defaults.rb
|
45
|
+
- lib/pundit_roles/policy/role.rb
|
46
|
+
- lib/pundit_roles/policy/role/base.rb
|
47
|
+
- lib/pundit_roles/pundit.rb
|
48
|
+
- lib/pundit_roles/version.rb
|
49
|
+
- pundit_roles.gemspec
|
50
|
+
homepage: https://github.com/StairwayB/pundit_roles
|
51
|
+
licenses:
|
52
|
+
- MIT
|
53
|
+
metadata: {}
|
54
|
+
post_install_message:
|
55
|
+
rdoc_options: []
|
56
|
+
require_paths:
|
57
|
+
- lib
|
58
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
59
|
+
requirements:
|
60
|
+
- - ">="
|
61
|
+
- !ruby/object:Gem::Version
|
62
|
+
version: 2.3.1
|
63
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
64
|
+
requirements:
|
65
|
+
- - ">="
|
66
|
+
- !ruby/object:Gem::Version
|
67
|
+
version: '0'
|
68
|
+
requirements: []
|
69
|
+
rubyforge_project:
|
70
|
+
rubygems_version: 2.6.11
|
71
|
+
signing_key:
|
72
|
+
specification_version: 4
|
73
|
+
summary: Extends Pundit with roles, which allow attribute and association level authorizations
|
74
|
+
test_files: []
|