Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
7094768
adr
elizabethhealy Mar 30, 2026
51273df
namespaced policy in decisions
elizabethhealy Mar 31, 2026
5d1dfd1
add cukes
elizabethhealy Mar 31, 2026
3441f0d
more cukes scenarios
elizabethhealy Mar 31, 2026
194bf75
Merge branch 'main' into dspx-2753-namespaced-policy-decisioning
elizabethhealy Mar 31, 2026
3ca7554
lint, attempt to fix bdd
elizabethhealy Mar 31, 2026
26740fd
use require
elizabethhealy Mar 31, 2026
6278ba4
code rabbit suggestions
elizabethhealy Mar 31, 2026
d26824c
remove file
elizabethhealy Mar 31, 2026
3af77a2
direct entitlement handling
elizabethhealy Mar 31, 2026
fc917bf
linting, rename function
elizabethhealy Apr 1, 2026
d2fed66
update ok
elizabethhealy Apr 1, 2026
2d17070
update features, change attr naming
elizabethhealy Apr 1, 2026
e03b0e5
add comments, fix attr rule
elizabethhealy Apr 1, 2026
7ef6a6e
coderabbit suggestions
elizabethhealy Apr 1, 2026
eb7dd89
handle namespaced rr fqn indexing
elizabethhealy Apr 2, 2026
4743bc1
rename flag, address comment
elizabethhealy Apr 7, 2026
ca09e51
suggestions
elizabethhealy Apr 7, 2026
49eec3f
lint
elizabethhealy Apr 8, 2026
fb9554b
Merge branch 'main' into dspx-2753-namespaced-policy-decisioning
elizabethhealy Apr 8, 2026
5078bd2
remove trace log for action id/name mismatch
elizabethhealy Apr 9, 2026
f1bfac9
RR entities and resource should be namespaced if flag on
elizabethhealy Apr 10, 2026
5eb0db3
better cukes setup with template add specific deny scenarios
elizabethhealy Apr 10, 2026
ef57189
add un-namespaced sm to feature
elizabethhealy Apr 10, 2026
57b785c
populate namespace for action for decisioning check
elizabethhealy Apr 13, 2026
1384ee5
move into validators, use fqns instead of id, remove scoped
elizabethhealy Apr 15, 2026
79e3a29
Merge branch 'main' into dspx-2753-namespaced-policy-decisioning
elizabethhealy Apr 15, 2026
f026dbe
extend integration test
elizabethhealy Apr 15, 2026
7612d4e
linting
elizabethhealy Apr 15, 2026
c89d3e4
lint
elizabethhealy Apr 15, 2026
6eb73e0
lint
elizabethhealy Apr 15, 2026
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
---
status: 'proposed'
date: '2026-03-30'
tags:
- policy
- authorization
- namespaced-policy
driver: '@elizabethhealy'
---

# Namespaced Subject Mapping Decisioning in PDP

## Context and Problem Statement

Policy objects are moving toward strict namespace ownership, but access decisioning still treats actions as unscoped names in several evaluation paths. Subject mappings are also transitioning from legacy unnamespaced records to namespace-owned records. This creates ambiguity when a request action name exists in multiple namespaces and when a single resource includes attributes from multiple namespaces.

We need a decisioning model that is namespace-correct, fail-closed, and compatible with staged rollout using the `EnforceNamespacedEntitlements` feature flag.

## Decision Drivers

- Preserve existing multi-namespace resource semantics (`AND` behavior) while adding namespace correctness.
- Prevent cross-namespace action matches when namespaced policy mode is enabled.
- Keep rollout safe via feature-flagged behavior split.
- Avoid startup coupling by keeping standard-action checks lazy at evaluation time.

## Decision Outcome

Chosen option: **Resolve request action identity within each evaluation namespace context**.

For `GetDecisionRequest`/`GetDecisionMultiResourceRequest`, request validation still requires `action.name` (current proto contract). During evaluation, matching is always applied per namespace context (derived from the rule/value being evaluated), not globally.

Request-action matching precedence is explicit (given the request action object):

1. `action.id` (exact identity, when present)
2. `action.name + action.namespace` (scoped identity, when namespace is present)
3. `action.name` only (contextual identity)

When identity is explicit (`id` or `name+namespace`), decisioning does not fall back to looser name-only matching. It fails closed only if that explicit identity is unresolved or mismatched for the evaluated namespace context.

Feature-flag mode split:

- `EnforceNamespacedEntitlements=false`: preserve existing legacy behavior (no new namespace filtering semantics introduced by this change).
- `EnforceNamespacedEntitlements=true`: enforce namespaced subject mapping evaluation (unnamespaced SMs are ignored) and require action namespace equality for each evaluated namespace.

Direct entitlements in strict mode:

- Direct entitlements are still modeled as action names per attribute-value FQN.
- During PDP evaluation, each direct-entitlement action is hydrated with the namespace of its attributed value context.
- This makes direct entitlements participate in the same namespace-aware action matching rules as subject-mapping-derived entitlements.
- Direct-entitlement actions are merged with subject-mapping actions per value FQN (not replacing them).

Subject mapping namespace enforcement (strict mode):

- Subject mapping namespace must match the namespace of the referenced attribute value.
- Subject mapping namespace must match the namespace of the referenced subject condition set.
- Name-based action matching is evaluated in the same namespace context as the SM/value under evaluation.

For multi-namespace resources, existing `AND` semantics remain unchanged: all required namespace-scoped checks must pass, and missing action support in any required namespace denies access.

## Consequences

- 🟩 **Good**, because action evaluation becomes deterministic and namespace-safe.
- 🟩 **Good**, because feature-flagged split allows staged migration without mixed-mode ambiguity.
- 🟩 **Good**, because fail-closed behavior prevents accidental entitlement via cross-namespace action reuse.
- 🟥 **Bad**, because policy admins must ensure required actions exist in each relevant namespace.
- 🟥 **Bad**, because debugging becomes harder without explicit namespace-aware logs.
Comment thread
elizabethhealy marked this conversation as resolved.

## Validation

Validation is done through PDP and decisioning tests covering:

- mode split (`EnforceNamespacedEntitlements=false` vs `true`) for subject mapping inclusion,
- strict-mode subject mapping namespace scoping (unnamespaced SMs skipped),
- namespace-aware action matching in rule evaluation paths,
- multi-namespace resource behavior where one missing namespace action causes deny,
- regression checks to confirm existing `AND` behavior is preserved.

## Implementation Notes

- Thread `EnforceNamespacedEntitlements` into PDP runtime configuration.
- Filter subject mappings at PDP construction by mode (namespaced vs unnamespaced).
- Enforce subject mapping namespace consistency during create/update operations.
- Centralize action matching in a namespace-aware helper used by all rule/action checks.
- Derive required namespace per evaluated value/rule context.
- Keep standard/custom action existence checks lazy at evaluation time.
- Add debug logs including requested action, required namespace, candidate namespace, and rule/value context.
Comment thread
elizabethhealy marked this conversation as resolved.

## Rollout

1. Land logic behind `EnforceNamespacedEntitlements`.
2. Keep default mode as legacy (`false`) until policy data migration is complete.
3. Validate namespaced policy data readiness.
4. Flip `EnforceNamespacedEntitlements=true` and monitor mismatch/deny behavior.
5. Remove legacy branch once namespaced mode is stable.
8 changes: 4 additions & 4 deletions service/authorization/v2/authorization.go
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,7 @@ func (as *Service) GetEntitlements(ctx context.Context, req *connect.Request[aut
withComprehensiveHierarchy := req.Msg.GetWithComprehensiveHierarchy()

// When authorization service can consume cached policy, switch to the other PDP (process based on policy passed in)
pdp, err := access.NewJustInTimePDP(ctx, as.logger, as.sdk, as.cache, as.config.AllowDirectEntitlements)
pdp, err := access.NewJustInTimePDP(ctx, as.logger, as.sdk, as.cache, as.config.AllowDirectEntitlements, as.config.EnforceNamespacedEntitlements)
if err != nil {
return nil, statusifyError(ctx, as.logger, errors.Join(ErrFailedToGetEntitlements, ErrFailedToInitPDP, err))
}
Expand All @@ -166,7 +166,7 @@ func (as *Service) GetDecision(ctx context.Context, req *connect.Request[authzV2
ctx, span := as.Start(ctx, "GetDecision")
defer span.End()

pdp, err := access.NewJustInTimePDP(ctx, as.logger, as.sdk, as.cache, as.config.AllowDirectEntitlements)
pdp, err := access.NewJustInTimePDP(ctx, as.logger, as.sdk, as.cache, as.config.AllowDirectEntitlements, as.config.EnforceNamespacedEntitlements)
if err != nil {
return nil, statusifyError(ctx, as.logger, errors.Join(ErrFailedToInitPDP, err))
}
Expand Down Expand Up @@ -212,7 +212,7 @@ func (as *Service) GetDecisionMultiResource(ctx context.Context, req *connect.Re
ctx, span := as.Start(ctx, "GetDecisionMultiResource")
defer span.End()

pdp, err := access.NewJustInTimePDP(ctx, as.logger, as.sdk, as.cache, as.config.AllowDirectEntitlements)
pdp, err := access.NewJustInTimePDP(ctx, as.logger, as.sdk, as.cache, as.config.AllowDirectEntitlements, as.config.EnforceNamespacedEntitlements)
if err != nil {
return nil, statusifyError(ctx, as.logger, errors.Join(ErrFailedToInitPDP, err))
}
Expand Down Expand Up @@ -261,7 +261,7 @@ func (as *Service) GetDecisionBulk(ctx context.Context, req *connect.Request[aut
ctx, span := as.Start(ctx, "GetDecisionBulk")
defer span.End()

pdp, err := access.NewJustInTimePDP(ctx, as.logger, as.sdk, as.cache, as.config.AllowDirectEntitlements)
pdp, err := access.NewJustInTimePDP(ctx, as.logger, as.sdk, as.cache, as.config.AllowDirectEntitlements, as.config.EnforceNamespacedEntitlements)
if err != nil {
return nil, statusifyError(ctx, as.logger, errors.Join(ErrFailedToInitPDP, err))
}
Expand Down
5 changes: 5 additions & 0 deletions service/authorization/v2/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ type Config struct {

// enable entity direct entitlements that do not require subject mappings
AllowDirectEntitlements bool `mapstructure:"allow_direct_entitlements" json:"allow_direct_entitlements" default:"false"`

// enforce strict namespaced entitlement evaluation behavior in access decisioning
EnforceNamespacedEntitlements bool `mapstructure:"enforce_namespaced_entitlements" json:"enforce_namespaced_entitlements" default:"false"`
}

// Validate tests for a sensible configuration
Expand Down Expand Up @@ -56,5 +59,7 @@ func (c *Config) LogValue() slog.Value {
slog.String("refresh_interval", c.Cache.RefreshInterval),
),
),
slog.Bool("allow_direct_entitlements", c.AllowDirectEntitlements),
slog.Bool("enforce_namespaced_entitlements", c.EnforceNamespacedEntitlements),
)
}
4 changes: 4 additions & 0 deletions service/integration/registered_resources_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -358,6 +358,8 @@ func (s *RegisteredResourcesSuite) Test_ListRegisteredResources_RegResValuesCont
for _, aav := range actionAttrValues {
s.NotNil(aav.GetAction())
s.NotNil(aav.GetAttributeValue())
s.NotNil(aav.GetAction().GetNamespace(), "action namespace should be populated for namespaced RR")
s.Equal("example.com", aav.GetAction().GetNamespace().GetName())
}
}
if v.GetId() == val2.GetId() {
Expand All @@ -367,6 +369,8 @@ func (s *RegisteredResourcesSuite) Test_ListRegisteredResources_RegResValuesCont
for _, aav := range actionAttrValues {
s.NotNil(aav.GetAction())
s.NotNil(aav.GetAttributeValue())
s.NotNil(aav.GetAction().GetNamespace(), "action namespace should be populated for namespaced RR")
s.Equal("example.com", aav.GetAction().GetNamespace().GetName())
}
}
}
Expand Down
Loading
Loading