decision_agent 0.3.0 → 1.1.0

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 (220) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +234 -14
  3. data/lib/decision_agent/ab_testing/ab_test.rb +5 -1
  4. data/lib/decision_agent/ab_testing/ab_test_assignment.rb +2 -0
  5. data/lib/decision_agent/ab_testing/ab_test_manager.rb +2 -0
  6. data/lib/decision_agent/ab_testing/ab_testing_agent.rb +2 -0
  7. data/lib/decision_agent/ab_testing/storage/activerecord_adapter.rb +2 -13
  8. data/lib/decision_agent/ab_testing/storage/adapter.rb +2 -0
  9. data/lib/decision_agent/ab_testing/storage/memory_adapter.rb +2 -0
  10. data/lib/decision_agent/agent.rb +78 -9
  11. data/lib/decision_agent/audit/adapter.rb +2 -0
  12. data/lib/decision_agent/audit/logger_adapter.rb +2 -0
  13. data/lib/decision_agent/audit/null_adapter.rb +2 -0
  14. data/lib/decision_agent/auth/access_audit_logger.rb +2 -0
  15. data/lib/decision_agent/auth/authenticator.rb +2 -0
  16. data/lib/decision_agent/auth/password_reset_manager.rb +2 -0
  17. data/lib/decision_agent/auth/password_reset_token.rb +2 -0
  18. data/lib/decision_agent/auth/permission.rb +2 -0
  19. data/lib/decision_agent/auth/permission_checker.rb +2 -0
  20. data/lib/decision_agent/auth/rbac_adapter.rb +2 -0
  21. data/lib/decision_agent/auth/rbac_config.rb +2 -0
  22. data/lib/decision_agent/auth/role.rb +2 -0
  23. data/lib/decision_agent/auth/session.rb +2 -0
  24. data/lib/decision_agent/auth/session_manager.rb +2 -0
  25. data/lib/decision_agent/auth/user.rb +2 -0
  26. data/lib/decision_agent/context.rb +14 -0
  27. data/lib/decision_agent/decision.rb +113 -4
  28. data/lib/decision_agent/dmn/adapter.rb +2 -0
  29. data/lib/decision_agent/dmn/cache.rb +2 -2
  30. data/lib/decision_agent/dmn/decision_graph.rb +7 -7
  31. data/lib/decision_agent/dmn/decision_tree.rb +16 -8
  32. data/lib/decision_agent/dmn/errors.rb +2 -0
  33. data/lib/decision_agent/dmn/exporter.rb +2 -0
  34. data/lib/decision_agent/dmn/feel/evaluator.rb +130 -114
  35. data/lib/decision_agent/dmn/feel/functions.rb +2 -0
  36. data/lib/decision_agent/dmn/feel/parser.rb +2 -0
  37. data/lib/decision_agent/dmn/feel/simple_parser.rb +98 -77
  38. data/lib/decision_agent/dmn/feel/transformer.rb +56 -102
  39. data/lib/decision_agent/dmn/feel/types.rb +2 -0
  40. data/lib/decision_agent/dmn/importer.rb +2 -0
  41. data/lib/decision_agent/dmn/model.rb +2 -4
  42. data/lib/decision_agent/dmn/parser.rb +2 -0
  43. data/lib/decision_agent/dmn/testing.rb +3 -2
  44. data/lib/decision_agent/dmn/validator.rb +5 -3
  45. data/lib/decision_agent/dmn/visualizer.rb +7 -6
  46. data/lib/decision_agent/dsl/condition_evaluator.rb +242 -1375
  47. data/lib/decision_agent/dsl/helpers/cache_helpers.rb +82 -0
  48. data/lib/decision_agent/dsl/helpers/comparison_helpers.rb +98 -0
  49. data/lib/decision_agent/dsl/helpers/date_helpers.rb +91 -0
  50. data/lib/decision_agent/dsl/helpers/geospatial_helpers.rb +85 -0
  51. data/lib/decision_agent/dsl/helpers/operator_evaluation_helpers.rb +160 -0
  52. data/lib/decision_agent/dsl/helpers/parameter_parsing_helpers.rb +206 -0
  53. data/lib/decision_agent/dsl/helpers/template_helpers.rb +39 -0
  54. data/lib/decision_agent/dsl/helpers/utility_helpers.rb +45 -0
  55. data/lib/decision_agent/dsl/operators/base.rb +70 -0
  56. data/lib/decision_agent/dsl/operators/basic_comparison_operators.rb +80 -0
  57. data/lib/decision_agent/dsl/operators/collection_operators.rb +60 -0
  58. data/lib/decision_agent/dsl/operators/date_arithmetic_operators.rb +206 -0
  59. data/lib/decision_agent/dsl/operators/date_time_operators.rb +47 -0
  60. data/lib/decision_agent/dsl/operators/duration_operators.rb +149 -0
  61. data/lib/decision_agent/dsl/operators/financial_operators.rb +237 -0
  62. data/lib/decision_agent/dsl/operators/geospatial_operators.rb +106 -0
  63. data/lib/decision_agent/dsl/operators/mathematical_operators.rb +234 -0
  64. data/lib/decision_agent/dsl/operators/moving_window_operators.rb +135 -0
  65. data/lib/decision_agent/dsl/operators/numeric_operators.rb +120 -0
  66. data/lib/decision_agent/dsl/operators/rate_operators.rb +65 -0
  67. data/lib/decision_agent/dsl/operators/statistical_aggregations.rb +187 -0
  68. data/lib/decision_agent/dsl/operators/string_aggregations.rb +84 -0
  69. data/lib/decision_agent/dsl/operators/string_operators.rb +72 -0
  70. data/lib/decision_agent/dsl/operators/time_component_operators.rb +72 -0
  71. data/lib/decision_agent/dsl/rule_parser.rb +2 -0
  72. data/lib/decision_agent/dsl/schema_validator.rb +37 -14
  73. data/lib/decision_agent/errors.rb +2 -0
  74. data/lib/decision_agent/evaluation.rb +14 -2
  75. data/lib/decision_agent/evaluators/base.rb +2 -0
  76. data/lib/decision_agent/evaluators/dmn_evaluator.rb +108 -19
  77. data/lib/decision_agent/evaluators/json_rule_evaluator.rb +56 -11
  78. data/lib/decision_agent/evaluators/static_evaluator.rb +2 -0
  79. data/lib/decision_agent/explainability/condition_trace.rb +85 -0
  80. data/lib/decision_agent/explainability/explainability_result.rb +50 -0
  81. data/lib/decision_agent/explainability/rule_trace.rb +41 -0
  82. data/lib/decision_agent/explainability/trace_collector.rb +26 -0
  83. data/lib/decision_agent/monitoring/alert_manager.rb +7 -16
  84. data/lib/decision_agent/monitoring/dashboard_server.rb +383 -250
  85. data/lib/decision_agent/monitoring/metrics_collector.rb +2 -0
  86. data/lib/decision_agent/monitoring/monitored_agent.rb +2 -0
  87. data/lib/decision_agent/monitoring/prometheus_exporter.rb +3 -1
  88. data/lib/decision_agent/replay/replay.rb +4 -1
  89. data/lib/decision_agent/scoring/base.rb +2 -0
  90. data/lib/decision_agent/scoring/consensus.rb +2 -0
  91. data/lib/decision_agent/scoring/max_weight.rb +2 -0
  92. data/lib/decision_agent/scoring/threshold.rb +2 -0
  93. data/lib/decision_agent/scoring/weighted_average.rb +2 -0
  94. data/lib/decision_agent/simulation/errors.rb +20 -0
  95. data/lib/decision_agent/simulation/impact_analyzer.rb +500 -0
  96. data/lib/decision_agent/simulation/monte_carlo_simulator.rb +638 -0
  97. data/lib/decision_agent/simulation/replay_engine.rb +488 -0
  98. data/lib/decision_agent/simulation/scenario_engine.rb +320 -0
  99. data/lib/decision_agent/simulation/scenario_library.rb +165 -0
  100. data/lib/decision_agent/simulation/shadow_test_engine.rb +274 -0
  101. data/lib/decision_agent/simulation/what_if_analyzer.rb +1008 -0
  102. data/lib/decision_agent/simulation.rb +19 -0
  103. data/lib/decision_agent/testing/batch_test_importer.rb +6 -2
  104. data/lib/decision_agent/testing/batch_test_runner.rb +5 -2
  105. data/lib/decision_agent/testing/test_coverage_analyzer.rb +2 -0
  106. data/lib/decision_agent/testing/test_result_comparator.rb +2 -0
  107. data/lib/decision_agent/testing/test_scenario.rb +2 -0
  108. data/lib/decision_agent/version.rb +3 -1
  109. data/lib/decision_agent/versioning/activerecord_adapter.rb +108 -43
  110. data/lib/decision_agent/versioning/adapter.rb +9 -0
  111. data/lib/decision_agent/versioning/file_storage_adapter.rb +19 -6
  112. data/lib/decision_agent/versioning/version_manager.rb +9 -0
  113. data/lib/decision_agent/web/dmn_editor/serialization.rb +74 -0
  114. data/lib/decision_agent/web/dmn_editor/xml_builder.rb +107 -0
  115. data/lib/decision_agent/web/dmn_editor.rb +8 -67
  116. data/lib/decision_agent/web/middleware/auth_middleware.rb +2 -0
  117. data/lib/decision_agent/web/middleware/permission_middleware.rb +3 -1
  118. data/lib/decision_agent/web/public/app.js +186 -26
  119. data/lib/decision_agent/web/public/batch_testing.html +80 -6
  120. data/lib/decision_agent/web/public/dmn-editor.html +2 -2
  121. data/lib/decision_agent/web/public/dmn-editor.js +74 -8
  122. data/lib/decision_agent/web/public/index.html +69 -3
  123. data/lib/decision_agent/web/public/login.html +1 -1
  124. data/lib/decision_agent/web/public/sample_batch.csv +11 -0
  125. data/lib/decision_agent/web/public/sample_impact.csv +11 -0
  126. data/lib/decision_agent/web/public/sample_replay.csv +11 -0
  127. data/lib/decision_agent/web/public/sample_rules.json +118 -0
  128. data/lib/decision_agent/web/public/sample_shadow.csv +11 -0
  129. data/lib/decision_agent/web/public/sample_whatif.csv +11 -0
  130. data/lib/decision_agent/web/public/simulation.html +146 -0
  131. data/lib/decision_agent/web/public/simulation_impact.html +495 -0
  132. data/lib/decision_agent/web/public/simulation_replay.html +547 -0
  133. data/lib/decision_agent/web/public/simulation_shadow.html +561 -0
  134. data/lib/decision_agent/web/public/simulation_whatif.html +549 -0
  135. data/lib/decision_agent/web/public/styles.css +65 -0
  136. data/lib/decision_agent/web/public/users.html +1 -1
  137. data/lib/decision_agent/web/rack_helpers.rb +106 -0
  138. data/lib/decision_agent/web/rack_request_helpers.rb +196 -0
  139. data/lib/decision_agent/web/server.rb +2126 -1374
  140. data/lib/decision_agent.rb +19 -1
  141. data/lib/generators/decision_agent/install/install_generator.rb +2 -0
  142. data/lib/generators/decision_agent/install/templates/ab_test_assignment_model.rb +2 -0
  143. data/lib/generators/decision_agent/install/templates/ab_test_model.rb +2 -0
  144. data/lib/generators/decision_agent/install/templates/ab_testing_migration.rb +2 -0
  145. data/lib/generators/decision_agent/install/templates/migration.rb +2 -0
  146. data/lib/generators/decision_agent/install/templates/rule.rb +2 -0
  147. data/lib/generators/decision_agent/install/templates/rule_version.rb +2 -0
  148. metadata +103 -89
  149. data/spec/ab_testing/ab_test_assignment_spec.rb +0 -253
  150. data/spec/ab_testing/ab_test_manager_spec.rb +0 -612
  151. data/spec/ab_testing/ab_test_spec.rb +0 -270
  152. data/spec/ab_testing/ab_testing_agent_spec.rb +0 -655
  153. data/spec/ab_testing/storage/adapter_spec.rb +0 -64
  154. data/spec/ab_testing/storage/memory_adapter_spec.rb +0 -485
  155. data/spec/activerecord_thread_safety_spec.rb +0 -553
  156. data/spec/advanced_operators_spec.rb +0 -3150
  157. data/spec/agent_spec.rb +0 -289
  158. data/spec/api_contract_spec.rb +0 -430
  159. data/spec/audit_adapters_spec.rb +0 -92
  160. data/spec/auth/access_audit_logger_spec.rb +0 -394
  161. data/spec/auth/authenticator_spec.rb +0 -112
  162. data/spec/auth/password_reset_spec.rb +0 -294
  163. data/spec/auth/permission_checker_spec.rb +0 -207
  164. data/spec/auth/permission_spec.rb +0 -73
  165. data/spec/auth/rbac_adapter_spec.rb +0 -778
  166. data/spec/auth/rbac_config_spec.rb +0 -82
  167. data/spec/auth/role_spec.rb +0 -51
  168. data/spec/auth/session_manager_spec.rb +0 -172
  169. data/spec/auth/session_spec.rb +0 -112
  170. data/spec/auth/user_spec.rb +0 -130
  171. data/spec/comprehensive_edge_cases_spec.rb +0 -1777
  172. data/spec/context_spec.rb +0 -127
  173. data/spec/decision_agent_spec.rb +0 -96
  174. data/spec/decision_spec.rb +0 -423
  175. data/spec/dmn/decision_graph_spec.rb +0 -282
  176. data/spec/dmn/decision_tree_spec.rb +0 -203
  177. data/spec/dmn/feel/errors_spec.rb +0 -18
  178. data/spec/dmn/feel/functions_spec.rb +0 -400
  179. data/spec/dmn/feel/simple_parser_spec.rb +0 -274
  180. data/spec/dmn/feel/types_spec.rb +0 -176
  181. data/spec/dmn/feel_parser_spec.rb +0 -489
  182. data/spec/dmn/hit_policy_spec.rb +0 -202
  183. data/spec/dmn/integration_spec.rb +0 -226
  184. data/spec/dsl/condition_evaluator_spec.rb +0 -774
  185. data/spec/dsl_validation_spec.rb +0 -648
  186. data/spec/edge_cases_spec.rb +0 -353
  187. data/spec/evaluation_spec.rb +0 -364
  188. data/spec/evaluation_validator_spec.rb +0 -165
  189. data/spec/examples/feedback_aware_evaluator_spec.rb +0 -460
  190. data/spec/examples.txt +0 -1909
  191. data/spec/fixtures/dmn/complex_decision.dmn +0 -81
  192. data/spec/fixtures/dmn/invalid_structure.dmn +0 -31
  193. data/spec/fixtures/dmn/simple_decision.dmn +0 -40
  194. data/spec/issue_verification_spec.rb +0 -759
  195. data/spec/json_rule_evaluator_spec.rb +0 -587
  196. data/spec/monitoring/alert_manager_spec.rb +0 -378
  197. data/spec/monitoring/metrics_collector_spec.rb +0 -501
  198. data/spec/monitoring/monitored_agent_spec.rb +0 -225
  199. data/spec/monitoring/prometheus_exporter_spec.rb +0 -242
  200. data/spec/monitoring/storage/activerecord_adapter_spec.rb +0 -498
  201. data/spec/monitoring/storage/base_adapter_spec.rb +0 -61
  202. data/spec/monitoring/storage/memory_adapter_spec.rb +0 -247
  203. data/spec/performance_optimizations_spec.rb +0 -493
  204. data/spec/replay_edge_cases_spec.rb +0 -699
  205. data/spec/replay_spec.rb +0 -210
  206. data/spec/rfc8785_canonicalization_spec.rb +0 -215
  207. data/spec/scoring_spec.rb +0 -225
  208. data/spec/spec_helper.rb +0 -60
  209. data/spec/testing/batch_test_importer_spec.rb +0 -693
  210. data/spec/testing/batch_test_runner_spec.rb +0 -307
  211. data/spec/testing/test_coverage_analyzer_spec.rb +0 -292
  212. data/spec/testing/test_result_comparator_spec.rb +0 -392
  213. data/spec/testing/test_scenario_spec.rb +0 -113
  214. data/spec/thread_safety_spec.rb +0 -490
  215. data/spec/thread_safety_spec.rb.broken +0 -878
  216. data/spec/versioning/adapter_spec.rb +0 -156
  217. data/spec/versioning_spec.rb +0 -1030
  218. data/spec/web/middleware/auth_middleware_spec.rb +0 -133
  219. data/spec/web/middleware/permission_middleware_spec.rb +0 -247
  220. data/spec/web_ui_rack_spec.rb +0 -2134
@@ -1,133 +0,0 @@
1
- require "spec_helper"
2
- require "rack/test"
3
- require_relative "../../../lib/decision_agent/web/middleware/auth_middleware"
4
-
5
- RSpec.describe DecisionAgent::Web::Middleware::AuthMiddleware do
6
- include Rack::Test::Methods
7
-
8
- let(:authenticator) { double("Authenticator") }
9
- let(:access_audit_logger) { double("AccessAuditLogger") }
10
- let(:app) { ->(_env) { [200, {}, ["OK"]] } }
11
- let(:middleware) { described_class.new(app, authenticator: authenticator, access_audit_logger: access_audit_logger) }
12
-
13
- describe "#initialize" do
14
- it "initializes with app and authenticator" do
15
- expect(middleware.instance_variable_get(:@app)).to eq(app)
16
- expect(middleware.instance_variable_get(:@authenticator)).to eq(authenticator)
17
- expect(middleware.instance_variable_get(:@access_audit_logger)).to eq(access_audit_logger)
18
- end
19
-
20
- it "initializes without access_audit_logger" do
21
- middleware = described_class.new(app, authenticator: authenticator)
22
- expect(middleware.instance_variable_get(:@access_audit_logger)).to be_nil
23
- end
24
- end
25
-
26
- describe "#call" do
27
- context "with Authorization header" do
28
- it "extracts token from Bearer header" do
29
- user = double("User", id: "user1")
30
- session = double("Session", token: "token123")
31
- auth_result = { user: user, session: session }
32
-
33
- allow(authenticator).to receive(:authenticate).with("token123").and_return(auth_result)
34
-
35
- env = Rack::MockRequest.env_for("/", "HTTP_AUTHORIZATION" => "Bearer token123")
36
- status, = middleware.call(env)
37
-
38
- expect(status).to eq(200)
39
- expect(env["decision_agent.user"]).to eq(user)
40
- expect(env["decision_agent.session"]).to eq(session)
41
- end
42
-
43
- it "handles missing Bearer prefix" do
44
- env = Rack::MockRequest.env_for("/", "HTTP_AUTHORIZATION" => "token123")
45
- status, = middleware.call(env)
46
-
47
- expect(status).to eq(200)
48
- expect(env["decision_agent.user"]).to be_nil
49
- end
50
- end
51
-
52
- context "with session cookie" do
53
- it "extracts token from cookie" do
54
- user = double("User", id: "user1")
55
- session = double("Session", token: "cookie_token")
56
- auth_result = { user: user, session: session }
57
-
58
- allow(authenticator).to receive(:authenticate).with("cookie_token").and_return(auth_result)
59
-
60
- env = Rack::MockRequest.env_for("/")
61
- request = Rack::Request.new(env)
62
- allow(request).to receive(:cookies).and_return("decision_agent_session" => "cookie_token")
63
- allow(Rack::Request).to receive(:new).and_return(request)
64
-
65
- status, = middleware.call(env)
66
-
67
- expect(status).to eq(200)
68
- expect(env["decision_agent.user"]).to eq(user)
69
- end
70
- end
71
-
72
- context "with query parameter" do
73
- it "extracts token from query parameter" do
74
- user = double("User", id: "user1")
75
- session = double("Session", token: "query_token")
76
- auth_result = { user: user, session: session }
77
-
78
- allow(authenticator).to receive(:authenticate).with("query_token").and_return(auth_result)
79
-
80
- env = Rack::MockRequest.env_for("/?token=query_token")
81
- status, = middleware.call(env)
82
-
83
- expect(status).to eq(200)
84
- expect(env["decision_agent.user"]).to eq(user)
85
- end
86
- end
87
-
88
- context "without token" do
89
- it "calls app without setting user" do
90
- env = Rack::MockRequest.env_for("/")
91
- status, = middleware.call(env)
92
-
93
- expect(status).to eq(200)
94
- expect(env["decision_agent.user"]).to be_nil
95
- expect(env["decision_agent.session"]).to be_nil
96
- end
97
- end
98
-
99
- context "with invalid token" do
100
- it "calls app without setting user when authentication fails" do
101
- allow(authenticator).to receive(:authenticate).with("invalid_token").and_return(nil)
102
-
103
- env = Rack::MockRequest.env_for("/", "HTTP_AUTHORIZATION" => "Bearer invalid_token")
104
- status, = middleware.call(env)
105
-
106
- expect(status).to eq(200)
107
- expect(env["decision_agent.user"]).to be_nil
108
- expect(env["decision_agent.session"]).to be_nil
109
- end
110
- end
111
-
112
- context "token priority" do
113
- it "prefers Authorization header over cookie" do
114
- user_header = double("User", id: "header_user")
115
- user_cookie = double("User", id: "cookie_user")
116
- session_header = double("Session")
117
- session_cookie = double("Session")
118
-
119
- allow(authenticator).to receive(:authenticate).with("header_token").and_return({ user: user_header, session: session_header })
120
- allow(authenticator).to receive(:authenticate).with("cookie_token").and_return({ user: user_cookie, session: session_cookie })
121
-
122
- env = Rack::MockRequest.env_for("/?token=cookie_token", "HTTP_AUTHORIZATION" => "Bearer header_token")
123
- request = Rack::Request.new(env)
124
- allow(request).to receive(:cookies).and_return("decision_agent_session" => "cookie_token")
125
- allow(Rack::Request).to receive(:new).and_return(request)
126
-
127
- middleware.call(env)
128
-
129
- expect(env["decision_agent.user"]).to eq(user_header)
130
- end
131
- end
132
- end
133
- end
@@ -1,247 +0,0 @@
1
- require "spec_helper"
2
- require "rack/test"
3
- require_relative "../../../lib/decision_agent/web/middleware/permission_middleware"
4
-
5
- RSpec.describe DecisionAgent::Web::Middleware::PermissionMiddleware do
6
- include Rack::Test::Methods
7
-
8
- let(:permission_checker) { double("PermissionChecker") }
9
- let(:access_audit_logger) { double("AccessAuditLogger") }
10
- let(:app) { ->(_env) { [200, {}, ["OK"]] } }
11
- let(:user) { double("User", id: "user1", email: "user@example.com") }
12
-
13
- describe "#initialize" do
14
- it "initializes with app and permission_checker" do
15
- middleware = described_class.new(app, permission_checker: permission_checker)
16
- expect(middleware.instance_variable_get(:@app)).to eq(app)
17
- expect(middleware.instance_variable_get(:@permission_checker)).to eq(permission_checker)
18
- expect(middleware.instance_variable_get(:@required_permission)).to be_nil
19
- end
20
-
21
- it "initializes with required_permission" do
22
- middleware = described_class.new(app, permission_checker: permission_checker, required_permission: :write)
23
- expect(middleware.instance_variable_get(:@required_permission)).to eq(:write)
24
- end
25
-
26
- it "initializes with access_audit_logger" do
27
- middleware = described_class.new(app, permission_checker: permission_checker, access_audit_logger: access_audit_logger)
28
- expect(middleware.instance_variable_get(:@access_audit_logger)).to eq(access_audit_logger)
29
- end
30
- end
31
-
32
- describe "#call" do
33
- context "without user" do
34
- it "returns 401 when user is not authenticated" do
35
- middleware = described_class.new(app, permission_checker: permission_checker)
36
- env = Rack::MockRequest.env_for("/")
37
-
38
- status, headers, body = middleware.call(env)
39
-
40
- expect(status).to eq(401)
41
- expect(headers["Content-Type"]).to include("application/json")
42
- body_text = body.first
43
- expect(JSON.parse(body_text)["error"]).to eq("Authentication required")
44
- end
45
- end
46
-
47
- context "with inactive user" do
48
- it "returns 403 when user is not active" do
49
- middleware = described_class.new(app, permission_checker: permission_checker)
50
- env = Rack::MockRequest.env_for("/")
51
- env["decision_agent.user"] = user
52
-
53
- allow(permission_checker).to receive(:active?).with(user).and_return(false)
54
-
55
- status, _, body = middleware.call(env)
56
-
57
- expect(status).to eq(403)
58
- body_text = body.first
59
- expect(JSON.parse(body_text)["error"]).to eq("User account is not active")
60
- end
61
- end
62
-
63
- context "without required permission" do
64
- it "calls app when no permission required" do
65
- middleware = described_class.new(app, permission_checker: permission_checker)
66
- env = Rack::MockRequest.env_for("/")
67
- env["decision_agent.user"] = user
68
-
69
- allow(permission_checker).to receive(:active?).with(user).and_return(true)
70
-
71
- status, _, body = middleware.call(env)
72
-
73
- expect(status).to eq(200)
74
- expect(body.first).to eq("OK")
75
- end
76
- end
77
-
78
- context "with required permission" do
79
- let(:middleware) { described_class.new(app, permission_checker: permission_checker, required_permission: :write, access_audit_logger: access_audit_logger) }
80
-
81
- before do
82
- allow(permission_checker).to receive(:active?).with(user).and_return(true)
83
- end
84
-
85
- it "calls app when permission is granted" do
86
- env = Rack::MockRequest.env_for("/api/rules/123")
87
- env["decision_agent.user"] = user
88
-
89
- allow(permission_checker).to receive(:can?).with(user, :write, nil).and_return(true)
90
- allow(permission_checker).to receive(:user_id).with(user).and_return("user1")
91
- allow(access_audit_logger).to receive(:log_permission_check)
92
-
93
- status, _, body = middleware.call(env)
94
-
95
- expect(status).to eq(200)
96
- expect(body.first).to eq("OK")
97
- end
98
-
99
- it "returns 403 when permission is denied" do
100
- env = Rack::MockRequest.env_for("/api/rules/123")
101
- env["decision_agent.user"] = user
102
-
103
- allow(permission_checker).to receive(:can?).with(user, :write, nil).and_return(false)
104
- allow(permission_checker).to receive(:user_id).with(user).and_return("user1")
105
- allow(access_audit_logger).to receive(:log_permission_check)
106
-
107
- status, _, body = middleware.call(env)
108
-
109
- expect(status).to eq(403)
110
- body_text = body.first
111
- expect(JSON.parse(body_text)["error"]).to eq("Permission denied: write")
112
- end
113
-
114
- it "logs permission check when access_audit_logger is provided" do
115
- env = Rack::MockRequest.env_for("/api/rules/123")
116
- env["decision_agent.user"] = user
117
-
118
- allow(permission_checker).to receive(:can?).with(user, :write, nil).and_return(true)
119
- allow(permission_checker).to receive(:user_id).with(user).and_return("user1")
120
- allow(access_audit_logger).to receive(:log_permission_check)
121
-
122
- middleware.call(env)
123
-
124
- expect(access_audit_logger).to have_received(:log_permission_check).with(
125
- user_id: "user1",
126
- permission: :write,
127
- resource_type: "rule",
128
- resource_id: "123",
129
- granted: true
130
- )
131
- end
132
-
133
- it "does not log when access_audit_logger is not provided" do
134
- middleware = described_class.new(app, permission_checker: permission_checker, required_permission: :write)
135
- env = Rack::MockRequest.env_for("/api/rules/123")
136
- env["decision_agent.user"] = user
137
-
138
- allow(permission_checker).to receive(:can?).with(user, :write, nil).and_return(true)
139
-
140
- expect { middleware.call(env) }.not_to raise_error
141
- end
142
- end
143
-
144
- describe "#extract_resource_type" do
145
- it "extracts resource type from path" do
146
- middleware = described_class.new(app, permission_checker: permission_checker)
147
- env = Rack::MockRequest.env_for("/api/rules/123")
148
- env["decision_agent.user"] = user
149
-
150
- allow(permission_checker).to receive(:active?).with(user).and_return(true)
151
-
152
- middleware.call(env)
153
-
154
- # Verify extraction happened (indirectly through logging)
155
- expect(permission_checker).to have_received(:active?).with(user)
156
- end
157
-
158
- it "handles paths without /api/ prefix" do
159
- middleware = described_class.new(app, permission_checker: permission_checker, required_permission: :read)
160
- env = Rack::MockRequest.env_for("/other/path")
161
- env["decision_agent.user"] = user
162
-
163
- allow(permission_checker).to receive(:active?).with(user).and_return(true)
164
- allow(permission_checker).to receive(:can?).with(user, :read, nil).and_return(true)
165
- allow(permission_checker).to receive(:user_id).with(user).and_return("user1")
166
-
167
- status, = middleware.call(env)
168
-
169
- expect(status).to eq(200)
170
- end
171
-
172
- it "singularizes resource type (removes trailing 's')" do
173
- middleware = described_class.new(app, permission_checker: permission_checker, required_permission: :read, access_audit_logger: access_audit_logger)
174
- env = Rack::MockRequest.env_for("/api/rules/123")
175
- env["decision_agent.user"] = user
176
-
177
- allow(permission_checker).to receive(:active?).with(user).and_return(true)
178
- allow(permission_checker).to receive(:can?).with(user, :read, nil).and_return(true)
179
- allow(permission_checker).to receive(:user_id).with(user).and_return("user1")
180
- allow(access_audit_logger).to receive(:log_permission_check)
181
-
182
- middleware.call(env)
183
-
184
- expect(access_audit_logger).to have_received(:log_permission_check).with(
185
- user_id: "user1",
186
- permission: :read,
187
- resource_type: "rule",
188
- resource_id: "123",
189
- granted: true
190
- )
191
- end
192
- end
193
-
194
- describe "#extract_resource_id" do
195
- it "extracts id from params" do
196
- middleware = described_class.new(app, permission_checker: permission_checker, required_permission: :read, access_audit_logger: access_audit_logger)
197
- env = Rack::MockRequest.env_for("/api/rules/123?id=456")
198
- env["decision_agent.user"] = user
199
-
200
- allow(permission_checker).to receive(:active?).with(user).and_return(true)
201
- allow(permission_checker).to receive(:can?).with(user, :read, nil).and_return(true)
202
- allow(permission_checker).to receive(:user_id).with(user).and_return("user1")
203
- allow(access_audit_logger).to receive(:log_permission_check)
204
-
205
- middleware.call(env)
206
-
207
- expect(access_audit_logger).to have_received(:log_permission_check) do |args|
208
- expect(args[:resource_id]).to eq("456")
209
- end
210
- end
211
-
212
- it "extracts rule_id from params" do
213
- middleware = described_class.new(app, permission_checker: permission_checker, required_permission: :read, access_audit_logger: access_audit_logger)
214
- env = Rack::MockRequest.env_for("/api/rules?rule_id=789")
215
- env["decision_agent.user"] = user
216
-
217
- allow(permission_checker).to receive(:active?).with(user).and_return(true)
218
- allow(permission_checker).to receive(:can?).with(user, :read, nil).and_return(true)
219
- allow(permission_checker).to receive(:user_id).with(user).and_return("user1")
220
- allow(access_audit_logger).to receive(:log_permission_check)
221
-
222
- middleware.call(env)
223
-
224
- expect(access_audit_logger).to have_received(:log_permission_check) do |args|
225
- expect(args[:resource_id]).to eq("789")
226
- end
227
- end
228
-
229
- it "extracts version_id from params" do
230
- middleware = described_class.new(app, permission_checker: permission_checker, required_permission: :read, access_audit_logger: access_audit_logger)
231
- env = Rack::MockRequest.env_for("/api/versions?version_id=999")
232
- env["decision_agent.user"] = user
233
-
234
- allow(permission_checker).to receive(:active?).with(user).and_return(true)
235
- allow(permission_checker).to receive(:can?).with(user, :read, nil).and_return(true)
236
- allow(permission_checker).to receive(:user_id).with(user).and_return("user1")
237
- allow(access_audit_logger).to receive(:log_permission_check)
238
-
239
- middleware.call(env)
240
-
241
- expect(access_audit_logger).to have_received(:log_permission_check) do |args|
242
- expect(args[:resource_id]).to eq("999")
243
- end
244
- end
245
- end
246
- end
247
- end