Skip to main content

tablo_core/auth/
config.rs

1//! [`Auth`]: the panel's authentication configuration, from the authenticator to the pages and
2//! mail it adds.
3
4use std::{marker::PhantomData, sync::Arc};
5
6use super::{
7    AdminUser, AuthMail, Authenticator, DynAuthenticator, EmailVerification, LoginThrottle,
8    PanelUser, PasswordAuth, PasswordReset, ProfileEditor, Registrar, TenantEditor,
9    TenantRegistration, TwoFactor,
10    external::{DynExternal, ExternalAuth, ExternalUsers},
11    profile::DynProfileEditor,
12    registration::DynRegistrar,
13    tenant_profile::DynTenantEditor,
14};
15
16/// The panel's authentication configuration (ADR-0013).
17///
18/// The default is [`Auth::password`]; [`Auth::custom`] installs an app-owned
19/// [`Authenticator`]; [`Auth::external`] hands sign-in to the host app; [`Auth::disabled`] is the
20/// explicit fail-open opt-out.
21/// Sign-in attempts are throttled per login and address by default; see [`Auth::throttle`].
22/// [`Auth::account_throttle`] adds an opt-in count per login across addresses.
23/// [`Auth::registration`] adds a sign-up page, [`Auth::profile`] a profile page, and
24/// [`Auth::password_reset`] a forgot-password page that mails reset links through
25/// [`Auth::mail`], [`Auth::tenant_registration`] a page creating a tenant, and
26/// [`Auth::tenant_profile`] a page editing one.
27///
28/// `U` is the user type the authenticator loads. Every seam that reads or writes a user, from the
29/// registrar to the tenant editor, names the same type, so a seam over another user type does not
30/// compile.
31pub struct Auth<U = AdminUser> {
32    pub(super) config: AuthConfig,
33    user: PhantomData<fn() -> U>,
34}
35
36impl Auth {
37    /// The shipped Argon2id + [`AdminUser`] authenticator.
38    #[must_use]
39    pub fn password() -> Self {
40        Self::custom(PasswordAuth)
41    }
42
43    /// Explicit fail-open opt-out for public demos (ADR-0013): no gate, no
44    /// login routes, sessions unused.
45    ///
46    /// It is no way to put the panel behind an app's own sign-in: a layer over the prefix leaves
47    /// Topcoat's runtime path open, where the panel's search and other endpoints answer anyone.
48    /// [`Auth::external`] gates both.
49    #[must_use]
50    pub fn disabled() -> Self {
51        Self::new(AuthConfig::new(None, LoginThrottle::off))
52    }
53}
54
55impl<U: PanelUser> Auth<U> {
56    /// An app-owned authenticator over its own user table.
57    #[must_use]
58    pub fn custom<A: Authenticator<User = U>>(authenticator: A) -> Self {
59        let authenticator: Box<dyn DynAuthenticator> = Box::new(authenticator);
60        Self::new(AuthConfig::new(Some(authenticator), LoginThrottle::default))
61    }
62
63    /// A panel whose users sign in through the host app: no password form, and no credential of
64    /// Tablo's. See [`ExternalAuth`].
65    ///
66    /// Mounting refuses what needs Tablo to own the credentials:
67    /// [`registration`](Self::registration), [`password_reset`](Self::password_reset),
68    /// [`email_verification`](Self::email_verification), [`two_factor`](Self::two_factor),
69    /// [`impersonation`](Self::impersonation) and [`oidc`](Self::oidc).
70    #[must_use]
71    pub fn external<E: ExternalAuth<User = U>>(external: E) -> Self {
72        let external = Arc::new(external);
73        let mut auth = Self::custom(ExternalUsers(Arc::clone(&external)));
74        auth.config.external = Some(external);
75        auth
76    }
77
78    fn new(config: AuthConfig) -> Self {
79        Self {
80            config,
81            user: PhantomData,
82        }
83    }
84
85    /// How many sign-in attempts one login may make from one client address
86    /// in a window; defaults to five a minute. [`LoginThrottle::off`] counts nothing, for a proxy
87    /// that limits sign-ins itself.
88    #[must_use]
89    pub fn throttle(mut self, throttle: LoginThrottle) -> Self {
90        self.config.throttle = throttle;
91        self
92    }
93
94    /// How many sign-in attempts one login may make from all addresses together in a window;
95    /// off by default. It stops a guesser who rotates addresses, at the cost of letting anyone
96    /// who knows a login refuse its owner until the window ends, again each window; a successful
97    /// sign-in clears it. Turn it on where a guessed password costs more than a locked-out
98    /// owner, with a limit high enough that only a distributed guesser reaches it, such as
99    /// `LoginThrottle::new(50, Duration::from_hours(1))`.
100    #[must_use]
101    pub fn account_throttle(mut self, throttle: LoginThrottle) -> Self {
102        self.config.account_throttle = throttle;
103        self
104    }
105
106    /// Lets visitors create their own account at `{prefix}/register`, through `registrar`; the
107    /// login page links to it.
108    ///
109    /// ```rust
110    /// use tablo_core::{Auth, PasswordSignUp};
111    ///
112    /// let auth = Auth::password().registration(PasswordSignUp::pending());
113    /// # let _ = auth;
114    /// ```
115    ///
116    /// [`PasswordSignUp`](crate::PasswordSignUp) registers an [`AdminUser`](super::AdminUser) from
117    /// a [`SignUp`](super::SignUp), inactive until an administrator approves it or, when built
118    /// with `open`, active and signed in at once. A custom [`Registrar`] decides what a new
119    /// account may do; one whose user may not access the panel lands on the login page.
120    ///
121    /// - The form is any `FormInput`, or the shipped `SignUp`. `#[form(password)]` renders a masked
122    ///   control, with a button that shows what was typed, that a refused form renders empty, and
123    ///   reads the password as typed, outer spaces included, as the login compares it.
124    ///   `#[form(email)]` refuses a malformed address before `validate` runs. A missing field and a
125    ///   malformed value answer 422 with the error under the field, as `validate`'s refusals do;
126    ///   nothing is written.
127    /// - `new_password_errors` refuses a password under eight characters and a confirmation that
128    ///   differs, under the `password` and `password_confirmation` keys.
129    /// - After the commit, an account that may open the panel is signed in, its session rotated as
130    ///   a login rotates it, and lands on `next` or the panel's home page. One that may not, such
131    ///   as an account awaiting approval, lands on the login page with a notification instead. A
132    ///   signed-in user who opens the sign-up page lands on the panel's home page.
133    /// - The registrar's `User` is `U`. Mounting refuses a registration on `Auth::disabled()`, a
134    ///   form field named `csrf_token` or `next`, a file field and a relationship choice, whose
135    ///   options would list records to a visitor nobody signed in.
136    /// - Throttling. Each client address may submit five sign-ups a minute, counted whether or not
137    ///   they create an account; `Auth::sign_up_throttle` changes the limit. Past it the page
138    ///   answers 429.
139    ///
140    /// The sign-up page tells a visitor whether an email is taken, which the login page never does:
141    /// an app that must keep its accounts private verifies the address by email before saying so,
142    /// which Tablo does not ship.
143    #[must_use]
144    pub fn registration(mut self, registrar: impl Registrar<User = U>) -> Self {
145        self.config.registrar = Some(Box::new(registrar));
146        self
147    }
148
149    /// How many sign-ups one client address may submit in a window; defaults to five a minute.
150    /// Every sign-up POST that passes the CSRF check and parses counts, whether or not it creates
151    /// an account.
152    #[must_use]
153    pub fn sign_up_throttle(mut self, throttle: LoginThrottle) -> Self {
154        self.config.sign_up_throttle = throttle;
155        self
156    }
157
158    /// Lets the signed-in user edit their own account at `{prefix}/profile`, through `editor`;
159    /// the user menu links to it.
160    ///
161    /// ```rust
162    /// use tablo_core::{Auth, PasswordAuth};
163    ///
164    /// let auth = Auth::password().profile(PasswordAuth);
165    /// # let _ = auth;
166    /// ```
167    ///
168    /// [`PasswordAuth`] edits an [`AdminUser`](super::AdminUser)'s name, email and password through
169    /// a [`Profile`](super::Profile), asking for the current password before the email or the
170    /// password changes.
171    ///
172    /// - The form is any `FormInput`, rendered in the panel's shell. The page shows what the
173    ///   input's `write` writes, so `Profile` never shows a password. A disabled control keeps what
174    ///   `fill` wrote, whatever the POST carries. A refused save renders the form again with each
175    ///   error under its field; nothing is written.
176    /// - `save` answers `ProfileUpdate::Details`, which keeps every session, or
177    ///   `ProfileUpdate::Credentials` for a change to what the user signs in with. The user's
178    ///   sessions then end on every panel whose authenticator loads the same user type, in the
179    ///   transaction that saves, and the browser that saved gets a new session under the tenant it
180    ///   acted for. `PasswordAuth` answers `Credentials` for a new email or password.
181    /// - An email another account holds is reported only once the current password verifies, so a
182    ///   stolen session cannot probe which addresses have accounts.
183    /// - After the commit the page reloads with a notification.
184    /// - The editor's `User` is `U`. Mounting refuses a profile on `Auth::disabled()`, a form field
185    ///   named `csrf_token`, a file field and a relationship choice, whose options no route serves
186    ///   the page.
187    /// - Throttling. Each user may submit five saves a minute, from any address, counted whether or
188    ///   not they save; `Auth::profile_throttle` changes the limit. Past it the page answers 429,
189    ///   which keeps a stolen session from guessing the current password. A user past the limit can
190    ///   still sign in.
191    #[must_use]
192    pub fn profile(mut self, editor: impl ProfileEditor<User = U>) -> Self {
193        self.config.profile_editor = Some(Box::new(editor));
194        self
195    }
196
197    /// How many profile saves one user may submit in a window, from any address; defaults to five
198    /// a minute. Every profile POST that passes the CSRF check and parses counts, whether or not
199    /// it saves.
200    #[must_use]
201    pub fn profile_throttle(mut self, throttle: LoginThrottle) -> Self {
202        self.config.profile_throttle = throttle;
203        self
204    }
205
206    /// Lets a visitor who forgot their password ask for a reset link at
207    /// `{prefix}/forgot-password`, mailed through [`Auth::mail`], and choose a new password at
208    /// `{prefix}/reset-password`; the login page links to it.
209    ///
210    /// - The request page answers the same whether an account uses the address or not: it redirects
211    ///   to the login page with a notification, and writes and mails the link after answering, so
212    ///   the database and the mail server add no time when an account exists. An account that may
213    ///   not enter the panel gets no link, and a failed send is logged, not shown.
214    /// - The link goes to the stored address, never the typed one. A lookup that matches loosely,
215    ///   such as a collation that folds accents, would otherwise mail one account's link to another
216    ///   address.
217    /// - A link works once, for one hour, on the panel that mailed it, and only while the account
218    ///   may still enter the panel. A new request replaces the account's earlier link, and a
219    ///   password change on the profile page ends it, as it ends every link the account holds. The
220    ///   token is stored only as a hash, and the reset page sends no referrer and may not be
221    ///   cached.
222    /// - A reset checks the new password with `new_password_errors`, then sets it, ends the
223    ///   account's sessions and reset links on every panel whose authenticator loads the same user
224    ///   type, and redirects to the login page. All of this commits in one transaction.
225    /// - Throttling. Each address may request three links in fifteen minutes, from any client,
226    ///   counted whether or not an account uses it; `PasswordReset::throttle` changes the limit.
227    ///   Past it the page answers 429. A limit per client belongs at the proxy.
228    /// - The resetter's `User` is `U`. Mounting refuses a reset on `Auth::disabled()`, a reset
229    ///   without `Auth::mail`, a base URL that is not an absolute `http` or `https` URL or that
230    ///   carries a query or fragment, and a router without a `Mailer`.
231    #[must_use]
232    pub fn password_reset(mut self, reset: PasswordReset<U>) -> Self {
233        self.config.reset = Some(reset.erase());
234        self
235    }
236
237    /// Makes each signed-in user prove they read the address [`PanelUser::email`] answers before
238    /// they reach anything but `{prefix}/verify-email`, which mails them a link through
239    /// [`Auth::mail`]. A sign-up mails the link at once, after the response.
240    ///
241    /// - Until the user verifies, a page redirects to `{prefix}/verify-email` and any other request
242    ///   answers 403. The page offers to mail a link, and to sign out. A user whose
243    ///   `PanelUser::email` is `None` has no address to prove and is not held.
244    /// - Accounts that predate verification are unverified: turning it on sends each of them,
245    ///   seeded admins included, to `{prefix}/verify-email` at their next request. Backfill
246    ///   `email_verified_at` (from `created_at`, say) for the addresses you already trust.
247    /// - A link works once, for one day, on the panel that mailed it, and only while the account
248    ///   may enter the panel and still stores the address it was mailed to. It verifies on `GET` in
249    ///   any browser, signed in or not, then lands on the panel or on the login page. A mail
250    ///   scanner that opens the link verifies the address too: it fetched the link from the mailbox
251    ///   the link proves.
252    /// - A new email waits for its own link. A profile editor calls `auth::request_email_change`
253    ///   from `save` in place of writing the address: on a panel with email verification it records
254    ///   the change and answers `EmailChange::Pending`, and once the save commits the panel mails
255    ///   the new address a link that stores it and the old address a link that keeps it. Elsewhere
256    ///   it answers `EmailChange::Immediate` and the editor stores the address itself.
257    ///   `PasswordAuth` does this. The link proves only that whoever asked reads the new mailbox,
258    ///   so ask for the current password before calling it, as `PasswordAuth` does: a stolen
259    ///   session could name its thief's mailbox.
260    /// - Either link opens a page whose button does the work, so a mail scanner that fetches it
261    ///   changes nothing.
262    /// - The new address's link stores it, verified, once, for one day. It is a credential change:
263    ///   the account's sessions and sign-in links end on every panel signing in its user type, and
264    ///   the browser that confirmed keeps a new session when it was signed in as the account.
265    /// - The old address's link works for a week, before or after the change goes through: it keeps
266    ///   the old address, restoring it if the change went through, and ends every session, sign-in
267    ///   link and pending change the account holds, the cancelling browser's included: whoever
268    ///   asked for the change may hold one.
269    /// - Throttling. Each account may ask for three links in fifteen minutes;
270    ///   `EmailVerification::throttle` changes the limit. Past it the page answers 429.
271    /// - The verifier's `User` is `U`. Mounting refuses a verification on `Auth::disabled()`, and
272    ///   one without `Auth::mail`.
273    ///
274    /// [`PanelUser::email`]: super::PanelUser::email
275    #[must_use]
276    pub fn email_verification(mut self, verification: EmailVerification<U>) -> Self {
277        self.config.verification = Some(verification.erase());
278        self
279    }
280
281    /// Lets the signed-in user create a tenant to act for at `{prefix}/register-tenant`, through
282    /// `registration`'s [`TenantRegistrar`](super::TenantRegistrar); the tenant menu links to
283    /// it from one membership on. [`TenantRegistration::required`] sends a user who acts for no
284    /// tenant there first.
285    ///
286    /// The registrar's `Input` is the page's form; `validate` and `register` run in one
287    /// transaction, and `register` writes the tenant and the user's membership of it, answering
288    /// that [`Membership`](crate::Membership). The session then acts for the new tenant, and the
289    /// next request's [`find_by_id`](crate::Authenticator::find_by_id) must list it.
290    ///
291    /// The registrar's [`User`](super::TenantRegistrar::User) is `U`. Mounting refuses a
292    /// registration on [`Auth::disabled`], a form field named `csrf_token`, a file field and a
293    /// relationship choice.
294    #[must_use]
295    pub fn tenant_registration(mut self, registration: TenantRegistration<U>) -> Self {
296        self.config.tenant_registration = Some(registration.erase());
297        self
298    }
299
300    /// Lets the signed-in user edit the tenant they act for at `{prefix}/tenant/profile`, through
301    /// `editor`; the tenant menu links to it for a user [`TenantEditor::may_edit`] admits, and the
302    /// page answers 403 to anyone else. `fill` loads the form from the tenant, and `validate` and
303    /// `save` run in one transaction. The form names the tenant it was filled from, so one posted
304    /// after another tab switched tenants saves nothing.
305    ///
306    /// The editor's [`User`](TenantEditor::User) is `U`. Mounting refuses a settings page on
307    /// [`Auth::disabled`], a form field named `csrf_token` or `editing_tenant`, a file field and a
308    /// relationship choice.
309    #[must_use]
310    pub fn tenant_profile(mut self, editor: impl TenantEditor<User = U>) -> Self {
311        self.config.tenant_editor = Some(Box::new(editor));
312        self
313    }
314
315    /// Lets each signed-in user add an authenticator app at `{prefix}/two-factor`; the user menu
316    /// links to it. An account with one signs in with a code from it, or a recovery code, at
317    /// `{prefix}/two-factor-challenge` after its password.
318    ///
319    /// - Setting up happens at `{prefix}/two-factor`, which the user menu links to. It asks for the
320    ///   current password first, through `Authenticator::confirm_password`, so a stolen session
321    ///   cannot add a factor its thief holds. That method verifies with the user's
322    ///   `PanelUser::email` as the login; override it when users sign in with something else. The
323    ///   page then shows a QR code and the key, and turns two-factor sign-in on once a code from
324    ///   the app matches. It then shows eight recovery codes, once: the panel stores only their
325    ///   hashes, keyed like the secrets.
326    /// - Each panel enrols apart. A factor signs in to the panel that set it up only, even when
327    ///   other panels load the same user type: enrol on each.
328    /// - Signing in, a correct password leads to `{prefix}/two-factor-challenge` instead of a
329    ///   session. The challenge lasts five minutes in its own cookie and takes a code from the app
330    ///   or a recovery code. Each code works once: an app code does not sign in twice, and a
331    ///   recovery code is used up.
332    /// - Wrong codes. Every fifth wrong code in a row ends the challenge, so the next try takes the
333    ///   password again. Past the fifth, each code of the app or the mail waits for the one before
334    ///   it: 30 seconds, doubling up to an hour, across challenges, until a right code; a recovery
335    ///   code never waits. Each fifth mails the account, through `Auth::mail`, that its password
336    ///   signed in and codes failed. Someone holding the password can so keep the app's codes
337    ///   waiting and make the owner spend recovery codes; the mail tells the owner, and a new
338    ///   password ends it.
339    /// - Turning it on or off is a credential change: the account's other sessions and links end.
340    ///   Turning it off, and showing new recovery codes, take a code.
341    /// - Codes by email. `TwoFactor::new().email_codes()` lets a user with an address choose codes
342    ///   mailed to it in place of an app, through `Auth::mail`. Each sign-in mails a six-digit code
343    ///   that works once, for ten minutes; the challenge page mails a new one on request, in place
344    ///   of the last. Each account may have three mailed in fifteen minutes;
345    ///   `TwoFactor::mail_throttle` changes the limit. An account holds one factor at a time: it
346    ///   turns one off before setting up the other. A panel that drops `email_codes()` mails no
347    ///   more codes: an account set up for them signs in with a recovery code. A mail server that
348    ///   fails leaves the challenge up, with its button to send another code.
349    /// - Throttling. Each account may try five codes or passwords a minute, at sign-in and on the
350    ///   settings page together; `TwoFactor::throttle` changes the limit. Past it the page answers
351    ///   429. The count lives in each process, so behind several the limit holds per process,
352    ///   unless the throttle counts `in_database`. Someone who knows the password can use up the
353    ///   owner's attempts, and each new sign-in replaces the last challenge.
354    /// - The cookie `Key` seals the secrets. Rotating it makes every app code fail until its owner
355    ///   signs in with a recovery code and sets the app up again; a leaked key opens every secret.
356    /// - Requiring it. `TwoFactor::new().required()` sends a signed-in user without a second factor
357    ///   to `{prefix}/two-factor` before anything else, as an unverified address does: a page
358    ///   redirects there and any other request answers 403. `TwoFactor::required_for` does so for
359    ///   the users its rule names.
360    /// - Mounting refuses two-factor sign-in on `Auth::disabled()`, a router without a cookie
361    ///   `Key`, and codes by email without `Auth::mail`.
362    #[must_use]
363    pub fn two_factor(mut self, two_factor: TwoFactor) -> Self {
364        self.config.two_factor = Some(two_factor);
365        self
366    }
367
368    /// Sends the panel's auth mail, such as reset links, from `mail`'s sender, with links that
369    /// start with its base URL. Mounting refuses a base URL that is not absolute, and a router
370    /// without a [`Mailer`](topcoat::mail::Mailer).
371    #[must_use]
372    pub fn mail(mut self, mail: AuthMail) -> Self {
373        self.config.mail = Some(mail);
374        self
375    }
376
377    /// Whether the explicit opt-out is set.
378    #[must_use]
379    pub fn is_disabled(&self) -> bool {
380        self.config.is_disabled()
381    }
382
383    /// The declaration, its user type checked.
384    pub(crate) fn into_config(self) -> AuthConfig {
385        self.config
386    }
387}
388
389impl Default for Auth {
390    fn default() -> Self {
391        Self::password()
392    }
393}
394
395impl<U> std::fmt::Debug for Auth<U> {
396    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
397        self.config.fmt(f)
398    }
399}
400
401/// What an [`Auth`] declares, with the user type erased once the builder has checked it.
402pub(crate) struct AuthConfig {
403    authenticator: Option<Box<dyn DynAuthenticator>>,
404    external: Option<Arc<dyn DynExternal>>,
405    throttle: LoginThrottle,
406    account_throttle: LoginThrottle,
407    pub(super) registrar: Option<Box<dyn DynRegistrar>>,
408    sign_up_throttle: LoginThrottle,
409    pub(super) profile_editor: Option<Box<dyn DynProfileEditor>>,
410    profile_throttle: LoginThrottle,
411    pub(super) reset: Option<PasswordReset<()>>,
412    pub(super) mail: Option<AuthMail>,
413    pub(super) verification: Option<EmailVerification<()>>,
414    pub(super) tenant_registration: Option<TenantRegistration<()>>,
415    pub(super) tenant_editor: Option<Box<dyn DynTenantEditor>>,
416    pub(super) two_factor: Option<TwoFactor>,
417    pub(super) impersonation: Option<super::impersonation::ImpersonationConfig>,
418    pub(super) oidc: Option<super::oidc::Oidc<()>>,
419}
420
421impl AuthConfig {
422    /// `authenticator` with no feature on, its default throttles from `throttle`.
423    fn new(
424        authenticator: Option<Box<dyn DynAuthenticator>>,
425        throttle: fn() -> LoginThrottle,
426    ) -> Self {
427        Self {
428            authenticator,
429            external: None,
430            throttle: throttle(),
431            account_throttle: LoginThrottle::off(),
432            registrar: None,
433            sign_up_throttle: throttle(),
434            profile_editor: None,
435            profile_throttle: throttle(),
436            reset: None,
437            mail: None,
438            verification: None,
439            tenant_registration: None,
440            tenant_editor: None,
441            two_factor: None,
442            impersonation: None,
443            oidc: None,
444        }
445    }
446
447    /// Whether the explicit opt-out is set.
448    pub(crate) fn is_disabled(&self) -> bool {
449        self.authenticator.is_none()
450    }
451
452    /// The authenticator, or `None` when auth is disabled.
453    pub(crate) fn authenticator(&self) -> Option<&dyn DynAuthenticator> {
454        self.authenticator.as_deref()
455    }
456
457    /// The host app's sign-in, when [`Auth::external`] installs it.
458    pub(crate) fn external(&self) -> Option<&dyn DynExternal> {
459        self.external.as_deref()
460    }
461
462    /// The sign-in throttle.
463    pub(crate) fn login_throttle(&self) -> &LoginThrottle {
464        &self.throttle
465    }
466
467    /// The sign-in throttle counted per login across addresses.
468    pub(crate) fn account_throttle_of(&self) -> &LoginThrottle {
469        &self.account_throttle
470    }
471
472    /// The registrar, or `None` without a sign-up page.
473    pub(crate) fn registrar(&self) -> Option<&dyn DynRegistrar> {
474        self.authenticator.as_ref().and(self.registrar.as_deref())
475    }
476
477    /// The sign-up throttle, counted per client address.
478    pub(crate) fn throttle_sign_ups(&self) -> &LoginThrottle {
479        &self.sign_up_throttle
480    }
481
482    /// The profile editor, or `None` without a profile page.
483    pub(crate) fn profile_editor(&self) -> Option<&dyn DynProfileEditor> {
484        self.authenticator
485            .as_ref()
486            .and(self.profile_editor.as_deref())
487    }
488
489    /// The profile throttle, counted per user.
490    pub(crate) fn throttle_profile(&self) -> &LoginThrottle {
491        &self.profile_throttle
492    }
493
494    /// Whether any of the panel's throttles counts in the database, which then needs
495    /// [`AuthThrottle`](super::AuthThrottle).
496    pub(crate) fn throttles_in_database(&self) -> bool {
497        let reset = self.reset().map(|reset| &reset.throttle);
498        let verification = self
499            .verification()
500            .map(|verification| &verification.throttle);
501        let two_factor = self
502            .two_factor_config()
503            .into_iter()
504            .flat_map(|two_factor| [&two_factor.throttle, &two_factor.mail_throttle]);
505        [
506            &self.throttle,
507            &self.account_throttle,
508            &self.sign_up_throttle,
509            &self.profile_throttle,
510        ]
511        .into_iter()
512        .chain(reset)
513        .chain(verification)
514        .chain(two_factor)
515        .any(LoginThrottle::counts_in_database)
516    }
517
518    /// The password reset, or `None` without a forgot-password page.
519    pub(crate) fn reset(&self) -> Option<&PasswordReset<()>> {
520        self.authenticator.as_ref().and(self.reset.as_ref())
521    }
522
523    /// The email verification, or `None` when the panel asks for none.
524    pub(crate) fn verification(&self) -> Option<&EmailVerification<()>> {
525        self.authenticator.as_ref().and(self.verification.as_ref())
526    }
527
528    /// The tenant registration, or `None` without a tenant registration page.
529    pub(crate) fn tenant_registration_page(&self) -> Option<&TenantRegistration<()>> {
530        self.authenticator
531            .as_ref()
532            .and(self.tenant_registration.as_ref())
533    }
534
535    /// The tenant editor, or `None` without a tenant settings page.
536    pub(crate) fn tenant_editor(&self) -> Option<&dyn DynTenantEditor> {
537        self.authenticator
538            .as_ref()
539            .and(self.tenant_editor.as_deref())
540    }
541
542    /// The two-factor sign-in, or `None` when the panel asks for one factor.
543    pub(crate) fn two_factor_config(&self) -> Option<&TwoFactor> {
544        self.authenticator.as_ref().and(self.two_factor.as_ref())
545    }
546
547    /// The provider sign-in, or `None` when the panel offers none.
548    pub(crate) fn oidc(&self) -> Option<&super::oidc::Oidc<()>> {
549        self.authenticator.as_ref().and(self.oidc.as_ref())
550    }
551
552    /// The auth mail, or `None` when the panel mails nothing.
553    pub(crate) fn auth_mail(&self) -> Option<&AuthMail> {
554        self.authenticator.as_ref().and(self.mail.as_ref())
555    }
556
557    /// The features that send mail, by name, for the mount's checks.
558    pub(super) fn mailing(&self) -> Vec<&'static str> {
559        [
560            self.reset.is_some().then_some("password_reset"),
561            self.verification.is_some().then_some("email_verification"),
562            self.two_factor
563                .as_ref()
564                .is_some_and(|two_factor| two_factor.mails())
565                .then_some("two_factor"),
566        ]
567        .into_iter()
568        .flatten()
569        .collect()
570    }
571
572    /// Whether the panel issues [`AuthToken`](super::AuthToken)s, so the model must be registered:
573    /// it mails links, or challenges a second factor.
574    pub(crate) fn issues_tokens(&self) -> bool {
575        self.authenticator.is_some() && (!self.mailing().is_empty() || self.two_factor.is_some())
576    }
577}
578
579impl std::fmt::Debug for AuthConfig {
580    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
581        if self.is_disabled() {
582            f.write_str("Auth::disabled")
583        } else {
584            f.debug_struct("Auth")
585                .field("throttle", &self.throttle)
586                .field("account_throttle", &self.account_throttle)
587                .field("registration", &self.registrar.is_some())
588                .field("profile", &self.profile_editor.is_some())
589                .field("password_reset", &self.reset)
590                .field("email_verification", &self.verification)
591                .field("tenant_registration", &self.tenant_registration)
592                .field("tenant_profile", &self.tenant_editor.is_some())
593                .field("two_factor", &self.two_factor)
594                .field("impersonation", &self.impersonation)
595                .field("oidc", &self.oidc)
596                .field("mail", &self.mail)
597                .finish_non_exhaustive()
598        }
599    }
600}