rabarber 5.0.0 → 5.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +11 -0
- data/README.md +184 -397
- data/lib/rabarber/controllers/concerns/authorization.rb +3 -3
- data/lib/rabarber/models/concerns/has_roles.rb +14 -4
- data/lib/rabarber/models/role.rb +0 -5
- data/lib/rabarber/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 874cd3840269f288baec19fd5e240afdd6b06e3e5b81d703eb30e8e62283fb32
|
4
|
+
data.tar.gz: 117d060ffe53ae884b657c1d79a94d9762de1de8b6726659128d7be805c77e24
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: c2207e66e0743b7e5a81acfcc2707d00313fa2a8e9d8e652c7f0dc41b331d8cfa0c4d3504ff5e857606666f0145023a68e8c6d961d6285d48e36c435d4597a38
|
7
|
+
data.tar.gz: ea1ab97b91cb45611f4ca24a6418484a0b7634d00b0f96cec8485ba543500fc763a33f1e0cbaf7027a8a375623e91481ca1de74d47f50cf86fe5b91bfe456d0b
|
data/CHANGELOG.md
CHANGED
@@ -1,3 +1,14 @@
|
|
1
|
+
## v5.1.0
|
2
|
+
|
3
|
+
### Features:
|
4
|
+
|
5
|
+
- Added `revoke_all_roles` method to revoke all user roles at once
|
6
|
+
|
7
|
+
### Bugs:
|
8
|
+
|
9
|
+
- Fixed HTTP status code for unauthorized non-HTML requests from 401 to 403
|
10
|
+
- Fixed some error types for consistency
|
11
|
+
|
1
12
|
## v5.0.0
|
2
13
|
|
3
14
|
### Breaking:
|
data/README.md
CHANGED
@@ -1,60 +1,37 @@
|
|
1
|
-
# Rabarber:
|
1
|
+
# Rabarber: Role-Based Authorization for Rails
|
2
2
|
|
3
3
|
[](http://badge.fury.io/rb/rabarber)
|
4
4
|
[](https://github.com/brownboxdev/rabarber/actions/workflows/ci.yml)
|
5
5
|
|
6
|
-
Rabarber is a role-based authorization library for Ruby on Rails.
|
6
|
+
Rabarber is a role-based authorization library for Ruby on Rails that focuses on controller-level access control. Rather than answering domain questions like "can this user create a post?", Rabarber answers "can this user access the create post endpoint?", providing a clean separation between authorization and business logic.
|
7
7
|
|
8
|
-
|
8
|
+
**Key Features:**
|
9
9
|
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
And this is how your controller might look with Rabarber:
|
17
|
-
|
18
|
-
```rb
|
19
|
-
class TicketsController < ApplicationController
|
20
|
-
grant_access roles: :admin
|
21
|
-
|
22
|
-
grant_access action: :index, roles: :manager
|
23
|
-
def index
|
24
|
-
# ...
|
25
|
-
end
|
26
|
-
|
27
|
-
def delete
|
28
|
-
# ...
|
29
|
-
end
|
30
|
-
end
|
31
|
-
```
|
32
|
-
|
33
|
-
This means that `admin` users can access everything in `TicketsController`, while `manager` role can access only `index` action.
|
10
|
+
- Role-based authorization
|
11
|
+
- Controller-level access control
|
12
|
+
- Multi-tenancy support through contextual roles
|
13
|
+
- Dynamic authorization with conditional logic
|
14
|
+
- View helpers for role-based content rendering
|
34
15
|
|
35
16
|
## Table of Contents
|
36
17
|
|
37
|
-
**Gem
|
18
|
+
**Gem Usage:**
|
38
19
|
- [Installation](#installation)
|
39
20
|
- [Configuration](#configuration)
|
40
|
-
- [Role Assignments](#role-assignments)
|
41
21
|
- [Role Management](#role-management)
|
42
|
-
- [
|
43
|
-
- [
|
44
|
-
- [
|
45
|
-
- [
|
46
|
-
- [Skip Authorization](#skip-authorization)
|
22
|
+
- [User Role Methods](#user-role-methods)
|
23
|
+
- [Controller Authorization](#controller-authorization)
|
24
|
+
- [Dynamic Rules](#dynamic-rules)
|
25
|
+
- [Multi-tenancy / Context](#multi-tenancy--context)
|
47
26
|
- [View Helpers](#view-helpers)
|
48
27
|
|
28
|
+
|
49
29
|
**Community Resources:**
|
50
|
-
- [Problems?](#problems)
|
51
30
|
- [Contributing](#contributing)
|
31
|
+
- [License](#license)
|
52
32
|
- [Code of Conduct](#code-of-conduct)
|
53
33
|
- [Old Versions](#old-versions)
|
54
34
|
|
55
|
-
**Legal:**
|
56
|
-
- [License](#license)
|
57
|
-
|
58
35
|
## Installation
|
59
36
|
|
60
37
|
Add Rabarber to your Gemfile:
|
@@ -69,19 +46,17 @@ Install the gem:
|
|
69
46
|
bundle install
|
70
47
|
```
|
71
48
|
|
72
|
-
Generate
|
49
|
+
Generate the migration for role storage (replace `users` with your user table name if different):
|
73
50
|
|
74
51
|
```shell
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
Rabarber supports UUIDs as primary keys. If your application uses UUIDs, add `--uuid` option:
|
52
|
+
# For standard integer IDs
|
53
|
+
rails generate rabarber:roles users
|
79
54
|
|
80
|
-
|
81
|
-
rails
|
55
|
+
# For UUID primary keys
|
56
|
+
rails generate rabarber:roles users --uuid
|
82
57
|
```
|
83
58
|
|
84
|
-
|
59
|
+
Run the migration:
|
85
60
|
|
86
61
|
```shell
|
87
62
|
rails db:migrate
|
@@ -89,507 +64,323 @@ rails db:migrate
|
|
89
64
|
|
90
65
|
## Configuration
|
91
66
|
|
92
|
-
|
67
|
+
Configure Rabarber in an initializer if customization is needed:
|
93
68
|
|
94
69
|
```rb
|
95
70
|
Rabarber.configure do |config|
|
96
|
-
config.cache_enabled = true
|
97
|
-
config.current_user_method = :current_user
|
98
|
-
config.user_model_name = "User"
|
71
|
+
config.cache_enabled = true # Enable role caching (default: true)
|
72
|
+
config.current_user_method = :current_user # Method to access current user (default: :current_user)
|
73
|
+
config.user_model_name = "User" # User model name (default: "User")
|
99
74
|
end
|
100
75
|
```
|
101
76
|
|
102
|
-
|
103
|
-
- `current_user_method` defines the method used to access the currently authenticated user. Default is `:current_user`.
|
104
|
-
- `user_model_name` sets the name of the model representing the user in your application. Default is `"User"`.
|
105
|
-
|
106
|
-
## Role Assignments
|
107
|
-
|
108
|
-
Rabarber automatically augments your user model (defined via `user_model_name` configuration) with role-related methods.
|
109
|
-
|
110
|
-
**`#assign_roles(*roles, context: nil, create_new: true)`**
|
111
|
-
|
112
|
-
To assign roles, use:
|
77
|
+
To clear the role cache manually:
|
113
78
|
|
114
79
|
```rb
|
115
|
-
|
116
|
-
```
|
117
|
-
|
118
|
-
By default, it will automatically create any roles that don't exist. To assign only existing roles and prevent automatic creation, pass `create_new: false`:
|
119
|
-
|
120
|
-
```rb
|
121
|
-
user.assign_roles(:accountant, :marketer, create_new: false)
|
122
|
-
```
|
123
|
-
|
124
|
-
The method returns an array of roles assigned to the user.
|
125
|
-
|
126
|
-
**`#revoke_roles(*roles, context: nil)`**
|
127
|
-
|
128
|
-
To revoke roles, use:
|
129
|
-
|
130
|
-
```rb
|
131
|
-
user.revoke_roles(:accountant, :marketer)
|
132
|
-
```
|
133
|
-
|
134
|
-
Roles the user doesn’t have are ignored.
|
135
|
-
|
136
|
-
The method returns an array of roles assigned to the user.
|
137
|
-
|
138
|
-
**`#has_role?(*roles, context: nil)`**
|
139
|
-
|
140
|
-
To check whether the user has a role, use:
|
141
|
-
|
142
|
-
```rb
|
143
|
-
user.has_role?(:accountant, :marketer)
|
144
|
-
```
|
145
|
-
|
146
|
-
It returns `true` if the user has at least one of the given roles.
|
147
|
-
|
148
|
-
**`#roles(context: nil)`**
|
149
|
-
|
150
|
-
To get the list of roles assigned to the user, use:
|
151
|
-
|
152
|
-
```rb
|
153
|
-
user.roles
|
154
|
-
```
|
155
|
-
|
156
|
-
**`#all_roles`**
|
157
|
-
|
158
|
-
To get all roles assigned to the user, grouped by context, use:
|
159
|
-
|
160
|
-
```rb
|
161
|
-
user.all_roles
|
162
|
-
```
|
163
|
-
|
164
|
-
**`.assignees(role_name, context: nil)`**
|
165
|
-
|
166
|
-
To get all the users to whom the role is assigned, use:
|
167
|
-
|
168
|
-
```rb
|
169
|
-
Rabarber::Role.assignees(:admin)
|
80
|
+
Rabarber::Cache.clear
|
170
81
|
```
|
171
82
|
|
172
83
|
## Role Management
|
173
84
|
|
174
|
-
|
175
|
-
|
176
|
-
**`.add(role_name, context: nil)`**
|
177
|
-
|
178
|
-
To add a new role, use:
|
85
|
+
### Direct Role Operations
|
179
86
|
|
180
87
|
```rb
|
88
|
+
# Create a new role
|
181
89
|
Rabarber::Role.add(:admin)
|
182
|
-
```
|
183
|
-
|
184
|
-
This will create a new role with the specified name and return `true`. If the role already exists, it will return `false`.
|
185
90
|
|
186
|
-
|
187
|
-
|
188
|
-
To rename a role, use:
|
189
|
-
|
190
|
-
```rb
|
91
|
+
# Rename a role
|
191
92
|
Rabarber::Role.rename(:admin, :administrator)
|
192
|
-
|
93
|
+
Rabarber::Role.rename(:admin, :administrator, force: true) # Force if role is assigned to users
|
193
94
|
|
194
|
-
|
95
|
+
# Remove a role
|
96
|
+
Rabarber::Role.remove(:admin)
|
97
|
+
Rabarber::Role.remove(:admin, force: true) # Force if role is assigned to users
|
195
98
|
|
196
|
-
|
99
|
+
# List available roles
|
100
|
+
Rabarber::Role.names
|
101
|
+
Rabarber::Role.all_names # All roles grouped by context
|
197
102
|
|
198
|
-
|
199
|
-
Rabarber::Role.
|
103
|
+
# Get users assigned to a role
|
104
|
+
Rabarber::Role.assignees(:admin)
|
200
105
|
```
|
201
106
|
|
202
|
-
|
107
|
+
## User Role Methods
|
108
|
+
|
109
|
+
Your user model is automatically augmented with role management methods:
|
203
110
|
|
204
|
-
|
111
|
+
### Role Assignment
|
205
112
|
|
206
113
|
```rb
|
207
|
-
|
208
|
-
|
114
|
+
# Assign roles (creates roles if they don't exist)
|
115
|
+
user.assign_roles(:accountant, :manager)
|
209
116
|
|
210
|
-
|
117
|
+
# Assign only existing roles
|
118
|
+
user.assign_roles(:accountant, :manager, create_new: false)
|
211
119
|
|
212
|
-
|
120
|
+
# Revoke specific roles
|
121
|
+
user.revoke_roles(:accountant, :manager)
|
213
122
|
|
214
|
-
|
215
|
-
|
123
|
+
# Revoke all roles
|
124
|
+
user.revoke_all_roles
|
216
125
|
```
|
217
126
|
|
218
|
-
|
219
|
-
|
220
|
-
If you need to list the roles available in your application, use:
|
127
|
+
### Role Queries
|
221
128
|
|
222
129
|
```rb
|
223
|
-
|
224
|
-
|
225
|
-
|
226
|
-
**`.all_names`**
|
130
|
+
# Check if user has any of the specified roles
|
131
|
+
user.has_role?(:accountant, :manager)
|
227
132
|
|
228
|
-
|
133
|
+
# Get user's roles
|
134
|
+
user.roles
|
229
135
|
|
230
|
-
|
231
|
-
|
136
|
+
# Get all roles grouped by context
|
137
|
+
user.all_roles
|
232
138
|
```
|
233
139
|
|
234
|
-
## Authorization
|
140
|
+
## Controller Authorization
|
235
141
|
|
236
|
-
|
142
|
+
### Basic Setup
|
143
|
+
|
144
|
+
Include the authorization module and configure protection:
|
237
145
|
|
238
146
|
```rb
|
239
147
|
class ApplicationController < ActionController::Base
|
240
148
|
include Rabarber::Authorization
|
241
149
|
|
242
|
-
with_authorization
|
243
|
-
|
244
|
-
# ...
|
150
|
+
with_authorization # Enable authorization for all actions
|
245
151
|
end
|
246
152
|
|
247
153
|
class InvoicesController < ApplicationController
|
248
|
-
with_authorization only: [:update, :destroy]
|
249
|
-
|
250
|
-
# ...
|
154
|
+
with_authorization only: [:update, :destroy] # Selective authorization
|
251
155
|
end
|
252
156
|
```
|
253
157
|
|
254
|
-
|
158
|
+
Authorization requires an authenticated user.
|
255
159
|
|
256
|
-
|
160
|
+
### Skip Authorization
|
257
161
|
|
258
|
-
|
162
|
+
You can also selectively skip authorization:
|
259
163
|
|
260
164
|
```rb
|
261
|
-
|
262
|
-
|
263
|
-
grant_access action: :index, roles: [:accountant, :admin]
|
264
|
-
def index
|
265
|
-
@invoices = Invoice.all
|
266
|
-
@invoices = @invoices.paid if current_user.has_role?(:accountant)
|
267
|
-
# ...
|
268
|
-
end
|
269
|
-
|
270
|
-
grant_access action: :destroy, roles: :admin
|
271
|
-
def destroy
|
272
|
-
# ...
|
273
|
-
end
|
274
|
-
end
|
165
|
+
class TicketsController < ApplicationController
|
166
|
+
skip_authorization except: [:create, :update, :destroy]
|
275
167
|
end
|
276
168
|
```
|
277
169
|
|
278
|
-
|
170
|
+
### Authorization Rules
|
279
171
|
|
280
|
-
|
172
|
+
Define access rules using `grant_access`:
|
281
173
|
|
282
174
|
```rb
|
283
|
-
|
284
|
-
|
285
|
-
|
286
|
-
|
287
|
-
grant_access action: :dashboard, roles: :marketer
|
288
|
-
def dashboard
|
289
|
-
# ...
|
290
|
-
end
|
291
|
-
end
|
292
|
-
end
|
175
|
+
class TicketsController < ApplicationController
|
176
|
+
# Controller-wide access
|
177
|
+
grant_access roles: :admin
|
293
178
|
|
294
|
-
|
295
|
-
|
296
|
-
|
297
|
-
|
298
|
-
|
299
|
-
end
|
179
|
+
# Action-specific access
|
180
|
+
grant_access action: :index, roles: [:manager, :support]
|
181
|
+
def index
|
182
|
+
# Accessible to admin, manager, and support roles
|
183
|
+
end
|
300
184
|
|
301
|
-
|
302
|
-
|
303
|
-
|
185
|
+
grant_access action: :destroy, roles: :admin
|
186
|
+
def destroy
|
187
|
+
# Accessible to admin role only
|
304
188
|
end
|
305
189
|
end
|
306
190
|
```
|
307
191
|
|
308
|
-
|
192
|
+
### Additive Rules
|
309
193
|
|
310
|
-
|
194
|
+
Rules are additive across inheritance chains and for the same actions:
|
311
195
|
|
312
196
|
```rb
|
313
|
-
class
|
314
|
-
grant_access
|
315
|
-
# ...
|
197
|
+
class BaseController < ApplicationController
|
198
|
+
grant_access roles: :admin # Admin can access everything
|
316
199
|
end
|
317
200
|
|
318
|
-
class InvoicesController <
|
319
|
-
grant_access
|
320
|
-
|
321
|
-
|
322
|
-
|
201
|
+
class InvoicesController < BaseController
|
202
|
+
grant_access roles: :accountant # Accountant can also access InvoicesController (along with admin)
|
203
|
+
|
204
|
+
grant_access action: :index, roles: :manager
|
205
|
+
grant_access action: :index, roles: :supervisor
|
206
|
+
# Index is accessible to admin, accountant, manager, and supervisor
|
323
207
|
end
|
324
208
|
```
|
325
209
|
|
326
|
-
|
210
|
+
### Unrestricted Access
|
327
211
|
|
328
|
-
|
212
|
+
Omit roles to allow unrestricted access:
|
329
213
|
|
330
214
|
```rb
|
331
|
-
|
332
|
-
|
333
|
-
grant_access roles: :admin
|
334
|
-
# ...
|
335
|
-
end
|
215
|
+
class UnrestrictedController < ApplicationController
|
216
|
+
grant_access # Allow all users
|
336
217
|
end
|
337
218
|
|
338
|
-
|
339
|
-
|
340
|
-
|
341
|
-
# ...
|
342
|
-
end
|
219
|
+
class MixedController < ApplicationController
|
220
|
+
grant_access action: :index # Unrestricted index action
|
221
|
+
grant_access action: :show, roles: :member # Restricted show action
|
343
222
|
end
|
344
223
|
```
|
345
224
|
|
346
|
-
|
225
|
+
### Custom Unauthorized Handling
|
347
226
|
|
348
|
-
|
227
|
+
Override `when_unauthorized` method to customize unauthorized access behavior:
|
349
228
|
|
350
229
|
```rb
|
351
|
-
|
352
|
-
|
353
|
-
|
354
|
-
|
355
|
-
|
356
|
-
|
357
|
-
|
358
|
-
|
359
|
-
|
360
|
-
|
230
|
+
class ApplicationController < ActionController::Base
|
231
|
+
include Rabarber::Authorization
|
232
|
+
|
233
|
+
with_authorization
|
234
|
+
|
235
|
+
private
|
236
|
+
|
237
|
+
def when_unauthorized
|
238
|
+
# Default behavior: redirect back (HTML) or return 403 (other formats)
|
239
|
+
# Custom behavior example:
|
240
|
+
head :not_found # Hide existence of protected resources
|
361
241
|
end
|
362
242
|
end
|
363
243
|
```
|
364
244
|
|
365
|
-
|
245
|
+
## Dynamic Rules
|
366
246
|
|
367
|
-
|
368
|
-
|
369
|
-
For more complex scenarios, Rabarber supports dynamic authorization rules:
|
247
|
+
Add conditional logic to authorization rules:
|
370
248
|
|
371
249
|
```rb
|
372
|
-
|
373
|
-
|
374
|
-
|
375
|
-
|
376
|
-
def index
|
377
|
-
# ...
|
378
|
-
end
|
379
|
-
|
380
|
-
private
|
381
|
-
|
382
|
-
def company_manager?
|
383
|
-
Company.find(params[:company_id]).manager == current_user
|
384
|
-
end
|
385
|
-
|
386
|
-
def fired?
|
387
|
-
current_user.fired?
|
388
|
-
end
|
389
|
-
end
|
390
|
-
end
|
250
|
+
class OrdersController < ApplicationController
|
251
|
+
# Method-based conditions
|
252
|
+
grant_access roles: :manager, if: :company_manager?, unless: :suspended?
|
391
253
|
|
392
|
-
|
393
|
-
|
394
|
-
grant_access roles: :senior_accountant
|
395
|
-
|
396
|
-
grant_access action: :index, roles: [:secretary, :accountant], if: -> { InvoicesPolicy.new(current_user).can_access?(:index) }
|
397
|
-
def index
|
398
|
-
@invoices = Invoice.all
|
399
|
-
@invoices = @invoices.where("total < 10000") if current_user.has_role?(:accountant)
|
400
|
-
@invoices = @invoices.unpaid if current_user.has_role?(:secretary)
|
401
|
-
# ...
|
402
|
-
end
|
403
|
-
|
404
|
-
grant_access action: :show, roles: :accountant, unless: -> { Invoice.find(params[:id]).total > 10_000 }
|
405
|
-
def show
|
406
|
-
# ...
|
407
|
-
end
|
408
|
-
end
|
409
|
-
end
|
410
|
-
```
|
254
|
+
# Proc-based conditions
|
255
|
+
grant_access action: :show, roles: :client, if: -> { current_user.company_id == Order.find(params[:id]).company_id }
|
411
256
|
|
412
|
-
|
257
|
+
# Dynamic-only rules (no roles required, can be used with custom policies)
|
258
|
+
grant_access action: :index, if: -> { OrdersPolicy.new(current_user).can_access?(:index) }
|
413
259
|
|
414
|
-
|
260
|
+
private
|
415
261
|
|
416
|
-
|
417
|
-
|
418
|
-
grant_access action: :index, if: -> { current_user.company == Company.find(params[:company_id]) }
|
419
|
-
def index
|
420
|
-
# ...
|
262
|
+
def company_manager?
|
263
|
+
current_user.manager_of?(Company.find(params[:company_id]))
|
421
264
|
end
|
422
|
-
end
|
423
|
-
```
|
424
|
-
|
425
|
-
This allows you to use Rabarber as a policy-based authorization library by calling your own custom policy within a dynamic rule:
|
426
265
|
|
427
|
-
|
428
|
-
|
429
|
-
grant_access action: :index, if: -> { InvoicesPolicy.new(current_user).can_access?(:index) }
|
430
|
-
def index
|
431
|
-
# ...
|
266
|
+
def suspended?
|
267
|
+
current_user.suspended?
|
432
268
|
end
|
433
269
|
end
|
434
270
|
```
|
435
271
|
|
436
|
-
##
|
437
|
-
|
438
|
-
Rabarber supports multi-tenancy through its context feature. This allows you to define and authorize roles and rules within a specific context.
|
272
|
+
## Multi-tenancy / Context
|
439
273
|
|
440
|
-
|
274
|
+
All Rabarber methods accept a `context` parameter, allowing you to work with roles within specific scopes rather than globally.
|
441
275
|
|
442
|
-
|
276
|
+
### Contextual Role Assignment
|
443
277
|
|
444
278
|
```rb
|
279
|
+
# Assign roles within a specific model instance
|
445
280
|
user.assign_roles(:owner, context: project)
|
446
|
-
|
447
|
-
```
|
448
|
-
|
449
|
-
You can then check roles like this:
|
281
|
+
user.assign_roles(:member, context: project)
|
450
282
|
|
451
|
-
|
452
|
-
user.has_role?(:owner, context: project)
|
453
|
-
another_user.has_role?(:member, context: project)
|
454
|
-
```
|
455
|
-
|
456
|
-
A role can also be added using a class as a context, e.g., for project admins who can manage all projects:
|
457
|
-
|
458
|
-
```rb
|
283
|
+
# Assign roles within a model class (e.g., project admin)
|
459
284
|
user.assign_roles(:admin, context: Project)
|
460
|
-
```
|
461
285
|
|
462
|
-
|
463
|
-
|
464
|
-
```rb
|
286
|
+
# Check contextual roles
|
287
|
+
user.has_role?(:owner, context: project)
|
465
288
|
user.has_role?(:admin, context: Project)
|
289
|
+
|
290
|
+
# Get roles within context
|
291
|
+
user.roles(context: project)
|
292
|
+
Rabarber::Role.names(context: Project)
|
466
293
|
```
|
467
294
|
|
468
|
-
|
295
|
+
### Contextual Authorization
|
469
296
|
|
470
297
|
```rb
|
471
298
|
class ProjectsController < ApplicationController
|
299
|
+
# Class-based context
|
472
300
|
grant_access roles: :admin, context: Project
|
473
301
|
|
474
|
-
|
475
|
-
|
476
|
-
# ...
|
477
|
-
end
|
302
|
+
# Instance-based context (method)
|
303
|
+
grant_access action: :show, roles: :member, context: :current_project
|
478
304
|
|
305
|
+
# Instance-based context (proc)
|
479
306
|
grant_access action: :update, roles: :owner, context: -> { Project.find(params[:id]) }
|
480
|
-
def update
|
481
|
-
# ...
|
482
|
-
end
|
483
307
|
|
484
308
|
private
|
485
309
|
|
486
|
-
def
|
487
|
-
Project.find(params[:id])
|
310
|
+
def current_project
|
311
|
+
@current_project ||= Project.find(params[:id])
|
488
312
|
end
|
489
313
|
end
|
490
314
|
```
|
491
315
|
|
492
|
-
|
493
|
-
|
494
|
-
If you want to see all the roles assigned to a user within a specific context, you can use:
|
495
|
-
|
496
|
-
```rb
|
497
|
-
user.roles(context: project)
|
498
|
-
```
|
316
|
+
### Context Migrations
|
499
317
|
|
500
|
-
|
318
|
+
Handle context changes when models are renamed or removed. These are irreversible data migrations.
|
501
319
|
|
502
320
|
```rb
|
503
|
-
|
504
|
-
|
505
|
-
|
506
|
-
If a context object (e.g., a project) is deleted, any roles tied to it automatically become irrelevant and are no longer returned by role queries. Rabarber treats them as orphaned and ignores them.
|
507
|
-
|
508
|
-
However, if a context class is renamed (e.g., `Project` becomes `Campaign`), an exception will be raised the next time Rabarber attempts to load roles for that class. This is to ensure you explicitly handle the migration, either by migrating existing roles to the new context or by cleaning up old data.
|
509
|
-
|
510
|
-
To help with such scenarios, Rabarber provides two helper methods that can be called in migrations:
|
511
|
-
|
512
|
-
- `migrate_authorization_context!(old_context, new_context)` - renames a context
|
513
|
-
- `delete_authorization_context!(context)` – removes roles tied to a deleted context
|
514
|
-
|
515
|
-
These are irreversible data migrations.
|
516
|
-
|
517
|
-
## When Unauthorized
|
518
|
-
|
519
|
-
By default, Rabarber redirects back on unauthorized access if the request format is HTML (falling back to the root path), and returns a 401 Unauthorized for other formats.
|
520
|
-
|
521
|
-
You can customize this behavior by overriding the private `when_unauthorized` method:
|
522
|
-
|
523
|
-
```rb
|
524
|
-
class ApplicationController < ActionController::Base
|
525
|
-
include Rabarber::Authorization
|
526
|
-
|
527
|
-
with_authorization
|
528
|
-
|
529
|
-
# ...
|
530
|
-
|
531
|
-
private
|
321
|
+
# Rename a context class (e.g., when you rename your Project model to Campaign)
|
322
|
+
migrate_authorization_context!("Project", "Campaign")
|
532
323
|
|
533
|
-
|
534
|
-
|
535
|
-
end
|
536
|
-
end
|
324
|
+
# Remove orphaned context data (e.g., when you delete a model entirely)
|
325
|
+
delete_authorization_context!("DeletedModel")
|
537
326
|
```
|
538
327
|
|
539
|
-
The method can be overridden in different controllers, providing flexibility in handling unauthorized access attempts.
|
540
|
-
|
541
|
-
## Skip Authorization
|
542
|
-
|
543
|
-
To skip authorization, use `.skip_authorization(options = {})` method:
|
544
|
-
|
545
|
-
```rb
|
546
|
-
class TicketsController < ApplicationController
|
547
|
-
skip_authorization only: :index
|
548
|
-
# ...
|
549
|
-
end
|
550
|
-
```
|
551
|
-
|
552
|
-
This method accepts the same options as Rails’ `skip_before_action`.
|
553
|
-
|
554
328
|
## View Helpers
|
555
329
|
|
556
|
-
|
330
|
+
Include view helpers in your application:
|
557
331
|
|
558
332
|
```rb
|
559
333
|
module ApplicationHelper
|
560
334
|
include Rabarber::Helpers
|
561
|
-
# ...
|
562
335
|
end
|
563
336
|
```
|
564
337
|
|
565
|
-
|
338
|
+
Use conditional rendering based on roles:
|
566
339
|
|
567
340
|
```erb
|
568
341
|
<%= visible_to(:admin, :manager) do %>
|
569
|
-
<
|
342
|
+
<div class="admin-panel">
|
343
|
+
<!-- Admin/Manager content -->
|
344
|
+
</div>
|
570
345
|
<% end %>
|
571
|
-
```
|
572
346
|
|
573
|
-
|
574
|
-
|
575
|
-
|
347
|
+
<%= hidden_from(:guest) do %>
|
348
|
+
<div class="member-content">
|
349
|
+
<!-- Content hidden from guests -->
|
350
|
+
</div>
|
351
|
+
<% end %>
|
352
|
+
|
353
|
+
<!-- With context -->
|
354
|
+
<%= visible_to(:owner, context: @project) do %>
|
355
|
+
<button>Delete Project</button>
|
576
356
|
<% end %>
|
577
357
|
```
|
578
358
|
|
579
|
-
##
|
359
|
+
## Contributing
|
580
360
|
|
581
|
-
|
361
|
+
### Getting Help
|
362
|
+
Have a question or need assistance? Open a discussion in our [discussions section](https://github.com/brownboxdev/rabarber/discussions) for:
|
363
|
+
- Usage questions
|
364
|
+
- Implementation guidance
|
365
|
+
- Feature suggestions
|
582
366
|
|
583
|
-
|
367
|
+
### Reporting Issues
|
368
|
+
Found a bug? Please [create an issue](https://github.com/brownboxdev/rabarber/issues) with:
|
369
|
+
- A clear description of the problem
|
370
|
+
- Steps to reproduce the issue
|
371
|
+
- Your environment details (Rails version, Ruby version, etc.)
|
584
372
|
|
585
|
-
|
373
|
+
### Contributing Code
|
374
|
+
Ready to contribute? You can:
|
375
|
+
- Fix bugs by submitting pull requests
|
376
|
+
- Improve documentation
|
377
|
+
- Add new features (please discuss first in our [discussions section](https://github.com/brownboxdev/rabarber/discussions))
|
586
378
|
|
587
|
-
|
588
|
-
- **Contribute a Solution**: Found a fix for the issue? Feel free to create a pull request with your changes.
|
379
|
+
Before contributing, please read the [contributing guidelines](https://github.com/brownboxdev/rabarber/blob/main/CONTRIBUTING.md)
|
589
380
|
|
590
|
-
##
|
381
|
+
## License
|
591
382
|
|
592
|
-
|
383
|
+
The gem is available as open source under the terms of the [MIT License](https://github.com/brownboxdev/rabarber/blob/main/LICENSE.txt).
|
593
384
|
|
594
385
|
## Code of Conduct
|
595
386
|
|
@@ -601,7 +392,3 @@ Only the latest major version is supported. Older versions are obsolete and not
|
|
601
392
|
|
602
393
|
[v4.x.x](https://github.com/brownboxdev/rabarber/blob/9353e70281971154d5acd70693620197a132c543/README.md) | [v3.x.x](https://github.com/brownboxdev/rabarber/blob/3bb273de7e342004abc7ef07fa4d0a9a3ce3e249/README.md)
|
603
394
|
| [v2.x.x](https://github.com/brownboxdev/rabarber/blob/875b357ea949404ddc3645ad66eddea7ed4e2ee4/README.md) | [v1.x.x](https://github.com/brownboxdev/rabarber/blob/b81428429404e197d70317b763e7b2a21e02c296/README.md)
|
604
|
-
|
605
|
-
## License
|
606
|
-
|
607
|
-
The gem is available as open source under the terms of the [MIT License](https://github.com/brownboxdev/rabarber/blob/main/LICENSE.txt).
|
@@ -9,13 +9,13 @@ module Rabarber
|
|
9
9
|
def with_authorization(options = {})
|
10
10
|
before_action :with_authorization, **options
|
11
11
|
rescue ArgumentError => e
|
12
|
-
raise Rabarber::
|
12
|
+
raise Rabarber::InvalidArgumentError, e.message
|
13
13
|
end
|
14
14
|
|
15
15
|
def skip_authorization(options = {})
|
16
16
|
skip_before_action :with_authorization, **options
|
17
17
|
rescue ArgumentError => e
|
18
|
-
raise Rabarber::
|
18
|
+
raise Rabarber::InvalidArgumentError, e.message
|
19
19
|
end
|
20
20
|
|
21
21
|
def grant_access(action: nil, roles: nil, context: nil, if: nil, unless: nil)
|
@@ -39,7 +39,7 @@ module Rabarber
|
|
39
39
|
end
|
40
40
|
|
41
41
|
def when_unauthorized
|
42
|
-
request.format.html? ? redirect_back(fallback_location: root_path) : head(:
|
42
|
+
request.format.html? ? redirect_back(fallback_location: root_path) : head(:forbidden)
|
43
43
|
end
|
44
44
|
end
|
45
45
|
end
|
@@ -40,7 +40,7 @@ module Rabarber
|
|
40
40
|
)
|
41
41
|
|
42
42
|
if roles_to_assign.any?
|
43
|
-
delete_roleable_cache(
|
43
|
+
delete_roleable_cache(contexts: [processed_context])
|
44
44
|
rabarber_roles << roles_to_assign
|
45
45
|
end
|
46
46
|
|
@@ -56,13 +56,23 @@ module Rabarber
|
|
56
56
|
)
|
57
57
|
|
58
58
|
if roles_to_revoke.any?
|
59
|
-
delete_roleable_cache(
|
59
|
+
delete_roleable_cache(contexts: [processed_context])
|
60
60
|
self.rabarber_roles -= roles_to_revoke
|
61
61
|
end
|
62
62
|
|
63
63
|
roles(context: processed_context)
|
64
64
|
end
|
65
65
|
|
66
|
+
def revoke_all_roles
|
67
|
+
return if rabarber_roles.none?
|
68
|
+
|
69
|
+
contexts = all_roles.keys.map { process_context(_1) }
|
70
|
+
|
71
|
+
rabarber_roles.clear
|
72
|
+
|
73
|
+
delete_roleable_cache(contexts:)
|
74
|
+
end
|
75
|
+
|
66
76
|
private
|
67
77
|
|
68
78
|
def create_new_roles(role_names, context:)
|
@@ -78,8 +88,8 @@ module Rabarber
|
|
78
88
|
Rabarber::Input::Context.new(context).process
|
79
89
|
end
|
80
90
|
|
81
|
-
def delete_roleable_cache(
|
82
|
-
Rabarber::Core::Cache.delete([roleable_id,
|
91
|
+
def delete_roleable_cache(contexts:)
|
92
|
+
Rabarber::Core::Cache.delete(*contexts.map { [roleable_id, _1] }, [roleable_id, :all])
|
83
93
|
end
|
84
94
|
|
85
95
|
def roleable_id
|
data/lib/rabarber/models/role.rb
CHANGED
@@ -4,11 +4,6 @@ module Rabarber
|
|
4
4
|
class Role < ActiveRecord::Base
|
5
5
|
self.table_name = "rabarber_roles"
|
6
6
|
|
7
|
-
validates :name, presence: true,
|
8
|
-
uniqueness: { scope: [:context_type, :context_id] },
|
9
|
-
format: { with: Rabarber::Input::Role::REGEX },
|
10
|
-
strict: true
|
11
|
-
|
12
7
|
belongs_to :context, polymorphic: true, optional: true
|
13
8
|
|
14
9
|
has_and_belongs_to_many :roleables, class_name: Rabarber::Configuration.instance.user_model_name,
|
data/lib/rabarber/version.rb
CHANGED