access_allow 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/MIT-LICENSE +20 -0
- data/README.md +374 -0
- data/Rakefile +3 -0
- data/lib/access_allow/abilities.rb +25 -0
- data/lib/access_allow/abilities_manager.rb +81 -0
- data/lib/access_allow/access_manager.rb +237 -0
- data/lib/access_allow/check.rb +55 -0
- data/lib/access_allow/controller_access_dsl.rb +113 -0
- data/lib/access_allow/railtie.rb +4 -0
- data/lib/access_allow/roles.rb +28 -0
- data/lib/access_allow/version.rb +3 -0
- data/lib/access_allow.rb +36 -0
- data/lib/generators/access_allow/USAGE +3 -0
- data/lib/generators/access_allow/install_generator.rb +26 -0
- data/lib/generators/access_allow/templates/access_allow.rb +10 -0
- data/lib/generators/access_allow/templates/migration.rb.erb +11 -0
- data/lib/tasks/access_allow_tasks.rake +4 -0
- metadata +76 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: a3f5b6c7372b951cf3e958813f2a5535a0e1889e646524590e10e1600106ec8d
|
4
|
+
data.tar.gz: abd0e5aa1f28a82206f02232ca527f84ac340364ed078d515363879d7e8de5f6
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 385a3320289a1b69ddba30897996105547027520009c926cf65f7ba794a7d1c430052bab3c52bae8026c20db61b66bf3954a5d915a8c0c21c02742082907d707
|
7
|
+
data.tar.gz: 4b04e9507daaeac18001dc29be96f80d60039073122996eb3b2ec7a7a97a6c5eb5afa130cee417d95cb667686fed85cd02a9de2bb4d760f29f305ff686653b11
|
data/MIT-LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright 2022 Stephen Ierodiaconou
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,374 @@
|
|
1
|
+
# AccessAllow
|
2
|
+
|
3
|
+
Permissions and access control gem for Rails.
|
4
|
+
|
5
|
+
# Roles, Abilities and the permissions model
|
6
|
+
|
7
|
+
Users should be assigned a `role` where a role is a named grouping of specific permissions (or abilities as we
|
8
|
+
call them). Roles are configured in the application configuration.
|
9
|
+
|
10
|
+
Abilities are named permissions that live inside a namespace. These are context dependant. For example we might think
|
11
|
+
of the ability for being able to check out a shopping cart as `shopping_cart: :check_out` where `shopping_cart` is the
|
12
|
+
ability namespace for anything to do with the shopping cart and `check_out` is the specific ability name.
|
13
|
+
|
14
|
+
Thus abilities are acquired by user either through their assigned role, or an ability can be directly assign in the
|
15
|
+
database, via the User association `permissions`.
|
16
|
+
|
17
|
+
## Role and abilities utility methods
|
18
|
+
|
19
|
+
`AccessAllow::Roles` provides a bunch of utility methods that
|
20
|
+
|
21
|
+
* check if a given role name is for a specific user type or not
|
22
|
+
* returns humanized versions of the role names
|
23
|
+
|
24
|
+
`AccessAllow::Abilities` currently provides utility methods to convert between string and hash representations of abilities.
|
25
|
+
|
26
|
+
# Configuring Roles & their abilities
|
27
|
+
|
28
|
+
Schema configured in `Configuration` and should be configured to create roles with their abilities.
|
29
|
+
|
30
|
+
The structure consists of:
|
31
|
+
|
32
|
+
<user_type_key>:
|
33
|
+
<user_role_key>:
|
34
|
+
<ability_namespace_key>:
|
35
|
+
<ability_key>: [true/false]
|
36
|
+
|
37
|
+
where
|
38
|
+
|
39
|
+
* `user_type_key` is determined from the model name of the user class (eg `User` => `user`)
|
40
|
+
* `user_role_key` is the name of the user role (eg `account_owner`)
|
41
|
+
* `ability_namespace_key` is the name of the group of abilites (eg `product_management`)
|
42
|
+
* `ability_key` is the name of the actual ability (eg `edit_product`) and is set to a boolean
|
43
|
+
to indicate if the ability is available to the specific configuration or not
|
44
|
+
|
45
|
+
__Note__: ability names must be defined in the correct user type, role, namespace key space otherwise
|
46
|
+
the app will raise an exception. This is to prevent accidentally forgetting to define the default
|
47
|
+
permissions of a role around a specific feature.
|
48
|
+
|
49
|
+
## Setting a user specific ability
|
50
|
+
|
51
|
+
User specific abilities are persisted in `Permission`s where the attribute `ability_name` stores the
|
52
|
+
ability namespace and name in one combined string. The format is `/` separated. Eg `tag_management/edit_tag`. Use
|
53
|
+
`AccessAllow::Abilities` to convert between string and hash representations of abilities.
|
54
|
+
|
55
|
+
The existence of a `Permission` sets the specific ability in the above described structure of abilities.
|
56
|
+
|
57
|
+
The `AbilitiesManager` handles mixing these assigned abilities into the users specific total ability list.
|
58
|
+
|
59
|
+
__Note__ that an ability defined in a `Permission` __must__ also exist in the role assigned abilities. If
|
60
|
+
it does not then it is ignored. In other words a `Permission` can only override abilities defined for the
|
61
|
+
role that are set to `false`. This allows a user to be given a specific ability that normally their role has not got,
|
62
|
+
but does not allow you to assign arbitrary abilities to a user, thus preventing dangerous situations where an ability
|
63
|
+
that say is only for Admins is assigned to a User role.
|
64
|
+
|
65
|
+
# Manually checking user abilities
|
66
|
+
|
67
|
+
The class `AccessAllow::Check` implements ability check logic. Using this class one can check if a user has a specific ability
|
68
|
+
and optionally raise if not.
|
69
|
+
|
70
|
+
You can either build a new instance of the check class and then use `#possible?` and `#possible!` of use the class
|
71
|
+
helper methods
|
72
|
+
|
73
|
+
* `.call(user, <ability_namespace>: <ability_name>)`: checks if user has `ability_name` in `ability_namespace`. Returns
|
74
|
+
a boolean result
|
75
|
+
* `.call!(user, <ability_namespace>: <ability_name>)`: checks if user has `ability_name` in `ability_namespace`. Returns
|
76
|
+
true or raises `AccessAllow::ViolationError`
|
77
|
+
|
78
|
+
The methods exposed by `Check` are useful for checking for abilities in other objects. To define abilities checks
|
79
|
+
around controller actions see the next section.
|
80
|
+
|
81
|
+
# Controller DSL for specifying requirements and abilities needed to perform actions
|
82
|
+
|
83
|
+
Much of the time permissions checks will occur in Controllers. Also many controller actions have specific checks and
|
84
|
+
requirements around the user or other entities related to the controller action. For example, when editing a user's
|
85
|
+
profile, one must check that the user who is trying to execute the `update` action has the ability (permission) to
|
86
|
+
do it, but also that the user is even from the same company as the user being edited.
|
87
|
+
|
88
|
+
As such a DSL exists that can be used in controllers to define sets of required checks and rules around actions that then
|
89
|
+
define what abilities or checks are needed to allow a specific action to execute. The rules can also define what
|
90
|
+
should happen if the checks do not pass, or if no rules match the current situation.
|
91
|
+
|
92
|
+
The DSL allows us to define 3 categories of our so called access rules:
|
93
|
+
|
94
|
+
## Required check rules
|
95
|
+
|
96
|
+
Many times we want to specify that certain requirements are required to allow a user to perform a certain action. These
|
97
|
+
requirements maybe certain checks on the user, or they maybe related to their role or abilities.
|
98
|
+
|
99
|
+
These checks must all pass to allow the user to continue. They are checked before any other access checks are executed.
|
100
|
+
If the checks do not pass then a 'violation' is returned, which is then handled by the controller accordingly.
|
101
|
+
|
102
|
+
These rules consist of a 'check', an optional set of required abilities, and optionally what violation type to raised
|
103
|
+
if the check does not pass.
|
104
|
+
|
105
|
+
## Action allow rules
|
106
|
+
|
107
|
+
Action allow rules are defined to provide specific rules which allow a user to perform a specific controller action.
|
108
|
+
|
109
|
+
Note that AccessAllow prevents an action from being executed unless it is explicitly allowed for the given user trying to
|
110
|
+
execute it.
|
111
|
+
|
112
|
+
For a user to be allowed to perform a given controller action, there must be a matching "action allow" rule for that
|
113
|
+
action for which the check passes and permissions requirements are met. Any matching rule will allow the user to execute
|
114
|
+
the action. Note if no rules match successfully then the no-match behaviour is executed.
|
115
|
+
|
116
|
+
These rules consist of a check, an optional set of required abilities, a set of action names to which the rule applies
|
117
|
+
and optionally a name to alias the check as a "named check" rule (see below).
|
118
|
+
|
119
|
+
## Named check rule
|
120
|
+
|
121
|
+
These are named checks that can then be referenced by name in action logic or in views to say perform some conditional
|
122
|
+
logic. The method provided for checking if any named check rule is valid for the current context is `access_allowed?`.
|
123
|
+
There is more details below on this.
|
124
|
+
|
125
|
+
Say for example you want a user to be `approved` and have the ability `company_profile/edit` to edit the company
|
126
|
+
profile, and want to conditionally display a "Edit" button in the view. You could define a named check, say
|
127
|
+
`:approved_can_edit` (that checks `approved` and that the user has the ability) and use it in the view to conditionally
|
128
|
+
display the button:
|
129
|
+
|
130
|
+
<% if access_allowed? :approved_can_edit %>
|
131
|
+
The button...
|
132
|
+
<% end %>
|
133
|
+
|
134
|
+
Note that when you define an "action allow" rule it is automatically also added to these 'named checks' by the action
|
135
|
+
name, for example, if there is action allow rule for `:create` then we can use `access_allowed? :create`.
|
136
|
+
|
137
|
+
Also note that it is possible to specify a custom 'named check' name for the "action allow" rule (see more below).
|
138
|
+
|
139
|
+
## No-match behaviour
|
140
|
+
|
141
|
+
The DSL also allows us to define what should happen when executing an action and no rule matches the current situation.
|
142
|
+
|
143
|
+
What should happen is defined using one of a set of predefined 'violations' which are handled in specific ways. See
|
144
|
+
the discussion below on violations.
|
145
|
+
|
146
|
+
## Violations
|
147
|
+
|
148
|
+
The behaviour when a "required check" or an action has no matching "allow rule" is defined with so called "violation"
|
149
|
+
configuration. These violations are handled in a standardised way by the controller callback that performs the rule
|
150
|
+
checks.
|
151
|
+
|
152
|
+
The violation types are:
|
153
|
+
|
154
|
+
* `severe`:
|
155
|
+
this violation is considered something unusual and is logged. The end user will simply see a 404 page to avoid exposing
|
156
|
+
to them that there is in fact an actionable endpoint at the route they tried to access.
|
157
|
+
* `hidden`:
|
158
|
+
this violation type is considered less severe, but still aims to avoid leaking information to the end user
|
159
|
+
about the actual available routes on the app. If this violation is raised the user will see a 404 page and
|
160
|
+
the violation is logged to the app logs.
|
161
|
+
* `not_permitted`:
|
162
|
+
this violation is used when a user can know that an action and route exists but that they do not
|
163
|
+
have the assigned 'abilities' to perform the action. The end user will see a 403 (forbidden) page
|
164
|
+
and the violation is simply logged to the app logs.
|
165
|
+
* `redirect`:
|
166
|
+
this violation type is used when we want to perform a redirect if the user does not have the necessary
|
167
|
+
permissions. By default it will redirect to `root_path` but you can use a block to specify the destination path.
|
168
|
+
The block must return a string or other structure that is accepted by `redirect_to`.
|
169
|
+
|
170
|
+
## The DSL & defining checks
|
171
|
+
|
172
|
+
The methods are as follows:
|
173
|
+
|
174
|
+
### `access_require(check, with:, violation: :severe, &block)`
|
175
|
+
|
176
|
+
Used to define a "required check rule".
|
177
|
+
|
178
|
+
Takes a check name (a symbol or array of symbols) (see details below), an optional `violation` type (defaults to
|
179
|
+
`severe`) for when the check does not pass, and a block for when the violation type is `redirect` and you want to
|
180
|
+
specify custom logic to determine the redirection destination. Also can take an an optional set of abilities (a hash)
|
181
|
+
passed to `with:` to check against the user.
|
182
|
+
|
183
|
+
### `access_allow(check, with: nil, to:, as: nil)`
|
184
|
+
|
185
|
+
Used to define an "action allow rule" with optional named check alias.
|
186
|
+
|
187
|
+
Takes a check name (a symbol or array of symbols) (see details below), an optional set of abilities (a hash) passed to
|
188
|
+
`with:` to check against the user, and an optional name (symbol) passed with `as:` to allow the rule to be used as a
|
189
|
+
"named check". The controller actions the rule applies to is passed to `to:` (symbol or array of symbols).
|
190
|
+
|
191
|
+
### `access_allow(check, with: nil, as:)`
|
192
|
+
|
193
|
+
Used to define a "named check rule".
|
194
|
+
|
195
|
+
Similar to the "action allow rule" but without the actions. This rule is thus only available to be used as a "named
|
196
|
+
check".
|
197
|
+
|
198
|
+
### `access_no_match(violation, &block)`
|
199
|
+
|
200
|
+
Used to define the "no match" behaviour, ie what happens when an action is trying to be executed by no access rule
|
201
|
+
matches or passes for the given user and action.
|
202
|
+
|
203
|
+
Takes a `violation` type and optionally a block for when the violation type is `redirect` and you want to specify
|
204
|
+
custom logic to determine the redirection destination.
|
205
|
+
|
206
|
+
### Defining abilities needed
|
207
|
+
|
208
|
+
Permissions requirements are specified for the rule with `with:`.
|
209
|
+
|
210
|
+
The permissions are defined as a hash containing keys representing the ability namespaces and associated values
|
211
|
+
representing the required abilities.
|
212
|
+
|
213
|
+
For example, `{tag_management: [:add_new, :edit_existing], product_management: :edit_variants}` would mean that the
|
214
|
+
user must have all 3 of the abilities, `tag_management: :add_new`, `tag_management: :edit_existing` and
|
215
|
+
`product_management: :edit_variants`.
|
216
|
+
|
217
|
+
### Defining Checks & predefined checks
|
218
|
+
|
219
|
+
Access rules must specify one or more 'checks' as part of their rule definition.
|
220
|
+
|
221
|
+
'Checks' are basically controller methods which return a boolean to determine if the check 'passed' or 'failed'. Checks
|
222
|
+
are normally custom code written for the given context of the feature. Note that checks do not need to perform the
|
223
|
+
abilities checks specified by `with:`, these are performed by the gem logic for you.
|
224
|
+
|
225
|
+
Checks are specified by providing an instance method on the controller named `allow_(name)?`, where `name` is the check
|
226
|
+
name, and which returns a boolean.
|
227
|
+
|
228
|
+
For example, if defining a check for an action allow rule where the user must be approved on the platform, and
|
229
|
+
have a specific ability assigned to them, then the 'check' part (named say `approved_user`) is "user must be approved
|
230
|
+
on the platform" part of the rule, and would be defined on the controller as an instance method `allow_approved_user?`.
|
231
|
+
|
232
|
+
There are some predefined 'common' checks, where you do not need to define the `allow_(name)?` method. These are:
|
233
|
+
|
234
|
+
* `:public`: anyone, logged in or not
|
235
|
+
* `:authenticated_user`: any logged in user (uses `current_user` or whatever is set as the `current_user_method` in the config)
|
236
|
+
|
237
|
+
# View helper to check permissions of user for conditional view sections
|
238
|
+
|
239
|
+
It is also possible to check `access` rules from inside views using the `access_allowed?` view helper, which takes
|
240
|
+
a list of "named check" names. If any of those check names passes the method returns `true`.
|
241
|
+
|
242
|
+
Note that check names also include the actions for which rules exists, as described earlier.
|
243
|
+
|
244
|
+
```erb
|
245
|
+
# in controller
|
246
|
+
allow_access :admin, to: :new
|
247
|
+
|
248
|
+
# in view
|
249
|
+
<% if access_allowed? :new %>
|
250
|
+
Only 'admin' users who are allowed to execute action `:new` can see this
|
251
|
+
<% end %>
|
252
|
+
```
|
253
|
+
|
254
|
+
and
|
255
|
+
|
256
|
+
```erb
|
257
|
+
# in controller
|
258
|
+
allow_access :my_check, as: named_rule
|
259
|
+
|
260
|
+
# in view
|
261
|
+
<% if access_allowed? :named_rule %>
|
262
|
+
Only users for whom the `named_rule` check passes can see this
|
263
|
+
<% end %>
|
264
|
+
```
|
265
|
+
|
266
|
+
# Example
|
267
|
+
|
268
|
+
Consider the following view fragment, and then the controller heirarchy defined below:
|
269
|
+
|
270
|
+
`tags_controller.rb`
|
271
|
+
|
272
|
+
```ruby
|
273
|
+
class TagsController < AdminController
|
274
|
+
# Allow any admin to access the :index and :show actions
|
275
|
+
access_allow :admin, to: [:index, :show]
|
276
|
+
# Only let admins with the ability `tag_management: :manage` to execute other actions.
|
277
|
+
# Also in our view we can use the check name `tag_management` to conditionally add say an "Add new Tag" button
|
278
|
+
access_allow :admin, with: {tag_management: :manage}, to: :all, as: :tag_management
|
279
|
+
# On the index page we also conditionally show some statistics about Tag usage, but only to admins with the right
|
280
|
+
# ability. This is done with the named check `:view_usage_stats`
|
281
|
+
access_allow :admin, with: {tag_management: :usage_stats}, as: :view_usage_stats
|
282
|
+
# Admins with a special flag called "im_magic" can also access the :magic action
|
283
|
+
access_allow :magic_admin, to: :magic
|
284
|
+
|
285
|
+
def allow_admin?
|
286
|
+
current_user.admin?
|
287
|
+
end
|
288
|
+
|
289
|
+
def allow_magic_admin?
|
290
|
+
current_user.im_magic? && allow_admin?
|
291
|
+
end
|
292
|
+
|
293
|
+
# ...
|
294
|
+
end
|
295
|
+
|
296
|
+
class AdminController < AuthenticatedController
|
297
|
+
# Only admins can access actions on this controller or its sub controllers. Any authenticated user who is not an
|
298
|
+
# admin user will generate a severe access violation. They will see a 404 but the violation will be logged.
|
299
|
+
access_require :admin, violation: :severe
|
300
|
+
# Once we have verified the user is an admin we can 403 them instead of 404 when they try to access a page they
|
301
|
+
# dont have permission for. We don't need to hide the existence of the action from them.
|
302
|
+
access_no_match :not_permitted
|
303
|
+
# ...
|
304
|
+
end
|
305
|
+
|
306
|
+
class AuthenticatedController < ApplicationController
|
307
|
+
# Any action requires an authenticated user. The defined behaviour is that if the user trying to access the action
|
308
|
+
# is not authenticated they are redirected to the sign-in page.
|
309
|
+
access_require :authenticated_user, violation: :redirect do
|
310
|
+
sign_in_path
|
311
|
+
end
|
312
|
+
|
313
|
+
# ...
|
314
|
+
end
|
315
|
+
|
316
|
+
class ApplicationController < ActionController::Base
|
317
|
+
# By default, if no access rules match when executing an action then show the user a 404 to prevent leaking the
|
318
|
+
# existence of the end point
|
319
|
+
access_no_match :hidden
|
320
|
+
# ...
|
321
|
+
end
|
322
|
+
|
323
|
+
```
|
324
|
+
|
325
|
+
`tags/index.html.erb`
|
326
|
+
|
327
|
+
```erb
|
328
|
+
<p>Tags Index</p>
|
329
|
+
<% if access_allowed? :tag_management %>
|
330
|
+
<button>Add new tag</button>
|
331
|
+
<% end %>
|
332
|
+
<% if access_allowed? :view_usage_stats %>
|
333
|
+
<div> ... </div>
|
334
|
+
<% end %>
|
335
|
+
<ul> ... </ul>
|
336
|
+
```
|
337
|
+
|
338
|
+
## Usage
|
339
|
+
|
340
|
+
Add this to your `ApplicationController`
|
341
|
+
|
342
|
+
```ruby
|
343
|
+
class ApplicationController < ActionController::Base
|
344
|
+
include AccessAllow::ControllerAccessDsl
|
345
|
+
end
|
346
|
+
```
|
347
|
+
|
348
|
+
## Installation
|
349
|
+
Add this line to your application's Gemfile:
|
350
|
+
|
351
|
+
```ruby
|
352
|
+
gem "access_allow"
|
353
|
+
```
|
354
|
+
|
355
|
+
And then execute:
|
356
|
+
```bash
|
357
|
+
$ bundle
|
358
|
+
```
|
359
|
+
|
360
|
+
Or install it yourself as:
|
361
|
+
```bash
|
362
|
+
$ gem install access_allow
|
363
|
+
```
|
364
|
+
|
365
|
+
Then run the **generator to add the initializer**
|
366
|
+
|
367
|
+
rails g access_allow:install
|
368
|
+
|
369
|
+
|
370
|
+
## Contributing
|
371
|
+
Contribution directions go here.
|
372
|
+
|
373
|
+
## License
|
374
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
data/Rakefile
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module AccessAllow
|
4
|
+
class Abilities
|
5
|
+
class << self
|
6
|
+
def qualified_name(ability_namespace, ability_name)
|
7
|
+
raise StandardError, "You can't have blank ability names" if ability_namespace.blank? || ability_name.blank?
|
8
|
+
"#{ability_namespace}/#{ability_name}"
|
9
|
+
end
|
10
|
+
|
11
|
+
def parse_qualified_name(name)
|
12
|
+
parts = name.split("/").map do |part|
|
13
|
+
raise StandardError "Ability namespaces or names cannot be blank" if part.blank?
|
14
|
+
part.to_sym
|
15
|
+
end
|
16
|
+
return parts if parts.size == 2
|
17
|
+
raise StandardError "Ability name must have a namespace and name (was #{name})"
|
18
|
+
end
|
19
|
+
|
20
|
+
def humanized_name(type, ability_namespace, ability_name)
|
21
|
+
I18n.t("abilities.#{type}.abilities.#{ability_namespace}.#{ability_name}")
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,81 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module AccessAllow
|
4
|
+
class AbilitiesManager
|
5
|
+
def initialize(user)
|
6
|
+
@user = user
|
7
|
+
end
|
8
|
+
|
9
|
+
def has?(ability_namespace, ability_name)
|
10
|
+
namespace = namespaced_context(ability_namespace)
|
11
|
+
namespace[ability_name]
|
12
|
+
end
|
13
|
+
|
14
|
+
def to_a
|
15
|
+
@to_a ||=
|
16
|
+
combined_role_and_user_assigned.flat_map do |namespace, abilities|
|
17
|
+
abilities
|
18
|
+
.to_a
|
19
|
+
.each_with_object([]) do |config, arr|
|
20
|
+
ability, permitted = config
|
21
|
+
arr << [namespace, ability.to_sym] if permitted
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
private
|
27
|
+
|
28
|
+
attr_reader :user
|
29
|
+
|
30
|
+
def namespaced_context(ability_namespace)
|
31
|
+
context = combined_role_and_user_assigned[ability_namespace]
|
32
|
+
raise StandardError, "Permission namespace unknown: #{ability_namespace}" unless context
|
33
|
+
context
|
34
|
+
end
|
35
|
+
|
36
|
+
def combined_role_and_user_assigned
|
37
|
+
@combined_role_and_user_assigned ||=
|
38
|
+
begin
|
39
|
+
base_perms = role_assigned.deep_dup
|
40
|
+
user.send(AccessAllow.configuration.permissions_association_name).each do |perm|
|
41
|
+
namespace, name = AccessAllow::Abilities.parse_qualified_name(perm.ability_name)
|
42
|
+
|
43
|
+
# We only assign if the permission already exists in the base role based configs
|
44
|
+
next if base_perms.dig(namespace, name).nil?
|
45
|
+
base_perms[namespace][name] = true
|
46
|
+
end
|
47
|
+
base_perms
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def role_assigned
|
52
|
+
unless role_based_abilities[user_type_key]
|
53
|
+
raise(StandardError, "User type (#{user_type_key}) has no permissions defined")
|
54
|
+
end
|
55
|
+
unless role_based_abilities[user_type_key][user_role_key]
|
56
|
+
raise(
|
57
|
+
StandardError,
|
58
|
+
"Role (#{user_role_key}) for user type (#{user_type_key}) has no permissions defined"
|
59
|
+
)
|
60
|
+
end
|
61
|
+
role_based_abilities[user_type_key][user_role_key]
|
62
|
+
end
|
63
|
+
|
64
|
+
def role_based_abilities
|
65
|
+
AccessAllow.configuration.roles_and_permissions
|
66
|
+
end
|
67
|
+
|
68
|
+
def user_type_key
|
69
|
+
user.class.name.underscore.to_sym
|
70
|
+
end
|
71
|
+
|
72
|
+
def user_role_key
|
73
|
+
role = user.send(AccessAllow.configuration.role_method_name)
|
74
|
+
(role.presence || "primary").to_sym
|
75
|
+
end
|
76
|
+
|
77
|
+
def about_user
|
78
|
+
"#{user.class} with ID #{user.id}"
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
@@ -0,0 +1,237 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module AccessAllow
|
4
|
+
class AccessManager
|
5
|
+
VIOLATION_TYPES = %i[severe hidden redirect not_permitted].freeze
|
6
|
+
|
7
|
+
def initialize
|
8
|
+
@required_rules = []
|
9
|
+
@action_rules = []
|
10
|
+
@named_rules_map = {}
|
11
|
+
@all_actions_rules = []
|
12
|
+
@no_match_rule = {violation: :severe}
|
13
|
+
end
|
14
|
+
|
15
|
+
# When initialising an access manager for a controller make sure to clone the current parent controllers rules
|
16
|
+
def initialize_clone(parent_manager)
|
17
|
+
@required_rules = parent_manager.required_rules.deep_dup
|
18
|
+
@action_rules = parent_manager.action_rules.deep_dup
|
19
|
+
@named_rules_map = parent_manager.named_rules_map.deep_dup
|
20
|
+
@all_actions_rules = parent_manager.all_actions_rules.deep_dup
|
21
|
+
@no_match_rule = parent_manager.no_match_rule.deep_dup
|
22
|
+
end
|
23
|
+
|
24
|
+
def add_allow_rule(rule, to, with = nil, as = nil)
|
25
|
+
insert_access_rule(parse_rule(rule, to: to, with: with, as: as))
|
26
|
+
end
|
27
|
+
|
28
|
+
def add_required_rule(rule, violation, with = nil, handler = nil)
|
29
|
+
unless VIOLATION_TYPES.include?(violation)
|
30
|
+
raise StandardError, "You must provide a valid violation type"
|
31
|
+
end
|
32
|
+
insert_required_rule(rule, with, violation, handler)
|
33
|
+
end
|
34
|
+
|
35
|
+
def configure_no_match(violation_type, &block)
|
36
|
+
rule = {violation: violation_type}
|
37
|
+
rule[:handler] = block if block
|
38
|
+
@no_match_rule = rule
|
39
|
+
end
|
40
|
+
|
41
|
+
# Rule is applied like this:
|
42
|
+
#
|
43
|
+
# First apply any required rules, ie all must pass to allow user to progress
|
44
|
+
#
|
45
|
+
# Then apply allow rules, where any pass will allow user to progress:
|
46
|
+
# * if there is a constraint on an action name, check that
|
47
|
+
# * if it has user type requirement, then apply that next
|
48
|
+
# * if it has a perms requirement, apply that after
|
49
|
+
# * if it has custom rules, apply those
|
50
|
+
|
51
|
+
# Compare against all configured rules which have actions
|
52
|
+
def allow_action?(user, controller, current_action)
|
53
|
+
# Required checks must all pass
|
54
|
+
required_rules.each do |config|
|
55
|
+
permitted_or_violation = execute_required_rule(config, user, controller, current_action)
|
56
|
+
return permitted_or_violation unless permitted_or_violation == true
|
57
|
+
end
|
58
|
+
|
59
|
+
# Check action rules
|
60
|
+
allowed =
|
61
|
+
action_rules.any? { |config| execute_rule(config, user, controller, current_action) }
|
62
|
+
return true if allowed
|
63
|
+
|
64
|
+
# return no-match
|
65
|
+
no_match_rule
|
66
|
+
end
|
67
|
+
|
68
|
+
# Evaluate specific rules. Ie we dont check action match, we just find if there are any checks that pass
|
69
|
+
# for the current context. Used in view helper. The rules are defined with access_allow and a name or alias.
|
70
|
+
def allow?(rules, user, current_controller)
|
71
|
+
Array
|
72
|
+
.wrap(rules)
|
73
|
+
.any? do |rule|
|
74
|
+
rule_configs = named_rules_map[rule]
|
75
|
+
rule_configs&.any? { |config| execute_rule(config, user, current_controller) }
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
# Introspection methods
|
80
|
+
|
81
|
+
def no_match_violation
|
82
|
+
no_match_rule[:violation]
|
83
|
+
end
|
84
|
+
|
85
|
+
def named_rule_exists?(name)
|
86
|
+
!named_rules_map[name].nil?
|
87
|
+
end
|
88
|
+
|
89
|
+
def required_check_exists?(name)
|
90
|
+
required_rules.any? do |r|
|
91
|
+
r[:rules_set][:all]&.include?(name) || r[:rules_set][:any]&.include?(name)
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
protected
|
96
|
+
|
97
|
+
attr_reader :required_rules, :action_rules, :named_rules_map, :all_actions_rules, :no_match_rule
|
98
|
+
|
99
|
+
private
|
100
|
+
|
101
|
+
def parse_rule(rule, with: nil, as: nil, to: nil)
|
102
|
+
# If rule is a hash then its rules + perms - one key is single rule, multiple considered AND
|
103
|
+
# If an array then AND condition on the rules
|
104
|
+
actions = Array.wrap(to)
|
105
|
+
given_names = Array.wrap(as)
|
106
|
+
if actions.empty? && given_names.empty?
|
107
|
+
raise StandardError,
|
108
|
+
"You must specify the actions which the rule applies to or if a check must have a name"
|
109
|
+
end
|
110
|
+
{
|
111
|
+
aliases: given_names.presence || actions,
|
112
|
+
rules_set: prepare_rule_set(rule),
|
113
|
+
perms: parse_permissions(with),
|
114
|
+
actions: actions
|
115
|
+
}
|
116
|
+
end
|
117
|
+
|
118
|
+
# Parse the (optional) permissions configuration for the rule, defined with the `with:` option
|
119
|
+
def parse_permissions(perm_config)
|
120
|
+
return if perm_config.blank?
|
121
|
+
|
122
|
+
# A permission comprises of a namespace key, and with a single or array of permission names.
|
123
|
+
# If multiple are specified they all must apply
|
124
|
+
perm_config.to_a.flat_map do |c|
|
125
|
+
namespace, perm_name = c
|
126
|
+
if perm_name.is_a?(Array)
|
127
|
+
perm_name.map { |n| {namespace => n} }
|
128
|
+
else
|
129
|
+
{namespace => perm_name}
|
130
|
+
end
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
def insert_required_rule(rule, with, violation, handler)
|
135
|
+
config = {
|
136
|
+
rules_set: prepare_rule_set(rule),
|
137
|
+
perms: parse_permissions(with),
|
138
|
+
violation: violation
|
139
|
+
}
|
140
|
+
config[:handler] = handler if handler
|
141
|
+
required_rules << config
|
142
|
+
end
|
143
|
+
|
144
|
+
def prepare_rule_set(rules)
|
145
|
+
return rules if rules.is_a?(Hash) && (rules[:any] || rules[:all])
|
146
|
+
{all: Array.wrap(rules)}
|
147
|
+
end
|
148
|
+
|
149
|
+
def insert_access_rule(parsed_rule)
|
150
|
+
# Or add alias and/or action rule
|
151
|
+
aliased_as = parsed_rule[:aliases]
|
152
|
+
aliased_as&.each do |alias_name|
|
153
|
+
named_rules_map[alias_name] = [] if named_rules_map[alias_name].nil?
|
154
|
+
named_rules_map[alias_name] << parsed_rule
|
155
|
+
end
|
156
|
+
return if parsed_rule[:actions].blank?
|
157
|
+
all_actions_rules << parsed_rule if parsed_rule[:actions].include?(:all)
|
158
|
+
action_rules << parsed_rule
|
159
|
+
end
|
160
|
+
|
161
|
+
# To execute a rule:
|
162
|
+
# - check that the action is valid if specified
|
163
|
+
# - check that the user has required abilities if specified
|
164
|
+
# - apply the rules (all rules for the given allow config) to check if it passes for the current context
|
165
|
+
def execute_rule(config, user, controller, action_name = nil)
|
166
|
+
return if action_name && !allowed_action?(config[:actions], action_name)
|
167
|
+
execute_rules_set(config[:rules_set], config[:perms], user, controller, action_name)
|
168
|
+
end
|
169
|
+
|
170
|
+
# Required rules either pass or return their configured violation state
|
171
|
+
def execute_required_rule(config, user, controller, action_name)
|
172
|
+
if execute_rules_set(config[:rules_set], config[:perms], user, controller, action_name)
|
173
|
+
return true
|
174
|
+
end
|
175
|
+
config.slice(:violation, :handler)
|
176
|
+
end
|
177
|
+
|
178
|
+
def execute_rules_set(rules_set, perms, user, controller, action_name)
|
179
|
+
return unless user_has_perms?(user, perms)
|
180
|
+
if rules_set[:all]
|
181
|
+
rules_set[:all].all? do |rule|
|
182
|
+
Array.wrap(rule).all? { |r| apply_rule(r, user, controller, action_name) }
|
183
|
+
end
|
184
|
+
elsif rules_set[:any]
|
185
|
+
rules_set[:any].any? do |rule|
|
186
|
+
Array.wrap(rule).all? { |r| apply_rule(r, user, controller, action_name) }
|
187
|
+
end
|
188
|
+
else
|
189
|
+
raise NotImplementedError, "Unknown rule set"
|
190
|
+
end
|
191
|
+
end
|
192
|
+
|
193
|
+
# Check specified action is even got a rule to apply to it
|
194
|
+
def allowed_action?(actions, action_name)
|
195
|
+
actions.include?(:all) || actions.include?(action_name)
|
196
|
+
end
|
197
|
+
|
198
|
+
# Check if the user has permissions defined in the rule
|
199
|
+
def user_has_perms?(user, perms)
|
200
|
+
return true if perms.blank?
|
201
|
+
perms.all? { |perm| AccessAllow::Check.call(user, perm) }
|
202
|
+
end
|
203
|
+
|
204
|
+
# Some rules are predefined, otherwise apply the rule by calling the methods on the controller
|
205
|
+
# which the rule defines, which are named after the rule with a `allow_` prefix
|
206
|
+
def apply_rule(rule, user, controller, action_name)
|
207
|
+
case rule
|
208
|
+
when :public
|
209
|
+
true
|
210
|
+
when :authenticated_user
|
211
|
+
user.present?
|
212
|
+
else
|
213
|
+
apply_custom_rule(rule, user, controller, action_name)
|
214
|
+
end
|
215
|
+
end
|
216
|
+
|
217
|
+
# Apply the rule by calling the appropriate `allow_#{name}` instance method on the controller
|
218
|
+
# The method can be optionally called with the current user being tested against the rule, and optionally
|
219
|
+
# the rule configuration itself.
|
220
|
+
def apply_custom_rule(rule, user, controller, action_name)
|
221
|
+
controller.instance_exec(user, rule: rule, action_name: action_name) do |uut, rule_info|
|
222
|
+
check_name = "allow_#{rule}?".to_sym
|
223
|
+
unless respond_to?(check_name)
|
224
|
+
raise NotImplementedError, "Check #{check_name} not implemented!"
|
225
|
+
end
|
226
|
+
case method(check_name).arity
|
227
|
+
when 1
|
228
|
+
send(check_name, uut)
|
229
|
+
when 2
|
230
|
+
send(check_name, uut, rule_info)
|
231
|
+
else
|
232
|
+
send(check_name)
|
233
|
+
end
|
234
|
+
end
|
235
|
+
end
|
236
|
+
end
|
237
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module AccessAllow
|
4
|
+
class Check
|
5
|
+
class << self
|
6
|
+
def call(user, config)
|
7
|
+
build_perms_checker(user, config).possible?
|
8
|
+
end
|
9
|
+
|
10
|
+
def call!(user, config)
|
11
|
+
build_perms_checker(user, config).possible!
|
12
|
+
end
|
13
|
+
|
14
|
+
private
|
15
|
+
|
16
|
+
def build_perms_checker(user, config)
|
17
|
+
perm_namespace, perm_name = config.to_a.first
|
18
|
+
new(user, perm_namespace, perm_name)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def initialize(user, ability_namespace, ability_name)
|
23
|
+
@user = user
|
24
|
+
@ability_manager = user ? AccessAllow::AbilitiesManager.new(user) : nil
|
25
|
+
@ability_namespace = ability_namespace.to_sym
|
26
|
+
@ability_name = ability_name.to_sym
|
27
|
+
end
|
28
|
+
|
29
|
+
def possible?
|
30
|
+
unless user
|
31
|
+
Rails.logger.info error_message(false)
|
32
|
+
return false
|
33
|
+
end
|
34
|
+
ability_manager.has?(ability_namespace, ability_name).tap { |can| Rails.logger.info error_message(can) }
|
35
|
+
end
|
36
|
+
|
37
|
+
def possible!
|
38
|
+
possible? || raise(AccessAllow::ViolationError, error_message(false))
|
39
|
+
end
|
40
|
+
|
41
|
+
private
|
42
|
+
|
43
|
+
attr_reader :user, :ability_namespace, :ability_name, :ability_manager
|
44
|
+
|
45
|
+
# Error messages
|
46
|
+
|
47
|
+
def about_user
|
48
|
+
user ? "#{user.class} with ID #{user.id}" : "Unauthenticated user"
|
49
|
+
end
|
50
|
+
|
51
|
+
def error_message(can)
|
52
|
+
"#{about_user} #{can ? "can" : "cannot"} do '#{ability_name}'"
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
@@ -0,0 +1,113 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module AccessAllow
|
4
|
+
# Setup rules and configuration to specify access for a controller. Either specific actions or all actions.
|
5
|
+
module ControllerAccessDsl
|
6
|
+
extend ActiveSupport::Concern
|
7
|
+
|
8
|
+
included do
|
9
|
+
helper_method :access_allowed?
|
10
|
+
|
11
|
+
# Add a before action to check `allow` permissions rules. Note this is a 'prepend' as we
|
12
|
+
# want to try to ensure this happens before anything else.
|
13
|
+
prepend_before_action do |controller|
|
14
|
+
ensure_authenticated_before_perms_check if respond_to?(:ensure_authenticated_before_perms_check)
|
15
|
+
access_result = self.class.access_manager.allow_action?(
|
16
|
+
send(AccessAllow.configuration.current_user_method),
|
17
|
+
controller,
|
18
|
+
controller.action_name.to_sym
|
19
|
+
)
|
20
|
+
next true if access_result == true
|
21
|
+
Rails.logger.info("Blocked access for #{access_log_user_info} to access '#{access_log_action_tried}'")
|
22
|
+
handle_access_violation(access_result)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
# The DSL of the access rule configuration is defined below
|
27
|
+
class_methods do
|
28
|
+
# The access controls are inherited to controller subclasses
|
29
|
+
def inherited(subclass)
|
30
|
+
subclass.instance_variable_set(:@access_manager, @access_manager.clone)
|
31
|
+
super
|
32
|
+
end
|
33
|
+
|
34
|
+
# Configure what should happen when no access rule matches the action being executed
|
35
|
+
def access_no_match(violation, &block)
|
36
|
+
access_manager.configure_no_match(violation, &block)
|
37
|
+
end
|
38
|
+
|
39
|
+
# TODO: consider a `if:` conditional allowed on rules
|
40
|
+
# Specify an access requirement, that must pass for any other action access checks to pass
|
41
|
+
# The default violation level is :severe
|
42
|
+
def access_require(check, with: nil, violation: :severe, &block)
|
43
|
+
access_manager.add_required_rule(check, violation, with, block)
|
44
|
+
end
|
45
|
+
|
46
|
+
# Specify an access rule requirement for a specific action or set of actions. You can optionally
|
47
|
+
# also specify what abilities are required to match the rule. Using `as:` and no `to:` actions you can also
|
48
|
+
# specify an access rule which is not used when actions are executed but instead can be checked by the given name,
|
49
|
+
# thus allowing one to define a check that is used inside the processing of an action rather than before it.
|
50
|
+
# You can also specify an action rule with `to:` and then alias it to a named check with `as:`
|
51
|
+
def access_allow(check, with: nil, to: nil, as: nil)
|
52
|
+
access_manager.add_allow_rule(check, to, with, as)
|
53
|
+
end
|
54
|
+
|
55
|
+
# Create a new manager instance for this particular controller
|
56
|
+
def access_manager
|
57
|
+
@access_manager ||= AccessAllow::AccessManager.new
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
# `access_allowed?` is exposed as a view helper to execute checks or allow rules and return if they
|
62
|
+
# passed or not. Useful for doing conditional work in the view or a controller action
|
63
|
+
def access_allowed?(*check_rules)
|
64
|
+
self.class.access_manager.allow?(
|
65
|
+
check_rules,
|
66
|
+
send(AccessAllow.configuration.current_user_method),
|
67
|
+
self
|
68
|
+
)
|
69
|
+
end
|
70
|
+
|
71
|
+
protected
|
72
|
+
|
73
|
+
# When required access rules are violated, or when no match on an action occurs, the result from the access
|
74
|
+
# manager is processed here.
|
75
|
+
def handle_access_violation(access_result)
|
76
|
+
case access_result[:violation]
|
77
|
+
when :redirect
|
78
|
+
redirect_destination = access_result[:handler] ? instance_exec(&access_result[:handler]) : root_path
|
79
|
+
raise ::AccessAllow::ResponseForbiddenError, "Not permitted" unless redirect_destination
|
80
|
+
Rails.logger.info("#{access_log_user_info} tried to access a page that they can't access and they were " \
|
81
|
+
"redirected to '#{redirect_destination}'. (#{access_log_action_tried})")
|
82
|
+
redirect_to redirect_destination
|
83
|
+
when :not_permitted
|
84
|
+
Rails.logger.info("#{access_log_user_info} tried to access a page that they can't access and they were " \
|
85
|
+
"told about it. (#{access_log_action_tried})")
|
86
|
+
raise ::AccessAllow::ResponseForbiddenError, "Not permitted"
|
87
|
+
when :hidden
|
88
|
+
Rails.logger.info("#{access_log_user_info} tried to access a page that they can't access and they won't " \
|
89
|
+
"know exists (#{access_log_action_tried})")
|
90
|
+
raise ActionController::RoutingError, "Not Found"
|
91
|
+
else
|
92
|
+
log_severe_access_violation_and_not_found
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
def log_severe_access_violation_and_not_found
|
97
|
+
Rails.logger.error(
|
98
|
+
"#{access_log_user_info} tried to access a page that they can't access and" \
|
99
|
+
" it is considered suspicious they should attempt to see it. The action was: #{access_log_action_tried}"
|
100
|
+
)
|
101
|
+
raise ActionController::RoutingError, "Not Found"
|
102
|
+
end
|
103
|
+
|
104
|
+
def access_log_user_info
|
105
|
+
user = send(AccessAllow.configuration.current_user_method)
|
106
|
+
user ? "User #{user.id}" : "An unauthenticated user"
|
107
|
+
end
|
108
|
+
|
109
|
+
def access_log_action_tried
|
110
|
+
"#{controller_name}##{action_name}"
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module AccessAllow
|
4
|
+
class Roles
|
5
|
+
class << self
|
6
|
+
# Get a human readable version of the role key
|
7
|
+
def humanized_name(type, role)
|
8
|
+
I18n.t("abilities.#{type}.roles.#{role}")
|
9
|
+
end
|
10
|
+
|
11
|
+
# Check roles are valid for specific user types
|
12
|
+
|
13
|
+
def for?(type, role)
|
14
|
+
roles_for(type).include?(role.to_sym)
|
15
|
+
end
|
16
|
+
|
17
|
+
def roles_for(type)
|
18
|
+
configuration[type.to_sym]&.keys || []
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
|
23
|
+
def configuration
|
24
|
+
AccessAllow.configuration.roles_and_permissions
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
data/lib/access_allow.rb
ADDED
@@ -0,0 +1,36 @@
|
|
1
|
+
require "access_allow/version"
|
2
|
+
require "access_allow/railtie"
|
3
|
+
require "access_allow/abilities"
|
4
|
+
require "access_allow/abilities_manager"
|
5
|
+
require "access_allow/access_manager"
|
6
|
+
require "access_allow/check"
|
7
|
+
require "access_allow/controller_access_dsl"
|
8
|
+
require "access_allow/roles"
|
9
|
+
|
10
|
+
module AccessAllow
|
11
|
+
class ViolationError < StandardError; end
|
12
|
+
|
13
|
+
class ResponseForbiddenError < StandardError; end
|
14
|
+
|
15
|
+
class << self
|
16
|
+
def configuration
|
17
|
+
@configuration ||= Configuration.new
|
18
|
+
end
|
19
|
+
|
20
|
+
def configure
|
21
|
+
yield(configuration) if block_given?
|
22
|
+
configuration
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
class Configuration
|
27
|
+
attr_accessor :roles_and_permissions, :current_user_method, :permissions_association_name, :role_method_name
|
28
|
+
|
29
|
+
def initialize
|
30
|
+
@roles_and_permissions = {}
|
31
|
+
@current_user_method = :current_user
|
32
|
+
@permissions_association_name = :permissions
|
33
|
+
@role_method_name = :role
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "rails/generators/base"
|
4
|
+
require "rails/generators/active_record/migration"
|
5
|
+
|
6
|
+
module AccessAllow
|
7
|
+
module Generators
|
8
|
+
# The Install generator `access_allow:install`
|
9
|
+
class InstallGenerator < ::Rails::Generators::Base
|
10
|
+
include ::ActiveRecord::Generators::Migration
|
11
|
+
|
12
|
+
source_root File.expand_path(__dir__)
|
13
|
+
|
14
|
+
desc "Creates an initial configuration, an initializer and copies in the Permission migration & model."
|
15
|
+
|
16
|
+
def copy_tasks
|
17
|
+
template "templates/access_allow.rb", "config/initializers/access_allow.rb"
|
18
|
+
migration_template "templates/migration.rb.erb", "db/migrate/access_allow_create_permissions.rb", migration_version: migration_version
|
19
|
+
end
|
20
|
+
|
21
|
+
def migration_version
|
22
|
+
"[#{Rails::VERSION::MAJOR}.#{Rails::VERSION::MINOR}]"
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,10 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
AccessAllow.configure do |config|
|
4
|
+
# Roles and permissions associated with each role (you might want to store this in a YAML file and load it here)
|
5
|
+
config.roles_and_permissions = {}
|
6
|
+
|
7
|
+
# config.current_user_method = :current_user
|
8
|
+
# config.permissions_association_name = :permissions
|
9
|
+
# config.role_method_name = :role
|
10
|
+
end
|
@@ -0,0 +1,11 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class AccessAllowCreatePermissions < ActiveRecord::Migration<%= migration_version %>
|
4
|
+
def change
|
5
|
+
create_table :permissions do |t|
|
6
|
+
t.string :ability_name
|
7
|
+
t.references :user, foreign_key: true, null: false
|
8
|
+
t.timestamps
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
metadata
ADDED
@@ -0,0 +1,76 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: access_allow
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Stephen Ierodiaconou
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2022-11-22 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: rails
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: 7.0.4
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: 7.0.4
|
27
|
+
description: Permissions and access control gem for Rails.
|
28
|
+
email:
|
29
|
+
- stevegeek@gmail.com
|
30
|
+
executables: []
|
31
|
+
extensions: []
|
32
|
+
extra_rdoc_files: []
|
33
|
+
files:
|
34
|
+
- MIT-LICENSE
|
35
|
+
- README.md
|
36
|
+
- Rakefile
|
37
|
+
- lib/access_allow.rb
|
38
|
+
- lib/access_allow/abilities.rb
|
39
|
+
- lib/access_allow/abilities_manager.rb
|
40
|
+
- lib/access_allow/access_manager.rb
|
41
|
+
- lib/access_allow/check.rb
|
42
|
+
- lib/access_allow/controller_access_dsl.rb
|
43
|
+
- lib/access_allow/railtie.rb
|
44
|
+
- lib/access_allow/roles.rb
|
45
|
+
- lib/access_allow/version.rb
|
46
|
+
- lib/generators/access_allow/USAGE
|
47
|
+
- lib/generators/access_allow/install_generator.rb
|
48
|
+
- lib/generators/access_allow/templates/access_allow.rb
|
49
|
+
- lib/generators/access_allow/templates/migration.rb.erb
|
50
|
+
- lib/tasks/access_allow_tasks.rake
|
51
|
+
homepage: https://github.com/stevegeek/access_allow
|
52
|
+
licenses:
|
53
|
+
- MIT
|
54
|
+
metadata:
|
55
|
+
homepage_uri: https://github.com/stevegeek/access_allow
|
56
|
+
source_code_uri: https://github.com/stevegeek/access_allow
|
57
|
+
post_install_message:
|
58
|
+
rdoc_options: []
|
59
|
+
require_paths:
|
60
|
+
- lib
|
61
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
62
|
+
requirements:
|
63
|
+
- - ">="
|
64
|
+
- !ruby/object:Gem::Version
|
65
|
+
version: '0'
|
66
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
67
|
+
requirements:
|
68
|
+
- - ">="
|
69
|
+
- !ruby/object:Gem::Version
|
70
|
+
version: '0'
|
71
|
+
requirements: []
|
72
|
+
rubygems_version: 3.3.7
|
73
|
+
signing_key:
|
74
|
+
specification_version: 4
|
75
|
+
summary: Permissions and access control gem for Rails.
|
76
|
+
test_files: []
|