1use serde::{Deserialize, Serialize};
4use thiserror::Error;
5
6#[derive(Debug, Clone, Error, Serialize, Deserialize, PartialEq, Eq)]
12#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
13#[serde(tag = "kind", rename_all = "snake_case")]
14pub enum AuthError {
15 #[error("missing secret for auth resolution")]
18 MissingSecret,
19 #[error("unsupported combination: backend={backend} auth={auth}")]
21 UnsupportedCombination { backend: String, auth: String },
22 #[error("missing required metadata: {0}")]
24 MissingRequiredMetadata(String),
25 #[error("workspace mismatch")]
27 WorkspaceMismatch,
28 #[error("stale credential material")]
30 StaleCredential,
31 #[error("credential refresh required")]
33 RefreshRequired,
34 #[error("auth lease absent")]
36 LeaseAbsent,
37 #[error("user reauth required")]
39 UserReauthRequired,
40 #[error("credential expired")]
42 Expired,
43 #[error("refresh failed: {0}")]
45 RefreshFailed(String),
46 #[error("interactive login required")]
49 InteractiveLoginRequired,
50 #[error("host-owned auth unavailable on this surface")]
52 HostOwnedUnavailable,
53 #[error("auth I/O failure: {0}")]
55 Io(String),
56 #[error("auth error: {0}")]
59 Other(String),
60}
61
62impl AuthError {
63 pub fn kind(&self) -> AuthErrorKind {
65 match self {
66 Self::MissingSecret => AuthErrorKind::MissingSecret,
67 Self::UnsupportedCombination { .. } => AuthErrorKind::UnsupportedCombination,
68 Self::MissingRequiredMetadata(_) => AuthErrorKind::MissingRequiredMetadata,
69 Self::WorkspaceMismatch => AuthErrorKind::WorkspaceMismatch,
70 Self::StaleCredential => AuthErrorKind::StaleCredential,
71 Self::RefreshRequired => AuthErrorKind::RefreshRequired,
72 Self::LeaseAbsent => AuthErrorKind::LeaseAbsent,
73 Self::UserReauthRequired => AuthErrorKind::UserReauthRequired,
74 Self::Expired => AuthErrorKind::Expired,
75 Self::RefreshFailed(_) => AuthErrorKind::RefreshFailed,
76 Self::InteractiveLoginRequired => AuthErrorKind::InteractiveLoginRequired,
77 Self::HostOwnedUnavailable => AuthErrorKind::HostOwnedUnavailable,
78 Self::Io(_) => AuthErrorKind::Io,
79 Self::Other(_) => AuthErrorKind::Other,
80 }
81 }
82}
83
84#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
87#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
88#[serde(rename_all = "snake_case")]
89pub enum AuthErrorKind {
90 MissingSecret,
91 UnsupportedCombination,
92 MissingRequiredMetadata,
93 WorkspaceMismatch,
94 StaleCredential,
95 RefreshRequired,
96 LeaseAbsent,
97 UserReauthRequired,
98 Expired,
99 RefreshFailed,
100 InteractiveLoginRequired,
101 HostOwnedUnavailable,
102 Io,
103 Other,
104}
105
106#[cfg(test)]
107#[allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
108mod tests {
109 use super::*;
110
111 #[test]
112 fn kind_maps_each_variant() {
113 assert_eq!(
114 AuthError::MissingSecret.kind(),
115 AuthErrorKind::MissingSecret
116 );
117 assert_eq!(
118 AuthError::WorkspaceMismatch.kind(),
119 AuthErrorKind::WorkspaceMismatch
120 );
121 assert_eq!(
122 AuthError::StaleCredential.kind(),
123 AuthErrorKind::StaleCredential
124 );
125 assert_eq!(
126 AuthError::RefreshRequired.kind(),
127 AuthErrorKind::RefreshRequired
128 );
129 assert_eq!(AuthError::LeaseAbsent.kind(), AuthErrorKind::LeaseAbsent);
130 assert_eq!(
131 AuthError::UserReauthRequired.kind(),
132 AuthErrorKind::UserReauthRequired
133 );
134 assert_eq!(AuthError::Expired.kind(), AuthErrorKind::Expired);
135 assert_eq!(
136 AuthError::RefreshFailed("x".into()).kind(),
137 AuthErrorKind::RefreshFailed,
138 );
139 assert_eq!(
140 AuthError::InteractiveLoginRequired.kind(),
141 AuthErrorKind::InteractiveLoginRequired,
142 );
143 }
144
145 #[test]
146 fn display_is_stable_nonempty() {
147 for err in [
148 AuthError::MissingSecret,
149 AuthError::UnsupportedCombination {
150 backend: "b".into(),
151 auth: "a".into(),
152 },
153 AuthError::MissingRequiredMetadata("workspace_id".into()),
154 AuthError::WorkspaceMismatch,
155 AuthError::StaleCredential,
156 AuthError::RefreshRequired,
157 AuthError::LeaseAbsent,
158 AuthError::UserReauthRequired,
159 AuthError::Expired,
160 AuthError::RefreshFailed("timeout".into()),
161 AuthError::InteractiveLoginRequired,
162 AuthError::HostOwnedUnavailable,
163 AuthError::Io("file missing".into()),
164 AuthError::Other("x".into()),
165 ] {
166 assert!(!err.to_string().is_empty(), "{err:?}");
167 }
168 }
169
170 #[test]
171 fn error_kind_serde_roundtrip() {
172 let k = AuthErrorKind::MissingSecret;
173 let s = serde_json::to_string(&k).unwrap();
174 assert_eq!(s, "\"missing_secret\"");
175 let back: AuthErrorKind = serde_json::from_str(&s).unwrap();
176 assert_eq!(back, k);
177 }
178}