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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 1de40ccff8f5d39718c2c908913c5a67149eeada18598a37b51ec53e900200cc
4
- data.tar.gz: d9a02594d2c2215499cd2609d57f3fa14e796d9b3df6d0176b5fa1b510cf2d72
3
+ metadata.gz: 874cd3840269f288baec19fd5e240afdd6b06e3e5b81d703eb30e8e62283fb32
4
+ data.tar.gz: 117d060ffe53ae884b657c1d79a94d9762de1de8b6726659128d7be805c77e24
5
5
  SHA512:
6
- metadata.gz: e7862b8b5793a4c3076b2e9c34eec91df58b79b629273b7e2fc9a6c97b74d3a42b7ac53bd85bad8ee270549c13a6d8e5d747ea6fe3472b2d72f067aafc247028
7
- data.tar.gz: a9f1cbf65045fde78aeac07e6217caca037dc1f85e97065e26a07f1c52bfce46deb4e60513617df6572a0883e792dacbf70a18c0f19d14f31e97171420903cc9
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: Simplified Authorization for Rails
1
+ # Rabarber: Role-Based Authorization for Rails
2
2
 
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/brownboxdev/rabarber/actions/workflows/ci.yml/badge.svg)](https://github.com/brownboxdev/rabarber/actions/workflows/ci.yml)
5
5
 
6
- Rabarber is a role-based authorization library for Ruby on Rails. It provides a set of tools for managing user roles and defining authorization rules, with support for multi-tenancy and fine-grained access control.
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
- **Example of Usage**:
11
-
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. You can define such authorization rules easily with Rabarber.
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 usage:**
18
+ **Gem Usage:**
38
19
  - [Installation](#installation)
39
20
  - [Configuration](#configuration)
40
- - [Role Assignments](#role-assignments)
41
21
  - [Role Management](#role-management)
42
- - [Authorization Rules](#authorization-rules)
43
- - [Dynamic Authorization Rules](#dynamic-authorization-rules)
44
- - [Context / Multi-tenancy](#context--multi-tenancy)
45
- - [When Unauthorized](#when-unauthorized)
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 a migration to create tables for storing roles. Run the generator with the table name used by the model that represents users in your application. For example, if the table is `users`, run:
49
+ Generate the migration for role storage (replace `users` with your user table name if different):
73
50
 
74
51
  ```shell
75
- rails g rabarber:roles users
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
- ```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
- Finally, run the migration:
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
- If customization is required, Rabarber can be configured using `.configure` method in an initializer:
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
- - `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.
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
- 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`:
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
- To manipulate roles directly, you can use the following methods:
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
- **`.rename(old_role_name, new_role_name, context: nil, force: false)`**
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
- 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.
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
- The rename also fails if the role is assigned to any user. To force it, use:
99
+ # List available roles
100
+ Rabarber::Role.names
101
+ Rabarber::Role.all_names # All roles grouped by context
197
102
 
198
- ```rb
199
- Rabarber::Role.rename(:admin, :administrator, force: true)
103
+ # Get users assigned to a role
104
+ Rabarber::Role.assignees(:admin)
200
105
  ```
201
106
 
202
- **`.remove(role_name, context: nil, force: false)`**
107
+ ## User Role Methods
108
+
109
+ Your user model is automatically augmented with role management methods:
203
110
 
204
- To remove a role, use:
111
+ ### Role Assignment
205
112
 
206
113
  ```rb
207
- Rabarber::Role.remove(:admin)
208
- ```
114
+ # Assign roles (creates roles if they don't exist)
115
+ user.assign_roles(:accountant, :manager)
209
116
 
210
- The old role must exist. If it doesn’t, an error is raised. The method returns `true` if successful.
117
+ # Assign only existing roles
118
+ user.assign_roles(:accountant, :manager, create_new: false)
211
119
 
212
- If the role is assigned to any user, removal will fail. To force it, use:
120
+ # Revoke specific roles
121
+ user.revoke_roles(:accountant, :manager)
213
122
 
214
- ```rb
215
- Rabarber::Role.remove(:admin, force: true)
123
+ # Revoke all roles
124
+ user.revoke_all_roles
216
125
  ```
217
126
 
218
- **`.names(context: nil)`**
219
-
220
- If you need to list the roles available in your application, use:
127
+ ### Role Queries
221
128
 
222
129
  ```rb
223
- Rabarber::Role.names
224
- ```
225
-
226
- **`.all_names`**
130
+ # Check if user has any of the specified roles
131
+ user.has_role?(:accountant, :manager)
227
132
 
228
- If you need to list all roles available in your application, grouped by context, use:
133
+ # Get user's roles
134
+ user.roles
229
135
 
230
- ```rb
231
- Rabarber::Role.all_names
136
+ # Get all roles grouped by context
137
+ user.all_roles
232
138
  ```
233
139
 
234
- ## Authorization Rules
140
+ ## Controller Authorization
235
141
 
236
- Include `Rabarber::Authorization` module in the controller where you want to define authorization rules. Typically, it is `ApplicationController`, but it can be any controller of your choice. Then use `.with_authorization(options = {})` method, which accepts the same options as Rails’ `before_action`, allowing you to perform authorization checks selectively.
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
- You must ensure the user is authenticated before authorization checks are performed.
158
+ Authorization requires an authenticated user.
255
159
 
256
- To define authorization rules, use `.grant_access(action: nil, roles: nil, context: nil, if: nil, unless: nil)` method.
160
+ ### Skip Authorization
257
161
 
258
- The most basic usage of the method is as follows:
162
+ You can also selectively skip authorization:
259
163
 
260
164
  ```rb
261
- module Crm
262
- class InvoicesController < ApplicationController
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
- This grants access to `index` action for users with `accountant` or `admin` role, and access to `destroy` action for `admin` users only.
170
+ ### Authorization Rules
279
171
 
280
- You can also define controller-wide rules (without `action` argument):
172
+ Define access rules using `grant_access`:
281
173
 
282
174
  ```rb
283
- module Crm
284
- class BaseController < ApplicationController
285
- grant_access roles: [:admin, :manager]
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
- module Crm
295
- class InvoicesController < Crm::BaseController
296
- grant_access roles: :accountant
297
- def index
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
- def delete
302
- # ...
303
- end
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
- In this example, `admin` and `manager` have access to all actions in `Crm::BaseController` and its descendants, while `accountant` role has access only to the actions in `Crm::InvoicesController`. Users with `marketer` role can only see the dashboard.
192
+ ### Additive Rules
309
193
 
310
- You can also omit roles to allow unrestricted access:
194
+ Rules are additive across inheritance chains and for the same actions:
311
195
 
312
196
  ```rb
313
- class OrdersController < ApplicationController
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 < ApplicationController
319
- grant_access action: :index
320
- def index
321
- # ...
322
- end
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
- This allows everyone to access `OrdersController` and its descendants as well as `index` action in `InvoicesController`.
210
+ ### Unrestricted Access
327
211
 
328
- Rules defined in descendant classes don't override ancestor rules but rather add to them:
212
+ Omit roles to allow unrestricted access:
329
213
 
330
214
  ```rb
331
- module Crm
332
- class BaseController < ApplicationController
333
- grant_access roles: :admin
334
- # ...
335
- end
215
+ class UnrestrictedController < ApplicationController
216
+ grant_access # Allow all users
336
217
  end
337
218
 
338
- module Crm
339
- class InvoicesController < Crm::BaseController
340
- grant_access roles: :accountant
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
- This means that `Crm::InvoicesController` is still accessible to `admin` but is also accessible to `accountant`.
225
+ ### Custom Unauthorized Handling
347
226
 
348
- This also applies when defining multiple rules for the same controller or action:
227
+ Override `when_unauthorized` method to customize unauthorized access behavior:
349
228
 
350
229
  ```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
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
- This will add rules for `manager` and `admin` roles for all actions in `Crm::OrdersController`, and for `client` and `accountant` roles for the `show` action.
245
+ ## Dynamic Rules
366
246
 
367
- ## Dynamic Authorization Rules
368
-
369
- For more complex scenarios, Rabarber supports dynamic authorization rules:
247
+ Add conditional logic to authorization rules:
370
248
 
371
249
  ```rb
372
- module Crm
373
- class OrdersController < ApplicationController
374
- grant_access roles: :manager, if: :company_manager?, unless: :fired?
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
- module Crm
393
- class InvoicesController < ApplicationController
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
- You can pass a dynamic rule as `if` or `unless` argument. You can pass a symbol (method name) or a proc. Symbols refer to instance methods, and procs are evaluated in the controller at request time.
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
- You can use only dynamic rules without specifying roles if that suits your needs:
260
+ private
415
261
 
416
- ```rb
417
- class InvoicesController < ApplicationController
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
- ```rb
428
- class InvoicesController < ApplicationController
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
- ## Context / Multi-tenancy
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
- Every Rabarber method can accept a context as an additional keyword argument. By default, the context is set to `nil`, meaning the roles are global. Thus, all examples from other sections of this README are valid for global roles. Besides being global, context can also be an instance of an `ActiveRecord` model or a class.
274
+ All Rabarber methods accept a `context` parameter, allowing you to work with roles within specific scopes rather than globally.
441
275
 
442
- E.g., consider a model named `Project`, where each project has its owner and regular members. Roles can be defined like this:
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
- another_user.assign_roles(:member, context: project)
447
- ```
448
-
449
- You can then check roles like this:
281
+ user.assign_roles(:member, context: project)
450
282
 
451
- ```rb
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
- And to check it:
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
- In authorization rules, the context can be used in the same way, but it also can be a proc or a symbol (similar to dynamic rules):
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
- grant_access action: :show, roles: :member, context: :project
475
- def show
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 project
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
- Role names are scoped by context, i.e. `admin` in a project is different from a global `admin`, or from an `admin` in another project.
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
- Or if you want to get all the roles available in a specific context, you can use:
318
+ Handle context changes when models are renamed or removed. These are irreversible data migrations.
501
319
 
502
320
  ```rb
503
- Rabarber::Role.names(context: Project)
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
- def when_unauthorized
534
- head :not_found # pretend the page doesn't exist
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
- Rabarber provides two helpers for use in views: `visible_to(*roles, context: nil, &block)` and `hidden_from(*roles, context: nil, &block)`. To enable them, include `Rabarber::Helpers` in your helper module, typically `ApplicationHelper`.
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
- The usage is straightforward:
338
+ Use conditional rendering based on roles:
566
339
 
567
340
  ```erb
568
341
  <%= visible_to(:admin, :manager) do %>
569
- <p>Visible only to admins and managers</p>
342
+ <div class="admin-panel">
343
+ <!-- Admin/Manager content -->
344
+ </div>
570
345
  <% end %>
571
- ```
572
346
 
573
- ```erb
574
- <%= hidden_from(:accountant) do %>
575
- <p>Accountant cannot see this</p>
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
- ## Problems?
359
+ ## Contributing
580
360
 
581
- Facing a problem or want to suggest an enhancement?
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
- - **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.
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
- Encountered a bug?
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
- - **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.
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
- ## Contributing
381
+ ## License
591
382
 
592
- Before creating an issue or a pull request, please read the [contributing guidelines](https://github.com/brownboxdev/rabarber/blob/main/CONTRIBUTING.md).
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::Error, e.message
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::Error, e.message
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(:unauthorized)
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(context: processed_context)
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(context: processed_context)
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(context:)
82
- Rabarber::Core::Cache.delete([roleable_id, context], [roleable_id, :all])
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
@@ -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,
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Rabarber
4
- VERSION = "5.0.0"
4
+ VERSION = "5.1.0"
5
5
  end
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: 5.0.0
4
+ version: 5.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - enjaku4