Skip to main content

RecoveryPolicy

Trait RecoveryPolicy 

Source
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:

  1. RFC 7239 Forwarded header (for / proto / host of the first hop)
  2. X-Forwarded-Proto + X-Forwarded-Host (first CSV entry of each)
  3. Host header (assumes http://)

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§

Source

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.

Source

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).

Source

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).

Source

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§

Source

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.

Source

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.

Source

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.

Source

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).

Source

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).

Source

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".

Implementors§