From a244da513fc2ab19b9f0f69f41a47e17fe11cf76 Mon Sep 17 00:00:00 2001 From: Ken Ho Date: Mon, 27 Jul 2026 13:28:36 -0400 Subject: [PATCH 1/2] chore(ers): add fail-fast and claims-LDAP-fallback BDD scenarios MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Expand multi-strategy ERS BDD coverage (DSPX-4100) with two new feature files: - @multi-strategy-ers-failfast: demonstrates that fail-fast stops entity resolution at first strategy error (claims provider fails due to missing JWTClaimsContextKey — see #3790), preventing LDAP fallback → DENY - @claims-ldap-fallback-ers: validates condition-based strategy routing where claims strategy is skipped (condition unmet) and LDAP provides department Signed-off-by: Ken Ho Co-Authored-By: Claude Opus 4.6 Signed-off-by: Ken Ho --- .../features/claims-ldap-fallback-ers.feature | 93 +++++++++++++++++++ .../multi-strategy-ers-failfast.feature | 72 ++++++++++++++ 2 files changed, 165 insertions(+) create mode 100644 tests-bdd/features/claims-ldap-fallback-ers.feature create mode 100644 tests-bdd/features/multi-strategy-ers-failfast.feature diff --git a/tests-bdd/features/claims-ldap-fallback-ers.feature b/tests-bdd/features/claims-ldap-fallback-ers.feature new file mode 100644 index 0000000000..e9608a7234 --- /dev/null +++ b/tests-bdd/features/claims-ldap-fallback-ers.feature @@ -0,0 +1,93 @@ +@claims-ldap-fallback-ers @stateless +Feature: Claims-to-LDAP fallback via condition-based strategy routing + Validate that condition-based strategy selection correctly routes entity + resolution: a claims strategy with condition "department exists" is skipped + when the entity lacks a department claim, and the LDAP strategy with + condition "userName exists" matches and provides the department from LDAP. + + This demonstrates the fallback pattern where claims serve as a fast path + (when claims already contain the needed attributes) and LDAP acts as the + fallback (when claims are incomplete). + + Background: + Given an LDAP directory with test users + And an ERS configuration with mode "multi-strategy" and failure strategy "continue" + And an ERS provider "jwt_claims" of type "claims" + And an ERS provider "ldap_directory" of type "ldap" connected to the LDAP directory + And an ERS mapping strategy "claims_with_department" using provider "jwt_claims" + """ + entity_type: subject + conditions: + jwt_claims: + - claim: department + operator: exists + output_mapping: + - source_claim: department + claim_name: department + - source_claim: userName + claim_name: username + """ + And an ERS mapping strategy "ldap_department_lookup" using provider "ldap_directory" + """ + entity_type: subject + conditions: + jwt_claims: + - claim: userName + operator: exists + ldap_search: + base_dn: "ou=users,dc=opentdf,dc=test" + filter: "(&(objectClass=inetOrgPerson)(uid={username}))" + scope: subtree + attributes: ["uid", "mail", "departmentNumber"] + input_mapping: + - jwt_claim: userName + parameter: username + output_mapping: + - source_attribute: departmentNumber + claim_name: department + - source_attribute: mail + claim_name: email + - source_attribute: uid + claim_name: username + """ + And a local platform with inline ERS configuration + + Scenario: Entity without department claim falls back to LDAP — engineering user gets PERMIT + Given I submit a request to create a namespace with name "fallback.test" and reference id "ns_fb" + And I send a request to create an attribute with: + | namespace_id | name | rule | values | + | ns_fb | department | anyOf | engineering,marketing,security | + Then the response should be successful + Given a condition group referenced as "cg_fb" with an "or" operator with conditions: + | selector_value | operator | values | + | .department | in | engineering | + And a subject set referenced as "ss_fb" containing the condition groups "cg_fb" + And I send a request to create a subject condition set referenced as "scs_fb" containing subject sets "ss_fb" + And I send a request to create a subject mapping with: + | reference_id | attribute_value | condition_set_name | standard actions | custom actions | + | sm_fb | https://fallback.test/attr/department/value/engineering | scs_fb | read | | + Then the response should be successful + Given there is a "user_name" subject entity with value "diana" and referenced as "diana_fb" + When I send a decision request for entity chain "diana_fb" for "read" action on resource "https://fallback.test/attr/department/value/engineering" + Then the response should be successful + And I should get a "PERMIT" decision response + + Scenario: Entity without department claim falls back to LDAP — operations user gets DENY + Given I submit a request to create a namespace with name "fallback-deny.test" and reference id "ns_fb_deny" + And I send a request to create an attribute with: + | namespace_id | name | rule | values | + | ns_fb_deny | department | anyOf | engineering,marketing,security | + Then the response should be successful + Given a condition group referenced as "cg_fb_deny" with an "or" operator with conditions: + | selector_value | operator | values | + | .department | in | engineering | + And a subject set referenced as "ss_fb_deny" containing the condition groups "cg_fb_deny" + And I send a request to create a subject condition set referenced as "scs_fb_deny" containing subject sets "ss_fb_deny" + And I send a request to create a subject mapping with: + | reference_id | attribute_value | condition_set_name | standard actions | custom actions | + | sm_fb_deny | https://fallback-deny.test/attr/department/value/engineering | scs_fb_deny | read | | + Then the response should be successful + Given there is a "user_name" subject entity with value "eve" and referenced as "eve_fb" + When I send a decision request for entity chain "eve_fb" for "read" action on resource "https://fallback-deny.test/attr/department/value/engineering" + Then the response should be successful + And I should get a "DENY" decision response diff --git a/tests-bdd/features/multi-strategy-ers-failfast.feature b/tests-bdd/features/multi-strategy-ers-failfast.feature new file mode 100644 index 0000000000..b4dbc67e73 --- /dev/null +++ b/tests-bdd/features/multi-strategy-ers-failfast.feature @@ -0,0 +1,72 @@ +@multi-strategy-ers-failfast @stateless +Feature: Multi-strategy ERS fail-fast behavior + Validate that failure_strategy "fail-fast" stops entity resolution at the first + strategy error, preventing fallback to subsequent strategies. This contrasts + with the "continue" behavior tested in multi-strategy-ers.feature where the + same user (alice) gets PERMIT because LDAP succeeds after claims fails. + + The claims_passthrough strategy always fails in the ResolveEntities path + because JWTClaimsContextKey is not populated in context (known bug). Under + "fail-fast", this aborts resolution immediately — LDAP never executes — so + the entity has no .department claim and no subject mapping matches → DENY. + + Background: + Given an LDAP directory with test users + And an ERS configuration with mode "multi-strategy" and failure strategy "fail-fast" + And an ERS provider "jwt_claims" of type "claims" + And an ERS provider "ldap_directory" of type "ldap" connected to the LDAP directory + And an ERS mapping strategy "claims_passthrough" using provider "jwt_claims" + """ + entity_type: subject + conditions: + jwt_claims: + - claim: userName + operator: exists + output_mapping: + - source_claim: userName + claim_name: username + """ + And an ERS mapping strategy "ldap_by_username" using provider "ldap_directory" + """ + entity_type: subject + conditions: + jwt_claims: + - claim: userName + operator: exists + ldap_search: + base_dn: "ou=users,dc=opentdf,dc=test" + filter: "(&(objectClass=inetOrgPerson)(uid={username}))" + scope: subtree + attributes: ["uid", "mail", "departmentNumber"] + input_mapping: + - jwt_claim: userName + parameter: username + output_mapping: + - source_attribute: departmentNumber + claim_name: department + - source_attribute: mail + claim_name: email + - source_attribute: uid + claim_name: username + """ + And a local platform with inline ERS configuration + + Scenario: Fail-fast prevents LDAP fallback — engineering user gets DENY + Given I submit a request to create a namespace with name "eng-failfast.test" and reference id "ns_eng_ff" + And I send a request to create an attribute with: + | namespace_id | name | rule | values | + | ns_eng_ff | department | anyOf | engineering,marketing,security | + Then the response should be successful + Given a condition group referenced as "cg_eng_ff" with an "or" operator with conditions: + | selector_value | operator | values | + | .department | in | engineering | + And a subject set referenced as "ss_eng_ff" containing the condition groups "cg_eng_ff" + And I send a request to create a subject condition set referenced as "scs_eng_ff" containing subject sets "ss_eng_ff" + And I send a request to create a subject mapping with: + | reference_id | attribute_value | condition_set_name | standard actions | custom actions | + | sm_eng_ff | https://eng-failfast.test/attr/department/value/engineering | scs_eng_ff | read | | + Then the response should be successful + Given there is a "user_name" subject entity with value "alice" and referenced as "alice_ff" + When I send a decision request for entity chain "alice_ff" for "read" action on resource "https://eng-failfast.test/attr/department/value/engineering" + Then the response should be successful + And I should get a "DENY" decision response From df002c979963b5068f1a5d882ec019edbd3d2250 Mon Sep 17 00:00:00 2001 From: Ken Ho Date: Mon, 27 Jul 2026 13:33:41 -0400 Subject: [PATCH 2/2] chore(ers): add claims-only ERS scenario reproducing #3790 Add @claims-only-ers feature that expects PERMIT for an Entity_Claims entity with inline department claim. Currently FAILS with "DECISION_DENY instead of DECISION_PERMIT" because ResolveEntities does not populate JWTClaimsContextKey in context (#3790). This test will pass once the bug is fixed. Also adds a DocString step definition for creating claims entities with embedded JSON (existing step regex rejects double quotes). Signed-off-by: Ken Ho Co-Authored-By: Claude Opus 4.6 Signed-off-by: Ken Ho --- tests-bdd/cukes/steps_authorization.go | 11 ++++ tests-bdd/features/claims-only-ers.feature | 73 ++++++++++++++++++++++ 2 files changed, 84 insertions(+) create mode 100644 tests-bdd/features/claims-only-ers.feature diff --git a/tests-bdd/cukes/steps_authorization.go b/tests-bdd/cukes/steps_authorization.go index 4ced33b43f..5ca365e63f 100644 --- a/tests-bdd/cukes/steps_authorization.go +++ b/tests-bdd/cukes/steps_authorization.go @@ -465,8 +465,19 @@ func (s *AuthorizationServiceStepDefinitions) theDecisionResponseForResourceShou return ctx, fmt.Errorf("resource %s not found in decision responses (known: %v)", resourceFQN, known) } +func (s *AuthorizationServiceStepDefinitions) thereIsAClaimsSubjectEntityReferencedAs(ctx context.Context, referenceID string, doc *godog.DocString) (context.Context, error) { + scenarioContext := GetPlatformScenarioContext(ctx) + entity, err := s.createEntity(referenceID, "SUBJECT", "claims", doc.Content) + if err != nil { + return ctx, err + } + scenarioContext.RecordObject(referenceID, entity) + return ctx, nil +} + func RegisterAuthorizationStepDefinitions(ctx *godog.ScenarioContext) { stepDefinitions := AuthorizationServiceStepDefinitions{} + ctx.Step(`^there is a "claims" subject entity referenced as "([^"]*)" with claims:$`, stepDefinitions.thereIsAClaimsSubjectEntityReferencedAs) ctx.Step(`^there is a "([^"]*)" subject entity with value "([^"]*)" and referenced as "([^"]*)"$`, stepDefinitions.thereIsASubjectEntityWithValueAndReferencedAs) ctx.Step(`^there is a "([^"]*)" environment entity with value "([^"]*)" and referenced as "([^"]*)"$`, stepDefinitions.thereIsAEnvEntityWithValueAndReferencedAs) ctx.Step(`^I send a decision request for entity chain "([^"]*)" for "([^"]*)" action on resource "([^"]*)"$`, stepDefinitions.iSendADecisionRequestForEntityChainForActionOnResource) diff --git a/tests-bdd/features/claims-only-ers.feature b/tests-bdd/features/claims-only-ers.feature new file mode 100644 index 0000000000..3a4d356404 --- /dev/null +++ b/tests-bdd/features/claims-only-ers.feature @@ -0,0 +1,73 @@ +@claims-only-ers @stateless +Feature: Claims-only ERS resolution (no LDAP) + Validate that Entity_Claims entities carrying claims inline can be resolved + by a claims-only multi-strategy ERS without needing LDAP. + + NOTE: These scenarios currently FAIL due to a known bug (#3790) where + ResolveEntities does not populate JWTClaimsContextKey in context, causing + the claims provider to always error with "JWT claims not found in context". + Once the bug is fixed, these scenarios should pass. + + Background: + And an ERS configuration with mode "multi-strategy" and failure strategy "continue" + And an ERS provider "jwt_claims" of type "claims" + And an ERS mapping strategy "claims_department" using provider "jwt_claims" + """ + entity_type: subject + conditions: + jwt_claims: + - claim: department + operator: exists + output_mapping: + - source_claim: department + claim_name: department + - source_claim: userName + claim_name: username + """ + And a local platform with inline ERS configuration + + Scenario: Claims entity with engineering department gets PERMIT + Given I submit a request to create a namespace with name "claims-only.test" and reference id "ns_claims" + And I send a request to create an attribute with: + | namespace_id | name | rule | values | + | ns_claims | department | anyOf | engineering,marketing,security | + Then the response should be successful + Given a condition group referenced as "cg_claims" with an "or" operator with conditions: + | selector_value | operator | values | + | .department | in | engineering | + And a subject set referenced as "ss_claims" containing the condition groups "cg_claims" + And I send a request to create a subject condition set referenced as "scs_claims" containing subject sets "ss_claims" + And I send a request to create a subject mapping with: + | reference_id | attribute_value | condition_set_name | standard actions | custom actions | + | sm_claims | https://claims-only.test/attr/department/value/engineering | scs_claims | read | | + Then the response should be successful + Given there is a "claims" subject entity referenced as "diana_claims" with claims: + """ + {"@type":"type.googleapis.com/google.protobuf.Struct","value":{"userName":"diana","department":"engineering"}} + """ + When I send a decision request for entity chain "diana_claims" for "read" action on resource "https://claims-only.test/attr/department/value/engineering" + Then the response should be successful + And I should get a "PERMIT" decision response + + Scenario: Claims entity with marketing department gets DENY for engineering resource + Given I submit a request to create a namespace with name "claims-deny.test" and reference id "ns_claims_deny" + And I send a request to create an attribute with: + | namespace_id | name | rule | values | + | ns_claims_deny | department | anyOf | engineering,marketing,security | + Then the response should be successful + Given a condition group referenced as "cg_claims_deny" with an "or" operator with conditions: + | selector_value | operator | values | + | .department | in | engineering | + And a subject set referenced as "ss_claims_deny" containing the condition groups "cg_claims_deny" + And I send a request to create a subject condition set referenced as "scs_claims_deny" containing subject sets "ss_claims_deny" + And I send a request to create a subject mapping with: + | reference_id | attribute_value | condition_set_name | standard actions | custom actions | + | sm_claims_deny | https://claims-deny.test/attr/department/value/engineering | scs_claims_deny | read | | + Then the response should be successful + Given there is a "claims" subject entity referenced as "bob_claims" with claims: + """ + {"@type":"type.googleapis.com/google.protobuf.Struct","value":{"userName":"bob","department":"marketing"}} + """ + When I send a decision request for entity chain "bob_claims" for "read" action on resource "https://claims-deny.test/attr/department/value/engineering" + Then the response should be successful + And I should get a "DENY" decision response