Repository navigation
feat(tpe): wire type-aware partial evaluation into the authorization engine - #372
Open
muditchaudhary wants to merge 1 commit into
Open
muditchaudhary wants to merge 1 commit into
muditchaudhary wants to merge 1 commit into
Conversation
…engine Signed-off-by: Mudit Chaudhary <chmudit@amazon.com>
muditchaudhary
marked this pull request as ready for review
October 1, 2026 21:39
| */ | ||
| @Experimental(ExperimentalFeature.TYPE_AWARE_PARTIAL_EVALUATION) | ||
| public AuthorizationResponse reauthorize(AuthorizationRequest request, | ||
| com.cedarpolicy.model.entity.Entities entities) throws AuthException { |
Contributor
There was a problem hiding this comment.
Nit importing com.cedarpolicy.model.entity.Entities above
mark-creamer-amazon
approved these changes
Oct 6, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Wire type-aware partial evaluation into the authorization engine
Summary
Adds
isAuthorizedTypeAwarePartialandreauthorizetoAuthorizationEngine, with the nativehandlers behind them. Completes the TPE surface begun in #370 (entity model) and #371 (request and
response types). Experimental, and the JSON contract between Java and Rust is CedarJava-owned and
unstable.
What
AuthorizationEngine.isAuthorizedTypeAwarePartial(request, policySet, entities)— two overloads,one taking
PartialEntities, one lifting fully-knownEntities. Bothdefault, so existingimplementors still compile.
TypeAwarePartialAuthorizationSuccessResponse.reauthorize(request, entities)— completes anauthorization once the unknowns are known. Runs natively; residuals never cross back.
getErroredResidualIds()/getErroredResiduals()— residuals containing a subexpression Cedaralready knows would error. Source-only.
getPolicySet()removed. Residuals are inspection-only.tpefeature is off, so acaller gets
MissingExperimentalFeatureExceptionrather than a deserialization error.Why
Residuals travel as source, not EST. Cedar renders a residual whose evaluation errors using a
synthetic
errornode that Cedar itself cannot parse back — in EST or in source form. So anydesign that re-imports Cedar's own output for such a residual fails, and because the policy set is
parsed in one call, one unparseable policy fails the entire response. Sending source keeps the
rendering faithful and lets Java hold it without parsing.
Residuals are inspection-only, so
getPolicySet()is gone. A residual has the originalrequest's values folded in — one computed for
User::"alice"literally containsUser::"alice"inits condition. Authorizing it for
User::"bob"grants Bob access only Alice had, silently and withno error.
getPolicySet()existed only inUnreleased, so removing it breaks no released API.Completion is native. The success response carries the inputs its residuals were computed from,
so
reauthorizecannot be handed partial inputs that disagree with what the caller inspected. Cedarthen validates the completed request and entities against them and rejects a mismatch — which
authorizing a residual set cannot do, because it has no partial request to compare against.
Errored ids come from the PST, via
Policy::to_pst()+pst::Expr::has_error(), noterror_permits()/error_forbids(). Those report only policies that reduced entirely to an errorand miss one nested inside a live condition, which is still unparseable. It also happens to be the
only detection available on Cedar 4.11, which has no error buckets at all.
Testing notes
Expectations in the new tests were derived by running the same fixtures through Cedar Rust directly,
not by observing CedarJava — several earlier tests were found passing for the wrong reason. A
throwaway differential harness compared 152 cases across both implementations (residual ids, residual
source byte-for-byte, the trivial/non-trivial/errored classification, and every reauthorization
outcome) at zero divergences. Not committed.
API surface
Everything a caller touches.
#370/#371columns mark types that landed earlier and are listed forcompleteness; the rest is new here.
Entry points —
AuthorizationEngineisAuthorizedTypeAwarePartial(request, policySet, PartialEntities)isAuthorizedTypeAwarePartial(request, policySet, Entities)reauthorize(request, policySet, partialEntities, concreteRequest, entities)TypeAwarePartialAuthorizationSuccessResponse.reauthorize, which supplies the first three from the response so they cannot disagree with the residuals you inspected.All three are
defaultand throwUnsupportedOperationExceptionunless the engine implements them, soadding them broke no existing implementor.
Building a request —
TypeAwarePartialAuthorizationRequest.builder().principal(EntityUID)/.resource(EntityUID).principal(EntityTypeName)/.resource(EntityTypeName).principal(PartialEntityUID)/.resource(PartialEntityUID).action(EntityUID).context(Map<String, Value>)/.context(Context).emptyContext().schema(Schema).build()InternalExceptionif it does not typecheck.Partially known entities
new PartialEntity(euid, Optional attrs, Optional parents, Optional tags, schema)Optional.empty()for a field means unknown; a present value means complete and final — a presentattrsmust carry every required attribute.new PartialEntity(Entity, schema)new PartialEntities(Set<PartialEntity>, schema)new PartialEntities(Entities, schema)PartialEntities.empty()new PartialEntityUID(type)/(type, id)Reading the response —
TypeAwarePartialAuthorizationResponsegetType()SuccessorFailure. Check this first — a policy set Cedar rejects arrives as aFailure, not an exception.getSuccess()Success.getErrors()DetailedErrors, present iffFailure. For a policy-set validation failure the actual reason is in each error'srelated.getWarnings()TpeResponseexposes none.Reading a success —
TypeAwarePartialAuthorizationSuccessResponsegetDecision()nullif TPE could not reach one.getResiduals()getNontrivialResiduals()getTrivialResiduals()getResiduals()with the above.getNontrivialResidualIds()getErroredResidualIds()getErroredResiduals()getSource(),getID(),toString()work;effect(),toJson(),getAnnotations(),getAnnotation()all throw.reauthorize(concreteRequest, entities)toString()Using it
All examples use this schema:
and this policy, which cannot be decided until
principal.departmentis known:1. Happy path — everything known, a decision comes straight back
Note the residual is still there with the condition folded to
true. Every policy produces aresidual, including ones that fully resolved.
2. Check for a failure before unwrapping
A policy set that authorizes fine can fail here, because TPE validates it against the schema and
plain authorization does not. An unconstrained scope means the condition must typecheck for every
entity type the action accepts, and an optional attribute needs a
hasguard.3. Something unknown — a residual comes back instead of a decision
Two shapes, and the difference matters.
Unknown principal id —
principalstays symbolic:Known id, unknown attributes — the entity is substituted into the condition:
User::"alice"is now hard-coded into the condition. This is why residuals must not be authorizedfor a different principal — evaluating that text for Bob reads Alice's attributes.
Omitted versus empty
Omitting a field means unknown; supplying it empty means known to be empty. They give
different answers:
The same rule applies to
parents,tags, and the request context — omitcontext()for unknown,call
emptyContext()for known-empty.4. Reauthorize — complete it once the unknowns are known
You do not pass the policy set or partial entities again — the response carries them, so they cannot
disagree with the residuals you just inspected.
A mismatched completion is rejected, not answered:
Cedar checks the principal, resource, action and context, and for every entity the partial set knew
about it compares the whole attribute map, the whole tag map, and the ancestor closure. Entities you
add are not checked.
5. Errored residuals
A policy whose evaluation errors — an integer overflow, say — still produces a residual, and Cedar
still reaches a decision:
These residuals are source-only: anything that re-parses the policy natively throws, because
Cedar cannot read its own error marker back. A clean residual has no such limitation:
Containing an error node does not mean the policy errors. If the error sits in a branch that
short-circuits away, the policy decides normally:
So the errored set cuts across the trivial/non-trivial split rather than being a third partition.
Reauthorization handles all of this, because it evaluates the real residual natively rather than
Cedar's rendering of it:
One shape worth knowing: an erroring forbid is skipped, so a trivially-true permit still wins —
Allowalongside a non-zero error count.6. If the native library lacks the feature
isAuthorizedTypeAwarePartial,reauthorize, and thePartialEntity/PartialEntitiesconstructorsall throw
MissingExperimentalFeatureException, naming the flag to rebuild with: