rabarber 5.0.0 → 5.1.1
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 +18 -0
- data/README.md +194 -384
- data/lib/rabarber/configuration.rb +43 -25
- data/lib/rabarber/controllers/concerns/authorization.rb +31 -8
- data/lib/rabarber/core/cache.rb +1 -1
- data/lib/rabarber/core/roleable.rb +3 -3
- data/lib/rabarber/core/rule.rb +6 -5
- data/lib/rabarber/inputs.rb +62 -0
- data/lib/rabarber/models/concerns/has_roles.rb +24 -6
- data/lib/rabarber/models/role.rb +12 -10
- data/lib/rabarber/railtie.rb +1 -1
- data/lib/rabarber/version.rb +1 -1
- data/lib/rabarber.rb +13 -24
- data/rabarber.gemspec +2 -0
- metadata +30 -12
- data/lib/rabarber/input/action.rb +0 -21
- data/lib/rabarber/input/ar_model.rb +0 -23
- data/lib/rabarber/input/authorization_context.rb +0 -25
- data/lib/rabarber/input/base.rb +0 -37
- data/lib/rabarber/input/context.rb +0 -33
- data/lib/rabarber/input/dynamic_rule.rb +0 -21
- data/lib/rabarber/input/role.rb +0 -23
- data/lib/rabarber/input/roles.rb +0 -25
- data/lib/rabarber/input/types/boolean.rb +0 -23
- data/lib/rabarber/input/types/proc.rb +0 -23
- data/lib/rabarber/input/types/symbol.rb +0 -23
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
|
21
|
+
- [User Role Methods](#user-role-methods)
|
41
22
|
- [Role Management](#role-management)
|
42
|
-
- [Authorization
|
43
|
-
- [Dynamic
|
44
|
-
- [
|
45
|
-
- [When Unauthorized](#when-unauthorized)
|
46
|
-
- [Skip Authorization](#skip-authorization)
|
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
|
-
- [
|
51
|
-
- [
|
30
|
+
- [Getting Help and Contributing](#getting-help-and-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
|
-
|
52
|
+
# For standard integer IDs
|
53
|
+
rails generate rabarber:roles users
|
77
54
|
|
78
|
-
|
79
|
-
|
80
|
-
```shell
|
81
|
-
rails g rabarber:roles users --uuid
|
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,346 @@ 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:
|
113
|
-
|
114
|
-
```rb
|
115
|
-
user.assign_roles(:accountant, :marketer)
|
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`:
|
77
|
+
To clear the role cache manually:
|
119
78
|
|
120
79
|
```rb
|
121
|
-
|
80
|
+
Rabarber::Cache.clear
|
122
81
|
```
|
123
82
|
|
124
|
-
|
83
|
+
## User Role Methods
|
125
84
|
|
126
|
-
|
85
|
+
Your user model is automatically augmented with role management methods:
|
127
86
|
|
128
|
-
|
87
|
+
### Role Assignment
|
129
88
|
|
130
89
|
```rb
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
Roles the user doesn’t have are ignored.
|
90
|
+
# Assign roles (creates roles if they don't exist)
|
91
|
+
user.assign_roles(:accountant, :manager)
|
135
92
|
|
136
|
-
|
93
|
+
# Assign only existing roles
|
94
|
+
user.assign_roles(:accountant, :manager, create_new: false)
|
137
95
|
|
138
|
-
|
96
|
+
# Revoke specific roles
|
97
|
+
user.revoke_roles(:accountant, :manager)
|
139
98
|
|
140
|
-
|
141
|
-
|
142
|
-
```rb
|
143
|
-
user.has_role?(:accountant, :marketer)
|
99
|
+
# Revoke all roles
|
100
|
+
user.revoke_all_roles
|
144
101
|
```
|
145
102
|
|
146
|
-
|
147
|
-
|
148
|
-
**`#roles(context: nil)`**
|
149
|
-
|
150
|
-
To get the list of roles assigned to the user, use:
|
103
|
+
### Role Queries
|
151
104
|
|
152
105
|
```rb
|
153
|
-
user
|
154
|
-
|
106
|
+
# Check if user has any of the specified roles
|
107
|
+
user.has_role?(:accountant, :manager)
|
155
108
|
|
156
|
-
|
157
|
-
|
158
|
-
To get all roles assigned to the user, grouped by context, use:
|
109
|
+
# Get user's roles
|
110
|
+
user.roles
|
159
111
|
|
160
|
-
|
112
|
+
# Get all roles grouped by context
|
161
113
|
user.all_roles
|
162
114
|
```
|
163
115
|
|
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)
|
170
|
-
```
|
171
|
-
|
172
116
|
## Role Management
|
173
117
|
|
174
|
-
|
175
|
-
|
176
|
-
**`.add(role_name, context: nil)`**
|
177
|
-
|
178
|
-
To add a new role, use:
|
118
|
+
### Direct Role Operations
|
179
119
|
|
180
120
|
```rb
|
121
|
+
# Create a new role
|
181
122
|
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
|
-
|
186
|
-
**`.rename(old_role_name, new_role_name, context: nil, force: false)`**
|
187
|
-
|
188
|
-
To rename a role, use:
|
189
123
|
|
190
|
-
|
124
|
+
# Rename a role
|
191
125
|
Rabarber::Role.rename(:admin, :administrator)
|
192
|
-
|
193
|
-
|
194
|
-
The old role must exist. If it doesn’t, an error is raised. If a role with the new name already exists, the method returns `false` and the rename fails.
|
195
|
-
|
196
|
-
The rename also fails if the role is assigned to any user. To force it, use:
|
197
|
-
|
198
|
-
```rb
|
199
|
-
Rabarber::Role.rename(:admin, :administrator, force: true)
|
200
|
-
```
|
201
|
-
|
202
|
-
**`.remove(role_name, context: nil, force: false)`**
|
203
|
-
|
204
|
-
To remove a role, use:
|
126
|
+
Rabarber::Role.rename(:admin, :administrator, force: true) # Force if role is assigned to users
|
205
127
|
|
206
|
-
|
128
|
+
# Remove a role
|
207
129
|
Rabarber::Role.remove(:admin)
|
208
|
-
|
209
|
-
|
210
|
-
The old role must exist. If it doesn’t, an error is raised. The method returns `true` if successful.
|
211
|
-
|
212
|
-
If the role is assigned to any user, removal will fail. To force it, use:
|
213
|
-
|
214
|
-
```rb
|
215
|
-
Rabarber::Role.remove(:admin, force: true)
|
216
|
-
```
|
217
|
-
|
218
|
-
**`.names(context: nil)`**
|
219
|
-
|
220
|
-
If you need to list the roles available in your application, use:
|
130
|
+
Rabarber::Role.remove(:admin, force: true) # Force if role is assigned to users
|
221
131
|
|
222
|
-
|
132
|
+
# List available roles
|
223
133
|
Rabarber::Role.names
|
224
|
-
|
225
|
-
|
226
|
-
**`.all_names`**
|
134
|
+
Rabarber::Role.all_names # All roles grouped by context
|
227
135
|
|
228
|
-
|
229
|
-
|
230
|
-
```rb
|
231
|
-
Rabarber::Role.all_names
|
136
|
+
# Get users assigned to a role
|
137
|
+
Rabarber::Role.assignees(:admin)
|
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: :owner, context: -> { Ticket.find(params[:id]) }
|
186
|
+
def destroy
|
187
|
+
# Accessible to admin and owner of the ticket
|
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
|
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
|
320
206
|
def index
|
321
|
-
#
|
207
|
+
# Index is accessible to admin, accountant, manager, and supervisor
|
322
208
|
end
|
323
209
|
end
|
324
210
|
```
|
325
211
|
|
326
|
-
|
212
|
+
### Unrestricted Access
|
327
213
|
|
328
|
-
|
214
|
+
Omit roles to allow unrestricted access:
|
329
215
|
|
330
216
|
```rb
|
331
|
-
|
332
|
-
|
333
|
-
grant_access roles: :admin
|
334
|
-
# ...
|
335
|
-
end
|
217
|
+
class UnrestrictedController < ApplicationController
|
218
|
+
grant_access # Allow all users
|
336
219
|
end
|
337
220
|
|
338
|
-
|
339
|
-
|
340
|
-
|
341
|
-
|
221
|
+
class MixedController < ApplicationController
|
222
|
+
grant_access action: :index # Unrestricted index action
|
223
|
+
def index
|
224
|
+
# Accessible to all users
|
342
225
|
end
|
343
|
-
end
|
344
|
-
```
|
345
226
|
|
346
|
-
|
347
|
-
|
348
|
-
|
349
|
-
|
350
|
-
```rb
|
351
|
-
module Crm
|
352
|
-
class OrdersController < ApplicationController
|
353
|
-
grant_access roles: :manager, context: Order
|
354
|
-
grant_access roles: :admin
|
355
|
-
|
356
|
-
grant_access action: :show, roles: :client, context: -> { Order.find(params[:id]) }
|
357
|
-
grant_access action: :show, roles: :accountant
|
358
|
-
def show
|
359
|
-
# ...
|
360
|
-
end
|
227
|
+
grant_access action: :show, roles: :member # Restricted show action
|
228
|
+
def show
|
229
|
+
# Accessible to members only
|
361
230
|
end
|
362
231
|
end
|
363
232
|
```
|
364
233
|
|
365
|
-
|
366
|
-
|
367
|
-
## Dynamic Authorization Rules
|
234
|
+
### Custom Unauthorized Handling
|
368
235
|
|
369
|
-
|
236
|
+
Override `when_unauthorized` method to customize unauthorized access behavior:
|
370
237
|
|
371
238
|
```rb
|
372
|
-
|
373
|
-
|
374
|
-
grant_access roles: :manager, if: :company_manager?, unless: :fired?
|
375
|
-
|
376
|
-
def index
|
377
|
-
# ...
|
378
|
-
end
|
379
|
-
|
380
|
-
private
|
239
|
+
class ApplicationController < ActionController::Base
|
240
|
+
include Rabarber::Authorization
|
381
241
|
|
382
|
-
|
383
|
-
Company.find(params[:company_id]).manager == current_user
|
384
|
-
end
|
242
|
+
with_authorization
|
385
243
|
|
386
|
-
|
387
|
-
current_user.fired?
|
388
|
-
end
|
389
|
-
end
|
390
|
-
end
|
244
|
+
private
|
391
245
|
|
392
|
-
|
393
|
-
|
394
|
-
|
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
|
246
|
+
def when_unauthorized
|
247
|
+
# Default behavior: redirect back (HTML) or return 403 (other formats)
|
248
|
+
# Custom behavior example:
|
249
|
+
head :not_found # Hide existence of protected resources
|
408
250
|
end
|
409
251
|
end
|
410
252
|
```
|
411
253
|
|
412
|
-
|
254
|
+
By default, Rabarber will redirect back (HTML format) or return 403 (other formats).
|
255
|
+
|
256
|
+
## Dynamic Rules
|
413
257
|
|
414
|
-
|
258
|
+
Add conditional logic to authorization rules:
|
415
259
|
|
416
260
|
```rb
|
417
|
-
class
|
418
|
-
|
261
|
+
class OrdersController < ApplicationController
|
262
|
+
# Method-based conditions
|
263
|
+
grant_access roles: :manager, if: :company_manager?, unless: :suspended?
|
264
|
+
|
265
|
+
# Proc-based conditions
|
266
|
+
grant_access action: :show, roles: :client, if: -> { current_user.company_id == Order.find(params[:id]).company_id }
|
267
|
+
def show
|
268
|
+
# Accessible to company managers unless suspended, and to clients if the client's company matches the order's company
|
269
|
+
end
|
270
|
+
|
271
|
+
# Dynamic-only rules (no roles required, can be used with custom policies)
|
272
|
+
grant_access action: :index, if: -> { OrdersPolicy.new(current_user).can_access?(:index) }
|
419
273
|
def index
|
420
|
-
#
|
274
|
+
# Accessible to company managers unless suspended, and to users based on custom policy logic
|
421
275
|
end
|
422
|
-
end
|
423
|
-
```
|
424
276
|
|
425
|
-
|
277
|
+
private
|
426
278
|
|
427
|
-
|
428
|
-
|
429
|
-
|
430
|
-
|
431
|
-
|
279
|
+
def company_manager?
|
280
|
+
current_user.manager_of?(Company.find(params[:company_id]))
|
281
|
+
end
|
282
|
+
|
283
|
+
def suspended?
|
284
|
+
current_user.suspended?
|
432
285
|
end
|
433
286
|
end
|
434
287
|
```
|
435
288
|
|
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.
|
289
|
+
## Multi-tenancy / Context
|
439
290
|
|
440
|
-
|
291
|
+
All Rabarber methods accept a `context` parameter, allowing you to work with roles within specific scopes rather than globally.
|
441
292
|
|
442
|
-
|
293
|
+
### Contextual Role Assignment
|
443
294
|
|
444
295
|
```rb
|
296
|
+
# Assign roles within a specific model instance
|
445
297
|
user.assign_roles(:owner, context: project)
|
446
|
-
|
447
|
-
```
|
448
|
-
|
449
|
-
You can then check roles like this:
|
298
|
+
user.assign_roles(:member, context: project)
|
450
299
|
|
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
|
300
|
+
# Assign roles within a model class (e.g., project admin)
|
459
301
|
user.assign_roles(:admin, context: Project)
|
460
|
-
```
|
461
302
|
|
462
|
-
|
463
|
-
|
464
|
-
```rb
|
303
|
+
# Check contextual roles
|
304
|
+
user.has_role?(:owner, context: project)
|
465
305
|
user.has_role?(:admin, context: Project)
|
306
|
+
|
307
|
+
# Get roles within context
|
308
|
+
user.roles(context: project)
|
309
|
+
Rabarber::Role.names(context: Project)
|
466
310
|
```
|
467
311
|
|
468
|
-
|
312
|
+
### Contextual Authorization
|
469
313
|
|
470
314
|
```rb
|
471
315
|
class ProjectsController < ApplicationController
|
316
|
+
# Class-based context
|
472
317
|
grant_access roles: :admin, context: Project
|
473
318
|
|
474
|
-
|
319
|
+
# Instance-based context (method)
|
320
|
+
grant_access action: :show, roles: :member, context: :current_project
|
475
321
|
def show
|
476
|
-
#
|
322
|
+
# Accessible to Project admin and members of the current project
|
477
323
|
end
|
478
324
|
|
325
|
+
# Instance-based context (proc)
|
479
326
|
grant_access action: :update, roles: :owner, context: -> { Project.find(params[:id]) }
|
480
327
|
def update
|
481
|
-
#
|
328
|
+
# Accessible to Project admin and owner of the current project
|
482
329
|
end
|
483
330
|
|
484
331
|
private
|
485
332
|
|
486
|
-
def
|
487
|
-
Project.find(params[:id])
|
333
|
+
def current_project
|
334
|
+
@current_project ||= Project.find(params[:id])
|
488
335
|
end
|
489
336
|
end
|
490
337
|
```
|
491
338
|
|
492
|
-
|
339
|
+
### Context Migrations
|
493
340
|
|
494
|
-
|
341
|
+
Handle context changes when models are renamed or removed. These are irreversible data migrations.
|
495
342
|
|
496
343
|
```rb
|
497
|
-
|
498
|
-
|
344
|
+
# Rename a context class (e.g., when you rename your Project model to Campaign)
|
345
|
+
migrate_authorization_context!("Project", "Campaign")
|
499
346
|
|
500
|
-
|
501
|
-
|
502
|
-
```rb
|
503
|
-
Rabarber::Role.names(context: Project)
|
347
|
+
# Remove orphaned context data (e.g., when you delete a model entirely)
|
348
|
+
delete_authorization_context!("DeletedModel")
|
504
349
|
```
|
505
350
|
|
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
|
532
|
-
|
533
|
-
def when_unauthorized
|
534
|
-
head :not_found # pretend the page doesn't exist
|
535
|
-
end
|
536
|
-
end
|
537
|
-
```
|
538
|
-
|
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
351
|
## View Helpers
|
555
352
|
|
556
|
-
|
353
|
+
Include view helpers in your application:
|
557
354
|
|
558
355
|
```rb
|
559
356
|
module ApplicationHelper
|
560
357
|
include Rabarber::Helpers
|
561
|
-
# ...
|
562
358
|
end
|
563
359
|
```
|
564
360
|
|
565
|
-
|
361
|
+
Use conditional rendering based on roles:
|
566
362
|
|
567
363
|
```erb
|
568
364
|
<%= visible_to(:admin, :manager) do %>
|
569
|
-
<
|
365
|
+
<div class="admin-panel">
|
366
|
+
<!-- Admin/Manager content -->
|
367
|
+
</div>
|
570
368
|
<% end %>
|
571
|
-
```
|
572
369
|
|
573
|
-
|
574
|
-
|
575
|
-
|
370
|
+
<%= hidden_from(:guest) do %>
|
371
|
+
<div class="member-content">
|
372
|
+
<!-- Content hidden from guests -->
|
373
|
+
</div>
|
374
|
+
<% end %>
|
375
|
+
|
376
|
+
<!-- With context -->
|
377
|
+
<%= visible_to(:owner, context: @project) do %>
|
378
|
+
<button>Delete Project</button>
|
576
379
|
<% end %>
|
577
380
|
```
|
578
381
|
|
579
|
-
##
|
382
|
+
## Getting Help and Contributing
|
580
383
|
|
581
|
-
|
384
|
+
### Getting Help
|
385
|
+
Have a question or need assistance? Open a discussion in our [discussions section](https://github.com/brownboxdev/rabarber/discussions) for:
|
386
|
+
- Usage questions
|
387
|
+
- Implementation guidance
|
388
|
+
- Feature suggestions
|
582
389
|
|
583
|
-
|
390
|
+
### Reporting Issues
|
391
|
+
Found a bug? Please [create an issue](https://github.com/brownboxdev/rabarber/issues) with:
|
392
|
+
- A clear description of the problem
|
393
|
+
- Steps to reproduce the issue
|
394
|
+
- Your environment details (Rails version, Ruby version, etc.)
|
584
395
|
|
585
|
-
|
396
|
+
### Contributing Code
|
397
|
+
Ready to contribute? You can:
|
398
|
+
- Fix bugs by submitting pull requests
|
399
|
+
- Improve documentation
|
400
|
+
- Add new features (please discuss first in our [discussions section](https://github.com/brownboxdev/rabarber/discussions))
|
586
401
|
|
587
|
-
|
588
|
-
- **Contribute a Solution**: Found a fix for the issue? Feel free to create a pull request with your changes.
|
402
|
+
Before contributing, please read the [contributing guidelines](https://github.com/brownboxdev/rabarber/blob/main/CONTRIBUTING.md)
|
589
403
|
|
590
|
-
##
|
404
|
+
## License
|
591
405
|
|
592
|
-
|
406
|
+
The gem is available as open source under the terms of the [MIT License](https://github.com/brownboxdev/rabarber/blob/main/LICENSE.txt).
|
593
407
|
|
594
408
|
## Code of Conduct
|
595
409
|
|
@@ -601,7 +415,3 @@ Only the latest major version is supported. Older versions are obsolete and not
|
|
601
415
|
|
602
416
|
[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
417
|
| [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).
|