rabarber 1.2.2 → 1.3.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: '084b409c4886e7622ee37f20b8f00c0d6e083546394bf3617bdd5277548a2764'
4
- data.tar.gz: 62c3f5fb0496fc5a0cf95f04f0f04d26ba4f847bdf1e838c248361dca72744ee
3
+ metadata.gz: fc0447acdc988dc558859b695d24d227697258a92bb1f444944fb76bfbace526
4
+ data.tar.gz: 975f4377d5cc4fb2f28c42060012b67b3a85fea589aadb5106c33ecb943cde4a
5
5
  SHA512:
6
- metadata.gz: 0a1b141efb53f2b863f0dbbd1fe4c03ce199b7278bc0c51f8868866b918ea3161b8aebd9920872b43d9aea8cf24c5161b0f87d6d535a7c76295aa7890ee4b337
7
- data.tar.gz: b740291aba6e4d1c529453e990c30e009b5215ccbdc99a5eac6c1b8c21d354c813e5bdd3f700c37e9aa22d528b025814ce69629aedb95449108a704e233c53d2
6
+ metadata.gz: cbfed2814ae750ec0b2870126818cb67c6b55c28a7529d8ab2db3364adc463768853b41ac89ba4653d014aa1024622ae261f5b2c8afcc10b99194fd916a40266
7
+ data.tar.gz: 7b77adeb8cc95acc4b104e6c39c6366b466262a8b0df0584d9e7d99d6369d96d01779a5962c7963696bccf7a941578a82774f2af23ac76d4a515bf37f7bed2ad
data/CHANGELOG.md CHANGED
@@ -1,3 +1,15 @@
1
+ ## 1.3.1
2
+
3
+ - Add `Rabarber::Role.assignees_for` method
4
+ - Fix inconsistent behavior where passing `nil` as a role name to role management methods would raise an `ActiveRecord` error instead of `Rabarber` error
5
+ - Various minor code improvements
6
+
7
+ ## 1.3.0
8
+
9
+ - Add methods to directly add, rename, and remove roles
10
+ - Modify `Rabarber::HasRoles#assign_roles` and `Rabarber::HasRoles#revoke_roles` methods to return the list of roles assigned to the user
11
+ - Minor performance improvements
12
+
1
13
  ## 1.2.2
2
14
 
3
15
  - Refactor to improve readability and maintainability
@@ -8,6 +20,7 @@
8
20
  - Cache roles to avoid unnecessary database queries
9
21
  - Introduce `cache_enabled` configuration option allowing to enable or disable role caching
10
22
  - Enhance the migration generator so that it can receive the table name of the model representing users in the application as an argument
23
+ - Fix an issue where an error would be raised if the user is not authenticated
11
24
  - Various minor improvements
12
25
 
13
26
  ## 1.2.0
@@ -36,7 +49,7 @@
36
49
  ## 1.0.2
37
50
 
38
51
  - Various enhancements for gem development and release
39
- - Modify `HasRoles#roles` method to return an array of role names instead of `Rabarber::Role` objects
52
+ - Modify `Rabarber::HasRoles#roles` method to return an array of role names instead of `Rabarber::Role` objects
40
53
 
41
54
  ## 1.0.1
42
55
 
@@ -55,7 +68,7 @@
55
68
 
56
69
  ## 0.1.4
57
70
 
58
- - Remove `HasRoles#role?` method as unnecessary
71
+ - Remove `Rabarber::HasRoles#role?` method as unnecessary
59
72
 
60
73
  ## 0.1.3
61
74
 
data/README.md CHANGED
@@ -3,9 +3,7 @@
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 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.
6
+ Rabarber is a role-based authorization library for Ruby on Rails, designed primarily for use in the application's web layer (specifically controllers and views) but not limited to that. It provides tools for managing user roles and defining authorization rules and mainly focuses on answering the question: 'Who has access to which endpoints?'.
9
7
 
10
8
  ---
11
9
 
@@ -53,8 +51,6 @@ Next, generate a migration to create tables for storing roles in the database. M
53
51
  rails g rabarber:roles users
54
52
  ```
55
53
 
56
- This will create a migration file in `db/migrate` directory.
57
-
58
54
  Finally, run the migration to apply the changes to the database:
59
55
 
60
56
  ```
@@ -63,30 +59,30 @@ rails db:migrate
63
59
 
64
60
  ## Configuration
65
61
 
66
- Rabarber can be configured by using `.configure` method in an initializer:
62
+ If specific customization is required, Rabarber can be configured by using `.configure` method in an initializer:
67
63
 
68
64
  ```rb
69
65
  Rabarber.configure do |config|
70
- config.cache_enabled = false
71
- config.current_user_method = :authenticated_user
72
- config.must_have_roles = true
66
+ config.cache_enabled = true
67
+ config.current_user_method = :current_user
68
+ config.must_have_roles = false
73
69
  config.when_actions_missing = -> (missing_actions, context) { ... }
74
70
  config.when_roles_missing = -> (missing_roles, context) { ... }
75
71
  config.when_unauthorized = -> (controller) { ... }
76
72
  end
77
73
  ```
78
74
 
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.
75
+ - `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 `Rabarber::Cache.clear` method.
80
76
 
81
- - `current_user_method` must be a symbol representing the method that returns the currently authenticated user. The default value is `:current_user`.
77
+ - `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
+ - `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)._
84
80
 
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.
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 symbols e.g., `[:index]`, `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
82
 
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.
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 symbols e.g., `[:admin]`, `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
84
 
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.
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._
90
86
 
91
87
  ## Roles
92
88
 
@@ -103,7 +99,7 @@ This adds the following methods:
103
99
 
104
100
  **`#assign_roles`**
105
101
 
106
- To assign roles to the user, use:
102
+ To assign roles, use:
107
103
 
108
104
  ```rb
109
105
  user.assign_roles(:accountant, :marketer)
@@ -112,6 +108,7 @@ By default, `#assign_roles` method will automatically create any roles that don'
112
108
  ```rb
113
109
  user.assign_roles(:accountant, :marketer, create_new: false)
114
110
  ```
111
+ The method returns an array of roles assigned to the user.
115
112
 
116
113
  **`#revoke_roles`**
117
114
 
@@ -122,6 +119,8 @@ user.revoke_roles(:accountant, :marketer)
122
119
  ```
123
120
  If any of the specified roles doesn't exist or the user doesn't have the role you want to revoke, it will be ignored.
124
121
 
122
+ The method returns an array of roles assigned to the user.
123
+
125
124
  **`#has_role?`**
126
125
 
127
126
  To check whether the user has a role, use:
@@ -140,15 +139,68 @@ To view all the roles assigned to the user, use:
140
139
  user.roles
141
140
  ```
142
141
 
142
+ ---
143
+
144
+ To manipulate roles directly, you can use `Rabarber::Role` methods:
145
+
146
+ **`.add`**
147
+
148
+ To add a new role, use:
149
+
150
+ ```rb
151
+ Rabarber::Role.add(:admin)
152
+ ```
153
+
154
+ This will create a new role with the specified name and return `true`. If the role already exists, it will return `false`.
155
+
156
+ **`.rename`**
157
+
158
+ To rename a role, use:
159
+
160
+ ```rb
161
+ Rabarber::Role.rename(:admin, :administrator)
162
+ ```
163
+ The first argument is the old name, and the second argument is the new name. This will rename the role and return `true`. If a role with the new name already exists, it will return `false`.
164
+
165
+ The method won't rename the role and will return `false` if it is assigned to any user. To force the rename, use the method with `force: true` argument:
166
+ ```rb
167
+ Rabarber::Role.rename(:admin, :administrator, force: true)
168
+ ```
169
+
170
+ **`.remove`**
171
+
172
+ To remove a role, use:
173
+
174
+ ```rb
175
+ Rabarber::Role.remove(:admin)
176
+ ```
177
+
178
+ This will remove the role and return `true`. If the role doesn't exist, it will return `false`.
179
+
180
+ The method won't remove the role and will return `false` if it is assigned to any user. To force the removal, use the method with `force: true` argument:
181
+ ```rb
182
+ Rabarber::Role.remove(:admin, force: true)
183
+ ```
184
+
185
+ **`.names`**
186
+
143
187
  If you need to list all the role names available in your application, use:
144
188
 
145
189
  ```rb
146
190
  Rabarber::Role.names
147
191
  ```
148
192
 
193
+ **`.assignees_for`**
194
+
195
+ To get all the users to whom the role is assigned, use:
196
+
197
+ ```rb
198
+ Rabarber::Role.assignees_for(:admin)
199
+ ```
200
+
149
201
  ## Authorization Rules
150
202
 
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.
203
+ Include `Rabarber::Authorization` module into the controller that needs authorization rules to be applied. Typically, it is `ApplicationController`, but it can be any controller of your choice.
152
204
 
153
205
  ```rb
154
206
  class ApplicationController < ActionController::Base
@@ -216,9 +268,9 @@ class InvoicesController < ApplicationController
216
268
  end
217
269
  ```
218
270
 
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`.
271
+ This allows everyone to access `OrdersController` and its children and also `index` action in `InvoicesController`. This 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`.
220
272
 
221
- Be aware that if the user is not authenticated (the method responsible for returning the currently authenticated user in your application returns `nil`), Rabarber will treat this situation as if the user with no roles assigned was authenticated.
273
+ _Be aware that if the user is not authenticated (the method responsible for returning the currently authenticated user in your application returns `nil`), Rabarber will treat this situation as if the user with no roles assigned was authenticated._
222
274
 
223
275
  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.
224
276
 
@@ -253,7 +305,7 @@ class InvoicesController < ApplicationController
253
305
  end
254
306
  end
255
307
  ```
256
- 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.
308
+ You can pass a dynamic rule as `if` or `unless` argument. It can be a symbol, in which case the method with the same name will be called. Alternatively, it can be a proc, which will be executed within the context of the controller's instance.
257
309
 
258
310
  Rules defined in child classes don't override parent rules but rather add to them:
259
311
  ```rb
@@ -271,7 +323,7 @@ This means that `Crm::InvoicesController` is still accessible to `admin` but is
271
323
 
272
324
  ## View Helpers
273
325
 
274
- 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):
326
+ 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 it is `ApplicationHelper`, but it can be any helper of your choice.
275
327
 
276
328
  ```rb
277
329
  module ApplicationHelper
@@ -296,14 +348,18 @@ The usage is straightforward:
296
348
 
297
349
  ## Problems?
298
350
 
299
- Encountered a bug or facing a problem?
351
+ Facing a problem or want to suggest an enhancement?
352
+
353
+ - **Open a Discussion**: If you have a question, experience difficulties using the gem, or have an improvement suggestion, feel free to use the Discussions section.
354
+
355
+ Encountered a bug?
300
356
 
301
- - **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.
357
+ - **Create an Issue**: If you've identified a bug, please create an issue. Be sure to provide detailed information about the problem, including the steps to reproduce it.
302
358
  - **Contribute a Solution**: Found a fix for the issue? Feel free to create a pull request with your changes.
303
359
 
304
360
  ## Contributing
305
361
 
306
- If you want to contribute, please read the [contributing guidelines](https://github.com/enjaku4/rabarber/blob/main/CONTRIBUTING.md).
362
+ Before opening an issue or creating a pull request, please read the [contributing guidelines](https://github.com/enjaku4/rabarber/blob/main/CONTRIBUTING.md).
307
363
 
308
364
  ## License
309
365
 
@@ -10,16 +10,16 @@ module Rabarber
10
10
  enabled? ? Rails.cache.fetch(key, options, &block) : yield
11
11
  end
12
12
 
13
- def delete(key)
14
- Rails.cache.delete(key) if enabled?
13
+ def delete(*keys)
14
+ Rails.cache.delete_multi(keys) if enabled?
15
15
  end
16
16
 
17
17
  def enabled?
18
18
  Rabarber::Configuration.instance.cache_enabled
19
19
  end
20
20
 
21
- def key_for(record)
22
- "rabarber:roles_#{record.public_send(record.class.primary_key)}"
21
+ def key_for(id)
22
+ "rabarber:roles_#{id}"
23
23
  end
24
24
 
25
25
  def clear
@@ -19,37 +19,37 @@ module Rabarber
19
19
  end
20
20
 
21
21
  def cache_enabled=(value)
22
- @cache_enabled = Rabarber::Input::Types::Booleans.new(
22
+ @cache_enabled = Rabarber::Input::Types::Boolean.new(
23
23
  value, Rabarber::ConfigurationError, "Configuration 'cache_enabled' must be a Boolean"
24
24
  ).process
25
25
  end
26
26
 
27
27
  def current_user_method=(method_name)
28
- @current_user_method = Rabarber::Input::Types::Symbols.new(
28
+ @current_user_method = Rabarber::Input::Types::Symbol.new(
29
29
  method_name, Rabarber::ConfigurationError, "Configuration 'current_user_method' must be a Symbol or a String"
30
30
  ).process
31
31
  end
32
32
 
33
33
  def must_have_roles=(value)
34
- @must_have_roles = Rabarber::Input::Types::Booleans.new(
34
+ @must_have_roles = Rabarber::Input::Types::Boolean.new(
35
35
  value, Rabarber::ConfigurationError, "Configuration 'must_have_roles' must be a Boolean"
36
36
  ).process
37
37
  end
38
38
 
39
39
  def when_actions_missing=(callable)
40
- @when_actions_missing = Rabarber::Input::Types::Procs.new(
40
+ @when_actions_missing = Rabarber::Input::Types::Proc.new(
41
41
  callable, Rabarber::ConfigurationError, "Configuration 'when_actions_missing' must be a Proc"
42
42
  ).process
43
43
  end
44
44
 
45
45
  def when_roles_missing=(callable)
46
- @when_roles_missing = Rabarber::Input::Types::Procs.new(
46
+ @when_roles_missing = Rabarber::Input::Types::Proc.new(
47
47
  callable, Rabarber::ConfigurationError, "Configuration 'when_roles_missing' must be a Proc"
48
48
  ).process
49
49
  end
50
50
 
51
51
  def when_unauthorized=(callable)
52
- @when_unauthorized = Rabarber::Input::Types::Procs.new(
52
+ @when_unauthorized = Rabarber::Input::Types::Proc.new(
53
53
  callable, Rabarber::ConfigurationError, "Configuration 'when_unauthorized' must be a Proc"
54
54
  ).process
55
55
  end
@@ -12,12 +12,12 @@ 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
- Rabarber::Permissions.add(
15
+ Rabarber::Core::Permissions.add(
16
16
  self,
17
- Rabarber::Input::Actions.new(action).process,
17
+ Rabarber::Input::Action.new(action).process,
18
18
  Rabarber::Input::Roles.new(roles).process,
19
- Rabarber::Input::DynamicRules.new(dynamic_rule).process,
20
- Rabarber::Input::DynamicRules.new(negated_dynamic_rule).process
19
+ Rabarber::Input::DynamicRule.new(dynamic_rule).process,
20
+ Rabarber::Input::DynamicRule.new(negated_dynamic_rule).process
21
21
  )
22
22
  end
23
23
  end
@@ -28,7 +28,7 @@ module Rabarber
28
28
  Rabarber::Missing::Actions.new(self.class).handle
29
29
  Rabarber::Missing::Roles.new(self.class).handle
30
30
 
31
- return if Rabarber::Permissions.access_granted?(rabarber_roles, self.class, action_name.to_sym, self)
31
+ return if Rabarber::Core::Permissions.access_granted?(rabarber_roles, self.class, action_name.to_sym, self)
32
32
 
33
33
  Rabarber::Configuration.instance.when_unauthorized.call(self)
34
34
  end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rabarber
4
+ module Core
5
+ module Access
6
+ def access_granted?(roles, controller, action, dynamic_rule_receiver)
7
+ controller_accessible?(roles, controller, dynamic_rule_receiver) ||
8
+ action_accessible?(roles, controller, action, dynamic_rule_receiver)
9
+ end
10
+
11
+ def controller_accessible?(roles, controller, dynamic_rule_receiver)
12
+ accessible_controllers(roles, dynamic_rule_receiver).any? do |accessible_controller|
13
+ controller <= accessible_controller
14
+ end
15
+ end
16
+
17
+ def action_accessible?(roles, controller, action, dynamic_rule_receiver)
18
+ action_rules[controller].any? { |rule| rule.verify_access(roles, dynamic_rule_receiver, action) }
19
+ end
20
+
21
+ private
22
+
23
+ def accessible_controllers(roles, dynamic_rule_receiver)
24
+ controller_rules.select { |_, rule| rule.verify_access(roles, dynamic_rule_receiver) }.keys
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "access"
4
+ require_relative "rule"
5
+
6
+ module Rabarber
7
+ module Core
8
+ class Permissions
9
+ include Singleton
10
+
11
+ extend Access
12
+
13
+ attr_reader :storage
14
+
15
+ def initialize
16
+ @storage = { controller_rules: Hash.new({}), action_rules: Hash.new([]) }
17
+ end
18
+
19
+ class << self
20
+ def add(controller, action, roles, dynamic_rule, negated_dynamic_rule)
21
+ rule = Rabarber::Core::Rule.new(action, roles, dynamic_rule, negated_dynamic_rule)
22
+
23
+ if action
24
+ instance.storage[:action_rules][controller] += [rule]
25
+ else
26
+ instance.storage[:controller_rules][controller] = rule
27
+ end
28
+ end
29
+
30
+ def controller_rules
31
+ instance.storage[:controller_rules]
32
+ end
33
+
34
+ def action_rules
35
+ instance.storage[:action_rules]
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rabarber
4
+ module Core
5
+ class Rule
6
+ attr_reader :action, :roles, :dynamic_rule, :negated_dynamic_rule
7
+
8
+ def initialize(action, roles, dynamic_rule, negated_dynamic_rule)
9
+ @action = action
10
+ @roles = Array(roles)
11
+ @dynamic_rule = dynamic_rule
12
+ @negated_dynamic_rule = negated_dynamic_rule
13
+ end
14
+
15
+ def verify_access(user_roles, dynamic_rule_receiver, action_name = nil)
16
+ action_accessible?(action_name) && roles_permitted?(user_roles) && dynamic_rule_followed?(dynamic_rule_receiver)
17
+ end
18
+
19
+ def action_accessible?(action_name)
20
+ action_name.nil? || action_name == action
21
+ end
22
+
23
+ def roles_permitted?(user_roles)
24
+ return false if Rabarber::Configuration.instance.must_have_roles && user_roles.empty?
25
+
26
+ roles.empty? || (roles & user_roles).any?
27
+ end
28
+
29
+ def dynamic_rule_followed?(dynamic_rule_receiver)
30
+ !!(execute_dynamic_rule(dynamic_rule_receiver, false) && execute_dynamic_rule(dynamic_rule_receiver, true))
31
+ end
32
+
33
+ private
34
+
35
+ def execute_dynamic_rule(dynamic_rule_receiver, is_negated)
36
+ rule = is_negated ? negated_dynamic_rule : dynamic_rule
37
+
38
+ return true if rule.nil?
39
+
40
+ result = if rule.is_a?(Proc)
41
+ dynamic_rule_receiver.instance_exec(&rule)
42
+ else
43
+ dynamic_rule_receiver.send(rule)
44
+ end
45
+
46
+ is_negated ? !result : result
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rabarber
4
+ module Input
5
+ class Action < Rabarber::Input::Base
6
+ def valid?
7
+ Rabarber::Input::Types::Symbol.new(value).valid? || value.nil?
8
+ end
9
+
10
+ private
11
+
12
+ def processed_value
13
+ case value
14
+ when String, Symbol
15
+ value.to_sym
16
+ when nil
17
+ value
18
+ end
19
+ end
20
+
21
+ def default_error_message
22
+ "Action name must be a Symbol or a String"
23
+ end
24
+ end
25
+ end
26
+ end
@@ -5,7 +5,7 @@ module Rabarber
5
5
  class Base
6
6
  attr_reader :value, :error_type, :error_message
7
7
 
8
- def initialize(value, error_type, error_message)
8
+ def initialize(value, error_type = Rabarber::InvalidArgumentError, error_message = default_error_message)
9
9
  @value = value
10
10
  @error_type = error_type
11
11
  @error_message = error_message
@@ -15,16 +15,20 @@ module Rabarber
15
15
  valid? ? processed_value : raise_error
16
16
  end
17
17
 
18
- private
19
-
20
18
  def valid?
21
19
  raise NotImplementedError
22
20
  end
23
21
 
22
+ private
23
+
24
24
  def processed_value
25
25
  raise NotImplementedError
26
26
  end
27
27
 
28
+ def default_error_message
29
+ raise NotImplementedError
30
+ end
31
+
28
32
  def raise_error
29
33
  raise error_type, error_message
30
34
  end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rabarber
4
+ module Input
5
+ class DynamicRule < Rabarber::Input::Base
6
+ def valid?
7
+ Rabarber::Input::Types::Symbol.new(value).valid? || Rabarber::Input::Types::Proc.new(value).valid? || value.nil?
8
+ end
9
+
10
+ private
11
+
12
+ def processed_value
13
+ case value
14
+ when String, Symbol
15
+ value.to_sym
16
+ when Proc, nil
17
+ value
18
+ end
19
+ end
20
+
21
+ def default_error_message
22
+ "Dynamic rule must be a Symbol, a String, or a Proc"
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rabarber
4
+ module Input
5
+ class Role < Rabarber::Input::Base
6
+ REGEX = /\A[a-z0-9_]+\z/
7
+
8
+ def valid?
9
+ Rabarber::Input::Types::Symbol.new(value).valid? && value.to_s.match?(REGEX)
10
+ end
11
+
12
+ private
13
+
14
+ def processed_value
15
+ value.to_sym
16
+ end
17
+
18
+ def default_error_message
19
+ "Role name must be a Symbol or a String and may only contain lowercase letters, numbers and underscores"
20
+ end
21
+ end
22
+ end
23
+ end
@@ -3,30 +3,23 @@
3
3
  module Rabarber
4
4
  module Input
5
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
6
  def value
18
7
  Array(super)
19
8
  end
20
9
 
21
- private
22
-
23
10
  def valid?
24
- value.all? { |role_name| (role_name.is_a?(Symbol) || role_name.is_a?(String)) && role_name.to_s.match?(REGEX) }
11
+ value.all? { |role_name| Rabarber::Input::Role.new(role_name).valid? }
25
12
  end
26
13
 
14
+ private
15
+
27
16
  def processed_value
28
17
  value.map(&:to_sym)
29
18
  end
19
+
20
+ def default_error_message
21
+ "Role names must be Symbols or Strings and may only contain lowercase letters, numbers and underscores"
22
+ end
30
23
  end
31
24
  end
32
25
  end
@@ -3,16 +3,20 @@
3
3
  module Rabarber
4
4
  module Input
5
5
  module Types
6
- class Booleans < Rabarber::Input::Base
7
- private
8
-
6
+ class Boolean < Rabarber::Input::Base
9
7
  def valid?
10
8
  [true, false].include?(value)
11
9
  end
12
10
 
11
+ private
12
+
13
13
  def processed_value
14
14
  value
15
15
  end
16
+
17
+ def default_error_message
18
+ "Value must be a Boolean"
19
+ end
16
20
  end
17
21
  end
18
22
  end
@@ -3,16 +3,20 @@
3
3
  module Rabarber
4
4
  module Input
5
5
  module Types
6
- class Procs < Rabarber::Input::Base
7
- private
8
-
6
+ class Proc < Rabarber::Input::Base
9
7
  def valid?
10
- value.is_a?(Proc)
8
+ value.is_a?(::Proc)
11
9
  end
12
10
 
11
+ private
12
+
13
13
  def processed_value
14
14
  value
15
15
  end
16
+
17
+ def default_error_message
18
+ "Value must be a Proc"
19
+ end
16
20
  end
17
21
  end
18
22
  end
@@ -3,16 +3,20 @@
3
3
  module Rabarber
4
4
  module Input
5
5
  module Types
6
- class Symbols < Rabarber::Input::Base
7
- private
8
-
6
+ class Symbol < Rabarber::Input::Base
9
7
  def valid?
10
- (value.is_a?(Symbol) || value.is_a?(String)) && value.present?
8
+ (value.is_a?(::Symbol) || value.is_a?(String)) && value.present?
11
9
  end
12
10
 
11
+ private
12
+
13
13
  def processed_value
14
14
  value.to_sym
15
15
  end
16
+
17
+ def default_error_message
18
+ "Value must be a Symbol or a String"
19
+ end
16
20
  end
17
21
  end
18
22
  end
@@ -41,17 +41,17 @@ module Rabarber
41
41
 
42
42
  def controller_rules
43
43
  if controller
44
- Rabarber::Permissions.controller_rules.slice(controller)
44
+ Rabarber::Core::Permissions.controller_rules.slice(controller)
45
45
  else
46
- Rabarber::Permissions.controller_rules
46
+ Rabarber::Core::Permissions.controller_rules
47
47
  end
48
48
  end
49
49
 
50
50
  def action_rules
51
51
  if controller
52
- Rabarber::Permissions.action_rules.slice(controller)
52
+ Rabarber::Core::Permissions.action_rules.slice(controller)
53
53
  else
54
- Rabarber::Permissions.action_rules
54
+ Rabarber::Core::Permissions.action_rules
55
55
  end
56
56
  end
57
57
  end
@@ -17,7 +17,7 @@ module Rabarber
17
17
  end
18
18
 
19
19
  def roles
20
- Rabarber::Cache.fetch(Rabarber::Cache.key_for(self), expires_in: 1.hour, race_condition_ttl: 5.seconds) do
20
+ Rabarber::Cache.fetch(Rabarber::Cache.key_for(roleable_id), expires_in: 1.hour, race_condition_ttl: 5.seconds) do
21
21
  rabarber_roles.names
22
22
  end
23
23
  end
@@ -31,17 +31,32 @@ module Rabarber
31
31
 
32
32
  create_new_roles(roles_to_assign) if create_new
33
33
 
34
- rabarber_roles << Rabarber::Role.where(name: roles_to_assign) - rabarber_roles
34
+ new_roles = Rabarber::Role.where(name: roles_to_assign) - rabarber_roles
35
35
 
36
- delete_cache
36
+ if new_roles.any?
37
+ delete_roleable_cache
38
+ rabarber_roles << new_roles
39
+ end
40
+
41
+ roles
37
42
  end
38
43
 
39
44
  def revoke_roles(*role_names)
40
- self.rabarber_roles = rabarber_roles - Rabarber::Role.where(name: process_role_names(role_names))
45
+ new_roles = rabarber_roles - Rabarber::Role.where(name: process_role_names(role_names))
46
+
47
+ if rabarber_roles != new_roles
48
+ delete_roleable_cache
49
+ self.rabarber_roles = new_roles
50
+ end
41
51
 
42
- delete_cache
52
+ roles
43
53
  end
44
54
 
55
+ def roleable_class
56
+ @@included.constantize
57
+ end
58
+ module_function :roleable_class
59
+
45
60
  private
46
61
 
47
62
  def create_new_roles(role_names)
@@ -53,8 +68,12 @@ module Rabarber
53
68
  Rabarber::Input::Roles.new(role_names).process
54
69
  end
55
70
 
56
- def delete_cache
57
- Rabarber::Cache.delete(Rabarber::Cache.key_for(self))
71
+ def delete_roleable_cache
72
+ Rabarber::Cache.delete(Rabarber::Cache.key_for(roleable_id))
73
+ end
74
+
75
+ def roleable_id
76
+ public_send(self.class.primary_key)
58
77
  end
59
78
  end
60
79
  end
@@ -4,18 +4,74 @@ 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: Rabarber::Input::Roles::REGEX }
7
+ validates :name, presence: true, uniqueness: true, format: { with: Rabarber::Input::Role::REGEX }, strict: true
8
8
 
9
- after_commit :delete_cache
9
+ has_and_belongs_to_many :roleables, join_table: "rabarber_roles_roleables"
10
10
 
11
- def self.names
12
- pluck(:name).map(&:to_sym)
13
- end
11
+ class << self
12
+ def names
13
+ pluck(:name).map(&:to_sym)
14
+ end
15
+
16
+ def add(name)
17
+ name = process_role_name(name)
18
+
19
+ return false if exists?(name: name)
20
+
21
+ delete_roles_cache
22
+
23
+ !!create!(name: name)
24
+ end
25
+
26
+ def rename(old_name, new_name, force: false)
27
+ role = find_by(name: process_role_name(old_name))
28
+ name = process_role_name(new_name)
29
+
30
+ return false if !role || exists?(name: name) || assigned_to_roleables(role).any? && !force
31
+
32
+ delete_roles_cache
33
+ delete_roleables_cache(role)
34
+
35
+ role.update!(name: name)
36
+ end
37
+
38
+ def remove(name, force: false)
39
+ role = find_by(name: process_role_name(name))
40
+
41
+ return false if !role || assigned_to_roleables(role).any? && !force
42
+
43
+ delete_roles_cache
44
+ delete_roleables_cache(role)
45
+
46
+ !!role.destroy!
47
+ end
48
+
49
+ def assignees_for(name)
50
+ Rabarber::HasRoles.roleable_class.joins(:rabarber_roles).where(
51
+ rabarber_roles: { name: Rabarber::Input::Role.new(name).process }
52
+ )
53
+ end
54
+
55
+ private
56
+
57
+ def delete_roles_cache
58
+ Rabarber::Cache.delete(Rabarber::Cache::ALL_ROLES_KEY)
59
+ end
60
+
61
+ def delete_roleables_cache(role)
62
+ keys = assigned_to_roleables(role).map { |roleable_id| Rabarber::Cache.key_for(roleable_id) }
63
+ Rabarber::Cache.delete(*keys) if keys.any?
64
+ end
14
65
 
15
- private
66
+ def assigned_to_roleables(role)
67
+ ActiveRecord::Base.connection.select_values(
68
+ "SELECT roleable_id FROM rabarber_roles_roleables WHERE role_id = #{role.id}"
69
+ )
70
+ end
16
71
 
17
- def delete_cache
18
- Rabarber::Cache.delete(Rabarber::Cache::ALL_ROLES_KEY)
72
+ def process_role_name(name)
73
+ Rabarber::Input::Role.new(name).process
74
+ end
19
75
  end
20
76
  end
21
77
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Rabarber
4
- VERSION = "1.2.2"
4
+ VERSION = "1.3.1"
5
5
  end
data/lib/rabarber.rb CHANGED
@@ -8,12 +8,13 @@ require "active_record"
8
8
  require "active_support"
9
9
 
10
10
  require_relative "rabarber/input/base"
11
- require_relative "rabarber/input/actions"
12
- require_relative "rabarber/input/dynamic_rules"
11
+ require_relative "rabarber/input/action"
12
+ require_relative "rabarber/input/dynamic_rule"
13
+ require_relative "rabarber/input/role"
13
14
  require_relative "rabarber/input/roles"
14
- require_relative "rabarber/input/types/booleans"
15
- require_relative "rabarber/input/types/procs"
16
- require_relative "rabarber/input/types/symbols"
15
+ require_relative "rabarber/input/types/boolean"
16
+ require_relative "rabarber/input/types/proc"
17
+ require_relative "rabarber/input/types/symbol"
17
18
 
18
19
  require_relative "rabarber/missing/base"
19
20
  require_relative "rabarber/missing/actions"
@@ -25,7 +26,8 @@ require_relative "rabarber/controllers/concerns/authorization"
25
26
  require_relative "rabarber/helpers/helpers"
26
27
  require_relative "rabarber/models/concerns/has_roles"
27
28
  require_relative "rabarber/models/role"
28
- require_relative "rabarber/permissions"
29
+
30
+ require_relative "rabarber/core/permissions"
29
31
 
30
32
  require_relative "rabarber/railtie"
31
33
 
data/rabarber.gemspec CHANGED
@@ -8,7 +8,7 @@ Gem::Specification.new do |spec|
8
8
  spec.authors = ["enjaku4", "trafium"]
9
9
  spec.email = ["rabarber_gem@icloud.com"]
10
10
 
11
- spec.summary = "Simple authorization library for Ruby on Rails."
11
+ spec.summary = "Simple role-based authorization library for Ruby on Rails."
12
12
  spec.homepage = "https://github.com/enjaku4/rabarber"
13
13
  spec.license = "MIT"
14
14
  spec.required_ruby_version = ">= 3.0"
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.2.2
4
+ version: 1.3.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-02-10 00:00:00.000000000 Z
12
+ date: 2024-03-07 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: rails
@@ -38,27 +38,28 @@ files:
38
38
  - lib/generators/rabarber/roles_generator.rb
39
39
  - lib/generators/rabarber/templates/create_rabarber_roles.rb.erb
40
40
  - lib/rabarber.rb
41
- - lib/rabarber/access.rb
42
41
  - lib/rabarber/cache.rb
43
42
  - lib/rabarber/configuration.rb
44
43
  - lib/rabarber/controllers/concerns/authorization.rb
44
+ - lib/rabarber/core/access.rb
45
+ - lib/rabarber/core/permissions.rb
46
+ - lib/rabarber/core/rule.rb
45
47
  - lib/rabarber/helpers/helpers.rb
46
- - lib/rabarber/input/actions.rb
48
+ - lib/rabarber/input/action.rb
47
49
  - lib/rabarber/input/base.rb
48
- - lib/rabarber/input/dynamic_rules.rb
50
+ - lib/rabarber/input/dynamic_rule.rb
51
+ - lib/rabarber/input/role.rb
49
52
  - 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/input/types/boolean.rb
54
+ - lib/rabarber/input/types/proc.rb
55
+ - lib/rabarber/input/types/symbol.rb
53
56
  - lib/rabarber/logger.rb
54
57
  - lib/rabarber/missing/actions.rb
55
58
  - lib/rabarber/missing/base.rb
56
59
  - lib/rabarber/missing/roles.rb
57
60
  - lib/rabarber/models/concerns/has_roles.rb
58
61
  - lib/rabarber/models/role.rb
59
- - lib/rabarber/permissions.rb
60
62
  - lib/rabarber/railtie.rb
61
- - lib/rabarber/rule.rb
62
63
  - lib/rabarber/version.rb
63
64
  - rabarber.gemspec
64
65
  homepage: https://github.com/enjaku4/rabarber
@@ -80,8 +81,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
80
81
  - !ruby/object:Gem::Version
81
82
  version: '0'
82
83
  requirements: []
83
- rubygems_version: 3.2.33
84
+ rubygems_version: 3.3.26
84
85
  signing_key:
85
86
  specification_version: 4
86
- summary: Simple authorization library for Ruby on Rails.
87
+ summary: Simple role-based authorization library for Ruby on Rails.
87
88
  test_files: []
@@ -1,26 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Rabarber
4
- module Access
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
- end
9
-
10
- def controller_accessible?(roles, controller, dynamic_rule_receiver)
11
- accessible_controllers(roles, dynamic_rule_receiver).any? do |accessible_controller|
12
- controller <= accessible_controller
13
- end
14
- end
15
-
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
- end
19
-
20
- private
21
-
22
- def accessible_controllers(roles, dynamic_rule_receiver)
23
- controller_rules.select { |_, rule| rule.verify_access(roles, dynamic_rule_receiver) }.keys
24
- end
25
- end
26
- end
@@ -1,30 +0,0 @@
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
@@ -1,30 +0,0 @@
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
@@ -1,36 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative "access"
4
- require_relative "rule"
5
-
6
- module Rabarber
7
- class Permissions
8
- include Singleton
9
-
10
- extend Access
11
-
12
- attr_reader :storage
13
-
14
- def initialize
15
- @storage = { controller_rules: Hash.new({}), action_rules: Hash.new([]) }
16
- end
17
-
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
-
21
- if action
22
- instance.storage[:action_rules][controller] += [rule]
23
- else
24
- instance.storage[:controller_rules][controller] = rule
25
- end
26
- end
27
-
28
- def self.controller_rules
29
- instance.storage[:controller_rules]
30
- end
31
-
32
- def self.action_rules
33
- instance.storage[:action_rules]
34
- end
35
- end
36
- end
data/lib/rabarber/rule.rb DELETED
@@ -1,48 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Rabarber
4
- class Rule
5
- attr_reader :action, :roles, :dynamic_rule, :negated_dynamic_rule
6
-
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
12
- end
13
-
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)
16
- end
17
-
18
- def action_accessible?(action_name)
19
- action_name.nil? || action_name == action
20
- end
21
-
22
- def roles_permitted?(user_roles)
23
- return false if Rabarber::Configuration.instance.must_have_roles && user_roles.empty?
24
-
25
- roles.empty? || (roles & user_roles).any?
26
- end
27
-
28
- def dynamic_rule_followed?(dynamic_rule_receiver)
29
- !!(execute_dynamic_rule(dynamic_rule_receiver, false) && execute_dynamic_rule(dynamic_rule_receiver, true))
30
- end
31
-
32
- private
33
-
34
- def execute_dynamic_rule(dynamic_rule_receiver, is_negated)
35
- rule = is_negated ? negated_dynamic_rule : dynamic_rule
36
-
37
- return true if rule.nil?
38
-
39
- result = if rule.is_a?(Proc)
40
- dynamic_rule_receiver.instance_exec(&rule)
41
- else
42
- dynamic_rule_receiver.send(rule)
43
- end
44
-
45
- is_negated ? !result : result
46
- end
47
- end
48
- end