Skip to main content

bucketwarden_auth/
credentials.rs

1use crate::CredentialScope;
2use bucketwarden_s3::sigv4::AwsCredentials;
3use serde::{Deserialize, Serialize};
4
5#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
6pub struct AccessKey {
7    pub access_key_id: String,
8    pub principal_id: String,
9    secret_access_key: String,
10    #[serde(default)]
11    pub previous_access_key_id: Option<String>,
12    #[serde(default)]
13    pub rotated_at_epoch_seconds: Option<u64>,
14    #[serde(default)]
15    pub leaked_at_epoch_seconds: Option<u64>,
16    pub enabled: bool,
17    pub expires_at_epoch_seconds: Option<u64>,
18    pub revoked_at_epoch_seconds: Option<u64>,
19    pub last_used_epoch_seconds: Option<u64>,
20}
21
22impl AccessKey {
23    pub fn active(
24        principal_id: impl Into<String>,
25        access_key_id: impl Into<String>,
26        secret_access_key: impl Into<String>,
27    ) -> Self {
28        Self {
29            access_key_id: access_key_id.into(),
30            principal_id: principal_id.into(),
31            secret_access_key: secret_access_key.into(),
32            previous_access_key_id: None,
33            rotated_at_epoch_seconds: None,
34            leaked_at_epoch_seconds: None,
35            enabled: true,
36            expires_at_epoch_seconds: None,
37            revoked_at_epoch_seconds: None,
38            last_used_epoch_seconds: None,
39        }
40    }
41
42    pub fn with_expiry(mut self, expires_at_epoch_seconds: u64) -> Self {
43        self.expires_at_epoch_seconds = Some(expires_at_epoch_seconds);
44        self
45    }
46
47    pub fn disable(&mut self) {
48        self.enabled = false;
49    }
50
51    pub fn revoke(&mut self, revoked_at_epoch_seconds: u64) {
52        self.enabled = false;
53        self.revoked_at_epoch_seconds = Some(revoked_at_epoch_seconds);
54    }
55
56    pub fn credentials(&self) -> AwsCredentials {
57        AwsCredentials::new(self.access_key_id.clone(), self.secret_access_key.clone())
58    }
59}
60
61#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
62pub struct CredentialRotation {
63    pub old_access_key_id: String,
64    pub new_access_key_id: String,
65    pub principal_id: String,
66    pub rotated_at_epoch_seconds: u64,
67}
68
69#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
70pub struct LeakedKeyResponse {
71    pub access_key_id: String,
72    pub principal_id: String,
73    pub leaked_at_epoch_seconds: u64,
74    pub revoked: bool,
75}
76
77#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
78pub struct MtlsClientCertificate {
79    pub certificate_fingerprint: String,
80    pub principal_id: String,
81    pub enabled: bool,
82    pub expires_at_epoch_seconds: Option<u64>,
83    pub revoked_at_epoch_seconds: Option<u64>,
84    pub last_used_epoch_seconds: Option<u64>,
85}
86
87impl MtlsClientCertificate {
88    pub fn active(
89        principal_id: impl Into<String>,
90        certificate_fingerprint: impl Into<String>,
91    ) -> Self {
92        Self {
93            certificate_fingerprint: certificate_fingerprint.into(),
94            principal_id: principal_id.into(),
95            enabled: true,
96            expires_at_epoch_seconds: None,
97            revoked_at_epoch_seconds: None,
98            last_used_epoch_seconds: None,
99        }
100    }
101
102    pub fn with_expiry(mut self, expires_at_epoch_seconds: u64) -> Self {
103        self.expires_at_epoch_seconds = Some(expires_at_epoch_seconds);
104        self
105    }
106
107    pub fn disable(&mut self) {
108        self.enabled = false;
109    }
110
111    pub fn revoke(&mut self, revoked_at_epoch_seconds: u64) {
112        self.enabled = false;
113        self.revoked_at_epoch_seconds = Some(revoked_at_epoch_seconds);
114    }
115}
116
117#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
118pub struct SessionCredential {
119    pub access_key_id: String,
120    pub principal_id: String,
121    pub parent_access_key_id: String,
122    secret_access_key: String,
123    session_token: String,
124    pub expires_at_epoch_seconds: u64,
125    #[serde(default)]
126    pub scope: Option<CredentialScope>,
127    pub revoked_at_epoch_seconds: Option<u64>,
128    pub last_used_epoch_seconds: Option<u64>,
129}
130
131impl SessionCredential {
132    pub fn new(
133        principal_id: impl Into<String>,
134        parent_access_key_id: impl Into<String>,
135        access_key_id: impl Into<String>,
136        secret_access_key: impl Into<String>,
137        session_token: impl Into<String>,
138        expires_at_epoch_seconds: u64,
139    ) -> Self {
140        Self {
141            access_key_id: access_key_id.into(),
142            principal_id: principal_id.into(),
143            parent_access_key_id: parent_access_key_id.into(),
144            secret_access_key: secret_access_key.into(),
145            session_token: session_token.into(),
146            expires_at_epoch_seconds,
147            scope: None,
148            revoked_at_epoch_seconds: None,
149            last_used_epoch_seconds: None,
150        }
151    }
152
153    pub fn with_scope(mut self, scope: CredentialScope) -> Self {
154        self.scope = Some(scope);
155        self
156    }
157
158    pub fn revoke(&mut self, revoked_at_epoch_seconds: u64) {
159        self.revoked_at_epoch_seconds = Some(revoked_at_epoch_seconds);
160    }
161
162    pub fn credentials(&self) -> AwsCredentials {
163        AwsCredentials::session(
164            self.access_key_id.clone(),
165            self.secret_access_key.clone(),
166            self.session_token.clone(),
167        )
168    }
169}
170
171#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
172pub enum CredentialRecord {
173    AccessKey(AccessKey),
174    Session(SessionCredential),
175}
176
177impl CredentialRecord {
178    pub fn principal_id(&self) -> &str {
179        match self {
180            Self::AccessKey(key) => &key.principal_id,
181            Self::Session(session) => &session.principal_id,
182        }
183    }
184
185    pub fn access_key_id(&self) -> &str {
186        match self {
187            Self::AccessKey(key) => &key.access_key_id,
188            Self::Session(session) => &session.access_key_id,
189        }
190    }
191
192    pub fn credentials(&self) -> AwsCredentials {
193        match self {
194            Self::AccessKey(key) => key.credentials(),
195            Self::Session(session) => session.credentials(),
196        }
197    }
198
199    pub fn last_used_epoch_seconds(&self) -> Option<u64> {
200        match self {
201            Self::AccessKey(key) => key.last_used_epoch_seconds,
202            Self::Session(session) => session.last_used_epoch_seconds,
203        }
204    }
205}
206
207pub const CREDENTIAL_RUNTIME_FEATURES: &[&str] = &[
208    "access-key-secret",
209    "sigv4",
210    "temporary-credentials",
211    "session-tokens",
212    "mtls-client-certificates",
213    "jwt",
214    "service-accounts",
215];
216
217pub const CREDENTIAL_ADMIN_SURFACES: &[&str] = &[
218    "AuthStore::put_access_key",
219    "AuthStore::put_session",
220    "AuthStore::put_mtls_client_certificate",
221    "AuthStore::rotate_access_key",
222    "AuthStore::revoke_credential",
223    "AuthStore::report_leaked_access_key",
224];
225
226pub const CREDENTIAL_SECURITY_CONTROLS: &[&str] = &[
227    "unknown credential rejection",
228    "disabled credential rejection",
229    "revoked credential rejection",
230    "expired credential rejection",
231    "parent access key validation",
232    "session scope preservation",
233    "mTLS fingerprint binding",
234    "last-used audit marker",
235];
236
237pub const CREDENTIAL_OBSERVABILITY_FIELDS: &[&str] = &[
238    "principal_id",
239    "tenant_id",
240    "access_key_id",
241    "certificate_fingerprint",
242    "session_token",
243    "scope",
244    "last_used_epoch_seconds",
245    "expires_at_epoch_seconds",
246    "revoked_at_epoch_seconds",
247];
248
249pub const CREDENTIAL_FAILURE_MODES: &[&str] = &[
250    "UnknownAccessKey",
251    "UnknownClientCertificate",
252    "UnknownParentAccessKey",
253    "DisabledAccessKey",
254    "DisabledClientCertificate",
255    "RevokedCredential",
256    "ExpiredCredential",
257    "NotAccessKey",
258];
259
260pub const CREDENTIAL_VALIDATION_TESTS: &[&str] = &[
261    "crates/bucketwarden-auth/tests/identity_access_keys.rs",
262    "crates/bucketwarden-auth/tests/session_credentials.rs",
263    "crates/bucketwarden-auth/tests/sts_scoped_sessions.rs",
264    "crates/bucketwarden-auth/tests/credential_rotation.rs",
265    "crates/bucketwarden-auth/tests/credential_support_contract.rs",
266];
267
268pub const CREDENTIAL_CAVEATS: &[&str] = &[
269    "mTLS support authenticates validated client-certificate fingerprints supplied by the listener boundary; TLS handshake termination remains a deployment concern.",
270    "JWT support is currently exercised through OIDC/SAML identity-provider assertions and scoped session issuance.",
271    "Credential persistence is in-memory unless the containing runtime snapshot is explicitly persisted.",
272];
273
274pub const TEMPORARY_CREDENTIAL_RUNTIME_FEATURES: &[&str] = &[
275    "parent-identity",
276    "scoped-credentials",
277    "expiration",
278    "revocation",
279    "policy-inheritance",
280];
281
282pub const TEMPORARY_CREDENTIAL_ADMIN_SURFACES: &[&str] = &[
283    "AuthStore::put_session",
284    "AuthStore::resolve_credential",
285    "AuthStore::revoke_credential",
286    "AuthStore::mark_used",
287    "AuthStore::assume_role",
288    "AuthStore::assume_role_with_web_identity",
289];
290
291pub const TEMPORARY_CREDENTIAL_SECURITY_CONTROLS: &[&str] = &[
292    "parent access key must resolve before session creation",
293    "session credentials preserve parent principal identity",
294    "session scope is explicit and non-expanding",
295    "expiration is enforced at credential resolution",
296    "revocation is enforced before resource access",
297    "session token is returned through AwsCredentials",
298];
299
300pub const TEMPORARY_CREDENTIAL_OBSERVABILITY_FIELDS: &[&str] = &[
301    "principal_id",
302    "tenant_id",
303    "parent_access_key_id",
304    "access_key_id",
305    "session_token",
306    "scope",
307    "expires_at_epoch_seconds",
308    "revoked_at_epoch_seconds",
309    "last_used_epoch_seconds",
310];
311
312pub const TEMPORARY_CREDENTIAL_FAILURE_MODES: &[&str] = &[
313    "UnknownParentAccessKey",
314    "UnknownAccessKey",
315    "UnknownPrincipal",
316    "DisabledPrincipal",
317    "ExpiredCredential",
318    "RevokedCredential",
319];
320
321pub const TEMPORARY_CREDENTIAL_VALIDATION_TESTS: &[&str] = &[
322    "crates/bucketwarden-auth/tests/session_credentials.rs",
323    "crates/bucketwarden-auth/tests/sts_scoped_sessions.rs",
324    "crates/bucketwarden-auth/tests/temporary_credential_support_contract.rs",
325];
326
327pub const TEMPORARY_CREDENTIAL_CAVEATS: &[&str] = &[
328    "Policy inheritance is represented by explicit session scopes that can narrow access but cannot expand beyond later authorization policy checks.",
329    "Session credentials are local runtime credentials rather than an AWS STS network endpoint.",
330    "Credential persistence is in-memory unless the containing runtime snapshot is explicitly persisted.",
331];
332
333#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
334pub struct CredentialSupportReport {
335    pub native_support_state: Vec<&'static str>,
336    pub semantic_parity: &'static str,
337    pub configuration_admin_surface: Vec<&'static str>,
338    pub security_governance_impact: Vec<&'static str>,
339    pub observability_evidence_fields: Vec<&'static str>,
340    pub failure_modes: Vec<&'static str>,
341    pub validation_test_coverage: Vec<&'static str>,
342    pub product_specific_caveats: Vec<&'static str>,
343}
344
345impl CredentialSupportReport {
346    pub fn current() -> Self {
347        Self {
348            native_support_state: CREDENTIAL_RUNTIME_FEATURES.to_vec(),
349            semantic_parity: "All credential mechanisms resolve to a known enabled principal and tenant before authorization; expired, revoked, disabled, or unknown credentials fail closed.",
350            configuration_admin_surface: CREDENTIAL_ADMIN_SURFACES.to_vec(),
351            security_governance_impact: CREDENTIAL_SECURITY_CONTROLS.to_vec(),
352            observability_evidence_fields: CREDENTIAL_OBSERVABILITY_FIELDS.to_vec(),
353            failure_modes: CREDENTIAL_FAILURE_MODES.to_vec(),
354            validation_test_coverage: CREDENTIAL_VALIDATION_TESTS.to_vec(),
355            product_specific_caveats: CREDENTIAL_CAVEATS.to_vec(),
356        }
357    }
358}
359
360#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
361pub struct TemporaryCredentialSupportReport {
362    pub native_support_state: Vec<&'static str>,
363    pub semantic_parity: &'static str,
364    pub configuration_admin_surface: Vec<&'static str>,
365    pub security_governance_impact: Vec<&'static str>,
366    pub observability_evidence_fields: Vec<&'static str>,
367    pub failure_modes: Vec<&'static str>,
368    pub validation_test_coverage: Vec<&'static str>,
369    pub product_specific_caveats: Vec<&'static str>,
370}
371
372impl TemporaryCredentialSupportReport {
373    pub fn current() -> Self {
374        Self {
375            native_support_state: TEMPORARY_CREDENTIAL_RUNTIME_FEATURES.to_vec(),
376            semantic_parity: "Temporary credentials resolve to the parent principal and tenant, carry a bounded session token, enforce explicit scope, and fail closed on unknown parents, expiration, or revocation.",
377            configuration_admin_surface: TEMPORARY_CREDENTIAL_ADMIN_SURFACES.to_vec(),
378            security_governance_impact: TEMPORARY_CREDENTIAL_SECURITY_CONTROLS.to_vec(),
379            observability_evidence_fields: TEMPORARY_CREDENTIAL_OBSERVABILITY_FIELDS.to_vec(),
380            failure_modes: TEMPORARY_CREDENTIAL_FAILURE_MODES.to_vec(),
381            validation_test_coverage: TEMPORARY_CREDENTIAL_VALIDATION_TESTS.to_vec(),
382            product_specific_caveats: TEMPORARY_CREDENTIAL_CAVEATS.to_vec(),
383        }
384    }
385}