rabarber 1.4.0 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rabarber
4
+ module Audit
5
+ module Events
6
+ class RolesAssigned < Base
7
+ private
8
+
9
+ def nil_roleable_allowed?
10
+ false
11
+ end
12
+
13
+ def log_level
14
+ :info
15
+ end
16
+
17
+ def message
18
+ "[Role Assignment] #{identity} has been assigned the following roles: #{roles_to_assign}, current roles: #{current_roles}"
19
+ end
20
+
21
+ def identity_with_roles?
22
+ false
23
+ end
24
+
25
+ def roles_to_assign
26
+ specifics.fetch(:roles_to_assign)
27
+ end
28
+
29
+ def current_roles
30
+ specifics.fetch(:current_roles)
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rabarber
4
+ module Audit
5
+ module Events
6
+ class RolesRevoked < Base
7
+ private
8
+
9
+ def nil_roleable_allowed?
10
+ false
11
+ end
12
+
13
+ def log_level
14
+ :info
15
+ end
16
+
17
+ def message
18
+ "[Role Revocation] #{identity} has been revoked from the following roles: #{roles_to_revoke}, current roles: #{current_roles}"
19
+ end
20
+
21
+ def identity_with_roles?
22
+ false
23
+ end
24
+
25
+ def roles_to_revoke
26
+ specifics.fetch(:roles_to_revoke)
27
+ end
28
+
29
+ def current_roles
30
+ specifics.fetch(:current_roles)
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rabarber
4
+ module Audit
5
+ module Events
6
+ class UnauthorizedAttempt < Base
7
+ private
8
+
9
+ def nil_roleable_allowed?
10
+ true
11
+ end
12
+
13
+ def log_level
14
+ :warn
15
+ end
16
+
17
+ def message
18
+ "[Unauthorized Attempt] #{identity} attempted to access '#{path}'"
19
+ end
20
+
21
+ def identity_with_roles?
22
+ true
23
+ end
24
+
25
+ def path
26
+ specifics.fetch(:path)
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "singleton"
4
+
5
+ module Rabarber
6
+ module Audit
7
+ class Logger
8
+ include Singleton
9
+
10
+ attr_reader :logger
11
+
12
+ def initialize
13
+ @logger = ::Logger.new(Rails.root.join("log/rabarber_audit.log"))
14
+ end
15
+
16
+ def self.log(log_level, message)
17
+ return unless Rabarber::Configuration.instance.audit_trail_enabled
18
+
19
+ instance.logger.public_send(log_level, message)
20
+ end
21
+ end
22
+ end
23
+ end
@@ -1,20 +1,18 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "singleton"
4
+
3
5
  module Rabarber
4
6
  class Configuration
5
7
  include Singleton
6
8
 
7
- attr_reader :audit_trail_enabled, :cache_enabled, :current_user_method, :must_have_roles,
8
- :when_actions_missing, :when_roles_missing, :when_unauthorized
9
+ attr_reader :audit_trail_enabled, :cache_enabled, :current_user_method, :must_have_roles
9
10
 
10
11
  def initialize
11
12
  @audit_trail_enabled = default_audit_trail_enabled
12
13
  @cache_enabled = default_cache_enabled
13
14
  @current_user_method = default_current_user_method
14
15
  @must_have_roles = default_must_have_roles
15
- @when_actions_missing = default_when_actions_missing
16
- @when_roles_missing = default_when_roles_missing
17
- @when_unauthorized = default_when_unauthorized
18
16
  end
19
17
 
20
18
  def audit_trail_enabled=(value)
@@ -41,24 +39,6 @@ module Rabarber
41
39
  ).process
42
40
  end
43
41
 
44
- def when_actions_missing=(callable)
45
- @when_actions_missing = Rabarber::Input::Types::Proc.new(
46
- callable, Rabarber::ConfigurationError, "Configuration 'when_actions_missing' must be a Proc"
47
- ).process
48
- end
49
-
50
- def when_roles_missing=(callable)
51
- @when_roles_missing = Rabarber::Input::Types::Proc.new(
52
- callable, Rabarber::ConfigurationError, "Configuration 'when_roles_missing' must be a Proc"
53
- ).process
54
- end
55
-
56
- def when_unauthorized=(callable)
57
- @when_unauthorized = Rabarber::Input::Types::Proc.new(
58
- callable, Rabarber::ConfigurationError, "Configuration 'when_unauthorized' must be a Proc"
59
- ).process
60
- end
61
-
62
42
  private
63
43
 
64
44
  def default_audit_trail_enabled
@@ -76,29 +56,5 @@ module Rabarber
76
56
  def default_must_have_roles
77
57
  false
78
58
  end
79
-
80
- def default_when_actions_missing
81
- -> (missing_actions, context) {
82
- raise(Rabarber::Error, "'grant_access' method called with non-existent actions: #{missing_actions}, context: '#{context[:controller]}'")
83
- }
84
- end
85
-
86
- def default_when_roles_missing
87
- -> (missing_roles, context) {
88
- delimiter = context[:action] ? "#" : ""
89
- message = "'grant_access' method called with non-existent roles: #{missing_roles}, context: '#{context[:controller]}#{delimiter}#{context[:action]}'"
90
- Rabarber::Logger.log(:warn, message)
91
- }
92
- end
93
-
94
- def default_when_unauthorized
95
- -> (controller) do
96
- if controller.request.format.html?
97
- controller.redirect_back fallback_location: controller.main_app.root_path
98
- else
99
- controller.head(:unauthorized)
100
- end
101
- end
102
- end
103
59
  end
104
60
  end
@@ -4,6 +4,8 @@ module Rabarber
4
4
  module Authorization
5
5
  extend ActiveSupport::Concern
6
6
 
7
+ include Rabarber::Core::Roleable
8
+
7
9
  included do
8
10
  before_action :verify_access
9
11
  end
@@ -25,21 +27,17 @@ module Rabarber
25
27
  private
26
28
 
27
29
  def verify_access
28
- Rabarber::Missing::Actions.new(self.class).handle
29
- Rabarber::Missing::Roles.new(self.class).handle
30
+ Rabarber::Core::PermissionsIntegrityChecker.new(self.class).run! unless Rails.configuration.eager_load
30
31
 
31
- roleable = send(Rabarber::Configuration.instance.current_user_method)
32
+ return if Rabarber::Core::Permissions.access_granted?(roleable_roles, self.class, action_name.to_sym, self)
32
33
 
33
- return if Rabarber::Core::Permissions.access_granted?(
34
- roleable ? roleable.roles : [], self.class, action_name.to_sym, self
35
- )
34
+ Rabarber::Audit::Events::UnauthorizedAttempt.trigger(roleable, path: request.path)
36
35
 
37
- Rabarber::Logger.audit(
38
- :warn,
39
- "[Unauthorized Attempt] #{Rabarber::Logger.roleable_identity(roleable, with_roles: true)} attempted to access '#{request.path}'"
40
- )
36
+ when_unauthorized
37
+ end
41
38
 
42
- Rabarber::Configuration.instance.when_unauthorized.call(self)
39
+ def when_unauthorized
40
+ request.format.html? ? redirect_back(fallback_location: root_path) : head(:unauthorized)
43
41
  end
44
42
  end
45
43
  end
@@ -9,19 +9,15 @@ module Rabarber
9
9
  end
10
10
 
11
11
  def controller_accessible?(roles, controller, dynamic_rule_receiver)
12
- accessible_controllers(roles, dynamic_rule_receiver).any? do |accessible_controller|
13
- controller <= accessible_controller
12
+ controller_rules.any? do |rule_controller, rule|
13
+ controller <= rule_controller && rule.verify_access(roles, dynamic_rule_receiver)
14
14
  end
15
15
  end
16
16
 
17
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
18
+ action_rules[controller].any? do |rule|
19
+ rule.action == action && rule.verify_access(roles, dynamic_rule_receiver)
20
+ end
25
21
  end
26
22
  end
27
23
  end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rabarber
4
+ module Core
5
+ module Cache
6
+ extend self
7
+
8
+ CACHE_PREFIX = "rabarber"
9
+ private_constant :CACHE_PREFIX
10
+
11
+ def fetch(roleable_id, options = { expires_in: 1.hour, race_condition_ttl: 5.seconds }, &block)
12
+ enabled? ? Rails.cache.fetch(key_for(roleable_id), **options, &block) : yield
13
+ end
14
+
15
+ def delete(*roleable_ids)
16
+ keys = roleable_ids.map { |roleable_id| key_for(roleable_id) }
17
+ Rails.cache.delete_multi(keys) if enabled? && keys.any?
18
+ end
19
+
20
+ def enabled?
21
+ Rabarber::Configuration.instance.cache_enabled
22
+ end
23
+
24
+ def clear
25
+ Rails.cache.delete_matched(/^#{CACHE_PREFIX}/o)
26
+ end
27
+
28
+ private
29
+
30
+ def key_for(id)
31
+ "#{CACHE_PREFIX}:roles_#{id}"
32
+ end
33
+ end
34
+ end
35
+
36
+ module Cache
37
+ def clear
38
+ Rabarber::Core::Cache.clear
39
+ end
40
+ module_function :clear
41
+ end
42
+ end
@@ -3,6 +3,8 @@
3
3
  require_relative "access"
4
4
  require_relative "rule"
5
5
 
6
+ require "singleton"
7
+
6
8
  module Rabarber
7
9
  module Core
8
10
  class Permissions
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rabarber
4
+ module Core
5
+ class PermissionsIntegrityChecker
6
+ attr_reader :controller
7
+
8
+ def initialize(controller = nil)
9
+ @controller = controller
10
+ end
11
+
12
+ def run!
13
+ return if missing_list.empty?
14
+
15
+ raise(
16
+ Rabarber::Error,
17
+ "Following actions were passed to 'grant_access' method but are not defined in the controller: #{missing_list}"
18
+ )
19
+ end
20
+
21
+ private
22
+
23
+ def missing_list
24
+ @missing_list ||= action_rules.each_with_object([]) do |(controller, rules), arr|
25
+ missing_actions = rules.map(&:action) - controller.action_methods.map(&:to_sym)
26
+ arr << { controller => missing_actions } if missing_actions.any?
27
+ end
28
+ end
29
+
30
+ def action_rules
31
+ if controller
32
+ Rabarber::Core::Permissions.action_rules.slice(controller)
33
+ else
34
+ Rabarber::Core::Permissions.action_rules
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rabarber
4
+ module Core
5
+ module Roleable
6
+ def roleable
7
+ send(Rabarber::Configuration.instance.current_user_method)
8
+ end
9
+
10
+ def roleable_roles
11
+ roleable&.roles.to_a
12
+ end
13
+ end
14
+ end
15
+ end
@@ -12,18 +12,14 @@ module Rabarber
12
12
  @negated_dynamic_rule = negated_dynamic_rule
13
13
  end
14
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)
15
+ def verify_access(roleable_roles, dynamic_rule_receiver)
16
+ roles_permitted?(roleable_roles) && dynamic_rule_followed?(dynamic_rule_receiver)
17
17
  end
18
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?
19
+ def roles_permitted?(roleable_roles)
20
+ return false if Rabarber::Configuration.instance.must_have_roles && roleable_roles.empty?
25
21
 
26
- roles.empty? || (roles & user_roles).any?
22
+ roles.empty? || roles.intersection(roleable_roles).any?
27
23
  end
28
24
 
29
25
  def dynamic_rule_followed?(dynamic_rule_receiver)
@@ -2,14 +2,16 @@
2
2
 
3
3
  module Rabarber
4
4
  module Helpers
5
+ include Rabarber::Core::Roleable
6
+
5
7
  def visible_to(*roles, &block)
6
- return unless send(Rabarber::Configuration.instance.current_user_method).has_role?(*roles)
8
+ return unless roleable_roles.intersection(Rabarber::Input::Roles.new(roles).process).any?
7
9
 
8
10
  capture(&block)
9
11
  end
10
12
 
11
13
  def hidden_from(*roles, &block)
12
- return if send(Rabarber::Configuration.instance.current_user_method).has_role?(*roles)
14
+ return if roleable_roles.intersection(Rabarber::Input::Roles.new(roles).process).any?
13
15
 
14
16
  capture(&block)
15
17
  end
@@ -15,13 +15,11 @@ module Rabarber
15
15
  end
16
16
 
17
17
  def roles
18
- Rabarber::Cache.fetch(Rabarber::Cache.key_for(roleable_id), expires_in: 1.hour, race_condition_ttl: 5.seconds) do
19
- rabarber_roles.names
20
- end
18
+ Rabarber::Core::Cache.fetch(roleable_id) { rabarber_roles.names }
21
19
  end
22
20
 
23
21
  def has_role?(*role_names)
24
- (roles & process_role_names(role_names)).any?
22
+ roles.intersection(process_role_names(role_names)).any?
25
23
  end
26
24
 
27
25
  def assign_roles(*role_names, create_new: true)
@@ -29,16 +27,13 @@ module Rabarber
29
27
 
30
28
  create_new_roles(processed_role_names) if create_new
31
29
 
32
- roles_to_assign = Rabarber::Role.where(name: processed_role_names) - rabarber_roles
30
+ roles_to_assign = Rabarber::Role.where(name: processed_role_names - rabarber_roles.names)
33
31
 
34
32
  if roles_to_assign.any?
35
33
  delete_roleable_cache
36
34
  rabarber_roles << roles_to_assign
37
35
 
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
- )
36
+ Rabarber::Audit::Events::RolesAssigned.trigger(self, roles_to_assign: roles_to_assign.names, current_roles: roles)
42
37
  end
43
38
 
44
39
  roles
@@ -52,10 +47,7 @@ module Rabarber
52
47
  delete_roleable_cache
53
48
  self.rabarber_roles -= roles_to_revoke
54
49
 
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
+ Rabarber::Audit::Events::RolesRevoked.trigger(self, roles_to_revoke: roles_to_revoke.names, current_roles: roles)
59
51
  end
60
52
 
61
53
  roles
@@ -78,7 +70,7 @@ module Rabarber
78
70
  end
79
71
 
80
72
  def delete_roleable_cache
81
- Rabarber::Cache.delete(Rabarber::Cache.key_for(roleable_id))
73
+ Rabarber::Core::Cache.delete(roleable_id)
82
74
  end
83
75
 
84
76
  def roleable_id
@@ -18,8 +18,6 @@ module Rabarber
18
18
 
19
19
  return false if exists?(name: name)
20
20
 
21
- delete_roles_cache
22
-
23
21
  !!create!(name: name)
24
22
  end
25
23
 
@@ -29,7 +27,6 @@ module Rabarber
29
27
 
30
28
  return false if !role || exists?(name: name) || assigned_to_roleables(role).any? && !force
31
29
 
32
- delete_roles_cache
33
30
  delete_roleables_cache(role)
34
31
 
35
32
  role.update!(name: name)
@@ -40,13 +37,12 @@ module Rabarber
40
37
 
41
38
  return false if !role || assigned_to_roleables(role).any? && !force
42
39
 
43
- delete_roles_cache
44
40
  delete_roleables_cache(role)
45
41
 
46
42
  !!role.destroy!
47
43
  end
48
44
 
49
- def assignees_for(name)
45
+ def assignees(name)
50
46
  Rabarber::HasRoles.roleable_class.joins(:rabarber_roles).where(
51
47
  rabarber_roles: { name: Rabarber::Input::Role.new(name).process }
52
48
  )
@@ -54,18 +50,15 @@ module Rabarber
54
50
 
55
51
  private
56
52
 
57
- def delete_roles_cache
58
- Rabarber::Cache.delete(Rabarber::Cache::ALL_ROLES_KEY)
59
- end
60
-
61
53
  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?
54
+ Rabarber::Core::Cache.delete(*assigned_to_roleables(role))
64
55
  end
65
56
 
66
57
  def assigned_to_roleables(role)
67
58
  ActiveRecord::Base.connection.select_values(
68
- "SELECT roleable_id FROM rabarber_roles_roleables WHERE role_id = #{role.id}"
59
+ ActiveRecord::Base.sanitize_sql(
60
+ ["SELECT roleable_id FROM rabarber_roles_roleables WHERE role_id = ?", role.id]
61
+ )
69
62
  )
70
63
  end
71
64
 
@@ -6,13 +6,7 @@ module Rabarber
6
6
  class Railtie < Rails::Railtie
7
7
  initializer "rabarber.after_initialize" do |app|
8
8
  app.config.after_initialize do
9
- Rabarber::Missing::Actions.new.handle
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
- )
9
+ Rabarber::Core::PermissionsIntegrityChecker.new.run! if Rails.configuration.eager_load
16
10
  end
17
11
  end
18
12
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Rabarber
4
- VERSION = "1.4.0"
4
+ VERSION = "2.0.0"
5
5
  end
data/lib/rabarber.rb CHANGED
@@ -1,9 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "singleton"
4
-
5
3
  require_relative "rabarber/version"
6
- require_relative "rabarber/logger"
7
4
  require_relative "rabarber/configuration"
8
5
 
9
6
  require "active_record"
@@ -18,11 +15,14 @@ require_relative "rabarber/input/types/boolean"
18
15
  require_relative "rabarber/input/types/proc"
19
16
  require_relative "rabarber/input/types/symbol"
20
17
 
21
- require_relative "rabarber/missing/base"
22
- require_relative "rabarber/missing/actions"
23
- require_relative "rabarber/missing/roles"
18
+ require_relative "rabarber/core/cache"
19
+
20
+ require_relative "rabarber/audit/events/base"
21
+ require_relative "rabarber/audit/events/roles_assigned"
22
+ require_relative "rabarber/audit/events/roles_revoked"
23
+ require_relative "rabarber/audit/events/unauthorized_attempt"
24
24
 
25
- require_relative "rabarber/cache"
25
+ require_relative "rabarber/core/roleable"
26
26
 
27
27
  require_relative "rabarber/controllers/concerns/authorization"
28
28
  require_relative "rabarber/helpers/helpers"
@@ -30,12 +30,11 @@ require_relative "rabarber/models/concerns/has_roles"
30
30
  require_relative "rabarber/models/role"
31
31
 
32
32
  require_relative "rabarber/core/permissions"
33
+ require_relative "rabarber/core/permissions_integrity_checker"
33
34
 
34
35
  require_relative "rabarber/railtie"
35
36
 
36
37
  module Rabarber
37
- module_function
38
-
39
38
  class Error < StandardError; end
40
39
  class ConfigurationError < Rabarber::Error; end
41
40
  class InvalidArgumentError < Rabarber::Error; end
@@ -43,4 +42,5 @@ module Rabarber
43
42
  def configure
44
43
  yield(Rabarber::Configuration.instance)
45
44
  end
45
+ module_function :configure
46
46
  end
data/rabarber.gemspec CHANGED
@@ -11,7 +11,7 @@ Gem::Specification.new do |spec|
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"
14
- spec.required_ruby_version = ">= 3.0"
14
+ spec.required_ruby_version = ">= 3.0", "< 3.4"
15
15
 
16
16
  spec.files = [
17
17
  "rabarber.gemspec", "README.md", "CHANGELOG.md", "LICENSE.txt"
@@ -19,5 +19,5 @@ Gem::Specification.new do |spec|
19
19
 
20
20
  spec.require_paths = ["lib"]
21
21
 
22
- spec.add_runtime_dependency "rails", ">= 6.1"
22
+ spec.add_runtime_dependency "rails", ">= 6.1", "< 7.2"
23
23
  end