Summary
The environment-gating example on the Cedar Authorization docs page is fail-open. If environment is absent from invocationState, the deploy tool is permitted — the example reads as a production guardrail but does not deny when it cannot establish where it is running.
It also contradicts two claims the same page makes: "The design is fail-closed" and "Cedar uses default-deny semantics."
The code
cedar-authorization.ts#L204-L213 (Python mirror: cedar-authorization.py#L249-L266):
policies: `
permit(principal, action == Action::"deploy", resource)
when { context.session has environment &&
context.session.environment != "production" };
`,
contextEnricher: ({ invocationState }) => ({
environment: String(invocationState.environment ?? 'unknown'),
}),
Two problems compound:
- It's a denylist with a permissive default.
'unknown' != "production" is true, so a missing environment permits the deploy. Same for any spelling that isn't byte-identical to "production" — "Production", "prod", "prod-us-east-1" all pass.
- The
has environment guard is dead code. The enricher unconditionally sets environment via ?? 'unknown', so has is always true. It reads like a safety check but can never fire.
Reproduction
Evaluating the doc's policy against the exact context shape the handler builds (cedarpy, no SDK needed):
import cedarpy
DOC_POLICY = '''
permit(principal, action == Action::"deploy", resource)
when { context.session has environment &&
context.session.environment != "production" };
'''
FIXED_POLICY = '''
permit(principal, action == Action::"deploy", resource)
when { context.session has environment &&
["staging", "dev"].contains(context.session.environment) };
'''
def decide(policy, environment_in_invocation_state):
# mirror the enricher: a missing environment becomes 'unknown'
session = {"environment": environment_in_invocation_state or "unknown",
"hour_utc": 14, "call_count": 1}
request = {"principal": 'User::"alice"', "action": 'Action::"deploy"',
"resource": 'Resource::"agent"',
"context": {"input": {"version": "1.2.3"}, "session": session}}
return "ALLOW" if cedarpy.is_authorized(request, policy, []).allowed else "DENY"
for label, env in [("environment='staging'", "staging"),
("environment='production'", "production"),
("environment MISSING", None),
("environment='Production'", "Production"),
("environment='prod'", "prod")]:
print(f"{label:<28} {decide(DOC_POLICY, env):<8} {decide(FIXED_POLICY, env)}")
Output:
case docs policy allowlist fix
environment='staging' ALLOW ALLOW
environment='production' DENY DENY
environment MISSING ALLOW DENY <-- the bug
environment='Production' ALLOW DENY
environment='prod' ALLOW DENY
Suggested fix
Invert to an allowlist so anything unrecognised denies. 'unknown' then falls outside the permitted set and no other change is needed:
policies: `
permit(principal, action == Action::"deploy", resource)
when { context.session has environment &&
["staging", "dev"].contains(context.session.environment) };
`,
For callers who would rather fail loudly than silently deny, the alternative worth documenting alongside it is to throw in the enricher when the field is absent, paired with onError: 'deny'.
Worth noting the role-based access control example on the same page already gets this right — role defaults to 'none' and no permit matches 'none', so a missing role denies. The environment example is the one place the page teaches default-permit, so this is an inconsistency within the page rather than a disagreement about approach.
Happy to open a PR for the TypeScript and Python snippets if the allowlist framing looks right.
Spotted while reviewing the interventions docs with the team at @clevvi.
Summary
The environment-gating example on the Cedar Authorization docs page is fail-open. If
environmentis absent frominvocationState, thedeploytool is permitted — the example reads as a production guardrail but does not deny when it cannot establish where it is running.It also contradicts two claims the same page makes: "The design is fail-closed" and "Cedar uses default-deny semantics."
The code
cedar-authorization.ts#L204-L213(Python mirror:cedar-authorization.py#L249-L266):Two problems compound:
'unknown' != "production"istrue, so a missing environment permits the deploy. Same for any spelling that isn't byte-identical to"production"—"Production","prod","prod-us-east-1"all pass.has environmentguard is dead code. The enricher unconditionally setsenvironmentvia?? 'unknown', sohasis alwaystrue. It reads like a safety check but can never fire.Reproduction
Evaluating the doc's policy against the exact context shape the handler builds (
cedarpy, no SDK needed):Output:
Suggested fix
Invert to an allowlist so anything unrecognised denies.
'unknown'then falls outside the permitted set and no other change is needed:For callers who would rather fail loudly than silently deny, the alternative worth documenting alongside it is to throw in the enricher when the field is absent, paired with
onError: 'deny'.Worth noting the role-based access control example on the same page already gets this right —
roledefaults to'none'and nopermitmatches'none', so a missing role denies. The environment example is the one place the page teaches default-permit, so this is an inconsistency within the page rather than a disagreement about approach.Happy to open a PR for the TypeScript and Python snippets if the allowlist framing looks right.
Spotted while reviewing the interventions docs with the team at @clevvi.