1use async_trait::async_trait;
17use serde::{Deserialize, Serialize};
18use zeroize::Zeroizing;
19
20use crate::Result;
21
22pub const ACCOUNT_CAPABILITY: &str = "admin.account.info";
24
25pub const ACCOUNT_MFA_CAPABILITY: &str = "admin.account.mfa";
27
28pub const USER_MFA_CAPABILITY: &str = "admin.user.mfa";
30
31pub const MAX_ACCOUNT_RESPONSE_BYTES: usize = 256 * 1024;
37
38#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
40#[serde(rename_all = "kebab-case")]
41pub enum IdentityType {
42 Root,
43 Iam,
44 Sts,
45 ServiceAccount,
46}
47
48impl std::fmt::Display for IdentityType {
49 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
50 formatter.write_str(match self {
51 Self::Root => "root",
52 Self::Iam => "iam",
53 Self::Sts => "sts",
54 Self::ServiceAccount => "service-account",
55 })
56 }
57}
58
59#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
61#[serde(rename_all = "kebab-case")]
62pub enum CredentialsSource {
63 Env,
65 Iam,
67}
68
69impl std::fmt::Display for CredentialsSource {
70 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
71 formatter.write_str(match self {
72 Self::Env => "env",
73 Self::Iam => "iam",
74 })
75 }
76}
77
78#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
80pub struct AccountMutability {
81 #[serde(default)]
82 pub password: bool,
83 #[serde(default)]
84 pub username: bool,
85}
86
87#[derive(Debug, Clone, Serialize, Deserialize, Default)]
89pub struct AccountMfaSummary {
90 #[serde(default)]
91 pub enabled: bool,
92 #[serde(default)]
93 pub pending: bool,
94 #[serde(default)]
95 pub activated_at: Option<String>,
96 #[serde(default)]
97 pub recovery_codes_remaining: u32,
98 #[serde(default)]
99 pub last_verified_at: Option<String>,
100 #[serde(default)]
101 pub enrollment_available: bool,
102 #[serde(default)]
103 pub enrollment_blocked_reason: Option<String>,
104}
105
106#[derive(Debug, Clone, Serialize, Deserialize)]
108pub struct AccountInfo {
109 pub access_key: String,
110 pub identity_type: IdentityType,
111 #[serde(default)]
112 pub session_access_key: Option<String>,
113 pub is_admin: bool,
114 pub status: String,
115 #[serde(default)]
116 pub member_of: Vec<String>,
117 #[serde(default)]
118 pub policies: Vec<String>,
119 pub credentials_source: CredentialsSource,
120 pub mutable: AccountMutability,
121 pub mfa: AccountMfaSummary,
122}
123
124#[derive(Debug, Clone, Serialize, Deserialize, Default)]
126pub struct MfaStatus {
127 #[serde(default)]
128 pub enabled: bool,
129 #[serde(default)]
130 pub pending: bool,
131 pub algorithm: String,
132 pub digits: u8,
133 pub period_seconds: u32,
134 #[serde(default)]
135 pub activated_at: Option<String>,
136 #[serde(default)]
137 pub pending_expires_at: Option<String>,
138 #[serde(default)]
139 pub recovery_codes_remaining: u32,
140 #[serde(default)]
141 pub last_verified_at: Option<String>,
142 #[serde(default)]
143 pub enrollment_available: bool,
144 #[serde(default)]
145 pub enrollment_blocked_reason: Option<String>,
146}
147
148#[derive(Debug, Clone, Serialize, Deserialize)]
153pub struct MfaEnrollment {
154 pub secret_base32: String,
155 pub otpauth_uri: String,
156 #[serde(default)]
159 pub qr_svg: String,
160 pub qr_utf8: String,
162 pub algorithm: String,
163 pub digits: u8,
164 pub period_seconds: u32,
165 pub expires_at: String,
166}
167
168#[derive(Debug, Clone, Serialize, Deserialize)]
170pub struct RecoveryCodes {
171 pub recovery_codes: Vec<String>,
172 pub generated_at: String,
173}
174
175#[derive(Debug, Clone, Copy, Serialize, Deserialize, Default)]
177pub struct PasswordChangeResult {
178 #[serde(default)]
179 pub sessions_revoked: u32,
180}
181
182#[derive(Debug, Clone, Serialize, Deserialize)]
184pub struct UserMfaStatus {
185 pub access_key: String,
186 pub enabled: bool,
187 #[serde(default)]
188 pub activated_at: Option<String>,
189 #[serde(default)]
190 pub recovery_codes_remaining: u32,
191}
192
193#[derive(Clone)]
198pub struct SecretValue(Zeroizing<String>);
199
200impl std::fmt::Debug for SecretValue {
201 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
202 formatter.write_str("SecretValue([REDACTED])")
203 }
204}
205
206impl SecretValue {
207 pub fn new(value: String) -> Self {
208 Self(Zeroizing::new(value))
209 }
210
211 pub fn expose(&self) -> &str {
212 self.0.as_str()
213 }
214
215 pub fn is_empty(&self) -> bool {
216 self.0.is_empty()
217 }
218}
219
220#[async_trait]
221pub trait AccountApi: Send + Sync {
222 async fn account_info(&self) -> Result<AccountInfo>;
224
225 async fn account_change_password(
230 &self,
231 current_secret_key: &SecretValue,
232 new_secret_key: &SecretValue,
233 ) -> Result<PasswordChangeResult>;
234}
235
236#[async_trait]
237pub trait AccountMfaApi: Send + Sync {
238 async fn account_mfa_status(&self) -> Result<MfaStatus>;
239
240 async fn account_mfa_enroll(&self) -> Result<MfaEnrollment>;
242
243 async fn account_mfa_activate(&self, code: &SecretValue) -> Result<RecoveryCodes>;
245
246 async fn account_mfa_disable(
248 &self,
249 code: &SecretValue,
250 current_secret_key: &SecretValue,
251 ) -> Result<()>;
252
253 async fn account_mfa_recovery_codes(&self, code: &SecretValue) -> Result<RecoveryCodes>;
255}
256
257#[async_trait]
258pub trait UserCredentialApi: Send + Sync {
259 async fn set_user_secret_key(
261 &self,
262 access_key: &str,
263 secret_key: &SecretValue,
264 ) -> Result<PasswordChangeResult>;
265
266 async fn user_mfa_status(&self, access_key: &str) -> Result<UserMfaStatus>;
267
268 async fn user_mfa_reset(&self, access_key: &str) -> Result<()>;
271}
272
273#[cfg(test)]
274mod tests {
275 use super::*;
276
277 #[test]
278 fn identity_type_renders_the_wire_value() {
279 assert_eq!(IdentityType::ServiceAccount.to_string(), "service-account");
280 assert_eq!(IdentityType::Root.to_string(), "root");
281 assert_eq!(
282 serde_json::to_string(&IdentityType::ServiceAccount).expect("serialize"),
283 "\"service-account\""
284 );
285 }
286
287 #[test]
288 fn credentials_source_renders_the_wire_value() {
289 assert_eq!(CredentialsSource::Env.to_string(), "env");
290 assert_eq!(
291 serde_json::to_string(&CredentialsSource::Iam).expect("serialize"),
292 "\"iam\""
293 );
294 }
295
296 #[test]
297 fn secret_values_never_print_their_contents() {
298 let secret = SecretValue::new("super-secret".to_string());
299
300 assert_eq!(format!("{secret:?}"), "SecretValue([REDACTED])");
301 assert!(!format!("{secret:?}").contains("super-secret"));
302 assert_eq!(secret.expose(), "super-secret");
303 }
304
305 #[test]
306 fn account_info_decodes_a_minimal_server_response() {
307 let decoded: AccountInfo = serde_json::from_str(
310 r#"{
311 "access_key": "sinan",
312 "identity_type": "iam",
313 "is_admin": true,
314 "status": "enabled",
315 "credentials_source": "iam",
316 "mutable": {"password": true, "username": false},
317 "mfa": {}
318 }"#,
319 )
320 .expect("deserialize");
321
322 assert_eq!(decoded.access_key, "sinan");
323 assert!(decoded.mutable.password);
324 assert!(!decoded.mfa.enabled);
325 assert!(decoded.member_of.is_empty());
326 }
327
328 #[test]
329 fn mfa_status_decodes_without_optional_timestamps() {
330 let decoded: MfaStatus =
331 serde_json::from_str(r#"{"algorithm":"SHA1","digits":6,"period_seconds":30}"#)
332 .expect("deserialize");
333
334 assert!(!decoded.enabled);
335 assert_eq!(decoded.digits, 6);
336 assert!(decoded.activated_at.is_none());
337 }
338}