rabarber 1.0.5 → 1.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +10 -0
- data/README.md +27 -13
- data/lib/generators/rabarber/templates/create_rabarber_roles.rb.erb +1 -1
- data/lib/rabarber/access.rb +9 -9
- data/lib/rabarber/configuration.rb +59 -19
- data/lib/rabarber/controllers/concerns/authorization.rb +16 -5
- data/lib/rabarber/helpers/helpers.rb +2 -2
- data/lib/rabarber/input/actions.rb +30 -0
- data/lib/rabarber/input/base.rb +33 -0
- data/lib/rabarber/input/dynamic_rules.rb +30 -0
- data/lib/rabarber/input/roles.rb +32 -0
- data/lib/rabarber/input/types/booleans.rb +19 -0
- data/lib/rabarber/input/types/procs.rb +19 -0
- data/lib/rabarber/input/types/symbols.rb +19 -0
- data/lib/rabarber/missing/actions.rb +24 -0
- data/lib/rabarber/missing/base.rb +62 -0
- data/lib/rabarber/missing/roles.rb +35 -0
- data/lib/rabarber/models/concerns/has_roles.rb +13 -7
- data/lib/rabarber/models/role.rb +1 -1
- data/lib/rabarber/permissions.rb +2 -2
- data/lib/rabarber/railtie.rb +14 -0
- data/lib/rabarber/rule.rb +20 -27
- data/lib/rabarber/version.rb +1 -1
- data/lib/rabarber.rb +18 -6
- metadata +13 -3
- data/lib/rabarber/role_names.rb +0 -20
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: e7eaa4eaef0f5c6d072e3a5b545b0ce152651df15b521be933aac78e1add85ba
|
4
|
+
data.tar.gz: d0557fcd900f2bebd58f6489c2f3f79c2f95c7c14bbeeeb59a294fdfcbcb3f49
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: cadc08751bf39ab0c6eb8a357854fe00bb478533a7f5a7e0760929d6acedad0fcecb31e17c21c980c764d59302a569df74e847f09c778b2e3f1c257c520c4336
|
7
|
+
data.tar.gz: 7067e81820ce07c0929e3f18ad079c9cfa44b0dc473138b042e4e882b8db1cda17bbc680a431634c42695434a088605fd897ab4a2e25a4d4d2b9bac7c8180fd5
|
data/CHANGELOG.md
CHANGED
@@ -1,3 +1,13 @@
|
|
1
|
+
## 1.2.0
|
2
|
+
|
3
|
+
- Enhance handling of missing actions and roles specified in `grant_access` method by raising an error for missing actions and logging a warning for missing roles.
|
4
|
+
- Introduce `when_actions_missing` and `when_roles_missing` configuration options, allowing to customize the behavior when actions or roles are not found.
|
5
|
+
|
6
|
+
## 1.1.0
|
7
|
+
|
8
|
+
- Add support for `unless` argument in `grant_access` method, allowing to define negated dynamic rules
|
9
|
+
- Fix a bug where specifying a dynamic rule as a symbol without specifying an action would result in an error
|
10
|
+
|
1
11
|
## 1.0.5
|
2
12
|
|
3
13
|
- Add co-author: [trafium](https://github.com/trafium)
|
data/README.md
CHANGED
@@ -3,13 +3,13 @@
|
|
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/enjaku4/rabarber/actions/workflows/ci.yml/badge.svg)](https://github.com/enjaku4/rabarber/actions/workflows/ci.yml)
|
5
5
|
|
6
|
-
Rabarber is
|
6
|
+
Rabarber is a role-based authorization library for Ruby on Rails, primarily designed for use in the application layer but not limited to that. It offers a set of useful tools for managing user roles and defining authorization rules.
|
7
7
|
|
8
8
|
---
|
9
9
|
|
10
10
|
**Example of Usage**:
|
11
11
|
|
12
|
-
Consider a CRM where users with different roles have distinct access levels. For instance, the role
|
12
|
+
Consider a CRM where users with different roles have distinct access levels. For instance, the role `accountant` can interact with invoices but cannot access marketing information, while the role `marketer` has access to marketing-related data. Such authorization rules can be easily defined with Rabarber.
|
13
13
|
|
14
14
|
---
|
15
15
|
|
@@ -63,20 +63,26 @@ rails db:migrate
|
|
63
63
|
|
64
64
|
## Configuration
|
65
65
|
|
66
|
-
Rabarber can be configured by
|
66
|
+
Rabarber can be configured by using `.configure` method in an initializer:
|
67
67
|
|
68
68
|
```rb
|
69
69
|
Rabarber.configure do |config|
|
70
70
|
config.current_user_method = :authenticated_user
|
71
71
|
config.must_have_roles = true
|
72
|
-
config.
|
73
|
-
|
74
|
-
}
|
72
|
+
config.when_actions_missing = -> (missing_actions, context) { ... }
|
73
|
+
config.when_roles_missing = -> (missing_roles, context) { ... }
|
74
|
+
config.when_unauthorized = -> (controller) { ... }
|
75
75
|
end
|
76
76
|
```
|
77
77
|
- `current_user_method` must be a symbol representing the method that returns the currently authenticated user. The default value is `:current_user`.
|
78
|
+
|
78
79
|
- `must_have_roles` must be a boolean determining whether a user with no roles can access endpoints permitted to everyone. The default value is `false` (allowing users without roles to access endpoints permitted to everyone).
|
79
|
-
|
80
|
+
|
81
|
+
- `when_actions_missing` must be a proc where you can define the behaviour when the actions specified in `grant_access` method cannot be found in the controller (`missing_actions` is an array of missing actions, `context` is a hash that looks like this: `{ controller: "InvoicesController" }`). This check is performed on every request and when the application is initialized if `eager_load` configuration is enabled in Rails. By default, an error is raised when actions are missing.
|
82
|
+
|
83
|
+
- `when_roles_missing` must be a proc where you can define the behaviour when the roles specified in `grant_access` method cannot be found in the database (`missing_roles` is an array of missing roles, `context` is a hash that looks like this: `{ controller: "InvoicesController", action: "index" }`). This check is performed on every request and when the application is initialized if `eager_load` configuration is enabled in Rails. By default, only a warning is logged when roles are missing.
|
84
|
+
|
85
|
+
- `when_unauthorized` must be a proc where you can define the behaviour when access is not authorized (`controller` is an instance of the controller where the code is executed). By default, the user is redirected back if the request format is HTML; otherwise, a 401 Unauthorized response is sent.
|
80
86
|
|
81
87
|
## Roles
|
82
88
|
|
@@ -108,7 +114,7 @@ You can also explicitly create new roles simply by using:
|
|
108
114
|
```rb
|
109
115
|
Rabarber::Role.create(name: "manager")
|
110
116
|
```
|
111
|
-
The role names
|
117
|
+
The role names must be unique.
|
112
118
|
|
113
119
|
**`#revoke_roles`**
|
114
120
|
|
@@ -145,8 +151,6 @@ Rabarber::Role.names
|
|
145
151
|
|
146
152
|
`Rabarber::Role` is a model that represents roles within your application. It has a single attribute, `name`, which is validated for both uniqueness and presence. You can treat `Rabarber::Role` as a regular Rails model and use Active Record methods on it if necessary.
|
147
153
|
|
148
|
-
Utilize the aforementioned methods to manipulate user roles. For example, create a custom UI for managing roles or assign necessary roles during migration or runtime (e.g., when the user is created). You can also write custom authorization policies based on `#has_role?` method (e.g., to scope the data that the user can access). Adapt these methods to fit the requirements of your application.
|
149
|
-
|
150
154
|
## Authorization Rules
|
151
155
|
|
152
156
|
Include `Rabarber::Authorization` module into the controller that needs authorization rules to be applied (authorization rules will be applied to the controller and its children). Typically, it is `ApplicationController`, but it can be any controller.
|
@@ -221,11 +225,12 @@ This allows everyone to access `OrdersController` and its children and `index` a
|
|
221
225
|
|
222
226
|
If you've set `must_have_roles` setting to `true`, then, only the users with at least one role can have access. This setting can be useful if your requirements are such that users without roles are not allowed to see anything.
|
223
227
|
|
224
|
-
For more complex
|
228
|
+
For more complex cases, Rabarber provides dynamic rules:
|
225
229
|
|
226
230
|
```rb
|
227
231
|
class OrdersController < ApplicationController
|
228
232
|
grant_access if: :user_has_access?
|
233
|
+
grant_access unless: :user_has_no_access?
|
229
234
|
...
|
230
235
|
|
231
236
|
private
|
@@ -233,6 +238,10 @@ class OrdersController < ApplicationController
|
|
233
238
|
def user_has_access?
|
234
239
|
...
|
235
240
|
end
|
241
|
+
|
242
|
+
def user_has_no_access?
|
243
|
+
...
|
244
|
+
end
|
236
245
|
end
|
237
246
|
|
238
247
|
class InvoicesController < ApplicationController
|
@@ -240,11 +249,16 @@ class InvoicesController < ApplicationController
|
|
240
249
|
def index
|
241
250
|
...
|
242
251
|
end
|
252
|
+
|
253
|
+
grant_access action: :show, roles: :client, unless: -> { current_user.banned? }
|
254
|
+
def show
|
255
|
+
...
|
256
|
+
end
|
243
257
|
end
|
244
258
|
```
|
245
|
-
You can pass a
|
259
|
+
You can pass a dynamic rule as `if` or `unless` argument. It can be a symbol (the method with the same name will be called) or a proc.
|
246
260
|
|
247
|
-
Rules defined in
|
261
|
+
Rules defined in child classes don't override parent rules but rather add to them:
|
248
262
|
```rb
|
249
263
|
class Crm::BaseController < ApplicationController
|
250
264
|
grant_access roles: :admin
|
@@ -12,6 +12,6 @@ class CreateRabarberRoles < ActiveRecord::Migration[<%= ActiveRecord::Migration.
|
|
12
12
|
t.belongs_to :roleable, null: false, index: true, foreign_key: { to_table: raise(Rabarber::Error, "Please specify your user model's table name") }
|
13
13
|
end
|
14
14
|
|
15
|
-
add_index :rabarber_roles_roleables,
|
15
|
+
add_index :rabarber_roles_roleables, [:role_id, :roleable_id], unique: true
|
16
16
|
end
|
17
17
|
end
|
data/lib/rabarber/access.rb
CHANGED
@@ -2,25 +2,25 @@
|
|
2
2
|
|
3
3
|
module Rabarber
|
4
4
|
module Access
|
5
|
-
def access_granted?(roles, controller, action,
|
6
|
-
controller_accessible?(roles, controller,
|
7
|
-
action_accessible?(roles, controller, action,
|
5
|
+
def access_granted?(roles, controller, action, dynamic_rule_receiver)
|
6
|
+
controller_accessible?(roles, controller, dynamic_rule_receiver) ||
|
7
|
+
action_accessible?(roles, controller, action, dynamic_rule_receiver)
|
8
8
|
end
|
9
9
|
|
10
|
-
def controller_accessible?(roles, controller,
|
11
|
-
accessible_controllers(roles,
|
10
|
+
def controller_accessible?(roles, controller, dynamic_rule_receiver)
|
11
|
+
accessible_controllers(roles, dynamic_rule_receiver).any? do |accessible_controller|
|
12
12
|
controller <= accessible_controller
|
13
13
|
end
|
14
14
|
end
|
15
15
|
|
16
|
-
def action_accessible?(roles, controller, action,
|
17
|
-
action_rules[controller].any? { |rule| rule.verify_access(roles,
|
16
|
+
def action_accessible?(roles, controller, action, dynamic_rule_receiver)
|
17
|
+
action_rules[controller].any? { |rule| rule.verify_access(roles, dynamic_rule_receiver, action) }
|
18
18
|
end
|
19
19
|
|
20
20
|
private
|
21
21
|
|
22
|
-
def accessible_controllers(roles,
|
23
|
-
controller_rules.select { |_, rule| rule.verify_access(roles,
|
22
|
+
def accessible_controllers(roles, dynamic_rule_receiver)
|
23
|
+
controller_rules.select { |_, rule| rule.verify_access(roles, dynamic_rule_receiver) }.keys
|
24
24
|
end
|
25
25
|
end
|
26
26
|
end
|
@@ -6,38 +6,78 @@ module Rabarber
|
|
6
6
|
class Configuration
|
7
7
|
include Singleton
|
8
8
|
|
9
|
-
attr_reader :current_user_method, :must_have_roles, :when_unauthorized
|
9
|
+
attr_reader :current_user_method, :must_have_roles, :when_actions_missing, :when_roles_missing, :when_unauthorized
|
10
10
|
|
11
11
|
def initialize
|
12
|
-
@current_user_method =
|
13
|
-
@must_have_roles =
|
14
|
-
@
|
15
|
-
|
16
|
-
|
17
|
-
else
|
18
|
-
controller.head(:unauthorized)
|
19
|
-
end
|
20
|
-
end
|
12
|
+
@current_user_method = default_current_user_method
|
13
|
+
@must_have_roles = default_must_have_roles
|
14
|
+
@when_actions_missing = default_when_actions_missing
|
15
|
+
@when_roles_missing = default_when_roles_missing
|
16
|
+
@when_unauthorized = default_when_unauthorized
|
21
17
|
end
|
22
18
|
|
23
19
|
def current_user_method=(method_name)
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
@current_user_method = method_name.to_sym
|
20
|
+
@current_user_method = Rabarber::Input::Types::Symbols.new(
|
21
|
+
method_name, Rabarber::ConfigurationError, "Configuration 'current_user_method' must be a Symbol or a String"
|
22
|
+
).process
|
29
23
|
end
|
30
24
|
|
31
25
|
def must_have_roles=(value)
|
32
|
-
|
26
|
+
@must_have_roles = Rabarber::Input::Types::Booleans.new(
|
27
|
+
value, Rabarber::ConfigurationError, "Configuration 'must_have_roles' must be a Boolean"
|
28
|
+
).process
|
29
|
+
end
|
30
|
+
|
31
|
+
def when_actions_missing=(callable)
|
32
|
+
@when_actions_missing = Rabarber::Input::Types::Procs.new(
|
33
|
+
callable, Rabarber::ConfigurationError, "Configuration 'when_actions_missing' must be a Proc"
|
34
|
+
).process
|
35
|
+
end
|
33
36
|
|
34
|
-
|
37
|
+
def when_roles_missing=(callable)
|
38
|
+
@when_roles_missing = Rabarber::Input::Types::Procs.new(
|
39
|
+
callable, Rabarber::ConfigurationError, "Configuration 'when_roles_missing' must be a Proc"
|
40
|
+
).process
|
35
41
|
end
|
36
42
|
|
37
43
|
def when_unauthorized=(callable)
|
38
|
-
|
44
|
+
@when_unauthorized = Rabarber::Input::Types::Procs.new(
|
45
|
+
callable, Rabarber::ConfigurationError, "Configuration 'when_unauthorized' must be a Proc"
|
46
|
+
).process
|
47
|
+
end
|
48
|
+
|
49
|
+
private
|
39
50
|
|
40
|
-
|
51
|
+
def default_current_user_method
|
52
|
+
:current_user
|
53
|
+
end
|
54
|
+
|
55
|
+
def default_must_have_roles
|
56
|
+
false
|
57
|
+
end
|
58
|
+
|
59
|
+
def default_when_actions_missing
|
60
|
+
-> (missing_actions, context) {
|
61
|
+
raise Rabarber::Error, "Missing actions: #{missing_actions}, context: #{context[:controller]}"
|
62
|
+
}
|
63
|
+
end
|
64
|
+
|
65
|
+
def default_when_roles_missing
|
66
|
+
-> (missing_roles, context) {
|
67
|
+
delimiter = context[:action] ? "#" : ""
|
68
|
+
message = "Missing roles: #{missing_roles}, context: #{context[:controller]}#{delimiter}#{context[:action]}"
|
69
|
+
Rails.logger.tagged("Rabarber") { Rails.logger.warn message }
|
70
|
+
}
|
71
|
+
end
|
72
|
+
|
73
|
+
def default_when_unauthorized
|
74
|
+
-> (controller) do
|
75
|
+
if controller.request.format.html?
|
76
|
+
controller.redirect_back fallback_location: controller.main_app.root_path
|
77
|
+
else
|
78
|
+
controller.head(:unauthorized)
|
79
|
+
end
|
80
|
+
end
|
41
81
|
end
|
42
82
|
end
|
43
83
|
end
|
@@ -9,19 +9,30 @@ module Rabarber
|
|
9
9
|
end
|
10
10
|
|
11
11
|
class_methods do
|
12
|
-
def grant_access(action: nil, roles: nil, if: nil)
|
13
|
-
|
12
|
+
def grant_access(action: nil, roles: nil, if: nil, unless: nil)
|
13
|
+
dynamic_rule, negated_dynamic_rule = binding.local_variable_get(:if), binding.local_variable_get(:unless)
|
14
|
+
|
15
|
+
Rabarber::Permissions.add(
|
16
|
+
self,
|
17
|
+
Rabarber::Input::Actions.new(action).process,
|
18
|
+
Rabarber::Input::Roles.new(roles).process,
|
19
|
+
Rabarber::Input::DynamicRules.new(dynamic_rule).process,
|
20
|
+
Rabarber::Input::DynamicRules.new(negated_dynamic_rule).process
|
21
|
+
)
|
14
22
|
end
|
15
23
|
end
|
16
24
|
|
17
25
|
private
|
18
26
|
|
19
27
|
def verify_access
|
20
|
-
|
21
|
-
|
28
|
+
Rabarber::Missing::Actions.new(self.class).handle
|
29
|
+
Rabarber::Missing::Roles.new(self.class).handle
|
30
|
+
|
31
|
+
return if Rabarber::Permissions.access_granted?(
|
32
|
+
send(Rabarber::Configuration.instance.current_user_method).roles, self.class, action_name.to_sym, self
|
22
33
|
)
|
23
34
|
|
24
|
-
|
35
|
+
Rabarber::Configuration.instance.when_unauthorized.call(self)
|
25
36
|
end
|
26
37
|
end
|
27
38
|
end
|
@@ -3,13 +3,13 @@
|
|
3
3
|
module Rabarber
|
4
4
|
module Helpers
|
5
5
|
def visible_to(*roles, &block)
|
6
|
-
return unless send(
|
6
|
+
return unless send(Rabarber::Configuration.instance.current_user_method).has_role?(*roles)
|
7
7
|
|
8
8
|
capture(&block)
|
9
9
|
end
|
10
10
|
|
11
11
|
def hidden_from(*roles, &block)
|
12
|
-
return if send(
|
12
|
+
return if send(Rabarber::Configuration.instance.current_user_method).has_role?(*roles)
|
13
13
|
|
14
14
|
capture(&block)
|
15
15
|
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Rabarber
|
4
|
+
module Input
|
5
|
+
class Actions < Rabarber::Input::Base
|
6
|
+
def initialize(
|
7
|
+
value,
|
8
|
+
error_type = Rabarber::InvalidArgumentError,
|
9
|
+
error_message = "Action name must be a Symbol or a String"
|
10
|
+
)
|
11
|
+
super
|
12
|
+
end
|
13
|
+
|
14
|
+
private
|
15
|
+
|
16
|
+
def valid?
|
17
|
+
(value.is_a?(String) || value.is_a?(Symbol)) && value.present? || value.nil?
|
18
|
+
end
|
19
|
+
|
20
|
+
def processed_value
|
21
|
+
case value
|
22
|
+
when String, Symbol
|
23
|
+
value.to_sym
|
24
|
+
when nil
|
25
|
+
value
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Rabarber
|
4
|
+
module Input
|
5
|
+
class Base
|
6
|
+
attr_reader :value, :error_type, :error_message
|
7
|
+
|
8
|
+
def initialize(value, error_type, error_message)
|
9
|
+
@value = value
|
10
|
+
@error_type = error_type
|
11
|
+
@error_message = error_message
|
12
|
+
end
|
13
|
+
|
14
|
+
def process
|
15
|
+
valid? ? processed_value : raise_error
|
16
|
+
end
|
17
|
+
|
18
|
+
private
|
19
|
+
|
20
|
+
def valid?
|
21
|
+
raise NotImplementedError
|
22
|
+
end
|
23
|
+
|
24
|
+
def processed_value
|
25
|
+
raise NotImplementedError
|
26
|
+
end
|
27
|
+
|
28
|
+
def raise_error
|
29
|
+
raise error_type, error_message
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Rabarber
|
4
|
+
module Input
|
5
|
+
class DynamicRules < Rabarber::Input::Base
|
6
|
+
def initialize(
|
7
|
+
value,
|
8
|
+
error_type = Rabarber::InvalidArgumentError,
|
9
|
+
error_message = "Dynamic rule must be a Symbol, a String, or a Proc"
|
10
|
+
)
|
11
|
+
super
|
12
|
+
end
|
13
|
+
|
14
|
+
private
|
15
|
+
|
16
|
+
def valid?
|
17
|
+
(value.is_a?(String) || value.is_a?(Symbol)) && value.present? || value.nil? || value.is_a?(Proc)
|
18
|
+
end
|
19
|
+
|
20
|
+
def processed_value
|
21
|
+
case value
|
22
|
+
when String, Symbol
|
23
|
+
value.to_sym
|
24
|
+
when Proc, nil
|
25
|
+
value
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Rabarber
|
4
|
+
module Input
|
5
|
+
class Roles < Rabarber::Input::Base
|
6
|
+
REGEX = /\A[a-z0-9_]+\z/
|
7
|
+
|
8
|
+
def initialize(
|
9
|
+
value,
|
10
|
+
error_type = Rabarber::InvalidArgumentError,
|
11
|
+
error_message =
|
12
|
+
"Role names must be Symbols or Strings and may only contain lowercase letters, numbers and underscores"
|
13
|
+
)
|
14
|
+
super
|
15
|
+
end
|
16
|
+
|
17
|
+
def value
|
18
|
+
Array(super)
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
|
23
|
+
def valid?
|
24
|
+
value.all? { |role_name| (role_name.is_a?(Symbol) || role_name.is_a?(String)) && role_name.to_s.match?(REGEX) }
|
25
|
+
end
|
26
|
+
|
27
|
+
def processed_value
|
28
|
+
value.map(&:to_sym)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Rabarber
|
4
|
+
module Input
|
5
|
+
module Types
|
6
|
+
class Booleans < Rabarber::Input::Base
|
7
|
+
private
|
8
|
+
|
9
|
+
def valid?
|
10
|
+
[true, false].include?(value)
|
11
|
+
end
|
12
|
+
|
13
|
+
def processed_value
|
14
|
+
value
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Rabarber
|
4
|
+
module Input
|
5
|
+
module Types
|
6
|
+
class Symbols < Rabarber::Input::Base
|
7
|
+
private
|
8
|
+
|
9
|
+
def valid?
|
10
|
+
(value.is_a?(Symbol) || value.is_a?(String)) && value.present?
|
11
|
+
end
|
12
|
+
|
13
|
+
def processed_value
|
14
|
+
value.to_sym
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Rabarber
|
4
|
+
module Missing
|
5
|
+
class Actions < Rabarber::Missing::Base
|
6
|
+
private
|
7
|
+
|
8
|
+
def check_controller_rules
|
9
|
+
nil
|
10
|
+
end
|
11
|
+
|
12
|
+
def check_action_rules
|
13
|
+
action_rules.each do |controller, controller_action_rules|
|
14
|
+
missing_actions = controller_action_rules.map(&:action) - controller.action_methods.map(&:to_sym)
|
15
|
+
missing_list << Rabarber::Missing::Item.new(missing_actions, controller, nil) if missing_actions.present?
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def configuration_name
|
20
|
+
:when_actions_missing
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Rabarber
|
4
|
+
module Missing
|
5
|
+
class Base
|
6
|
+
attr_reader :controller
|
7
|
+
|
8
|
+
def initialize(controller = nil)
|
9
|
+
@controller = controller
|
10
|
+
end
|
11
|
+
|
12
|
+
def handle
|
13
|
+
check_controller_rules
|
14
|
+
check_action_rules
|
15
|
+
|
16
|
+
return if missing_list.empty?
|
17
|
+
|
18
|
+
missing_list.each do |item|
|
19
|
+
Rabarber::Configuration.instance.public_send(configuration_name).call(
|
20
|
+
item.missing, controller: item.controller, action: item.action
|
21
|
+
)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
private
|
26
|
+
|
27
|
+
def check_controller_rules
|
28
|
+
raise NotImplementedError
|
29
|
+
end
|
30
|
+
|
31
|
+
def check_action_rules
|
32
|
+
raise NotImplementedError
|
33
|
+
end
|
34
|
+
|
35
|
+
def configuration_name
|
36
|
+
raise NotImplementedError
|
37
|
+
end
|
38
|
+
|
39
|
+
def missing_list
|
40
|
+
@missing_list ||= []
|
41
|
+
end
|
42
|
+
|
43
|
+
def controller_rules
|
44
|
+
if controller
|
45
|
+
{ controller => Rabarber::Permissions.controller_rules[controller] }
|
46
|
+
else
|
47
|
+
Rabarber::Permissions.controller_rules
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def action_rules
|
52
|
+
if controller
|
53
|
+
{ controller => Rabarber::Permissions.action_rules[controller] }
|
54
|
+
else
|
55
|
+
Rabarber::Permissions.action_rules
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
Item = Struct.new(:missing, :controller, :action)
|
61
|
+
end
|
62
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Rabarber
|
4
|
+
module Missing
|
5
|
+
class Roles < Rabarber::Missing::Base
|
6
|
+
private
|
7
|
+
|
8
|
+
def check_controller_rules
|
9
|
+
controller_rules.each do |controller, controller_rule|
|
10
|
+
missing_roles = controller_rule.roles - all_roles if controller_rule.present?
|
11
|
+
missing_list << Rabarber::Missing::Item.new(missing_roles, controller, nil) if missing_roles.present?
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
def check_action_rules
|
16
|
+
action_rules.each do |controller, controller_action_rules|
|
17
|
+
controller_action_rules.each do |action_rule|
|
18
|
+
missing_roles = action_rule.roles - all_roles
|
19
|
+
if missing_roles.any?
|
20
|
+
missing_list << Rabarber::Missing::Item.new(missing_roles, controller, action_rule.action)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def configuration_name
|
27
|
+
:when_roles_missing
|
28
|
+
end
|
29
|
+
|
30
|
+
def all_roles
|
31
|
+
@all_roles ||= Rabarber::Role.names
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -5,7 +5,9 @@ module Rabarber
|
|
5
5
|
extend ActiveSupport::Concern
|
6
6
|
|
7
7
|
included do
|
8
|
-
|
8
|
+
if defined?(@@included) && @@included != name
|
9
|
+
raise Rabarber::Error, "Rabarber::HasRoles can only be included once"
|
10
|
+
end
|
9
11
|
|
10
12
|
@@included = name
|
11
13
|
|
@@ -19,26 +21,30 @@ module Rabarber
|
|
19
21
|
end
|
20
22
|
|
21
23
|
def has_role?(*role_names)
|
22
|
-
(roles &
|
24
|
+
(roles & process_role_names(role_names)).any?
|
23
25
|
end
|
24
26
|
|
25
27
|
def assign_roles(*role_names, create_new: true)
|
26
|
-
roles_to_assign =
|
28
|
+
roles_to_assign = process_role_names(role_names)
|
27
29
|
|
28
30
|
create_new_roles(roles_to_assign) if create_new
|
29
31
|
|
30
|
-
rabarber_roles << Role.where(name: roles_to_assign) - rabarber_roles
|
32
|
+
rabarber_roles << Rabarber::Role.where(name: roles_to_assign) - rabarber_roles
|
31
33
|
end
|
32
34
|
|
33
35
|
def revoke_roles(*role_names)
|
34
|
-
self.rabarber_roles = rabarber_roles - Role.where(name:
|
36
|
+
self.rabarber_roles = rabarber_roles - Rabarber::Role.where(name: process_role_names(role_names))
|
35
37
|
end
|
36
38
|
|
37
39
|
private
|
38
40
|
|
39
41
|
def create_new_roles(role_names)
|
40
|
-
new_roles = role_names - Role.names
|
41
|
-
new_roles.each { |role_name| Role.create!(name: role_name) }
|
42
|
+
new_roles = role_names - Rabarber::Role.names
|
43
|
+
new_roles.each { |role_name| Rabarber::Role.create!(name: role_name) }
|
44
|
+
end
|
45
|
+
|
46
|
+
def process_role_names(role_names)
|
47
|
+
Rabarber::Input::Roles.new(role_names).process
|
42
48
|
end
|
43
49
|
end
|
44
50
|
end
|
data/lib/rabarber/models/role.rb
CHANGED
@@ -4,7 +4,7 @@ module Rabarber
|
|
4
4
|
class Role < ActiveRecord::Base
|
5
5
|
self.table_name = "rabarber_roles"
|
6
6
|
|
7
|
-
validates :name, presence: true, uniqueness: true, format: { with:
|
7
|
+
validates :name, presence: true, uniqueness: true, format: { with: Rabarber::Input::Roles::REGEX }
|
8
8
|
|
9
9
|
def self.names
|
10
10
|
pluck(:name).map(&:to_sym)
|
data/lib/rabarber/permissions.rb
CHANGED
@@ -15,8 +15,8 @@ module Rabarber
|
|
15
15
|
@storage = { controller_rules: Hash.new({}), action_rules: Hash.new([]) }
|
16
16
|
end
|
17
17
|
|
18
|
-
def self.
|
19
|
-
rule = Rule.new(action, roles,
|
18
|
+
def self.add(controller, action, roles, dynamic_rule, negated_dynamic_rule)
|
19
|
+
rule = Rabarber::Rule.new(action, roles, dynamic_rule, negated_dynamic_rule)
|
20
20
|
|
21
21
|
if action
|
22
22
|
instance.storage[:action_rules][controller] += [rule]
|
@@ -0,0 +1,14 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "rails/railtie"
|
4
|
+
|
5
|
+
module Rabarber
|
6
|
+
class Railtie < Rails::Railtie
|
7
|
+
initializer "rabarber.after_initialize" do |app|
|
8
|
+
app.config.after_initialize do
|
9
|
+
Rabarber::Missing::Actions.new.handle
|
10
|
+
Rabarber::Missing::Roles.new.handle
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
data/lib/rabarber/rule.rb
CHANGED
@@ -2,16 +2,17 @@
|
|
2
2
|
|
3
3
|
module Rabarber
|
4
4
|
class Rule
|
5
|
-
attr_reader :action, :roles, :
|
5
|
+
attr_reader :action, :roles, :dynamic_rule, :negated_dynamic_rule
|
6
6
|
|
7
|
-
def initialize(action, roles,
|
8
|
-
@action =
|
9
|
-
@roles =
|
10
|
-
@
|
7
|
+
def initialize(action, roles, dynamic_rule, negated_dynamic_rule)
|
8
|
+
@action = action
|
9
|
+
@roles = Array(roles)
|
10
|
+
@dynamic_rule = dynamic_rule
|
11
|
+
@negated_dynamic_rule = negated_dynamic_rule
|
11
12
|
end
|
12
13
|
|
13
|
-
def verify_access(user_roles,
|
14
|
-
action_accessible?(action_name) && roles_permitted?(user_roles) &&
|
14
|
+
def verify_access(user_roles, dynamic_rule_receiver, action_name = nil)
|
15
|
+
action_accessible?(action_name) && roles_permitted?(user_roles) && dynamic_rule_followed?(dynamic_rule_receiver)
|
15
16
|
end
|
16
17
|
|
17
18
|
def action_accessible?(action_name)
|
@@ -19,37 +20,29 @@ module Rabarber
|
|
19
20
|
end
|
20
21
|
|
21
22
|
def roles_permitted?(user_roles)
|
22
|
-
return false if
|
23
|
+
return false if Rabarber::Configuration.instance.must_have_roles && user_roles.empty?
|
23
24
|
|
24
25
|
roles.empty? || (roles & user_roles).any?
|
25
26
|
end
|
26
27
|
|
27
|
-
def
|
28
|
-
|
28
|
+
def dynamic_rule_followed?(dynamic_rule_receiver)
|
29
|
+
!!(execute_dynamic_rule(dynamic_rule_receiver, false) && execute_dynamic_rule(dynamic_rule_receiver, true))
|
29
30
|
end
|
30
31
|
|
31
32
|
private
|
32
33
|
|
33
|
-
def
|
34
|
-
|
35
|
-
custom_rule_receiver.instance_exec(&custom)
|
36
|
-
else
|
37
|
-
custom_rule_receiver.send(custom)
|
38
|
-
end
|
39
|
-
end
|
40
|
-
|
41
|
-
def pre_process_action(action)
|
42
|
-
return action.to_sym if (action.is_a?(String) || action.is_a?(Symbol)) && action.present?
|
43
|
-
return action if action.nil?
|
34
|
+
def execute_dynamic_rule(dynamic_rule_receiver, is_negated)
|
35
|
+
rule = is_negated ? negated_dynamic_rule : dynamic_rule
|
44
36
|
|
45
|
-
|
46
|
-
end
|
37
|
+
return true if rule.nil?
|
47
38
|
|
48
|
-
|
49
|
-
|
50
|
-
|
39
|
+
result = if rule.is_a?(Proc)
|
40
|
+
dynamic_rule_receiver.instance_exec(&rule)
|
41
|
+
else
|
42
|
+
dynamic_rule_receiver.send(rule)
|
43
|
+
end
|
51
44
|
|
52
|
-
|
45
|
+
is_negated ? !result : result
|
53
46
|
end
|
54
47
|
end
|
55
48
|
end
|
data/lib/rabarber/version.rb
CHANGED
data/lib/rabarber.rb
CHANGED
@@ -6,7 +6,17 @@ require_relative "rabarber/configuration"
|
|
6
6
|
require "active_record"
|
7
7
|
require "active_support"
|
8
8
|
|
9
|
-
require_relative "rabarber/
|
9
|
+
require_relative "rabarber/input/base"
|
10
|
+
require_relative "rabarber/input/actions"
|
11
|
+
require_relative "rabarber/input/dynamic_rules"
|
12
|
+
require_relative "rabarber/input/roles"
|
13
|
+
require_relative "rabarber/input/types/booleans"
|
14
|
+
require_relative "rabarber/input/types/procs"
|
15
|
+
require_relative "rabarber/input/types/symbols"
|
16
|
+
|
17
|
+
require_relative "rabarber/missing/base"
|
18
|
+
require_relative "rabarber/missing/actions"
|
19
|
+
require_relative "rabarber/missing/roles"
|
10
20
|
|
11
21
|
require_relative "rabarber/controllers/concerns/authorization"
|
12
22
|
require_relative "rabarber/helpers/helpers"
|
@@ -14,14 +24,16 @@ require_relative "rabarber/models/concerns/has_roles"
|
|
14
24
|
require_relative "rabarber/models/role"
|
15
25
|
require_relative "rabarber/permissions"
|
16
26
|
|
27
|
+
require_relative "rabarber/railtie"
|
28
|
+
|
17
29
|
module Rabarber
|
18
30
|
module_function
|
19
31
|
|
32
|
+
class Error < StandardError; end
|
33
|
+
class ConfigurationError < Rabarber::Error; end
|
34
|
+
class InvalidArgumentError < Rabarber::Error; end
|
35
|
+
|
20
36
|
def configure
|
21
|
-
yield(Configuration.instance)
|
37
|
+
yield(Rabarber::Configuration.instance)
|
22
38
|
end
|
23
|
-
|
24
|
-
class Error < StandardError; end
|
25
|
-
class ConfigurationError < Error; end
|
26
|
-
class InvalidArgumentError < Error; end
|
27
39
|
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: 1.0
|
4
|
+
version: 1.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- enjaku4
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2024-01-
|
12
|
+
date: 2024-01-28 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: rails
|
@@ -42,10 +42,20 @@ files:
|
|
42
42
|
- lib/rabarber/configuration.rb
|
43
43
|
- lib/rabarber/controllers/concerns/authorization.rb
|
44
44
|
- lib/rabarber/helpers/helpers.rb
|
45
|
+
- lib/rabarber/input/actions.rb
|
46
|
+
- lib/rabarber/input/base.rb
|
47
|
+
- lib/rabarber/input/dynamic_rules.rb
|
48
|
+
- lib/rabarber/input/roles.rb
|
49
|
+
- lib/rabarber/input/types/booleans.rb
|
50
|
+
- lib/rabarber/input/types/procs.rb
|
51
|
+
- lib/rabarber/input/types/symbols.rb
|
52
|
+
- lib/rabarber/missing/actions.rb
|
53
|
+
- lib/rabarber/missing/base.rb
|
54
|
+
- lib/rabarber/missing/roles.rb
|
45
55
|
- lib/rabarber/models/concerns/has_roles.rb
|
46
56
|
- lib/rabarber/models/role.rb
|
47
57
|
- lib/rabarber/permissions.rb
|
48
|
-
- lib/rabarber/
|
58
|
+
- lib/rabarber/railtie.rb
|
49
59
|
- lib/rabarber/rule.rb
|
50
60
|
- lib/rabarber/version.rb
|
51
61
|
- rabarber.gemspec
|
data/lib/rabarber/role_names.rb
DELETED
@@ -1,20 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module Rabarber
|
4
|
-
module RoleNames
|
5
|
-
module_function
|
6
|
-
|
7
|
-
REGEX = /\A[a-z0-9_]+\z/
|
8
|
-
|
9
|
-
def pre_process(role_names)
|
10
|
-
return role_names.map(&:to_sym) if role_names.all? do |role_name|
|
11
|
-
(role_name.is_a?(Symbol) || role_name.is_a?(String)) && role_name.to_s.match?(REGEX)
|
12
|
-
end
|
13
|
-
|
14
|
-
raise(
|
15
|
-
InvalidArgumentError,
|
16
|
-
"Role names must be Symbols or Strings and may only contain lowercase letters, numbers and underscores"
|
17
|
-
)
|
18
|
-
end
|
19
|
-
end
|
20
|
-
end
|