Skip to content

feat(security): Spring Security parity — Tier 3 method-security depth (v26.6.32)#40

Merged
ancongui merged 5 commits into
mainfrom
feat/spring-security-tier3-method-security
Jun 19, 2026
Merged

feat(security): Spring Security parity — Tier 3 method-security depth (v26.6.32)#40
ancongui merged 5 commits into
mainfrom
feat/spring-security-tier3-method-security

Conversation

@ancongui

Copy link
Copy Markdown
Contributor

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

  • Expression-based #[pre_authorize] — a non-keyword argument is a boolean
    Rust 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's PermissionEvaluator /
    hasPermission(target, permission). Process-wide set_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); element is the per-element &T. No ambient
    context denies with Unauthenticated.

Adversarial review fixes (8 confirmed findings)

  • HIGH (fail-open): a parameter named auth/result/element was silently
    shadowed by the injected binding (the result case authorized the wrong value).
    All four macros now reject a colliding parameter at expansion time (+ 3 trybuild
    compile-fail fixtures lock it).
  • Documented the &dyn Any &&T-silent-deny footgun, the async move
    move-out limitation (post predicates → result/element+auth), the
    no-move-in-pre_authorize-expression rule, and the set-once evaluator race;
    fail-closed role = 42 compile-fail fixture.

Tests & docs

  • 10 method-security e2e tests + trybuild (3 new fixtures) + 2 permission tests +
    full macros/security suites green; fmt + clippy clean.
  • Book Spring Security Parity appendix updated (EN + ES, professional SVG status
    icons), book republished (PDF + EPUB, both editions). CHANGELOG v26.6.32; MODULES.md.

Version bumped to 26.6.32.

Andres Contreras 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.
@ancongui ancongui merged commit dea877e into main Jun 19, 2026
3 of 4 checks passed
@ancongui ancongui deleted the feat/spring-security-tier3-method-security branch June 19, 2026 18:53
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