rabarber 1.4.1 → 2.0.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 +22 -0
- data/README.md +122 -66
- data/lib/generators/rabarber/roles_generator.rb +2 -0
- data/lib/generators/rabarber/templates/create_rabarber_roles.rb.erb +3 -3
- data/lib/rabarber/audit/events/base.rb +64 -0
- data/lib/rabarber/audit/events/roles_assigned.rb +35 -0
- data/lib/rabarber/audit/events/roles_revoked.rb +35 -0
- data/lib/rabarber/audit/events/unauthorized_attempt.rb +31 -0
- data/lib/rabarber/audit/logger.rb +23 -0
- data/lib/rabarber/configuration.rb +3 -47
- data/lib/rabarber/controllers/concerns/authorization.rb +9 -11
- data/lib/rabarber/core/access.rb +3 -1
- data/lib/rabarber/core/cache.rb +42 -0
- data/lib/rabarber/core/permissions.rb +2 -0
- data/lib/rabarber/core/permissions_integrity_checker.rb +39 -0
- data/lib/rabarber/core/roleable.rb +15 -0
- data/lib/rabarber/core/rule.rb +5 -9
- data/lib/rabarber/helpers/helpers.rb +4 -2
- data/lib/rabarber/models/concerns/has_roles.rb +6 -14
- data/lib/rabarber/models/role.rb +5 -12
- data/lib/rabarber/railtie.rb +1 -7
- data/lib/rabarber/version.rb +1 -1
- data/lib/rabarber.rb +9 -9
- data/rabarber.gemspec +2 -2
- metadata +19 -7
- data/lib/rabarber/cache.rb +0 -29
- data/lib/rabarber/logger.rb +0 -40
- data/lib/rabarber/missing/actions.rb +0 -24
- data/lib/rabarber/missing/base.rb +0 -61
- data/lib/rabarber/missing/roles.rb +0 -35
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: f719eb670170521e94a58a1c75d30110699ab941da6d5dae151fbf53e0eb9880
|
4
|
+
data.tar.gz: bd06646e15ab2eb5ff7b49d2a18316c4b539b95aa98bd2ec1b4de5c6b5fb921a
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 0db86c7d46337decbe9972ea3b0c1ad30e1c66f2b76b3ef27a494ef8c2ecc9bcf35ce3f135360e74045b7f20beb38149f5ca308af0cb3d3a0db829ff9dc7147b
|
7
|
+
data.tar.gz: d1a20c732318d12ccfe49338df7338dcf09bd1222fd574e8581d035cb38b55295e501bfa45ef63ea0a556a8c21a28abdad7d06ca765c3f14eed6e6cd98db93be
|
data/CHANGELOG.md
CHANGED
@@ -1,3 +1,25 @@
|
|
1
|
+
## v2.0.0
|
2
|
+
|
3
|
+
### Breaking:
|
4
|
+
|
5
|
+
- Removed `when_actions_missing` and `when_roles_missing` configuration options
|
6
|
+
- Replaced `when_unauthorized` configuration option with an overridable controller method
|
7
|
+
- Renamed `Rabarber::Role.assignees_for` method to `Rabarber::Role.assignees`
|
8
|
+
|
9
|
+
To upgrade to v2.0.0, please refer to the [migration guide](https://github.com/enjaku4/rabarber/discussions/52).
|
10
|
+
|
11
|
+
### Features:
|
12
|
+
|
13
|
+
- Added support for UUID primary keys
|
14
|
+
|
15
|
+
### Bugs:
|
16
|
+
|
17
|
+
- Fixed the issue where an error would occur if the user was not authenticated
|
18
|
+
|
19
|
+
### Misc:
|
20
|
+
|
21
|
+
- Significant refactoring and code improvements
|
22
|
+
|
1
23
|
## v1.4.1
|
2
24
|
|
3
25
|
- Fix an issue where an error could be raised when using controller-wide dynamic rules
|
data/README.md
CHANGED
@@ -9,7 +9,7 @@ Rabarber is a role-based authorization library for Ruby on Rails, primarily desi
|
|
9
9
|
|
10
10
|
**Example of Usage**:
|
11
11
|
|
12
|
-
Consider a CRM where users with different roles have distinct access levels. For instance, the role `accountant` can interact with invoices but cannot access marketing information, while the role `marketer` has access to marketing-related data. Such authorization rules can be easily defined with Rabarber.
|
12
|
+
Consider a CRM system where users with different roles have distinct access levels. For instance, the role `accountant` can interact with invoices but cannot access marketing information, while the role `marketer` has access to marketing-related data. Such authorization rules can be easily defined with Rabarber.
|
13
13
|
|
14
14
|
---
|
15
15
|
|
@@ -21,39 +21,65 @@ class TicketsController < ApplicationController
|
|
21
21
|
|
22
22
|
grant_access action: :index, roles: :manager
|
23
23
|
def index
|
24
|
-
...
|
24
|
+
# ...
|
25
25
|
end
|
26
26
|
|
27
27
|
def delete
|
28
|
-
...
|
28
|
+
# ...
|
29
29
|
end
|
30
30
|
end
|
31
31
|
```
|
32
32
|
This means that `admin` users can access everything in `TicketsController`, while `manager` role can access only `index` action.
|
33
33
|
|
34
|
+
## Table of Contents
|
35
|
+
|
36
|
+
**Gem usage:**
|
37
|
+
- [Installation](#installation)
|
38
|
+
- [Configuration](#configuration)
|
39
|
+
- [Roles](#roles)
|
40
|
+
- [Authorization Rules](#authorization-rules)
|
41
|
+
- [Dynamic Authorization Rules](#dynamic-authorization-rules)
|
42
|
+
- [When Unauthorized](#when-unauthorized)
|
43
|
+
- [View Helpers](#view-helpers)
|
44
|
+
- [Audit Trail](#audit-trail)
|
45
|
+
|
46
|
+
**Community Resources:**
|
47
|
+
- [Problems?](#problems)
|
48
|
+
- [Contributing](#contributing)
|
49
|
+
- [Code of Conduct](#code-of-conduct)
|
50
|
+
|
51
|
+
**Legal:**
|
52
|
+
- [License](#license)
|
53
|
+
|
34
54
|
## Installation
|
35
55
|
|
36
56
|
Add the Rabarber gem to your Gemfile:
|
37
57
|
|
38
|
-
```
|
58
|
+
```rb
|
39
59
|
gem "rabarber"
|
40
60
|
```
|
41
61
|
|
42
62
|
Install the gem:
|
43
63
|
|
44
|
-
```
|
64
|
+
```shell
|
45
65
|
bundle install
|
46
66
|
```
|
47
67
|
|
48
68
|
Next, generate a migration to create tables for storing roles in the database. Make sure to specify the table name of the model representing users in your application as an argument. For instance, if the table name is `users`, run:
|
49
69
|
|
50
|
-
```
|
70
|
+
```shell
|
51
71
|
rails g rabarber:roles users
|
52
72
|
```
|
53
73
|
|
54
|
-
|
74
|
+
Rabarber supports UUIDs as primary keys. If your application uses UUIDs, add `--uuid` option to the generator:
|
55
75
|
|
76
|
+
```shell
|
77
|
+
rails g rabarber:roles users --uuid
|
56
78
|
```
|
79
|
+
|
80
|
+
Finally, run the migration to apply the changes to the database:
|
81
|
+
|
82
|
+
```shell
|
57
83
|
rails db:migrate
|
58
84
|
```
|
59
85
|
|
@@ -67,28 +93,13 @@ Rabarber.configure do |config|
|
|
67
93
|
config.cache_enabled = true
|
68
94
|
config.current_user_method = :current_user
|
69
95
|
config.must_have_roles = false
|
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
|
-
}
|
77
96
|
end
|
78
97
|
```
|
79
98
|
|
80
|
-
- `audit_trail_enabled`
|
81
|
-
- `cache_enabled`
|
82
|
-
- `current_user_method`
|
83
|
-
- `must_have_roles`
|
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._
|
85
|
-
|
86
|
-
### Deprecated Configuration Options
|
87
|
-
|
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)):
|
89
|
-
|
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._
|
91
|
-
- `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._
|
99
|
+
- `audit_trail_enabled` determines whether the audit trail functionality is enabled. The audit trail is enabled by default.
|
100
|
+
- `cache_enabled` determines whether roles are cached to avoid unnecessary database queries. Roles are cached by default. If you need to clear the cache, use `Rabarber::Cache.clear` method.
|
101
|
+
- `current_user_method` represents the method that returns the currently authenticated user. The default value is `:current_user`.
|
102
|
+
- `must_have_roles` determines whether a user with no roles can access endpoints permitted to everyone. The default value is `false` (allowing users without roles to access such endpoints).
|
92
103
|
|
93
104
|
## Roles
|
94
105
|
|
@@ -97,7 +108,7 @@ Include `Rabarber::HasRoles` module in your model representing users in your app
|
|
97
108
|
```rb
|
98
109
|
class User < ApplicationRecord
|
99
110
|
include Rabarber::HasRoles
|
100
|
-
...
|
111
|
+
# ...
|
101
112
|
end
|
102
113
|
```
|
103
114
|
|
@@ -123,7 +134,7 @@ To revoke roles, use:
|
|
123
134
|
```rb
|
124
135
|
user.revoke_roles(:accountant, :marketer)
|
125
136
|
```
|
126
|
-
If
|
137
|
+
If the user doesn't have the role you want to revoke, it will be ignored.
|
127
138
|
|
128
139
|
The method returns an array of roles assigned to the user.
|
129
140
|
|
@@ -139,7 +150,7 @@ It returns `true` if the user has at least one role and `false` otherwise.
|
|
139
150
|
|
140
151
|
**`#roles`**
|
141
152
|
|
142
|
-
To
|
153
|
+
To get all the roles assigned to the user, use:
|
143
154
|
|
144
155
|
```rb
|
145
156
|
user.roles
|
@@ -149,7 +160,7 @@ user.roles
|
|
149
160
|
|
150
161
|
To manipulate roles directly, you can use `Rabarber::Role` methods:
|
151
162
|
|
152
|
-
**`.add(
|
163
|
+
**`.add(role_name)`**
|
153
164
|
|
154
165
|
To add a new role, use:
|
155
166
|
|
@@ -166,14 +177,14 @@ To rename a role, use:
|
|
166
177
|
```rb
|
167
178
|
Rabarber::Role.rename(:admin, :administrator)
|
168
179
|
```
|
169
|
-
The first argument is the old name, and the second argument is the new name. This will rename the role and return `true`. If
|
180
|
+
The first argument is the old name, and the second argument is the new name. This will rename the role and return `true`. If the role with a new name already exists, it will return `false`.
|
170
181
|
|
171
182
|
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:
|
172
183
|
```rb
|
173
184
|
Rabarber::Role.rename(:admin, :administrator, force: true)
|
174
185
|
```
|
175
186
|
|
176
|
-
**`.remove(
|
187
|
+
**`.remove(role_name, force: false)`**
|
177
188
|
|
178
189
|
To remove a role, use:
|
179
190
|
|
@@ -196,12 +207,12 @@ If you need to list all the role names available in your application, use:
|
|
196
207
|
Rabarber::Role.names
|
197
208
|
```
|
198
209
|
|
199
|
-
**`.
|
210
|
+
**`.assignees(role_name)`**
|
200
211
|
|
201
212
|
To get all the users to whom the role is assigned, use:
|
202
213
|
|
203
214
|
```rb
|
204
|
-
Rabarber::Role.
|
215
|
+
Rabarber::Role.assignees(:admin)
|
205
216
|
```
|
206
217
|
|
207
218
|
## Authorization Rules
|
@@ -211,7 +222,7 @@ Include `Rabarber::Authorization` module into the controller that needs authoriz
|
|
211
222
|
```rb
|
212
223
|
class ApplicationController < ActionController::Base
|
213
224
|
include Rabarber::Authorization
|
214
|
-
...
|
225
|
+
# ...
|
215
226
|
end
|
216
227
|
```
|
217
228
|
This adds `.grant_access(action: nil, roles: nil, if: nil, unless: nil)` method which allows you to define the authorization rules.
|
@@ -219,22 +230,24 @@ This adds `.grant_access(action: nil, roles: nil, if: nil, unless: nil)` method
|
|
219
230
|
The most basic usage of the method is as follows:
|
220
231
|
|
221
232
|
```rb
|
222
|
-
class InvoicesController < ApplicationController
|
233
|
+
class Crm::InvoicesController < ApplicationController
|
223
234
|
grant_access action: :index, roles: [:accountant, :admin]
|
224
235
|
def index
|
225
236
|
@invoices = Invoice.all
|
226
237
|
@invoices = @invoices.paid if current_user.has_role?(:accountant)
|
227
|
-
...
|
238
|
+
# ...
|
228
239
|
end
|
229
240
|
|
230
241
|
grant_access action: :destroy, roles: :admin
|
231
242
|
def destroy
|
232
|
-
...
|
243
|
+
# ...
|
233
244
|
end
|
234
245
|
end
|
235
246
|
```
|
236
247
|
This grants access to `index` action for users with `accountant` or `admin` role, and access to `destroy` action for `admin` users only.
|
237
248
|
|
249
|
+
Please note that Rabarber does not provide any built-in data scoping mechanism as it is not a part of the authorization layer and is not necessarily role specific or has anything to do with the current user. The business logic can vary drastically depending on the application, so you're encouraged to limit the data visibility yourself, for example, in the same way as in the example above, where `accountant` role can only see paid invoices.
|
250
|
+
|
238
251
|
You can also define controller-wide rules (without `action` argument):
|
239
252
|
|
240
253
|
```rb
|
@@ -243,18 +256,18 @@ class Crm::BaseController < ApplicationController
|
|
243
256
|
|
244
257
|
grant_access action: :dashboard, roles: :marketer
|
245
258
|
def dashboard
|
246
|
-
...
|
259
|
+
# ...
|
247
260
|
end
|
248
261
|
end
|
249
262
|
|
250
263
|
class Crm::InvoicesController < Crm::BaseController
|
251
264
|
grant_access roles: :accountant
|
252
265
|
def index
|
253
|
-
...
|
266
|
+
# ...
|
254
267
|
end
|
255
268
|
|
256
269
|
def delete
|
257
|
-
...
|
270
|
+
# ...
|
258
271
|
end
|
259
272
|
end
|
260
273
|
```
|
@@ -265,31 +278,47 @@ Roles can also be omitted:
|
|
265
278
|
```rb
|
266
279
|
class OrdersController < ApplicationController
|
267
280
|
grant_access
|
268
|
-
...
|
281
|
+
# ...
|
269
282
|
end
|
270
283
|
|
271
284
|
class InvoicesController < ApplicationController
|
272
285
|
grant_access action: :index
|
273
286
|
def index
|
274
|
-
...
|
287
|
+
# ...
|
275
288
|
end
|
276
289
|
end
|
277
290
|
```
|
278
291
|
|
279
292
|
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`.
|
280
293
|
|
281
|
-
|
294
|
+
If the user is not authenticated (the method responsible for returning the currently authenticated user in your application returns `nil`), Rabarber will handle this situation as if the user has no roles.
|
295
|
+
|
296
|
+
If you've set `must_have_roles` setting to `true`, then only the users with at least one role can gain access. This setting can be useful if your requirements are such that users without roles (or unauthenticated users) are not allowed to access anything.
|
297
|
+
|
298
|
+
Also keep in mind that rules defined in child classes don't override parent rules but rather add to them:
|
299
|
+
```rb
|
300
|
+
class Crm::BaseController < ApplicationController
|
301
|
+
grant_access roles: :admin
|
302
|
+
# ...
|
303
|
+
end
|
304
|
+
|
305
|
+
class Crm::InvoicesController < Crm::BaseController
|
306
|
+
grant_access roles: :accountant
|
307
|
+
# ...
|
308
|
+
end
|
309
|
+
```
|
310
|
+
This means that `Crm::InvoicesController` is still accessible to `admin` but is also accessible to `accountant`.
|
282
311
|
|
283
|
-
|
312
|
+
## Dynamic Authorization Rules
|
284
313
|
|
285
314
|
For more complex cases, Rabarber provides dynamic rules:
|
286
315
|
|
287
316
|
```rb
|
288
|
-
class OrdersController < ApplicationController
|
317
|
+
class Crm::OrdersController < ApplicationController
|
289
318
|
grant_access roles: :manager, if: :company_manager?, unless: :fired?
|
290
319
|
|
291
320
|
def index
|
292
|
-
...
|
321
|
+
# ...
|
293
322
|
end
|
294
323
|
|
295
324
|
private
|
@@ -303,7 +332,7 @@ class OrdersController < ApplicationController
|
|
303
332
|
end
|
304
333
|
end
|
305
334
|
|
306
|
-
class InvoicesController < ApplicationController
|
335
|
+
class Crm::InvoicesController < ApplicationController
|
307
336
|
grant_access roles: :senior_accountant
|
308
337
|
|
309
338
|
grant_access action: :index, roles: [:secretary, :accountant], if: -> { InvoicesPolicy.new(current_user).can_access?(:index) }
|
@@ -311,30 +340,57 @@ class InvoicesController < ApplicationController
|
|
311
340
|
@invoices = Invoice.all
|
312
341
|
@invoices = @invoices.where("total < 10000") if current_user.has_role?(:accountant)
|
313
342
|
@invoices = @invoices.unpaid if current_user.has_role?(:secretary)
|
314
|
-
...
|
343
|
+
# ...
|
315
344
|
end
|
316
345
|
|
317
346
|
grant_access action: :show, roles: :accountant, unless: -> { Invoice.find(params[:id]).total > 10_000 }
|
318
347
|
def show
|
319
|
-
...
|
348
|
+
# ...
|
320
349
|
end
|
321
350
|
end
|
322
351
|
```
|
323
|
-
You can pass a dynamic rule as `if` or `unless` argument. It can be a symbol, in which case the method with
|
352
|
+
You can pass a dynamic rule as `if` or `unless` argument. It can be a symbol, in which case the method with that name will be called. Alternatively, it can be a proc, which will be executed within the context of the controller's instance.
|
324
353
|
|
325
|
-
|
354
|
+
You can use only dynamic rules without specifying roles if that suits your needs:
|
326
355
|
```rb
|
327
|
-
class
|
328
|
-
grant_access
|
329
|
-
|
356
|
+
class InvoicesController < ApplicationController
|
357
|
+
grant_access action: :index, if: -> { current_user.company == Company.find(params[:company_id]) }
|
358
|
+
def index
|
359
|
+
# ...
|
360
|
+
end
|
361
|
+
end
|
362
|
+
```
|
363
|
+
This basically allows you to use Rabarber as a policy-based authorization library by calling your own custom policy within a dynamic rule:
|
364
|
+
```rb
|
365
|
+
class InvoicesController < ApplicationController
|
366
|
+
grant_access action: :index, if: -> { InvoicesPolicy.new(current_user).can_access?(:index) }
|
367
|
+
def index
|
368
|
+
# ...
|
369
|
+
end
|
330
370
|
end
|
371
|
+
```
|
331
372
|
|
332
|
-
|
333
|
-
|
334
|
-
|
373
|
+
## When Unauthorized
|
374
|
+
|
375
|
+
By default, in the event of an unauthorized attempt, Rabarber redirects the user back if the request format is HTML (with fallback to the root path), and returns a 401 (Unauthorized) status code otherwise.
|
376
|
+
|
377
|
+
This behavior can be customized by overriding private `when_unauthorized` method:
|
378
|
+
|
379
|
+
```rb
|
380
|
+
class ApplicationController < ActionController::Base
|
381
|
+
include Rabarber::Authorization
|
382
|
+
|
383
|
+
# ...
|
384
|
+
|
385
|
+
private
|
386
|
+
|
387
|
+
def when_unauthorized
|
388
|
+
head :not_found # pretend the page doesn't exist
|
389
|
+
end
|
335
390
|
end
|
336
391
|
```
|
337
|
-
|
392
|
+
|
393
|
+
The method can be overridden in different controllers, providing flexibility in handling unauthorized access attempts.
|
338
394
|
|
339
395
|
## View Helpers
|
340
396
|
|
@@ -343,7 +399,7 @@ Rabarber also provides a couple of helpers that can be used in views: `visible_t
|
|
343
399
|
```rb
|
344
400
|
module ApplicationHelper
|
345
401
|
include Rabarber::Helpers
|
346
|
-
...
|
402
|
+
# ...
|
347
403
|
end
|
348
404
|
```
|
349
405
|
|
@@ -375,7 +431,7 @@ The logs are written to the file `log/rabarber_audit.log` unless the `audit_trai
|
|
375
431
|
|
376
432
|
Facing a problem or want to suggest an enhancement?
|
377
433
|
|
378
|
-
- **Open a Discussion**: If you have a question, experience difficulties using the gem, or have
|
434
|
+
- **Open a Discussion**: If you have a question, experience difficulties using the gem, or have a suggestion for improvements, feel free to use the Discussions section.
|
379
435
|
|
380
436
|
Encountered a bug?
|
381
437
|
|
@@ -384,12 +440,12 @@ Encountered a bug?
|
|
384
440
|
|
385
441
|
## Contributing
|
386
442
|
|
387
|
-
Before
|
388
|
-
|
389
|
-
## License
|
390
|
-
|
391
|
-
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
443
|
+
Before creating an issue or a pull request, please read the [contributing guidelines](https://github.com/enjaku4/rabarber/blob/main/CONTRIBUTING.md).
|
392
444
|
|
393
445
|
## Code of Conduct
|
394
446
|
|
395
447
|
Everyone interacting in the Rabarber project is expected to follow the [code of conduct](https://github.com/enjaku4/rabarber/blob/main/CODE_OF_CONDUCT.md).
|
448
|
+
|
449
|
+
## License
|
450
|
+
|
451
|
+
The gem is available as open source under the terms of the [MIT License](https://github.com/enjaku4/rabarber/blob/main/LICENSE.txt).
|
@@ -10,6 +10,8 @@ module Rabarber
|
|
10
10
|
|
11
11
|
argument :table_name, type: :string, required: true
|
12
12
|
|
13
|
+
class_option :uuid, type: :boolean, default: false, desc: "Use UUIDs as primary keys"
|
14
|
+
|
13
15
|
def create_migrations
|
14
16
|
migration_template "create_rabarber_roles.rb.erb", "db/migrate/create_rabarber_roles.rb"
|
15
17
|
end
|
@@ -2,14 +2,14 @@
|
|
2
2
|
|
3
3
|
class CreateRabarberRoles < ActiveRecord::Migration[<%= ActiveRecord::Migration.current_version.to_s %>]
|
4
4
|
def change
|
5
|
-
create_table :rabarber_roles do |t|
|
5
|
+
create_table :rabarber_roles<%= ", id: :uuid" if options[:uuid] %> do |t|
|
6
6
|
t.string :name, null: false, index: { unique: true }
|
7
7
|
t.timestamps
|
8
8
|
end
|
9
9
|
|
10
10
|
create_table :rabarber_roles_roleables, id: false do |t|
|
11
|
-
t.belongs_to :role, null: false, index: true, foreign_key: { to_table: :rabarber_roles }
|
12
|
-
t.belongs_to :roleable, null: false, index: true, foreign_key: { to_table: <%= table_name.to_sym.inspect %> }
|
11
|
+
t.belongs_to :role, null: false, index: true, foreign_key: { to_table: :rabarber_roles }<%= ", type: :uuid" if options[:uuid] %>
|
12
|
+
t.belongs_to :roleable, null: false, index: true, foreign_key: { to_table: <%= table_name.to_sym.inspect %> }<%= ", type: :uuid" if options[:uuid] %>
|
13
13
|
end
|
14
14
|
|
15
15
|
add_index :rabarber_roles_roleables, [:role_id, :roleable_id], unique: true
|
@@ -0,0 +1,64 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "../logger"
|
4
|
+
|
5
|
+
module Rabarber
|
6
|
+
module Audit
|
7
|
+
module Events
|
8
|
+
class Base
|
9
|
+
attr_reader :roleable, :specifics
|
10
|
+
|
11
|
+
def self.trigger(roleable, specifics)
|
12
|
+
new(roleable, specifics).send(:log)
|
13
|
+
end
|
14
|
+
|
15
|
+
private
|
16
|
+
|
17
|
+
def initialize(roleable, specifics)
|
18
|
+
raise ArgumentError, "Roleable is required for #{self.class} event" if roleable.nil? && !nil_roleable_allowed?
|
19
|
+
|
20
|
+
@roleable = roleable
|
21
|
+
@specifics = specifics
|
22
|
+
end
|
23
|
+
|
24
|
+
def nil_roleable_allowed?
|
25
|
+
raise NotImplementedError
|
26
|
+
end
|
27
|
+
|
28
|
+
def log
|
29
|
+
Rabarber::Audit::Logger.log(log_level, message)
|
30
|
+
end
|
31
|
+
|
32
|
+
def log_level
|
33
|
+
raise NotImplementedError
|
34
|
+
end
|
35
|
+
|
36
|
+
def message
|
37
|
+
raise NotImplementedError
|
38
|
+
end
|
39
|
+
|
40
|
+
def identity
|
41
|
+
roleable_identity(with_roles: identity_with_roles?)
|
42
|
+
end
|
43
|
+
|
44
|
+
def identity_with_roles?
|
45
|
+
raise NotImplementedError
|
46
|
+
end
|
47
|
+
|
48
|
+
def roleable_identity(with_roles:)
|
49
|
+
if roleable
|
50
|
+
model_name = roleable.model_name.human
|
51
|
+
primary_key = roleable.class.primary_key
|
52
|
+
roleable_id = roleable.public_send(primary_key)
|
53
|
+
|
54
|
+
roles = with_roles ? ", roles: #{roleable.roles}" : ""
|
55
|
+
|
56
|
+
"#{model_name} with #{primary_key}: '#{roleable_id}'#{roles}"
|
57
|
+
else
|
58
|
+
"Unauthenticated #{Rabarber::HasRoles.roleable_class.model_name.human.downcase}"
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Rabarber
|
4
|
+
module Audit
|
5
|
+
module Events
|
6
|
+
class RolesAssigned < Base
|
7
|
+
private
|
8
|
+
|
9
|
+
def nil_roleable_allowed?
|
10
|
+
false
|
11
|
+
end
|
12
|
+
|
13
|
+
def log_level
|
14
|
+
:info
|
15
|
+
end
|
16
|
+
|
17
|
+
def message
|
18
|
+
"[Role Assignment] #{identity} has been assigned the following roles: #{roles_to_assign}, current roles: #{current_roles}"
|
19
|
+
end
|
20
|
+
|
21
|
+
def identity_with_roles?
|
22
|
+
false
|
23
|
+
end
|
24
|
+
|
25
|
+
def roles_to_assign
|
26
|
+
specifics.fetch(:roles_to_assign)
|
27
|
+
end
|
28
|
+
|
29
|
+
def current_roles
|
30
|
+
specifics.fetch(:current_roles)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Rabarber
|
4
|
+
module Audit
|
5
|
+
module Events
|
6
|
+
class RolesRevoked < Base
|
7
|
+
private
|
8
|
+
|
9
|
+
def nil_roleable_allowed?
|
10
|
+
false
|
11
|
+
end
|
12
|
+
|
13
|
+
def log_level
|
14
|
+
:info
|
15
|
+
end
|
16
|
+
|
17
|
+
def message
|
18
|
+
"[Role Revocation] #{identity} has been revoked from the following roles: #{roles_to_revoke}, current roles: #{current_roles}"
|
19
|
+
end
|
20
|
+
|
21
|
+
def identity_with_roles?
|
22
|
+
false
|
23
|
+
end
|
24
|
+
|
25
|
+
def roles_to_revoke
|
26
|
+
specifics.fetch(:roles_to_revoke)
|
27
|
+
end
|
28
|
+
|
29
|
+
def current_roles
|
30
|
+
specifics.fetch(:current_roles)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Rabarber
|
4
|
+
module Audit
|
5
|
+
module Events
|
6
|
+
class UnauthorizedAttempt < Base
|
7
|
+
private
|
8
|
+
|
9
|
+
def nil_roleable_allowed?
|
10
|
+
true
|
11
|
+
end
|
12
|
+
|
13
|
+
def log_level
|
14
|
+
:warn
|
15
|
+
end
|
16
|
+
|
17
|
+
def message
|
18
|
+
"[Unauthorized Attempt] #{identity} attempted to access '#{path}'"
|
19
|
+
end
|
20
|
+
|
21
|
+
def identity_with_roles?
|
22
|
+
true
|
23
|
+
end
|
24
|
+
|
25
|
+
def path
|
26
|
+
specifics.fetch(:path)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "singleton"
|
4
|
+
|
5
|
+
module Rabarber
|
6
|
+
module Audit
|
7
|
+
class Logger
|
8
|
+
include Singleton
|
9
|
+
|
10
|
+
attr_reader :logger
|
11
|
+
|
12
|
+
def initialize
|
13
|
+
@logger = ::Logger.new(Rails.root.join("log/rabarber_audit.log"))
|
14
|
+
end
|
15
|
+
|
16
|
+
def self.log(log_level, message)
|
17
|
+
return unless Rabarber::Configuration.instance.audit_trail_enabled
|
18
|
+
|
19
|
+
instance.logger.public_send(log_level, message)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|