use super::handle::CredentialRef;
use super::principal::{Principal, Scope};
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
#[non_exhaustive]
pub enum CredentialError {
#[error(
"no credential stored for `{credential}` (principal `{principal}`); \
store it with `config keys set {credential}`{hint}"
)]
Missing {
credential: CredentialRef,
principal: Principal,
hint: String,
},
#[error(
"principal `{principal}` is not granted `{credential}`; \
issue a grant for that principal, or accept the denial"
)]
Denied {
credential: CredentialRef,
principal: Principal,
},
#[error("`{credential}` is expired or revoked for principal `{principal}`; re-authenticate")]
Expired {
credential: CredentialRef,
principal: Principal,
},
#[error(
"principal `{principal}`'s grant on `{credential}` does not cover scope `{requested}`; \
widen the grant's scope"
)]
ZeroScope {
credential: CredentialRef,
principal: Principal,
requested: Scope,
},
#[error(
"provider for `{credential}` cannot issue a credential at scope `{requested}` \
(principal `{principal}`); this provider has no such scope"
)]
ScopeUnavailable {
credential: CredentialRef,
principal: Principal,
requested: Scope,
},
}
impl CredentialError {
pub fn credential(&self) -> &CredentialRef {
match self {
Self::Missing { credential, .. }
| Self::Denied { credential, .. }
| Self::Expired { credential, .. }
| Self::ZeroScope { credential, .. }
| Self::ScopeUnavailable { credential, .. } => credential,
}
}
pub fn principal(&self) -> &Principal {
match self {
Self::Missing { principal, .. }
| Self::Denied { principal, .. }
| Self::Expired { principal, .. }
| Self::ZeroScope { principal, .. }
| Self::ScopeUnavailable { principal, .. } => principal,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn one_of_each() -> Vec<CredentialError> {
let credential = CredentialRef::parse("github/work").unwrap();
let principal = Principal::Operator;
vec![
CredentialError::Missing {
credential: credential.clone(),
principal: principal.clone(),
hint: String::new(),
},
CredentialError::Denied {
credential: credential.clone(),
principal: principal.clone(),
},
CredentialError::Expired {
credential: credential.clone(),
principal: principal.clone(),
},
CredentialError::ZeroScope {
credential: credential.clone(),
principal: principal.clone(),
requested: Scope::write(),
},
CredentialError::ScopeUnavailable {
credential,
principal,
requested: Scope::read(),
},
]
}
#[test]
fn every_variant_is_constructible_and_matchable() {
let all = one_of_each();
assert_eq!(all.len(), 5, "DOC-45 C-5.1–C-5.5 name five variants");
for e in &all {
let named = match e {
CredentialError::Missing { .. } => "missing",
CredentialError::Denied { .. } => "denied",
CredentialError::Expired { .. } => "expired",
CredentialError::ZeroScope { .. } => "zero-scope",
CredentialError::ScopeUnavailable { .. } => "scope-unavailable",
};
assert!(!named.is_empty());
assert_eq!(e.credential().provider(), "github");
assert_eq!(e.principal(), &Principal::Operator);
}
assert_ne!(all[3], all[4]);
}
#[test]
fn remediation_names_the_principal_and_the_ref() {
for e in one_of_each() {
let rendered = e.to_string();
assert!(
rendered.contains("github/work"),
"no credential ref in: {rendered}"
);
assert!(rendered.contains("operator"), "no principal in: {rendered}");
}
}
#[test]
fn no_variant_renders_secret_material() {
let secret = "ghp_16C7e42F292c6912E7710c838347Ae178B4a";
for e in one_of_each() {
let rendered = format!("{e} {e:?}");
assert!(!rendered.contains(secret), "leaked: {rendered}");
assert!(!rendered.contains("ghp_"), "leaked prefix: {rendered}");
}
}
}