1use std::collections::{BTreeMap, HashSet};
2use std::time::{Duration, SystemTime};
3
4use http::Method;
5use jsonwebtoken::{Algorithm, Header, TokenData, Validation, decode, decode_header};
6use objectstore_service::id::ObjectContext;
7use objectstore_types::auth::Permission;
8use objectstore_types::presign::CanonicalRequest;
9use serde::{Deserialize, Serialize};
10
11use crate::auth::KeyId;
12use crate::auth::error::AuthError;
13use crate::auth::key_directory::PublicKeyDirectory;
14use crate::auth::util::StringOrWildcard;
15
16#[derive(Deserialize, Serialize, Debug, Clone)]
17struct JwtRes {
18 #[serde(rename = "os:usecase")]
19 usecase: String,
20
21 #[serde(flatten)]
22 scope: BTreeMap<String, StringOrWildcard>,
23}
24
25#[derive(Deserialize, Serialize, Debug, Clone)]
26struct JwtClaims {
27 res: JwtRes,
28 permissions: HashSet<Permission>,
29}
30
31fn jwt_validation_params(jwt_header: &Header) -> Validation {
32 let mut validation = Validation::new(jwt_header.alg);
33 validation.set_audience(&["objectstore"]);
34 validation.set_issuer(&["sentry", "relay"]);
35 validation.set_required_spec_claims(&["exp"]);
36 validation
37}
38
39#[derive(Debug, PartialEq)]
41#[non_exhaustive]
42pub struct ScopedContext {
43 pub usecase: String,
47
48 pub scopes: BTreeMap<String, StringOrWildcard>,
52
53 pub permissions: HashSet<Permission>,
55}
56
57const MAX_PRESIGN_DURATION: Duration = Duration::from_secs(7 * 24 * 60 * 60); #[derive(Debug, Deserialize)]
62pub struct PresignParams {
63 #[serde(rename = "os_kid")]
65 pub key_id: KeyId,
66 #[serde(rename = "os_sig")]
68 pub signature: String,
69 #[serde(rename = "os_timestamp", with = "humantime_serde")]
71 pub timestamp: SystemTime,
72 #[serde(rename = "os_duration")]
74 pub duration_secs: u64,
75}
76
77#[derive(Debug, PartialEq)]
82pub enum AuthContext {
83 Disabled,
85 Scoped(ScopedContext),
87 Preauthorized,
89}
90
91impl AuthContext {
92 pub fn from_encoded_jwt(
105 encoded_token: Option<&str>,
106 key_directory: &PublicKeyDirectory,
107 ) -> Result<AuthContext, AuthError> {
108 let encoded_token =
109 encoded_token.ok_or(AuthError::BadRequest("No authorization token provided"))?;
110
111 let jwt_header = decode_header(encoded_token)?;
112 let key_id = jwt_header
113 .kid
114 .as_ref()
115 .ok_or(AuthError::BadRequest("JWT header is missing `kid` field"))?;
116
117 let key_config = key_directory
118 .keys
119 .get(key_id)
120 .ok_or(AuthError::UnknownKey)?;
121
122 if jwt_header.alg != Algorithm::EdDSA {
123 objectstore_log::warn!(
124 algorithm = ?jwt_header.alg,
125 "JWT signed with unexpected algorithm",
126 );
127 let kind = jsonwebtoken::errors::ErrorKind::InvalidAlgorithm;
128 return Err(AuthError::ValidationFailure(kind.into()));
129 }
130
131 let mut verified_claims: Option<TokenData<JwtClaims>> = None;
132 for key_version in &key_config.key_versions {
133 let decode_result = decode::<JwtClaims>(
134 encoded_token,
135 &key_version.decoding_key,
136 &jwt_validation_params(&jwt_header),
137 );
138
139 use jsonwebtoken::errors::ErrorKind;
141 if decode_result
142 .as_ref()
143 .is_err_and(|err| err.kind() == &ErrorKind::InvalidSignature)
144 {
145 continue;
146 }
147
148 verified_claims = Some(decode_result?);
149 break;
150 }
151 let verified_claims = verified_claims.ok_or(AuthError::VerificationFailure)?;
152
153 let usecase = verified_claims.claims.res.usecase;
154 let scope = verified_claims.claims.res.scope;
155
156 let permissions = verified_claims
159 .claims
160 .permissions
161 .intersection(&key_config.max_permissions)
162 .copied()
163 .collect();
164
165 Ok(AuthContext::Scoped(ScopedContext {
166 usecase,
167 scopes: scope,
168 permissions,
169 }))
170 }
171
172 pub fn from_presigned_request(
179 method: &Method,
180 path: &str,
181 raw_query: Option<&str>,
182 params: &PresignParams,
183 key_directory: &PublicKeyDirectory,
184 now: SystemTime,
185 ) -> Result<AuthContext, AuthError> {
186 let key_config = key_directory
187 .keys
188 .get(¶ms.key_id)
189 .ok_or(AuthError::UnknownKey)?;
190
191 let duration = Duration::from_secs(params.duration_secs);
192 if duration > MAX_PRESIGN_DURATION {
193 return Err(AuthError::BadRequest(
194 "presigned URL validity exceeds the maximum of 1 week",
195 ));
196 }
197
198 let start = params
199 .timestamp
200 .checked_sub(Duration::from_secs(60))
202 .ok_or(AuthError::VerificationFailure)?;
203 let end = params
204 .timestamp
205 .checked_add(duration)
206 .ok_or(AuthError::VerificationFailure)?;
207 if now < start || now > end {
208 return Err(AuthError::VerificationFailure);
209 }
210
211 let canonical = CanonicalRequest::new(method, path, raw_query);
212
213 let verified = key_config.key_versions.iter().any(|key| {
214 canonical
215 .verify(key.verifying_key.as_bytes(), ¶ms.signature)
216 .is_ok()
217 });
218 if !verified {
219 return Err(AuthError::VerificationFailure);
220 }
221
222 if !key_config.max_permissions.contains(&Permission::ObjectRead) {
224 return Err(AuthError::NotPermitted);
225 }
226
227 Ok(AuthContext::Preauthorized)
228 }
229
230 pub fn assert_authorized(
239 &self,
240 perm: Permission,
241 context: &ObjectContext,
242 ) -> Result<(), AuthError> {
243 let scoped = match self {
244 AuthContext::Disabled => return Ok(()),
245 AuthContext::Preauthorized =>
246 {
248 return if perm == Permission::ObjectRead {
249 Ok(())
250 } else {
251 Err(AuthError::NotPermitted)
252 };
253 }
254 AuthContext::Scoped(scoped) => scoped,
255 };
256
257 if !scoped.permissions.contains(&perm) || scoped.usecase != context.usecase {
258 return Err(AuthError::NotPermitted);
259 }
260
261 for scope in &context.scopes {
262 let authorized = match scoped.scopes.get(scope.name()) {
263 Some(StringOrWildcard::String(s)) => s == scope.value(),
264 Some(StringOrWildcard::Wildcard) => true,
265 None => false,
266 };
267 if !authorized {
268 return Err(AuthError::NotPermitted);
269 }
270 }
271
272 Ok(())
273 }
274}
275
276#[cfg(test)]
277mod tests {
278 use super::*;
279 use crate::auth::{PublicKey, PublicKeyConfig};
280 use ed25519_dalek::pkcs8::{DecodePrivateKey, DecodePublicKey};
281 use ed25519_dalek::{SigningKey, VerifyingKey};
282 use jsonwebtoken::DecodingKey;
283 use objectstore_types::presign::{
284 CanonicalRequest, PARAM_DURATION, PARAM_KID, PARAM_SIG, PARAM_TIMESTAMP,
285 };
286 use objectstore_types::scope::{Scope, Scopes};
287 use serde_json::json;
288
289 use objectstore_test::server::{TEST_EDDSA_KID, TEST_EDDSA_PRIVKEY, TEST_EDDSA_PUBKEY};
290
291 #[derive(Serialize, Deserialize)]
292 struct TestJwtClaims {
293 exp: u64,
294 #[serde(flatten)]
295 claims: JwtClaims,
296 }
297
298 fn max_permission() -> HashSet<Permission> {
299 HashSet::from([
300 Permission::ObjectRead,
301 Permission::ObjectWrite,
302 Permission::ObjectDelete,
303 ])
304 }
305
306 fn test_key_config(max_permissions: HashSet<Permission>) -> PublicKeyDirectory {
307 let public_key = PublicKeyConfig {
308 key_versions: vec![PublicKey {
309 decoding_key: DecodingKey::from_ed_pem(TEST_EDDSA_PUBKEY.as_bytes()).unwrap(),
310 verifying_key: VerifyingKey::from_public_key_pem(TEST_EDDSA_PUBKEY).unwrap(),
311 }],
312 max_permissions,
313 };
314 PublicKeyDirectory {
315 keys: BTreeMap::from([(TEST_EDDSA_KID.into(), public_key)]),
316 }
317 }
318
319 fn sign_token(claims: &JwtClaims, signing_secret: &str, exp: Option<u64>) -> String {
320 use jsonwebtoken::{Algorithm, EncodingKey, Header, encode, get_current_timestamp};
321
322 let mut header = Header::new(Algorithm::EdDSA);
323 header.kid = Some(TEST_EDDSA_KID.into());
324 header.typ = Some("JWT".into());
325
326 let claims = TestJwtClaims {
327 exp: exp.unwrap_or_else(|| get_current_timestamp() + 300),
328 claims: claims.clone(),
329 };
330
331 let key = EncodingKey::from_ed_pem(signing_secret.as_bytes()).unwrap();
332 encode(&header, &claims, &key).unwrap()
333 }
334
335 fn sample_claims(
336 org: &str,
337 proj: &str,
338 usecase: &str,
339 permissions: HashSet<Permission>,
340 ) -> JwtClaims {
341 serde_json::from_value(json!({
342 "res": {
343 "os:usecase": usecase,
344 "org": org,
345 "project": proj,
346 },
347 "permissions": permissions,
348 }))
349 .unwrap()
350 }
351
352 fn sample_auth_context(org: &str, proj: &str, permissions: HashSet<Permission>) -> AuthContext {
353 AuthContext::Scoped(ScopedContext {
354 usecase: "attachments".into(),
355 permissions,
356 scopes: serde_json::from_value(json!({"org": org, "project": proj})).unwrap(),
357 })
358 }
359
360 #[test]
361 fn test_from_encoded_jwt_basic() -> Result<(), AuthError> {
362 let claims = sample_claims("123", "456", "attachments", max_permission());
364 let encoded_token = sign_token(&claims, TEST_EDDSA_PRIVKEY, None);
365
366 let test_config = test_key_config(max_permission());
368 let auth_context =
369 AuthContext::from_encoded_jwt(Some(encoded_token.as_str()), &test_config)?;
370
371 let expected = sample_auth_context("123", "456", max_permission());
373 assert_eq!(auth_context, expected);
374
375 Ok(())
376 }
377
378 #[test]
379 fn test_from_encoded_jwt_max_permissions_limit() -> Result<(), AuthError> {
380 let claims = sample_claims("123", "456", "attachments", max_permission());
382 let encoded_token = sign_token(&claims, TEST_EDDSA_PRIVKEY, None);
383
384 let ro_permission = HashSet::from([Permission::ObjectRead]);
386 let test_config = test_key_config(ro_permission.clone());
387 let auth_context =
388 AuthContext::from_encoded_jwt(Some(encoded_token.as_str()), &test_config)?;
389
390 let expected = sample_auth_context("123", "456", ro_permission);
392 assert_eq!(auth_context, expected);
393
394 Ok(())
395 }
396
397 #[test]
398 fn test_from_encoded_jwt_invalid_token_fails() -> Result<(), AuthError> {
399 let encoded_token = "abcdef";
401
402 let test_config = test_key_config(max_permission());
404 let auth_context = AuthContext::from_encoded_jwt(Some(encoded_token), &test_config);
405
406 assert!(matches!(auth_context, Err(AuthError::ValidationFailure(_))));
408
409 Ok(())
410 }
411
412 #[test]
413 fn test_from_encoded_jwt_unknown_key_fails() -> Result<(), AuthError> {
414 let claims = sample_claims("123", "456", "attachments", max_permission());
415 let unknown_key = r#"-----BEGIN PRIVATE KEY-----
416MC4CAQAwBQYDK2VwBCIEIKwVoE4TmTfWoqH3HgLVsEcHs9PHNe+ar/Hp6e4To8pK
417-----END PRIVATE KEY-----
418"#;
419 let encoded_token = sign_token(&claims, unknown_key, None);
420
421 let test_config = test_key_config(max_permission());
423 let auth_context =
424 AuthContext::from_encoded_jwt(Some(encoded_token.as_str()), &test_config);
425
426 assert!(matches!(auth_context, Err(AuthError::VerificationFailure)));
428
429 Ok(())
430 }
431
432 #[test]
433 fn test_from_encoded_jwt_expired() -> Result<(), AuthError> {
434 let claims = sample_claims("123", "456", "attachments", max_permission());
435 let encoded_token = sign_token(
436 &claims,
437 TEST_EDDSA_PRIVKEY,
438 Some(jsonwebtoken::get_current_timestamp() - 100),
439 );
440
441 let test_config = test_key_config(max_permission());
443 let auth_context =
444 AuthContext::from_encoded_jwt(Some(encoded_token.as_str()), &test_config);
445
446 let Err(AuthError::ValidationFailure(error)) = auth_context else {
448 panic!("auth must fail");
449 };
450 assert_eq!(
451 error.kind(),
452 &jsonwebtoken::errors::ErrorKind::ExpiredSignature
453 );
454
455 Ok(())
456 }
457
458 fn sample_object_context(org: &str, project: &str) -> ObjectContext {
459 ObjectContext {
460 usecase: "attachments".into(),
461 scopes: Scopes::from_iter([
462 Scope::create("org", org).unwrap(),
463 Scope::create("project", project).unwrap(),
464 ]),
465 }
466 }
467
468 #[test]
472 fn test_assert_authorized_exact_scope_allowed() -> Result<(), AuthError> {
473 let auth_context = sample_auth_context("123", "456", max_permission());
474 let object = sample_object_context("123", "456");
475
476 auth_context.assert_authorized(Permission::ObjectRead, &object)?;
477
478 Ok(())
479 }
480
481 #[test]
485 fn test_assert_authorized_wildcard_project_allowed() -> Result<(), AuthError> {
486 let auth_context = sample_auth_context("123", "*", max_permission());
487 let object = sample_object_context("123", "456");
488
489 auth_context.assert_authorized(Permission::ObjectRead, &object)?;
490
491 Ok(())
492 }
493
494 #[test]
498 fn test_assert_authorized_org_only_path_allowed() -> Result<(), AuthError> {
499 let auth_context = sample_auth_context("123", "456", max_permission());
500 let object = ObjectContext {
501 usecase: "attachments".into(),
502 scopes: Scopes::from_iter([Scope::create("org", "123").unwrap()]),
503 };
504
505 auth_context.assert_authorized(Permission::ObjectRead, &object)?;
506
507 Ok(())
508 }
509
510 #[test]
517 fn test_assert_authorized_scope_mismatch_fails() -> Result<(), AuthError> {
518 let auth_context = sample_auth_context("123", "456", max_permission());
519 let object = sample_object_context("123", "999");
520
521 let result = auth_context.assert_authorized(Permission::ObjectRead, &object);
522 assert_eq!(result, Err(AuthError::NotPermitted));
523
524 let auth_context = sample_auth_context("123", "456", max_permission());
525 let object = sample_object_context("999", "456");
526
527 let result = auth_context.assert_authorized(Permission::ObjectRead, &object);
528 assert_eq!(result, Err(AuthError::NotPermitted));
529
530 Ok(())
531 }
532
533 #[test]
534 fn test_assert_authorized_wrong_usecase_fails() -> Result<(), AuthError> {
535 let AuthContext::Scoped(mut scoped) = sample_auth_context("123", "456", max_permission())
536 else {
537 panic!("expected a scoped auth context");
538 };
539 scoped.usecase = "debug-files".into();
540 let auth_context = AuthContext::Scoped(scoped);
541 let object = sample_object_context("123", "456");
542
543 let result = auth_context.assert_authorized(Permission::ObjectRead, &object);
544 assert_eq!(result, Err(AuthError::NotPermitted));
545
546 Ok(())
547 }
548
549 #[test]
550 fn test_assert_authorized_auth_context_missing_permission_fails() -> Result<(), AuthError> {
551 let auth_context =
552 sample_auth_context("123", "456", HashSet::from([Permission::ObjectRead]));
553 let object = sample_object_context("123", "456");
554
555 let result = auth_context.assert_authorized(Permission::ObjectWrite, &object);
556 assert_eq!(result, Err(AuthError::NotPermitted));
557
558 Ok(())
559 }
560
561 #[test]
562 fn test_auth_context_from_presigned() {
563 let key_directory = test_key_config(HashSet::from([Permission::ObjectRead]));
564
565 let path = "/v1/objects/test/org=1/key";
566 let timestamp = humantime::format_rfc3339(SystemTime::now()).to_string();
567 let base = format!(
568 "{PARAM_KID}={TEST_EDDSA_KID}&{PARAM_TIMESTAMP}={timestamp}&{PARAM_DURATION}=3600"
569 );
570
571 let signing_key = SigningKey::from_pkcs8_pem(TEST_EDDSA_PRIVKEY).unwrap();
572 let canonical = CanonicalRequest::new(&Method::GET, path, Some(&base));
573 let signature = canonical.sign(signing_key.as_bytes());
574 let query = format!("{base}&{PARAM_SIG}={signature}");
575
576 let params = PresignParams {
577 signature,
578 key_id: TEST_EDDSA_KID.to_string(),
579 timestamp: SystemTime::now(),
580 duration_secs: 3600,
581 };
582
583 let context = AuthContext::from_presigned_request(
584 &Method::GET,
585 path,
586 Some(&query),
587 ¶ms,
588 &key_directory,
589 SystemTime::now(),
590 )
591 .unwrap();
592
593 assert_eq!(context, AuthContext::Preauthorized);
594 }
595
596 #[test]
597 fn test_presigned_request_rejected_without_read_permission() {
598 let key_directory = test_key_config(HashSet::from([
599 Permission::ObjectWrite,
600 Permission::ObjectDelete,
601 ]));
602
603 let path = "/v1/objects/test/org=1/key";
604 let timestamp = humantime::format_rfc3339(SystemTime::now()).to_string();
605 let base = format!(
606 "{PARAM_KID}={TEST_EDDSA_KID}&{PARAM_TIMESTAMP}={timestamp}&{PARAM_DURATION}=3600"
607 );
608
609 let signing_key = SigningKey::from_pkcs8_pem(TEST_EDDSA_PRIVKEY).unwrap();
610 let canonical = CanonicalRequest::new(&Method::GET, path, Some(&base));
611 let signature = canonical.sign(signing_key.as_bytes());
612 let query = format!("{base}&{PARAM_SIG}={signature}");
613
614 let params = PresignParams {
615 signature,
616 key_id: TEST_EDDSA_KID.to_string(),
617 timestamp: SystemTime::now(),
618 duration_secs: 3600,
619 };
620
621 let result = AuthContext::from_presigned_request(
622 &Method::GET,
623 path,
624 Some(&query),
625 ¶ms,
626 &key_directory,
627 SystemTime::now(),
628 );
629
630 assert_eq!(result, Err(AuthError::NotPermitted));
631 }
632}