Skip to content

Commit 78664ae

Browse files
szhGitHub Enterprise
authored andcommitted
Merge pull request #857 from Conjur-Enterprise/422
CNJR-9138: Return 422 when policy references non-existent resource
2 parents 5f496bd + b603870 commit 78664ae

File tree

11 files changed

+75
-33
lines changed

11 files changed

+75
-33
lines changed

CHANGELOG.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
99
- Nothing should go in this section, please add to the latest unreleased version
1010
(and update the corresponding date), or add a new version.
1111

12-
## [1.22.0] - 2025-01-13
12+
## [1.22.0] - 2025-04-02
1313

1414
### Added
1515
- Added the dynamic secrets Issuers API and data model. CNJR-7828
@@ -20,6 +20,8 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
2020
- Attempt to authenticate using the built-in authenticator (`authn`) with a GET
2121
request now results in a `404` response, rather than logging an authenticator
2222
not enabled message. CNJR-5854
23+
- Attempt to load a policy that references a non-existent resource now
24+
results in a `422` response, rather than a `404` error. CNJR-9122
2325
- Set the default and maximal limit value for resources list API to 1000 in order
2426
to align with the documentation. CNJR-8485
2527
- Ensure Kubernetes authenticator websocket connections are closed when a

app/controllers/application_controller.rb

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ class UnprocessableEntity < RuntimeError
6767
rescue_from Sequel::ForeignKeyConstraintViolation, with: :foreign_key_constraint_violation
6868
rescue_from Exceptions::EnhancedPolicyError, with: :enhanced_policy_error
6969
rescue_from Exceptions::InvalidPolicyObject, with: :policy_invalid
70+
rescue_from Exceptions::PolicyLoadRecordNotFound, with: :policy_invalid
7071
rescue_from Conjur::PolicyParser::Invalid, with: :policy_invalid
7172
rescue_from Conjur::PolicyParser::ResolverError, with: :policy_invalid
7273
rescue_from NoMethodError, with: :validation_failed
@@ -138,7 +139,7 @@ def foreign_key_constraint_violation e
138139
if e.is_a?(Sequel::ForeignKeyConstraintViolation) &&
139140
e.cause.is_a?(PG::ForeignKeyViolation) &&
140141
(e.cause.result.error_field(PG::PG_DIAG_MESSAGE_DETAIL) =~ /Key \(([^)]+)\)=\(([^)]+)\) is not present in table "([^"]+)"/ rescue false)
141-
violating_key = $2
142+
violating_key = ::Regexp.last_match(2)
142143

143144
exc = Exceptions::RecordNotFound.new(violating_key)
144145
render_record_not_found(exc)
@@ -178,7 +179,7 @@ def validation_failed e
178179
def policy_invalid e
179180
logger.debug("#{e}\n#{e.backtrace.join("\n")}")
180181

181-
msg = e.message == nil ? e.to_s : e.message
182+
msg = e.message.nil? ? e.to_s : e.message
182183
error = { code: "policy_invalid", message: msg }
183184

184185
if e.instance_of?(Conjur::PolicyParser::Invalid)

app/controllers/policies_controller.rb

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ class PoliciesController < RestController
1818
def get
1919
action = :read
2020
unless params[:kind] == 'policy'
21-
raise(Errors::EffectivePolicy::PathParamError.new(params[:kind]))
21+
raise Errors::EffectivePolicy::PathParamError, params[:kind]
2222
end
2323

2424
authorize_ownership
@@ -152,6 +152,7 @@ def load_policy(mode, mode_class, delete_permitted:)
152152
get_policy_mode.call(strategy_class)
153153
policy_mode.call_pr(policy_result) unless policy_erred.call
154154
rescue => e
155+
e = sql_to_policy_error(e)
155156
policy_result.error=(enhance_error(e))
156157
end
157158
}
@@ -212,7 +213,7 @@ def load_policy(mode, mode_class, delete_permitted:)
212213
rescue => e
213214
original_error = e
214215
if e.instance_of?(Exceptions::EnhancedPolicyError) && e.original_error
215-
original_error = e.original_error
216+
original_error = e.original_error
216217
end
217218

218219
audit_failure(original_error, mode)
@@ -337,4 +338,20 @@ def parse_submitted_policy(policy_result)
337338
def publish_event
338339
Monitoring::PubSub.instance.publish('conjur.policy_loaded')
339340
end
341+
342+
# This method is used to convert a Sequel::ForeignKeyConstraintViolation error
343+
# into a PolicyLoadRecordNotFound error.
344+
def sql_to_policy_error(exception)
345+
if !exception.is_a?(Sequel::ForeignKeyConstraintViolation) ||
346+
!exception.cause.is_a?(PG::ForeignKeyViolation)
347+
return exception
348+
end
349+
350+
# Try to parse the error message to find the violating key. This is based on
351+
# `foreign_key_constraint_violation` in application_controller.rb.
352+
return exception unless exception.cause.result.error_field(PG::PG_DIAG_MESSAGE_DETAIL) =~ /Key \(([^)]+)\)=\(([^)]+)\) is not present in table "([^"]+)"/
353+
354+
violating_key = ::Regexp.last_match(2)
355+
Exceptions::PolicyLoadRecordNotFound.new(violating_key)
356+
end
340357
end
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
# frozen_string_literal: true
2+
3+
module Exceptions
4+
# This exception is raised when a record required for policy loading is not found.
5+
# It is different from a standard RecordNotFound exception because it is used in the
6+
# context of policy loading, where the record not existing should be treated as a
7+
# validation error of the policy load object and return a 422 status code, rather
8+
# than a 404.
9+
class PolicyLoadRecordNotFound < RuntimeError
10+
attr_reader :id, :account, :kind, :identifier
11+
12+
def initialize id, message: nil
13+
super(message || self.class.build_message(id))
14+
15+
@id = id
16+
@account, @kind, @identifier = self.class.parse_id(id)
17+
end
18+
19+
class << self
20+
def parse_id id
21+
id.split(':', 3)
22+
end
23+
24+
def build_message id
25+
account, kind, id = parse_id(id)
26+
kind ||= 'unknown kind'
27+
account ||= 'unknown account'
28+
"#{kind.capitalize} '#{id}' not found in account '#{account}'"
29+
end
30+
end
31+
end
32+
end

app/models/loader/types.rb

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -54,11 +54,11 @@ def find_ownerid
5454
end
5555

5656
def find_roleid id
57-
(::Role[id] || public_roles_model[id]).try(:role_id) || raise(Exceptions::RecordNotFound, id)
57+
(::Role[id] || public_roles_model[id]).try(:role_id) || raise(Exceptions::PolicyLoadRecordNotFound, id)
5858
end
5959

6060
def find_resourceid id
61-
(::Resource[id] || public_resources_model[id]).try(:resource_id) || raise(Exceptions::RecordNotFound, id)
61+
(::Resource[id] || public_resources_model[id]).try(:resource_id) || raise(Exceptions::PolicyLoadRecordNotFound, id)
6262
end
6363

6464
protected
@@ -318,7 +318,7 @@ def verify_dynamic_secret
318318

319319
if issuer.nil?
320320
issuer_exception_id = "#{@policy_object.account}:issuer:#{issuer_id}"
321-
raise Exceptions::RecordNotFound, issuer_exception_id
321+
raise Exceptions::PolicyLoadRecordNotFound, issuer_exception_id
322322
end
323323

324324
if annotations["#{Issuer::DYNAMIC_ANNOTATION_PREFIX}method"].nil?
@@ -361,7 +361,7 @@ def auth_issuer_resource(privilege, resource_id, issuer_id, account)
361361
resource = ::Resource[resource_id]
362362
unless current_user.allowed_to?(privilege, resource)
363363
issuer_exception_id = "#{account}:issuer:#{issuer_id}"
364-
raise Exceptions::RecordNotFound, issuer_exception_id
364+
raise Exceptions::PolicyLoadRecordNotFound, issuer_exception_id
365365
end
366366
end
367367

cucumber/api/features/policy_load_errors.feature

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -16,19 +16,13 @@ Feature: Policy loading error messages
1616
privilege: [ execute ]
1717
resource: !variable password
1818
"""
19-
Then the HTTP response status code is 404
19+
Then the HTTP response status code is 422
2020
And the JSON response should be:
2121
"""
2222
{
2323
"error": {
24-
"code": "not_found",
25-
"message": "User 'bob' not found in account 'cucumber'",
26-
"target": "user",
27-
"details": {
28-
"code": "not_found",
29-
"target": "id",
30-
"message": "cucumber:user:bob"
31-
}
24+
"code": "policy_invalid",
25+
"message": "User 'bob' not found in account 'cucumber'"
3226
}
3327
}
3428
"""

cucumber/policy/features/host_factory.feature

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ Feature: Host Factories can be managed through policies.
4444
layers: [ !layer ../default ]
4545
"""
4646
Then there's an error
47-
And the error code is "not_found"
47+
And the error code is "policy_invalid"
4848
And the error message is "Layer 'myapp/../default' not found in account 'cucumber'"
4949

5050
@acceptance

cucumber/policy/features/replace.feature

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ A policy can be reloaded using the --replace flag
4848
- !user developer2
4949
"""
5050
Then there's an error
51-
And the error code is "not_found"
51+
And the error code is "policy_invalid"
5252
And the error message is "Group 'developers' not found in account 'cucumber'"
5353

5454
@negative @acceptance
@@ -85,7 +85,7 @@ A policy can be reloaded using the --replace flag
8585
- !user developer2
8686
"""
8787
Then there's an error
88-
And the error code is "not_found"
88+
And the error code is "policy_invalid"
8989
And the error message is "Group 'security-admin' not found in account 'cucumber'"
9090

9191
@smoke
@@ -281,7 +281,7 @@ A policy can be reloaded using the --replace flag
281281
resource: !layer ops
282282
"""
283283
Then there's an error
284-
And the error code is "not_found"
284+
And the error code is "policy_invalid"
285285
And the error message is "Layer 'ops' not found in account 'cucumber'"
286286

287287
@acceptance

cucumber/policy/features/step_definitions/policy_steps.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525
Given(/^I try to load a policy with an unresolvable reference:$/) do |policy|
2626
@client = Client.for("user", "admin")
2727
@result = @client.load_policy(id: 'root', policy: policy)
28-
expect(@result.code).to eq(404)
28+
expect(@result.code).to eq(422)
2929
end
3030

3131
Then("the result includes an API key for {string}") do |role_id|

spec/controllers/policy_validation_spec.rb

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,8 @@ def validation_error_text
2727
def validation_explanation
2828
body = JSON.parse(response.body)
2929
msg = body['errors'][0]['message']
30-
adv = msg.match(/^[^\n]*\n{1,1}(.*)$/).to_s
31-
adv
30+
msg.match(/^[^\n]*\n{1,1}(.*)$/).to_s
31+
3232
end
3333

3434
describe PoliciesController, type: :request do
@@ -156,14 +156,10 @@ def validation_explanation
156156
resource: !variable password
157157
YAML
158158
)
159-
expect(response.code).to eq("404")
159+
expect(response.code).to eq("422")
160160
body = JSON.parse(response.body)
161-
expect(body['error']['code']).to eq("not_found")
161+
expect(body['error']['code']).to eq("policy_invalid")
162162
expect(body['error']['message']).to eq("User 'bob' not found in account 'rspec'")
163-
expect(body['error']['target']).to eq("user")
164-
expect(body['error']['details']["code"]).to eq("not_found")
165-
expect(body['error']['details']["target"]).to eq("id")
166-
expect(body['error']['details']["message"]).to eq("rspec:user:bob")
167163
end
168164
end
169165

0 commit comments

Comments
 (0)