Skip to main content

AdminSession

Struct AdminSession 

Source
pub struct AdminSession {
    pub token_hash: String,
    pub user_id: Uuid,
    pub csrf_token: String,
    pub state: String,
    pub mfa_attempts: i64,
    pub created_at: i64,
    pub expires_at: i64,
    pub last_seen_at: i64,
    pub created_ip: Option<String>,
    pub user_agent: Option<String>,
}
Expand description

One logged-in browser session of an crate::admin_user::AdminUser.

This layer never sees the session token. token_hash arrives already hashed from webadmin::session, which is the only place the plaintext exists: it goes into a Set-Cookie and is never written down. A read of this table – a backup, a .dump, an injection – therefore yields nothing that can be replayed. That is also why there is no bare find_by_id: the hash is the lookup key, and knowing it means already holding the token.

AdminSession::find_by_user_and_fingerprint is not that: it resolves the displayed fingerprint (see to_json’s id) back to a row, but only within one caller-supplied user_id – so reaching a row still requires already being authorized to see that user’s sessions, the web panel’s “revoke one of these” buttons being the only reason it exists.

§Methods

  • create: persist a session for a user
  • find_by_token_hash: the per-request resolution path
  • find_by_user_and_fingerprint: resolve a displayed id back to a row, scoped to the one user it can belong to – see the note there
  • touch: advance the idle deadline
  • delete / delete_for_user / delete_for_user_except: logout, revoke all, and the “keep the session doing the changing” case a password change needs
  • list_all: admin CLI visibility
  • cleanup: the reaper’s sweep
  • to_json: admin-facing rendering (never the token hash or the CSRF token)

Fields§

§token_hash: String

Hex-encoded SHA-256 of the cookie’s bearer token.

§user_id: Uuid§csrf_token: String

Per-session CSRF token, plaintext: it authorises nothing on its own.

§state: String

active, or pending_mfa once a second factor exists to be outstanding.

§mfa_attempts: i64

Second-factor codes rejected against this session. Advanced only by AdminSession::record_mfa_failure, which is where the reasoning is.

§created_at: i64§expires_at: i64

Absolute deadline. Never extended.

§last_seen_at: i64

Idle deadline, advanced by AdminSession::touch.

§created_ip: Option<String>

Forensics only – never compared against the request being served.

§user_agent: Option<String>

Implementations§

Source§

impl AdminSession

Source

pub async fn create( new: NewSession<'_>, ttl: Duration, database: &Database, ) -> Result<AdminSession, Error>

Persists a fresh active session expiring ttl from now.

Source

pub async fn create_pending( new: NewSession<'_>, ttl: Duration, database: &Database, ) -> Result<AdminSession, Error>

Persists a fresh pending_mfa session: a password was accepted and nothing more.

ttl is webadmin::session::PENDING_MFA_TTL, not the configured session lifetime – a half-authenticated row should not outlive the tab that created it, and that short absolute deadline is one of the two bounds on how long an attacker holding a password may keep guessing codes (the other is AdminSession::record_mfa_failure).

The reaper needs no special case for these: expires_at is set the same way, so cleanup’s existing expires_at <= ? sweeps them.

Source

pub async fn promote( pending_token_hash: &str, new_token_hash: &str, new_csrf_token: &str, ttl: Duration, database: &Database, ) -> Result<Option<AdminSession>, Error>

Replaces a pending_mfa session with a fresh active one: a new token, a new CSRF token, a full ttl, and the same user and forensics.

A rotation, not an UPDATE state. The pending token is a real bearer token that a browser stored and that has crossed the wire, minted before authentication completed; its privilege level changing means its value changes – the same rule that makes sign_in delete whatever session the request already carried. The CSRF token rotates for free, which matters because the challenge page had to be handed one.

One transaction, and the DELETE’s rows_affected is the concurrency guard: two submissions of one code promote exactly once. None means nothing pending sat under pending_token_hash – already promoted, already swept, or never there.

The side effect worth having: with no setter for state anywhere, the column is write-once at INSERT, so no code path can move a session between states in place.

Source

pub async fn record_mfa_failure( token_hash: &str, database: &Database, ) -> Result<Option<i64>, Error>

Counts one rejected second-factor code against this session, returning the new total.

The bound an attacker cannot shed. webadmin::session::LoginLimiter keys on the peer address, and a pending_mfa cookie is valid from any address on purpose (see created_ip: pinning breaks CGNAT and mobile). Somebody holding a correct password could therefore mint one pending session and spend admin.login_max_attempts guesses per source address, which a single IPv6 /64 supplies 2^64 of. This counter travels with the session instead, so rotating addresses buys nothing.

One UPDATE ... RETURNING, which is also what makes it race-free: this listener carries no admission control by design, so a read-then-write would let K concurrent submissions all observe zero and each get a free guess. None means the row is already gone – swept, promoted, or deleted by a concurrent submission that hit the cap first.

Source

pub async fn find_by_token_hash( token_hash: &str, database: &Database, ) -> Result<Option<AdminSession>, Error>

The per-request resolution path. Returns the row whatever its state – expiry, idleness and state are the caller’s to judge, since each maps to a different refusal.

Source

pub async fn find_by_user_and_fingerprint( user_id: Uuid, fingerprint_id: &str, database: &Database, ) -> Result<Option<AdminSession>, Error>

Resolves a session’s displayed fingerprint (to_json’s id) back to a row, scoped to user_id – the web panel’s “revoke this session” button, for either the caller’s own sessions or, on the operators surface, another operator’s.

Scoping to one user rather than searching every session by fingerprint is the whole security property: a caller who has not already been authorized to act on user_id cannot reach a row this way even by observing its fingerprint elsewhere. Built over Self::list_all rather than a dedicated query – one operator’s live sessions are a small scan, and the fingerprint is derived, not a column, so there is nothing for SQL to compare it against directly.

Source

pub async fn touch(&mut self, database: &Database) -> Result<(), Error>

Advances the idle deadline. Callers rate-limit this – see webadmin::session::SESSION_TOUCH_INTERVAL – because a polling page would otherwise take the WAL writer lock on every request.

Source

pub async fn delete( token_hash: &str, database: &Database, ) -> Result<bool, Error>

Logout. Returns whether a row existed.

Source

pub async fn delete_for_user( user_id: Uuid, database: &Database, ) -> Result<u64, Error>

Revokes every session of one user – admin session revoke --user, and the “log me out everywhere” case. Returns how many went.

Source

pub async fn delete_for_user_except( user_id: Uuid, keep_token_hash: &str, database: &Database, ) -> Result<u64, Error>

Revokes every session of one user except the one named – what a password change needs, so the operator making it is not logged out by their own action while every other browser is.

Source

pub async fn delete_all(database: &Database) -> Result<u64, Error>

Revokes every session on the server – admin session revoke --all, the “log everybody out” lever after a scare. Returns how many went.

Source

pub async fn list_all( user_id: Option<Uuid>, database: &Database, ) -> Result<Vec<AdminSession>, Error>

Every session, or every session of one user, newest first.

A scan, not a listing: its one caller is admin::users::confirm_delete_user, which counts what the delete will cascade to so the prompt can name it. Nothing renders it – see AdminUser::list_all for why that is what lets it sit beside AdminSession::search.

Source

pub async fn search( user_id: Option<Uuid>, limit: i64, offset: i64, database: &Database, ) -> Result<(Vec<AdminSession>, i64), Error>

One page of the same listing, plus the total those filters match unpaged.

admin session list’s window. Newest first like Self::list_all, and tie-broken on token_hash for the same reason Eab::search breaks on kid: created_at is a whole second, and a browser signing in twice lands two rows inside one.

Source

pub async fn cleanup( idle_timeout: Duration, database: &Database, ) -> Result<u64, Error>

The reaper’s sweep: everything past its absolute deadline, plus everything idle longer than idle_timeout. Unlike nonces, sessions outlive a restart, so a startup-only sweep would leak.

Source

pub fn is_expired(&self, now: i64) -> bool

Past its absolute deadline.

Source

pub fn is_idle(&self, now: i64, idle_timeout: Duration) -> bool

Unused for longer than idle_timeout.

Source

pub fn is_active(&self) -> bool

Whether the session has completed authentication. A pending_mfa session has a valid password behind it and nothing more.

Source

pub fn to_json(&self) -> Value

The admin-facing rendering. Never the token hash (it is the lookup key, and printing it in admin session list would put every live session’s key on a terminal) nor the CSRF token.

id is a fingerprint of the hash: enough to name one session to admin session revoke, not enough to reconstruct the key.

Trait Implementations§

Source§

impl Clone for AdminSession

Source§

fn clone(&self) -> Self

Returns a duplicate of the value. Read more
1.0.0 (const: unstable) · Source§

fn clone_from(&mut self, source: &Self)

Performs copy-assignment from source. Read more
Source§

impl Debug for AdminSession

Source§

fn fmt(&self, f: &mut Formatter<'_>) -> Result

Formats the value using the given formatter. Read more

Auto Trait Implementations§

Blanket Implementations§

Source§

impl<T> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

impl<'a, T, E> AsTaggedExplicit<'a, E> for T
where T: 'a,

Source§

fn explicit(self, class: Class, tag: u32) -> TaggedParser<'a, Explicit, Self, E>

Source§

impl<'a, T, E> AsTaggedImplicit<'a, E> for T
where T: 'a,

Source§

fn implicit( self, class: Class, constructed: bool, tag: u32, ) -> TaggedParser<'a, Implicit, Self, E>

Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
Source§

impl<T> CloneToUninit for T
where T: Clone,

Source§

unsafe fn clone_to_uninit(&self, dest: *mut u8)

🔬This is a nightly-only experimental API. (clone_to_uninit)
Performs copy-assignment from self to dest. Read more
Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

Source§

impl<T> FromRef<T> for T
where T: Clone,

Source§

fn from_ref(input: &T) -> T

Converts to this type from a reference to the input type.
Source§

impl<A, B, T> HttpServerConnExec<A, B> for T
where B: Body,

Source§

impl<T> Instrument for T

Source§

fn instrument(self, span: Span) -> Instrumented<Self> ⓘ

Instruments this type with the provided Span, returning an Instrumented wrapper. Read more
Source§

fn in_current_span(self) -> Instrumented<Self> ⓘ

Instruments this type with the current Span, returning an Instrumented wrapper. Read more
Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

Source§

impl<T> IntoEither for T

Source§

fn into_either(self, into_left: bool) -> Either<Self, Self> ⓘ

Converts 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 more
Source§

fn into_either_with<F>(self, into_left: F) -> Either<Self, Self> ⓘ
where F: FnOnce(&Self) -> bool,

Converts 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
Source§

impl<T> Same for T

Source§

type Output = T

Should always be Self
Source§

impl<T> ToOwned for T
where T: Clone,

Source§

type Owned = T

The resulting type after obtaining ownership.
Source§

fn to_owned(&self) -> T

Creates owned data from borrowed data, usually by cloning. Read more
Source§

fn clone_into(&self, target: &mut T)

Uses borrowed data to replace owned data, usually by cloning. Read more
Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = !

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, !>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.
Source§

impl<T> WithSubscriber for T

Source§

fn with_subscriber<S>(self, subscriber: S) -> WithDispatch<Self> ⓘ
where S: Into<Dispatch>,

Attaches the provided Subscriber to this type, returning a WithDispatch wrapper. Read more
Source§

fn with_current_subscriber(self) -> WithDispatch<Self> ⓘ

Attaches the current default Subscriber to this type, returning a WithDispatch wrapper. Read more