use std::io;
use std::path::PathBuf;
#[derive(Debug, thiserror::Error)]
pub enum Error {
#[error("failed to run {program}: {source}")]
Spawn {
program: String,
#[source]
source: io::Error,
},
#[error("{program} failed ({status}): {stderr}")]
CommandFailed {
program: String,
status: String,
stderr: String,
},
#[error("{program} timed out after {seconds}s and was killed")]
Timeout { program: String, seconds: u64 },
#[error("no machine principal (NAME$@REALM) found in keytab {}", keytab.display())]
NoMachinePrincipal { keytab: PathBuf },
#[error("no machine principal for realm {realm} in keytab {}", keytab.display())]
NoMachinePrincipalForRealm { realm: String, keytab: PathBuf },
#[error(
"keytab {} is not readable \
(machine keytabs are root-only — run as root, e.g. via the systemd service)",
keytab.display()
)]
KeytabUnreadable {
keytab: PathBuf,
#[source]
source: io::Error,
},
#[error(
"keytab {} not found — is this machine AD-joined? (realm join <domain>)",
keytab.display()
)]
KeytabMissing { keytab: PathBuf },
#[error("group '{0}' not found — create it (`groupadd -r {0}`) or pass a different group")]
GroupNotFound(String),
#[error("ticket cache {} is still invalid after renew/mint", .0.display())]
CacheStillInvalid(PathBuf),
#[error(
"refusing to manage {}: not a dedicated absolute directory (shared system path, \
relative, or contains '..')",
.0.display()
)]
RefusingSharedDir(PathBuf),
#[error("refusing to operate on symlink {}", .0.display())]
RefusingSymlink(PathBuf),
#[error(transparent)]
Io(#[from] io::Error),
}
pub type Result<T> = std::result::Result<T, Error>;
impl Error {
pub fn is_transient(&self) -> bool {
match self {
Error::KeytabMissing { .. }
| Error::KeytabUnreadable { .. }
| Error::NoMachinePrincipal { .. }
| Error::NoMachinePrincipalForRealm { .. }
| Error::GroupNotFound(_)
| Error::RefusingSharedDir(_)
| Error::RefusingSymlink(_)
| Error::Spawn { .. } => false,
Error::CommandFailed { stderr, .. } => krb_error_is_transient(stderr),
Error::Timeout { .. } => true,
Error::CacheStillInvalid(_) => true,
Error::Io(e) => {
e.kind() != io::ErrorKind::PermissionDenied && e.raw_os_error() != Some(libc::EROFS)
}
}
}
}
fn krb_error_is_transient(stderr: &str) -> bool {
let s = stderr.to_ascii_lowercase();
const PERMANENT: &[&str] = &[
"preauthentication failed",
"client not found",
"credentials have been revoked",
"not found in kerberos database",
"decrypt integrity check failed",
"keytab entry not found",
"no key table entry found",
"entry in database has expired", "no support for encryption type", "client not yet valid", ];
!PERMANENT.iter().any(|m| s.contains(m))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn classifies_setup_errors_as_permanent() {
assert!(
!Error::KeytabMissing {
keytab: "/x".into()
}
.is_transient()
);
assert!(
!Error::NoMachinePrincipalForRealm {
realm: "R".into(),
keytab: "/x".into()
}
.is_transient()
);
assert!(!Error::GroupNotFound("g".into()).is_transient());
}
fn kinit_err(stderr: &str) -> Error {
Error::CommandFailed {
program: "kinit".into(),
status: "exit 1".into(),
stderr: stderr.into(),
}
}
#[test]
fn classifies_kdc_errors() {
for permanent in [
"kinit: Client's credentials have been revoked while getting initial credentials",
"kinit: Client's entry in database has expired while getting initial credentials",
"kinit: KDC has no support for encryption type while getting initial credentials",
"kinit: Client not yet valid - try again later while getting initial credentials",
"kinit: Preauthentication failed while getting initial credentials",
] {
assert!(!kinit_err(permanent).is_transient(), "{permanent}");
}
for transient in [
"kinit: Cannot contact any KDC for realm 'EXAMPLE.COM'",
"kinit: KDC policy rejects request while getting initial credentials", "kinit: Clock skew too great while getting initial credentials",
] {
assert!(kinit_err(transient).is_transient(), "{transient}");
}
}
#[test]
fn classifies_io_by_kind() {
let denied = Error::Io(io::Error::from(io::ErrorKind::PermissionDenied));
assert!(!denied.is_transient());
let rofs = Error::Io(io::Error::from_raw_os_error(libc::EROFS));
assert!(!rofs.is_transient());
let other = Error::Io(io::Error::from(io::ErrorKind::Interrupted));
assert!(other.is_transient());
}
}