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,42 @@
|
|
|
1
|
+
module DecisionAgent
|
|
2
|
+
module Testing
|
|
3
|
+
# Represents a single test scenario with context and expected results
|
|
4
|
+
class TestScenario
|
|
5
|
+
attr_reader :id, :context, :expected_decision, :expected_confidence, :metadata
|
|
6
|
+
|
|
7
|
+
def initialize(id:, context:, expected_decision: nil, expected_confidence: nil, metadata: {})
|
|
8
|
+
@id = id.to_s.freeze
|
|
9
|
+
@context = context.is_a?(Hash) ? context.freeze : context
|
|
10
|
+
@expected_decision = expected_decision&.to_s&.freeze
|
|
11
|
+
@expected_confidence = expected_confidence&.to_f if expected_confidence
|
|
12
|
+
@metadata = metadata.is_a?(Hash) ? metadata.freeze : metadata
|
|
13
|
+
|
|
14
|
+
freeze
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def to_h
|
|
18
|
+
{
|
|
19
|
+
id: @id,
|
|
20
|
+
context: @context,
|
|
21
|
+
expected_decision: @expected_decision,
|
|
22
|
+
expected_confidence: @expected_confidence,
|
|
23
|
+
metadata: @metadata
|
|
24
|
+
}
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def expected_result?
|
|
28
|
+
!@expected_decision.nil?
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def ==(other)
|
|
32
|
+
other.is_a?(TestScenario) &&
|
|
33
|
+
@id == other.id &&
|
|
34
|
+
@context == other.context &&
|
|
35
|
+
@expected_decision == other.expected_decision &&
|
|
36
|
+
(@expected_confidence.nil? || other.expected_confidence.nil? ||
|
|
37
|
+
(@expected_confidence - other.expected_confidence).abs < 0.0001) &&
|
|
38
|
+
@metadata == other.metadata
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
@@ -1,3 +1,12 @@
|
|
|
1
1
|
module DecisionAgent
|
|
2
|
-
|
|
2
|
+
# Semantic version: MAJOR.MINOR.PATCH
|
|
3
|
+
# MAJOR: Incremented for incompatible API changes
|
|
4
|
+
# MINOR: Incremented for backward-compatible functionality additions
|
|
5
|
+
# PATCH: Incremented for backward-compatible bug fixes
|
|
6
|
+
VERSION = "0.1.6".freeze
|
|
7
|
+
|
|
8
|
+
# Validate version format (semantic versioning)
|
|
9
|
+
unless VERSION.match?(/\A\d+\.\d+\.\d+(-[a-zA-Z0-9-]+(\.[a-zA-Z0-9-]+)*)?(\+[a-zA-Z0-9-]+(\.[a-zA-Z0-9-]+)*)?\z/)
|
|
10
|
+
raise ArgumentError, "Invalid version format: #{VERSION}. Must follow semantic versioning (MAJOR.MINOR.PATCH)"
|
|
11
|
+
end
|
|
3
12
|
end
|
|
@@ -88,14 +88,24 @@ module DecisionAgent
|
|
|
88
88
|
|
|
89
89
|
def get_version(version_id:)
|
|
90
90
|
# Use index to find rule_id quickly - O(1) instead of O(n)
|
|
91
|
-
|
|
91
|
+
begin
|
|
92
|
+
rule_id = get_rule_id_from_index(version_id)
|
|
93
|
+
rescue StandardError
|
|
94
|
+
# If index lookup fails, version doesn't exist
|
|
95
|
+
return nil
|
|
96
|
+
end
|
|
92
97
|
return nil unless rule_id
|
|
93
98
|
|
|
94
99
|
# Now lock on the specific rule
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
100
|
+
begin
|
|
101
|
+
with_rule_lock(rule_id) do
|
|
102
|
+
# Read only this rule's versions
|
|
103
|
+
versions = list_versions_unsafe(rule_id: rule_id)
|
|
104
|
+
versions.find { |v| v[:id] == version_id }
|
|
105
|
+
end
|
|
106
|
+
rescue StandardError
|
|
107
|
+
# If any error occurs during lookup, treat as version not found
|
|
108
|
+
nil
|
|
99
109
|
end
|
|
100
110
|
end
|
|
101
111
|
|
|
@@ -140,34 +150,85 @@ module DecisionAgent
|
|
|
140
150
|
|
|
141
151
|
def delete_version(version_id:)
|
|
142
152
|
# Use index to find rule_id quickly - O(1) instead of O(n)
|
|
143
|
-
|
|
144
|
-
|
|
153
|
+
begin
|
|
154
|
+
rule_id = get_rule_id_from_index(version_id)
|
|
155
|
+
rescue StandardError
|
|
156
|
+
# If index lookup fails, version doesn't exist
|
|
157
|
+
raise DecisionAgent::NotFoundError, "Version not found: #{version_id}"
|
|
158
|
+
end
|
|
145
159
|
|
|
146
|
-
#
|
|
147
|
-
|
|
148
|
-
# Read only this rule's versions
|
|
149
|
-
versions = list_versions_unsafe(rule_id: rule_id)
|
|
150
|
-
version = versions.find { |v| v[:id] == version_id }
|
|
151
|
-
raise DecisionAgent::NotFoundError, "Version not found: #{version_id}" unless version
|
|
160
|
+
# Validate rule_id - must be present and non-empty
|
|
161
|
+
raise DecisionAgent::NotFoundError, "Version not found: #{version_id}" unless rule_id && !rule_id.to_s.strip.empty?
|
|
152
162
|
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
163
|
+
# Now lock on the specific rule
|
|
164
|
+
begin
|
|
165
|
+
with_rule_lock(rule_id) do
|
|
166
|
+
# Read only this rule's versions
|
|
167
|
+
versions = list_versions_unsafe(rule_id: rule_id)
|
|
168
|
+
version = versions.find { |v| v[:id] == version_id || v[:id].to_s == version_id.to_s }
|
|
169
|
+
|
|
170
|
+
# If version not in list, check if file exists - might have been manually deleted
|
|
171
|
+
unless version
|
|
172
|
+
rule_dir = File.join(@storage_path, sanitize_filename(rule_id))
|
|
173
|
+
# Try to find the file by checking all version files
|
|
174
|
+
file_found = false
|
|
175
|
+
begin
|
|
176
|
+
Dir.glob(File.join(rule_dir, "*.json")).each do |filepath|
|
|
177
|
+
file_data = JSON.parse(File.read(filepath))
|
|
178
|
+
if file_data["id"] == version_id || file_data[:id] == version_id ||
|
|
179
|
+
file_data["id"].to_s == version_id.to_s || file_data[:id].to_s == version_id.to_s
|
|
180
|
+
# File exists but not in versions list - remove from index and return false
|
|
181
|
+
file_found = true
|
|
182
|
+
remove_from_index(version_id)
|
|
183
|
+
return false
|
|
184
|
+
end
|
|
185
|
+
rescue Errno::ENOENT, JSON::ParserError
|
|
186
|
+
# File was deleted or corrupted, continue searching
|
|
187
|
+
next
|
|
188
|
+
end
|
|
189
|
+
rescue Errno::ENOENT
|
|
190
|
+
# Directory doesn't exist, version not found
|
|
191
|
+
end
|
|
192
|
+
# Version not found in list and file doesn't exist - clean up index and return false
|
|
193
|
+
remove_from_index(version_id)
|
|
194
|
+
return false
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
# Prevent deletion of active versions
|
|
198
|
+
if version[:status] == "active"
|
|
199
|
+
raise DecisionAgent::ValidationError, "Cannot delete active version. Please activate another version first."
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
# Delete the file
|
|
203
|
+
rule_dir = File.join(@storage_path, sanitize_filename(rule_id))
|
|
204
|
+
filename = "#{version[:version_number]}.json"
|
|
205
|
+
filepath = File.join(rule_dir, filename)
|
|
206
|
+
|
|
207
|
+
if File.exist?(filepath)
|
|
208
|
+
File.delete(filepath)
|
|
209
|
+
# Remove from index
|
|
210
|
+
remove_from_index(version_id)
|
|
211
|
+
true
|
|
212
|
+
else
|
|
213
|
+
# File already deleted - clean up index and return false
|
|
214
|
+
remove_from_index(version_id)
|
|
215
|
+
false
|
|
216
|
+
end
|
|
156
217
|
end
|
|
157
|
-
|
|
158
|
-
#
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
218
|
+
rescue DecisionAgent::ValidationError, DecisionAgent::NotFoundError
|
|
219
|
+
# Re-raise expected errors
|
|
220
|
+
raise
|
|
221
|
+
rescue StandardError
|
|
222
|
+
# If any unexpected error occurs during the lock operation, treat as version not found
|
|
223
|
+
# This prevents 500 errors from propagating when version doesn't exist or is in an invalid state
|
|
224
|
+
# This handles ThreadError (deadlocks, recursive locks), SystemCallError (file system issues), etc.
|
|
225
|
+
# This is safe because if the version existed and was valid, we would have found it above
|
|
226
|
+
begin
|
|
166
227
|
remove_from_index(version_id)
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
false
|
|
228
|
+
rescue StandardError
|
|
229
|
+
nil
|
|
170
230
|
end
|
|
231
|
+
raise DecisionAgent::NotFoundError, "Version not found: #{version_id}"
|
|
171
232
|
end
|
|
172
233
|
end
|
|
173
234
|
|
|
@@ -181,6 +242,9 @@ module DecisionAgent
|
|
|
181
242
|
|
|
182
243
|
Dir.glob(File.join(rule_dir, "*.json")).each do |file|
|
|
183
244
|
versions << JSON.parse(File.read(file), symbolize_names: true)
|
|
245
|
+
rescue JSON::ParserError, Errno::ENOENT
|
|
246
|
+
# Skip corrupted or deleted files
|
|
247
|
+
next
|
|
184
248
|
end
|
|
185
249
|
|
|
186
250
|
versions.sort_by! { |v| -v[:version_number] }
|
|
@@ -249,6 +313,10 @@ module DecisionAgent
|
|
|
249
313
|
def with_rule_lock(rule_id, &block)
|
|
250
314
|
mutex = @rule_mutexes_lock.synchronize { @rule_mutexes[rule_id] }
|
|
251
315
|
mutex.synchronize(&block)
|
|
316
|
+
rescue ThreadError => e
|
|
317
|
+
# Handle potential deadlock or recursive locking issues in Ruby 3.3+
|
|
318
|
+
# Re-raise as StandardError so it can be caught by callers
|
|
319
|
+
raise StandardError, "Lock error: #{e.message}"
|
|
252
320
|
end
|
|
253
321
|
|
|
254
322
|
# Index management methods for O(1) version_id -> rule_id lookups
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
require "rack"
|
|
2
|
+
|
|
3
|
+
module DecisionAgent
|
|
4
|
+
module Web
|
|
5
|
+
module Middleware
|
|
6
|
+
class AuthMiddleware
|
|
7
|
+
def initialize(app, authenticator:, access_audit_logger: nil)
|
|
8
|
+
@app = app
|
|
9
|
+
@authenticator = authenticator
|
|
10
|
+
@access_audit_logger = access_audit_logger
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def call(env)
|
|
14
|
+
request = Rack::Request.new(env)
|
|
15
|
+
token = extract_token(request)
|
|
16
|
+
|
|
17
|
+
if token
|
|
18
|
+
auth_result = @authenticator.authenticate(token)
|
|
19
|
+
if auth_result
|
|
20
|
+
env["decision_agent.user"] = auth_result[:user]
|
|
21
|
+
env["decision_agent.session"] = auth_result[:session]
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
@app.call(env)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
private
|
|
29
|
+
|
|
30
|
+
def extract_token(request)
|
|
31
|
+
# Check Authorization header: Bearer <token>
|
|
32
|
+
auth_header = request.get_header("HTTP_AUTHORIZATION")
|
|
33
|
+
return auth_header[7..] if auth_header&.start_with?("Bearer ")
|
|
34
|
+
|
|
35
|
+
# Check session cookie
|
|
36
|
+
cookie_token = request.cookies["decision_agent_session"]
|
|
37
|
+
return cookie_token if cookie_token
|
|
38
|
+
|
|
39
|
+
# Check query parameter (less secure, but useful for some cases)
|
|
40
|
+
request.params["token"]
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
require "rack"
|
|
2
|
+
require "json"
|
|
3
|
+
|
|
4
|
+
module DecisionAgent
|
|
5
|
+
module Web
|
|
6
|
+
module Middleware
|
|
7
|
+
class PermissionMiddleware
|
|
8
|
+
def initialize(app, permission_checker:, required_permission: nil, access_audit_logger: nil)
|
|
9
|
+
@app = app
|
|
10
|
+
@permission_checker = permission_checker
|
|
11
|
+
@required_permission = required_permission
|
|
12
|
+
@access_audit_logger = access_audit_logger
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def call(env)
|
|
16
|
+
user = env["decision_agent.user"]
|
|
17
|
+
|
|
18
|
+
return unauthorized_response("Authentication required") unless user
|
|
19
|
+
|
|
20
|
+
# Check if user is active using the adapter
|
|
21
|
+
return forbidden_response("User account is not active") unless @permission_checker.active?(user)
|
|
22
|
+
|
|
23
|
+
if @required_permission
|
|
24
|
+
resource_type = extract_resource_type(env)
|
|
25
|
+
resource_id = extract_resource_id(env)
|
|
26
|
+
|
|
27
|
+
granted = @permission_checker.can?(user, @required_permission, nil)
|
|
28
|
+
|
|
29
|
+
if @access_audit_logger
|
|
30
|
+
user_id = @permission_checker.user_id(user)
|
|
31
|
+
@access_audit_logger.log_permission_check(
|
|
32
|
+
user_id: user_id,
|
|
33
|
+
permission: @required_permission,
|
|
34
|
+
resource_type: resource_type,
|
|
35
|
+
resource_id: resource_id,
|
|
36
|
+
granted: granted
|
|
37
|
+
)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
return forbidden_response("Permission denied: #{@required_permission}") unless granted
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
@app.call(env)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
private
|
|
47
|
+
|
|
48
|
+
def extract_resource_type(env)
|
|
49
|
+
request = Rack::Request.new(env)
|
|
50
|
+
# Try to extract from path, e.g., /api/rules/:id -> "rule"
|
|
51
|
+
path = request.path
|
|
52
|
+
if (match = path.match(%r{/api/(\w+)}))
|
|
53
|
+
# Simple singularize: remove trailing 's'
|
|
54
|
+
word = match[1]
|
|
55
|
+
word&.end_with?("s") ? word[0..-2] : word
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def extract_resource_id(env)
|
|
60
|
+
request = Rack::Request.new(env)
|
|
61
|
+
# Try params first (for query parameters and Sinatra path params)
|
|
62
|
+
resource_id = request.params["id"] || request.params["rule_id"] || request.params["version_id"]
|
|
63
|
+
|
|
64
|
+
# If not in params, try to extract from path (e.g., /api/rules/123 -> 123)
|
|
65
|
+
unless resource_id
|
|
66
|
+
path = request.path
|
|
67
|
+
# Match patterns like /api/rules/123 or /api/versions/v1
|
|
68
|
+
if (match = path.match(%r{/api/(?:rules|versions)/([^/]+)}))
|
|
69
|
+
resource_id = match[1]
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
resource_id
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def unauthorized_response(message)
|
|
77
|
+
[
|
|
78
|
+
401,
|
|
79
|
+
{ "Content-Type" => "application/json" },
|
|
80
|
+
[{ error: message }.to_json]
|
|
81
|
+
]
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def forbidden_response(message)
|
|
85
|
+
[
|
|
86
|
+
403,
|
|
87
|
+
{ "Content-Type" => "application/json" },
|
|
88
|
+
[{ error: message }.to_json]
|
|
89
|
+
]
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|