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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: bf7a31c94f695cb0edb8209562d07971f3fc712bd868bf589fd4b1ee4ffb2865
4
- data.tar.gz: 2c5d939673b685beba64910dc4ee361f811d760cc8bb6982b502b3e0f4354c5b
3
+ metadata.gz: 3ae0f6a7f272a6d718ddf591e61b236a20b26da7c7eddc56f838637e6fd16b72
4
+ data.tar.gz: 07bb483c2ed0fc8e04b12fab9183d333cddae5ef4d3b3690d98119e1d9d97c5b
5
5
  SHA512:
6
- metadata.gz: c513a7ea4140a12b37384b0bc91969eec5305f1d422a24d3a6117607d9b1b0dbdaa77398f4ce49fa2025b9fb1101439c09c274cded09acbb58a46b0bd54b9b3b
7
- data.tar.gz: 93f18fe608254896b97e772e05d9c94e31f17f769fd4d4f5314460450d19408c7f3f41a1197a7834b43ab81e6bbf694446ed94b23df9af6b9e904040534afc1a
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 an 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.
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 adding the following code into an initializer:
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.when_unauthorized = ->(controller) {
73
- controller.head 418
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
- - `when_unauthorized` must be a lambda where you can define your actions 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.
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 see anything.
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 lambda.
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`. The usage is straightforward:
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 or have a feature request, 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.
296
- - **Contribute a Solution**: Found a fix for the issue or want to contribute to the project? Feel free to create a pull request with your changes.
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).
@@ -8,6 +8,8 @@ module Rabarber
8
8
 
9
9
  source_root File.expand_path("templates", __dir__)
10
10
 
11
+ argument :table_name, type: :string, required: true
12
+
11
13
  def create_migrations
12
14
  migration_template "create_rabarber_roles.rb.erb", "db/migrate/create_rabarber_roles.rb"
13
15
  end
@@ -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: raise(Rabarber::Error, "Please specify your user model's table name") }
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, %i[role_id roleable_id], unique: true
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, :when_unauthorized
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
- @current_user_method = :current_user
13
- @must_have_roles = false
14
- @when_unauthorized = ->(controller) do
15
- if controller.request.format.html?
16
- controller.redirect_back fallback_location: controller.main_app.root_path
17
- else
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 current_user_method=(method_name)
24
- unless (method_name.is_a?(Symbol) || method_name.is_a?(String)) && method_name.present?
25
- raise ConfigurationError, "Configuration 'current_user_method' must be a Symbol or a String"
26
- end
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
- @current_user_method = method_name.to_sym
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
- raise ConfigurationError, "Configuration 'must_have_roles' must be a Boolean" unless [true, false].include?(value)
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
- @must_have_roles = value
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
- raise ConfigurationError, "Configuration 'when_unauthorized' must be a Proc" unless callable.is_a?(Proc)
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
- @when_unauthorized = callable
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.write(self, action, roles, dynamic_rule, negated_dynamic_rule)
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
- return if Permissions.access_granted?(
23
- send(::Rabarber::Configuration.instance.current_user_method).roles, self.class, action_name.to_sym, self
24
- )
28
+ Rabarber::Missing::Actions.new(self.class).handle
29
+ Rabarber::Missing::Roles.new(self.class).handle
25
30
 
26
- ::Rabarber::Configuration.instance.when_unauthorized.call(self)
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(::Rabarber::Configuration.instance.current_user_method).has_role?(*roles)
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(::Rabarber::Configuration.instance.current_user_method).has_role?(*roles)
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 Procs < Rabarber::Input::Base
7
+ private
8
+
9
+ def valid?
10
+ value.is_a?(Proc)
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
- raise Error, "Rabarber::HasRoles can only be included once" if defined?(@@included) && @@included != name
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
- rabarber_roles.names
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 & RoleNames.pre_process(role_names)).any?
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 = RoleNames.pre_process(role_names)
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: RoleNames.pre_process(role_names))
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
@@ -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: RoleNames::REGEX }
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
@@ -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.write(controller, action, roles, dynamic_rule, negated_dynamic_rule)
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 = pre_process_action(action)
9
- @roles = RoleNames.pre_process(Array(roles))
10
- @dynamic_rule = pre_process_dynamic_rule(dynamic_rule)
11
- @negated_dynamic_rule = pre_process_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
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 ::Rabarber::Configuration.instance.must_have_roles && user_roles.empty?
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Rabarber
4
- VERSION = "1.1.0"
4
+ VERSION = "1.2.1"
5
5
  end
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/role_names"
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 = ["enjaku4@gmail.com"]
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.0
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-01-18 00:00:00.000000000 Z
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
- - enjaku4@gmail.com
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/role_names.rb
59
+ - lib/rabarber/railtie.rb
49
60
  - lib/rabarber/rule.rb
50
61
  - lib/rabarber/version.rb
51
62
  - rabarber.gemspec
@@ -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