pub trait RecoveryPolicy: Send + Sync {
// Required methods
fn reset_token_ttl(&self) -> ChronoDuration;
fn request_rate_limit(&self) -> (u32, StdDuration);
fn consume_rate_limit(&self) -> (u32, StdDuration);
fn strict_mailer_required(&self) -> bool;
// Provided methods
fn public_site_url(&self, req: &Request) -> Option<String> { ... }
fn login_throttle(&self) -> LoginThrottle { ... }
fn reauth_window(&self) -> ChronoDuration { ... }
fn mfa_step_seconds(&self) -> u64 { ... }
fn mfa_skew_steps(&self) -> u32 { ... }
fn scope_for(&self, _identity: &Identity) -> Option<SharedRecoveryPolicy> { ... }
}Expand description
Tunables for the R1 recovery flow: token TTL, rate-limit shape, strict-mailer boot guard, and public-site-URL derivation.
Admin::new() seeds DefaultRecoveryPolicy; projects override
via crate::admin::Admin::recovery_policy. The trait is Send + Sync so the Arc<dyn RecoveryPolicy> lives on Admin and is
cheap to clone into async futures.
The trait method public_site_url has a provided default that
derives the URL from request headers via [derive_public_site_url]
per DESIGN_RECOVERY.md §12.3. Projects whose deployment can’t
rely on the standard Forwarded / X-Forwarded-* / Host headers
override this method and return their own absolute URL (e.g.
stamped at deployment time from a config secret).
§Trust boundary for forwarded headers
The default public_site_url honours these client-supplied
inputs in priority order:
- RFC 7239
Forwardedheader (for / proto / hostof the first hop) X-Forwarded-Proto+X-Forwarded-Host(first CSV entry of each)Hostheader (assumeshttp://)
The operator’s reverse proxy MUST strip incoming versions of
these headers before adding its own. The framework cannot know
the deployment topology; if a hostile client can reach the
process directly with a chosen Forwarded: … header set, the
reset link in the dispatched email will point wherever they ask.
proto is whitelisted to {http, https} (case-insensitive) and
host is rejected when it contains whitespace, control bytes, or
CRLF — so direct injection of \r\n-style header smuggling
fails — but a malicious yet shape-conformant value still needs
to be filtered upstream.
Projects that need a stricter trust posture: override
public_site_url to return a fixed string (e.g. read from
project config at startup) and the framework will use that
regardless of headers.
Required Methods§
Sourcefn reset_token_ttl(&self) -> ChronoDuration
fn reset_token_ttl(&self) -> ChronoDuration
How long a freshly-issued reset token stays valid. Default
1 hour. Locked-decision per DESIGN_RECOVERY.md §17.
Sourcefn request_rate_limit(&self) -> (u32, StdDuration)
fn request_rate_limit(&self) -> (u32, StdDuration)
Per-IP rate-limit on POST /admin/forgot-password. Returned
as (capacity, window): at most capacity requests within
window. Default (5, 15min).
Sourcefn consume_rate_limit(&self) -> (u32, StdDuration)
fn consume_rate_limit(&self) -> (u32, StdDuration)
Per-IP rate-limit on POST /admin/reset-password/<token>.
Tighter than the request limit since the consume path is the
brute-force surface. Default (10, 5min).
Sourcefn strict_mailer_required(&self) -> bool
fn strict_mailer_required(&self) -> bool
When true, the framework refuses to start at boot if the
registered mailer is still the default crate::email::LogMailer
(production deployments must opt in to a real mailer).
Default false. Enforcement lands when the recovery handlers
ship (R1 commit #7+); this commit ships the declaration only.
Provided Methods§
Sourcefn public_site_url(&self, req: &Request) -> Option<String>
fn public_site_url(&self, req: &Request) -> Option<String>
Derive the absolute base URL the reset email’s link should
point at. Default: see [derive_public_site_url] +
trust-boundary docs on this trait. Projects override this
method to return a fixed string (e.g. read from config) when
header derivation isn’t appropriate for their topology.
Returns None when nothing resolves; the caller (R1 issue
handler, commit #7) treats None as a hard failure and
records metadata.email_send_status = "failed" with a clear
log line.
Sourcefn login_throttle(&self) -> LoginThrottle
fn login_throttle(&self) -> LoginThrottle
Auto-throttle parameters for the login flow. Default
LoginThrottle::DEFAULT (5 / 10min / 15min).
Projects override to relax for development environments
(max_attempts: 100) or tighten for high-sensitivity
deployments (max_attempts: 3, lock_minutes: 60).
Setting max_attempts = 0 disables the auto-throttle
entirely; manual lock via /admin/users/:id/lock (R2
commit #16) remains available.
Sourcefn reauth_window(&self) -> ChronoDuration
fn reauth_window(&self) -> ChronoDuration
Window during which a session that has cleared the re-auth
wall (/admin/reauth) is considered elevated and may
access destructive admin-recovery surfaces (admin-driven
password reset, lock, unlock, revoke-sessions). Default
15 minutes (DESIGN_R2_ORGANISATIONAL.md §12 locked-decision).
Re-auth state lives on the session row’s elevated_until
column (R0 schema, runtime lands in R2 commit #10). Returning
a duration of zero or negative is a no-op promotion: every
admin-recovery action will require a fresh re-auth.
Sourcefn mfa_step_seconds(&self) -> u64
fn mfa_step_seconds(&self) -> u64
TOTP step interval in seconds. Locked at 30 per
DESIGN_R3_MFA.md Appendix B — RFC 6238 industry
standard for interop with every common authenticator app
(Google Authenticator, Authy, 1Password, Bitwarden,
Aegis, Raivo, etc.). Returning a different value would
break the QR provisioning URL’s implicit period; the
design treats this as a major-version concern.
The runtime consults this via
auth::mfa::verify_totp_for_user and
auth::mfa::confirm_enrolment (R3 commits #6, #7).
Sourcefn mfa_skew_steps(&self) -> u32
fn mfa_skew_steps(&self) -> u32
TOTP step skew tolerance, in steps. Locked at 1 per
DESIGN_R3_MFA.md Appendix B — gives a 90-second total
acceptance window at the canonical 30-second step
(current ± 1 ≡ [current - 1, current + 1]). The
design treats wider skew as a security regression:
2-step skew would accept a code generated 60 seconds
ago, which extends the network-replay window without
improving UX for users with reasonable clock drift.
The runtime consults this via
auth::mfa::verify_totp_for_user and
auth::mfa::confirm_enrolment (R3 commits #6, #7).
Sourcefn scope_for(&self, _identity: &Identity) -> Option<SharedRecoveryPolicy>
fn scope_for(&self, _identity: &Identity) -> Option<SharedRecoveryPolicy>
Multi-tenant readiness hook. Returns Some(scoped_policy) to
scope rate-limits / TTLs / lockout windows per tenant when an
authenticated identity is in scope; returns None to mean
“no scoping, the caller continues to use the
Admin-bound recovery policy unchanged”.
Default returns None — single-tenant deployments see no
change. Multi-tenant projects override to look up the
tenant from identity.user_id (or a project-specific
claim) and return a fresh Arc<dyn RecoveryPolicy>
with that tenant’s tunables. Per
DESIGN_R2_ORGANISATIONAL.md §6.3 the framework call site
is:
let policy = admin
.recovery_policy
.scope_for(&identity)
.unwrap_or_else(|| Arc::clone(&admin.recovery_policy));Why Option<SharedRecoveryPolicy> and not
SharedRecoveryPolicy (as the design doc’s first sketch
suggested): returning a fresh Arc<Self> from &self
requires the trait method to either receive the policy’s own
Arc as a parameter (awkward at every call site) or rely on
dyn-clone (extra dependency). Option::None expresses
“no override” without either. Multi-tenant impls return
Some(Arc::new(per_tenant_policy)), which is cheap and
idiomatic.
Dyn Compatibility§
This trait is dyn compatible.
In older versions of Rust, dyn compatibility was called "object safety".