decision_agent 0.1.4 → 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 (85) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +84 -233
  3. data/lib/decision_agent/ab_testing/ab_testing_agent.rb +46 -10
  4. data/lib/decision_agent/agent.rb +5 -3
  5. data/lib/decision_agent/auth/access_audit_logger.rb +122 -0
  6. data/lib/decision_agent/auth/authenticator.rb +127 -0
  7. data/lib/decision_agent/auth/password_reset_manager.rb +57 -0
  8. data/lib/decision_agent/auth/password_reset_token.rb +33 -0
  9. data/lib/decision_agent/auth/permission.rb +29 -0
  10. data/lib/decision_agent/auth/permission_checker.rb +43 -0
  11. data/lib/decision_agent/auth/rbac_adapter.rb +278 -0
  12. data/lib/decision_agent/auth/rbac_config.rb +51 -0
  13. data/lib/decision_agent/auth/role.rb +56 -0
  14. data/lib/decision_agent/auth/session.rb +33 -0
  15. data/lib/decision_agent/auth/session_manager.rb +57 -0
  16. data/lib/decision_agent/auth/user.rb +70 -0
  17. data/lib/decision_agent/context.rb +24 -4
  18. data/lib/decision_agent/decision.rb +10 -3
  19. data/lib/decision_agent/dsl/condition_evaluator.rb +378 -1
  20. data/lib/decision_agent/dsl/schema_validator.rb +8 -1
  21. data/lib/decision_agent/errors.rb +38 -0
  22. data/lib/decision_agent/evaluation.rb +10 -3
  23. data/lib/decision_agent/evaluation_validator.rb +8 -13
  24. data/lib/decision_agent/monitoring/dashboard_server.rb +1 -0
  25. data/lib/decision_agent/monitoring/metrics_collector.rb +17 -5
  26. data/lib/decision_agent/testing/batch_test_importer.rb +373 -0
  27. data/lib/decision_agent/testing/batch_test_runner.rb +244 -0
  28. data/lib/decision_agent/testing/test_coverage_analyzer.rb +191 -0
  29. data/lib/decision_agent/testing/test_result_comparator.rb +235 -0
  30. data/lib/decision_agent/testing/test_scenario.rb +42 -0
  31. data/lib/decision_agent/version.rb +10 -1
  32. data/lib/decision_agent/versioning/activerecord_adapter.rb +1 -1
  33. data/lib/decision_agent/versioning/file_storage_adapter.rb +96 -28
  34. data/lib/decision_agent/web/middleware/auth_middleware.rb +45 -0
  35. data/lib/decision_agent/web/middleware/permission_middleware.rb +94 -0
  36. data/lib/decision_agent/web/public/app.js +184 -29
  37. data/lib/decision_agent/web/public/batch_testing.html +640 -0
  38. data/lib/decision_agent/web/public/index.html +37 -9
  39. data/lib/decision_agent/web/public/login.html +298 -0
  40. data/lib/decision_agent/web/public/users.html +679 -0
  41. data/lib/decision_agent/web/server.rb +873 -7
  42. data/lib/decision_agent.rb +52 -0
  43. data/lib/generators/decision_agent/install/templates/rule_version.rb +1 -1
  44. data/spec/ab_testing/ab_test_assignment_spec.rb +253 -0
  45. data/spec/ab_testing/ab_test_manager_spec.rb +282 -0
  46. data/spec/ab_testing/ab_testing_agent_spec.rb +481 -0
  47. data/spec/ab_testing/storage/adapter_spec.rb +64 -0
  48. data/spec/ab_testing/storage/memory_adapter_spec.rb +485 -0
  49. data/spec/advanced_operators_spec.rb +1003 -0
  50. data/spec/agent_spec.rb +40 -0
  51. data/spec/audit_adapters_spec.rb +18 -0
  52. data/spec/auth/access_audit_logger_spec.rb +394 -0
  53. data/spec/auth/authenticator_spec.rb +112 -0
  54. data/spec/auth/password_reset_spec.rb +294 -0
  55. data/spec/auth/permission_checker_spec.rb +207 -0
  56. data/spec/auth/permission_spec.rb +73 -0
  57. data/spec/auth/rbac_adapter_spec.rb +550 -0
  58. data/spec/auth/rbac_config_spec.rb +82 -0
  59. data/spec/auth/role_spec.rb +51 -0
  60. data/spec/auth/session_manager_spec.rb +172 -0
  61. data/spec/auth/session_spec.rb +112 -0
  62. data/spec/auth/user_spec.rb +130 -0
  63. data/spec/context_spec.rb +43 -0
  64. data/spec/decision_agent_spec.rb +96 -0
  65. data/spec/decision_spec.rb +423 -0
  66. data/spec/dsl/condition_evaluator_spec.rb +774 -0
  67. data/spec/evaluation_spec.rb +364 -0
  68. data/spec/evaluation_validator_spec.rb +165 -0
  69. data/spec/examples.txt +1542 -612
  70. data/spec/monitoring/metrics_collector_spec.rb +220 -2
  71. data/spec/monitoring/storage/activerecord_adapter_spec.rb +153 -1
  72. data/spec/monitoring/storage/base_adapter_spec.rb +61 -0
  73. data/spec/performance_optimizations_spec.rb +486 -0
  74. data/spec/spec_helper.rb +23 -0
  75. data/spec/testing/batch_test_importer_spec.rb +693 -0
  76. data/spec/testing/batch_test_runner_spec.rb +307 -0
  77. data/spec/testing/test_coverage_analyzer_spec.rb +292 -0
  78. data/spec/testing/test_result_comparator_spec.rb +392 -0
  79. data/spec/testing/test_scenario_spec.rb +113 -0
  80. data/spec/versioning/adapter_spec.rb +156 -0
  81. data/spec/versioning_spec.rb +253 -0
  82. data/spec/web/middleware/auth_middleware_spec.rb +133 -0
  83. data/spec/web/middleware/permission_middleware_spec.rb +247 -0
  84. data/spec/web_ui_rack_spec.rb +1705 -0
  85. metadata +99 -6
@@ -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
@@ -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