rabarber 1.3.0 → 1.4.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +16 -3
- data/README.md +60 -35
- data/lib/rabarber/configuration.rb +20 -12
- data/lib/rabarber/controllers/concerns/authorization.rb +14 -10
- data/lib/rabarber/core/access.rb +28 -0
- data/lib/rabarber/core/permissions.rb +40 -0
- data/lib/rabarber/core/rule.rb +50 -0
- data/lib/rabarber/input/action.rb +26 -0
- data/lib/rabarber/input/base.rb +7 -3
- data/lib/rabarber/input/dynamic_rule.rb +26 -0
- data/lib/rabarber/input/role.rb +23 -0
- data/lib/rabarber/input/roles.rb +7 -14
- data/lib/rabarber/input/types/{booleans.rb → boolean.rb} +7 -3
- data/lib/rabarber/input/types/{procs.rb → proc.rb} +8 -4
- data/lib/rabarber/input/types/{symbols.rb → symbol.rb} +8 -4
- data/lib/rabarber/logger.rb +33 -4
- data/lib/rabarber/missing/base.rb +4 -4
- data/lib/rabarber/missing/roles.rb +1 -3
- data/lib/rabarber/models/concerns/has_roles.rb +25 -11
- data/lib/rabarber/models/role.rb +8 -2
- data/lib/rabarber/railtie.rb +5 -0
- data/lib/rabarber/version.rb +1 -1
- data/lib/rabarber.rb +10 -6
- data/rabarber.gemspec +2 -2
- metadata +15 -13
- data/lib/rabarber/access.rb +0 -26
- data/lib/rabarber/input/actions.rb +0 -30
- data/lib/rabarber/input/dynamic_rules.rb +0 -30
- data/lib/rabarber/permissions.rb +0 -38
- data/lib/rabarber/rule.rb +0 -48
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: bcddaf07627eb382c58a733150e460348527476d2234f65fe0db2760bce6206f
|
4
|
+
data.tar.gz: 3d1bbfab2a57d860bcb9656cadb5606d09674f887a58b96db0897c3516aed5d2
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: e5ae51e17b580757cc427f269b43155ca3ea5eda5b4813be5d023c273f2271d96108639e24c1921ace823de4cc06b43c03372eb98d64f6d4376f9628e7b6d616
|
7
|
+
data.tar.gz: 232a9ded49264955b8cf7d22de5a1443947dd84d90f44f15f6dc9c9346ee7b1cee56afa85a55bf44bf5325bb1d26e17e6fcf0b8076d3f14381a9bb43254038ab
|
data/CHANGELOG.md
CHANGED
@@ -1,7 +1,19 @@
|
|
1
|
+
## 1.4.0
|
2
|
+
|
3
|
+
- Add 'Audit trail' feature: Logging of role assignments, revocations, and unauthorized access attempts
|
4
|
+
- Add `audit_trail_enabled` configuration option, allowing to enable or disable the audit trail
|
5
|
+
- Deprecate `when_actions_missing` and `when_roles_missing` configuration options (see [the discussion](https://github.com/enjaku4/rabarber/discussions/48))
|
6
|
+
|
7
|
+
## 1.3.1
|
8
|
+
|
9
|
+
- Add `Rabarber::Role.assignees_for` method
|
10
|
+
- Fix inconsistent behavior where passing `nil` as a role name to role management methods would raise an `ActiveRecord` error instead of `Rabarber` error
|
11
|
+
- Various minor code improvements
|
12
|
+
|
1
13
|
## 1.3.0
|
2
14
|
|
3
15
|
- Add methods to directly add, rename, and remove roles
|
4
|
-
- `HasRoles#assign_roles` and `HasRoles#revoke_roles` methods
|
16
|
+
- Modify `Rabarber::HasRoles#assign_roles` and `Rabarber::HasRoles#revoke_roles` methods to return the list of roles assigned to the user
|
5
17
|
- Minor performance improvements
|
6
18
|
|
7
19
|
## 1.2.2
|
@@ -14,6 +26,7 @@
|
|
14
26
|
- Cache roles to avoid unnecessary database queries
|
15
27
|
- Introduce `cache_enabled` configuration option allowing to enable or disable role caching
|
16
28
|
- Enhance the migration generator so that it can receive the table name of the model representing users in the application as an argument
|
29
|
+
- Fix an issue where an error would be raised if the user is not authenticated
|
17
30
|
- Various minor improvements
|
18
31
|
|
19
32
|
## 1.2.0
|
@@ -42,7 +55,7 @@
|
|
42
55
|
## 1.0.2
|
43
56
|
|
44
57
|
- Various enhancements for gem development and release
|
45
|
-
- Modify `HasRoles#roles` method to return an array of role names instead of `Rabarber::Role` objects
|
58
|
+
- Modify `Rabarber::HasRoles#roles` method to return an array of role names instead of `Rabarber::Role` objects
|
46
59
|
|
47
60
|
## 1.0.1
|
48
61
|
|
@@ -61,7 +74,7 @@
|
|
61
74
|
|
62
75
|
## 0.1.4
|
63
76
|
|
64
|
-
- Remove `HasRoles#role?` method as unnecessary
|
77
|
+
- Remove `Rabarber::HasRoles#role?` method as unnecessary
|
65
78
|
|
66
79
|
## 0.1.3
|
67
80
|
|
data/README.md
CHANGED
@@ -3,9 +3,7 @@
|
|
3
3
|
[![Gem Version](https://badge.fury.io/rb/rabarber.svg)](http://badge.fury.io/rb/rabarber)
|
4
4
|
[![Github Actions badge](https://github.com/enjaku4/rabarber/actions/workflows/ci.yml/badge.svg)](https://github.com/enjaku4/rabarber/actions/workflows/ci.yml)
|
5
5
|
|
6
|
-
Rabarber is a role-based authorization library for Ruby on Rails, designed
|
7
|
-
|
8
|
-
Unlike some other libraries, Rabarber does not handle data scoping. Instead, it focuses on providing a lightweight and flexible solution for role-based access control, allowing developers to implement data scoping according to their specific business rules directly within their application's code.
|
6
|
+
Rabarber is a role-based authorization library for Ruby on Rails, primarily designed for use in the web layer of your application but not limited to that. It provides a set of tools for managing user roles and defining authorization rules, along with audit logging for enhanced security.
|
9
7
|
|
10
8
|
---
|
11
9
|
|
@@ -53,8 +51,6 @@ Next, generate a migration to create tables for storing roles in the database. M
|
|
53
51
|
rails g rabarber:roles users
|
54
52
|
```
|
55
53
|
|
56
|
-
This will create a migration file in `db/migrate` directory.
|
57
|
-
|
58
54
|
Finally, run the migration to apply the changes to the database:
|
59
55
|
|
60
56
|
```
|
@@ -63,30 +59,37 @@ rails db:migrate
|
|
63
59
|
|
64
60
|
## Configuration
|
65
61
|
|
66
|
-
Rabarber can be configured by using `.configure` method in an initializer:
|
62
|
+
If specific customization is required, Rabarber can be configured by using `.configure` method in an initializer:
|
67
63
|
|
68
64
|
```rb
|
69
65
|
Rabarber.configure do |config|
|
66
|
+
config.audit_trail_enabled = true
|
70
67
|
config.cache_enabled = true
|
71
68
|
config.current_user_method = :current_user
|
72
69
|
config.must_have_roles = false
|
73
|
-
config.
|
74
|
-
|
75
|
-
|
70
|
+
config.when_unauthorized = -> (controller) {
|
71
|
+
if controller.request.format.html?
|
72
|
+
controller.redirect_back fallback_location: controller.root_path
|
73
|
+
else
|
74
|
+
controller.head :unauthorized
|
75
|
+
end
|
76
|
+
}
|
76
77
|
end
|
77
78
|
```
|
78
79
|
|
79
|
-
- `
|
80
|
+
- `audit_trail_enabled` must be a boolean determining whether the audit trail functionality is enabled. _The audit trail is enabled by default._
|
81
|
+
- `cache_enabled` must be a boolean determining whether roles are cached. _Roles are cached by default to avoid unnecessary database queries._ If you want to disable caching, set this option to `false`. If caching is enabled and you need to clear the cache, use `Rabarber::Cache.clear` method.
|
82
|
+
- `current_user_method` must be a symbol representing the method that returns the currently authenticated user. _The default value is `:current_user`._
|
83
|
+
- `must_have_roles` must be a boolean determining whether a user with no roles can access endpoints permitted to everyone. _The default value is `false` (allowing users without roles to access endpoints permitted to everyone)._
|
84
|
+
- `when_unauthorized` must be a proc where you can define the behaviour when access is not authorized. Lambda argument `controller` is an instance of the controller where the code is executed. _By default, the user is redirected back if the request format is HTML; otherwise, a 401 Unauthorized response is sent._
|
80
85
|
|
81
|
-
|
86
|
+
### Deprecated Configuration Options
|
82
87
|
|
83
|
-
|
88
|
+
The following configuration options are deprecated and will be removed in the next major version (see [the discussion](https://github.com/enjaku4/rabarber/discussions/48)):
|
84
89
|
|
85
|
-
- `when_actions_missing` must be a proc where you can define the behaviour when the
|
90
|
+
- `when_actions_missing` must be a proc where you can define the behaviour when the action specified in `grant_access` method cannot be found in the controller. Lambda argument `missing_actions` is an array of symbols, e.g., `[:index]`, while `context` argument is a hash that looks like this: `{ controller: "InvoicesController" }`. This check is performed when the application is initialized if `eager_load` configuration is enabled in Rails and also on every request. _By default, an error is raised when action is missing._
|
86
91
|
|
87
|
-
- `when_roles_missing` must be a proc where you can define the behaviour when the roles specified in `grant_access` method cannot be found in the database
|
88
|
-
|
89
|
-
- `when_unauthorized` must be a proc where you can define the behaviour when access is not authorized (`controller` is an instance of the controller where the code is executed). By default, the user is redirected back if the request format is HTML; otherwise, a 401 Unauthorized response is sent.
|
92
|
+
- `when_roles_missing` must be a proc where you can define the behaviour when the roles specified in `grant_access` method cannot be found in the database. Lambda argument `missing_roles` is an array of symbols, e.g., `[:admin]`, while `context` argument is a hash that looks like this: `{ controller: "InvoicesController", action: "index" }`. This check is performed when the application is initialized if `eager_load` configuration is enabled in Rails and also on every request. _By default, a warning is logged when roles are missing._
|
90
93
|
|
91
94
|
## Roles
|
92
95
|
|
@@ -101,20 +104,20 @@ end
|
|
101
104
|
|
102
105
|
This adds the following methods:
|
103
106
|
|
104
|
-
**`#assign_roles`**
|
107
|
+
**`#assign_roles(*roles, create_new: true)`**
|
105
108
|
|
106
|
-
To assign roles
|
109
|
+
To assign roles, use:
|
107
110
|
|
108
111
|
```rb
|
109
112
|
user.assign_roles(:accountant, :marketer)
|
110
113
|
```
|
111
|
-
By default,
|
114
|
+
By default, it will automatically create any roles that don't exist. If you want to assign only existing roles and prevent the creation of new ones, use the method with `create_new: false` argument:
|
112
115
|
```rb
|
113
116
|
user.assign_roles(:accountant, :marketer, create_new: false)
|
114
117
|
```
|
115
118
|
The method returns an array of roles assigned to the user.
|
116
119
|
|
117
|
-
**`#revoke_roles`**
|
120
|
+
**`#revoke_roles(*roles)`**
|
118
121
|
|
119
122
|
To revoke roles, use:
|
120
123
|
|
@@ -125,7 +128,7 @@ If any of the specified roles doesn't exist or the user doesn't have the role yo
|
|
125
128
|
|
126
129
|
The method returns an array of roles assigned to the user.
|
127
130
|
|
128
|
-
**`#has_role
|
131
|
+
**`#has_role?(*roles)`**
|
129
132
|
|
130
133
|
To check whether the user has a role, use:
|
131
134
|
|
@@ -147,7 +150,7 @@ user.roles
|
|
147
150
|
|
148
151
|
To manipulate roles directly, you can use `Rabarber::Role` methods:
|
149
152
|
|
150
|
-
**`.add`**
|
153
|
+
**`.add(role)`**
|
151
154
|
|
152
155
|
To add a new role, use:
|
153
156
|
|
@@ -157,7 +160,7 @@ Rabarber::Role.add(:admin)
|
|
157
160
|
|
158
161
|
This will create a new role with the specified name and return `true`. If the role already exists, it will return `false`.
|
159
162
|
|
160
|
-
**`.rename`**
|
163
|
+
**`.rename(old_role_name, new_role_name, force: false)`**
|
161
164
|
|
162
165
|
To rename a role, use:
|
163
166
|
|
@@ -166,12 +169,12 @@ Rabarber::Role.rename(:admin, :administrator)
|
|
166
169
|
```
|
167
170
|
The first argument is the old name, and the second argument is the new name. This will rename the role and return `true`. If a role with the new name already exists, it will return `false`.
|
168
171
|
|
169
|
-
The method won't rename the role if it is assigned to any user. To force the rename, use the method with `force: true` argument:
|
172
|
+
The method won't rename the role and will return `false` if it is assigned to any user. To force the rename, use the method with `force: true` argument:
|
170
173
|
```rb
|
171
174
|
Rabarber::Role.rename(:admin, :administrator, force: true)
|
172
175
|
```
|
173
176
|
|
174
|
-
**`.remove`**
|
177
|
+
**`.remove(role, force: false)`**
|
175
178
|
|
176
179
|
To remove a role, use:
|
177
180
|
|
@@ -181,7 +184,7 @@ Rabarber::Role.remove(:admin)
|
|
181
184
|
|
182
185
|
This will remove the role and return `true`. If the role doesn't exist, it will return `false`.
|
183
186
|
|
184
|
-
The method won't remove the role if it is assigned to any user. To force the removal, use the method with `force: true` argument:
|
187
|
+
The method won't remove the role and will return `false` if it is assigned to any user. To force the removal, use the method with `force: true` argument:
|
185
188
|
```rb
|
186
189
|
Rabarber::Role.remove(:admin, force: true)
|
187
190
|
```
|
@@ -194,9 +197,17 @@ If you need to list all the role names available in your application, use:
|
|
194
197
|
Rabarber::Role.names
|
195
198
|
```
|
196
199
|
|
200
|
+
**`.assignees_for(role)`**
|
201
|
+
|
202
|
+
To get all the users to whom the role is assigned, use:
|
203
|
+
|
204
|
+
```rb
|
205
|
+
Rabarber::Role.assignees_for(:admin)
|
206
|
+
```
|
207
|
+
|
197
208
|
## Authorization Rules
|
198
209
|
|
199
|
-
Include `Rabarber::Authorization` module into the controller that needs authorization rules to be applied
|
210
|
+
Include `Rabarber::Authorization` module into the controller that needs authorization rules to be applied. Typically, it is `ApplicationController`, but it can be any controller of your choice.
|
200
211
|
|
201
212
|
```rb
|
202
213
|
class ApplicationController < ActionController::Base
|
@@ -204,7 +215,7 @@ class ApplicationController < ActionController::Base
|
|
204
215
|
...
|
205
216
|
end
|
206
217
|
```
|
207
|
-
This adds `.grant_access` method which allows you to define the authorization rules.
|
218
|
+
This adds `.grant_access(action: nil, roles: nil, if: nil, unless: nil)` method which allows you to define the authorization rules.
|
208
219
|
|
209
220
|
The most basic usage of the method is as follows:
|
210
221
|
|
@@ -264,9 +275,9 @@ class InvoicesController < ApplicationController
|
|
264
275
|
end
|
265
276
|
```
|
266
277
|
|
267
|
-
This allows everyone to access `OrdersController` and its children and `index` action in `InvoicesController`. This
|
278
|
+
This allows everyone to access `OrdersController` and its children and also `index` action in `InvoicesController`. This extends to scenarios where there is no user present, i.e. when the method responsible for returning the currently authenticated user in your application returns `nil`.
|
268
279
|
|
269
|
-
|
280
|
+
_Be aware that if the user is not authenticated (the method responsible for returning the currently authenticated user in your application returns `nil`), Rabarber will treat this situation as if the user with no roles assigned was authenticated._
|
270
281
|
|
271
282
|
If you've set `must_have_roles` setting to `true`, then, only the users with at least one role can have access. This setting can be useful if your requirements are such that users without roles are not allowed to access anything.
|
272
283
|
|
@@ -301,7 +312,7 @@ class InvoicesController < ApplicationController
|
|
301
312
|
end
|
302
313
|
end
|
303
314
|
```
|
304
|
-
You can pass a dynamic rule as `if` or `unless` argument. It can be a symbol
|
315
|
+
You can pass a dynamic rule as `if` or `unless` argument. It can be a symbol, in which case the method with the same name will be called. Alternatively, it can be a proc, which will be executed within the context of the controller's instance.
|
305
316
|
|
306
317
|
Rules defined in child classes don't override parent rules but rather add to them:
|
307
318
|
```rb
|
@@ -319,7 +330,7 @@ This means that `Crm::InvoicesController` is still accessible to `admin` but is
|
|
319
330
|
|
320
331
|
## View Helpers
|
321
332
|
|
322
|
-
Rabarber also provides a couple of helpers that can be used in views: `visible_to` and `hidden_from`. To use them, simply include `Rabarber::Helpers` in the desired helper
|
333
|
+
Rabarber also provides a couple of helpers that can be used in views: `visible_to(*roles, &block)` and `hidden_from(*roles, &block)`. To use them, simply include `Rabarber::Helpers` in the desired helper. Usually it is `ApplicationHelper`, but it can be any helper of your choice.
|
323
334
|
|
324
335
|
```rb
|
325
336
|
module ApplicationHelper
|
@@ -342,16 +353,30 @@ The usage is straightforward:
|
|
342
353
|
<% end %>
|
343
354
|
```
|
344
355
|
|
356
|
+
## Audit Trail
|
357
|
+
|
358
|
+
Rabarber supports audit trail, which provides a record of user access control activity. This feature logs the following events:
|
359
|
+
|
360
|
+
- Role assignments to users
|
361
|
+
- Role revocations from users
|
362
|
+
- Unauthorized access attempts
|
363
|
+
|
364
|
+
The logs are written to the file `log/rabarber_audit.log` unless the `audit_trail_enabled` configuration option is set to `false`.
|
365
|
+
|
345
366
|
## Problems?
|
346
367
|
|
347
|
-
|
368
|
+
Facing a problem or want to suggest an enhancement?
|
369
|
+
|
370
|
+
- **Open a Discussion**: If you have a question, experience difficulties using the gem, or have an improvement suggestion, feel free to use the Discussions section.
|
371
|
+
|
372
|
+
Encountered a bug?
|
348
373
|
|
349
|
-
- **Create an Issue**: If you've identified a
|
374
|
+
- **Create an Issue**: If you've identified a bug, please create an issue. Be sure to provide detailed information about the problem, including the steps to reproduce it.
|
350
375
|
- **Contribute a Solution**: Found a fix for the issue? Feel free to create a pull request with your changes.
|
351
376
|
|
352
377
|
## Contributing
|
353
378
|
|
354
|
-
|
379
|
+
Before opening an issue or creating a pull request, please read the [contributing guidelines](https://github.com/enjaku4/rabarber/blob/main/CONTRIBUTING.md).
|
355
380
|
|
356
381
|
## License
|
357
382
|
|
@@ -1,15 +1,14 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require "singleton"
|
4
|
-
|
5
3
|
module Rabarber
|
6
4
|
class Configuration
|
7
5
|
include Singleton
|
8
6
|
|
9
|
-
attr_reader :cache_enabled, :current_user_method, :must_have_roles,
|
7
|
+
attr_reader :audit_trail_enabled, :cache_enabled, :current_user_method, :must_have_roles,
|
10
8
|
:when_actions_missing, :when_roles_missing, :when_unauthorized
|
11
9
|
|
12
10
|
def initialize
|
11
|
+
@audit_trail_enabled = default_audit_trail_enabled
|
13
12
|
@cache_enabled = default_cache_enabled
|
14
13
|
@current_user_method = default_current_user_method
|
15
14
|
@must_have_roles = default_must_have_roles
|
@@ -18,44 +17,54 @@ module Rabarber
|
|
18
17
|
@when_unauthorized = default_when_unauthorized
|
19
18
|
end
|
20
19
|
|
20
|
+
def audit_trail_enabled=(value)
|
21
|
+
@audit_trail_enabled = Rabarber::Input::Types::Boolean.new(
|
22
|
+
value, Rabarber::ConfigurationError, "Configuration 'audit_trail_enabled' must be a Boolean"
|
23
|
+
).process
|
24
|
+
end
|
25
|
+
|
21
26
|
def cache_enabled=(value)
|
22
|
-
@cache_enabled = Rabarber::Input::Types::
|
27
|
+
@cache_enabled = Rabarber::Input::Types::Boolean.new(
|
23
28
|
value, Rabarber::ConfigurationError, "Configuration 'cache_enabled' must be a Boolean"
|
24
29
|
).process
|
25
30
|
end
|
26
31
|
|
27
32
|
def current_user_method=(method_name)
|
28
|
-
@current_user_method = Rabarber::Input::Types::
|
33
|
+
@current_user_method = Rabarber::Input::Types::Symbol.new(
|
29
34
|
method_name, Rabarber::ConfigurationError, "Configuration 'current_user_method' must be a Symbol or a String"
|
30
35
|
).process
|
31
36
|
end
|
32
37
|
|
33
38
|
def must_have_roles=(value)
|
34
|
-
@must_have_roles = Rabarber::Input::Types::
|
39
|
+
@must_have_roles = Rabarber::Input::Types::Boolean.new(
|
35
40
|
value, Rabarber::ConfigurationError, "Configuration 'must_have_roles' must be a Boolean"
|
36
41
|
).process
|
37
42
|
end
|
38
43
|
|
39
44
|
def when_actions_missing=(callable)
|
40
|
-
@when_actions_missing = Rabarber::Input::Types::
|
45
|
+
@when_actions_missing = Rabarber::Input::Types::Proc.new(
|
41
46
|
callable, Rabarber::ConfigurationError, "Configuration 'when_actions_missing' must be a Proc"
|
42
47
|
).process
|
43
48
|
end
|
44
49
|
|
45
50
|
def when_roles_missing=(callable)
|
46
|
-
@when_roles_missing = Rabarber::Input::Types::
|
51
|
+
@when_roles_missing = Rabarber::Input::Types::Proc.new(
|
47
52
|
callable, Rabarber::ConfigurationError, "Configuration 'when_roles_missing' must be a Proc"
|
48
53
|
).process
|
49
54
|
end
|
50
55
|
|
51
56
|
def when_unauthorized=(callable)
|
52
|
-
@when_unauthorized = Rabarber::Input::Types::
|
57
|
+
@when_unauthorized = Rabarber::Input::Types::Proc.new(
|
53
58
|
callable, Rabarber::ConfigurationError, "Configuration 'when_unauthorized' must be a Proc"
|
54
59
|
).process
|
55
60
|
end
|
56
61
|
|
57
62
|
private
|
58
63
|
|
64
|
+
def default_audit_trail_enabled
|
65
|
+
true
|
66
|
+
end
|
67
|
+
|
59
68
|
def default_cache_enabled
|
60
69
|
true
|
61
70
|
end
|
@@ -70,21 +79,20 @@ module Rabarber
|
|
70
79
|
|
71
80
|
def default_when_actions_missing
|
72
81
|
-> (missing_actions, context) {
|
73
|
-
raise
|
82
|
+
raise(Rabarber::Error, "'grant_access' method called with non-existent actions: #{missing_actions}, context: '#{context[:controller]}'")
|
74
83
|
}
|
75
84
|
end
|
76
85
|
|
77
86
|
def default_when_roles_missing
|
78
87
|
-> (missing_roles, context) {
|
79
88
|
delimiter = context[:action] ? "#" : ""
|
80
|
-
message = "
|
89
|
+
message = "'grant_access' method called with non-existent roles: #{missing_roles}, context: '#{context[:controller]}#{delimiter}#{context[:action]}'"
|
81
90
|
Rabarber::Logger.log(:warn, message)
|
82
91
|
}
|
83
92
|
end
|
84
93
|
|
85
94
|
def default_when_unauthorized
|
86
95
|
-> (controller) do
|
87
|
-
Rabarber::Logger.log(:warn, "Unauthorized attempt")
|
88
96
|
if controller.request.format.html?
|
89
97
|
controller.redirect_back fallback_location: controller.main_app.root_path
|
90
98
|
else
|
@@ -12,12 +12,12 @@ module Rabarber
|
|
12
12
|
def grant_access(action: nil, roles: nil, if: nil, unless: nil)
|
13
13
|
dynamic_rule, negated_dynamic_rule = binding.local_variable_get(:if), binding.local_variable_get(:unless)
|
14
14
|
|
15
|
-
Rabarber::Permissions.add(
|
15
|
+
Rabarber::Core::Permissions.add(
|
16
16
|
self,
|
17
|
-
Rabarber::Input::
|
17
|
+
Rabarber::Input::Action.new(action).process,
|
18
18
|
Rabarber::Input::Roles.new(roles).process,
|
19
|
-
Rabarber::Input::
|
20
|
-
Rabarber::Input::
|
19
|
+
Rabarber::Input::DynamicRule.new(dynamic_rule).process,
|
20
|
+
Rabarber::Input::DynamicRule.new(negated_dynamic_rule).process
|
21
21
|
)
|
22
22
|
end
|
23
23
|
end
|
@@ -28,14 +28,18 @@ module Rabarber
|
|
28
28
|
Rabarber::Missing::Actions.new(self.class).handle
|
29
29
|
Rabarber::Missing::Roles.new(self.class).handle
|
30
30
|
|
31
|
-
|
31
|
+
roleable = send(Rabarber::Configuration.instance.current_user_method)
|
32
32
|
|
33
|
-
Rabarber::
|
34
|
-
|
33
|
+
return if Rabarber::Core::Permissions.access_granted?(
|
34
|
+
roleable ? roleable.roles : [], self.class, action_name.to_sym, self
|
35
|
+
)
|
35
36
|
|
36
|
-
|
37
|
-
|
38
|
-
|
37
|
+
Rabarber::Logger.audit(
|
38
|
+
:warn,
|
39
|
+
"[Unauthorized Attempt] #{Rabarber::Logger.roleable_identity(roleable, with_roles: true)} attempted to access '#{request.path}'"
|
40
|
+
)
|
41
|
+
|
42
|
+
Rabarber::Configuration.instance.when_unauthorized.call(self)
|
39
43
|
end
|
40
44
|
end
|
41
45
|
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Rabarber
|
4
|
+
module Core
|
5
|
+
module Access
|
6
|
+
def access_granted?(roles, controller, action, dynamic_rule_receiver)
|
7
|
+
controller_accessible?(roles, controller, dynamic_rule_receiver) ||
|
8
|
+
action_accessible?(roles, controller, action, dynamic_rule_receiver)
|
9
|
+
end
|
10
|
+
|
11
|
+
def controller_accessible?(roles, controller, dynamic_rule_receiver)
|
12
|
+
accessible_controllers(roles, dynamic_rule_receiver).any? do |accessible_controller|
|
13
|
+
controller <= accessible_controller
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def action_accessible?(roles, controller, action, dynamic_rule_receiver)
|
18
|
+
action_rules[controller].any? { |rule| rule.verify_access(roles, dynamic_rule_receiver, action) }
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
|
23
|
+
def accessible_controllers(roles, dynamic_rule_receiver)
|
24
|
+
controller_rules.select { |_, rule| rule.verify_access(roles, dynamic_rule_receiver) }.keys
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "access"
|
4
|
+
require_relative "rule"
|
5
|
+
|
6
|
+
module Rabarber
|
7
|
+
module Core
|
8
|
+
class Permissions
|
9
|
+
include Singleton
|
10
|
+
|
11
|
+
extend Access
|
12
|
+
|
13
|
+
attr_reader :storage
|
14
|
+
|
15
|
+
def initialize
|
16
|
+
@storage = { controller_rules: Hash.new({}), action_rules: Hash.new([]) }
|
17
|
+
end
|
18
|
+
|
19
|
+
class << self
|
20
|
+
def add(controller, action, roles, dynamic_rule, negated_dynamic_rule)
|
21
|
+
rule = Rabarber::Core::Rule.new(action, roles, dynamic_rule, negated_dynamic_rule)
|
22
|
+
|
23
|
+
if action
|
24
|
+
instance.storage[:action_rules][controller] += [rule]
|
25
|
+
else
|
26
|
+
instance.storage[:controller_rules][controller] = rule
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def controller_rules
|
31
|
+
instance.storage[:controller_rules]
|
32
|
+
end
|
33
|
+
|
34
|
+
def action_rules
|
35
|
+
instance.storage[:action_rules]
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Rabarber
|
4
|
+
module Core
|
5
|
+
class Rule
|
6
|
+
attr_reader :action, :roles, :dynamic_rule, :negated_dynamic_rule
|
7
|
+
|
8
|
+
def initialize(action, roles, dynamic_rule, negated_dynamic_rule)
|
9
|
+
@action = action
|
10
|
+
@roles = Array(roles)
|
11
|
+
@dynamic_rule = dynamic_rule
|
12
|
+
@negated_dynamic_rule = negated_dynamic_rule
|
13
|
+
end
|
14
|
+
|
15
|
+
def verify_access(user_roles, dynamic_rule_receiver, action_name = nil)
|
16
|
+
action_accessible?(action_name) && roles_permitted?(user_roles) && dynamic_rule_followed?(dynamic_rule_receiver)
|
17
|
+
end
|
18
|
+
|
19
|
+
def action_accessible?(action_name)
|
20
|
+
action_name.nil? || action_name == action
|
21
|
+
end
|
22
|
+
|
23
|
+
def roles_permitted?(user_roles)
|
24
|
+
return false if Rabarber::Configuration.instance.must_have_roles && user_roles.empty?
|
25
|
+
|
26
|
+
roles.empty? || (roles & user_roles).any?
|
27
|
+
end
|
28
|
+
|
29
|
+
def dynamic_rule_followed?(dynamic_rule_receiver)
|
30
|
+
!!(execute_dynamic_rule(dynamic_rule_receiver, false) && execute_dynamic_rule(dynamic_rule_receiver, true))
|
31
|
+
end
|
32
|
+
|
33
|
+
private
|
34
|
+
|
35
|
+
def execute_dynamic_rule(dynamic_rule_receiver, is_negated)
|
36
|
+
rule = is_negated ? negated_dynamic_rule : dynamic_rule
|
37
|
+
|
38
|
+
return true if rule.nil?
|
39
|
+
|
40
|
+
result = if rule.is_a?(Proc)
|
41
|
+
dynamic_rule_receiver.instance_exec(&rule)
|
42
|
+
else
|
43
|
+
dynamic_rule_receiver.send(rule)
|
44
|
+
end
|
45
|
+
|
46
|
+
is_negated ? !result : result
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Rabarber
|
4
|
+
module Input
|
5
|
+
class Action < Rabarber::Input::Base
|
6
|
+
def valid?
|
7
|
+
Rabarber::Input::Types::Symbol.new(value).valid? || value.nil?
|
8
|
+
end
|
9
|
+
|
10
|
+
private
|
11
|
+
|
12
|
+
def processed_value
|
13
|
+
case value
|
14
|
+
when String, Symbol
|
15
|
+
value.to_sym
|
16
|
+
when nil
|
17
|
+
value
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def default_error_message
|
22
|
+
"Action name must be a Symbol or a String"
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
data/lib/rabarber/input/base.rb
CHANGED
@@ -5,7 +5,7 @@ module Rabarber
|
|
5
5
|
class Base
|
6
6
|
attr_reader :value, :error_type, :error_message
|
7
7
|
|
8
|
-
def initialize(value, error_type, error_message)
|
8
|
+
def initialize(value, error_type = Rabarber::InvalidArgumentError, error_message = default_error_message)
|
9
9
|
@value = value
|
10
10
|
@error_type = error_type
|
11
11
|
@error_message = error_message
|
@@ -15,16 +15,20 @@ module Rabarber
|
|
15
15
|
valid? ? processed_value : raise_error
|
16
16
|
end
|
17
17
|
|
18
|
-
private
|
19
|
-
|
20
18
|
def valid?
|
21
19
|
raise NotImplementedError
|
22
20
|
end
|
23
21
|
|
22
|
+
private
|
23
|
+
|
24
24
|
def processed_value
|
25
25
|
raise NotImplementedError
|
26
26
|
end
|
27
27
|
|
28
|
+
def default_error_message
|
29
|
+
raise NotImplementedError
|
30
|
+
end
|
31
|
+
|
28
32
|
def raise_error
|
29
33
|
raise error_type, error_message
|
30
34
|
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Rabarber
|
4
|
+
module Input
|
5
|
+
class DynamicRule < Rabarber::Input::Base
|
6
|
+
def valid?
|
7
|
+
Rabarber::Input::Types::Symbol.new(value).valid? || Rabarber::Input::Types::Proc.new(value).valid? || value.nil?
|
8
|
+
end
|
9
|
+
|
10
|
+
private
|
11
|
+
|
12
|
+
def processed_value
|
13
|
+
case value
|
14
|
+
when String, Symbol
|
15
|
+
value.to_sym
|
16
|
+
when Proc, nil
|
17
|
+
value
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def default_error_message
|
22
|
+
"Dynamic rule must be a Symbol, a String, or a Proc"
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Rabarber
|
4
|
+
module Input
|
5
|
+
class Role < Rabarber::Input::Base
|
6
|
+
REGEX = /\A[a-z0-9_]+\z/
|
7
|
+
|
8
|
+
def valid?
|
9
|
+
Rabarber::Input::Types::Symbol.new(value).valid? && value.to_s.match?(REGEX)
|
10
|
+
end
|
11
|
+
|
12
|
+
private
|
13
|
+
|
14
|
+
def processed_value
|
15
|
+
value.to_sym
|
16
|
+
end
|
17
|
+
|
18
|
+
def default_error_message
|
19
|
+
"Role name must be a Symbol or a String and may only contain lowercase letters, numbers and underscores"
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
data/lib/rabarber/input/roles.rb
CHANGED
@@ -3,30 +3,23 @@
|
|
3
3
|
module Rabarber
|
4
4
|
module Input
|
5
5
|
class Roles < Rabarber::Input::Base
|
6
|
-
REGEX = /\A[a-z0-9_]+\z/
|
7
|
-
|
8
|
-
def initialize(
|
9
|
-
value,
|
10
|
-
error_type = Rabarber::InvalidArgumentError,
|
11
|
-
error_message =
|
12
|
-
"Role names must be Symbols or Strings and may only contain lowercase letters, numbers and underscores"
|
13
|
-
)
|
14
|
-
super
|
15
|
-
end
|
16
|
-
|
17
6
|
def value
|
18
7
|
Array(super)
|
19
8
|
end
|
20
9
|
|
21
|
-
private
|
22
|
-
|
23
10
|
def valid?
|
24
|
-
value.all? { |role_name|
|
11
|
+
value.all? { |role_name| Rabarber::Input::Role.new(role_name).valid? }
|
25
12
|
end
|
26
13
|
|
14
|
+
private
|
15
|
+
|
27
16
|
def processed_value
|
28
17
|
value.map(&:to_sym)
|
29
18
|
end
|
19
|
+
|
20
|
+
def default_error_message
|
21
|
+
"Role names must be Symbols or Strings and may only contain lowercase letters, numbers and underscores"
|
22
|
+
end
|
30
23
|
end
|
31
24
|
end
|
32
25
|
end
|
@@ -3,16 +3,20 @@
|
|
3
3
|
module Rabarber
|
4
4
|
module Input
|
5
5
|
module Types
|
6
|
-
class
|
7
|
-
private
|
8
|
-
|
6
|
+
class Boolean < Rabarber::Input::Base
|
9
7
|
def valid?
|
10
8
|
[true, false].include?(value)
|
11
9
|
end
|
12
10
|
|
11
|
+
private
|
12
|
+
|
13
13
|
def processed_value
|
14
14
|
value
|
15
15
|
end
|
16
|
+
|
17
|
+
def default_error_message
|
18
|
+
"Value must be a Boolean"
|
19
|
+
end
|
16
20
|
end
|
17
21
|
end
|
18
22
|
end
|
@@ -3,16 +3,20 @@
|
|
3
3
|
module Rabarber
|
4
4
|
module Input
|
5
5
|
module Types
|
6
|
-
class
|
7
|
-
private
|
8
|
-
|
6
|
+
class Proc < Rabarber::Input::Base
|
9
7
|
def valid?
|
10
|
-
value.is_a?(Proc)
|
8
|
+
value.is_a?(::Proc)
|
11
9
|
end
|
12
10
|
|
11
|
+
private
|
12
|
+
|
13
13
|
def processed_value
|
14
14
|
value
|
15
15
|
end
|
16
|
+
|
17
|
+
def default_error_message
|
18
|
+
"Value must be a Proc"
|
19
|
+
end
|
16
20
|
end
|
17
21
|
end
|
18
22
|
end
|
@@ -3,16 +3,20 @@
|
|
3
3
|
module Rabarber
|
4
4
|
module Input
|
5
5
|
module Types
|
6
|
-
class
|
7
|
-
private
|
8
|
-
|
6
|
+
class Symbol < Rabarber::Input::Base
|
9
7
|
def valid?
|
10
|
-
(value.is_a?(Symbol) || value.is_a?(String)) && value.present?
|
8
|
+
(value.is_a?(::Symbol) || value.is_a?(String)) && value.present?
|
11
9
|
end
|
12
10
|
|
11
|
+
private
|
12
|
+
|
13
13
|
def processed_value
|
14
14
|
value.to_sym
|
15
15
|
end
|
16
|
+
|
17
|
+
def default_error_message
|
18
|
+
"Value must be a Symbol or a String"
|
19
|
+
end
|
16
20
|
end
|
17
21
|
end
|
18
22
|
end
|
data/lib/rabarber/logger.rb
CHANGED
@@ -1,11 +1,40 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
module Rabarber
|
4
|
-
|
5
|
-
|
4
|
+
class Logger
|
5
|
+
include Singleton
|
6
6
|
|
7
|
-
|
8
|
-
|
7
|
+
attr_reader :rails_logger, :audit_logger
|
8
|
+
|
9
|
+
def initialize
|
10
|
+
@rails_logger = Rails.logger
|
11
|
+
@audit_logger = ::Logger.new(Rails.root.join("log/rabarber_audit.log"))
|
12
|
+
end
|
13
|
+
|
14
|
+
class << self
|
15
|
+
def log(log_level, message)
|
16
|
+
instance.rails_logger.tagged("Rabarber") { instance.rails_logger.public_send(log_level, message) }
|
17
|
+
end
|
18
|
+
|
19
|
+
def audit(log_level, message)
|
20
|
+
return unless Rabarber::Configuration.instance.audit_trail_enabled
|
21
|
+
|
22
|
+
instance.audit_logger.public_send(log_level, message)
|
23
|
+
end
|
24
|
+
|
25
|
+
def roleable_identity(roleable, with_roles:)
|
26
|
+
if roleable
|
27
|
+
model_name = roleable.model_name.human
|
28
|
+
primary_key = roleable.class.primary_key
|
29
|
+
roleable_id = roleable.public_send(primary_key)
|
30
|
+
|
31
|
+
roles = with_roles ? ", roles: #{roleable.roles}" : ""
|
32
|
+
|
33
|
+
"#{model_name} with #{primary_key}: '#{roleable_id}'#{roles}"
|
34
|
+
else
|
35
|
+
"Unauthenticated user"
|
36
|
+
end
|
37
|
+
end
|
9
38
|
end
|
10
39
|
end
|
11
40
|
end
|
@@ -41,17 +41,17 @@ module Rabarber
|
|
41
41
|
|
42
42
|
def controller_rules
|
43
43
|
if controller
|
44
|
-
Rabarber::Permissions.controller_rules.slice(controller)
|
44
|
+
Rabarber::Core::Permissions.controller_rules.slice(controller)
|
45
45
|
else
|
46
|
-
Rabarber::Permissions.controller_rules
|
46
|
+
Rabarber::Core::Permissions.controller_rules
|
47
47
|
end
|
48
48
|
end
|
49
49
|
|
50
50
|
def action_rules
|
51
51
|
if controller
|
52
|
-
Rabarber::Permissions.action_rules.slice(controller)
|
52
|
+
Rabarber::Core::Permissions.action_rules.slice(controller)
|
53
53
|
else
|
54
|
-
Rabarber::Permissions.action_rules
|
54
|
+
Rabarber::Core::Permissions.action_rules
|
55
55
|
end
|
56
56
|
end
|
57
57
|
end
|
@@ -16,9 +16,7 @@ module Rabarber
|
|
16
16
|
action_rules.each do |controller, controller_action_rules|
|
17
17
|
controller_action_rules.each do |action_rule|
|
18
18
|
missing_roles = action_rule.roles - all_roles
|
19
|
-
if missing_roles.any?
|
20
|
-
missing_list << Rabarber::Missing::Item.new(missing_roles, controller, action_rule.action)
|
21
|
-
end
|
19
|
+
missing_list << Rabarber::Missing::Item.new(missing_roles, controller, action_rule.action) if missing_roles.any?
|
22
20
|
end
|
23
21
|
end
|
24
22
|
end
|
@@ -5,9 +5,7 @@ module Rabarber
|
|
5
5
|
extend ActiveSupport::Concern
|
6
6
|
|
7
7
|
included do
|
8
|
-
if defined?(@@included) && @@included != name
|
9
|
-
raise Rabarber::Error, "Rabarber::HasRoles can only be included once"
|
10
|
-
end
|
8
|
+
raise Rabarber::Error, "Rabarber::HasRoles can only be included once" if defined?(@@included) && @@included != name
|
11
9
|
|
12
10
|
@@included = name
|
13
11
|
|
@@ -27,31 +25,47 @@ module Rabarber
|
|
27
25
|
end
|
28
26
|
|
29
27
|
def assign_roles(*role_names, create_new: true)
|
30
|
-
|
28
|
+
processed_role_names = process_role_names(role_names)
|
31
29
|
|
32
|
-
create_new_roles(
|
30
|
+
create_new_roles(processed_role_names) if create_new
|
33
31
|
|
34
|
-
|
32
|
+
roles_to_assign = Rabarber::Role.where(name: processed_role_names) - rabarber_roles
|
35
33
|
|
36
|
-
if
|
34
|
+
if roles_to_assign.any?
|
37
35
|
delete_roleable_cache
|
38
|
-
rabarber_roles <<
|
36
|
+
rabarber_roles << roles_to_assign
|
37
|
+
|
38
|
+
Rabarber::Logger.audit(
|
39
|
+
:info,
|
40
|
+
"[Role Assignment] #{Rabarber::Logger.roleable_identity(self, with_roles: false)} has been assigned the following roles: #{roles_to_assign.pluck(:name).map(&:to_sym)}, current roles: #{roles}"
|
41
|
+
)
|
39
42
|
end
|
40
43
|
|
41
44
|
roles
|
42
45
|
end
|
43
46
|
|
44
47
|
def revoke_roles(*role_names)
|
45
|
-
|
48
|
+
processed_role_names = process_role_names(role_names)
|
49
|
+
roles_to_revoke = Rabarber::Role.where(name: processed_role_names.intersection(roles))
|
46
50
|
|
47
|
-
if
|
51
|
+
if roles_to_revoke.any?
|
48
52
|
delete_roleable_cache
|
49
|
-
self.rabarber_roles
|
53
|
+
self.rabarber_roles -= roles_to_revoke
|
54
|
+
|
55
|
+
Rabarber::Logger.audit(
|
56
|
+
:info,
|
57
|
+
"[Role Revocation] #{Rabarber::Logger.roleable_identity(self, with_roles: false)} has been revoked from the following roles: #{roles_to_revoke.pluck(:name).map(&:to_sym)}, current roles: #{roles}"
|
58
|
+
)
|
50
59
|
end
|
51
60
|
|
52
61
|
roles
|
53
62
|
end
|
54
63
|
|
64
|
+
def roleable_class
|
65
|
+
@@included.constantize
|
66
|
+
end
|
67
|
+
module_function :roleable_class
|
68
|
+
|
55
69
|
private
|
56
70
|
|
57
71
|
def create_new_roles(role_names)
|
data/lib/rabarber/models/role.rb
CHANGED
@@ -4,7 +4,7 @@ module Rabarber
|
|
4
4
|
class Role < ActiveRecord::Base
|
5
5
|
self.table_name = "rabarber_roles"
|
6
6
|
|
7
|
-
validates :name, presence: true, uniqueness: true, format: { with: Rabarber::Input::
|
7
|
+
validates :name, presence: true, uniqueness: true, format: { with: Rabarber::Input::Role::REGEX }, strict: true
|
8
8
|
|
9
9
|
has_and_belongs_to_many :roleables, join_table: "rabarber_roles_roleables"
|
10
10
|
|
@@ -46,6 +46,12 @@ module Rabarber
|
|
46
46
|
!!role.destroy!
|
47
47
|
end
|
48
48
|
|
49
|
+
def assignees_for(name)
|
50
|
+
Rabarber::HasRoles.roleable_class.joins(:rabarber_roles).where(
|
51
|
+
rabarber_roles: { name: Rabarber::Input::Role.new(name).process }
|
52
|
+
)
|
53
|
+
end
|
54
|
+
|
49
55
|
private
|
50
56
|
|
51
57
|
def delete_roles_cache
|
@@ -64,7 +70,7 @@ module Rabarber
|
|
64
70
|
end
|
65
71
|
|
66
72
|
def process_role_name(name)
|
67
|
-
Rabarber::Input::
|
73
|
+
Rabarber::Input::Role.new(name).process
|
68
74
|
end
|
69
75
|
end
|
70
76
|
end
|
data/lib/rabarber/railtie.rb
CHANGED
@@ -8,6 +8,11 @@ module Rabarber
|
|
8
8
|
app.config.after_initialize do
|
9
9
|
Rabarber::Missing::Actions.new.handle
|
10
10
|
Rabarber::Missing::Roles.new.handle if Rabarber::Role.table_exists?
|
11
|
+
|
12
|
+
Rabarber::Logger.log(
|
13
|
+
:warn,
|
14
|
+
"DEPRECATION WARNING: Configurations 'when_actions_missing' and 'when_roles_missing' are deprecated and will be removed in v2.0.0"
|
15
|
+
)
|
11
16
|
end
|
12
17
|
end
|
13
18
|
end
|
data/lib/rabarber/version.rb
CHANGED
data/lib/rabarber.rb
CHANGED
@@ -1,5 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require "singleton"
|
4
|
+
|
3
5
|
require_relative "rabarber/version"
|
4
6
|
require_relative "rabarber/logger"
|
5
7
|
require_relative "rabarber/configuration"
|
@@ -8,12 +10,13 @@ require "active_record"
|
|
8
10
|
require "active_support"
|
9
11
|
|
10
12
|
require_relative "rabarber/input/base"
|
11
|
-
require_relative "rabarber/input/
|
12
|
-
require_relative "rabarber/input/
|
13
|
+
require_relative "rabarber/input/action"
|
14
|
+
require_relative "rabarber/input/dynamic_rule"
|
15
|
+
require_relative "rabarber/input/role"
|
13
16
|
require_relative "rabarber/input/roles"
|
14
|
-
require_relative "rabarber/input/types/
|
15
|
-
require_relative "rabarber/input/types/
|
16
|
-
require_relative "rabarber/input/types/
|
17
|
+
require_relative "rabarber/input/types/boolean"
|
18
|
+
require_relative "rabarber/input/types/proc"
|
19
|
+
require_relative "rabarber/input/types/symbol"
|
17
20
|
|
18
21
|
require_relative "rabarber/missing/base"
|
19
22
|
require_relative "rabarber/missing/actions"
|
@@ -25,7 +28,8 @@ require_relative "rabarber/controllers/concerns/authorization"
|
|
25
28
|
require_relative "rabarber/helpers/helpers"
|
26
29
|
require_relative "rabarber/models/concerns/has_roles"
|
27
30
|
require_relative "rabarber/models/role"
|
28
|
-
|
31
|
+
|
32
|
+
require_relative "rabarber/core/permissions"
|
29
33
|
|
30
34
|
require_relative "rabarber/railtie"
|
31
35
|
|
data/rabarber.gemspec
CHANGED
@@ -7,8 +7,8 @@ Gem::Specification.new do |spec|
|
|
7
7
|
spec.version = Rabarber::VERSION
|
8
8
|
spec.authors = ["enjaku4", "trafium"]
|
9
9
|
spec.email = ["rabarber_gem@icloud.com"]
|
10
|
-
|
11
|
-
spec.summary = "Simple authorization library for Ruby on Rails."
|
10
|
+
spec.metadata["rubygems_mfa_required"] = "true"
|
11
|
+
spec.summary = "Simple role-based authorization library for Ruby on Rails."
|
12
12
|
spec.homepage = "https://github.com/enjaku4/rabarber"
|
13
13
|
spec.license = "MIT"
|
14
14
|
spec.required_ruby_version = ">= 3.0"
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: rabarber
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.
|
4
|
+
version: 1.4.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- enjaku4
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2024-
|
12
|
+
date: 2024-03-17 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: rails
|
@@ -38,33 +38,35 @@ files:
|
|
38
38
|
- lib/generators/rabarber/roles_generator.rb
|
39
39
|
- lib/generators/rabarber/templates/create_rabarber_roles.rb.erb
|
40
40
|
- lib/rabarber.rb
|
41
|
-
- lib/rabarber/access.rb
|
42
41
|
- lib/rabarber/cache.rb
|
43
42
|
- lib/rabarber/configuration.rb
|
44
43
|
- lib/rabarber/controllers/concerns/authorization.rb
|
44
|
+
- lib/rabarber/core/access.rb
|
45
|
+
- lib/rabarber/core/permissions.rb
|
46
|
+
- lib/rabarber/core/rule.rb
|
45
47
|
- lib/rabarber/helpers/helpers.rb
|
46
|
-
- lib/rabarber/input/
|
48
|
+
- lib/rabarber/input/action.rb
|
47
49
|
- lib/rabarber/input/base.rb
|
48
|
-
- lib/rabarber/input/
|
50
|
+
- lib/rabarber/input/dynamic_rule.rb
|
51
|
+
- lib/rabarber/input/role.rb
|
49
52
|
- lib/rabarber/input/roles.rb
|
50
|
-
- lib/rabarber/input/types/
|
51
|
-
- lib/rabarber/input/types/
|
52
|
-
- lib/rabarber/input/types/
|
53
|
+
- lib/rabarber/input/types/boolean.rb
|
54
|
+
- lib/rabarber/input/types/proc.rb
|
55
|
+
- lib/rabarber/input/types/symbol.rb
|
53
56
|
- lib/rabarber/logger.rb
|
54
57
|
- lib/rabarber/missing/actions.rb
|
55
58
|
- lib/rabarber/missing/base.rb
|
56
59
|
- lib/rabarber/missing/roles.rb
|
57
60
|
- lib/rabarber/models/concerns/has_roles.rb
|
58
61
|
- lib/rabarber/models/role.rb
|
59
|
-
- lib/rabarber/permissions.rb
|
60
62
|
- lib/rabarber/railtie.rb
|
61
|
-
- lib/rabarber/rule.rb
|
62
63
|
- lib/rabarber/version.rb
|
63
64
|
- rabarber.gemspec
|
64
65
|
homepage: https://github.com/enjaku4/rabarber
|
65
66
|
licenses:
|
66
67
|
- MIT
|
67
|
-
metadata:
|
68
|
+
metadata:
|
69
|
+
rubygems_mfa_required: 'true'
|
68
70
|
post_install_message:
|
69
71
|
rdoc_options: []
|
70
72
|
require_paths:
|
@@ -80,8 +82,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
80
82
|
- !ruby/object:Gem::Version
|
81
83
|
version: '0'
|
82
84
|
requirements: []
|
83
|
-
rubygems_version: 3.
|
85
|
+
rubygems_version: 3.2.33
|
84
86
|
signing_key:
|
85
87
|
specification_version: 4
|
86
|
-
summary: Simple authorization library for Ruby on Rails.
|
88
|
+
summary: Simple role-based authorization library for Ruby on Rails.
|
87
89
|
test_files: []
|
data/lib/rabarber/access.rb
DELETED
@@ -1,26 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module Rabarber
|
4
|
-
module Access
|
5
|
-
def access_granted?(roles, controller, action, dynamic_rule_receiver)
|
6
|
-
controller_accessible?(roles, controller, dynamic_rule_receiver) ||
|
7
|
-
action_accessible?(roles, controller, action, dynamic_rule_receiver)
|
8
|
-
end
|
9
|
-
|
10
|
-
def controller_accessible?(roles, controller, dynamic_rule_receiver)
|
11
|
-
accessible_controllers(roles, dynamic_rule_receiver).any? do |accessible_controller|
|
12
|
-
controller <= accessible_controller
|
13
|
-
end
|
14
|
-
end
|
15
|
-
|
16
|
-
def action_accessible?(roles, controller, action, dynamic_rule_receiver)
|
17
|
-
action_rules[controller].any? { |rule| rule.verify_access(roles, dynamic_rule_receiver, action) }
|
18
|
-
end
|
19
|
-
|
20
|
-
private
|
21
|
-
|
22
|
-
def accessible_controllers(roles, dynamic_rule_receiver)
|
23
|
-
controller_rules.select { |_, rule| rule.verify_access(roles, dynamic_rule_receiver) }.keys
|
24
|
-
end
|
25
|
-
end
|
26
|
-
end
|
@@ -1,30 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module Rabarber
|
4
|
-
module Input
|
5
|
-
class Actions < Rabarber::Input::Base
|
6
|
-
def initialize(
|
7
|
-
value,
|
8
|
-
error_type = Rabarber::InvalidArgumentError,
|
9
|
-
error_message = "Action name must be a Symbol or a String"
|
10
|
-
)
|
11
|
-
super
|
12
|
-
end
|
13
|
-
|
14
|
-
private
|
15
|
-
|
16
|
-
def valid?
|
17
|
-
(value.is_a?(String) || value.is_a?(Symbol)) && value.present? || value.nil?
|
18
|
-
end
|
19
|
-
|
20
|
-
def processed_value
|
21
|
-
case value
|
22
|
-
when String, Symbol
|
23
|
-
value.to_sym
|
24
|
-
when nil
|
25
|
-
value
|
26
|
-
end
|
27
|
-
end
|
28
|
-
end
|
29
|
-
end
|
30
|
-
end
|
@@ -1,30 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module Rabarber
|
4
|
-
module Input
|
5
|
-
class DynamicRules < Rabarber::Input::Base
|
6
|
-
def initialize(
|
7
|
-
value,
|
8
|
-
error_type = Rabarber::InvalidArgumentError,
|
9
|
-
error_message = "Dynamic rule must be a Symbol, a String, or a Proc"
|
10
|
-
)
|
11
|
-
super
|
12
|
-
end
|
13
|
-
|
14
|
-
private
|
15
|
-
|
16
|
-
def valid?
|
17
|
-
(value.is_a?(String) || value.is_a?(Symbol)) && value.present? || value.nil? || value.is_a?(Proc)
|
18
|
-
end
|
19
|
-
|
20
|
-
def processed_value
|
21
|
-
case value
|
22
|
-
when String, Symbol
|
23
|
-
value.to_sym
|
24
|
-
when Proc, nil
|
25
|
-
value
|
26
|
-
end
|
27
|
-
end
|
28
|
-
end
|
29
|
-
end
|
30
|
-
end
|
data/lib/rabarber/permissions.rb
DELETED
@@ -1,38 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require_relative "access"
|
4
|
-
require_relative "rule"
|
5
|
-
|
6
|
-
module Rabarber
|
7
|
-
class Permissions
|
8
|
-
include Singleton
|
9
|
-
|
10
|
-
extend Access
|
11
|
-
|
12
|
-
attr_reader :storage
|
13
|
-
|
14
|
-
def initialize
|
15
|
-
@storage = { controller_rules: Hash.new({}), action_rules: Hash.new([]) }
|
16
|
-
end
|
17
|
-
|
18
|
-
class << self
|
19
|
-
def add(controller, action, roles, dynamic_rule, negated_dynamic_rule)
|
20
|
-
rule = Rabarber::Rule.new(action, roles, dynamic_rule, negated_dynamic_rule)
|
21
|
-
|
22
|
-
if action
|
23
|
-
instance.storage[:action_rules][controller] += [rule]
|
24
|
-
else
|
25
|
-
instance.storage[:controller_rules][controller] = rule
|
26
|
-
end
|
27
|
-
end
|
28
|
-
|
29
|
-
def controller_rules
|
30
|
-
instance.storage[:controller_rules]
|
31
|
-
end
|
32
|
-
|
33
|
-
def action_rules
|
34
|
-
instance.storage[:action_rules]
|
35
|
-
end
|
36
|
-
end
|
37
|
-
end
|
38
|
-
end
|
data/lib/rabarber/rule.rb
DELETED
@@ -1,48 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module Rabarber
|
4
|
-
class Rule
|
5
|
-
attr_reader :action, :roles, :dynamic_rule, :negated_dynamic_rule
|
6
|
-
|
7
|
-
def initialize(action, roles, dynamic_rule, negated_dynamic_rule)
|
8
|
-
@action = action
|
9
|
-
@roles = Array(roles)
|
10
|
-
@dynamic_rule = dynamic_rule
|
11
|
-
@negated_dynamic_rule = negated_dynamic_rule
|
12
|
-
end
|
13
|
-
|
14
|
-
def verify_access(user_roles, dynamic_rule_receiver, action_name = nil)
|
15
|
-
action_accessible?(action_name) && roles_permitted?(user_roles) && dynamic_rule_followed?(dynamic_rule_receiver)
|
16
|
-
end
|
17
|
-
|
18
|
-
def action_accessible?(action_name)
|
19
|
-
action_name.nil? || action_name == action
|
20
|
-
end
|
21
|
-
|
22
|
-
def roles_permitted?(user_roles)
|
23
|
-
return false if Rabarber::Configuration.instance.must_have_roles && user_roles.empty?
|
24
|
-
|
25
|
-
roles.empty? || (roles & user_roles).any?
|
26
|
-
end
|
27
|
-
|
28
|
-
def dynamic_rule_followed?(dynamic_rule_receiver)
|
29
|
-
!!(execute_dynamic_rule(dynamic_rule_receiver, false) && execute_dynamic_rule(dynamic_rule_receiver, true))
|
30
|
-
end
|
31
|
-
|
32
|
-
private
|
33
|
-
|
34
|
-
def execute_dynamic_rule(dynamic_rule_receiver, is_negated)
|
35
|
-
rule = is_negated ? negated_dynamic_rule : dynamic_rule
|
36
|
-
|
37
|
-
return true if rule.nil?
|
38
|
-
|
39
|
-
result = if rule.is_a?(Proc)
|
40
|
-
dynamic_rule_receiver.instance_exec(&rule)
|
41
|
-
else
|
42
|
-
dynamic_rule_receiver.send(rule)
|
43
|
-
end
|
44
|
-
|
45
|
-
is_negated ? !result : result
|
46
|
-
end
|
47
|
-
end
|
48
|
-
end
|