feat(security): Spring Security parity — Tier 3 method-security depth (v26.6.32)#40
Merged
Merged
Conversation
added 5 commits
June 19, 2026 19:23
…inding) [T3.1] Extend #[pre_authorize] so a non-keyword argument is a boolean Rust expression evaluated BEFORE the body with the function's parameters and `auth` (a &Authentication) in scope — the Rust analog of Spring's @PreAuthorize("#id == authentication.name"). A false expression denies with Forbidden; no ambient context denies with Unauthenticated (fail-closed). - classify_rule() recognizes the keyword forms (authenticated / role / any_role / authority / any_authority) and returns None for anything else, which is then parsed as a syn::Expr and lowered to a bound check. Fully backward compatible — existing keyword rules are unchanged. - Composes with auth's methods (auth.has_role("ADMIN") || auth.principal == owner) and, next, has_permission(...) for PermissionEvaluator. 2 new e2e tests (argument+principal binding; role-or-ownership); the 5 existing method-security tests still pass. fmt + clippy clean.
Add the Rust analog of Spring's PermissionEvaluator / SpEL hasPermission(target, permission): - PermissionEvaluator trait: has_permission(auth, target: &dyn Any, permission) -> bool; a single registered evaluator serves every domain type by downcasting (mirrors Spring's reflective contract, type-safe at the call site). - Process-wide set-once registry (set_permission_evaluator over a OnceLock) and a has_permission<T: Any>(auth, target, permission) helper that delegates. Secure default: with no evaluator registered, every permission is DENIED (fail-closed). - Composes with T3.1: callable directly inside #[pre_authorize]/#[post_authorize] expressions (they bind auth), e.g. #[pre_authorize(firefly::security::has_permission(auth, account, "read"))]. 3 tests (downcast grant/deny incl. unknown type+permission; global default-deny → delegate → set-once) + an end-to-end macro test driving hasPermission through #[pre_authorize]. fmt + clippy clean.
Add Spring Security's @PostFilter / @PreFilter: - #[post_filter(<expr>)] on an async fn returning Result<C, E>: after the body, retains only the elements of the returned collection (Vec<T> or any type with retain(FnMut(&T) -> bool)) for which <expr> over `element` (a &T, Spring's filterObject) and `auth` is true. No ambient context denies the whole call with Unauthenticated (fail-closed). - #[pre_filter(<param>, <expr>)]: before the body, filters the named owned `mut` collection parameter in place by the same predicate. Returns Result for the Unauthenticated `?`. Re-exported through the firefly facade (firefly::post_filter / firefly::pre_filter). 4 new e2e tests (post_filter per-caller retain + unauthenticated; pre_filter drops-before-body + unauthenticated); all 10 method-security tests green. fmt + clippy clean.
Adversarial multi-agent review of the Tier 3 method-security surface surfaced 8 confirmed findings (1 high, 2 medium, 5 low); fixes: - Parameter-shadowing (HIGH, fail-open): a user parameter named `auth`, `result`, or `element` was silently shadowed by the macro's injected binding, so the rule could authorize against the framework value instead of the argument (the `result` case granted against the return value with no compile error). All four macros now reject a colliding parameter (and the internal `__`-temporaries) at expansion time via reject_reserved_params, with a clear rename diagnostic. Three trybuild compile-fail fixtures lock this. - Permission &dyn Any footgun (MEDIUM) + module/has_permission/set_evaluator docs: document that has_permission takes a reference to the OWNED domain value (an accidental &&T has a different TypeId and silently denies — fail-closed), that a registration race means a different evaluator is authoritative, and that the Any erasure is sound but not a compile-time recognition guarantee. - async move move-out (MEDIUM) + pre_authorize move (LOW): document that post_authorize/post_filter predicates should reference only result/element + auth (a non-Copy param the body consumes can't be borrowed after), and that pre_authorize expressions must borrow (not move) their parameters. - classify_rule fail-soft (LOW) + missing compile-fail coverage (LOW): added trybuild fixtures pinning that a malformed keyword rule (role = 42) fails closed with the expect_str diagnostic. 10 method-security tests + trybuild (3 new fixtures) + 2 permission tests green; fmt + clippy clean.
- Spring Security Parity appendix (EN + ES): mark method security as supporting SpEL-style expressions; add a "method-security depth" matrix row (@PreFilter/@PostFilter + PermissionEvaluator) and a "Method security" section covering argument/principal binding, has_permission, and pre/post filtering; mark the method-security-depth tier done in the roadmap. - CHANGELOG: v26.6.32 entry (Tier 3). MODULES.md: firefly-macros row now lists method-security attributes; firefly-security row lists PermissionEvaluator. - Bump workspace + path-dep versions to 26.6.32; refresh Cargo.lock. - Rebuild and republish both editions (PDF + EPUB, EN + ES) in docs/book/dist.
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.
Spring Security parity — Tier 3: method-security depth
The SpEL-equivalent layer over the existing
#[pre_authorize]/#[post_authorize]macros. All additive — no behaviour change to existing code. Adversarially
reviewed before release.
Added
#[pre_authorize]— a non-keyword argument is a booleanRust expression evaluated before the body with the method's parameters and
auth(&Authentication) in scope (Spring's@PreAuthorize("#id == authentication.name")).Keyword rules unchanged + backward compatible. Fail-closed.
PermissionEvaluator+has_permission— Spring'sPermissionEvaluator/hasPermission(target, permission). Process-wideset_permission_evaluator;has_permission(auth, target, permission)usable inside pre/post expressions.Secure default: deny when no evaluator is registered.
#[pre_filter]/#[post_filter]— collection filtering (Spring's@PreFilter/@PostFilter);elementis the per-element&T. No ambientcontext denies with
Unauthenticated.Adversarial review fixes (8 confirmed findings)
auth/result/elementwas silentlyshadowed by the injected binding (the
resultcase authorized the wrong value).All four macros now reject a colliding parameter at expansion time (+ 3 trybuild
compile-fail fixtures lock it).
&dyn Any&&T-silent-deny footgun, theasync movemove-out limitation (post predicates →
result/element+auth), theno-move-in-
pre_authorize-expression rule, and the set-once evaluator race;fail-closed
role = 42compile-fail fixture.Tests & docs
full macros/security suites green;
fmt+clippyclean.icons), book republished (PDF + EPUB, both editions). CHANGELOG v26.6.32; MODULES.md.
Version bumped to 26.6.32.