access_allow 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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: []
|