Skip to content

feat: expose type-aware partial evaluation (TPE) as tpe_authorize - #109

Draft
h0rv wants to merge 3 commits into
k9securityio:mainfrom
h0rv:feat/tpe-authorize
Draft

feat: expose type-aware partial evaluation (TPE) as tpe_authorize#109
h0rv wants to merge 3 commits into
k9securityio:mainfrom
h0rv:feat/tpe-authorize

Conversation

@h0rv

@h0rv h0rv commented Aug 21, 2026

Copy link
Copy Markdown

Draft, stacked on #108. Read only the last commit, 409d6b6.

The branch carries #108's commit 02e359f underneath, because GitHub cannot base a pull request on a branch that lives on a fork. #108 merges first, then this rebases onto main and 02e359f drops out, leaving only the TPE change. Transitively includes and is blocked by #106.

What

Adds tpe_authorize, an entry point for type-aware partial evaluation (TPE). Use it when you know the type of the principal or resource but not which one, e.g. some User, not sure which, instead of a concrete User::"alice".

This is separate from is_authorized_partial. It does not call it, change it, or share its response shape. is_authorized_partial and residuals are untouched. The two partial-eval implementations in cedar-policy behave differently in a way that matters here. is_authorized_partial's residuals can contain untyped unknown(...) nodes that PST cannot represent, per #108 and #107. TPE's residuals are checked against the schema and do convert. Confirmed by calling to_pst() on real output from both paths.

principal and resource each accept Type::"id" (concrete) or a bare Type (known type, unknown id). TPE's PartialEntityUid needs a type even when the id is unknown, so is_authorized_partial's convention of None meaning wholly unknown does not carry enough information here. action must be concrete. schema is required, unlike is_authorized/is_authorized_partial where it is optional, because TPE builds its request and entity types against it. entities must be fully concrete; this does not expose TPE's on-demand entity loading.

The response is a TpeAuthzResult dataclass. permits and forbids stay separate, each a TpeClassification of residual_ids, true_ids, false_ids, error_ids. residual_policies maps policy id to a cedarpy.pst.Template, the same typed node #108 exposes, not JSON.

Every failure raises ValueError. Unlike is_authorized/is_authorized_partial, there is no decision to fall back to when the input itself cannot be resolved.

The tpe feature

Enables tpe alongside partial-eval in Cargo.toml. Checked the resolved lockfile before and after: no change. cedar-policy's tpe = ["cedar-policy-core/tpe"] and cedar-policy-core's tpe = [] both have no dependency edges. cedar-policy 4.12.0 puts partial-eval and tpe in the same experimental bucket, and cedarpy already ships partial-eval as its documented partial-authorization API.

Testing

Built with maturin develop --release on macOS arm64, Python 3.14.

Suite Result
pytest tests/unit 255 passed, 2 subtests (242 through #108 + 13 new)
pytest tests/integration/test_cedar_integration_tests.py 74 passed
pytest tests/integration/test_cedar_corpus_tests.py 59696 passed
git diff Cargo.lock empty
cargo clippy --release 10 warnings, all pre-existing on main, none in this diff

New tests cover a type-only resource, a concrete resource that resolves allow and deny, a satisfied forbid overriding a satisfied permit while the permit stays in true_ids, a residual forbid blocking an otherwise-true permit from becoming a decision, the residual as a typed node with a working pattern match on it, trivial residuals not repeated in residual_policies, is_authorized_partial behaving exactly as before, and error handling for a missing schema, an unparseable principal, a bare-type action, unparseable policies, and unparseable entities.

Not run: make benchmark-compare, same reason as #108.

Marked draft because it stacks on #108, which stacks on #106.

h0rv added 3 commits August 21, 2026 13:53
Cargo.toml already allows this (caret on 4.8.2); only the lock held it
back. No source changes were needed.

Transitive churn is Cedar's own: jiff/jiff-tzdb/jiff-tzdb-platform back
the datetime extension added upstream, and bs58, itertools, syn 3,
rustc-literal-escaper, portable-atomic-util and windows-sys 0.61.2 come
with it. beef, lazy_static, rustc_lexer and rustc_version drop out.
Adds policies_to_pst, which parses policy text into cedarpy.pst nodes:
frozen dataclasses, one per cedar_policy::pst node kind. A consumer
pattern-matches on real types instead of walking a dict and matching
string keys.

Rust walks pst::Expr once (matching what to_json_str already parses,
just to a different target) and constructs the corresponding dataclass
directly by calling its constructor from Rust. No JSON in between, no
second Rust converter: the typed tree is the only thing Rust builds.
dataclasses.asdict() and json.dumps() work on the result for free,
since the nodes are plain dataclasses.

Several pst types are #[non_exhaustive] upstream, so the matches on
them keep a wildcard arm regardless. It raises ValueError naming the
variant rather than building something silently wrong.

Static policies and unlinked templates only. A residual from
is_authorized_partial cannot convert this way: PST's own policy type
rejects any clause containing an unresolved unknown(...) node, and
every non-trivial residual has one. Confirmed by calling to_pst() on
real residuals, not from the changelog. is_authorized_partial and its
residuals field are untouched.

Closes part of k9securityio#107.
Adds tpe_authorize for a request whose principal and/or resource
identity is unknown but whose type is known. Separate from
is_authorized_partial, which it does not call, change, or share a
response shape with. TPE's residuals are checked against the schema
and convert to PST; is_authorized_partial's do not, for the reason the
previous commit found.

principal/resource accept Type::"id" or a bare Type, since TPE's
PartialEntityUid needs a type even when the id is unknown. action
must be concrete. schema is required. entities must be fully concrete.

The response is a TpeAuthzResult dataclass. permits/forbids stay
separate, each a TpeClassification of residual/true/false/error
policy ids. residual_policies maps id to a cedarpy.pst.Template, the
same typed node the previous commit exposes, not JSON. Reuses its
PstClasses/build_template rather than a second converter.

Enables the tpe Cargo feature. Checked the resolved lockfile: no
change, since neither crate's tpe feature has a dependency edge.

Every failure raises ValueError: unlike is_authorized/
is_authorized_partial, there is no decision to fall back to when the
input itself cannot be resolved.

Closes the rest of k9securityio#107.
@h0rv
h0rv force-pushed the feat/tpe-authorize branch from b4b516d to 409d6b6 Compare August 21, 2026 19:01
@h0rv h0rv closed this Aug 21, 2026
@h0rv h0rv reopened this Aug 21, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant