feat: complete session preferences and PAM fixture gates

This commit is contained in:
2026-09-26 02:44:29 +05:30
parent 83345a1842
commit ee6186ed0b
34 changed files with 801 additions and 327 deletions

View File

@@ -14,90 +14,61 @@ use crate::{
error::{Error, ResultExt},
};
/// An account that can be offered by the login screen.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Identity {
/// Canonical username.
pub username: String,
/// Numeric user ID.
pub uid: u32,
/// Primary group ID.
pub gid: u32,
/// Home directory.
pub home: PathBuf,
/// Login shell.
pub shell: PathBuf,
}
/// Effective account state determined from the login shell and shadow aging data.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AccountState {
/// Account can be offered for login.
Active,
/// Password is disabled by the shadow lock marker.
Locked,
/// Password must be changed before login can continue.
PasswordExpired,
/// Account expiration date has elapsed.
AccountExpired,
/// Password inactivity period has elapsed.
AccountInactive,
/// Login shell explicitly disallows interactive sessions.
NoLoginShell,
/// Shadow data was not available or could not be parsed.
Unknown,
}
/// Non-sensitive password and account aging information.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct AccountStatus {
/// Effective state used by the chooser and diagnostics.
pub state: AccountState,
/// Day the password was last changed, if present.
pub password_changed_day: Option<i64>,
/// Day the password expires, if the policy specifies one.
pub password_expiry_day: Option<i64>,
/// Day the account expires, if specified.
pub account_expiry_day: Option<i64>,
/// Day after which an expired password disables the account.
pub inactive_after_day: Option<i64>,
}
impl AccountStatus {
/// Returns whether this account may be presented as an active login choice.
pub const fn is_login_allowed(self) -> bool {
matches!(self.state, AccountState::Active)
}
}
/// An identity paired with its non-sensitive account policy state.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AccountRecord {
/// Discovered NSS identity.
pub identity: Identity,
/// Current local account status.
pub status: AccountStatus,
}
/// A currently logged-in process obtained from UTMPX.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ActiveSession {
/// Login name associated with the session.
pub username: String,
/// UTMPX line, such as `tty2` or `pts/0`.
pub terminal: String,
/// Session leader PID when reported by UTMPX.
pub pid: libc::pid_t,
/// Remote host, if supplied by the session producer.
pub host: Option<String>,
}
/// Runtime session-management backend detected on this host.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SessionBackend {
/// systemd-logind runtime markers are available.
SystemdLogind,
/// UTMPX is available as a conservative fallback.
Utmpx,
}
@@ -113,15 +84,10 @@ struct ShadowEntry {
/// A non-authoritative account lockout summary.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct FaillockStatus {
/// Whether the faillock helper returned a usable result.
pub available: bool,
/// Number of observed failed attempts.
pub failures: u32,
/// Configured failure threshold, when discoverable.
pub max_tries: Option<u32>,
/// Remaining attempts before the advisory threshold.
pub remaining: Option<u32>,
/// Whether the advisory threshold has been reached.
pub locked_out: bool,
}
@@ -291,12 +257,8 @@ pub fn faillock_status(username: &str, config: &AccountsConfig) -> Result<Faillo
Ok(faillock_status_from_output(&output.stdout, max_tries))
}
/// Converts stable helper output into an advisory lockout status.
///
/// This parser accepts both the legacy `count timestamp` format and modern
/// rows containing a username prefix or timestamp columns. It intentionally
/// treats malformed output as zero failures instead of making authentication
/// fail open or fail closed.
/// Parses the legacy count format and current rows with usernames or timestamps.
/// Malformed output reports zero failures and does not control authentication.
pub fn faillock_status_from_output(output: &[u8], max_tries: Option<u32>) -> FaillockStatus {
let failures = parse_failure_count(output);
let remaining = max_tries.map(|limit| limit.saturating_sub(failures));