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}