objectstore_client/
auth.rs

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