1use std::collections::{BTreeMap, HashSet};
2
3use jsonwebtoken::{Algorithm, Header, TokenData, Validation, decode, decode_header};
4use objectstore_service::id::ObjectContext;
5use objectstore_types::auth::Permission;
6use serde::{Deserialize, Serialize};
7
8use crate::auth::error::AuthError;
9use crate::auth::key_directory::PublicKeyDirectory;
10use crate::auth::util::StringOrWildcard;
11
12#[derive(Deserialize, Serialize, Debug, Clone)]
13struct JwtRes {
14 #[serde(rename = "os:usecase")]
15 usecase: String,
16
17 #[serde(flatten)]
18 scope: BTreeMap<String, StringOrWildcard>,
19}
20
21#[derive(Deserialize, Serialize, Debug, Clone)]
22struct JwtClaims {
23 res: JwtRes,
24 permissions: HashSet<Permission>,
25}
26
27fn jwt_validation_params(jwt_header: &Header) -> Validation {
28 let mut validation = Validation::new(jwt_header.alg);
29 validation.set_audience(&["objectstore"]);
30 validation.set_issuer(&["sentry", "relay"]);
31 validation.set_required_spec_claims(&["exp"]);
32 validation
33}
34
35#[derive(Debug, PartialEq)]
40#[non_exhaustive]
41pub struct AuthContext {
42 pub usecase: String,
46
47 pub scopes: BTreeMap<String, StringOrWildcard>,
51
52 pub permissions: HashSet<Permission>,
54}
55
56impl AuthContext {
57 pub fn from_encoded_jwt(
70 encoded_token: Option<&str>,
71 key_directory: &PublicKeyDirectory,
72 ) -> Result<AuthContext, AuthError> {
73 let encoded_token =
74 encoded_token.ok_or(AuthError::BadRequest("No authorization token provided"))?;
75
76 let jwt_header = decode_header(encoded_token)?;
77 let key_id = jwt_header
78 .kid
79 .as_ref()
80 .ok_or(AuthError::BadRequest("JWT header is missing `kid` field"))?;
81
82 let key_config = key_directory
83 .keys
84 .get(key_id)
85 .ok_or_else(|| AuthError::InternalError(format!("Key `{key_id}` not configured")))?;
86
87 if jwt_header.alg != Algorithm::EdDSA {
88 objectstore_log::warn!(
89 algorithm = ?jwt_header.alg,
90 "JWT signed with unexpected algorithm",
91 );
92 let kind = jsonwebtoken::errors::ErrorKind::InvalidAlgorithm;
93 return Err(AuthError::ValidationFailure(kind.into()));
94 }
95
96 let mut verified_claims: Option<TokenData<JwtClaims>> = None;
97 for decoding_key in &key_config.key_versions {
98 let decode_result = decode::<JwtClaims>(
99 encoded_token,
100 decoding_key,
101 &jwt_validation_params(&jwt_header),
102 );
103
104 use jsonwebtoken::errors::ErrorKind;
106 if decode_result
107 .as_ref()
108 .is_err_and(|err| err.kind() == &ErrorKind::InvalidSignature)
109 {
110 continue;
111 }
112
113 verified_claims = Some(decode_result?);
114 break;
115 }
116 let verified_claims = verified_claims.ok_or(AuthError::VerificationFailure)?;
117
118 let usecase = verified_claims.claims.res.usecase;
119 let scope = verified_claims.claims.res.scope;
120
121 let permissions = verified_claims
124 .claims
125 .permissions
126 .intersection(&key_config.max_permissions)
127 .cloned()
128 .collect();
129
130 Ok(AuthContext {
131 usecase,
132 scopes: scope,
133 permissions,
134 })
135 }
136
137 pub fn assert_authorized(
143 &self,
144 perm: Permission,
145 context: &ObjectContext,
146 ) -> Result<(), AuthError> {
147 if !self.permissions.contains(&perm) || self.usecase != context.usecase {
148 return Err(AuthError::NotPermitted);
149 }
150
151 for scope in &context.scopes {
152 let authorized = match self.scopes.get(scope.name()) {
153 Some(StringOrWildcard::String(s)) => s == scope.value(),
154 Some(StringOrWildcard::Wildcard) => true,
155 None => false,
156 };
157 if !authorized {
158 return Err(AuthError::NotPermitted);
159 }
160 }
161
162 Ok(())
163 }
164}
165
166#[cfg(test)]
167mod tests {
168 use super::*;
169 use crate::auth::PublicKeyConfig;
170 use jsonwebtoken::DecodingKey;
171 use objectstore_types::scope::{Scope, Scopes};
172 use serde_json::json;
173
174 use objectstore_test::server::{TEST_EDDSA_KID, TEST_EDDSA_PRIVKEY, TEST_EDDSA_PUBKEY};
175
176 #[derive(Serialize, Deserialize)]
177 struct TestJwtClaims {
178 exp: u64,
179 #[serde(flatten)]
180 claims: JwtClaims,
181 }
182
183 fn max_permission() -> HashSet<Permission> {
184 HashSet::from([
185 Permission::ObjectRead,
186 Permission::ObjectWrite,
187 Permission::ObjectDelete,
188 ])
189 }
190
191 fn test_key_config(max_permissions: HashSet<Permission>) -> PublicKeyDirectory {
192 let public_key = PublicKeyConfig {
193 key_versions: vec![DecodingKey::from_ed_pem(TEST_EDDSA_PUBKEY.as_bytes()).unwrap()],
194 max_permissions,
195 };
196 PublicKeyDirectory {
197 keys: BTreeMap::from([(TEST_EDDSA_KID.into(), public_key)]),
198 }
199 }
200
201 fn sign_token(claims: &JwtClaims, signing_secret: &str, exp: Option<u64>) -> String {
202 use jsonwebtoken::{Algorithm, EncodingKey, Header, encode, get_current_timestamp};
203
204 let mut header = Header::new(Algorithm::EdDSA);
205 header.kid = Some(TEST_EDDSA_KID.into());
206 header.typ = Some("JWT".into());
207
208 let claims = TestJwtClaims {
209 exp: exp.unwrap_or_else(|| get_current_timestamp() + 300),
210 claims: claims.clone(),
211 };
212
213 let key = EncodingKey::from_ed_pem(signing_secret.as_bytes()).unwrap();
214 encode(&header, &claims, &key).unwrap()
215 }
216
217 fn sample_claims(
218 org: &str,
219 proj: &str,
220 usecase: &str,
221 permissions: HashSet<Permission>,
222 ) -> JwtClaims {
223 serde_json::from_value(json!({
224 "res": {
225 "os:usecase": usecase,
226 "org": org,
227 "project": proj,
228 },
229 "permissions": permissions,
230 }))
231 .unwrap()
232 }
233
234 fn sample_auth_context(org: &str, proj: &str, permissions: HashSet<Permission>) -> AuthContext {
235 AuthContext {
236 usecase: "attachments".into(),
237 permissions,
238 scopes: serde_json::from_value(json!({"org": org, "project": proj})).unwrap(),
239 }
240 }
241
242 #[test]
243 fn test_from_encoded_jwt_basic() -> Result<(), AuthError> {
244 let claims = sample_claims("123", "456", "attachments", max_permission());
246 let encoded_token = sign_token(&claims, &TEST_EDDSA_PRIVKEY, None);
247
248 let test_config = test_key_config(max_permission());
250 let auth_context =
251 AuthContext::from_encoded_jwt(Some(encoded_token.as_str()), &test_config)?;
252
253 let expected = sample_auth_context("123", "456", max_permission());
255 assert_eq!(auth_context, expected);
256
257 Ok(())
258 }
259
260 #[test]
261 fn test_from_encoded_jwt_max_permissions_limit() -> Result<(), AuthError> {
262 let claims = sample_claims("123", "456", "attachments", max_permission());
264 let encoded_token = sign_token(&claims, &TEST_EDDSA_PRIVKEY, None);
265
266 let ro_permission = HashSet::from([Permission::ObjectRead]);
268 let test_config = test_key_config(ro_permission.clone());
269 let auth_context =
270 AuthContext::from_encoded_jwt(Some(encoded_token.as_str()), &test_config)?;
271
272 let expected = sample_auth_context("123", "456", ro_permission);
274 assert_eq!(auth_context, expected);
275
276 Ok(())
277 }
278
279 #[test]
280 fn test_from_encoded_jwt_invalid_token_fails() -> Result<(), AuthError> {
281 let encoded_token = "abcdef";
283
284 let test_config = test_key_config(max_permission());
286 let auth_context = AuthContext::from_encoded_jwt(Some(encoded_token), &test_config);
287
288 assert!(matches!(auth_context, Err(AuthError::ValidationFailure(_))));
290
291 Ok(())
292 }
293
294 #[test]
295 fn test_from_encoded_jwt_unknown_key_fails() -> Result<(), AuthError> {
296 let claims = sample_claims("123", "456", "attachments", max_permission());
297 let unknown_key = r#"-----BEGIN PRIVATE KEY-----
298MC4CAQAwBQYDK2VwBCIEIKwVoE4TmTfWoqH3HgLVsEcHs9PHNe+ar/Hp6e4To8pK
299-----END PRIVATE KEY-----
300"#;
301 let encoded_token = sign_token(&claims, unknown_key, None);
302
303 let test_config = test_key_config(max_permission());
305 let auth_context =
306 AuthContext::from_encoded_jwt(Some(encoded_token.as_str()), &test_config);
307
308 assert!(matches!(auth_context, Err(AuthError::VerificationFailure)));
310
311 Ok(())
312 }
313
314 #[test]
315 fn test_from_encoded_jwt_expired() -> Result<(), AuthError> {
316 let claims = sample_claims("123", "456", "attachments", max_permission());
317 let encoded_token = sign_token(
318 &claims,
319 &TEST_EDDSA_PRIVKEY,
320 Some(jsonwebtoken::get_current_timestamp() - 100),
321 );
322
323 let test_config = test_key_config(max_permission());
325 let auth_context =
326 AuthContext::from_encoded_jwt(Some(encoded_token.as_str()), &test_config);
327
328 let Err(AuthError::ValidationFailure(error)) = auth_context else {
330 panic!("auth must fail");
331 };
332 assert_eq!(
333 error.kind(),
334 &jsonwebtoken::errors::ErrorKind::ExpiredSignature
335 );
336
337 Ok(())
338 }
339
340 fn sample_object_context(org: &str, project: &str) -> ObjectContext {
341 ObjectContext {
342 usecase: "attachments".into(),
343 scopes: Scopes::from_iter([
344 Scope::create("org", org).unwrap(),
345 Scope::create("project", project).unwrap(),
346 ]),
347 }
348 }
349
350 #[test]
354 fn test_assert_authorized_exact_scope_allowed() -> Result<(), AuthError> {
355 let auth_context = sample_auth_context("123", "456", max_permission());
356 let object = sample_object_context("123", "456");
357
358 auth_context.assert_authorized(Permission::ObjectRead, &object)?;
359
360 Ok(())
361 }
362
363 #[test]
367 fn test_assert_authorized_wildcard_project_allowed() -> Result<(), AuthError> {
368 let auth_context = sample_auth_context("123", "*", max_permission());
369 let object = sample_object_context("123", "456");
370
371 auth_context.assert_authorized(Permission::ObjectRead, &object)?;
372
373 Ok(())
374 }
375
376 #[test]
380 fn test_assert_authorized_org_only_path_allowed() -> Result<(), AuthError> {
381 let auth_context = sample_auth_context("123", "456", max_permission());
382 let object = ObjectContext {
383 usecase: "attachments".into(),
384 scopes: Scopes::from_iter([Scope::create("org", "123").unwrap()]),
385 };
386
387 auth_context.assert_authorized(Permission::ObjectRead, &object)?;
388
389 Ok(())
390 }
391
392 #[test]
399 fn test_assert_authorized_scope_mismatch_fails() -> Result<(), AuthError> {
400 let auth_context = sample_auth_context("123", "456", max_permission());
401 let object = sample_object_context("123", "999");
402
403 let result = auth_context.assert_authorized(Permission::ObjectRead, &object);
404 assert_eq!(result, Err(AuthError::NotPermitted));
405
406 let auth_context = sample_auth_context("123", "456", max_permission());
407 let object = sample_object_context("999", "456");
408
409 let result = auth_context.assert_authorized(Permission::ObjectRead, &object);
410 assert_eq!(result, Err(AuthError::NotPermitted));
411
412 Ok(())
413 }
414
415 #[test]
416 fn test_assert_authorized_wrong_usecase_fails() -> Result<(), AuthError> {
417 let mut auth_context = sample_auth_context("123", "456", max_permission());
418 auth_context.usecase = "debug-files".into();
419 let object = sample_object_context("123", "456");
420
421 let result = auth_context.assert_authorized(Permission::ObjectRead, &object);
422 assert_eq!(result, Err(AuthError::NotPermitted));
423
424 Ok(())
425 }
426
427 #[test]
428 fn test_assert_authorized_auth_context_missing_permission_fails() -> Result<(), AuthError> {
429 let auth_context =
430 sample_auth_context("123", "456", HashSet::from([Permission::ObjectRead]));
431 let object = sample_object_context("123", "456");
432
433 let result = auth_context.assert_authorized(Permission::ObjectWrite, &object);
434 assert_eq!(result, Err(AuthError::NotPermitted));
435
436 Ok(())
437 }
438}