Skip to content

[DOCS] Cedar environment-gating example is fail-open when environment is unset #3695

Description

@jrdeck

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:

  1. 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.
  2. 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 rightrole 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    Language

    None yet

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions