objectstore_client/
auth.rs

1use std::collections::{BTreeMap, HashSet};
2
3use jsonwebtoken::{Algorithm, EncodingKey, Header, encode, get_current_timestamp};
4use objectstore_types::Permission;
5use serde::{Deserialize, Serialize};
6
7use crate::ScopeInner;
8
9const DEFAULT_EXPIRY_SECONDS: u64 = 60;
10const DEFAULT_PERMISSIONS: [Permission; 3] = [
11    Permission::ObjectRead,
12    Permission::ObjectWrite,
13    Permission::ObjectDelete,
14];
15
16/// Key configuration that will be used to sign tokens in Objectstore requests.
17pub struct SecretKey {
18    /// A key ID that Objectstore must use to load the corresponding public key.
19    pub kid: String,
20
21    /// An EdDSA private key.
22    pub secret_key: String,
23}
24
25impl std::fmt::Debug for SecretKey {
26    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
27        f.debug_struct("SecretKey")
28            .field("kid", &self.kid)
29            .field("secret_key", &"[redacted]")
30            .finish()
31    }
32}
33
34/// A utility to generate auth tokens to be used in Objectstore requests.
35///
36/// Tokens are signed with an EdDSA private key and have certain permissions and expiry timeouts
37/// applied.
38#[derive(Debug)]
39pub struct TokenGenerator {
40    kid: String,
41    encoding_key: EncodingKey,
42    expiry_seconds: u64,
43    permissions: HashSet<Permission>,
44}
45
46#[derive(Serialize, Deserialize)]
47struct JwtRes {
48    #[serde(rename = "os:usecase")]
49    usecase: String,
50
51    #[serde(flatten)]
52    scopes: BTreeMap<String, String>,
53}
54
55#[derive(Serialize, Deserialize)]
56struct JwtClaims {
57    exp: u64,
58    permissions: HashSet<Permission>,
59    res: JwtRes,
60}
61
62impl TokenGenerator {
63    /// Create a new [`TokenGenerator`] for a given key configuration.
64    pub fn new(secret_key: SecretKey) -> crate::Result<TokenGenerator> {
65        let encoding_key = EncodingKey::from_ed_pem(secret_key.secret_key.as_bytes())?;
66        Ok(TokenGenerator {
67            kid: secret_key.kid,
68            encoding_key,
69            expiry_seconds: DEFAULT_EXPIRY_SECONDS,
70            permissions: HashSet::from(DEFAULT_PERMISSIONS),
71        })
72    }
73
74    /// Set the expiry duration for tokens signed by this generator.
75    pub fn expiry_seconds(mut self, expiry_seconds: u64) -> Self {
76        self.expiry_seconds = expiry_seconds;
77        self
78    }
79
80    /// Set the permissions that will be granted to tokens signed by this generator.
81    pub fn permissions(mut self, permissions: &[Permission]) -> Self {
82        self.permissions = HashSet::from_iter(permissions.iter().cloned());
83        self
84    }
85
86    /// Sign a new token for the passed-in scope using the configured expiry and permissions.
87    pub(crate) fn sign_for_scope(&self, scope: &ScopeInner) -> crate::Result<String> {
88        let claims = JwtClaims {
89            exp: get_current_timestamp() + self.expiry_seconds,
90            permissions: self.permissions.clone(),
91            res: JwtRes {
92                usecase: scope.usecase().name().into(),
93                scopes: scope
94                    .scopes()
95                    .iter()
96                    .map(|scope| (scope.name().to_string(), scope.value().to_string()))
97                    .collect(),
98            },
99        };
100
101        let mut header = Header::new(Algorithm::EdDSA);
102        header.kid = Some(self.kid.clone());
103
104        Ok(encode(&header, &claims, &self.encoding_key)?)
105    }
106}