Releases: fireflyframework/fireflyframework-rust
v26.6.35
Spring Security parity — Tier 5c: ACL / domain-object security. The Rust
analog of spring-security-acl, answering hasPermission(object, permission)
from per-object access-control lists. Pure Rust — no new dependencies. All
additive. Adversarially reviewed before release.
Added
- ACL core (
spring-security-aclparity):Permission— theBasePermissionbitmask (READ=1,WRITE=2,
CREATE=4,DELETE=8,ADMINISTRATION=16), with cumulativeunion,
bit-contains, and case-insensitive name parsing.Sid(Principal/Authority— Spring'sPrincipalSid/
GrantedAuthoritySid),ObjectIdentity(type+identifier),
AccessControlEntry(sid + permission + granting), andAcl(owner- ordered ACEs + optional parent for inheritance).
AclService+InMemoryAclService(Spring'sMutableAclService),
and the freeis_grantedresolver.
AclPermissionEvaluator— bridges anAclServiceto the Tier 3
PermissionEvaluator, resolvinghasPermission(...)against per-object ACLs
by object reference or(type, id). The principal and its roles/authorities
map toPrincipalSid/GrantedAuthoritySid(each role matched both bare and
ROLE_-prefixed).PermissionEvaluator::has_permission_for_id+ the free
has_permission_for_id— Spring's id-basedhasPermissionoverload
(default-deny, backward compatible).
Security
- ACL evaluation is default-deny: a permission is granted only when an
applicable granting entry is found (locally or up the inheritance chain);
the first entry matching a(sid, permission)wins, so a deny placed
before a grant takes precedence (Spring'sDefaultPermissionGrantingStrategy).
The inheritance walk is bounded, so a cyclic or pathologically deep parent
chain terminates and denies rather than looping.
v26.6.34
Spring Security parity — Tier 5a: LDAP / Active Directory authentication.
The first of the Tier 5 "big subsystems", delivered as an opt-in feature. All
additive (no behaviour change to existing code; the default build does not
compile the new module). Adversarially reviewed before release.
Added
ldapfeature (opt-in, pulls inldap3) — Spring's
ldapAuthentication():LdapAuthenticationProvider— bind authentication as an
AuthenticationProvider(plugs intoProviderManager): search the user DN
under a base+filter ((uid={0}), username RFC 4515-escaped), bind as that
DN with the password (the directory verifies it), then map group membership
((member={0})) toROLE_<GROUP>authorities — Spring's
BindAuthenticator+DefaultLdapAuthoritiesPopulator.ActiveDirectoryLdapAuthenticationProvider— binds as the
userPrincipalName(user@domain) and maps the user'smemberOfgroups to
roles.LdapOperationsport (+escape_filter_value,cn_from_dn,
LdapEntry) with the productionLdap3Operationsadapter overldap3.
The port makes the provider logic unit-testable without a live directory.
- Security defaults: an empty password is rejected before binding (a simple
bind with an empty password is an anonymous bind that most directories accept
— an authentication bypass); the username/DN are RFC 4515-escaped in search
filters (LDAP-injection safe); unknown-user and wrong-password fail with the
same error value; a non-zero LDAP bind result code is an error (never a silent
success). - Hardened from the pre-release adversarial review: an ambiguous user search
(more than one matching entry) is rejected rather than binding against an
arbitrary first match (Spring'sIncorrectResultSizeDataAccessException); a
directory error while populating authorities propagates and fails the
login instead of silently authenticating with no roles (Spring's
DefaultLdapAuthoritiesPopulatorsemantics); and a malformed directory
entry is caught and turned into a clean error rather than aborting the
authentication task.
Notes
- The live
Ldap3Operationsadapter is exercised by an integration test gated
onFIREFLY_TEST_LDAP_URL(skipped when unset); the provider logic is fully
covered by mock-LdapOperationsunit tests.
v26.6.33
Spring Security parity — Tier 4: the OAuth2 ecosystem. The wider OAuth2
surface beyond the browser login flow. All additive (no behaviour change to
existing code). Adversarially reviewed before release.
Added
- Opaque-token introspection (RFC 7662) —
RemoteTokenIntrospector
(Spring'sOpaqueTokenIntrospector): POSTs a non-JWT bearer token to the
authorization server's introspection endpoint (HTTP Basic client auth) and,
onactive: true, maps the response to anAuthentication. Implements
Verifier, so it drops into aBearerLayeras an alternative to local JWT
verification. Fails closed (transport error / non-2xx / non-JSON /
active: false/absent all reject). - Outbound OAuth2 client (
AuthorizedClientManager) —
OAuth2AuthorizedClientManager+OAuth2AuthorizedClientService(+
InMemoryOAuth2AuthorizedClientService) obtain, cache, and auto-refresh the
access tokens the app needs to call downstream services: the client-credentials
grant (service-to-service) and the refresh-token grant, reusing a cached
OAuth2AuthorizedClientuntil it is within the clock-skew window of expiry. - RP-initiated logout (OIDC) —
oidc_logout_url+ClientRegistration's new
end_session_endpoint/post_logout_redirect_uri:POST /logoutinvalidates
the local session and, when the provider advertises anend_session_endpoint,
redirects to it withid_token_hint+post_logout_redirect_uri(Spring's
OidcClientInitiatedLogoutSuccessHandler). The login callback now stores the
registration_id+id_tokenfor the hint. - Authorization-server HTTP endpoints —
AuthorizationServerRoutermounts
the previously callable-onlyAuthorizationServerasPOST /oauth2/token
(RFC 6749; client-credentials + refresh-token,client_secret_post; RFC 6749
§5.2 error envelope) andGET /.well-known/oauth-authorization-server(RFC 8414
metadata).
Security notes & known limitations (roadmap)
- The OAuth2 HTTP clients (introspection, outbound token, JWKS, login) now apply
connect/read timeouts and cap the response body, so a slow or hostile endpoint
cannot hang the bearer-verification path or force unbounded allocation; a token
response with noexpires_inis assumed short-lived (bounded fallback), never
immortal.ClientRegistrationandOAuth2AuthorizedClientredact their
secrets/tokens inDebug. - The authorization server signs HS256 (symmetric), so no
jwks_uriis
published; the server-side authorization_code grant + PKCE, an/authorize
endpoint, and a client-authenticated/oauth2/revoke(RFC 7009) remain a
follow-up. OAuth2AuthorizedClientManagerdoes not single-flight concurrent
authorizations for the same registration: concurrent callers may each hit the
token endpoint, and against an authorization server that rotates refresh
tokens, concurrent refreshes can lose a rotated token (last-writer-wins).
Serialize refreshes for the same client if your AS rotates. Token-endpoint
failures surface as the HTTP status (the structured RFC 6749 §5.2 error body is
not yet parsed back).
v26.6.32
Spring Security parity — Tier 3: method-security depth. Expression-based
method security and domain-object permissions, 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 now a
boolean Rust expression evaluated before the body with the method's
parameters andauth(a&Authentication) in scope (Spring's
@PreAuthorize("#id == authentication.name")), e.g.
#[pre_authorize(auth.has_role("ADMIN") || auth.principal == owner)]. The
keyword rules (authenticated,role,any_role,authority,
any_authority) are unchanged and fully backward-compatible. Fail-closed: no
ambient context denies withUnauthenticated, a false expression with
Forbidden. PermissionEvaluator+has_permission— the Rust analog of Spring's
PermissionEvaluator/hasPermission(target, permission). Register one
process-wide withset_permission_evaluator; call
has_permission(auth, target, permission)inside any pre/post expression. The
target is erased toAnyso one evaluator serves every domain type by
downcasting. Secure default: with no evaluator registered, every permission
is denied.#[pre_filter]/#[post_filter]— collection filtering (Spring's
@PreFilter/@PostFilter).#[post_filter(element.owner == auth.principal)]
retains only the elements of the returned collection the predicate accepts;
#[pre_filter(items, …)]filters a named ownedmutcollection argument
before the body.elementis the per-element&T(Spring'sfilterObject);
no ambient context denies the call withUnauthenticated.
Known limitations (roadmap)
PermissionEvaluatoris a process-global set-once registry (one evaluator per
process, like Spring's single bean); there is no per-scope override.#[pre_filter]requires the targeted parameter to be an ownedmut
collection withretain(e.g.mut items: Vec<T>).
v26.6.31
Spring Security parity — Tier 2: the web authentication mechanisms. The
classic browser/login surface from Spring's HttpSecurity, built on the Tier 1
authentication spine. All additive (no behaviour change to existing code).
Adversarially reviewed before release; the review's six confirmed findings are
fixed in this release.
Added
- HTTP Basic (
httpBasic()) —HttpBasicLayerreads
Authorization: Basic …and authenticates through theAuthenticationManager
spine. An absent header passes through (so a session/bearer layer can take
over); an invalid or malformed one is rejected with401and a
WWW-Authenticate: Basic realm="…"challenge (configurable realm, pluggable
BasicAuthenticationEntryPoint) — Spring'sBasicAuthenticationFilter. - Form login (
formLogin()) —form_login_routesmountsPOST /login
(url-encodedusername+password), rotates the session id on success
(anti-fixation) before persisting the context through a
SecurityContextRepository, then redirects. Success/failure responses are
swappable (FormLoginSuccessHandler/FormLoginFailureHandler), and the
success path is saved-request-aware. - Remember-me (
rememberMe()) —TokenBasedRememberMeServicesmints a
signed, expiring cookie token whose signature is an HMAC-SHA256 keyed by a
server secret over the username, expiry, and the user's stored password hash:
a password change, an expired clock, a tampered token, or the wrong key all
reject. New trust-level methods onAuthentication—
is_remembered()/is_fully_authenticated()(+REMEMBERED_CLAIM) — so a
remembered context is authenticated but not fully authenticated (Spring's
isFullyAuthenticated()), and a sensitive route can demand a fresh login. RequestCache/SavedRequest—HttpSessionRequestCacheremembers the
page an unauthenticated user wanted; form login returns them there after
login instead of the default target (Spring's
SavedRequestAwareAuthenticationSuccessHandler). Only same-origin targets
are honoured (SavedRequest::is_safe_redirect): a protocol-relative,
backslash-tricked, absolute, or control-char target falls back to the
configured success URL, so the login flow can't be turned into an open
redirect.NullRequestCachefor stateless surfaces.SessionCreationPolicy—Always/IfRequired(default) /Never/
Stateless(Spring'ssessionManagement().sessionCreationPolicy(...)).
SessionAuthenticationLayer::session_creation_policy(...)installs the implied
SecurityContextRepository;Statelessuses the null repository (no session
context) for token-only APIs.- Multiple filter chains —
SecurityFilterChainsroutes each request to the
first chain whoseRequestMatcher(AnyRequestMatcher/
PathRequestMatcher, segment-aware, optional method) matches, so a
locked-down/api/**and a permissive web surface coexist (Spring's
FilterChainProxy); an unmatched request passes through. The dispatcher
honours tower's readiness contract for a backpressure-bearing inner service.
Known limitations (roadmap)
TokenBasedRememberMeServicesis the stateless of Spring's two
remember-me strategies: a captured cookie replays for the full validity window
(default 14 days) until the embedded expiry passes or the user's password hash
changes — there is no per-token series/rotation theft detection (Spring's
PersistentTokenBasedRememberMeServices) and no server-side revocation list.
Use a shorttoken_validity_secondsand serve the cookieHttpOnly+SecureSameSite. A persistent/series variant is a follow-up.
RequestCache::save_requestis provided for an authentication entry point to
call before redirecting to login; wiring an entry point that auto-saves the
request is left to the application (the consume side is wired into form login).
v26.6.30
Spring Security parity — Tier 1: the authentication spine. The core of
Spring Security's authentication architecture, the foundation later tiers build
on. All additive (no behaviour change to existing code).
Added
- Authentication manager spine —
AuthenticationManager/ProviderManager
/AuthenticationProvider(Spring's authentication architecture). An
AuthenticationRequest(UsernamePassword/BearerToken,#[non_exhaustive])
is resolved by the first supporting provider;BearerTokenAuthenticationProvider
adapts the existingVerifierinto the spine. UserDetails+ DAO authentication —UserDetails(with the four Spring
account-status flags),UserDetailsService,UserDetailsChecker/
AccountStatusUserDetailsChecker,InMemoryUserDetailsService, and
DaoAuthenticationProvider(an enumeration-safe username/password provider:
unknown user and wrong password both fail asBad credentialswith comparable
bcrypt work).DelegatingPasswordEncoder— Spring's recommended{id}-prefixed password
storage ({bcrypt}/{argon2}/{noop}),upgrade_encodingfor re-hash-on-login,
and seamless migration of legacy bare hashes; plusNoOpPasswordEncoder.SecurityContextRepository— the pluggable between-request context store
(HttpSessionSecurityContextRepositorydefault,NullSecurityContextRepository
for stateless surfaces).SessionAuthenticationLayernow loads the context
through a swappable repository instead of a hardcoded session key; added
Authentication::is_authenticated().AuthenticationEventPublisher—AuthenticationEvent::{Success,Failure}
published byProviderManagerfor every outcome (LoggingAuthenticationEvent- Publisherdefault).- Pluggable
AuthenticationEntryPoint/AccessDeniedHandler(Spring's
ExceptionTranslationFilterseam) —FilterChainrenders its401/403
through them, defaulting to the canonical problem+json and overridable via
with_authentication_entry_point/with_access_denied_handler.
Known limitations (roadmap)
ProviderManagercontinues to the next supporting provider after a failure
(Spring rethrows anAccountStatusExceptionimmediately). With one provider
per credential kind — the norm — the outcome is identical; a terminal/continue
error taxonomy is a follow-up.DelegatingPasswordEncoder::upgrade_encodingcompares the stored{id}only
(algorithm migration); it does not yet flag a within-algorithm work-factor
increase for re-hash.with_defaults()registers{noop}(plaintext — dev only, as Spring does)
and verifies legacy unprefixed hashes as bcrypt to ease migration; disable
the latter withwith_unprefixed(None)for Spring's stricter reject-on-bare
behaviour.
v26.6.29
A Spring Security 6 parity increment (Tier 0): an adversarially-verified
audit of the security tier against Spring Security 6 / Spring Boot 3, followed
by the hardening pass that closes the silent semantic divergences in shipping
code, plus the two Spring Security 6.4 passwordless mechanisms. See the new
Spring Security Parity book appendix for the full coverage matrix.
Added
- One-time-token (magic-link) login — Spring 6.4
oneTimeTokenLogin():
OneTimeTokenService(single-use, expiring; in-memory impl) +
OneTimeTokenGenerationSuccessHandlerfor out-of-band delivery +
ott_login_routes(POST /ott/generate,GET /login/ott) that redeems a
token, rotates the session id, and establishes the security context. - WebAuthn / passkeys — Spring 6.4
webAuthn(): a feature-gatedwebauthn
module with the registration and authentication ceremonies overwebauthn-rs
and a pluggable credential repository (opt-in; off by default). - EC + EdDSA JWKS keys —
JwksVerifiernow verifiesES256/ES384and
EdDSAtokens in addition to RSA (RS*/PS*). FilterChain::try_layer/CorsLayer::try_new— fallible builders
that surface invalid glob patterns / unsafe CORS config as a recoverable
error instead of panicking at startup.- Configurable clock-skew (
clock_skew_seconds, default 60s) andnbf
validation onJwksVerifierandJwtService. - A Spring Security Parity appendix in the book (EN + ES).
Changed (Spring-faithful defaults — each with an escape hatch)
- Method security works behind every authentication mechanism.
SessionAuthenticationLayernow scopes the task-local security context, so
#[pre_authorize]/current_authentication()work for session- and
OAuth2-login-authenticated callers (previously bearer-only). hasRole('X')matches theROLE_Xauthority (Spring's prefix) as well as
a bare role name.- HSTS is sent only over secure requests by default
(hsts_include_insecureto force it). - The CSRF cookie is
Secureonly when the request is secure
(CookieSecure::{Auto,Always,Never}, defaultAuto). - A wildcard CORS origin with
allow_credentialsis rejected at
construction. - JWT/JWKS validation tolerates 60s clock skew (was zero).
- Path-prefix authorization is segment-aware —
permit("/api")no longer
matches/api-internal.
Fixed (security)
- OIDC
id_tokenis never trusted without validation — the login fails if
it cannot be verified, instead of silently falling through to userinfo. - Bearer rejections carry an RFC 6750
WWW-Authenticate: Bearerchallenge
(error="invalid_token"when a token was supplied). - No user-enumeration timing oracle — an unknown username runs comparable
bcrypt work to a wrong password (internal-db IdP). - Postgres
SessionRegistryrows expire (opt-in absolute TTL + pruning, via
with_ttl) so an orphaned session can no longer inflate the per-principal
concurrency count. Pruning is off by default — a fixed TTL would wrongly
evict still-active sliding sessions, so enable it only with an absolute
session lifetime.
Known limitations (roadmap)
request_is_securetrustsX-Forwarded-Protofrom any caller; deploy behind a
trusted proxy (or terminate TLS in-process, which Firefly marks automatically).
A trusted-proxy allowlist is planned.- WebAuthn
authenticate/optionsreveals whether a username has registered
passkeys; use discoverable (usernameless) credentials to avoid enumeration. - Sliding-session expiry isn't synced into the distributed
SessionRegistry
(noHttpSessionEventPublisheranalog yet) — deregister on logout or set an
absolute TTL. - One-time-token magic links are redeemed via
GET(token in the URL);
single-use + short expiry mitigate referer leakage.
v26.6.28
A Spring Boot parity increment: the declarative HTTP-interface client — the
highest single value lever from the parity-gap analysis (it lifts the REST/HTTP
clients area off the floor).
Added
#[http_client]— a declarative HTTP-interface client, the analog of
Spring 6's@HttpExchange(the modern OpenFeign replacement). Annotate a
trait of methods with the same verb attributes a#[rest_controller]
uses and the macro generates a<Trait>Implthat issues the requests over a
WebClient— the mirror image of a controller.- Verbs:
#[get("/path")]/#[post]/#[put]/#[delete]/
#[patch]+ generic#[request(method = "…")]. Path variables use the
framework's:idsyntax (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
:vararg is the path variable, the lone non-scalar arg on a body verb is
the JSON body, the rest are query params (Optionomits whenNone,
Vec/&[_]repeat). Override with#[path]/#[query("k")]/
#[header("X")]/#[body]. Every:varmust bind exactly once or it is a
compile error; anOption/Vec/slice path variable is rejected. - Return shapes:
async fn -> Result<T, ClientError>(the ergonomic
default),Result<T, E: From<ClientError>>, non-asyncMono<T>/Flux<T>
(returned directly; aFluxdefaultsAccept: application/x-ndjson), and
WebClientResponse(the.exchange()escape hatch). - Construction:
<Trait>Impl::new(base_url)or::with_client(WebClient);
the type isClone. With#[http_client(... bean)]it is registered as a
@Serviceand bound todyn Trait, so#[autowired] Arc<dyn Trait>
resolves (pulling a sharedWebClientbean, named viaclient = "…"). - Error fidelity (documented): an awaited
Result<T, ClientError>
surfaces every failure asClientError::Problem(carrying aFireflyError
with the original status/code, so the classifiers still work); the
structuredTransport/Decode/Encode/InvalidUrlvariants survive only
on theMono/Fluxreturn forms.
- Verbs:
firefly_client::encode_path_segment— RFC 3986 path-segment
percent-encoding (used by generated clients; also public).
The macro reuses the server #[rest_controller]'s verb-attribute grammar
(MappingAttr/VERBS/join_path), so client and server can't drift. Designed
via a scored 3-proposal panel and adversarially reviewed (the review caught a
runtime footgun — an Option path variable producing …/Some(x) URLs — now a
compile error). The firefly::prelude now also re-exports WebClient /
ClientError / new_web_client.
v26.6.27
A Spring Boot parity increment: declarative rollback rules on
#[transactional]. Chosen from a 16-area parity-gap analysis as the best
value-to-effort gap (the transaction runtime already supported it).
Added
-
#[transactional(no_rollback_for = "<pat>", rollback_only_for = "<pat>")]
— declarative transaction rollback rules. Spring names exception types;
because Rust'sResultalready separates failure from success, the Firefly
analog names an error pattern. By default everyErrrolls back; then:no_rollback_for = "P"— Spring's@Transactional(noRollbackFor = …):
anErrmatching patternPcommits instead of rolling back;rollback_only_for = "P": roll back only when theErrmatchesP,
committing the rest;- with both,
no_rollback_forwins on overlap.
The pattern is any Rust match pattern valid for the fn's error type (no
if
guard), alternatives included ("Error::A | Error::B"). The macro lowers to
the already-presenttransactional_with/transactional_with_onruntime
entry points (which take ashould_rollback(&E) -> boolpredicate), composes
withmanager = "…", and the generated predicate ismatches!-based, so a
pattern that does not fit the error type is a compile error.rollback_only_foris not namedrollback_for: Spring'srollbackForis
additive (it widens the set of exceptions that roll back), but Rust has no
checked/unchecked split — everyErralready rolls back — so the faithful
rule here is restrictive. Writingrollback_foris a friendly compile error
pointing at the two rules above, so a Spring port can't be silently inverted.
No runtime or API changes elsewhere.
v26.6.26
A correctness release. Every one of the 74 per-crate README.md files was
audited against that crate's actual shipped public API (43 confirmed fixes
across 28 crates), and the audit surfaced a real framework bug: the per-crate
VERSION constant was a hardcoded literal frozen at "26.6.24" instead of
tracking the crate version.
Fixed
-
VERSIONno longer drifts from the crate version. Every crate's
pub const VERSIONwas a hardcoded"26.6.24"string that nothing kept in
sync with the workspace version — sofirefly_kernel::VERSION, the actuator
/actuator/versionpayload, and the startup banner all reported a stale
release number, and theversion_matches_crate_versionguard tests only
passed while the workspace happened to sit at26.6.24. All 52 hardcoded
constants now derive fromenv!("CARGO_PKG_VERSION")(the re-exporting
crates already chained tofirefly_kernel::VERSION), soVERSIONis now
always exactly the crate version and can never drift again. Thecli
FRAMEWORK_VERSIONconstant got the same treatment, and the handful of
unit/integration tests that assertedVERSION == "26.6.24"against a frozen
literal now assert againstenv!("CARGO_PKG_VERSION"), so the guard holds for
every release instead of only when the workspace happened to sit at that
number. (The CLI'srender_for/ SBOM-parser fixtures keep their literal
sample versions — that string is arbitrary test data, not the build version.) -
Phantom / incomplete public-surface docs. Documented APIs now match the
source:admin'sAdminDepsgained itsenvironmentfield;openapi's
RouteDef(4 missing fields:request_schema/response_schema/
query_schema/pageable),Parameter::{query,header},Builder::{add_schema, add_schema_descriptors, from_inventory, docs_router}, and theDocsConfig
struct are now listed;orchestration'sCompensationPolicy(now all six
variants incl.GroupedParallel) andSagaError(all four variants);
starter-web'sWebStack::{set_security, set_exception_advice};testkit's
BuiltSlice::web_client;idp'sErrorenum +change_passwordsignature;
security,webhooks,kernel,transactional,pluginssurface fixes. -
Wrong signatures / variant names. The
notifications-*READMEs used the
wire spellingsEmailStatus::SENT/FAILED; the Rust variants areSent/
Failed(#[serde(rename = "SENT")]only affects the JSON).pluginsshowed
aVec<Arc<dyn Any>>annotation that does not type-check against the real
Extension = Arc<dyn Any + Send + Sync>.notifications-twilio,
session-redisparameter names corrected. -
Wrong facts.
admin: the bean graph does ship dependencyedges
(one per autowired dependency), not "nodes-only".backoffice: the middleware
order includesTraceContext(Problem → TraceContext → Correlation → Idempotency → BackOffice).resilience,starter-core,eda-kafka,
session-postgres,session-mongodb,container(awarm→formtypo) fixes. -
Stale version pins. Crate-README dependency examples that still pinned the
long-stale26.6.7now use the self-maintaining minor pinversion = "26.6"
(the conventionfirefly/testkit/webhooksalready used); example
VERSIONoutputs updated to the release version. -
firefly-cachedoc comment. Removed the stale "once the Redis adapter
ships in the next minor" note —firefly-cache-redis(RedisAdapter) has
shipped and is a published workspace member.