Skip to content

Make Cedar group entity type name configurable#5121

Open
jhrozek wants to merge 1 commit intomainfrom
worktree-issue-5072-cedar-configurable
Open

Make Cedar group entity type name configurable#5121
jhrozek wants to merge 1 commit intomainfrom
worktree-issue-5072-cedar-configurable

Conversation

@jhrozek
Copy link
Copy Markdown
Contributor

@jhrozek jhrozek commented Apr 29, 2026

Summary

  • The Cedar authorizer hardcoded EntityTypeTHVGroup = "THVGroup" as the entity type used to construct principal Parents UIDs from JWT group/role claims. Deployments whose policy vocabulary doesn't use "THVGroup" had no way to switch without patching the authorizer.
  • Add an optional GroupEntityType field to cedar.ConfigOptions so deployments can use names like OrgRole or Group that fit their policy vocabulary. Empty defaults to "THVGroup", so existing deployments are unaffected.
  • Validate non-empty values up front in NewCedarAuthorizer by round-tripping through cedar-go's policy parser. This delegates Cedar's identifier grammar to upstream — reserved words, ANYIDENT shape, the __cedar token — so future cedar-go refinements land automatically. The one project-specific rule we keep is rejecting the :: namespace separator (namespaced types are out of scope until parseCedarEntityID handles them).
  • Warn at construction when entities_json contains stale entities of type "THVGroup" while GroupEntityType is set to a different value. The mismatch causes Cedar's in operator to silently evaluate to false on those entities — a silent deny that is otherwise hard to debug.

Closes #5072

Type of change

  • Bug fix
  • New feature
  • Refactoring (no behavior change)
  • Dependency update
  • Documentation
  • Other (describe):

Test plan

  • Unit tests (task test)
  • Linting (task lint-fix)

No e2e tests added — the new field is consumed today only via the standalone CLI's --authz-config <file> path. The operator and vMCP construct cedar.ConfigOptions by hand and don't yet surface the field; that's tracked as a separate follow-up.

API Compatibility

  • This PR does not break the v1beta1 API. The new field lives on the inline Cedar ConfigOptions struct, which CRD consumers pass through as opaque YAML/JSON — no CRD schema change.

Changes

File Change
pkg/authz/authorizers/cedar/core.go Add GroupEntityType field to ConfigOptions; add validateGroupEntityType helper that delegates to cedar-go's policy parser; wire validation + factory construction into NewCedarAuthorizer; emit silent-deny warn diagnostic
pkg/authz/authorizers/cedar/entity.go Make EntityFactory stateful with a groupEntityType field; change NewEntityFactory signature to accept the type (empty defaults to EntityTypeTHVGroup); replace the hardcoded constant in CreateEntitiesForRequest with the configured value
pkg/authz/authorizers/cedar/core_test.go Validator unit test, constructor wiring proof, permit/deny end-to-end contrast, stale-THVGroup warn diagnostic test
pkg/authz/authorizers/cedar/entity_test.go Custom-entity-type test plus updated call sites for the new NewEntityFactory signature

Does this introduce a user-facing change?

Yes. Users with a YAML/JSON authz config (thv run --authz-config <file>) can now set an optional group_entity_type: field to any valid Cedar identifier. Empty (or absent) keeps the existing "THVGroup" behaviour. Invalid values fail at startup with a message naming the offending input — namespace separators (::), Cedar reserved words, and identifier-grammar violations are all rejected.

Implementation plan

Approved implementation plan

Context

pkg/authz/authorizers/cedar/entity.go hardcoded EntityTypeTHVGroup cedar.EntityType = "THVGroup". EntityFactory.CreateEntitiesForRequest used it to build Client.Parents UIDs from JWT group/role claims. Existing deployments couldn't rename the principal-hierarchy entity type without a code change.

Approach

Add an optional GroupEntityType field to ConfigOptions (json:"group_entity_type,omitempty"). Make EntityFactory stateful so the configured type is resolved once at construction. Keep EntityTypeTHVGroup as the default constant and fallback. The wire type stays string; NewEntityFactory takes a cedar.EntityType so callers cast at the boundary.

Validate non-empty input in NewCedarAuthorizer before allocation. Originally the validator hand-rolled the rules (regex + reserved-word map + __cedar prefix check); the final form delegates to cedar.Policy.UnmarshalCedar against a synthetic permit(principal in <TYPE>::"x", action, resource); policy. Reasoning:

  • cedar-go's parser is the authoritative grammar.
  • Future Cedar grammar refinements upstream land automatically.
  • Empty input is not a misconfiguration — it explicitly means "use the default" and is accepted.
  • The :: namespace rejection stays as a project-specific rule (issue text defers namespaced types).

Add a startup WARN log when entities_json contains entities of type "THVGroup" while GroupEntityType is configured to a different non-empty value. Cedar's in would silently evaluate to false on those stale entities; the diagnostic surfaces the mismatch so operators don't spend hours debugging a silent deny. Scoped to entities_json — policy-text scanning was rejected as fragile.

Out of scope

  • Namespaced entity types (Foo::Bar). Blocked by parseCedarEntityID. Tracked for follow-up.
  • Surfacing GroupEntityType (and the existing GroupClaimName / RoleClaimName) through the operator's AuthzConfigRef.Inline and vMCP's AuthzConfig boundaries. Both layers currently construct cedar.ConfigOptions by hand and forward only a subset of fields. Tracked for a separate PR.

Files modified

  • pkg/authz/authorizers/cedar/core.go (production)
  • pkg/authz/authorizers/cedar/entity.go (production)
  • pkg/authz/authorizers/cedar/core_test.go (test)
  • pkg/authz/authorizers/cedar/entity_test.go (test)

Special notes for reviewers

  • cedar-go delegation in the validator: rather than hand-rolling Cedar's identifier grammar, the validator round-trips a synthetic permit() policy through cedar.Policy.UnmarshalCedar. That makes cedar-go the source of truth for "is this a valid Cedar identifier" and means we don't have to track upstream grammar changes. The one project-specific rule we keep is rejecting ::. The behavioural consequence vs a strict hand-rolled version is that __cedarFoo is now accepted — the Cedar spec only reserves the literal __cedar token, not the entire prefix namespace.
  • Silent-deny diagnostic at construction: scoped to entities_json only. Policy-text scanning was considered and rejected — false positives during phased migrations are likely, and the policy parser has no schema-aware type check. The entities-json scan is deterministic (entity types in JSON are explicit) and runs once at startup.
  • Test coverage philosophy: TestValidateGroupEntityType deliberately covers our package's branches (empty short-circuit, :: rejection, valid/invalid smoke tests, the __cedarFoo acceptance) rather than enumerating every input cedar-go's parser would reject. The exhaustive grammar coverage lives in cedar-go's test suite — re-asserting it here would test a dependency, not our code.

🤖 Generated with Claude Code

The Cedar authorizer hardcoded EntityTypeTHVGroup ("THVGroup") as the
entity type used to construct Client.Parents UIDs from JWT group/role
claims. Deployments whose policy vocabulary doesn't use "THVGroup"
had no way to switch without patching the authorizer.

Add an optional GroupEntityType field to ConfigOptions and thread it
through EntityFactory so deployments can use names like OrgRole or
Group that fit their policy vocabulary. Empty defaults to "THVGroup"
so existing deployments are unaffected.

Validate non-empty values up front in NewCedarAuthorizer, per the
constructor-validation rule in .claude/rules/go-style.md, so a typo
fails at startup with a message naming the offending input rather
than producing broken entity lookups at request time. Delegate the
identifier-grammar check to cedar-go's policy parser by round-tripping
a synthetic permit() against the configured type — Cedar's parser is
the source of truth for what a valid identifier looks like, and any
future grammar refinements upstream land here automatically. Reject
the "::" namespace separator ourselves: namespaced types are out of
scope until parseCedarEntityID handles them.

Warn at construction when entities_json contains entities of type
"THVGroup" while GroupEntityType is configured to a different value.
The mismatch causes Cedar's "in" operator to evaluate to false on
those stale entities — a silent deny that is hard to debug without
this diagnostic.

Closes #5072
@github-actions github-actions Bot added the size/M Medium PR: 300-599 lines changed label Apr 29, 2026
@codecov
Copy link
Copy Markdown

codecov Bot commented Apr 29, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 67.13%. Comparing base (4c23ade) to head (0de7deb).

Additional details and impacted files
@@           Coverage Diff           @@
##             main    #5121   +/-   ##
=======================================
  Coverage   67.12%   67.13%           
=======================================
  Files         597      597           
  Lines       60170    60192   +22     
=======================================
+ Hits        40390    40408   +18     
- Misses      16706    16712    +6     
+ Partials     3074     3072    -2     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/M Medium PR: 300-599 lines changed

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(authz): make Cedar group entity type name configurable via ConfigOptions

1 participant