rabarber 1.1.0 → 1.2.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +12 -0
- data/LICENSE.txt +1 -1
- data/README.md +43 -31
- data/lib/generators/rabarber/roles_generator.rb +2 -0
- data/lib/generators/rabarber/templates/create_rabarber_roles.rb.erb +2 -2
- data/lib/rabarber/cache.rb +29 -0
- data/lib/rabarber/configuration.rb +72 -19
- data/lib/rabarber/controllers/concerns/authorization.rb +17 -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 +61 -0
- data/lib/rabarber/missing/roles.rb +37 -0
- data/lib/rabarber/models/concerns/has_roles.rb +24 -8
- data/lib/rabarber/models/role.rb +9 -1
- data/lib/rabarber/permissions.rb +2 -2
- data/lib/rabarber/railtie.rb +14 -0
- data/lib/rabarber/rule.rb +5 -19
- data/lib/rabarber/version.rb +1 -1
- data/lib/rabarber.rb +20 -6
- data/rabarber.gemspec +1 -1
- metadata +15 -4
- 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: 3ae0f6a7f272a6d718ddf591e61b236a20b26da7c7eddc56f838637e6fd16b72
|
4
|
+
data.tar.gz: 07bb483c2ed0fc8e04b12fab9183d333cddae5ef4d3b3690d98119e1d9d97c5b
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 8c87020ec8feb37dc344f332843eb49498197edbeab72d3c7b57edfb5e727029627aa5b8289fa506bdcb262d2df47fc378ca3d17f138879b3a64e51e998d22ab
|
7
|
+
data.tar.gz: d4e83ff02a8dfc17e8a2706756fa84f4e93035bd09c98455e8f5f2eb92f9c48636135200ed42fe6b6a6bda94671c187de97c1a85d6a21425d93191423c38711f
|
data/CHANGELOG.md
CHANGED
@@ -1,3 +1,15 @@
|
|
1
|
+
## 1.2.1
|
2
|
+
|
3
|
+
- Cache roles to avoid unnecessary database queries
|
4
|
+
- Introduce `cache_enabled` configuration option allowing to enable or disable role caching
|
5
|
+
- Enhance the migration generator so that it can receive the table name of the model representing users in the application as an argument
|
6
|
+
- Various minor improvements
|
7
|
+
|
8
|
+
## 1.2.0
|
9
|
+
|
10
|
+
- 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
|
11
|
+
- Introduce `when_actions_missing` and `when_roles_missing` configuration options, allowing to customize the behavior when actions or roles are not found
|
12
|
+
|
1
13
|
## 1.1.0
|
2
14
|
|
3
15
|
- Add support for `unless` argument in `grant_access` method, allowing to define negated dynamic rules
|
data/LICENSE.txt
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
The MIT License (MIT)
|
2
2
|
|
3
|
-
Copyright (c) 2023 enjaku4, trafium
|
3
|
+
Copyright (c) 2023 enjaku4 (https://github.com/enjaku4), trafium (https://github.com/trafium)
|
4
4
|
|
5
5
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
6
|
of this software and associated documentation files (the "Software"), to deal
|
data/README.md
CHANGED
@@ -3,7 +3,9 @@
|
|
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, designed primarily for use in the web layer (specifically controllers and views) but not limited to that. It provides tools for managing user roles and defining authorization rules, mainly focusing on answering the question of 'Who can access which endpoint?'.
|
7
|
+
|
8
|
+
Unlike some other libraries, Rabarber does not handle data scoping. Instead, it focuses on providing a lightweight and flexible solution for role-based access control, allowing developers to implement data scoping according to their specific business rules directly within their application's code.
|
7
9
|
|
8
10
|
---
|
9
11
|
|
@@ -45,16 +47,14 @@ Install the gem:
|
|
45
47
|
bundle install
|
46
48
|
```
|
47
49
|
|
48
|
-
Next, generate a migration to create tables for storing roles in the database:
|
50
|
+
Next, generate a migration to create tables for storing roles in the database. Make sure to specify the table name of the model representing users in your application as an argument. For instance, if the table name is `users`, run:
|
49
51
|
|
50
52
|
```
|
51
|
-
rails g rabarber:roles
|
53
|
+
rails g rabarber:roles users
|
52
54
|
```
|
53
55
|
|
54
56
|
This will create a migration file in `db/migrate` directory.
|
55
57
|
|
56
|
-
Replace `raise(Rabarber::Error, "Please specify your user model's table name")` in that file with the name of your user model's table.
|
57
|
-
|
58
58
|
Finally, run the migration to apply the changes to the database:
|
59
59
|
|
60
60
|
```
|
@@ -63,20 +63,30 @@ 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
|
+
config.cache_enabled = false
|
70
71
|
config.current_user_method = :authenticated_user
|
71
72
|
config.must_have_roles = true
|
72
|
-
config.
|
73
|
-
|
74
|
-
}
|
73
|
+
config.when_actions_missing = -> (missing_actions, context) { ... }
|
74
|
+
config.when_roles_missing = -> (missing_roles, context) { ... }
|
75
|
+
config.when_unauthorized = -> (controller) { ... }
|
75
76
|
end
|
76
77
|
```
|
78
|
+
|
79
|
+
- `cache_enabled` must be a boolean determining whether roles are cached. Roles are cached by default to avoid unnecessary database queries. If you want to disable caching, set this option to `false`. If caching is enabled and you need to clear the cache, use the `Rabarber::Cache.clear` method.
|
80
|
+
|
77
81
|
- `current_user_method` must be a symbol representing the method that returns the currently authenticated user. The default value is `:current_user`.
|
82
|
+
|
78
83
|
- `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
|
-
|
84
|
+
|
85
|
+
- `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.
|
86
|
+
|
87
|
+
- `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.
|
88
|
+
|
89
|
+
- `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
90
|
|
81
91
|
## Roles
|
82
92
|
|
@@ -103,13 +113,6 @@ By default, `#assign_roles` method will automatically create any roles that don'
|
|
103
113
|
user.assign_roles(:accountant, :marketer, create_new: false)
|
104
114
|
```
|
105
115
|
|
106
|
-
You can also explicitly create new roles simply by using:
|
107
|
-
|
108
|
-
```rb
|
109
|
-
Rabarber::Role.create(name: "manager")
|
110
|
-
```
|
111
|
-
The role names are unique.
|
112
|
-
|
113
116
|
**`#revoke_roles`**
|
114
117
|
|
115
118
|
To revoke roles, use:
|
@@ -143,14 +146,6 @@ If you need to list all the role names available in your application, use:
|
|
143
146
|
Rabarber::Role.names
|
144
147
|
```
|
145
148
|
|
146
|
-
`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
|
-
|
148
|
-
*Utilize the aforementioned methods to manipulate user roles. For example, create a UI for managing roles or assign roles during migration or runtime (e.g. when the user is created).*
|
149
|
-
|
150
|
-
*You are also encouraged to write your own authorization policies based on `#has_role?` method (e.g. to scope the data that the role can access).*
|
151
|
-
|
152
|
-
*Adapt the tools Rabarber provides to fit the requirements of your application.*
|
153
|
-
|
154
149
|
## Authorization Rules
|
155
150
|
|
156
151
|
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,9 +216,9 @@ class InvoicesController < ApplicationController
|
|
221
216
|
end
|
222
217
|
```
|
223
218
|
|
224
|
-
This allows everyone to access `OrdersController` and its children and `index` action in `InvoicesController`.
|
219
|
+
This allows everyone to access `OrdersController` and its children and `index` action in `InvoicesController`. This also extends to scenarios where there is no user present, i.e. when the method responsible for returning the currently authenticated user in your application returns `nil`.
|
225
220
|
|
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
|
221
|
+
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 access anything.
|
227
222
|
|
228
223
|
For more complex cases, Rabarber provides dynamic rules:
|
229
224
|
|
@@ -256,7 +251,7 @@ class InvoicesController < ApplicationController
|
|
256
251
|
end
|
257
252
|
end
|
258
253
|
```
|
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
|
254
|
+
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.
|
260
255
|
|
261
256
|
Rules defined in child classes don't override parent rules but rather add to them:
|
262
257
|
```rb
|
@@ -274,7 +269,16 @@ This means that `Crm::InvoicesController` is still accessible to `admin` but is
|
|
274
269
|
|
275
270
|
## View Helpers
|
276
271
|
|
277
|
-
Rabarber also provides a couple of helpers that can be used in views: `visible_to` and `hidden_from`.
|
272
|
+
Rabarber also provides a couple of helpers that can be used in views: `visible_to` and `hidden_from`. To use them, simply include `Rabarber::Helpers` in the desired helper (usually `ApplicationHelper`, but it can be any helper):
|
273
|
+
|
274
|
+
```rb
|
275
|
+
module ApplicationHelper
|
276
|
+
include Rabarber::Helpers
|
277
|
+
...
|
278
|
+
end
|
279
|
+
```
|
280
|
+
|
281
|
+
The usage is straightforward:
|
278
282
|
|
279
283
|
```erb
|
280
284
|
<%= visible_to(:admin, :manager) do %>
|
@@ -292,9 +296,17 @@ Rabarber also provides a couple of helpers that can be used in views: `visible_t
|
|
292
296
|
|
293
297
|
Encountered a bug or facing a problem?
|
294
298
|
|
295
|
-
- **Create an Issue**: If you've identified a problem
|
296
|
-
- **Contribute a Solution**: Found a fix for the issue
|
299
|
+
- **Create an Issue**: If you've identified a problem, please create an issue on the gem's GitHub repository. Be sure to provide detailed information about the problem, including the steps to reproduce it.
|
300
|
+
- **Contribute a Solution**: Found a fix for the issue? Feel free to create a pull request with your changes.
|
301
|
+
|
302
|
+
## Contributing
|
303
|
+
|
304
|
+
If you want to contribute, please read the [contributing guidelines](https://github.com/enjaku4/rabarber/blob/main/CONTRIBUTING.md).
|
297
305
|
|
298
306
|
## License
|
299
307
|
|
300
308
|
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
309
|
+
|
310
|
+
## Code of Conduct
|
311
|
+
|
312
|
+
Everyone interacting in the Rabarber project is expected to follow the [code of conduct](https://github.com/enjaku4/rabarber/blob/main/CODE_OF_CONDUCT.md).
|
@@ -9,9 +9,9 @@ class CreateRabarberRoles < ActiveRecord::Migration[<%= ActiveRecord::Migration.
|
|
9
9
|
|
10
10
|
create_table :rabarber_roles_roleables, id: false do |t|
|
11
11
|
t.belongs_to :role, null: false, index: true, foreign_key: { to_table: :rabarber_roles }
|
12
|
-
t.belongs_to :roleable, null: false, index: true, foreign_key: { to_table:
|
12
|
+
t.belongs_to :roleable, null: false, index: true, foreign_key: { to_table: <%= table_name.to_sym.inspect %> }
|
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
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Rabarber
|
4
|
+
module Cache
|
5
|
+
module_function
|
6
|
+
|
7
|
+
ALL_ROLES_KEY = "rabarber:roles"
|
8
|
+
|
9
|
+
def fetch(key, options, &block)
|
10
|
+
enabled? ? Rails.cache.fetch(key, options, &block) : yield
|
11
|
+
end
|
12
|
+
|
13
|
+
def delete(key)
|
14
|
+
Rails.cache.delete(key) if enabled?
|
15
|
+
end
|
16
|
+
|
17
|
+
def enabled?
|
18
|
+
Rabarber::Configuration.instance.cache_enabled
|
19
|
+
end
|
20
|
+
|
21
|
+
def key_for(record)
|
22
|
+
"rabarber:roles_#{record.public_send(record.class.primary_key)}"
|
23
|
+
end
|
24
|
+
|
25
|
+
def clear
|
26
|
+
Rails.cache.delete_matched(/^rabarber/)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -6,38 +6,91 @@ module Rabarber
|
|
6
6
|
class Configuration
|
7
7
|
include Singleton
|
8
8
|
|
9
|
-
attr_reader :current_user_method, :must_have_roles,
|
9
|
+
attr_reader :cache_enabled, :current_user_method, :must_have_roles,
|
10
|
+
:when_actions_missing, :when_roles_missing, :when_unauthorized
|
10
11
|
|
11
12
|
def initialize
|
12
|
-
@
|
13
|
-
@
|
14
|
-
@
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
controller.head(:unauthorized)
|
19
|
-
end
|
20
|
-
end
|
13
|
+
@cache_enabled = default_cache_enabled
|
14
|
+
@current_user_method = default_current_user_method
|
15
|
+
@must_have_roles = default_must_have_roles
|
16
|
+
@when_actions_missing = default_when_actions_missing
|
17
|
+
@when_roles_missing = default_when_roles_missing
|
18
|
+
@when_unauthorized = default_when_unauthorized
|
21
19
|
end
|
22
20
|
|
23
|
-
def
|
24
|
-
|
25
|
-
|
26
|
-
|
21
|
+
def cache_enabled=(value)
|
22
|
+
@cache_enabled = Rabarber::Input::Types::Booleans.new(
|
23
|
+
value, Rabarber::ConfigurationError, "Configuration 'cache_enabled' must be a Boolean"
|
24
|
+
).process
|
25
|
+
end
|
27
26
|
|
28
|
-
|
27
|
+
def current_user_method=(method_name)
|
28
|
+
@current_user_method = Rabarber::Input::Types::Symbols.new(
|
29
|
+
method_name, Rabarber::ConfigurationError, "Configuration 'current_user_method' must be a Symbol or a String"
|
30
|
+
).process
|
29
31
|
end
|
30
32
|
|
31
33
|
def must_have_roles=(value)
|
32
|
-
|
34
|
+
@must_have_roles = Rabarber::Input::Types::Booleans.new(
|
35
|
+
value, Rabarber::ConfigurationError, "Configuration 'must_have_roles' must be a Boolean"
|
36
|
+
).process
|
37
|
+
end
|
38
|
+
|
39
|
+
def when_actions_missing=(callable)
|
40
|
+
@when_actions_missing = Rabarber::Input::Types::Procs.new(
|
41
|
+
callable, Rabarber::ConfigurationError, "Configuration 'when_actions_missing' must be a Proc"
|
42
|
+
).process
|
43
|
+
end
|
33
44
|
|
34
|
-
|
45
|
+
def when_roles_missing=(callable)
|
46
|
+
@when_roles_missing = Rabarber::Input::Types::Procs.new(
|
47
|
+
callable, Rabarber::ConfigurationError, "Configuration 'when_roles_missing' must be a Proc"
|
48
|
+
).process
|
35
49
|
end
|
36
50
|
|
37
51
|
def when_unauthorized=(callable)
|
38
|
-
|
52
|
+
@when_unauthorized = Rabarber::Input::Types::Procs.new(
|
53
|
+
callable, Rabarber::ConfigurationError, "Configuration 'when_unauthorized' must be a Proc"
|
54
|
+
).process
|
55
|
+
end
|
39
56
|
|
40
|
-
|
57
|
+
private
|
58
|
+
|
59
|
+
def default_cache_enabled
|
60
|
+
true
|
61
|
+
end
|
62
|
+
|
63
|
+
def default_current_user_method
|
64
|
+
:current_user
|
65
|
+
end
|
66
|
+
|
67
|
+
def default_must_have_roles
|
68
|
+
false
|
69
|
+
end
|
70
|
+
|
71
|
+
def default_when_actions_missing
|
72
|
+
-> (missing_actions, context) {
|
73
|
+
raise Rabarber::Error, "Missing actions: #{missing_actions}, context: #{context[:controller]}"
|
74
|
+
}
|
75
|
+
end
|
76
|
+
|
77
|
+
def default_when_roles_missing
|
78
|
+
-> (missing_roles, context) {
|
79
|
+
delimiter = context[:action] ? "#" : ""
|
80
|
+
message = "Missing roles: #{missing_roles}, context: #{context[:controller]}#{delimiter}#{context[:action]}"
|
81
|
+
Rails.logger.tagged("Rabarber") { Rails.logger.warn message }
|
82
|
+
}
|
83
|
+
end
|
84
|
+
|
85
|
+
def default_when_unauthorized
|
86
|
+
-> (controller) do
|
87
|
+
Rails.logger.tagged("Rabarber") { Rails.logger.warn "Unauthorized attempt" }
|
88
|
+
if controller.request.format.html?
|
89
|
+
controller.redirect_back fallback_location: controller.main_app.root_path
|
90
|
+
else
|
91
|
+
controller.head(:unauthorized)
|
92
|
+
end
|
93
|
+
end
|
41
94
|
end
|
42
95
|
end
|
43
96
|
end
|
@@ -12,18 +12,30 @@ module Rabarber
|
|
12
12
|
def grant_access(action: nil, roles: nil, if: nil, unless: nil)
|
13
13
|
dynamic_rule, negated_dynamic_rule = binding.local_variable_get(:if), binding.local_variable_get(:unless)
|
14
14
|
|
15
|
-
Permissions.
|
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
|
+
)
|
16
22
|
end
|
17
23
|
end
|
18
24
|
|
19
25
|
private
|
20
26
|
|
21
27
|
def verify_access
|
22
|
-
|
23
|
-
|
24
|
-
)
|
28
|
+
Rabarber::Missing::Actions.new(self.class).handle
|
29
|
+
Rabarber::Missing::Roles.new(self.class).handle
|
25
30
|
|
26
|
-
|
31
|
+
return if Rabarber::Permissions.access_granted?(rabarber_roles, self.class, action_name.to_sym, self)
|
32
|
+
|
33
|
+
Rabarber::Configuration.instance.when_unauthorized.call(self)
|
34
|
+
end
|
35
|
+
|
36
|
+
def rabarber_roles
|
37
|
+
user = send(Rabarber::Configuration.instance.current_user_method)
|
38
|
+
user ? user.roles : []
|
27
39
|
end
|
28
40
|
end
|
29
41
|
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,61 @@
|
|
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
|
+
context = item.action ? { controller: item.controller, action: item.action } : { controller: item.controller }
|
20
|
+
Rabarber::Configuration.instance.public_send(configuration_name).call(item.missing, context)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
private
|
25
|
+
|
26
|
+
def check_controller_rules
|
27
|
+
raise NotImplementedError
|
28
|
+
end
|
29
|
+
|
30
|
+
def check_action_rules
|
31
|
+
raise NotImplementedError
|
32
|
+
end
|
33
|
+
|
34
|
+
def configuration_name
|
35
|
+
raise NotImplementedError
|
36
|
+
end
|
37
|
+
|
38
|
+
def missing_list
|
39
|
+
@missing_list ||= []
|
40
|
+
end
|
41
|
+
|
42
|
+
def controller_rules
|
43
|
+
if controller
|
44
|
+
{ controller => Rabarber::Permissions.controller_rules[controller] }
|
45
|
+
else
|
46
|
+
Rabarber::Permissions.controller_rules
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def action_rules
|
51
|
+
if controller
|
52
|
+
{ controller => Rabarber::Permissions.action_rules[controller] }
|
53
|
+
else
|
54
|
+
Rabarber::Permissions.action_rules
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
Item = Struct.new(:missing, :controller, :action)
|
60
|
+
end
|
61
|
+
end
|
@@ -0,0 +1,37 @@
|
|
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::Cache.fetch(
|
32
|
+
Rabarber::Cache::ALL_ROLES_KEY, expires_in: 1.day, race_condition_ttl: 10.seconds
|
33
|
+
) { Rabarber::Role.names }
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
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
|
|
@@ -15,30 +17,44 @@ module Rabarber
|
|
15
17
|
end
|
16
18
|
|
17
19
|
def roles
|
18
|
-
|
20
|
+
Rabarber::Cache.fetch(Rabarber::Cache.key_for(self), expires_in: 1.hour, race_condition_ttl: 5.seconds) do
|
21
|
+
rabarber_roles.names
|
22
|
+
end
|
19
23
|
end
|
20
24
|
|
21
25
|
def has_role?(*role_names)
|
22
|
-
(roles &
|
26
|
+
(roles & process_role_names(role_names)).any?
|
23
27
|
end
|
24
28
|
|
25
29
|
def assign_roles(*role_names, create_new: true)
|
26
|
-
roles_to_assign =
|
30
|
+
roles_to_assign = process_role_names(role_names)
|
27
31
|
|
28
32
|
create_new_roles(roles_to_assign) if create_new
|
29
33
|
|
30
|
-
rabarber_roles << Role.where(name: roles_to_assign) - rabarber_roles
|
34
|
+
rabarber_roles << Rabarber::Role.where(name: roles_to_assign) - rabarber_roles
|
35
|
+
|
36
|
+
delete_cache
|
31
37
|
end
|
32
38
|
|
33
39
|
def revoke_roles(*role_names)
|
34
|
-
self.rabarber_roles = rabarber_roles - Role.where(name:
|
40
|
+
self.rabarber_roles = rabarber_roles - Rabarber::Role.where(name: process_role_names(role_names))
|
41
|
+
|
42
|
+
delete_cache
|
35
43
|
end
|
36
44
|
|
37
45
|
private
|
38
46
|
|
39
47
|
def create_new_roles(role_names)
|
40
|
-
new_roles = role_names - Role.names
|
41
|
-
new_roles.each { |role_name| Role.create!(name: role_name) }
|
48
|
+
new_roles = role_names - Rabarber::Role.names
|
49
|
+
new_roles.each { |role_name| Rabarber::Role.create!(name: role_name) }
|
50
|
+
end
|
51
|
+
|
52
|
+
def process_role_names(role_names)
|
53
|
+
Rabarber::Input::Roles.new(role_names).process
|
54
|
+
end
|
55
|
+
|
56
|
+
def delete_cache
|
57
|
+
Rabarber::Cache.delete(Rabarber::Cache.key_for(self))
|
42
58
|
end
|
43
59
|
end
|
44
60
|
end
|
data/lib/rabarber/models/role.rb
CHANGED
@@ -4,10 +4,18 @@ 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
|
+
|
9
|
+
after_commit :delete_cache
|
8
10
|
|
9
11
|
def self.names
|
10
12
|
pluck(:name).map(&:to_sym)
|
11
13
|
end
|
14
|
+
|
15
|
+
private
|
16
|
+
|
17
|
+
def delete_cache
|
18
|
+
Rabarber::Cache.delete(Rabarber::Cache::ALL_ROLES_KEY)
|
19
|
+
end
|
12
20
|
end
|
13
21
|
end
|
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, dynamic_rule, negated_dynamic_rule)
|
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 if Rabarber::Role.table_exists?
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
data/lib/rabarber/rule.rb
CHANGED
@@ -5,10 +5,10 @@ module Rabarber
|
|
5
5
|
attr_reader :action, :roles, :dynamic_rule, :negated_dynamic_rule
|
6
6
|
|
7
7
|
def initialize(action, roles, dynamic_rule, negated_dynamic_rule)
|
8
|
-
@action =
|
9
|
-
@roles =
|
10
|
-
@dynamic_rule =
|
11
|
-
@negated_dynamic_rule =
|
8
|
+
@action = action
|
9
|
+
@roles = Array(roles)
|
10
|
+
@dynamic_rule = dynamic_rule
|
11
|
+
@negated_dynamic_rule = negated_dynamic_rule
|
12
12
|
end
|
13
13
|
|
14
14
|
def verify_access(user_roles, dynamic_rule_receiver, action_name = nil)
|
@@ -20,7 +20,7 @@ module Rabarber
|
|
20
20
|
end
|
21
21
|
|
22
22
|
def roles_permitted?(user_roles)
|
23
|
-
return false if
|
23
|
+
return false if Rabarber::Configuration.instance.must_have_roles && user_roles.empty?
|
24
24
|
|
25
25
|
roles.empty? || (roles & user_roles).any?
|
26
26
|
end
|
@@ -44,19 +44,5 @@ module Rabarber
|
|
44
44
|
|
45
45
|
is_negated ? !result : result
|
46
46
|
end
|
47
|
-
|
48
|
-
def pre_process_action(action)
|
49
|
-
return action.to_sym if (action.is_a?(String) || action.is_a?(Symbol)) && action.present?
|
50
|
-
return action if action.nil?
|
51
|
-
|
52
|
-
raise InvalidArgumentError, "Action name must be a Symbol or a String"
|
53
|
-
end
|
54
|
-
|
55
|
-
def pre_process_dynamic_rule(rule)
|
56
|
-
return rule.to_sym if (rule.is_a?(String) || rule.is_a?(Symbol)) && rule.present?
|
57
|
-
return rule if rule.nil? || rule.is_a?(Proc)
|
58
|
-
|
59
|
-
raise InvalidArgumentError, "Dynamic rule must be a Symbol, a String, or a Proc"
|
60
|
-
end
|
61
47
|
end
|
62
48
|
end
|
data/lib/rabarber/version.rb
CHANGED
data/lib/rabarber.rb
CHANGED
@@ -6,7 +6,19 @@ 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"
|
20
|
+
|
21
|
+
require_relative "rabarber/cache"
|
10
22
|
|
11
23
|
require_relative "rabarber/controllers/concerns/authorization"
|
12
24
|
require_relative "rabarber/helpers/helpers"
|
@@ -14,14 +26,16 @@ require_relative "rabarber/models/concerns/has_roles"
|
|
14
26
|
require_relative "rabarber/models/role"
|
15
27
|
require_relative "rabarber/permissions"
|
16
28
|
|
29
|
+
require_relative "rabarber/railtie"
|
30
|
+
|
17
31
|
module Rabarber
|
18
32
|
module_function
|
19
33
|
|
34
|
+
class Error < StandardError; end
|
35
|
+
class ConfigurationError < Rabarber::Error; end
|
36
|
+
class InvalidArgumentError < Rabarber::Error; end
|
37
|
+
|
20
38
|
def configure
|
21
|
-
yield(Configuration.instance)
|
39
|
+
yield(Rabarber::Configuration.instance)
|
22
40
|
end
|
23
|
-
|
24
|
-
class Error < StandardError; end
|
25
|
-
class ConfigurationError < Error; end
|
26
|
-
class InvalidArgumentError < Error; end
|
27
41
|
end
|
data/rabarber.gemspec
CHANGED
@@ -6,7 +6,7 @@ Gem::Specification.new do |spec|
|
|
6
6
|
spec.name = "rabarber"
|
7
7
|
spec.version = Rabarber::VERSION
|
8
8
|
spec.authors = ["enjaku4", "trafium"]
|
9
|
-
spec.email = ["
|
9
|
+
spec.email = ["rabarber_gem@icloud.com"]
|
10
10
|
|
11
11
|
spec.summary = "Simple authorization library for Ruby on Rails."
|
12
12
|
spec.homepage = "https://github.com/enjaku4/rabarber"
|
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.1
|
4
|
+
version: 1.2.1
|
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-
|
12
|
+
date: 2024-02-07 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: rails
|
@@ -27,7 +27,7 @@ dependencies:
|
|
27
27
|
version: '6.1'
|
28
28
|
description:
|
29
29
|
email:
|
30
|
-
-
|
30
|
+
- rabarber_gem@icloud.com
|
31
31
|
executables: []
|
32
32
|
extensions: []
|
33
33
|
extra_rdoc_files: []
|
@@ -39,13 +39,24 @@ files:
|
|
39
39
|
- lib/generators/rabarber/templates/create_rabarber_roles.rb.erb
|
40
40
|
- lib/rabarber.rb
|
41
41
|
- lib/rabarber/access.rb
|
42
|
+
- lib/rabarber/cache.rb
|
42
43
|
- lib/rabarber/configuration.rb
|
43
44
|
- lib/rabarber/controllers/concerns/authorization.rb
|
44
45
|
- lib/rabarber/helpers/helpers.rb
|
46
|
+
- lib/rabarber/input/actions.rb
|
47
|
+
- lib/rabarber/input/base.rb
|
48
|
+
- lib/rabarber/input/dynamic_rules.rb
|
49
|
+
- lib/rabarber/input/roles.rb
|
50
|
+
- lib/rabarber/input/types/booleans.rb
|
51
|
+
- lib/rabarber/input/types/procs.rb
|
52
|
+
- lib/rabarber/input/types/symbols.rb
|
53
|
+
- lib/rabarber/missing/actions.rb
|
54
|
+
- lib/rabarber/missing/base.rb
|
55
|
+
- lib/rabarber/missing/roles.rb
|
45
56
|
- lib/rabarber/models/concerns/has_roles.rb
|
46
57
|
- lib/rabarber/models/role.rb
|
47
58
|
- lib/rabarber/permissions.rb
|
48
|
-
- lib/rabarber/
|
59
|
+
- lib/rabarber/railtie.rb
|
49
60
|
- lib/rabarber/rule.rb
|
50
61
|
- lib/rabarber/version.rb
|
51
62
|
- 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
|