objectstore_client/
auth.rs

1use std::collections::{BTreeMap, HashSet};
2
3use jsonwebtoken::{Algorithm, EncodingKey, Header, encode, get_current_timestamp};
4use objectstore_types::scope;
5use serde::{Deserialize, Serialize};
6
7use crate::ScopeInner;
8
9pub use objectstore_types::auth::Permission;
10
11const DEFAULT_EXPIRY_SECONDS: u64 = 60;
12const DEFAULT_PERMISSIONS: [Permission; 3] = [
13    Permission::ObjectRead,
14    Permission::ObjectWrite,
15    Permission::ObjectDelete,
16];
17
18/// Key configuration that will be used to sign tokens in Objectstore requests.
19pub struct SecretKey {
20    /// A key ID that Objectstore must use to load the corresponding public key.
21    pub kid: String,
22
23    /// An EdDSA private key.
24    pub secret_key: String,
25}
26
27impl std::fmt::Debug for SecretKey {
28    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
29        f.debug_struct("SecretKey")
30            .field("kid", &self.kid)
31            .field("secret_key", &"[redacted]")
32            .finish()
33    }
34}
35
36/// Authentication provider for Objectstore requests.
37///
38/// Can be either a [`TokenGenerator`] that signs a fresh JWT per request,
39/// or a static pre-signed JWT string.
40pub enum TokenProvider {
41    /// A pre-signed JWT token string, used as-is for every request.
42    Static(String),
43    /// A generator that signs a fresh JWT for each request using an EdDSA keypair.
44    Generator(TokenGenerator),
45}
46
47impl std::fmt::Debug for TokenProvider {
48    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
49        match self {
50            TokenProvider::Static(_) => f.write_str("TokenProvider::Static([redacted])"),
51            TokenProvider::Generator(g) => {
52                f.debug_tuple("TokenProvider::Generator").field(g).finish()
53            }
54        }
55    }
56}
57
58impl From<TokenGenerator> for TokenProvider {
59    fn from(generator: TokenGenerator) -> Self {
60        TokenProvider::Generator(generator)
61    }
62}
63
64impl From<String> for TokenProvider {
65    fn from(token: String) -> Self {
66        TokenProvider::Static(token)
67    }
68}
69
70impl From<&str> for TokenProvider {
71    fn from(token: &str) -> Self {
72        TokenProvider::Static(token.to_owned())
73    }
74}
75
76/// A utility to generate auth tokens to be used in Objectstore requests.
77///
78/// Tokens are signed with an EdDSA private key and have certain permissions and expiry timeouts
79/// applied.
80///
81/// Use this for internal services that have access to an EdDSA keypair. You can pass a
82/// `TokenGenerator` directly to [`ClientBuilder::token`](crate::ClientBuilder::token),
83/// and it will be automatically converted into a [`TokenProvider::Generator`].
84#[derive(Debug)]
85pub struct TokenGenerator {
86    kid: String,
87    encoding_key: EncodingKey,
88    expiry_seconds: u64,
89    permissions: HashSet<Permission>,
90}
91
92#[derive(Serialize, Deserialize)]
93struct JwtRes {
94    #[serde(rename = "os:usecase")]
95    usecase: String,
96
97    #[serde(flatten)]
98    scopes: BTreeMap<String, String>,
99}
100
101#[derive(Serialize, Deserialize)]
102struct JwtClaims {
103    exp: u64,
104    permissions: HashSet<Permission>,
105    res: JwtRes,
106}
107
108impl TokenGenerator {
109    /// Create a new [`TokenGenerator`] for a given key configuration.
110    pub fn new(secret_key: SecretKey) -> crate::Result<TokenGenerator> {
111        let encoding_key = EncodingKey::from_ed_pem(secret_key.secret_key.as_bytes())?;
112        Ok(TokenGenerator {
113            kid: secret_key.kid,
114            encoding_key,
115            expiry_seconds: DEFAULT_EXPIRY_SECONDS,
116            permissions: HashSet::from(DEFAULT_PERMISSIONS),
117        })
118    }
119
120    /// Set the expiry duration for tokens signed by this generator.
121    pub fn expiry_seconds(mut self, expiry_seconds: u64) -> Self {
122        self.expiry_seconds = expiry_seconds;
123        self
124    }
125
126    /// Set the permissions that will be granted to tokens signed by this generator.
127    pub fn permissions(mut self, permissions: &[Permission]) -> Self {
128        self.permissions = HashSet::from_iter(permissions.iter().cloned());
129        self
130    }
131
132    /// Sign a token for the given [`Scope`](crate::Scope), returning the JWT string.
133    ///
134    /// Use this to produce a static token that can be handed to an external service
135    /// which then passes it to [`ClientBuilder::token`](crate::ClientBuilder::token).
136    ///
137    /// # Errors
138    ///
139    /// Returns an error if the scope is invalid or the JWT cannot be signed.
140    pub fn sign(&self, scope: &crate::Scope) -> crate::Result<String> {
141        let scope = match &scope.0 {
142            Ok(inner) => inner,
143            Err(crate::Error::InvalidScope(err)) => {
144                return Err(err.clone().into());
145            }
146            // Return an ad-hoc `Unreachable` variant to avoid panicking.
147            // It should be impossible to run into a different error variant other than
148            // `InvalidScope`, unless we add a new variant and forget to update this code path.
149            _ => return Err(scope::InvalidScopeError::Unreachable.into()),
150        };
151        self.sign_for_scope(scope)
152    }
153
154    /// Sign a new token for the passed-in scope using the configured expiry and permissions.
155    pub(crate) fn sign_for_scope(&self, scope: &ScopeInner) -> crate::Result<String> {
156        let claims = JwtClaims {
157            exp: get_current_timestamp() + self.expiry_seconds,
158            permissions: self.permissions.clone(),
159            res: JwtRes {
160                usecase: scope.usecase().name().into(),
161                scopes: scope
162                    .scopes()
163                    .iter()
164                    .map(|scope| (scope.name().to_string(), scope.value().to_string()))
165                    .collect(),
166            },
167        };
168
169        let mut header = Header::new(Algorithm::EdDSA);
170        header.kid = Some(self.kid.clone());
171
172        Ok(encode(&header, &claims, &self.encoding_key)?)
173    }
174}