rbacan 0.4.0 → 0.5.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: 066e77f9b2f426249a569e679c9c1348e28b2f3af5f19f480a9059ce237b5258
4
- data.tar.gz: 83cb20bd2b2e97c558f0f97f2c0452417f01668f73cc3bccbf48eddde3e7cd81
3
+ metadata.gz: d73e61377cfc8c44c60d79857c8a5c09be75ddd8c7e6bd1547895dc11f03a673
4
+ data.tar.gz: 7027bf24c300f0996fd12cb94875bfb16ab24f0119b4442547da1ea054fb365b
5
5
  SHA512:
6
- metadata.gz: 5b2c8e3ac69a2fc9ea125022eb1d8ac2fd63ee702c2b6e8b1300e6c5dc317ae9db8b5dc1fd96167ea418b1a1cded3f2b7282edd6f09a2ec4fd19783556b4eb4d
7
- data.tar.gz: f1a0312c57018c184383266dfeab2b9c2fe216431e63558996d14162e1c56291c85edee0ab00bab716498da4bd34f7d3dd56232ded31e48947b1c932078ac72a
6
+ metadata.gz: 5363d5c569e9b65c754066baac8407b7d4047b9ecb11c5ba81ac66cec9f2fe2096837feb4e1ce3940cf53374fddeaa0feea034553917285e1871c0f6f968f2b7
7
+ data.tar.gz: ac1e65f2064108e82a9686b895b1ae1ea6f7badc9a49b3bcdc5e299f861413b9aaedc0bc3ad17605c0cbd10ea95ca5abf2872abaa23af9067bf7480dd0692a8a
data/CHANGELOG.md CHANGED
@@ -2,6 +2,31 @@
2
2
 
3
3
  All notable changes to this project will be documented in this file.
4
4
 
5
+ ## [0.5.0] - 2026-09-12
6
+
7
+ ### Added
8
+
9
+ - Host-owned authorization through immutable, value-based `Rbacan::AuthorizationContext` and pluggable authorization/assignment adapters.
10
+ - Association-free `Rbacan::Authorizable` concern for applications that own their RBAC schema and lifecycle.
11
+ - Explicit user and context resolvers for hardened route constraints.
12
+ - Stable denial codes on `Rbacan::NotAuthorized` for host-side localization.
13
+ - Configurable `role_key` and `permission_key` columns.
14
+ - Deeply immutable JSON-like authorization metadata and copied immutable target codes.
15
+
16
+ ### Changed
17
+
18
+ - Hardened mode is the default. Authorization requires an explicit context and fails closed without a host adapter.
19
+ - Destructive/unscoped `Rbacan::Permittable`, catalog, role-permission, and seed-helper APIs require explicit `legacy_mode = true` opt-in.
20
+ - Authorization contexts require a tenant and snapshot/freeze JSON-like tenant, property, vertical, and subject identifiers plus their timestamp.
21
+ - Controller and view authorization use the host adapter in hardened mode.
22
+ - Hardened redirect denials resolve their stable code through the host I18n backend.
23
+ - Assignment dispatch rejects missing users or blank roles before invoking the host adapter.
24
+ - Gem packaging uses an explicit runtime/documentation allowlist and excludes repository-only files.
25
+
26
+ ### Compatibility
27
+
28
+ - Applications using the generated 0.4 schema must consciously enable legacy mode while migrating.
29
+
5
30
  ## [0.4.0] - 2026-04-13
6
31
 
7
32
  ### Added
data/README.md CHANGED
@@ -38,6 +38,60 @@ bundle install
38
38
 
39
39
  ## Setup
40
40
 
41
+ RBACan 0.5 has two explicit integration modes:
42
+
43
+ - **Host-owned mode (recommended):** the application owns schema, tenancy, scopes, assignment history,
44
+ and record policies. Include `Rbacan::Authorizable` and configure adapters. Do not run the generator.
45
+ - **Legacy generated-schema mode:** preserves the 0.4 Active Record API only after conscious
46
+ `config.legacy_mode = true` opt-in. Its removal methods delete join rows and are unsuitable for
47
+ immutable assignment-history requirements.
48
+
49
+ ### Host-owned mode
50
+
51
+ ```ruby
52
+ class User < ApplicationRecord
53
+ include Rbacan::Authorizable
54
+ end
55
+
56
+ Rbacan.configure do |config|
57
+ config.role_class = 'Role'
58
+ config.role_key = :code
59
+ config.permission_class = 'Permission'
60
+ config.permission_key = :code
61
+ config.authorization_adapter = MyAuthorizationAdapter.new
62
+ config.assignment_adapter = MyAssignmentAdapter.new
63
+ end
64
+ ```
65
+
66
+ Every check receives an explicit immutable value context. Pass stable identifiers or JSON-like
67
+ references, not live Active Record objects:
68
+
69
+ ```ruby
70
+ context = Rbacan::AuthorizationContext.new(
71
+ tenant: current_tenant.id,
72
+ property: current_property.id,
73
+ vertical: current_property.property_type.to_s,
74
+ subject: { type: reservation.class.name, id: reservation.id },
75
+ at: Time.current
76
+ )
77
+
78
+ current_user.can?(:hotel_reservations_read, context: context)
79
+ ```
80
+
81
+ The tenant identifier is required and cannot be nil, boolean, or empty. Tenant, property, vertical,
82
+ subject, metadata, and timestamp values are copied
83
+ and frozen when the context is created. Context identifiers must come from records already loaded
84
+ through the host application's authenticated scope; the authorization adapter resolves and validates
85
+ their relationships under the host's tenancy/RLS rules. The request's `user` remains the live host
86
+ principal object.
87
+
88
+ Adapters receive one request object. Authorization adapters respond to `allowed?(request)` or `call`;
89
+ assignment adapters respond to `assign(request)` and `revoke(request)`. RBACan never prescribes deletion
90
+ or caching in host-owned mode. Optional context metadata is deeply copied and frozen and accepts only
91
+ hashes, arrays, strings, finite integers/floats, booleans, nil, and symbols.
92
+
93
+ ### Legacy generated-schema mode
94
+
41
95
  **1. Run the generator**
42
96
 
43
97
  ```bash
@@ -72,13 +126,21 @@ rails db:seed
72
126
 
73
127
  ## Configuration
74
128
 
75
- The generator creates `config/initializers/rbacan.rb`. All options are optional — the defaults work for a standard Devise setup.
129
+ The generator creates `config/initializers/rbacan.rb`. Generated-schema applications must explicitly
130
+ enable legacy mode; the remaining defaults then work for a standard Devise setup.
76
131
 
77
132
  ```ruby
78
133
  Rbacan.configure do |config|
134
+ # Conscious opt-in required for the destructive/unscoped 0.4 API while migrating.
135
+ config.legacy_mode = true
136
+
79
137
  # Your user model name (default: "User")
80
138
  config.permittable_class = "User"
81
139
 
140
+ # Host key columns default to :name.
141
+ # config.role_key = :code
142
+ # config.permission_key = :code
143
+
82
144
  # Override model class names if you have custom implementations
83
145
  # config.role_class = "Rbacan::Role"
84
146
  # config.permission_class = "Rbacan::Permission"
@@ -107,7 +169,7 @@ end
107
169
 
108
170
  ## Defining Roles and Permissions
109
171
 
110
- Use the `Rbacan::RolesAndPermissions` module in your seeds. All methods are idempotent — safe to run multiple times.
172
+ In legacy mode, use the `Rbacan::RolesAndPermissions` module in your seeds. All methods are idempotent — safe to run multiple times. Hardened mode rejects these unscoped helpers; host-owned applications must seed their own scoped authorization records.
111
173
 
112
174
  ```ruby
113
175
  # db/seeds.rb
@@ -201,9 +263,20 @@ Include `Rbacan::Authorization` in your `ApplicationController` (or any specific
201
263
  ```ruby
202
264
  class ApplicationController < ActionController::Base
203
265
  include Rbacan::Authorization
266
+ helper_method :rbacan_authorization_context
267
+
268
+ private
269
+
270
+ def rbacan_authorization_context
271
+ AuthorizationContextResolver.call(request: request, user: current_user, subject: @post)
272
+ end
204
273
  end
205
274
  ```
206
275
 
276
+ The host resolver must build the immutable identifier context only after authenticating the user and
277
+ loading tenant, property, and optional subject records through the authorized scope.
278
+ `AuthorizationContextResolver` is application-defined; RBACan does not infer these values.
279
+
207
280
  Then use `authorize!` or `authorize_role!` as `before_action` callbacks:
208
281
 
209
282
  ```ruby
@@ -255,7 +328,9 @@ config.unauthorized_handler = ->(controller, permission:, role:) {
255
328
 
256
329
  ## View Helpers
257
330
 
258
- `Rbacan::ViewHelpers` is automatically included in all views. Use `authorized?` to conditionally render content:
331
+ `Rbacan::ViewHelpers` is automatically included in all views. In host-owned mode the host must expose
332
+ `current_user` and `rbacan_authorization_context` to the view. Use `authorized?` to conditionally
333
+ render content:
259
334
 
260
335
  ```erb
261
336
  <% authorized?(:delete_post) do %>
@@ -267,40 +342,47 @@ config.unauthorized_handler = ->(controller, permission:, role:) {
267
342
  <% end %>
268
343
  ```
269
344
 
270
- You can also use `has_role?` and `has_any_role?` directly in views since they are instance methods on the user:
345
+ You can also use `has_role?` directly in views through the host-owned concern:
271
346
 
272
347
  ```erb
273
- <% if current_user.has_role?(:admin) %>
348
+ <% if current_user.has_role?(:admin, context: rbacan_authorization_context) %>
274
349
  <%= link_to "Admin Panel", admin_root_path %>
275
350
  <% end %>
276
-
277
- <% if current_user.has_any_role?(:admin, :moderator) %>
278
- <%= link_to "Moderation Queue", moderation_path %>
279
- <% end %>
280
351
  ```
281
352
 
282
353
  ---
283
354
 
284
355
  ## Route Constraints
285
356
 
286
- Restrict access to entire route namespaces based on role or permission. Add constraints in `config/routes.rb`:
357
+ Restrict access to entire route namespaces based on role or permission. Hardened mode requires explicit
358
+ request resolvers:
287
359
 
288
360
  ```ruby
289
- # Restrict by role
290
- constraints Rbacan::RouteConstraint.new(role: :admin) do
361
+ resolve_user = ->(request) { CurrentSessionResolver.call(request) }
362
+ resolve_context = ->(request) { AuthorizationContextResolver.call(request) }
363
+
364
+ constraints Rbacan::RouteConstraint.new(
365
+ role: :admin,
366
+ user_resolver: resolve_user,
367
+ context_resolver: resolve_context
368
+ ) do
291
369
  namespace :admin do
292
370
  resources :users
293
371
  resources :roles
294
372
  end
295
373
  end
296
374
 
297
- # Restrict by permission
298
- constraints Rbacan::RouteConstraint.new(permission: :access_dashboard) do
375
+ constraints Rbacan::RouteConstraint.new(
376
+ permission: :access_dashboard,
377
+ user_resolver: resolve_user,
378
+ context_resolver: resolve_context
379
+ ) do
299
380
  get "/dashboard", to: "dashboard#index"
300
381
  end
301
382
  ```
302
383
 
303
- The constraint reads the current user from the Warden session (used by Devise). For apps using a manual session, it falls back to looking up `session[:user_id]`.
384
+ Without both resolvers hardened constraints fail closed. In explicitly enabled legacy mode only, the
385
+ constraint retains the 0.4 behavior of reading Warden or `session[:user_id]`.
304
386
 
305
387
  ---
306
388
 
@@ -1,31 +1,38 @@
1
1
  Rbacan.configure do |config|
2
- # The name of your user model (default: "User")
2
+ # Hardened host-owned mode is the default. The generated 0.4-style schema remains
3
+ # inactive until the host consciously opts into its destructive/unscoped APIs:
4
+ # config.legacy_mode = true
5
+
6
+ # Name of your user model (default: "User")
3
7
  # config.permittable_class = "User"
4
8
 
5
- # Override AR model class names if you have custom models
6
- # config.role_class = "Rbacan::Role"
7
- # config.permission_class = "Rbacan::Permission"
8
- # config.user_role_class = "Rbacan::UserRole"
9
- # config.role_permission_class = "Rbacan::RolePermission"
9
+ # Override ActiveRecord model class names if you use custom models
10
+ # config.role_class = "Rbacan::Role"
11
+ # config.permission_class = "Rbacan::Permission"
12
+ # config.user_role_class = "Rbacan::UserRole"
13
+ # config.role_permission_class = "Rbacan::RolePermission"
14
+
15
+ # Host role/permission lookup columns (default: :name)
16
+ # config.role_key = :code
17
+ # config.permission_key = :code
10
18
 
11
19
  # Override table names if needed
12
- # config.role_table = "roles"
13
- # config.permission_table = "permissions"
14
- # config.user_role_table = "user_roles"
15
- # config.role_permission_table = "role_permissions"
16
-
17
- # Primary key type for generated migrations.
18
- # Set to :uuid if your app uses UUID primary keys.
19
- # When nil, auto-detects from your Rails generator config (primary_key_type).
20
+ # config.role_table = "roles"
21
+ # config.permission_table = "permissions"
22
+ # config.user_role_table = "user_roles"
23
+ # config.role_permission_table = "role_permissions"
24
+
25
+ # Primary key type for migrations.
26
+ # Set :uuid if your app uses UUID primary keys.
27
+ # When nil, auto-detects Rails generator config (primary_key_type).
20
28
  # config.primary_key_type = :uuid
21
29
 
22
30
  # Enable tenant scoping for roles and user_roles.
23
- # When true, adds tenant_id to the roles and user_roles tables,
24
- # allowing tenant-specific roles and role assignments.
25
- # Permissions always remain global regardless of this setting.
31
+ # When true, adds tenant_id to roles and user_roles.
32
+ # Permissions remain global regardless of this setting.
26
33
  # config.tenant_scoped = false
27
34
 
28
- # The name of your tenant model class (default: "Tenant").
35
+ # Name of your tenant model class (default: "Tenant").
29
36
  # Only used when tenant_scoped is true.
30
37
  # config.tenant_class = "Tenant"
31
38
 
@@ -33,13 +40,6 @@ Rbacan.configure do |config|
33
40
  # :raise — raises Rbacan::NotAuthorized (default)
34
41
  # :redirect — redirects to unauthorized_redirect_path
35
42
  # lambda — called with (controller, permission:, role:)
36
- #
37
43
  # config.unauthorized_handler = :raise
38
- # config.unauthorized_handler = :redirect
39
- # config.unauthorized_handler = ->(controller, permission:, role:) {
40
- # controller.render plain: "Forbidden", status: :forbidden
41
- # }
42
-
43
- # Path to redirect to when unauthorized_handler is :redirect
44
44
  # config.unauthorized_redirect_path = "/"
45
45
  end
@@ -0,0 +1,15 @@
1
+ require 'active_support/concern'
2
+
3
+ module Rbacan
4
+ module Authorizable
5
+ extend ActiveSupport::Concern
6
+
7
+ def can?(permission, context:)
8
+ Rbacan.authorized?(user: self, permission: permission, context: context)
9
+ end
10
+
11
+ def has_role?(role, context:)
12
+ Rbacan.authorized?(user: self, role: role, context: context)
13
+ end
14
+ end
15
+ end
@@ -1,29 +1,51 @@
1
1
  require 'active_support/concern'
2
+ require 'i18n'
2
3
 
3
4
  module Rbacan
4
5
  module Authorization
5
6
  extend ActiveSupport::Concern
6
7
 
7
- included do
8
- helper_method :authorized? if respond_to?(:helper_method)
9
- end
10
-
11
8
  # Calls the unauthorized handler if current_user lacks the given permission.
12
- def authorize!(permission)
13
- unless current_user&.can?(permission.to_s)
9
+ def authorize!(permission, context: nil)
10
+ allowed = if Rbacan.legacy_mode
11
+ current_user&.can?(permission.to_s)
12
+ else
13
+ Rbacan.authorized?(
14
+ user: current_user,
15
+ permission: permission,
16
+ context: context || rbacan_authorization_context
17
+ )
18
+ end
19
+
20
+ unless allowed
14
21
  _handle_unauthorized(permission: permission.to_s)
15
22
  end
16
23
  end
17
24
 
18
25
  # Calls the unauthorized handler if current_user lacks the given role.
19
- def authorize_role!(role)
20
- unless current_user&.has_role?(role.to_s)
26
+ def authorize_role!(role, context: nil)
27
+ allowed = if Rbacan.legacy_mode
28
+ current_user&.has_role?(role.to_s)
29
+ else
30
+ Rbacan.authorized?(
31
+ user: current_user,
32
+ role: role,
33
+ context: context || rbacan_authorization_context
34
+ )
35
+ end
36
+
37
+ unless allowed
21
38
  _handle_unauthorized(role: role.to_s)
22
39
  end
23
40
  end
24
41
 
25
42
  private
26
43
 
44
+ def rbacan_authorization_context
45
+ raise Rbacan::ConfigurationError,
46
+ 'authorization context is required; define rbacan_authorization_context in the host controller'
47
+ end
48
+
27
49
  def _handle_unauthorized(permission: nil, role: nil)
28
50
  handler = Rbacan.unauthorized_handler
29
51
 
@@ -31,8 +53,14 @@ module Rbacan
31
53
  when :raise
32
54
  raise Rbacan::NotAuthorized.new(nil, permission: permission, role: role)
33
55
  when :redirect
56
+ error = Rbacan::NotAuthorized.new(nil, permission: permission, role: role)
57
+ alert = if Rbacan.legacy_mode
58
+ error.message
59
+ else
60
+ I18n.t(error.code, permission: permission, role: role, raise: true)
61
+ end
34
62
  redirect_to Rbacan.unauthorized_redirect_path,
35
- alert: Rbacan::NotAuthorized.new(nil, permission: permission, role: role).message
63
+ alert: alert
36
64
  else
37
65
  if handler.respond_to?(:call)
38
66
  handler.call(self, permission: permission, role: role)
@@ -0,0 +1,165 @@
1
+ module Rbacan
2
+ class ConfigurationError < StandardError; end
3
+ class UnsafeLegacyApi < StandardError; end
4
+
5
+ class AuthorizationContext
6
+ ATTRIBUTES = %i[tenant property vertical subject at metadata].freeze
7
+
8
+ attr_reader(*ATTRIBUTES)
9
+
10
+ def initialize(tenant:, property: nil, vertical: nil, subject: nil, at: Time.now, metadata: {})
11
+ raise ArgumentError, 'authorization tenant must identify a tenant' unless identifying_tenant?(tenant)
12
+
13
+ @tenant = deep_copy_and_freeze(tenant, label: 'authorization tenant')
14
+ @property = deep_copy_and_freeze(property, label: 'authorization property')
15
+ @vertical = deep_copy_and_freeze(vertical, label: 'authorization vertical')
16
+ @subject = deep_copy_and_freeze(subject, label: 'authorization subject')
17
+ @at = copy_and_freeze_time(at)
18
+ @metadata = deep_copy_and_freeze(metadata, label: 'authorization metadata')
19
+ freeze
20
+ end
21
+
22
+ private
23
+
24
+ def deep_copy_and_freeze(value, label:)
25
+ copy = case value
26
+ when Hash
27
+ value.to_h do |key, item|
28
+ [ deep_copy_and_freeze(key, label: label), deep_copy_and_freeze(item, label: label) ]
29
+ end
30
+ when Array
31
+ value.map { |item| deep_copy_and_freeze(item, label: label) }
32
+ when String
33
+ value.dup
34
+ when Integer
35
+ value
36
+ when Float
37
+ raise ArgumentError, "#{label} supports only finite JSON numbers" unless value.finite?
38
+
39
+ value
40
+ when NilClass, TrueClass, FalseClass, Symbol
41
+ value
42
+ else
43
+ raise ArgumentError,
44
+ "#{label} supports only hashes, arrays, strings, numbers, booleans, nil, and symbols"
45
+ end
46
+ copy.freeze
47
+ end
48
+
49
+ def copy_and_freeze_time(value)
50
+ time = value.to_time if value.respond_to?(:to_time)
51
+ raise ArgumentError, 'authorization timestamp must be time-like' unless time.is_a?(Time)
52
+
53
+ time.dup.freeze
54
+ end
55
+
56
+ def identifying_tenant?(value)
57
+ case value
58
+ when String, Array, Hash
59
+ !value.empty?
60
+ when Integer
61
+ true
62
+ when Float
63
+ value.finite?
64
+ when Symbol
65
+ !value.to_s.empty?
66
+ else
67
+ false
68
+ end
69
+ end
70
+ end
71
+
72
+ class AuthorizationRequest
73
+ attr_reader :user, :permission, :role, :context
74
+
75
+ def initialize(user:, permission:, role:, context:)
76
+ @user = user
77
+ @permission = permission&.to_s&.dup&.freeze
78
+ @role = role&.to_s&.dup&.freeze
79
+ @context = context
80
+ freeze
81
+ end
82
+ end
83
+
84
+ class AssignmentRequest
85
+ attr_reader :user, :role, :context
86
+
87
+ def initialize(user:, role:, context:)
88
+ @user = user
89
+ @role = role.to_s.dup.freeze
90
+ @context = context
91
+ freeze
92
+ end
93
+ end
94
+
95
+ class << self
96
+ def ensure_legacy_mode!(method_name)
97
+ return if legacy_mode
98
+
99
+ raise UnsafeLegacyApi,
100
+ "#{method_name} is a legacy unscoped API; set Rbacan.legacy_mode = true to opt in"
101
+ end
102
+
103
+ def authorized?(user:, permission: nil, role: nil, context:)
104
+ validate_authorization_target!(permission, role)
105
+ validate_context!(context)
106
+ return false unless user && authorization_adapter
107
+
108
+ request = AuthorizationRequest.new(
109
+ user: user,
110
+ permission: permission&.to_s,
111
+ role: role&.to_s,
112
+ context: context
113
+ )
114
+
115
+ !!call_authorization_adapter(request)
116
+ end
117
+
118
+ def assign_role(user:, role:, context:)
119
+ call_assignment_adapter(:assign, user: user, role: role, context: context)
120
+ end
121
+
122
+ def revoke_role(user:, role:, context:)
123
+ call_assignment_adapter(:revoke, user: user, role: role, context: context)
124
+ end
125
+
126
+ private
127
+
128
+ def validate_authorization_target!(permission, role)
129
+ if permission.nil? ^ role.nil?
130
+ target = permission || role
131
+ raise ArgumentError, 'authorization permission or role cannot be blank' if target.to_s.empty?
132
+
133
+ return
134
+ end
135
+
136
+ raise ArgumentError, 'Provide exactly one permission or role'
137
+ end
138
+
139
+ def validate_context!(context)
140
+ raise ArgumentError, 'authorization context is required' unless context.is_a?(AuthorizationContext)
141
+ end
142
+
143
+ def call_authorization_adapter(request)
144
+ if authorization_adapter.respond_to?(:allowed?)
145
+ authorization_adapter.allowed?(request)
146
+ elsif authorization_adapter.respond_to?(:call)
147
+ authorization_adapter.call(request)
148
+ else
149
+ raise ConfigurationError, 'authorization adapter must respond to allowed? or call'
150
+ end
151
+ end
152
+
153
+ def call_assignment_adapter(action, user:, role:, context:)
154
+ validate_context!(context)
155
+ raise ArgumentError, 'assignment user is required' unless user
156
+ raise ArgumentError, 'assignment role is required' if role.nil? || role.to_s.empty?
157
+ unless assignment_adapter&.respond_to?(action)
158
+ raise ConfigurationError, "assignment adapter must respond to #{action}"
159
+ end
160
+
161
+ request = AssignmentRequest.new(user: user, role: role.to_s, context: context)
162
+ assignment_adapter.public_send(action, request)
163
+ end
164
+ end
165
+ end
@@ -1,10 +1,17 @@
1
1
  module Rbacan
2
2
  class NotAuthorized < StandardError
3
- attr_reader :permission, :role
3
+ attr_reader :permission, :role, :code
4
4
 
5
5
  def initialize(message = nil, permission: nil, role: nil)
6
6
  @permission = permission
7
- @role = role
7
+ @role = role
8
+ @code = if permission
9
+ 'rbacan.not_authorized.permission'
10
+ elsif role
11
+ 'rbacan.not_authorized.role'
12
+ else
13
+ 'rbacan.not_authorized'
14
+ end
8
15
  super(message || default_message)
9
16
  end
10
17
 
@@ -16,7 +23,7 @@ module Rbacan
16
23
  elsif role
17
24
  "Not authorized: missing role '#{role}'"
18
25
  else
19
- "Not authorized"
26
+ 'Not authorized'
20
27
  end
21
28
  end
22
29
  end