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-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/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 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