use std::sync::Arc;
use galata_vault_client::{ApiError, Pre};
use galata_vault_keys::KeyError;
use galata_vault_proto::api::ErrorCode;
use galata_vault_proto::audit::ChainError;
use galata_vault_proto::integrity::IntegrityError;
use galata_vault_seal::SealError;
use crate::store::StoreError;
pub mod code {
pub const INVALID_TOKEN: &str = "invalid_token";
pub const INVALID_TOKEN_FILE: &str = "invalid_token_file";
pub const INVALID_SERVER: &str = "invalid_server";
pub const INVALID_PROXY: &str = "invalid_proxy";
pub const MISSING_SERVER: &str = "missing_server";
pub const INVALID_ENVIRONMENT: &str = "invalid_environment";
pub const INVALID_NAME: &str = "invalid_name";
pub const INVALID_CONFIG: &str = "invalid_config";
pub const INVALID_AUDIT_HEAD: &str = "invalid_audit_head";
pub const CREDENTIAL_LITERAL: &str = "credential_literal";
pub const UNSUPPORTED_FORMAT: &str = "unsupported_format";
pub const NOT_TEXT: &str = "not_text";
pub const NOT_FOUND: &str = "not_found";
pub const FORBIDDEN: &str = "forbidden";
pub const CONFLICT: &str = "conflict";
pub const STALE_GENERATION: &str = "stale_generation";
pub const UNREACHABLE: &str = "unreachable";
pub const ERROR: &str = "error";
pub const BAD_SIGNATURE: &str = "bad_signature";
pub const KEY_MISMATCH: &str = "key_mismatch";
pub const BINDING_MISMATCH: &str = "binding_mismatch";
pub const VERSION_ROLLBACK: &str = "version_rollback";
pub const GENERATION_ROLLBACK: &str = "generation_rollback";
pub const AUDIT_MISMATCH: &str = "audit_mismatch";
pub const STORE: &str = "store";
pub const UNKNOWN_PATH: &str = "unknown_path";
pub const INVALID_PATH: &str = "invalid_path";
pub const INVALID_KIT: &str = "invalid_kit";
pub const UNKNOWN_PROJECT: &str = "unknown_project";
pub const PROJECT_EXISTS: &str = "project_exists";
pub const ENV_EXISTS: &str = "env_exists";
pub const IS_PROJECT: &str = "is_project";
pub const HAS_CHILDREN: &str = "has_children";
pub const VAULT_EXISTS: &str = "vault_exists";
pub const NO_KEY: &str = "no_key";
pub const SERVER_MISMATCH: &str = "server_mismatch";
pub const REKEY_IN_PROGRESS: &str = "rekey_in_progress";
pub const NO_REKEY: &str = "no_rekey";
pub const REKEY_FORWARD_ONLY: &str = "rekey_forward_only";
pub const DATA_DIR_IN_USE: &str = "data_dir_in_use";
pub const INVALID_DATA_DIR: &str = "invalid_data_dir";
pub const UNSUPPORTED_PROTOCOL: &str = "unsupported_protocol";
pub const UNSUPPORTED_BY_CLIENT: &str = "unsupported_by_client";
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum ErrorKind {
Auth,
Forbidden,
NotFound,
Conflict,
Transport,
Invalid,
Integrity,
Other,
}
impl ErrorKind {
pub fn as_str(self) -> &'static str {
match self {
ErrorKind::Auth => "auth",
ErrorKind::Forbidden => "forbidden",
ErrorKind::NotFound => "not_found",
ErrorKind::Conflict => "conflict",
ErrorKind::Transport => "transport",
ErrorKind::Invalid => "invalid",
ErrorKind::Integrity => "integrity",
ErrorKind::Other => "error",
}
}
}
pub fn kind_of(code: &str) -> ErrorKind {
if let Some(kind) = ErrorCode::parse(code).and_then(kind_of_protocol) {
return kind;
}
match code {
code::INVALID_TOKEN => ErrorKind::Auth,
code::UNREACHABLE => ErrorKind::Transport,
code::INVALID_TOKEN_FILE
| code::INVALID_SERVER
| code::INVALID_PROXY
| code::MISSING_SERVER
| code::INVALID_ENVIRONMENT
| code::INVALID_NAME
| code::INVALID_CONFIG
| code::INVALID_AUDIT_HEAD
| code::CREDENTIAL_LITERAL
| code::UNSUPPORTED_FORMAT
| code::NOT_TEXT
| code::UNKNOWN_PATH
| code::INVALID_PATH
| code::INVALID_KIT
| code::UNKNOWN_PROJECT
| code::PROJECT_EXISTS
| code::ENV_EXISTS
| code::IS_PROJECT
| code::HAS_CHILDREN
| code::VAULT_EXISTS
| code::NO_KEY
| code::SERVER_MISMATCH
| code::REKEY_IN_PROGRESS
| code::NO_REKEY
| code::REKEY_FORWARD_ONLY
| code::DATA_DIR_IN_USE
| code::INVALID_DATA_DIR
| code::UNSUPPORTED_PROTOCOL
| code::UNSUPPORTED_BY_CLIENT => ErrorKind::Invalid,
code::BINDING_MISMATCH | code::AUDIT_MISMATCH => ErrorKind::Integrity,
_ => ErrorKind::Other,
}
}
fn kind_of_protocol(code: ErrorCode) -> Option<ErrorKind> {
Some(match code {
ErrorCode::Unauthorized | ErrorCode::TokenExpired => ErrorKind::Auth,
ErrorCode::Forbidden => ErrorKind::Forbidden,
ErrorCode::NotFound => ErrorKind::NotFound,
ErrorCode::PreconditionFailed
| ErrorCode::Conflict
| ErrorCode::StaleGeneration
| ErrorCode::VersionMismatch => ErrorKind::Conflict,
ErrorCode::BadSignature
| ErrorCode::KeyMismatch
| ErrorCode::VersionRollback
| ErrorCode::GenerationRollback => ErrorKind::Integrity,
_ => return None,
})
}
pub fn kind_of_status(status: u16) -> ErrorKind {
match status {
401 => ErrorKind::Auth,
403 => ErrorKind::Forbidden,
404 => ErrorKind::NotFound,
409 | 412 => ErrorKind::Conflict,
_ => ErrorKind::Other,
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum Expected {
Absent,
Version(u64),
}
impl From<Pre> for Expected {
fn from(pre: Pre) -> Expected {
match pre {
Pre::Create | Pre::Revive(_) => Expected::Absent,
Pre::Update(v) => Expected::Version(v),
}
}
}
impl std::fmt::Display for Expected {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Expected::Absent => f.write_str("no existing record"),
Expected::Version(v) => write!(f, "version {v}"),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum IntegrityFailure {
BadSignature(&'static str),
KeyMismatch(&'static str),
BindingMismatch(&'static str),
VersionRollback {
known: u64,
got: u64,
},
GenerationRollback {
known: u32,
got: u32,
},
AuditMismatch,
}
impl IntegrityFailure {
pub fn code(&self) -> &'static str {
match self {
IntegrityFailure::BadSignature(_) => code::BAD_SIGNATURE,
IntegrityFailure::KeyMismatch(_) => code::KEY_MISMATCH,
IntegrityFailure::BindingMismatch(_) => code::BINDING_MISMATCH,
IntegrityFailure::VersionRollback { .. } => code::VERSION_ROLLBACK,
IntegrityFailure::GenerationRollback { .. } => code::GENERATION_ROLLBACK,
IntegrityFailure::AuditMismatch => code::AUDIT_MISMATCH,
}
}
}
impl From<&IntegrityError> for IntegrityFailure {
fn from(e: &IntegrityError) -> IntegrityFailure {
match *e {
IntegrityError::BadSignature(w) => IntegrityFailure::BadSignature(w),
IntegrityError::KeyMismatch(w) => IntegrityFailure::KeyMismatch(w),
IntegrityError::BindingMismatch(w) | IntegrityError::Malformed(w) => {
IntegrityFailure::BindingMismatch(w)
}
IntegrityError::VersionRollback { known, got } => {
IntegrityFailure::VersionRollback { known, got }
}
IntegrityError::GenerationRollback { known, got } => {
IntegrityFailure::GenerationRollback { known, got }
}
_ => IntegrityFailure::BindingMismatch("integrity check"),
}
}
}
#[derive(Debug, Clone)]
#[non_exhaustive]
pub enum Error {
Invalid {
code: &'static str,
message: String,
},
Auth {
code: String,
message: String,
},
Forbidden {
message: String,
},
NotFound {
message: String,
},
VaultNotFound {
label: String,
message: String,
},
UnknownPath {
path: String,
suggestion: Option<String>,
project_known: bool,
message: String,
},
Conflict {
name: String,
expected: Expected,
current: Option<u64>,
message: String,
},
StaleGeneration {
label: String,
message: String,
},
Integrity {
failure: IntegrityFailure,
message: String,
},
Server {
code: String,
status: u16,
message: String,
},
Transport {
server: String,
message: String,
source: Arc<dyn std::error::Error + Send + Sync>,
},
Store {
message: String,
source: Arc<StoreError>,
},
Other {
message: String,
},
}
impl Error {
pub fn code(&self) -> &str {
match self {
Error::Invalid { code, .. } => code,
Error::Auth { code, .. } | Error::Server { code, .. } => code,
Error::Forbidden { .. } => code::FORBIDDEN,
Error::NotFound { .. } | Error::VaultNotFound { .. } => code::NOT_FOUND,
Error::UnknownPath { .. } => code::UNKNOWN_PATH,
Error::Conflict { .. } => code::CONFLICT,
Error::StaleGeneration { .. } => code::STALE_GENERATION,
Error::Integrity { failure, .. } => failure.code(),
Error::Transport { .. } => code::UNREACHABLE,
Error::Store { .. } => code::STORE,
Error::Other { .. } => code::ERROR,
}
}
pub fn kind(&self) -> ErrorKind {
match self {
Error::Server { code, status, .. } if ErrorCode::parse(code).is_none() => {
kind_of_status(*status)
}
_ => kind_of(self.code()),
}
}
pub fn message(&self) -> &str {
match self {
Error::Invalid { message, .. }
| Error::Auth { message, .. }
| Error::Forbidden { message }
| Error::NotFound { message }
| Error::VaultNotFound { message, .. }
| Error::UnknownPath { message, .. }
| Error::Conflict { message, .. }
| Error::StaleGeneration { message, .. }
| Error::Integrity { message, .. }
| Error::Server { message, .. }
| Error::Transport { message, .. }
| Error::Store { message, .. }
| Error::Other { message } => message,
}
}
fn message_mut(&mut self) -> &mut String {
match self {
Error::Invalid { message, .. }
| Error::Auth { message, .. }
| Error::Forbidden { message }
| Error::NotFound { message }
| Error::VaultNotFound { message, .. }
| Error::UnknownPath { message, .. }
| Error::Conflict { message, .. }
| Error::StaleGeneration { message, .. }
| Error::Integrity { message, .. }
| Error::Server { message, .. }
| Error::Transport { message, .. }
| Error::Store { message, .. }
| Error::Other { message } => message,
}
}
pub(crate) fn context(mut self, context: impl std::fmt::Display) -> Error {
let m = self.message_mut();
*m = format!("{context}: {m}");
self
}
pub(crate) fn local(code: &'static str, message: impl Into<String>) -> Error {
Error::Invalid {
code,
message: message.into(),
}
}
pub(crate) fn forbidden(message: impl Into<String>) -> Error {
Error::Forbidden {
message: message.into(),
}
}
pub(crate) fn not_found(message: impl Into<String>) -> Error {
Error::NotFound {
message: message.into(),
}
}
pub(crate) fn other(message: impl Into<String>) -> Error {
Error::Other {
message: message.into(),
}
}
pub(crate) fn stale(label: &str, message: impl Into<String>) -> Error {
Error::StaleGeneration {
label: label.to_owned(),
message: message.into(),
}
}
pub(crate) fn unsupported(message: impl Into<String>) -> Error {
Error::Invalid {
code: code::UNSUPPORTED_BY_CLIENT,
message: message.into(),
}
}
pub(crate) fn integrity(label: &str, e: IntegrityError) -> Error {
Error::Integrity {
failure: IntegrityFailure::from(&e),
message: format!("{label}: integrity check failed: {e}"),
}
}
pub(crate) fn conflict(name: &str, pre: Pre, current: Option<u64>, deleting: bool) -> Error {
let expected = Expected::from(pre);
let current_text = current.map_or("none".to_owned(), |v| v.to_string());
let outcome = if deleting {
"nothing was deleted"
} else {
"nothing was overwritten"
};
Error::Conflict {
name: name.to_owned(),
expected,
current,
message: format!(
"{name} changed on the server: expected {expected}, the server has version {current_text}; {outcome}"
),
}
}
pub(crate) fn seal(e: SealError, context: impl std::fmt::Display) -> Error {
let message = format!("{context}: {}", chain(&e));
match seal_class(&e) {
Class::Integrity(failure) => Error::Integrity { failure, message },
Class::Stale => Error::StaleGeneration {
label: context.to_string(),
message,
},
Class::Unsupported => Error::unsupported(message),
Class::Other => Error::Other { message },
}
}
pub(crate) fn key(e: KeyError, context: impl std::fmt::Display) -> Error {
let message = format!("{context}: {}", chain(&e));
match key_class(&e) {
Class::Integrity(failure) => Error::Integrity { failure, message },
Class::Unsupported => Error::unsupported(message),
_ => Error::Other { message },
}
}
pub(crate) fn audit_chain(e: ChainError, context: impl std::fmt::Display) -> Error {
let message = format!("{context}: {}", chain(&e));
match e {
ChainError::UnknownValue { .. } => Error::unsupported(message),
ChainError::HeadMismatch => Error::Integrity {
failure: IntegrityFailure::BindingMismatch("audit head"),
message,
},
_ => Error::Integrity {
failure: IntegrityFailure::AuditMismatch,
message,
},
}
}
pub(crate) fn api(e: ApiError, label: Option<&str>) -> Error {
let labelled = |m: String| match label {
Some(l) => format!("{l}: {m}"),
None => m,
};
match &e {
ApiError::Transport(t) => Error::Transport {
server: t.server().to_owned(),
message: labelled(t.to_string()),
source: t.cause().unwrap_or_else(|| Arc::new(t.clone())),
},
ApiError::UnsupportedProtocol { .. } => Error::Invalid {
code: code::UNSUPPORTED_PROTOCOL,
message: labelled(e.to_string()),
},
ApiError::Refused { status, .. } => {
let code = e.stable_code();
let message = labelled(e.to_string());
match kind_of(&code) {
ErrorKind::Auth => Error::Auth { code, message },
ErrorKind::Forbidden => Error::Forbidden { message },
ErrorKind::NotFound => Error::NotFound { message },
_ => Error::Server {
code,
status: *status,
message,
},
}
}
_ => Error::Other {
message: e.to_string(),
},
}
}
pub(crate) fn store(e: StoreError) -> Error {
Error::Store {
message: e.message().to_owned(),
source: Arc::new(e),
}
}
#[cfg(feature = "http")]
pub(crate) fn build(e: galata_vault_client::BuildError) -> Error {
match e {
galata_vault_client::BuildError::InvalidServer(m) => {
Error::local(code::INVALID_SERVER, m)
}
other => Error::local(code::INVALID_PROXY, other.to_string()),
}
}
}
impl std::fmt::Display for Error {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.message())
}
}
impl std::error::Error for Error {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Error::Transport { source, .. } => Some(&**source),
Error::Store { source, .. } => Some(&**source),
_ => None,
}
}
}
pub(crate) fn chain(e: &dyn std::error::Error) -> String {
let mut out = e.to_string();
let mut cur = e.source();
while let Some(s) = cur {
out.push_str(": ");
out.push_str(&s.to_string());
cur = s.source();
}
out
}
enum Class {
Integrity(IntegrityFailure),
Stale,
Unsupported,
Other,
}
fn key_class(e: &KeyError) -> Class {
match e {
KeyError::Integrity(i) => Class::Integrity(IntegrityFailure::from(i)),
KeyError::Binding(w) => Class::Integrity(IntegrityFailure::BindingMismatch(w)),
KeyError::NameMismatch => {
Class::Integrity(IntegrityFailure::BindingMismatch("record name"))
}
KeyError::Kind { .. } => Class::Integrity(IntegrityFailure::BindingMismatch("bundle kind")),
KeyError::UnsupportedScope(_) => Class::Unsupported,
_ => Class::Other,
}
}
fn seal_class(e: &SealError) -> Class {
match e {
SealError::Integrity(i) => Class::Integrity(IntegrityFailure::from(i)),
SealError::Key(k) => key_class(k),
SealError::Binding(w) => Class::Integrity(IntegrityFailure::BindingMismatch(w)),
SealError::NameMismatch => {
Class::Integrity(IntegrityFailure::BindingMismatch("record name"))
}
SealError::KindMismatch { .. } => {
Class::Integrity(IntegrityFailure::BindingMismatch("record kind"))
}
SealError::StaleGeneration { .. } => Class::Stale,
SealError::UnsupportedByClient(_) => Class::Unsupported,
_ => Class::Other,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn codes_map_to_kinds() {
assert_eq!(kind_of("token_expired"), ErrorKind::Auth);
assert_eq!(kind_of(code::INVALID_TOKEN), ErrorKind::Auth);
assert_eq!(kind_of("precondition_failed"), ErrorKind::Conflict);
assert_eq!(kind_of("version_mismatch"), ErrorKind::Conflict);
assert_eq!(kind_of(code::STALE_GENERATION), ErrorKind::Conflict);
assert_eq!(kind_of(code::CREDENTIAL_LITERAL), ErrorKind::Invalid);
assert_eq!(kind_of(code::UNKNOWN_PATH), ErrorKind::Invalid);
for c in [
code::BAD_SIGNATURE,
code::KEY_MISMATCH,
code::VERSION_ROLLBACK,
code::GENERATION_ROLLBACK,
code::AUDIT_MISMATCH,
] {
assert_eq!(kind_of(c), ErrorKind::Integrity, "{c}");
}
assert_eq!(Error::forbidden("x").code(), code::FORBIDDEN);
}
#[test]
fn protocol_codes_classify_through_their_type() {
for (c, kind) in [
(ErrorCode::Unauthorized, ErrorKind::Auth),
(ErrorCode::TokenExpired, ErrorKind::Auth),
(ErrorCode::Forbidden, ErrorKind::Forbidden),
(ErrorCode::NotFound, ErrorKind::NotFound),
(ErrorCode::PreconditionFailed, ErrorKind::Conflict),
(ErrorCode::Conflict, ErrorKind::Conflict),
(ErrorCode::StaleGeneration, ErrorKind::Conflict),
(ErrorCode::VersionMismatch, ErrorKind::Conflict),
(ErrorCode::BadSignature, ErrorKind::Integrity),
(ErrorCode::KeyMismatch, ErrorKind::Integrity),
(ErrorCode::VersionRollback, ErrorKind::Integrity),
(ErrorCode::GenerationRollback, ErrorKind::Integrity),
] {
assert_eq!(kind_of(c.as_str()), kind, "{c}");
}
assert_eq!(kind_of(ErrorCode::RateLimited.as_str()), ErrorKind::Other);
assert_eq!(kind_of(ErrorCode::Unavailable.as_str()), ErrorKind::Other);
}
#[test]
fn integrity_failures_keep_their_code_through_context() {
let e = Error::seal(
SealError::Integrity(IntegrityError::BadSignature("record")),
"acme/dev: version 2 of DB",
);
assert_eq!(
(e.code(), e.kind()),
(code::BAD_SIGNATURE, ErrorKind::Integrity)
);
assert!(e.message().starts_with("acme/dev: version 2 of DB"), "{e}");
assert!(matches!(
e,
Error::Integrity {
failure: IntegrityFailure::BadSignature("record"),
..
}
));
let stale = Error::seal(
SealError::StaleGeneration {
bundle: 1,
vault: 2,
},
"a",
);
assert_eq!(stale.code(), code::STALE_GENERATION);
}
#[test]
fn a_conflict_carries_both_versions() {
let e = Error::conflict("app", Pre::Update(7), Some(8), false);
assert!(matches!(
e,
Error::Conflict {
expected: Expected::Version(7),
current: Some(8),
..
}
));
assert_eq!(e.code(), code::CONFLICT);
assert_eq!(
e.message(),
"app changed on the server: expected version 7, the server has version 8; nothing was overwritten"
);
assert_eq!(e.clone().context("acme/dev").kind(), ErrorKind::Conflict);
}
#[test]
fn an_unknown_server_code_keeps_its_code_message_and_status_kind() {
let body = br#"{"error":"future_conflict","message":"try again later"}"#;
let transport = galata_vault_client::RecordingTransport::responding(move |_| {
Ok(galata_vault_client::Response::new(409, body.to_vec()))
});
let e = galata_vault_client::Api::new(transport)
.status(galata_vault_client::Auth::None)
.unwrap_err();
let e = Error::api(e, Some("acme/dev"));
assert_eq!(e.code(), "future_conflict");
assert_eq!(e.kind(), ErrorKind::Conflict);
assert!(e.message().contains("try again later"), "{e}");
assert_eq!(kind_of_status(401), ErrorKind::Auth);
assert_eq!(kind_of_status(418), ErrorKind::Other);
assert_eq!(kind_of(code::UNSUPPORTED_BY_CLIENT), ErrorKind::Invalid);
assert_eq!(kind_of(code::UNSUPPORTED_PROTOCOL), ErrorKind::Invalid);
}
#[test]
fn an_error_is_shareable() {
fn shared<T: Send + Sync + Clone + std::error::Error>() {}
shared::<Error>();
}
#[test]
fn a_transport_failure_keeps_its_cause() {
let io = std::io::Error::new(std::io::ErrorKind::ConnectionRefused, "refused");
let t = galata_vault_client::TransportError::with_source("https://vault.example", io);
let e = Error::api(ApiError::Transport(t), Some("acme/dev"));
assert_eq!(e.code(), code::UNREACHABLE);
assert!(e.message().starts_with("acme/dev: could not reach"), "{e}");
let source = std::error::Error::source(&e).expect("a cause");
assert!(source.downcast_ref::<std::io::Error>().is_some());
}
}