decision_agent 0.1.3 → 0.1.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (110) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +84 -233
  3. data/lib/decision_agent/ab_testing/ab_test.rb +197 -0
  4. data/lib/decision_agent/ab_testing/ab_test_assignment.rb +76 -0
  5. data/lib/decision_agent/ab_testing/ab_test_manager.rb +317 -0
  6. data/lib/decision_agent/ab_testing/ab_testing_agent.rb +188 -0
  7. data/lib/decision_agent/ab_testing/storage/activerecord_adapter.rb +155 -0
  8. data/lib/decision_agent/ab_testing/storage/adapter.rb +67 -0
  9. data/lib/decision_agent/ab_testing/storage/memory_adapter.rb +116 -0
  10. data/lib/decision_agent/agent.rb +5 -3
  11. data/lib/decision_agent/auth/access_audit_logger.rb +122 -0
  12. data/lib/decision_agent/auth/authenticator.rb +127 -0
  13. data/lib/decision_agent/auth/password_reset_manager.rb +57 -0
  14. data/lib/decision_agent/auth/password_reset_token.rb +33 -0
  15. data/lib/decision_agent/auth/permission.rb +29 -0
  16. data/lib/decision_agent/auth/permission_checker.rb +43 -0
  17. data/lib/decision_agent/auth/rbac_adapter.rb +278 -0
  18. data/lib/decision_agent/auth/rbac_config.rb +51 -0
  19. data/lib/decision_agent/auth/role.rb +56 -0
  20. data/lib/decision_agent/auth/session.rb +33 -0
  21. data/lib/decision_agent/auth/session_manager.rb +57 -0
  22. data/lib/decision_agent/auth/user.rb +70 -0
  23. data/lib/decision_agent/context.rb +24 -4
  24. data/lib/decision_agent/decision.rb +10 -3
  25. data/lib/decision_agent/dsl/condition_evaluator.rb +378 -1
  26. data/lib/decision_agent/dsl/schema_validator.rb +8 -1
  27. data/lib/decision_agent/errors.rb +38 -0
  28. data/lib/decision_agent/evaluation.rb +10 -3
  29. data/lib/decision_agent/evaluation_validator.rb +8 -13
  30. data/lib/decision_agent/monitoring/dashboard_server.rb +1 -0
  31. data/lib/decision_agent/monitoring/metrics_collector.rb +164 -7
  32. data/lib/decision_agent/monitoring/storage/activerecord_adapter.rb +253 -0
  33. data/lib/decision_agent/monitoring/storage/base_adapter.rb +90 -0
  34. data/lib/decision_agent/monitoring/storage/memory_adapter.rb +222 -0
  35. data/lib/decision_agent/testing/batch_test_importer.rb +373 -0
  36. data/lib/decision_agent/testing/batch_test_runner.rb +244 -0
  37. data/lib/decision_agent/testing/test_coverage_analyzer.rb +191 -0
  38. data/lib/decision_agent/testing/test_result_comparator.rb +235 -0
  39. data/lib/decision_agent/testing/test_scenario.rb +42 -0
  40. data/lib/decision_agent/version.rb +10 -1
  41. data/lib/decision_agent/versioning/activerecord_adapter.rb +1 -1
  42. data/lib/decision_agent/versioning/file_storage_adapter.rb +96 -28
  43. data/lib/decision_agent/web/middleware/auth_middleware.rb +45 -0
  44. data/lib/decision_agent/web/middleware/permission_middleware.rb +94 -0
  45. data/lib/decision_agent/web/public/app.js +184 -29
  46. data/lib/decision_agent/web/public/batch_testing.html +640 -0
  47. data/lib/decision_agent/web/public/index.html +37 -9
  48. data/lib/decision_agent/web/public/login.html +298 -0
  49. data/lib/decision_agent/web/public/users.html +679 -0
  50. data/lib/decision_agent/web/server.rb +873 -7
  51. data/lib/decision_agent.rb +59 -0
  52. data/lib/generators/decision_agent/install/install_generator.rb +37 -0
  53. data/lib/generators/decision_agent/install/templates/ab_test_assignment_model.rb +45 -0
  54. data/lib/generators/decision_agent/install/templates/ab_test_model.rb +54 -0
  55. data/lib/generators/decision_agent/install/templates/ab_testing_migration.rb +43 -0
  56. data/lib/generators/decision_agent/install/templates/ab_testing_tasks.rake +189 -0
  57. data/lib/generators/decision_agent/install/templates/decision_agent_tasks.rake +114 -0
  58. data/lib/generators/decision_agent/install/templates/decision_log.rb +57 -0
  59. data/lib/generators/decision_agent/install/templates/error_metric.rb +53 -0
  60. data/lib/generators/decision_agent/install/templates/evaluation_metric.rb +43 -0
  61. data/lib/generators/decision_agent/install/templates/monitoring_migration.rb +109 -0
  62. data/lib/generators/decision_agent/install/templates/performance_metric.rb +76 -0
  63. data/lib/generators/decision_agent/install/templates/rule_version.rb +1 -1
  64. data/spec/ab_testing/ab_test_assignment_spec.rb +253 -0
  65. data/spec/ab_testing/ab_test_manager_spec.rb +612 -0
  66. data/spec/ab_testing/ab_test_spec.rb +270 -0
  67. data/spec/ab_testing/ab_testing_agent_spec.rb +481 -0
  68. data/spec/ab_testing/storage/adapter_spec.rb +64 -0
  69. data/spec/ab_testing/storage/memory_adapter_spec.rb +485 -0
  70. data/spec/advanced_operators_spec.rb +1003 -0
  71. data/spec/agent_spec.rb +40 -0
  72. data/spec/audit_adapters_spec.rb +18 -0
  73. data/spec/auth/access_audit_logger_spec.rb +394 -0
  74. data/spec/auth/authenticator_spec.rb +112 -0
  75. data/spec/auth/password_reset_spec.rb +294 -0
  76. data/spec/auth/permission_checker_spec.rb +207 -0
  77. data/spec/auth/permission_spec.rb +73 -0
  78. data/spec/auth/rbac_adapter_spec.rb +550 -0
  79. data/spec/auth/rbac_config_spec.rb +82 -0
  80. data/spec/auth/role_spec.rb +51 -0
  81. data/spec/auth/session_manager_spec.rb +172 -0
  82. data/spec/auth/session_spec.rb +112 -0
  83. data/spec/auth/user_spec.rb +130 -0
  84. data/spec/context_spec.rb +43 -0
  85. data/spec/decision_agent_spec.rb +96 -0
  86. data/spec/decision_spec.rb +423 -0
  87. data/spec/dsl/condition_evaluator_spec.rb +774 -0
  88. data/spec/evaluation_spec.rb +364 -0
  89. data/spec/evaluation_validator_spec.rb +165 -0
  90. data/spec/examples.txt +1542 -548
  91. data/spec/issue_verification_spec.rb +95 -21
  92. data/spec/monitoring/metrics_collector_spec.rb +221 -3
  93. data/spec/monitoring/monitored_agent_spec.rb +1 -1
  94. data/spec/monitoring/prometheus_exporter_spec.rb +1 -1
  95. data/spec/monitoring/storage/activerecord_adapter_spec.rb +498 -0
  96. data/spec/monitoring/storage/base_adapter_spec.rb +61 -0
  97. data/spec/monitoring/storage/memory_adapter_spec.rb +247 -0
  98. data/spec/performance_optimizations_spec.rb +486 -0
  99. data/spec/spec_helper.rb +23 -0
  100. data/spec/testing/batch_test_importer_spec.rb +693 -0
  101. data/spec/testing/batch_test_runner_spec.rb +307 -0
  102. data/spec/testing/test_coverage_analyzer_spec.rb +292 -0
  103. data/spec/testing/test_result_comparator_spec.rb +392 -0
  104. data/spec/testing/test_scenario_spec.rb +113 -0
  105. data/spec/versioning/adapter_spec.rb +156 -0
  106. data/spec/versioning_spec.rb +253 -0
  107. data/spec/web/middleware/auth_middleware_spec.rb +133 -0
  108. data/spec/web/middleware/permission_middleware_spec.rb +247 -0
  109. data/spec/web_ui_rack_spec.rb +1705 -0
  110. metadata +123 -6
@@ -0,0 +1,67 @@
1
+ module DecisionAgent
2
+ module ABTesting
3
+ module Storage
4
+ # Base adapter interface for A/B test persistence
5
+ class Adapter
6
+ # Save an A/B test
7
+ # @param test [ABTest] The test to save
8
+ # @return [ABTest] The saved test with ID
9
+ def save_test(test)
10
+ raise NotImplementedError, "#{self.class} must implement #save_test"
11
+ end
12
+
13
+ # Get an A/B test by ID
14
+ # @param test_id [String, Integer] The test ID
15
+ # @return [ABTest, nil] The test or nil
16
+ def get_test(test_id)
17
+ raise NotImplementedError, "#{self.class} must implement #get_test"
18
+ end
19
+
20
+ # Update an A/B test
21
+ # @param test_id [String, Integer] The test ID
22
+ # @param attributes [Hash] Attributes to update
23
+ # @return [ABTest] The updated test
24
+ def update_test(test_id, attributes)
25
+ raise NotImplementedError, "#{self.class} must implement #update_test"
26
+ end
27
+
28
+ # List A/B tests
29
+ # @param status [String, nil] Filter by status
30
+ # @param limit [Integer, nil] Limit results
31
+ # @return [Array<ABTest>] Array of tests
32
+ def list_tests(status: nil, limit: nil)
33
+ raise NotImplementedError, "#{self.class} must implement #list_tests"
34
+ end
35
+
36
+ # Save an assignment
37
+ # @param assignment [ABTestAssignment] The assignment to save
38
+ # @return [ABTestAssignment] The saved assignment with ID
39
+ def save_assignment(assignment)
40
+ raise NotImplementedError, "#{self.class} must implement #save_assignment"
41
+ end
42
+
43
+ # Update an assignment
44
+ # @param assignment_id [String, Integer] The assignment ID
45
+ # @param attributes [Hash] Attributes to update
46
+ # @return [ABTestAssignment] The updated assignment
47
+ def update_assignment(assignment_id, attributes)
48
+ raise NotImplementedError, "#{self.class} must implement #update_assignment"
49
+ end
50
+
51
+ # Get assignments for a test
52
+ # @param test_id [String, Integer] The test ID
53
+ # @return [Array<ABTestAssignment>] Array of assignments
54
+ def get_assignments(test_id)
55
+ raise NotImplementedError, "#{self.class} must implement #get_assignments"
56
+ end
57
+
58
+ # Delete a test and its assignments
59
+ # @param test_id [String, Integer] The test ID
60
+ # @return [Boolean] True if deleted
61
+ def delete_test(test_id)
62
+ raise NotImplementedError, "#{self.class} must implement #delete_test"
63
+ end
64
+ end
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,116 @@
1
+ require "monitor"
2
+
3
+ module DecisionAgent
4
+ module ABTesting
5
+ module Storage
6
+ # In-memory storage adapter for A/B tests
7
+ # Useful for testing and development
8
+ class MemoryAdapter < Adapter
9
+ include MonitorMixin
10
+
11
+ def initialize
12
+ super
13
+ @tests = {}
14
+ @assignments = {}
15
+ @test_id_counter = 0
16
+ @assignment_id_counter = 0
17
+ end
18
+
19
+ def save_test(test)
20
+ synchronize do
21
+ @test_id_counter += 1
22
+ test_data = test.to_h.merge(id: @test_id_counter)
23
+ @tests[@test_id_counter] = test_data
24
+
25
+ ABTest.new(**test_data)
26
+ end
27
+ end
28
+
29
+ def get_test(test_id)
30
+ synchronize do
31
+ test_data = @tests[test_id.to_i]
32
+ test_data ? ABTest.new(**test_data) : nil
33
+ end
34
+ end
35
+
36
+ def update_test(test_id, attributes)
37
+ synchronize do
38
+ test_data = @tests[test_id.to_i]
39
+ raise TestNotFoundError, "Test not found: #{test_id}" unless test_data
40
+
41
+ test_data.merge!(attributes)
42
+ @tests[test_id.to_i] = test_data
43
+
44
+ ABTest.new(**test_data)
45
+ end
46
+ end
47
+
48
+ def list_tests(status: nil, limit: nil)
49
+ synchronize do
50
+ tests = @tests.values
51
+
52
+ tests = tests.select { |t| t[:status] == status } if status
53
+ tests = tests.last(limit) if limit
54
+
55
+ tests.map { |t| ABTest.new(**t) }
56
+ end
57
+ end
58
+
59
+ def save_assignment(assignment)
60
+ synchronize do
61
+ @assignment_id_counter += 1
62
+ assignment_data = assignment.to_h.merge(id: @assignment_id_counter)
63
+ @assignments[@assignment_id_counter] = assignment_data
64
+
65
+ ABTestAssignment.new(**assignment_data)
66
+ end
67
+ end
68
+
69
+ def update_assignment(assignment_id, attributes)
70
+ synchronize do
71
+ assignment_data = @assignments[assignment_id.to_i]
72
+ raise "Assignment not found: #{assignment_id}" unless assignment_data
73
+
74
+ assignment_data.merge!(attributes)
75
+ @assignments[assignment_id.to_i] = assignment_data
76
+
77
+ ABTestAssignment.new(**assignment_data)
78
+ end
79
+ end
80
+
81
+ def get_assignments(test_id)
82
+ synchronize do
83
+ assignments = @assignments.values.select { |a| a[:ab_test_id] == test_id }
84
+ assignments.map { |a| ABTestAssignment.new(**a) }
85
+ end
86
+ end
87
+
88
+ def delete_test(test_id)
89
+ synchronize do
90
+ @tests.delete(test_id.to_i)
91
+ @assignments.delete_if { |_id, a| a[:ab_test_id] == test_id }
92
+ true
93
+ end
94
+ end
95
+
96
+ # Additional helper methods
97
+ def clear!
98
+ synchronize do
99
+ @tests.clear
100
+ @assignments.clear
101
+ @test_id_counter = 0
102
+ @assignment_id_counter = 0
103
+ end
104
+ end
105
+
106
+ def test_count
107
+ synchronize { @tests.size }
108
+ end
109
+
110
+ def assignment_count
111
+ synchronize { @assignments.size }
112
+ end
113
+ end
114
+ end
115
+ end
116
+ end
@@ -6,10 +6,12 @@ module DecisionAgent
6
6
  class Agent
7
7
  attr_reader :evaluators, :scoring_strategy, :audit_adapter
8
8
 
9
- def initialize(evaluators:, scoring_strategy: nil, audit_adapter: nil)
9
+ def initialize(evaluators:, scoring_strategy: nil, audit_adapter: nil, validate_evaluations: nil)
10
10
  @evaluators = Array(evaluators)
11
11
  @scoring_strategy = scoring_strategy || Scoring::WeightedAverage.new
12
12
  @audit_adapter = audit_adapter || Audit::NullAdapter.new
13
+ # Default to validating in development, skip in production for performance
14
+ @validate_evaluations = validate_evaluations.nil? ? (ENV["RAILS_ENV"] != "production") : validate_evaluations
13
15
 
14
16
  validate_configuration!
15
17
 
@@ -24,8 +26,8 @@ module DecisionAgent
24
26
 
25
27
  raise NoEvaluationsError if evaluations.empty?
26
28
 
27
- # Validate all evaluations for correctness and thread-safety
28
- EvaluationValidator.validate_all!(evaluations)
29
+ # Validate all evaluations for correctness and thread-safety (optional for performance)
30
+ EvaluationValidator.validate_all!(evaluations) if @validate_evaluations
29
31
 
30
32
  scored_result = @scoring_strategy.score(evaluations)
31
33
 
@@ -0,0 +1,122 @@
1
+ require "json"
2
+ require_relative "../audit/adapter"
3
+
4
+ module DecisionAgent
5
+ module Auth
6
+ class AccessAuditLogger
7
+ attr_reader :adapter
8
+
9
+ def initialize(adapter: nil)
10
+ @adapter = adapter || Audit::InMemoryAccessAdapter.new
11
+ end
12
+
13
+ def log_authentication(event_type, user_id:, email: nil, success: true, reason: nil)
14
+ log_entry = {
15
+ event_type: event_type.to_s,
16
+ user_id: user_id,
17
+ email: email,
18
+ success: success,
19
+ reason: reason,
20
+ timestamp: Time.now.utc.iso8601,
21
+ ip_address: nil # Can be set by middleware
22
+ }
23
+
24
+ @adapter.record_access(log_entry)
25
+ end
26
+
27
+ def log_permission_check(user_id:, permission:, resource_type: nil, resource_id: nil, granted: true)
28
+ log_entry = {
29
+ event_type: "permission_check",
30
+ user_id: user_id,
31
+ permission: permission.to_s,
32
+ resource_type: resource_type,
33
+ resource_id: resource_id,
34
+ granted: granted,
35
+ timestamp: Time.now.utc.iso8601
36
+ }
37
+
38
+ @adapter.record_access(log_entry)
39
+ end
40
+
41
+ def log_access(user_id:, action:, resource_type: nil, resource_id: nil, success: true)
42
+ log_entry = {
43
+ event_type: "access",
44
+ user_id: user_id,
45
+ action: action.to_s,
46
+ resource_type: resource_type,
47
+ resource_id: resource_id,
48
+ success: success,
49
+ timestamp: Time.now.utc.iso8601
50
+ }
51
+
52
+ @adapter.record_access(log_entry)
53
+ end
54
+
55
+ def query(filters = {})
56
+ @adapter.query_access_logs(filters)
57
+ end
58
+ end
59
+ end
60
+
61
+ module Audit
62
+ class AccessAdapter < Adapter
63
+ def record_access(log_entry)
64
+ raise NotImplementedError, "Subclasses must implement #record_access"
65
+ end
66
+
67
+ def query_access_logs(filters = {})
68
+ raise NotImplementedError, "Subclasses must implement #query_access_logs"
69
+ end
70
+ end
71
+
72
+ class InMemoryAccessAdapter < AccessAdapter
73
+ def initialize
74
+ super
75
+ @logs = []
76
+ @mutex = Mutex.new
77
+ end
78
+
79
+ def record_access(log_entry)
80
+ @mutex.synchronize do
81
+ @logs << log_entry.dup
82
+ end
83
+ end
84
+
85
+ def query_access_logs(filters = {})
86
+ @mutex.synchronize do
87
+ results = @logs.dup
88
+
89
+ results.select! { |log| log[:user_id] == filters[:user_id] } if filters[:user_id]
90
+
91
+ results.select! { |log| log[:event_type] == filters[:event_type].to_s } if filters[:event_type]
92
+
93
+ if filters[:start_time]
94
+ start_time = filters[:start_time].is_a?(String) ? Time.parse(filters[:start_time]) : filters[:start_time]
95
+ results.select! { |log| Time.parse(log[:timestamp]) >= start_time }
96
+ end
97
+
98
+ if filters[:end_time]
99
+ end_time = filters[:end_time].is_a?(String) ? Time.parse(filters[:end_time]) : filters[:end_time]
100
+ results.select! { |log| Time.parse(log[:timestamp]) <= end_time }
101
+ end
102
+
103
+ results = results.last(filters[:limit]) if filters[:limit]
104
+
105
+ results.reverse # Most recent first
106
+ end
107
+ end
108
+
109
+ def all_logs
110
+ @mutex.synchronize do
111
+ @logs.dup
112
+ end
113
+ end
114
+
115
+ def clear
116
+ @mutex.synchronize do
117
+ @logs.clear
118
+ end
119
+ end
120
+ end
121
+ end
122
+ end
@@ -0,0 +1,127 @@
1
+ module DecisionAgent
2
+ module Auth
3
+ class Authenticator
4
+ attr_reader :user_store, :session_manager, :password_reset_manager
5
+
6
+ def initialize(user_store: nil, session_manager: nil, password_reset_manager: nil)
7
+ @user_store = user_store || InMemoryUserStore.new
8
+ @session_manager = session_manager || SessionManager.new
9
+ @password_reset_manager = password_reset_manager || PasswordResetManager.new
10
+ end
11
+
12
+ def login(email, password)
13
+ user = @user_store.find_by_email(email)
14
+ return nil unless user
15
+ return nil unless user.active
16
+ return nil unless user.authenticate(password)
17
+
18
+ @session_manager.create_session(user.id)
19
+ end
20
+
21
+ def logout(token)
22
+ @session_manager.delete_session(token)
23
+ end
24
+
25
+ def authenticate(token)
26
+ session = @session_manager.get_session(token)
27
+ return nil unless session
28
+
29
+ user = @user_store.find_by_id(session.user_id)
30
+ return nil unless user
31
+ return nil unless user.active
32
+
33
+ { user: user, session: session }
34
+ end
35
+
36
+ def create_user(email:, password:, roles: [])
37
+ user = User.new(email: email, password: password, roles: roles)
38
+ @user_store.save(user)
39
+ user
40
+ end
41
+
42
+ def find_user(user_id)
43
+ @user_store.find_by_id(user_id)
44
+ end
45
+
46
+ def find_user_by_email(email)
47
+ @user_store.find_by_email(email)
48
+ end
49
+
50
+ def request_password_reset(email)
51
+ user = @user_store.find_by_email(email)
52
+ return nil unless user
53
+ return nil unless user.active
54
+
55
+ # Delete any existing reset tokens for this user
56
+ @password_reset_manager.delete_user_tokens(user.id)
57
+
58
+ # Create a new reset token (expires in 1 hour)
59
+ @password_reset_manager.create_token(user.id, expires_in: 3600)
60
+ end
61
+
62
+ def reset_password(token_string, new_password)
63
+ token = @password_reset_manager.get_token(token_string)
64
+ return nil unless token
65
+
66
+ user = @user_store.find_by_id(token.user_id)
67
+ return nil unless user
68
+ return nil unless user.active
69
+
70
+ # Update the password
71
+ user.update_password(new_password)
72
+ @user_store.save(user)
73
+
74
+ # Delete the used token and all other tokens for this user
75
+ @password_reset_manager.delete_user_tokens(user.id)
76
+
77
+ # Invalidate all existing sessions for security
78
+ @session_manager.delete_user_sessions(user.id)
79
+
80
+ user
81
+ end
82
+ end
83
+
84
+ # In-memory user store (can be replaced with ActiveRecord adapter later)
85
+ class InMemoryUserStore
86
+ def initialize
87
+ @users = {}
88
+ @users_by_email = {}
89
+ @mutex = Mutex.new
90
+ end
91
+
92
+ def save(user)
93
+ @mutex.synchronize do
94
+ @users[user.id] = user
95
+ @users_by_email[user.email.downcase] = user
96
+ end
97
+ user
98
+ end
99
+
100
+ def find_by_id(id)
101
+ @mutex.synchronize do
102
+ @users[id]
103
+ end
104
+ end
105
+
106
+ def find_by_email(email)
107
+ @mutex.synchronize do
108
+ @users_by_email[email.downcase]
109
+ end
110
+ end
111
+
112
+ def all
113
+ @mutex.synchronize do
114
+ @users.values.dup
115
+ end
116
+ end
117
+
118
+ def delete(id)
119
+ @mutex.synchronize do
120
+ user = @users.delete(id)
121
+ @users_by_email.delete(user.email.downcase) if user
122
+ user
123
+ end
124
+ end
125
+ end
126
+ end
127
+ end
@@ -0,0 +1,57 @@
1
+ module DecisionAgent
2
+ module Auth
3
+ class PasswordResetManager
4
+ def initialize
5
+ @tokens = {}
6
+ @mutex = Mutex.new
7
+ @cleanup_interval = 300 # 5 minutes
8
+ @last_cleanup = Time.now
9
+ end
10
+
11
+ def create_token(user_id, expires_in: 3600)
12
+ token = PasswordResetToken.new(user_id: user_id, expires_in: expires_in)
13
+ @mutex.synchronize do
14
+ @tokens[token.token] = token
15
+ cleanup_expired_tokens
16
+ end
17
+ token
18
+ end
19
+
20
+ def get_token(token_string)
21
+ @mutex.synchronize do
22
+ token = @tokens[token_string]
23
+ return nil unless token
24
+ return nil if token.expired?
25
+
26
+ token
27
+ end
28
+ end
29
+
30
+ def delete_token(token_string)
31
+ @mutex.synchronize do
32
+ @tokens.delete(token_string)
33
+ end
34
+ end
35
+
36
+ def delete_user_tokens(user_id)
37
+ @mutex.synchronize do
38
+ @tokens.delete_if { |_token_string, token| token.user_id == user_id }
39
+ end
40
+ end
41
+
42
+ def cleanup_expired_tokens
43
+ now = Time.now
44
+ return if (now - @last_cleanup) < @cleanup_interval
45
+
46
+ @tokens.delete_if { |_token_string, token| token.expired? }
47
+ @last_cleanup = now
48
+ end
49
+
50
+ def count
51
+ @mutex.synchronize do
52
+ @tokens.size
53
+ end
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,33 @@
1
+ require "securerandom"
2
+
3
+ module DecisionAgent
4
+ module Auth
5
+ class PasswordResetToken
6
+ attr_reader :token, :user_id, :created_at, :expires_at
7
+
8
+ def initialize(user_id:, expires_in: 3600)
9
+ @token = SecureRandom.hex(32)
10
+ @user_id = user_id
11
+ @created_at = Time.now.utc
12
+ @expires_at = @created_at + expires_in
13
+ end
14
+
15
+ def expired?
16
+ Time.now.utc > @expires_at
17
+ end
18
+
19
+ def valid?
20
+ !expired?
21
+ end
22
+
23
+ def to_h
24
+ {
25
+ token: @token,
26
+ user_id: @user_id,
27
+ created_at: @created_at.iso8601,
28
+ expires_at: @expires_at.iso8601
29
+ }
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,29 @@
1
+ module DecisionAgent
2
+ module Auth
3
+ class Permission
4
+ PERMISSIONS = {
5
+ read: "Read access to rules and versions",
6
+ write: "Create and modify rules",
7
+ delete: "Delete rules and versions",
8
+ approve: "Approve rule changes",
9
+ deploy: "Deploy rule versions",
10
+ manage_users: "Manage users and roles",
11
+ audit: "Access audit logs"
12
+ }.freeze
13
+
14
+ class << self
15
+ def all
16
+ PERMISSIONS.keys
17
+ end
18
+
19
+ def exists?(permission)
20
+ PERMISSIONS.key?(permission.to_sym)
21
+ end
22
+
23
+ def description_for(permission)
24
+ PERMISSIONS[permission.to_sym]
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,43 @@
1
+ module DecisionAgent
2
+ module Auth
3
+ class PermissionChecker
4
+ attr_reader :adapter
5
+
6
+ def initialize(adapter: nil)
7
+ @adapter = adapter || DefaultAdapter.new
8
+ end
9
+
10
+ def can?(user, permission, resource = nil)
11
+ @adapter.can?(user, permission, resource)
12
+ end
13
+
14
+ def require_permission!(user, permission, resource = nil)
15
+ raise PermissionDeniedError, "User does not have permission: #{permission}" unless can?(user, permission, resource)
16
+
17
+ true
18
+ end
19
+
20
+ def has_role?(user, role)
21
+ @adapter.has_role?(user, role)
22
+ end
23
+
24
+ def require_role!(user, role)
25
+ raise PermissionDeniedError, "User does not have role: #{role}" unless has_role?(user, role)
26
+
27
+ true
28
+ end
29
+
30
+ def active?(user)
31
+ @adapter.active?(user)
32
+ end
33
+
34
+ def user_id(user)
35
+ @adapter.user_id(user)
36
+ end
37
+
38
+ def user_email(user)
39
+ @adapter.user_email(user)
40
+ end
41
+ end
42
+ end
43
+ end