Tags: fireflyframework/fireflyframework-rust
Tags
feat(security): Spring Security 6 parity — Tier 1 authentication spin… …e (26.6.30) (#38) * feat(security): Tier 1 — authentication manager spine (T1.1) AuthenticationManager / ProviderManager / AuthenticationProvider — the Rust analog of Spring's authentication architecture. AuthenticationRequest is a #[non_exhaustive] enum (UsernamePassword / BearerToken) standing in for Spring's pre-auth token; ProviderManager tries each supporting provider in order, first success wins, provider-not-found otherwise. BearerTokenAuthenticationProvider adapts the existing Verifier so JWT/JWKS verifiers slot into the spine. +5 tests; security lib 99 green; clippy + fmt clean. * feat(security): Tier 1 — DelegatingPasswordEncoder + NoOp (T1.3) Spring's recommended {id}-prefixed password storage: DelegatingPasswordEncoder hashes with a default encoder and prefixes {id} ({bcrypt}$2b$…), verify() reads the {id} and delegates, and upgrade_encoding() flags a stored hash for re-encoding on next login when its {id} differs from the default or it is a legacy unprefixed hash. with_defaults() = {bcrypt} default + {argon2} + {noop}, with bare/legacy hashes verified as bcrypt for seamless migration. NoOpPasswordEncoder ({noop}) for tests/dev. +5 tests; security lib 104 green; clippy + fmt clean. * feat(security): Tier 1 — UserDetails + DaoAuthenticationProvider (T1.2) UserDetails (stored credential + the four Spring account-status flags), UserDetailsService (load_user_by_username -> Option), UserDetailsChecker + AccountStatusUserDetailsChecker (locked/disabled/expired), InMemoryUserDetails- Service, and DaoAuthenticationProvider — an AuthenticationProvider for the UsernamePassword request that loads the user, runs the status checks, verifies via a PasswordEncoder, and checks credentials expiry. Enumeration-safe: an unknown user and a wrong password both fail as 'Bad credentials' with comparable encoder work. Plugs into the ProviderManager spine alongside the bearer provider. +4 tests; security lib 108 green; clippy + fmt clean. * feat(security): Tier 1 — SecurityContextRepository (T1.4) SecurityContextRepository (load/save/contains) — Spring's pluggable between-request context store — with HttpSessionSecurityContextRepository (default; configurable session key, wire-compatible with the OAuth2/OTT/WebAuthn handlers) and NullSecurityContextRepository (stateless). SessionAuthenticationLayer now loads the context through a (swappable) repository instead of a hardcoded session key; added Authentication::is_authenticated(). Restore-semantics tests moved to the repository module. +4 repo tests; full security suite (107 lib + integration) green; clippy + fmt clean. * feat(security): Tier 1 — auth events + pluggable exception handlers (T1.5) AuthenticationEventPublisher (AuthenticationEvent::Success/Failure) wired into ProviderManager — publishes every authentication outcome (with the attempted username on failure); LoggingAuthenticationEventPublisher default. Completes the authentication-manager spine. ExceptionTranslationFilter seam: AuthenticationEntryPoint (401) + AccessDenied- Handler (403) traits with the canonical problem+json defaults; FilterChain gains with_authentication_entry_point / with_access_denied_handler so a deployment can customise the rejection (login redirect, WWW-Authenticate, etc.). +2 tests; full security suite (109 lib + integration) green; clippy + fmt clean. * docs+release: Tier 1 authentication spine — CHANGELOG, book appendix, version 26.6.30 CHANGELOG 26.6.30 (Tier 1: AuthenticationManager spine, UserDetails+DAO, DelegatingPasswordEncoder, SecurityContextRepository, auth events, pluggable entry-point/access-denied). Spring Security Parity appendix (EN+ES) marks the authentication-spine row + roadmap tier as done; designed book rebuilt (EN+ES). Workspace bumped to 26.6.30. * fix(security): address Tier 1 adversarial review findings - [MED] DaoAuthenticationProvider: the not-found timing-mitigation dummy hash now falls back to a valid bcrypt hash if the configured encoder's hash() fails, so the user-enumeration timing equalization can't silently collapse to an empty (instant-fail) hash under a misconfigured encoder. - Test gaps closed: SecurityContextRepository seam on SessionAuthenticationLayer (NullSecurityContextRepository swap), Authentication::is_authenticated, DAO backend-error propagation, HttpSession typed-object load fallback, and the bearer (username:None) failure event. - Documented the remaining by-design/refinement findings (ProviderManager account-status short-circuit, upgrade_encoding {id}-only, {noop}/unprefixed defaults) as Known limitations in the 26.6.30 CHANGELOG. +4 tests; full security suite (113 lib + integration) green; clippy + fmt clean. --------- Co-authored-by: Andres Contreras <andres.contreras@soon.es>
feat(security): Spring Security 6 parity — Tier 0 hardening + passwor… …dless (26.6.29) (#37) * fix(security): Tier 0 auth-core hardening (H1/H2/H14) H1: SessionAuthenticationLayer now scopes the task-local CURRENT_AUTH around the downstream call (not just the request extension), so #[pre_authorize] / check_access / current_authentication() work for session- and OAuth2-login- authenticated callers — previously method security silently failed unless the caller was behind BearerLayer. H2: Authentication::has_role accepts Spring's ROLE_ prefix (hasRole('ADMIN') matches authority ROLE_ADMIN) while keeping bare roles backward-compatible. H14: guards::permit_all() admits anonymous/absent principals like Spring permitAll(). Tests: +4 security unit tests (red→green); full firefly-security suite and the reactive-banking e2e suite stay green. * fix(security): Tier 0 filter-chain hardening (H3/H10) H3: path-prefix rules are now path-segment aware (Spring AntPathRequestMatcher semantics) — permit("/api") matches /api and /api/... but no longer leaks to /api-internal or /apixyz, which a raw starts_with allowed. H10: glob compilation no longer panics on an invalid pattern — FilterChain gains try_layer() returning a recoverable SecurityError (layer() still panics for ergonomic use). Builders defer validation to layer/try_layer. Tests: +2 filter-chain unit tests (red→green); full suite + reactive-banking e2e stay green. * feat(security): Tier 0 JWT/JWKS/bearer hardening (H5/H6/H7/H8) H5: JwksVerifier now accepts EC (ES256/ES384) and OKP/EdDSA keys in addition to RSA (RS*/PS*); a resource server fronting an EC/EdDSA-signing IdP can now verify tokens. Default allowed-alg set is the asymmetric family (never HS*). H6: OIDC id_token is never silently trusted — handle_callback rejects an id_token it cannot validate (no JWKS) instead of falling through to userinfo; exp/nbf now use a configurable clock-skew leeway (default 60s, Spring's JwtTimestampValidator default) on both JwksVerifier and JwtService. H7: nbf is now validated (future-dated tokens rejected). H8: bearer rejections carry an RFC 6750 WWW-Authenticate: Bearer challenge (bare when no token; error="invalid_token" when present-but-invalid). Tests: +9 (jwks EC/EdDSA/nbf/leeway, jwt leeway/nbf, bearer challenge x2, oauth2 id_token no-jwks); full suite + reactive-banking e2e green; clippy clean. * fix(web,security): Tier 0 web-layer hardening (H4/H9/H11) H4: CSRF cookie Secure attribute now follows the request scheme by default (CookieSecure::Auto) instead of being unconditional, so the double-submit pair works over plain-HTTP dev; Always/Never override. Applied to both the firefly-web and firefly-security CSRF layers. H9: HSTS is emitted only on secure requests by default (Spring's HstsHeaderWriter); hsts_include_insecure forces it on. H11: CorsLayer rejects the illegal wildcard-origin + credentials combination (Spring's validateAllowCredentials) via try_new()->Result; new() panics. BREAKING (Spring-faithful, flagged): default HSTS no longer sent over HTTP; CSRF cookie no longer Secure over HTTP; wildcard-origin+credentials CORS config now rejected. Three pyfly-parity tests updated to the new semantics; +5 tests. Full security+web suites + reactive-banking e2e green; clippy clean. * fix(session-postgres,idp): Tier 0 store hardening (H12/H13) H12: PostgresSessionRegistry gains an expires_at column (idempotent ALTER for existing tables) and TTL-driven pruning, so an orphaned session row ages out instead of inflating the per-principal concurrency count forever. Default TTL 30m; with_ttl() overrides (ZERO disables); prune_expired() exposed for a scheduled sweep. Verified end-to-end against real Postgres. H13: idp-internal-db login runs a comparable bcrypt op on the unknown-user path so an unknown username can't be distinguished from a wrong password by latency (user-enumeration oracle) — Spring's userNotFoundEncodedPassword guard. Tests: +1 idp timing test (red→green), +H12 real-Postgres pruning test, +SQL const guards; existing PG integration tests pin TTL off (synthetic timestamps). clippy clean. * feat(security): one-time-token (magic-link) login — Spring 6.4 oneTimeTokenLogin() New ott module: OneTimeTokenService (generate/consume, single-use, expiry) with an in-memory impl; OneTimeTokenGenerationSuccessHandler for out-of-band delivery (default logs issuance only, never the token value); and ott_login_routes exposing POST /ott/generate + GET /login/ott (magic link) that redeems a token, rotates the session id (anti-fixation), and stores the SECURITY_CONTEXT for SessionAuthenticationLayer to restore. Adds tracing dep. Tests: +6 (generate/consume, single-use, unknown/expired rejection, generate endpoint doesn't leak the token, login endpoint authenticates + sets session context + rejects replay). Full security suite green; clippy clean. * docs(book): add the 'Spring Security Parity' appendix (EN + ES) A published reference companion to the design spec: the Spring Security 6 coverage matrix, the Spring-faithful behaviours of the Tier 0 hardening, the passwordless (one-time-token + WebAuthn) story, and the tiered roadmap. Wired into book.yaml + book-es.yaml (Appendix B) and SUMMARY.md; the designed book builds (EN verified, ES wired symmetrically). * feat(security): WebAuthn / passkey login — Spring 6.4 webAuthn() (feature-gated) New, opt-in `webauthn` module (off by default; pulls in webauthn-rs 0.5.5): - WebAuthnRelyingParty over webauthn_rs::Webauthn (start/finish passkey registration + authentication). - Pluggable ports: PasskeyCredentialRepository, PublicKeyCredentialUserEntity- Repository, CeremonyStateStore (+ in-memory impls). - webauthn_routes: POST /webauthn/register/options|register, /webauthn/authenticate/options, POST /login/webauthn — the login route rotates the session id and stores the security context (reused by SessionAuthenticationLayer), mirroring the OTT flow. - WebAuthnProperties (rp-id / rp-name / allowed-origins), WebAuthnError. Tests: +5 incl. a real end-to-end ceremony driven by a software authenticator (register → authenticate → session context set) and an RP-level round-trip. Verified: cargo test --features webauthn (96 lib + all suites green), default build (feature off) compiles, clippy clean. * feat(security): wire configurable JWT clock-skew through SecurityProperties (Batch 6) JwtProperties gains clock_skew_seconds; verifier_from_config applies it to both the JWKS and HMAC verifiers (0 keeps the Spring-faithful 60s default). The other Tier 0 knobs are already serde-bound on their config structs (SecurityHeadersConfig.hsts_include_insecure, WebAuthnProperties) or set at the layer builder (CookieSecure on CsrfLayer, OTT ttl). +1 config test. * fix(security,web,session-postgres): address adversarial review findings Following an adversarial review of the Tier 0 diff (5 dimensions, each finding re-verified): - [HIGH] In-process TLS termination now marks requests secure (SecureRequest extension, set by serve() when TLS is configured), so HSTS and the CSRF Secure-cookie flag are no longer silently dropped on direct-HTTPS deployments (request_is_secure previously only honoured X-Forwarded-Proto / URI scheme). - [HIGH/MED] FilterChain role rules are now ROLE_-prefix aware (and match a ROLE_-prefixed authority), consistent with Authentication::has_role — a ROLE_ADMIN principal satisfies require(..., ["ADMIN"]) just as it does #[pre_authorize]. Closes the cross-surface H2 asymmetry. - [MED] Postgres SessionRegistry pruning is now opt-in (default OFF): a fixed created_at+ttl expiry would wrongly evict still-active sliding sessions and under-count maximumSessions. with_ttl enables it for absolute-lifetime caps. - Tests: ROLE_ cross-surface test, in-app-TLS HSTS test, default-no-prune test (real PG), OTT anti-fixation rotation assertion, de-flaked OTT expiry test, permit_all combinator test. Lower-severity findings documented as known limitations. web 48+60+11+19 + security 94 + session-postgres 18 (real PG) green; clippy clean. * chore(release): 26.6.29 — Spring Security 6 parity (Tier 0) Bump the workspace to 26.6.29 and record the CHANGELOG for the Spring Security parity increment: the Tier 0 hardening (H1–H14), one-time-token login, WebAuthn, configurable clock-skew, the parity book appendix, the post-review fixes, and the documented known limitations. * docs(security): document the parity work across the book, README & MODULES - MODULES.md + crates/security/README.md: reflect ROLE_-aware/segment-safe FilterChain, RSA/EC/EdDSA JWKS, one-time-token + WebAuthn passwordless login, Argon2id, and the Spring Security 6-faithful hardening. - Security chapter (EN + ES): cross-link the new Spring Security Parity appendix. - Rebuild the designed book dist (EN + ES PDF/EPUB) so the release ships the appendix and cross-links. --------- Co-authored-by: Andrés Contreras Guillén <ancongui@Andress-MacBook-Pro-2.local> Co-authored-by: Andres Contreras <andres.contreras@soon.es>
feat: declarative #[http_client] HTTP-interface client (26.6.28) (#30) A Spring Boot parity increment — the highest single value lever from the 16-area parity-gap analysis (it lifts the REST/HTTP-clients area off the floor). Designed via a scored 3-proposal judge panel and adversarially reviewed before merge. #[http_client] — the analog of Spring 6's @HttpExchange (modern OpenFeign replacement). Annotate a trait of methods with the SAME verb attributes a #[rest_controller] uses; the macro generates a <Trait>Impl that issues the requests over a WebClient — the mirror image of a controller. - Verbs: #[get("/path")]/#[post]/#[put]/#[delete]/#[patch] + generic #[request(method="...")]. Path vars use the framework's :id syntax (same as the server macro); {id} is a compile error pointing at :id. - Argument binding needs no attributes in the common case: a name-matched :var arg is the path variable, the lone non-scalar arg on a body verb is the JSON body, the rest are query params (Option omits when None, Vec/&[_] repeat). Override with #[path]/#[query("k")]/#[header("X")]/#[body]. Every :var must bind exactly once or it's a compile error; an Option/Vec/slice path variable is rejected (caught by review — it produced .../Some(x) URLs). - Return shapes: async fn -> Result<T, ClientError> (ergonomic default), Result<T, E: From<ClientError>>, non-async Mono<T>/Flux<T> (returned directly; Flux defaults Accept: application/x-ndjson), WebClientResponse (.exchange() escape hatch). - Construction: <Trait>Impl::new(base_url) or ::with_client(WebClient); Clone. With #[http_client(... bean)] it's registered as a @service and bound to dyn Trait, so #[autowired] Arc<dyn Trait> resolves (sharing a WebClient bean, named via client = "..."). - Error fidelity (documented): awaited Result<T, ClientError> surfaces every failure as ClientError::Problem (FireflyError with original status/code, so is_not_found()/is_server_error()/is_retryable() still classify); structured Transport/Decode/Encode/InvalidUrl variants survive only on Mono/Flux. Reuses the server #[rest_controller] verb grammar (MappingAttr/VERBS/join_path made pub(crate); no code moved) so client and server can't drift. Added firefly_client::encode_path_segment (RFC 3986 path-segment encoding). The firefly::prelude now also re-exports WebClient/ClientError/new_web_client. Tests: in-process axum round-trip (path-var encode, Option-query omit, header, JSON body, Vec decode, 204->(), 404->Problem.is_not_found(), NDJSON Flux, Option empty-body fold, custom-error map_err, DI dyn-Trait resolve) + 12 trybuild compile-fail cases. make ci green: 317 suites, 4503 tests, 0 failed. Book: docs/book/src/13-http-clients.md "The declarative client" section. Co-authored-by: Andrés Contreras Guillén <ancongui@Andress-MacBook-Pro-2.local>
feat: declarative rollback rules on #[transactional] (26.6.27) (#29) A Spring Boot parity increment, chosen from a 16-area parity-gap analysis (~84% overall) as the best value-to-effort gap — the transaction runtime already supported per-error rollback decisions; only the macro surface was missing. #[transactional(no_rollback_for = "<pat>", rollback_only_for = "<pat>")] Spring names exception *types*; because Rust's Result already separates failure from success, the Firefly analog names an error *pattern*. By default every Err rolls back; then: - no_rollback_for = "P" — Spring's @transactional(noRollbackFor = …): an Err matching pattern P commits instead of rolling back; - rollback_only_for = "P": roll back only for errors matching P, committing the rest; - with both, no_rollback_for wins on overlap. The macro lowers to the already-present transactional_with / transactional_with_on runtime entry points (which take a should_rollback(&E) -> bool predicate), composes with manager = "…", and the generated predicate is matches!-based, so a pattern that does not fit the error type is a compile error. Patterns allow `A | B` alternatives (no `if` guard). Deliberately NOT named rollback_for: Spring's rollbackFor is *additive* (it widens the always-rollback set), but Rust has no checked/unchecked split — every Err already rolls back — so the faithful rule is *restrictive*. Writing rollback_for is a friendly compile error pointing at the two rules above, so a Spring port can't be silently inverted. Adversarially reviewed (caught and fixed the rollback_for naming footgun, the no-guard constraint, and an untested process-global path before merge). Tests: a spy TransactionManager records the per-call commit-vs-rollback decision across all four cases on both the explicit-manager and process-global paths. Docs: 07-persistence.md + CHANGELOG. make ci green: 316 suites, 4489 tests, 0 failed. Co-authored-by: Andrés Contreras Guillén <ancongui@Andress-MacBook-Pro-2.local>
PreviousNext