pub struct AdminUser {Show 13 fields
pub id: Uuid,
pub username: String,
pub password_hash: String,
pub status: String,
pub role: Option<String>,
pub totp_secret: Option<Vec<u8>>,
pub totp_pending_secret: Option<Vec<u8>>,
pub totp_last_step: Option<i64>,
pub created_at: i64,
pub updated_at: i64,
pub last_login_at: Option<i64>,
pub contact_email: Option<String>,
pub known_login_ips: Vec<String>,
}Expand description
An operator of the web admin interface.
Not an ACME concept and never joined to one: an AdminUser is a person
with a password, an accounts row is a client key. There is no profile
column – an admin user sees every endpoint this process serves.
§Methods
create: persist a new operator,activefind_by_id/find_by_username: lookup (the latter is the login path)list_all: every operator, oldest firstset_password_hash/set_status/set_role/mark_logged_in: in-place updatesrole: the privilege tier,NULLresolved toAdminRole::Adminset_totp_pending/confirm_totp/clear_totp/claim_totp_step: the second factor’s lifecycle, and RFC 6238 §5.2’s replay guarddelete: remove, cascading to the operator’s sessions and recovery codesto_json: admin-facing rendering (never the password hash, never a secret)
Fields§
§id: Uuid§username: StringAlways lowercase: AdminUser::create normalizes before writing, so
Alice and alice cannot become two logins that read as one.
password_hash: StringThe encoded KDF output – see admin::password. Never rendered.
status: String§role: Option<String>The privilege tier, raw from the column. None is a row that predates
the role column and reads as AdminRole::Admin; use
AdminUser::role rather than matching this directly.
totp_secret: Option<Vec<u8>>Set once the owner has proven a code against a pending enrolment.
None means no second factor is configured.
totp_pending_secret: Option<Vec<u8>>An enrolment begun but not yet confirmed. Not a usable second factor.
totp_last_step: Option<i64>The last TOTP time step accepted, so a code cannot be replayed inside its own window.
created_at: i64§updated_at: i64§last_login_at: Option<i64>§contact_email: Option<String>Where to send this operator security notifications (a completed sign-in
from an unfamiliar address, a refused second factor, a credential
change). None means none are sent – the event is still logged.
known_login_ips: Vec<String>The operator’s recent distinct login addresses, most-recent-first,
capped at KNOWN_LOGIN_IPS. Compared against the live request, but
only to decide whether to notify – never to authorise. Persisted as
a JSON array (the accounts.contact convention).
Implementations§
Source§impl AdminUser
impl AdminUser
Sourcepub async fn create(
username: &str,
password_hash: &str,
role: Option<AdminRole>,
database: &Database,
) -> Result<AdminUser, Error>
pub async fn create( username: &str, password_hash: &str, role: Option<AdminRole>, database: &Database, ) -> Result<AdminUser, Error>
Persists a new operator, active. username is lowercased and trimmed
here rather than at the call sites, so every path – the CLI, a future
API – stores the same thing.
password_hash is already encoded by admin::password: this
layer never sees a plaintext password and cannot hash one.
role is written in the same INSERT. None leaves the column
NULL, which reads as AdminRole::Admin – the safe default for the
bootstrap operator, and what every row created before the column existed
holds. It used to be the only option, with admin::users::create_user
calling AdminUser::set_role afterwards for a narrower tier; that made
admin user create --role viewer two writes, so a failure between them
left an operator at full admin with their password already set.
A duplicate username surfaces as the UNIQUE violation it is; the caller
(admin::users::create_user) checks first and reports it in words.
Sourcepub async fn find_by_id(
id: Uuid,
database: &Database,
) -> Result<Option<AdminUser>, Error>
pub async fn find_by_id( id: Uuid, database: &Database, ) -> Result<Option<AdminUser>, Error>
Looks an operator up by id: the session path, which carries the id.
Sourcepub async fn find_by_username(
username: &str,
database: &Database,
) -> Result<Option<AdminUser>, Error>
pub async fn find_by_username( username: &str, database: &Database, ) -> Result<Option<AdminUser>, Error>
The login path. Lowercases the argument for the same reason
AdminUser::create does – a login typed Alice must find alice.
Sourcepub async fn list_all(database: &Database) -> Result<Vec<AdminUser>, Error>
pub async fn list_all(database: &Database) -> Result<Vec<AdminUser>, Error>
Every operator, oldest first.
A scan, not a listing: its one caller is
admin::mfa::operators_without_a_factor, which counts the operators
with no confirmed factor for the admin.require_mfa startup warning.
Nothing renders it, which is why it can sit beside AdminUser::search
without being the second listing the paging pass deleted
Account::list_all for – an order nothing displays cannot disagree
with the paged one.
Sourcepub async fn search(
limit: i64,
offset: i64,
database: &Database,
) -> Result<(Vec<AdminUser>, i64), Error>
pub async fn search( limit: i64, offset: i64, database: &Database, ) -> Result<(Vec<AdminUser>, i64), Error>
One page of admin user list, plus the total the table holds unpaged.
Oldest first, and the one listing in the binary that is: every other
paged listing puts the newest row on top, but the bootstrap operator –
the one created before the panel could be signed in to at all – is
precisely the row whose position should not move as colleagues are
added. id breaks the created_at tie for Eab::search’s reason:
created_at is a whole second, and operators are created in one go.
Sourcepub async fn set_password_hash(
&mut self,
password_hash: &str,
database: &Database,
) -> Result<(), Error>
pub async fn set_password_hash( &mut self, password_hash: &str, database: &Database, ) -> Result<(), Error>
Replaces the stored hash. Callers are responsible for invalidating the
owner’s sessions – admin::users::set_password does, and a password
change that left them alive would be a change in name only.
Sourcepub async fn set_status(
&mut self,
status: &str,
database: &Database,
) -> Result<(), Error>
pub async fn set_status( &mut self, status: &str, database: &Database, ) -> Result<(), Error>
Moves between active and disabled. A disabled operator cannot log
in, and an existing session of theirs is refused on its next use –
the session rows are left for the reaper rather than deleted here, so
re-enabling is a single UPDATE either way.
Sourcepub async fn set_role(
&mut self,
role: AdminRole,
database: &Database,
) -> Result<(), Error>
pub async fn set_role( &mut self, role: AdminRole, database: &Database, ) -> Result<(), Error>
Sets the privilege tier. Callers revoke the operator’s sessions –
admin::users::set_role does, matching a disable and a password
change; the write extractors also re-read role every request, so a
demotion takes effect on the next call regardless.
Sourcepub async fn set_contact_email(
&mut self,
email: Option<&str>,
database: &Database,
) -> Result<(), Error>
pub async fn set_contact_email( &mut self, email: Option<&str>, database: &Database, ) -> Result<(), Error>
Sets (or clears, with None) the address this operator receives security
notifications at. Not a credential – no session is revoked. The address
shape is the caller’s to validate (admin::users::set_contact_email);
this layer only stores what it is handed.
Sourcepub async fn set_totp_pending(
&mut self,
secret: &[u8],
database: &Database,
) -> Result<(), Error>
pub async fn set_totp_pending( &mut self, secret: &[u8], database: &Database, ) -> Result<(), Error>
Stores an enrolment the owner has not yet proven a code against.
Not a usable second factor: AdminUser::has_totp stays false until
AdminUser::confirm_totp moves it across, which is what stops an
abandoned enrolment from locking its own owner out.
Sourcepub async fn confirm_totp(&mut self, database: &Database) -> Result<(), Error>
pub async fn confirm_totp(&mut self, database: &Database) -> Result<(), Error>
Promotes the pending secret to the real one.
One statement, deliberately: a half-applied enrolment would leave the
operator believing they have a factor that nothing checks, or holding a
pending secret alongside a live one. totp_last_step is cleared with
them – the replay guard belongs to the secret it was recorded against.
A no-op when nothing is pending, so a double-submit cannot clear a live factor.
Sourcepub async fn clear_totp(&mut self, database: &Database) -> Result<(), Error>
pub async fn clear_totp(&mut self, database: &Database) -> Result<(), Error>
Removes the factor, any half-finished enrolment and the replay guard together. Callers drop the recovery codes too – a code that recovers access to a factor that no longer exists is a second password.
Sourcepub async fn claim_totp_step(
&mut self,
step: i64,
database: &Database,
) -> Result<bool, Error>
pub async fn claim_totp_step( &mut self, step: i64, database: &Database, ) -> Result<bool, Error>
Records step as accepted, refusing one that is not strictly newer than
the stored value – RFC 6238 §5.2’s replay guard.
The comparison lives in the WHERE clause rather than in Rust: a code
observed in flight and resubmitted inside its own 30-second window must
not be accepted twice, and with two requests racing it is
rows_affected that decides which one was first. Same primitive as
Nonce::verify.
Sourcepub async fn mark_logged_in(
&mut self,
client_ip: Option<&str>,
database: &Database,
) -> Result<(), Error>
pub async fn mark_logged_in( &mut self, client_ip: Option<&str>, database: &Database, ) -> Result<(), Error>
Stamps last_login_at, and folds client_ip into known_login_ips
(move-to-front, deduplicated, capped at KNOWN_LOGIN_IPS). Advisory
only – nothing authorises on either column; the address set exists so a
sign-in from an unfamiliar address can be noticed.
Called when a login completes, which for an operator with a second
factor is one request later than the password being accepted. A caller
that needs the pre-login address set (to decide whether this sign-in
is from a new address) must read known_login_ips before calling.
Sourcepub async fn delete(id: Uuid, database: &Database) -> Result<bool, Error>
pub async fn delete(id: Uuid, database: &Database) -> Result<bool, Error>
Removes the operator. Their sessions go with them via the schema’s
ON DELETE CASCADE, which needs foreign_keys on – Database::open
and connect_in_memory both pin it. Returns whether a row existed.
Sourcepub fn role(&self) -> AdminRole
pub fn role(&self) -> AdminRole
The privilege tier, with NULL resolved to AdminRole::Admin. Match
on this, never on the raw AdminUser::role field.
Sourcepub fn has_totp(&self) -> bool
pub fn has_totp(&self) -> bool
Whether a confirmed second factor is configured. A pending enrolment does not count – it has never been proven against a code.
Sourcepub fn has_pending_totp(&self) -> bool
pub fn has_pending_totp(&self) -> bool
Whether an enrolment is half-finished: a secret was generated and shown, and no code has proven it yet.
Deliberately not folded into AdminUser::has_totp and deliberately
not in AdminUser::to_json: the login path must treat this operator as
having no factor, and the only surface that cares is the enrolment
page deciding whether to offer “start over”.
Trait Implementations§
Auto Trait Implementations§
impl Freeze for AdminUser
impl RefUnwindSafe for AdminUser
impl Send for AdminUser
impl Sync for AdminUser
impl Unpin for AdminUser
impl UnsafeUnpin for AdminUser
impl UnwindSafe for AdminUser
Blanket Implementations§
Source§impl<'a, T, E> AsTaggedExplicit<'a, E> for Twhere
T: 'a,
impl<'a, T, E> AsTaggedExplicit<'a, E> for Twhere
T: 'a,
Source§impl<'a, T, E> AsTaggedImplicit<'a, E> for Twhere
T: 'a,
impl<'a, T, E> AsTaggedImplicit<'a, E> for Twhere
T: 'a,
Source§impl<T> BorrowMut<T> for Twhere
T: ?Sized,
impl<T> BorrowMut<T> for Twhere
T: ?Sized,
Source§fn borrow_mut(&mut self) -> &mut T
fn borrow_mut(&mut self) -> &mut T
Source§impl<T> CloneToUninit for Twhere
T: Clone,
impl<T> CloneToUninit for Twhere
T: Clone,
impl<A, B, T> HttpServerConnExec<A, B> for Twhere
B: Body,
Source§impl<T> Instrument for T
impl<T> Instrument for T
Source§fn instrument(self, span: Span) -> Instrumented<Self> ⓘ
fn instrument(self, span: Span) -> Instrumented<Self> ⓘ
Source§fn in_current_span(self) -> Instrumented<Self> ⓘ
fn in_current_span(self) -> Instrumented<Self> ⓘ
Source§impl<T> IntoEither for T
impl<T> IntoEither for T
Source§fn into_either(self, into_left: bool) -> Either<Self, Self> ⓘ
fn into_either(self, into_left: bool) -> Either<Self, Self> ⓘ
self into a Left variant of Either<Self, Self>
if into_left is true.
Converts self into a Right variant of Either<Self, Self>
otherwise. Read moreSource§fn into_either_with<F>(self, into_left: F) -> Either<Self, Self> ⓘ
fn into_either_with<F>(self, into_left: F) -> Either<Self, Self> ⓘ
self into a Left variant of Either<Self, Self>
if into_left(&self) returns true.
Converts self into a Right variant of Either<Self, Self>
otherwise. Read more