feat: expose the PST as typed nodes via policies_to_pst - #108
Draft
h0rv wants to merge 2 commits into
Draft
Conversation
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.
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.
This includes and is blocked by #106. It cannot merge before that one does. Base is main; the diff to review is the last commit only.
What
Adds
policies_to_pst(policies) -> cedarpy.pst.PolicySet. It parses Cedar policy text and returns the PST as typed nodes. Each node kind is a frozen Python dataclass, one percedar_policy::pstnode. A consumer pattern-matches on real types:Rust builds these nodes directly. It walks
pst::Expronce and, for each variant, calls the matching Python dataclass constructor from Rust with the child nodes already built. There is no JSON step and no second walker.dataclasses.asdict()andjson.dumps()already work on the result, since the nodes are plain dataclasses.Why a typed model instead of JSON
Cedar already checked this on the Rust side. A clause cannot contain a slot. A clause cannot contain an unknown. Every expression is well formed. A JSON string throws that away and makes every consumer re-check it, usually wrong, by matching on string keys. A typed object carries the guarantee in the type, so there is nothing left to re-check.
What this does not cover
Static policies and unlinked templates only. A residual from
is_authorized_partialcannot be represented this way.pst::Template's own constructor rejects any clause containing an unresolvedunknown(...)node, and every non-trivial residual fromis_authorized_partialhas one. I confirmed this by callingto_pst()on real residuals for a permit and a forbid; both raised. The claim in the changelog that residuals convert to PST is true only for the separatetpefeature. See #107.is_authorized_partialandresidualsare untouched.pyo3 notes
pst::Exprand several of its operator enums are#[non_exhaustive]upstream, so the Rust match on them needs a wildcard arm regardless of what this PR wants. That arm raisesValueErrornaming the variant. It should not fire against this cedar-policy version; it exists so a future engine bump that adds a variant fails loudly instead of building something wrong.I looked at pyo3 complex enums first (
#[pyclass]on a Rust enum with struct variants). They work, including the recursive case withPy<T>fields, and pyo3 auto-generates__match_args__so both keyword and positional patterns work. I did not use them here: calling a plain Python class constructor from Rust gives real dataclasses with no intermediate representation, andasdict/json.dumpscome for free. A pyo3 complex enum would need a hand-written dict conversion instead.One thing I ran into:
types.MappingProxyTypebreaksdataclasses.asdict()(it callscopy.deepcopy, which cannot pickle amappingproxy). Mapping fields (annotations,templates,static_policies, slotvalues) are plaindict, typed asMappingin the annotation to say they should not be mutated, without runtime enforcement.Testing
Built with
maturin develop --releaseon macOS arm64, Python 3.14.pytest tests/unitpytest tests/integration/test_cedar_integration_tests.pypytest tests/integration/test_cedar_corpus_tests.pycargo clippy --releaseNew tests cover every clause and scope constraint shape, every literal kind, set and record,
has(including the multi-attribute path, checked against real parser output:resource has a.blowers to two chained single-attribute nodes, not one node with two attrs),like,is/is in,if/then/else, several operators, a template with a slot, keyword and positional structural pattern matching,asdict/json.dumps, frozen enforcement, and error handling.Not run:
make benchmark-compare. The build machine was busy throughout, and the repo's own guidance says the comparison is load-sensitive.Marked draft because it depends on #106.