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.
- checksums.yaml +4 -4
- data/README.md +84 -233
- data/lib/decision_agent/ab_testing/ab_test.rb +197 -0
- data/lib/decision_agent/ab_testing/ab_test_assignment.rb +76 -0
- data/lib/decision_agent/ab_testing/ab_test_manager.rb +317 -0
- data/lib/decision_agent/ab_testing/ab_testing_agent.rb +188 -0
- data/lib/decision_agent/ab_testing/storage/activerecord_adapter.rb +155 -0
- data/lib/decision_agent/ab_testing/storage/adapter.rb +67 -0
- data/lib/decision_agent/ab_testing/storage/memory_adapter.rb +116 -0
- data/lib/decision_agent/agent.rb +5 -3
- data/lib/decision_agent/auth/access_audit_logger.rb +122 -0
- data/lib/decision_agent/auth/authenticator.rb +127 -0
- data/lib/decision_agent/auth/password_reset_manager.rb +57 -0
- data/lib/decision_agent/auth/password_reset_token.rb +33 -0
- data/lib/decision_agent/auth/permission.rb +29 -0
- data/lib/decision_agent/auth/permission_checker.rb +43 -0
- data/lib/decision_agent/auth/rbac_adapter.rb +278 -0
- data/lib/decision_agent/auth/rbac_config.rb +51 -0
- data/lib/decision_agent/auth/role.rb +56 -0
- data/lib/decision_agent/auth/session.rb +33 -0
- data/lib/decision_agent/auth/session_manager.rb +57 -0
- data/lib/decision_agent/auth/user.rb +70 -0
- data/lib/decision_agent/context.rb +24 -4
- data/lib/decision_agent/decision.rb +10 -3
- data/lib/decision_agent/dsl/condition_evaluator.rb +378 -1
- data/lib/decision_agent/dsl/schema_validator.rb +8 -1
- data/lib/decision_agent/errors.rb +38 -0
- data/lib/decision_agent/evaluation.rb +10 -3
- data/lib/decision_agent/evaluation_validator.rb +8 -13
- data/lib/decision_agent/monitoring/dashboard_server.rb +1 -0
- data/lib/decision_agent/monitoring/metrics_collector.rb +164 -7
- data/lib/decision_agent/monitoring/storage/activerecord_adapter.rb +253 -0
- data/lib/decision_agent/monitoring/storage/base_adapter.rb +90 -0
- data/lib/decision_agent/monitoring/storage/memory_adapter.rb +222 -0
- data/lib/decision_agent/testing/batch_test_importer.rb +373 -0
- data/lib/decision_agent/testing/batch_test_runner.rb +244 -0
- data/lib/decision_agent/testing/test_coverage_analyzer.rb +191 -0
- data/lib/decision_agent/testing/test_result_comparator.rb +235 -0
- data/lib/decision_agent/testing/test_scenario.rb +42 -0
- data/lib/decision_agent/version.rb +10 -1
- data/lib/decision_agent/versioning/activerecord_adapter.rb +1 -1
- data/lib/decision_agent/versioning/file_storage_adapter.rb +96 -28
- data/lib/decision_agent/web/middleware/auth_middleware.rb +45 -0
- data/lib/decision_agent/web/middleware/permission_middleware.rb +94 -0
- data/lib/decision_agent/web/public/app.js +184 -29
- data/lib/decision_agent/web/public/batch_testing.html +640 -0
- data/lib/decision_agent/web/public/index.html +37 -9
- data/lib/decision_agent/web/public/login.html +298 -0
- data/lib/decision_agent/web/public/users.html +679 -0
- data/lib/decision_agent/web/server.rb +873 -7
- data/lib/decision_agent.rb +59 -0
- data/lib/generators/decision_agent/install/install_generator.rb +37 -0
- data/lib/generators/decision_agent/install/templates/ab_test_assignment_model.rb +45 -0
- data/lib/generators/decision_agent/install/templates/ab_test_model.rb +54 -0
- data/lib/generators/decision_agent/install/templates/ab_testing_migration.rb +43 -0
- data/lib/generators/decision_agent/install/templates/ab_testing_tasks.rake +189 -0
- data/lib/generators/decision_agent/install/templates/decision_agent_tasks.rake +114 -0
- data/lib/generators/decision_agent/install/templates/decision_log.rb +57 -0
- data/lib/generators/decision_agent/install/templates/error_metric.rb +53 -0
- data/lib/generators/decision_agent/install/templates/evaluation_metric.rb +43 -0
- data/lib/generators/decision_agent/install/templates/monitoring_migration.rb +109 -0
- data/lib/generators/decision_agent/install/templates/performance_metric.rb +76 -0
- data/lib/generators/decision_agent/install/templates/rule_version.rb +1 -1
- data/spec/ab_testing/ab_test_assignment_spec.rb +253 -0
- data/spec/ab_testing/ab_test_manager_spec.rb +612 -0
- data/spec/ab_testing/ab_test_spec.rb +270 -0
- data/spec/ab_testing/ab_testing_agent_spec.rb +481 -0
- data/spec/ab_testing/storage/adapter_spec.rb +64 -0
- data/spec/ab_testing/storage/memory_adapter_spec.rb +485 -0
- data/spec/advanced_operators_spec.rb +1003 -0
- data/spec/agent_spec.rb +40 -0
- data/spec/audit_adapters_spec.rb +18 -0
- data/spec/auth/access_audit_logger_spec.rb +394 -0
- data/spec/auth/authenticator_spec.rb +112 -0
- data/spec/auth/password_reset_spec.rb +294 -0
- data/spec/auth/permission_checker_spec.rb +207 -0
- data/spec/auth/permission_spec.rb +73 -0
- data/spec/auth/rbac_adapter_spec.rb +550 -0
- data/spec/auth/rbac_config_spec.rb +82 -0
- data/spec/auth/role_spec.rb +51 -0
- data/spec/auth/session_manager_spec.rb +172 -0
- data/spec/auth/session_spec.rb +112 -0
- data/spec/auth/user_spec.rb +130 -0
- data/spec/context_spec.rb +43 -0
- data/spec/decision_agent_spec.rb +96 -0
- data/spec/decision_spec.rb +423 -0
- data/spec/dsl/condition_evaluator_spec.rb +774 -0
- data/spec/evaluation_spec.rb +364 -0
- data/spec/evaluation_validator_spec.rb +165 -0
- data/spec/examples.txt +1542 -548
- data/spec/issue_verification_spec.rb +95 -21
- data/spec/monitoring/metrics_collector_spec.rb +221 -3
- data/spec/monitoring/monitored_agent_spec.rb +1 -1
- data/spec/monitoring/prometheus_exporter_spec.rb +1 -1
- data/spec/monitoring/storage/activerecord_adapter_spec.rb +498 -0
- data/spec/monitoring/storage/base_adapter_spec.rb +61 -0
- data/spec/monitoring/storage/memory_adapter_spec.rb +247 -0
- data/spec/performance_optimizations_spec.rb +486 -0
- data/spec/spec_helper.rb +23 -0
- data/spec/testing/batch_test_importer_spec.rb +693 -0
- data/spec/testing/batch_test_runner_spec.rb +307 -0
- data/spec/testing/test_coverage_analyzer_spec.rb +292 -0
- data/spec/testing/test_result_comparator_spec.rb +392 -0
- data/spec/testing/test_scenario_spec.rb +113 -0
- data/spec/versioning/adapter_spec.rb +156 -0
- data/spec/versioning_spec.rb +253 -0
- data/spec/web/middleware/auth_middleware_spec.rb +133 -0
- data/spec/web/middleware/permission_middleware_spec.rb +247 -0
- data/spec/web_ui_rack_spec.rb +1705 -0
- metadata +123 -6
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
module DecisionAgent
|
|
2
|
+
module Auth
|
|
3
|
+
# Base adapter interface for RBAC integration
|
|
4
|
+
# Users can extend this to integrate with any authentication/authorization system
|
|
5
|
+
class RbacAdapter
|
|
6
|
+
# Check if a user has a specific permission
|
|
7
|
+
# @param user [Object] The user object from your auth system
|
|
8
|
+
# @param permission [Symbol, String] The permission to check
|
|
9
|
+
# @param resource [Object, nil] Optional resource for resource-level permissions
|
|
10
|
+
# @return [Boolean] true if user has permission, false otherwise
|
|
11
|
+
def can?(user, permission, resource = nil)
|
|
12
|
+
raise NotImplementedError, "Subclasses must implement #can?"
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# Check if a user has a specific role
|
|
16
|
+
# @param user [Object] The user object from your auth system
|
|
17
|
+
# @param role [Symbol, String] The role to check
|
|
18
|
+
# @return [Boolean] true if user has role, false otherwise
|
|
19
|
+
def has_role?(user, role)
|
|
20
|
+
raise NotImplementedError, "Subclasses must implement #has_role?"
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Check if a user is active/enabled
|
|
24
|
+
# @param user [Object] The user object from your auth system
|
|
25
|
+
# @return [Boolean] true if user is active, false otherwise
|
|
26
|
+
def active?(user)
|
|
27
|
+
return false unless user
|
|
28
|
+
|
|
29
|
+
# Default implementation - can be overridden
|
|
30
|
+
user.respond_to?(:active?) ? user.active? : true
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Get user ID for audit/logging purposes
|
|
34
|
+
# @param user [Object] The user object from your auth system
|
|
35
|
+
# @return [String, Integer] User identifier
|
|
36
|
+
def user_id(user)
|
|
37
|
+
return nil unless user
|
|
38
|
+
|
|
39
|
+
user.respond_to?(:id) ? user.id : user.to_s
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Get user email for display/logging purposes
|
|
43
|
+
# @param user [Object] The user object from your auth system
|
|
44
|
+
# @return [String, nil] User email
|
|
45
|
+
def user_email(user)
|
|
46
|
+
return nil unless user
|
|
47
|
+
|
|
48
|
+
user.respond_to?(:email) ? user.email : nil
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Default adapter using the built-in User/Role/Permission system
|
|
53
|
+
class DefaultAdapter < RbacAdapter
|
|
54
|
+
def can?(user, permission, _resource = nil)
|
|
55
|
+
return false unless user
|
|
56
|
+
return false unless active?(user)
|
|
57
|
+
|
|
58
|
+
# Check if user has any role with the required permission
|
|
59
|
+
roles = extract_roles(user)
|
|
60
|
+
roles.any? do |role|
|
|
61
|
+
Role.has_permission?(role, permission)
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def has_role?(user, role)
|
|
66
|
+
return false unless user
|
|
67
|
+
|
|
68
|
+
roles = extract_roles(user)
|
|
69
|
+
roles.include?(role.to_sym)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def active?(user)
|
|
73
|
+
return false unless user
|
|
74
|
+
|
|
75
|
+
user.respond_to?(:active) ? user.active : true
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
private
|
|
79
|
+
|
|
80
|
+
def extract_roles(user)
|
|
81
|
+
if user.respond_to?(:roles)
|
|
82
|
+
Array(user.roles).map(&:to_sym)
|
|
83
|
+
elsif user.respond_to?(:role)
|
|
84
|
+
[user.role.to_sym]
|
|
85
|
+
else
|
|
86
|
+
[]
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# Adapter for Devise + CanCanCan integration
|
|
92
|
+
class DeviseCanCanAdapter < RbacAdapter
|
|
93
|
+
def initialize(ability_class: nil)
|
|
94
|
+
super()
|
|
95
|
+
@ability_class = ability_class
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def can?(user, permission, resource = nil)
|
|
99
|
+
return false unless user
|
|
100
|
+
return false unless active?(user)
|
|
101
|
+
|
|
102
|
+
# CanCanCan uses :can? method with action and resource
|
|
103
|
+
if user.respond_to?(:can?)
|
|
104
|
+
# Map permission to CanCanCan action
|
|
105
|
+
action = map_permission_to_action(permission)
|
|
106
|
+
user.can?(action, resource || Object)
|
|
107
|
+
elsif @ability_class
|
|
108
|
+
# Use Ability class if provided
|
|
109
|
+
ability = @ability_class.new(user)
|
|
110
|
+
action = map_permission_to_action(permission)
|
|
111
|
+
ability.can?(action, resource || Object)
|
|
112
|
+
else
|
|
113
|
+
false
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def has_role?(user, role)
|
|
118
|
+
return false unless user
|
|
119
|
+
return false unless active?(user)
|
|
120
|
+
|
|
121
|
+
# Check if user has role via CanCanCan roles or other methods
|
|
122
|
+
if user.respond_to?(:has_role?)
|
|
123
|
+
user.has_role?(role)
|
|
124
|
+
elsif user.respond_to?(:roles)
|
|
125
|
+
user.roles.any? { |r| r.to_s == role.to_s || r.name.to_s == role.to_s }
|
|
126
|
+
else
|
|
127
|
+
false
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def active?(user)
|
|
132
|
+
return false unless user
|
|
133
|
+
|
|
134
|
+
# Devise typically uses active_for_authentication? or active?
|
|
135
|
+
if user.respond_to?(:active_for_authentication?)
|
|
136
|
+
user.active_for_authentication?
|
|
137
|
+
elsif user.respond_to?(:active?)
|
|
138
|
+
user.active?
|
|
139
|
+
else
|
|
140
|
+
true
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
private
|
|
145
|
+
|
|
146
|
+
def map_permission_to_action(permission)
|
|
147
|
+
# Map decision_agent permissions to CanCanCan actions
|
|
148
|
+
mapping = {
|
|
149
|
+
read: :read,
|
|
150
|
+
write: :create,
|
|
151
|
+
delete: :destroy,
|
|
152
|
+
approve: :approve,
|
|
153
|
+
deploy: :deploy,
|
|
154
|
+
manage_users: :manage,
|
|
155
|
+
audit: :read
|
|
156
|
+
}
|
|
157
|
+
mapping[permission.to_sym] || permission.to_sym
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
# Adapter for Pundit authorization
|
|
162
|
+
class PunditAdapter < RbacAdapter
|
|
163
|
+
def can?(user, permission, resource = nil)
|
|
164
|
+
return false unless user
|
|
165
|
+
return false unless active?(user)
|
|
166
|
+
|
|
167
|
+
# Pundit uses policy classes
|
|
168
|
+
if resource.respond_to?(:policy_class)
|
|
169
|
+
policy = resource.policy_class.new(user, resource)
|
|
170
|
+
action = map_permission_to_action(permission)
|
|
171
|
+
policy.respond_to?(action) && policy.public_send(action)
|
|
172
|
+
elsif resource
|
|
173
|
+
# Try to infer policy class from resource
|
|
174
|
+
policy_class_name = "#{resource.class.name}Policy"
|
|
175
|
+
if Object.const_defined?(policy_class_name)
|
|
176
|
+
policy_class = Object.const_get(policy_class_name)
|
|
177
|
+
policy = policy_class.new(user, resource)
|
|
178
|
+
action = map_permission_to_action(permission)
|
|
179
|
+
policy.respond_to?(action) && policy.public_send(action)
|
|
180
|
+
else
|
|
181
|
+
false
|
|
182
|
+
end
|
|
183
|
+
else
|
|
184
|
+
false
|
|
185
|
+
end
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
def has_role?(user, role)
|
|
189
|
+
return false unless user
|
|
190
|
+
return false unless active?(user)
|
|
191
|
+
|
|
192
|
+
if user.respond_to?(:has_role?)
|
|
193
|
+
user.has_role?(role)
|
|
194
|
+
elsif user.respond_to?(:roles)
|
|
195
|
+
user.roles.any? { |r| r.to_s == role.to_s || r.name.to_s == role.to_s }
|
|
196
|
+
else
|
|
197
|
+
false
|
|
198
|
+
end
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
private
|
|
202
|
+
|
|
203
|
+
def map_permission_to_action(permission)
|
|
204
|
+
mapping = {
|
|
205
|
+
read: :show,
|
|
206
|
+
write: :create,
|
|
207
|
+
delete: :destroy,
|
|
208
|
+
approve: :approve,
|
|
209
|
+
deploy: :deploy,
|
|
210
|
+
manage_users: :manage,
|
|
211
|
+
audit: :audit
|
|
212
|
+
}
|
|
213
|
+
mapping[permission.to_sym] || permission.to_sym
|
|
214
|
+
end
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
# Custom adapter that allows users to provide their own logic via blocks/procs
|
|
218
|
+
class CustomAdapter < RbacAdapter
|
|
219
|
+
def initialize(
|
|
220
|
+
can_proc: nil,
|
|
221
|
+
has_role_proc: nil,
|
|
222
|
+
active_proc: nil,
|
|
223
|
+
user_id_proc: nil,
|
|
224
|
+
user_email_proc: nil
|
|
225
|
+
)
|
|
226
|
+
super()
|
|
227
|
+
@can_proc = can_proc
|
|
228
|
+
@has_role_proc = has_role_proc
|
|
229
|
+
@active_proc = active_proc
|
|
230
|
+
@user_id_proc = user_id_proc
|
|
231
|
+
@user_email_proc = user_email_proc
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
def can?(user, permission, resource = nil)
|
|
235
|
+
return false unless user
|
|
236
|
+
return false unless active?(user)
|
|
237
|
+
|
|
238
|
+
raise NotImplementedError, "CustomAdapter requires can_proc to be provided" unless @can_proc
|
|
239
|
+
|
|
240
|
+
@can_proc.call(user, permission, resource)
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
def has_role?(user, role)
|
|
244
|
+
return false unless user
|
|
245
|
+
|
|
246
|
+
raise NotImplementedError, "CustomAdapter requires has_role_proc to be provided" unless @has_role_proc
|
|
247
|
+
|
|
248
|
+
@has_role_proc.call(user, role)
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
def active?(user)
|
|
252
|
+
return false unless user
|
|
253
|
+
|
|
254
|
+
if @active_proc
|
|
255
|
+
@active_proc.call(user)
|
|
256
|
+
else
|
|
257
|
+
super
|
|
258
|
+
end
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
def user_id(user)
|
|
262
|
+
if @user_id_proc
|
|
263
|
+
@user_id_proc.call(user)
|
|
264
|
+
else
|
|
265
|
+
super
|
|
266
|
+
end
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
def user_email(user)
|
|
270
|
+
if @user_email_proc
|
|
271
|
+
@user_email_proc.call(user)
|
|
272
|
+
else
|
|
273
|
+
super
|
|
274
|
+
end
|
|
275
|
+
end
|
|
276
|
+
end
|
|
277
|
+
end
|
|
278
|
+
end
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
module DecisionAgent
|
|
2
|
+
module Auth
|
|
3
|
+
# Configuration class for RBAC adapter
|
|
4
|
+
class RbacConfig
|
|
5
|
+
attr_accessor :authenticator, :user_store
|
|
6
|
+
|
|
7
|
+
def initialize
|
|
8
|
+
@adapter = nil
|
|
9
|
+
@authenticator = nil
|
|
10
|
+
@user_store = nil
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
# Configure with a built-in adapter
|
|
14
|
+
# @param adapter_type [Symbol] :default, :devise_cancan, :pundit, or :custom
|
|
15
|
+
# @param options [Hash] Options for the adapter
|
|
16
|
+
def use(adapter_type, **options)
|
|
17
|
+
case adapter_type.to_sym
|
|
18
|
+
when :default
|
|
19
|
+
@adapter = DefaultAdapter.new
|
|
20
|
+
when :devise_cancan
|
|
21
|
+
@adapter = DeviseCanCanAdapter.new(**options)
|
|
22
|
+
when :pundit
|
|
23
|
+
@adapter = PunditAdapter.new(**options)
|
|
24
|
+
when :custom
|
|
25
|
+
@adapter = CustomAdapter.new(**options)
|
|
26
|
+
else
|
|
27
|
+
raise ArgumentError, "Unknown adapter type: #{adapter_type}. Use :default, :devise_cancan, :pundit, or :custom"
|
|
28
|
+
end
|
|
29
|
+
self
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Configure with a custom adapter instance
|
|
33
|
+
# @param adapter_instance [RbacAdapter] An instance of RbacAdapter or subclass
|
|
34
|
+
def adapter=(adapter_instance)
|
|
35
|
+
raise ArgumentError, "Adapter must be an instance of DecisionAgent::Auth::RbacAdapter" unless adapter_instance.is_a?(RbacAdapter)
|
|
36
|
+
|
|
37
|
+
@adapter = adapter_instance
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Get the configured adapter, or return default if none configured
|
|
41
|
+
def adapter
|
|
42
|
+
@adapter || DefaultAdapter.new
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Check if an adapter has been configured
|
|
46
|
+
def configured?
|
|
47
|
+
!@adapter.nil?
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
module DecisionAgent
|
|
2
|
+
module Auth
|
|
3
|
+
class Role
|
|
4
|
+
ROLES = {
|
|
5
|
+
admin: {
|
|
6
|
+
name: "Admin",
|
|
7
|
+
permissions: %i[read write delete approve deploy manage_users audit]
|
|
8
|
+
},
|
|
9
|
+
editor: {
|
|
10
|
+
name: "Editor",
|
|
11
|
+
permissions: %i[read write]
|
|
12
|
+
},
|
|
13
|
+
viewer: {
|
|
14
|
+
name: "Viewer",
|
|
15
|
+
permissions: [:read]
|
|
16
|
+
},
|
|
17
|
+
auditor: {
|
|
18
|
+
name: "Auditor",
|
|
19
|
+
permissions: %i[read audit]
|
|
20
|
+
},
|
|
21
|
+
approver: {
|
|
22
|
+
name: "Approver",
|
|
23
|
+
permissions: %i[read approve]
|
|
24
|
+
}
|
|
25
|
+
}.freeze
|
|
26
|
+
|
|
27
|
+
class << self
|
|
28
|
+
def all
|
|
29
|
+
ROLES.keys
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def exists?(role)
|
|
33
|
+
ROLES.key?(role.to_sym)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def permissions_for(role)
|
|
37
|
+
role_data = ROLES[role.to_sym]
|
|
38
|
+
return [] unless role_data
|
|
39
|
+
|
|
40
|
+
role_data[:permissions]
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def name_for(role)
|
|
44
|
+
role_data = ROLES[role.to_sym]
|
|
45
|
+
return nil unless role_data
|
|
46
|
+
|
|
47
|
+
role_data[:name]
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def has_permission?(role, permission)
|
|
51
|
+
permissions_for(role).include?(permission.to_sym)
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
require "securerandom"
|
|
2
|
+
|
|
3
|
+
module DecisionAgent
|
|
4
|
+
module Auth
|
|
5
|
+
class Session
|
|
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,57 @@
|
|
|
1
|
+
module DecisionAgent
|
|
2
|
+
module Auth
|
|
3
|
+
class SessionManager
|
|
4
|
+
def initialize
|
|
5
|
+
@sessions = {}
|
|
6
|
+
@mutex = Mutex.new
|
|
7
|
+
@cleanup_interval = 300 # 5 minutes
|
|
8
|
+
@last_cleanup = Time.now
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def create_session(user_id, expires_in: 3600)
|
|
12
|
+
session = Session.new(user_id: user_id, expires_in: expires_in)
|
|
13
|
+
@mutex.synchronize do
|
|
14
|
+
@sessions[session.token] = session
|
|
15
|
+
cleanup_expired_sessions
|
|
16
|
+
end
|
|
17
|
+
session
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def get_session(token)
|
|
21
|
+
@mutex.synchronize do
|
|
22
|
+
session = @sessions[token]
|
|
23
|
+
return nil unless session
|
|
24
|
+
return nil if session.expired?
|
|
25
|
+
|
|
26
|
+
session
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def delete_session(token)
|
|
31
|
+
@mutex.synchronize do
|
|
32
|
+
@sessions.delete(token)
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def delete_user_sessions(user_id)
|
|
37
|
+
@mutex.synchronize do
|
|
38
|
+
@sessions.delete_if { |_token, session| session.user_id == user_id }
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def cleanup_expired_sessions
|
|
43
|
+
now = Time.now
|
|
44
|
+
return if (now - @last_cleanup) < @cleanup_interval
|
|
45
|
+
|
|
46
|
+
@sessions.delete_if { |_token, session| session.expired? }
|
|
47
|
+
@last_cleanup = now
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def count
|
|
51
|
+
@mutex.synchronize do
|
|
52
|
+
@sessions.size
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
require "bcrypt"
|
|
2
|
+
require "securerandom"
|
|
3
|
+
|
|
4
|
+
module DecisionAgent
|
|
5
|
+
module Auth
|
|
6
|
+
class User
|
|
7
|
+
attr_reader :id, :email, :roles, :created_at, :updated_at
|
|
8
|
+
attr_accessor :active
|
|
9
|
+
|
|
10
|
+
def initialize(email:, password: nil, password_hash: nil, roles: [], active: true, id: nil)
|
|
11
|
+
@id = id || SecureRandom.uuid
|
|
12
|
+
@email = email
|
|
13
|
+
@roles = Array(roles)
|
|
14
|
+
@active = active
|
|
15
|
+
@created_at = Time.now.utc
|
|
16
|
+
@updated_at = Time.now.utc
|
|
17
|
+
|
|
18
|
+
if password_hash
|
|
19
|
+
@password_hash = password_hash
|
|
20
|
+
elsif password
|
|
21
|
+
@password_hash = BCrypt::Password.create(password)
|
|
22
|
+
else
|
|
23
|
+
raise ArgumentError, "Either password or password_hash must be provided"
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def authenticate(password)
|
|
28
|
+
return false unless @active
|
|
29
|
+
|
|
30
|
+
BCrypt::Password.new(@password_hash) == password
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def assign_role(role)
|
|
34
|
+
role_symbol = role.to_sym
|
|
35
|
+
@roles << role_symbol unless @roles.include?(role_symbol)
|
|
36
|
+
@updated_at = Time.now.utc
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def remove_role(role)
|
|
40
|
+
role_symbol = role.to_sym
|
|
41
|
+
@roles.delete(role_symbol)
|
|
42
|
+
@updated_at = Time.now.utc
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def has_role?(role)
|
|
46
|
+
@roles.include?(role.to_sym)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def update_password(new_password)
|
|
50
|
+
@password_hash = BCrypt::Password.create(new_password)
|
|
51
|
+
@updated_at = Time.now.utc
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def to_h
|
|
55
|
+
{
|
|
56
|
+
id: @id,
|
|
57
|
+
email: @email,
|
|
58
|
+
roles: @roles.map(&:to_s),
|
|
59
|
+
active: @active,
|
|
60
|
+
created_at: @created_at.iso8601,
|
|
61
|
+
updated_at: @updated_at.iso8601
|
|
62
|
+
}
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def to_json(*args)
|
|
66
|
+
to_h.to_json(*args)
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
@@ -3,7 +3,9 @@ module DecisionAgent
|
|
|
3
3
|
attr_reader :data
|
|
4
4
|
|
|
5
5
|
def initialize(data)
|
|
6
|
-
|
|
6
|
+
# Create a deep copy before freezing to avoid mutating the original
|
|
7
|
+
data_hash = data.is_a?(Hash) ? data : {}
|
|
8
|
+
@data = deep_freeze(deep_dup(data_hash))
|
|
7
9
|
end
|
|
8
10
|
|
|
9
11
|
def [](key)
|
|
@@ -28,15 +30,33 @@ module DecisionAgent
|
|
|
28
30
|
|
|
29
31
|
private
|
|
30
32
|
|
|
31
|
-
def
|
|
33
|
+
def deep_dup(obj)
|
|
32
34
|
case obj
|
|
33
35
|
when Hash
|
|
34
|
-
obj.transform_values { |v|
|
|
36
|
+
obj.transform_values { |v| deep_dup(v) }
|
|
35
37
|
when Array
|
|
36
|
-
obj.map { |v|
|
|
38
|
+
obj.map { |v| deep_dup(v) }
|
|
37
39
|
else
|
|
40
|
+
obj
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def deep_freeze(obj)
|
|
45
|
+
return obj if obj.frozen?
|
|
46
|
+
|
|
47
|
+
case obj
|
|
48
|
+
when Hash
|
|
49
|
+
obj.each_value { |v| deep_freeze(v) }
|
|
50
|
+
obj.freeze
|
|
51
|
+
when Array
|
|
52
|
+
obj.each { |v| deep_freeze(v) }
|
|
53
|
+
obj.freeze
|
|
54
|
+
when String, Symbol, Numeric, TrueClass, FalseClass, NilClass
|
|
38
55
|
obj.freeze
|
|
56
|
+
else
|
|
57
|
+
obj.freeze if obj.respond_to?(:freeze)
|
|
39
58
|
end
|
|
59
|
+
obj
|
|
40
60
|
end
|
|
41
61
|
end
|
|
42
62
|
end
|
|
@@ -40,14 +40,21 @@ module DecisionAgent
|
|
|
40
40
|
end
|
|
41
41
|
|
|
42
42
|
def deep_freeze(obj)
|
|
43
|
+
return obj if obj.frozen?
|
|
44
|
+
|
|
43
45
|
case obj
|
|
44
46
|
when Hash
|
|
45
|
-
obj.
|
|
47
|
+
obj.each_value { |v| deep_freeze(v) }
|
|
48
|
+
obj.freeze
|
|
46
49
|
when Array
|
|
47
|
-
obj.
|
|
48
|
-
else
|
|
50
|
+
obj.each { |v| deep_freeze(v) }
|
|
49
51
|
obj.freeze
|
|
52
|
+
when String, Symbol, Numeric, TrueClass, FalseClass, NilClass
|
|
53
|
+
obj.freeze
|
|
54
|
+
else
|
|
55
|
+
obj.freeze if obj.respond_to?(:freeze)
|
|
50
56
|
end
|
|
57
|
+
obj
|
|
51
58
|
end
|
|
52
59
|
end
|
|
53
60
|
end
|