Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions tests-bdd/cukes/steps_authorization.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
93 changes: 93 additions & 0 deletions tests-bdd/features/claims-ldap-fallback-ers.feature
Original file line number Diff line number Diff line change
@@ -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
73 changes: 73 additions & 0 deletions tests-bdd/features/claims-only-ers.feature
Original file line number Diff line number Diff line change
@@ -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.
Comment on lines +6 to +9

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== candidate files =="
git ls-files | rg '(^tests-bdd/features/claims-only-ers\.feature$|go\.mod$|go\.sum$|main\.go$|cmd/.*)' | head -200

echo
echo "== feature file =="
if [ -f tests-bdd/features/claims-only-ers.feature ]; then
  cat -n tests-bdd/features/claims-only-ers.feature
fi

echo
echo "== godog references =="
rg -n "godog|Features|TestMain|claims-only-ers|pending|skip|beforeScenario|afterScenario|Run" tests-bdd . --glob '!vendor/**' --glob '!node_modules/**' | head -250

Repository: opentdf/platform

Length of output: 32525


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== tests-bdd/platform_test.go =="
cat -n tests-bdd/platform_test.go

echo
echo "== read-only parsing/test suite option model =="
python3 - <<'PY'
from pathlib import Path
p = Path("tests-bdd/platform_test.go")
text = p.read_text()
for needle in ["var opts = godog.Options{", "opts.Summary", "opts.Concurrent", "opts.ScenarioEvents", "features := godog.Options{Features:", "godog.TestSuite{"]:
    print(f"contains {needle!r}: {needle in text}")
PY

echo
echo "== any pending/skip tags for this feature =="
rg -n '`@pending`|`@skip`|claims-only-ers|stateless|godog.tags' tests-bdd/README.md tests-bdd/platform_test.go tests-bdd/features/claims-only-ers.feature

Repository: opentdf/platform

Length of output: 6629


Quarantine the known-failing feature from normal BDD runs.

tests-bdd/platform_test.go runs Godog with the default options and no Options.Tags filter, so @claims-only-ers is included in normal BDD invocations despite the in-file note that the scenarios currently fail. Add an executable pending/skip control and exclude it until #3790 is fixed.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests-bdd/features/claims-only-ers.feature` around lines 6 - 9, Mark the
claims-only ERS feature with the repository’s executable pending/skip control so
its scenarios are excluded from normal Godog runs. Update the feature metadata
near the existing NOTE in claims-only-ers.feature, preserving the
`@claims-only-ers` tag and ensuring the quarantine can be removed once
ResolveEntities fixes issue `#3790`.


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
Comment on lines +52 to +73

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Make the marketing case prove claims resolution.

DENY is also the documented result when JWT claims are absent, so this passes even if Bob’s department is never resolved. Add a marketing PERMIT case or assert the resolved claim before relying on this negative case.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests-bdd/features/claims-only-ers.feature` around lines 52 - 73, Strengthen
the “Claims entity with marketing department gets DENY” scenario so it verifies
Bob’s marketing claim is resolved rather than relying only on the DENY result.
Add a corresponding marketing resource/action that must return PERMIT, or assert
the resolved department claim before the existing engineering DENY request,
while preserving the current negative case.

72 changes: 72 additions & 0 deletions tests-bdd/features/multi-strategy-ers-failfast.feature
Original file line number Diff line number Diff line change
@@ -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
Loading