1use std::collections::{BTreeMap, HashSet};
2
3use jsonwebtoken::{Algorithm, Header, TokenData, Validation, decode, decode_header};
4use objectstore_service::id::ObjectContext;
5use objectstore_types::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 tracing::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 tracing::debug!(?self, ?perm, ?context, "Authorization failed");
149 return Err(AuthError::NotPermitted);
150 }
151
152 for scope in &context.scopes {
153 let authorized = match self.scopes.get(scope.name()) {
154 Some(StringOrWildcard::String(s)) => s == scope.value(),
155 Some(StringOrWildcard::Wildcard) => true,
156 None => false,
157 };
158 if !authorized {
159 tracing::debug!(?self, ?perm, ?context, "Authorization failed");
160 return Err(AuthError::NotPermitted);
161 }
162 }
163
164 Ok(())
165 }
166}
167
168#[cfg(test)]
169mod tests {
170 use super::*;
171 use crate::auth::PublicKeyConfig;
172 use jsonwebtoken::DecodingKey;
173 use objectstore_types::scope::{Scope, Scopes};
174 use serde_json::json;
175
176 use objectstore_test::server::{TEST_EDDSA_KID, TEST_EDDSA_PRIVKEY, TEST_EDDSA_PUBKEY};
177
178 #[derive(Serialize, Deserialize)]
179 struct TestJwtClaims {
180 exp: u64,
181 #[serde(flatten)]
182 claims: JwtClaims,
183 }
184
185 fn max_permission() -> HashSet<Permission> {
186 HashSet::from([
187 Permission::ObjectRead,
188 Permission::ObjectWrite,
189 Permission::ObjectDelete,
190 ])
191 }
192
193 fn test_key_config(max_permissions: HashSet<Permission>) -> PublicKeyDirectory {
194 let public_key = PublicKeyConfig {
195 key_versions: vec![DecodingKey::from_ed_pem(TEST_EDDSA_PUBKEY.as_bytes()).unwrap()],
196 max_permissions,
197 };
198 PublicKeyDirectory {
199 keys: BTreeMap::from([(TEST_EDDSA_KID.into(), public_key)]),
200 }
201 }
202
203 fn sign_token(claims: &JwtClaims, signing_secret: &str, exp: Option<u64>) -> String {
204 use jsonwebtoken::{Algorithm, EncodingKey, Header, encode, get_current_timestamp};
205
206 let mut header = Header::new(Algorithm::EdDSA);
207 header.kid = Some(TEST_EDDSA_KID.into());
208 header.typ = Some("JWT".into());
209
210 let claims = TestJwtClaims {
211 exp: exp.unwrap_or_else(|| get_current_timestamp() + 300),
212 claims: claims.clone(),
213 };
214
215 let key = EncodingKey::from_ed_pem(signing_secret.as_bytes()).unwrap();
216 encode(&header, &claims, &key).unwrap()
217 }
218
219 fn sample_claims(
220 org: &str,
221 proj: &str,
222 usecase: &str,
223 permissions: HashSet<Permission>,
224 ) -> JwtClaims {
225 serde_json::from_value(json!({
226 "res": {
227 "os:usecase": usecase,
228 "org": org,
229 "project": proj,
230 },
231 "permissions": permissions,
232 }))
233 .unwrap()
234 }
235
236 fn sample_auth_context(org: &str, proj: &str, permissions: HashSet<Permission>) -> AuthContext {
237 AuthContext {
238 usecase: "attachments".into(),
239 permissions,
240 scopes: serde_json::from_value(json!({"org": org, "project": proj})).unwrap(),
241 }
242 }
243
244 #[test]
245 fn test_from_encoded_jwt_basic() -> Result<(), AuthError> {
246 let claims = sample_claims("123", "456", "attachments", max_permission());
248 let encoded_token = sign_token(&claims, &TEST_EDDSA_PRIVKEY, None);
249
250 let test_config = test_key_config(max_permission());
252 let auth_context =
253 AuthContext::from_encoded_jwt(Some(encoded_token.as_str()), &test_config)?;
254
255 let expected = sample_auth_context("123", "456", max_permission());
257 assert_eq!(auth_context, expected);
258
259 Ok(())
260 }
261
262 #[test]
263 fn test_from_encoded_jwt_max_permissions_limit() -> Result<(), AuthError> {
264 let claims = sample_claims("123", "456", "attachments", max_permission());
266 let encoded_token = sign_token(&claims, &TEST_EDDSA_PRIVKEY, None);
267
268 let ro_permission = HashSet::from([Permission::ObjectRead]);
270 let test_config = test_key_config(ro_permission.clone());
271 let auth_context =
272 AuthContext::from_encoded_jwt(Some(encoded_token.as_str()), &test_config)?;
273
274 let expected = sample_auth_context("123", "456", ro_permission);
276 assert_eq!(auth_context, expected);
277
278 Ok(())
279 }
280
281 #[test]
282 fn test_from_encoded_jwt_invalid_token_fails() -> Result<(), AuthError> {
283 let encoded_token = "abcdef";
285
286 let test_config = test_key_config(max_permission());
288 let auth_context = AuthContext::from_encoded_jwt(Some(encoded_token), &test_config);
289
290 assert!(matches!(auth_context, Err(AuthError::ValidationFailure(_))));
292
293 Ok(())
294 }
295
296 #[test]
297 fn test_from_encoded_jwt_unknown_key_fails() -> Result<(), AuthError> {
298 let claims = sample_claims("123", "456", "attachments", max_permission());
299 let unknown_key = r#"-----BEGIN PRIVATE KEY-----
300MC4CAQAwBQYDK2VwBCIEIKwVoE4TmTfWoqH3HgLVsEcHs9PHNe+ar/Hp6e4To8pK
301-----END PRIVATE KEY-----
302"#;
303 let encoded_token = sign_token(&claims, unknown_key, None);
304
305 let test_config = test_key_config(max_permission());
307 let auth_context =
308 AuthContext::from_encoded_jwt(Some(encoded_token.as_str()), &test_config);
309
310 assert!(matches!(auth_context, Err(AuthError::VerificationFailure)));
312
313 Ok(())
314 }
315
316 #[test]
317 fn test_from_encoded_jwt_expired() -> Result<(), AuthError> {
318 let claims = sample_claims("123", "456", "attachments", max_permission());
319 let encoded_token = sign_token(
320 &claims,
321 &TEST_EDDSA_PRIVKEY,
322 Some(jsonwebtoken::get_current_timestamp() - 100),
323 );
324
325 let test_config = test_key_config(max_permission());
327 let auth_context =
328 AuthContext::from_encoded_jwt(Some(encoded_token.as_str()), &test_config);
329
330 let Err(AuthError::ValidationFailure(error)) = auth_context else {
332 panic!("auth must fail");
333 };
334 assert_eq!(
335 error.kind(),
336 &jsonwebtoken::errors::ErrorKind::ExpiredSignature
337 );
338
339 Ok(())
340 }
341
342 fn sample_object_context(org: &str, project: &str) -> ObjectContext {
343 ObjectContext {
344 usecase: "attachments".into(),
345 scopes: Scopes::from_iter([
346 Scope::create("org", org).unwrap(),
347 Scope::create("project", project).unwrap(),
348 ]),
349 }
350 }
351
352 #[test]
356 fn test_assert_authorized_exact_scope_allowed() -> Result<(), AuthError> {
357 let auth_context = sample_auth_context("123", "456", max_permission());
358 let object = sample_object_context("123", "456");
359
360 auth_context.assert_authorized(Permission::ObjectRead, &object)?;
361
362 Ok(())
363 }
364
365 #[test]
369 fn test_assert_authorized_wildcard_project_allowed() -> Result<(), AuthError> {
370 let auth_context = sample_auth_context("123", "*", max_permission());
371 let object = sample_object_context("123", "456");
372
373 auth_context.assert_authorized(Permission::ObjectRead, &object)?;
374
375 Ok(())
376 }
377
378 #[test]
382 fn test_assert_authorized_org_only_path_allowed() -> Result<(), AuthError> {
383 let auth_context = sample_auth_context("123", "456", max_permission());
384 let object = ObjectContext {
385 usecase: "attachments".into(),
386 scopes: Scopes::from_iter([Scope::create("org", "123").unwrap()]),
387 };
388
389 auth_context.assert_authorized(Permission::ObjectRead, &object)?;
390
391 Ok(())
392 }
393
394 #[test]
401 fn test_assert_authorized_scope_mismatch_fails() -> Result<(), AuthError> {
402 let auth_context = sample_auth_context("123", "456", max_permission());
403 let object = sample_object_context("123", "999");
404
405 let result = auth_context.assert_authorized(Permission::ObjectRead, &object);
406 assert_eq!(result, Err(AuthError::NotPermitted));
407
408 let auth_context = sample_auth_context("123", "456", max_permission());
409 let object = sample_object_context("999", "456");
410
411 let result = auth_context.assert_authorized(Permission::ObjectRead, &object);
412 assert_eq!(result, Err(AuthError::NotPermitted));
413
414 Ok(())
415 }
416
417 #[test]
418 fn test_assert_authorized_wrong_usecase_fails() -> Result<(), AuthError> {
419 let mut auth_context = sample_auth_context("123", "456", max_permission());
420 auth_context.usecase = "debug-files".into();
421 let object = sample_object_context("123", "456");
422
423 let result = auth_context.assert_authorized(Permission::ObjectRead, &object);
424 assert_eq!(result, Err(AuthError::NotPermitted));
425
426 Ok(())
427 }
428
429 #[test]
430 fn test_assert_authorized_auth_context_missing_permission_fails() -> Result<(), AuthError> {
431 let auth_context =
432 sample_auth_context("123", "456", HashSet::from([Permission::ObjectRead]));
433 let object = sample_object_context("123", "456");
434
435 let result = auth_context.assert_authorized(Permission::ObjectWrite, &object);
436 assert_eq!(result, Err(AuthError::NotPermitted));
437
438 Ok(())
439 }
440}