rabarber 1.3.1 → 1.4.0

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: fc0447acdc988dc558859b695d24d227697258a92bb1f444944fb76bfbace526
4
- data.tar.gz: 975f4377d5cc4fb2f28c42060012b67b3a85fea589aadb5106c33ecb943cde4a
3
+ metadata.gz: bcddaf07627eb382c58a733150e460348527476d2234f65fe0db2760bce6206f
4
+ data.tar.gz: 3d1bbfab2a57d860bcb9656cadb5606d09674f887a58b96db0897c3516aed5d2
5
5
  SHA512:
6
- metadata.gz: cbfed2814ae750ec0b2870126818cb67c6b55c28a7529d8ab2db3364adc463768853b41ac89ba4653d014aa1024622ae261f5b2c8afcc10b99194fd916a40266
7
- data.tar.gz: 7b77adeb8cc95acc4b104e6c39c6366b466262a8b0df0584d9e7d99d6369d96d01779a5962c7963696bccf7a941578a82774f2af23ac76d4a515bf37f7bed2ad
6
+ metadata.gz: e5ae51e17b580757cc427f269b43155ca3ea5eda5b4813be5d023c273f2271d96108639e24c1921ace823de4cc06b43c03372eb98d64f6d4376f9628e7b6d616
7
+ data.tar.gz: 232a9ded49264955b8cf7d22de5a1443947dd84d90f44f15f6dc9c9346ee7b1cee56afa85a55bf44bf5325bb1d26e17e6fcf0b8076d3f14381a9bb43254038ab
data/CHANGELOG.md CHANGED
@@ -1,3 +1,9 @@
1
+ ## 1.4.0
2
+
3
+ - Add 'Audit trail' feature: Logging of role assignments, revocations, and unauthorized access attempts
4
+ - Add `audit_trail_enabled` configuration option, allowing to enable or disable the audit trail
5
+ - Deprecate `when_actions_missing` and `when_roles_missing` configuration options (see [the discussion](https://github.com/enjaku4/rabarber/discussions/48))
6
+
1
7
  ## 1.3.1
2
8
 
3
9
  - Add `Rabarber::Role.assignees_for` method
data/README.md CHANGED
@@ -3,7 +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 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?'.
6
+ Rabarber is a role-based authorization library for Ruby on Rails, primarily designed for use in the web layer of your application but not limited to that. It provides a set of tools for managing user roles and defining authorization rules, along with audit logging for enhanced security.
7
7
 
8
8
  ---
9
9
 
@@ -63,26 +63,33 @@ If specific customization is required, Rabarber can be configured by using `.con
63
63
 
64
64
  ```rb
65
65
  Rabarber.configure do |config|
66
+ config.audit_trail_enabled = true
66
67
  config.cache_enabled = true
67
68
  config.current_user_method = :current_user
68
69
  config.must_have_roles = false
69
- config.when_actions_missing = -> (missing_actions, context) { ... }
70
- config.when_roles_missing = -> (missing_roles, context) { ... }
71
- config.when_unauthorized = -> (controller) { ... }
70
+ config.when_unauthorized = -> (controller) {
71
+ if controller.request.format.html?
72
+ controller.redirect_back fallback_location: controller.root_path
73
+ else
74
+ controller.head :unauthorized
75
+ end
76
+ }
72
77
  end
73
78
  ```
74
79
 
80
+ - `audit_trail_enabled` must be a boolean determining whether the audit trail functionality is enabled. _The audit trail is enabled by default._
75
81
  - `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.
76
-
77
82
  - `current_user_method` must be a symbol representing the method that returns the currently authenticated user. _The default value is `:current_user`._
78
-
79
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)._
84
+ - `when_unauthorized` must be a proc where you can define the behaviour when access is not authorized. Lambda argument `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
85
 
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
+ ### Deprecated Configuration Options
82
87
 
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
+ The following configuration options are deprecated and will be removed in the next major version (see [the discussion](https://github.com/enjaku4/rabarber/discussions/48)):
84
89
 
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
+ - `when_actions_missing` must be a proc where you can define the behaviour when the action specified in `grant_access` method cannot be found in the controller. Lambda argument `missing_actions` is an array of symbols, e.g., `[:index]`, while `context` argument is a hash that looks like this: `{ controller: "InvoicesController" }`. This check is performed when the application is initialized if `eager_load` configuration is enabled in Rails and also on every request. _By default, an error is raised when action is missing._
91
+
92
+ - `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. Lambda argument `missing_roles` is an array of symbols, e.g., `[:admin]`, while `context` argument is a hash that looks like this: `{ controller: "InvoicesController", action: "index" }`. This check is performed when the application is initialized if `eager_load` configuration is enabled in Rails and also on every request. _By default, a warning is logged when roles are missing._
86
93
 
87
94
  ## Roles
88
95
 
@@ -97,20 +104,20 @@ end
97
104
 
98
105
  This adds the following methods:
99
106
 
100
- **`#assign_roles`**
107
+ **`#assign_roles(*roles, create_new: true)`**
101
108
 
102
109
  To assign roles, use:
103
110
 
104
111
  ```rb
105
112
  user.assign_roles(:accountant, :marketer)
106
113
  ```
107
- By default, `#assign_roles` method will automatically create any roles that don't exist. If you want to assign only existing roles and prevent the creation of new ones, use the method with `create_new: false` argument:
114
+ By default, it will automatically create any roles that don't exist. If you want to assign only existing roles and prevent the creation of new ones, use the method with `create_new: false` argument:
108
115
  ```rb
109
116
  user.assign_roles(:accountant, :marketer, create_new: false)
110
117
  ```
111
118
  The method returns an array of roles assigned to the user.
112
119
 
113
- **`#revoke_roles`**
120
+ **`#revoke_roles(*roles)`**
114
121
 
115
122
  To revoke roles, use:
116
123
 
@@ -121,7 +128,7 @@ If any of the specified roles doesn't exist or the user doesn't have the role yo
121
128
 
122
129
  The method returns an array of roles assigned to the user.
123
130
 
124
- **`#has_role?`**
131
+ **`#has_role?(*roles)`**
125
132
 
126
133
  To check whether the user has a role, use:
127
134
 
@@ -143,7 +150,7 @@ user.roles
143
150
 
144
151
  To manipulate roles directly, you can use `Rabarber::Role` methods:
145
152
 
146
- **`.add`**
153
+ **`.add(role)`**
147
154
 
148
155
  To add a new role, use:
149
156
 
@@ -153,7 +160,7 @@ Rabarber::Role.add(:admin)
153
160
 
154
161
  This will create a new role with the specified name and return `true`. If the role already exists, it will return `false`.
155
162
 
156
- **`.rename`**
163
+ **`.rename(old_role_name, new_role_name, force: false)`**
157
164
 
158
165
  To rename a role, use:
159
166
 
@@ -167,7 +174,7 @@ The method won't rename the role and will return `false` if it is assigned to an
167
174
  Rabarber::Role.rename(:admin, :administrator, force: true)
168
175
  ```
169
176
 
170
- **`.remove`**
177
+ **`.remove(role, force: false)`**
171
178
 
172
179
  To remove a role, use:
173
180
 
@@ -190,7 +197,7 @@ If you need to list all the role names available in your application, use:
190
197
  Rabarber::Role.names
191
198
  ```
192
199
 
193
- **`.assignees_for`**
200
+ **`.assignees_for(role)`**
194
201
 
195
202
  To get all the users to whom the role is assigned, use:
196
203
 
@@ -208,7 +215,7 @@ class ApplicationController < ActionController::Base
208
215
  ...
209
216
  end
210
217
  ```
211
- This adds `.grant_access` method which allows you to define the authorization rules.
218
+ This adds `.grant_access(action: nil, roles: nil, if: nil, unless: nil)` method which allows you to define the authorization rules.
212
219
 
213
220
  The most basic usage of the method is as follows:
214
221
 
@@ -323,7 +330,7 @@ This means that `Crm::InvoicesController` is still accessible to `admin` but is
323
330
 
324
331
  ## View Helpers
325
332
 
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.
333
+ Rabarber also provides a couple of helpers that can be used in views: `visible_to(*roles, &block)` and `hidden_from(*roles, &block)`. To use them, simply include `Rabarber::Helpers` in the desired helper. Usually it is `ApplicationHelper`, but it can be any helper of your choice.
327
334
 
328
335
  ```rb
329
336
  module ApplicationHelper
@@ -346,6 +353,16 @@ The usage is straightforward:
346
353
  <% end %>
347
354
  ```
348
355
 
356
+ ## Audit Trail
357
+
358
+ Rabarber supports audit trail, which provides a record of user access control activity. This feature logs the following events:
359
+
360
+ - Role assignments to users
361
+ - Role revocations from users
362
+ - Unauthorized access attempts
363
+
364
+ The logs are written to the file `log/rabarber_audit.log` unless the `audit_trail_enabled` configuration option is set to `false`.
365
+
349
366
  ## Problems?
350
367
 
351
368
  Facing a problem or want to suggest an enhancement?
@@ -1,15 +1,14 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "singleton"
4
-
5
3
  module Rabarber
6
4
  class Configuration
7
5
  include Singleton
8
6
 
9
- attr_reader :cache_enabled, :current_user_method, :must_have_roles,
7
+ attr_reader :audit_trail_enabled, :cache_enabled, :current_user_method, :must_have_roles,
10
8
  :when_actions_missing, :when_roles_missing, :when_unauthorized
11
9
 
12
10
  def initialize
11
+ @audit_trail_enabled = default_audit_trail_enabled
13
12
  @cache_enabled = default_cache_enabled
14
13
  @current_user_method = default_current_user_method
15
14
  @must_have_roles = default_must_have_roles
@@ -18,6 +17,12 @@ module Rabarber
18
17
  @when_unauthorized = default_when_unauthorized
19
18
  end
20
19
 
20
+ def audit_trail_enabled=(value)
21
+ @audit_trail_enabled = Rabarber::Input::Types::Boolean.new(
22
+ value, Rabarber::ConfigurationError, "Configuration 'audit_trail_enabled' must be a Boolean"
23
+ ).process
24
+ end
25
+
21
26
  def cache_enabled=(value)
22
27
  @cache_enabled = Rabarber::Input::Types::Boolean.new(
23
28
  value, Rabarber::ConfigurationError, "Configuration 'cache_enabled' must be a Boolean"
@@ -56,6 +61,10 @@ module Rabarber
56
61
 
57
62
  private
58
63
 
64
+ def default_audit_trail_enabled
65
+ true
66
+ end
67
+
59
68
  def default_cache_enabled
60
69
  true
61
70
  end
@@ -70,21 +79,20 @@ module Rabarber
70
79
 
71
80
  def default_when_actions_missing
72
81
  -> (missing_actions, context) {
73
- raise Rabarber::Error, "Missing actions: #{missing_actions}, context: #{context[:controller]}"
82
+ raise(Rabarber::Error, "'grant_access' method called with non-existent actions: #{missing_actions}, context: '#{context[:controller]}'")
74
83
  }
75
84
  end
76
85
 
77
86
  def default_when_roles_missing
78
87
  -> (missing_roles, context) {
79
88
  delimiter = context[:action] ? "#" : ""
80
- message = "Missing roles: #{missing_roles}, context: #{context[:controller]}#{delimiter}#{context[:action]}"
89
+ message = "'grant_access' method called with non-existent roles: #{missing_roles}, context: '#{context[:controller]}#{delimiter}#{context[:action]}'"
81
90
  Rabarber::Logger.log(:warn, message)
82
91
  }
83
92
  end
84
93
 
85
94
  def default_when_unauthorized
86
95
  -> (controller) do
87
- Rabarber::Logger.log(:warn, "Unauthorized attempt")
88
96
  if controller.request.format.html?
89
97
  controller.redirect_back fallback_location: controller.main_app.root_path
90
98
  else
@@ -28,14 +28,18 @@ 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::Core::Permissions.access_granted?(rabarber_roles, self.class, action_name.to_sym, self)
31
+ roleable = send(Rabarber::Configuration.instance.current_user_method)
32
32
 
33
- Rabarber::Configuration.instance.when_unauthorized.call(self)
34
- end
33
+ return if Rabarber::Core::Permissions.access_granted?(
34
+ roleable ? roleable.roles : [], self.class, action_name.to_sym, self
35
+ )
35
36
 
36
- def rabarber_roles
37
- user = send(Rabarber::Configuration.instance.current_user_method)
38
- user ? user.roles : []
37
+ Rabarber::Logger.audit(
38
+ :warn,
39
+ "[Unauthorized Attempt] #{Rabarber::Logger.roleable_identity(roleable, with_roles: true)} attempted to access '#{request.path}'"
40
+ )
41
+
42
+ Rabarber::Configuration.instance.when_unauthorized.call(self)
39
43
  end
40
44
  end
41
45
  end
@@ -1,11 +1,40 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Rabarber
4
- module Logger
5
- module_function
4
+ class Logger
5
+ include Singleton
6
6
 
7
- def log(log_level, message)
8
- Rails.logger.tagged("Rabarber") { Rails.logger.public_send(log_level, message) }
7
+ attr_reader :rails_logger, :audit_logger
8
+
9
+ def initialize
10
+ @rails_logger = Rails.logger
11
+ @audit_logger = ::Logger.new(Rails.root.join("log/rabarber_audit.log"))
12
+ end
13
+
14
+ class << self
15
+ def log(log_level, message)
16
+ instance.rails_logger.tagged("Rabarber") { instance.rails_logger.public_send(log_level, message) }
17
+ end
18
+
19
+ def audit(log_level, message)
20
+ return unless Rabarber::Configuration.instance.audit_trail_enabled
21
+
22
+ instance.audit_logger.public_send(log_level, message)
23
+ end
24
+
25
+ def roleable_identity(roleable, with_roles:)
26
+ if roleable
27
+ model_name = roleable.model_name.human
28
+ primary_key = roleable.class.primary_key
29
+ roleable_id = roleable.public_send(primary_key)
30
+
31
+ roles = with_roles ? ", roles: #{roleable.roles}" : ""
32
+
33
+ "#{model_name} with #{primary_key}: '#{roleable_id}'#{roles}"
34
+ else
35
+ "Unauthenticated user"
36
+ end
37
+ end
9
38
  end
10
39
  end
11
40
  end
@@ -16,9 +16,7 @@ module Rabarber
16
16
  action_rules.each do |controller, controller_action_rules|
17
17
  controller_action_rules.each do |action_rule|
18
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
19
+ missing_list << Rabarber::Missing::Item.new(missing_roles, controller, action_rule.action) if missing_roles.any?
22
20
  end
23
21
  end
24
22
  end
@@ -5,9 +5,7 @@ module Rabarber
5
5
  extend ActiveSupport::Concern
6
6
 
7
7
  included do
8
- if defined?(@@included) && @@included != name
9
- raise Rabarber::Error, "Rabarber::HasRoles can only be included once"
10
- end
8
+ raise Rabarber::Error, "Rabarber::HasRoles can only be included once" if defined?(@@included) && @@included != name
11
9
 
12
10
  @@included = name
13
11
 
@@ -27,26 +25,37 @@ module Rabarber
27
25
  end
28
26
 
29
27
  def assign_roles(*role_names, create_new: true)
30
- roles_to_assign = process_role_names(role_names)
28
+ processed_role_names = process_role_names(role_names)
31
29
 
32
- create_new_roles(roles_to_assign) if create_new
30
+ create_new_roles(processed_role_names) if create_new
33
31
 
34
- new_roles = Rabarber::Role.where(name: roles_to_assign) - rabarber_roles
32
+ roles_to_assign = Rabarber::Role.where(name: processed_role_names) - rabarber_roles
35
33
 
36
- if new_roles.any?
34
+ if roles_to_assign.any?
37
35
  delete_roleable_cache
38
- rabarber_roles << new_roles
36
+ rabarber_roles << roles_to_assign
37
+
38
+ Rabarber::Logger.audit(
39
+ :info,
40
+ "[Role Assignment] #{Rabarber::Logger.roleable_identity(self, with_roles: false)} has been assigned the following roles: #{roles_to_assign.pluck(:name).map(&:to_sym)}, current roles: #{roles}"
41
+ )
39
42
  end
40
43
 
41
44
  roles
42
45
  end
43
46
 
44
47
  def revoke_roles(*role_names)
45
- new_roles = rabarber_roles - Rabarber::Role.where(name: process_role_names(role_names))
48
+ processed_role_names = process_role_names(role_names)
49
+ roles_to_revoke = Rabarber::Role.where(name: processed_role_names.intersection(roles))
46
50
 
47
- if rabarber_roles != new_roles
51
+ if roles_to_revoke.any?
48
52
  delete_roleable_cache
49
- self.rabarber_roles = new_roles
53
+ self.rabarber_roles -= roles_to_revoke
54
+
55
+ Rabarber::Logger.audit(
56
+ :info,
57
+ "[Role Revocation] #{Rabarber::Logger.roleable_identity(self, with_roles: false)} has been revoked from the following roles: #{roles_to_revoke.pluck(:name).map(&:to_sym)}, current roles: #{roles}"
58
+ )
50
59
  end
51
60
 
52
61
  roles
@@ -8,6 +8,11 @@ module Rabarber
8
8
  app.config.after_initialize do
9
9
  Rabarber::Missing::Actions.new.handle
10
10
  Rabarber::Missing::Roles.new.handle if Rabarber::Role.table_exists?
11
+
12
+ Rabarber::Logger.log(
13
+ :warn,
14
+ "DEPRECATION WARNING: Configurations 'when_actions_missing' and 'when_roles_missing' are deprecated and will be removed in v2.0.0"
15
+ )
11
16
  end
12
17
  end
13
18
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Rabarber
4
- VERSION = "1.3.1"
4
+ VERSION = "1.4.0"
5
5
  end
data/lib/rabarber.rb CHANGED
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "singleton"
4
+
3
5
  require_relative "rabarber/version"
4
6
  require_relative "rabarber/logger"
5
7
  require_relative "rabarber/configuration"
data/rabarber.gemspec CHANGED
@@ -7,7 +7,7 @@ Gem::Specification.new do |spec|
7
7
  spec.version = Rabarber::VERSION
8
8
  spec.authors = ["enjaku4", "trafium"]
9
9
  spec.email = ["rabarber_gem@icloud.com"]
10
-
10
+ spec.metadata["rubygems_mfa_required"] = "true"
11
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"
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.3.1
4
+ version: 1.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - enjaku4
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2024-03-07 00:00:00.000000000 Z
12
+ date: 2024-03-17 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: rails
@@ -65,7 +65,8 @@ files:
65
65
  homepage: https://github.com/enjaku4/rabarber
66
66
  licenses:
67
67
  - MIT
68
- metadata: {}
68
+ metadata:
69
+ rubygems_mfa_required: 'true'
69
70
  post_install_message:
70
71
  rdoc_options: []
71
72
  require_paths:
@@ -81,7 +82,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
81
82
  - !ruby/object:Gem::Version
82
83
  version: '0'
83
84
  requirements: []
84
- rubygems_version: 3.3.26
85
+ rubygems_version: 3.2.33
85
86
  signing_key:
86
87
  specification_version: 4
87
88
  summary: Simple role-based authorization library for Ruby on Rails.