Skip to content

feat: Add support for partial evaluation#82

Merged
skuenzli merged 21 commits into
k9securityio:mainfrom
swenger:swenger-partial
May 28, 2026
Merged

feat: Add support for partial evaluation#82
skuenzli merged 21 commits into
k9securityio:mainfrom
swenger:swenger-partial

Conversation

@swenger
Copy link
Copy Markdown
Contributor

@swenger swenger commented May 21, 2026

Summary

Adds is_authorized_partial(request, policies, entities, schema=None), a Python binding for Cedar's partial-evaluation authorizer. Request fields that are None or absent are treated as unknowns; the authorizer returns Decision.Allow / Decision.Deny when the unknowns can't change the outcome, or Decision.NoDecision plus residual policies (as Cedar JSON) for the caller to re-evaluate once unknowns are bound.

PartialAuthzResult mirrors AuthzResult's shape (decision / correlation_id / diagnostics / metrics) and adds may_be_determining, must_be_determining, nontrivial_residuals, unknown_entities to diagnostics. There is no engine version change. cedarpy continues to use cedar-policy 4.8.2 with the partial-eval Cargo feature enabled.

The docstring and CHANGELOG both warn that partial-eval results are not a final authorization decision: callers must re-run is_authorized once unknowns are bound, since schema type-checking (including action-typed context shapes) is skipped while fields remain unknown.

Fixes #28.

Test plan

  • make integration-tests passes
  • pytest tests/unit -v — full suite passes, including 27 new tests in tests/unit/test_authorize_partial.py covering: unknowns at each request slot, definitive Allow/Deny with unknowns, schema interactions, errored-policy diagnostics, residual JSON structure, @id annotation surfacing (incl. on errored policies), and three "progressive request filling" cases that exercise partial → complete request transitions
  • make benchmark-compare — no regression on the post-merge benchmark gate
  • Manually verify a residual returned from is_authorized_partial can be inspected and acted on by a caller

@skuenzli
Copy link
Copy Markdown
Contributor

@swenger - thank you for this PR. I will take a look when I can. That may be in a couple days.

Can you share how you're using or planning to use this in your own application?

@swenger
Copy link
Copy Markdown
Contributor Author

swenger commented May 21, 2026

Thanks @skuenzli!

Can you share how you're using or planning to use this in your own application?

Sure! We want to control access to data that's stored in a SQL database. Especially for queries returning more than one result, that means that at least some of the access control needs to happen in SQL (keeping only the rows the user is allowed to see). In order to keep the amount of work in the WHERE clause manageable, all parts of the policy set that do not depend on the database row are going to be evaluated before building the query (using the code from this PR), and only the residuals that depend on the row are translated to SQL. We're likely going to perform one last pass with the full policy set on the returned rows 1) as defense in depth against possible mistakes in our SQL implementation and 2) to evaluate any conditions that depend on rows but cannot be translated to SQL.

Currently, implementing this would require starting the cedar CLI in a new process, which isn't great for latency and scalability (besides not being very convenient API to code against).

Does this sound reasonable?

@swenger swenger marked this pull request as ready for review May 21, 2026 17:19
@skuenzli
Copy link
Copy Markdown
Contributor

Thank you @swenger - your use case is great context and I understand it completely -- to the extent I need to ;)

Copy link
Copy Markdown
Contributor

@skuenzli skuenzli left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for proposing an implementation to support partial evaluation. Overall, I think is_authorized_partial uses a good approach.

I've added a several comments and requests.

If we can work through those, then I think we can land this change.

Comment thread Cargo.toml Outdated
Comment thread cedarpy/__init__.py
Comment thread cedarpy/__init__.py Outdated
Comment thread tests/unit/test_authorize_partial.py
@swenger swenger requested a review from skuenzli May 26, 2026 15:53
Comment thread Cargo.toml
Comment thread src/lib.rs
@swenger swenger requested a review from skuenzli May 27, 2026 08:41
@skuenzli
Copy link
Copy Markdown
Contributor

@swenger - thanks for the updates and your patience. I wasn't able to review the changes today, but will try to review it tomorrow.

skuenzli and others added 7 commits May 27, 2026 21:24
PR was branched from an older main and had transitive-dep churn unrelated
to partial-eval. Restore the lock to main's state to keep the PR's
dependency-change surface aligned with its scope.

Cedar-policy and its family already resolve to 4.8.2 on main, so no
manifest pin is required.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The partial-eval response populated id_annotations_by_reason from
definitely_satisfied and all_residuals, but not from definitely_errored.
Errored policies are in neither of the first two sets, so callers
inspecting diagnostics.errors had no way to recover the @id label of
a policy that errored at evaluation.

Add a third lookup loop over errored_ids alongside the existing two,
and a parallel test covering both satisfied and errored policies with
@id annotations.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Documents the remediation for the security review finding on
is_authorized_partial: schema-based context type-checking is skipped
for fields that remain unknown (notably, action-typed context shapes
when action is unbound), so callers must re-run is_authorized once
unknowns are bound rather than treating a residual or a partial Allow
as a final decision.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Resolves CHANGELOG.md conflict by keeping both Unreleased entries in
Keep a Changelog section order (Added, then Changed).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@skuenzli
Copy link
Copy Markdown
Contributor

No changes in performance of existing codepaths were expected.

The existing benchmark suite passes, verifying that:

===== HEAD: cae8ad3 Merge branch 'main' into swenger-partial =====
===== RUNS=5  OUT_DIR=tests/benchmark/results/current =====
---- building (release) at 22:47:43 ----
📦 Built wheel for CPython 3.11 to /var/folders/k2/tnw8n1c54tv8nt4557pfx3440000gp/T/.tmpGYw3js/cedarpy-4.8.3-cp311-cp311-macosx_11_0_arm64.whl
✏️ Setting installed package as editable
🛠 Installed cedarpy-4.8.3
  [22:47:45] run 1 -> tests/benchmark/results/current/run1.json
  Outliers: 1 Standard Deviation from Mean; 1.5 IQR (InterQuartile Range) from 1st Quartile and 3rd Quartile.
  OPS: Operations Per Second, computed as 1 / Mean
============================= 26 passed in 24.74s ==============================
  [22:48:10] run 2 -> tests/benchmark/results/current/run2.json
  Outliers: 1 Standard Deviation from Mean; 1.5 IQR (InterQuartile Range) from 1st Quartile and 3rd Quartile.
  OPS: Operations Per Second, computed as 1 / Mean
============================= 26 passed in 25.19s ==============================
  [22:48:36] run 3 -> tests/benchmark/results/current/run3.json
  Outliers: 1 Standard Deviation from Mean; 1.5 IQR (InterQuartile Range) from 1st Quartile and 3rd Quartile.
  OPS: Operations Per Second, computed as 1 / Mean
============================= 26 passed in 25.07s ==============================
  [22:49:01] run 4 -> tests/benchmark/results/current/run4.json
  Outliers: 1 Standard Deviation from Mean; 1.5 IQR (InterQuartile Range) from 1st Quartile and 3rd Quartile.
  OPS: Operations Per Second, computed as 1 / Mean
============================= 26 passed in 24.46s ==============================
  [22:49:26] run 5 -> tests/benchmark/results/current/run5.json
  Outliers: 1 Standard Deviation from Mean; 1.5 IQR (InterQuartile Range) from 1st Quartile and 3rd Quartile.
  OPS: Operations Per Second, computed as 1 / Mean
============================= 26 passed in 24.54s ==============================
===== DONE at 22:49:51 =====

Benchmark                            Baseline (μs)  Median (μs)   Δ median  Status
-----------------------------------  -------------  -----------  ---------  ------
test_batch_complex_policy                     1234         1283      +4.0%  PASS
test_batch_simple_policy                       384          385      +0.2%  PASS
test_batch_size_scaling[100]                  4112         4034      -1.9%  PASS
test_batch_size_scaling[10]                   1105         1072      -3.0%  PASS
test_batch_size_scaling[1]                     799          789      -1.3%  PASS
test_batch_size_scaling[25]                   1595         1572      -1.4%  PASS
test_batch_size_scaling[50]                   2430         2374      -2.3%  PASS
test_batch_size_scaling[5]                     933          912      -2.3%  PASS
test_complex_policy                            273          269      -1.5%  PASS
test_context_as_dict                           802          790      -1.6%  PASS
test_context_as_json_string                    799          790      -1.1%  PASS
test_entities_as_json_string                   727          710      -2.4%  PASS
test_entities_as_list                          796          788      -1.0%  PASS
test_large_entity_set                         3875         3930      +1.4%  PASS
test_medium_entity_set                         795          795      +0.0%  PASS
test_medium_policy                             187          184      -1.3%  PASS
test_sandbox_b_batch_mixed_actions            1098         1115      +1.5%  PASS
test_sandbox_b_batch_multiple_users            683          684      +0.1%  PASS
test_sandbox_b_delete_with_auth                574          573      -0.1%  PASS
test_sandbox_b_view_own_photo                  574          585      +1.9%  PASS
test_sandbox_b_view_public_photo               576          588      +2.1%  PASS
test_simple_policy_allow                       144          142      -1.5%  PASS
test_simple_policy_deny                        143          143      -0.3%  PASS
test_small_entity_set                          187          188      +0.4%  PASS
test_ten_batch_call                           1098         1079      -1.7%  PASS
test_ten_single_calls                         8243         8121      -1.5%  PASS

N=5 current runs vs tests/benchmark/results/baseline.json; threshold: median Δ > 5% fails
RESULT: PASS

Copy link
Copy Markdown
Contributor

@skuenzli skuenzli left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for making the requested adjustments! I have also clarified and fixed a few things. I think this PR is ready to merge.

Notably, I restored the Cargo.lock from main to avoid picking up some dependency changes that were introduced early in your branch. The result is that there are no dependency changes except enabling cedar-policy 4.8.2's partial-eval feature.

Please review the full PR including my changes. Then either resolve or add/update conversations if you think we need to make more changes.

Comment thread cedarpy/__init__.py
Comment thread src/lib.rs Outdated
@swenger
Copy link
Copy Markdown
Contributor Author

swenger commented May 28, 2026

Thanks @skuenzli! I've made one minor change for you to review (see #82 (comment)) and left another question (#82 (comment)), but generally all looks good to me. Feel free to merge if you agree!

I've updated my SQL generation code to use the latest version (with nontrivial residuals etc.) and it seems to work fine 👍

Grounds the is_authorized_partial docstring warning with a concrete
test. When schema is provided but action is unknown, Cedar's
action-specific context type check is not performed, and ill-typed
context values (a string where the schema requires Boolean) pass
through without a diagnostic.

The test deliberately uses a policy that doesn't constrain action,
so the result is Decision.Allow despite the unknown action — making
the missing type check the actually-surprising part.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@skuenzli
Copy link
Copy Markdown
Contributor

Thanks @skuenzli! I've made one minor change for you to review (see #82 (comment)) and left another question (#82 (comment)), but generally all looks good to me. Feel free to merge if you agree!

I've updated my SQL generation code to use the latest version (with nontrivial residuals etc.) and it seems to work fine 👍

That's terrific! Thank you for testing.

@swenger swenger requested a review from skuenzli May 28, 2026 18:54
swenger and others added 2 commits May 28, 2026 21:01
…bling

`test_partial_with_schema_unknown_action_skips_context_type_check_when_complete`
inverted its scenario in the name. Rename to
`test_partial_with_schema_known_action_enforces_context_type_check` so the
two paired tests read as a clear input/outcome contrast:

  unknown action  →  skips context type check
  known action    →  enforces context type check

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor

@skuenzli skuenzli left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This addition looks great. Thank you for your contribution and patience iterating on the solution.

@skuenzli skuenzli merged commit 49beb5e into k9securityio:main May 28, 2026
7 checks passed
@swenger
Copy link
Copy Markdown
Contributor Author

swenger commented May 29, 2026

Thanks @skuenzli for the review and great collaboration! I think what we merged is indeed much better than what I started out with ❤️

@skuenzli
Copy link
Copy Markdown
Contributor

@swenger - I'm so glad you feel that way. I think so too!

"Simple" isn't easy!

@skuenzli skuenzli mentioned this pull request May 29, 2026
4 tasks
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.

Support for partial evaluation?

2 participants