objectstore_client/
auth.rs1use 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::{Scope, 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
18pub struct SecretKey {
20 pub kid: String,
22
23 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
36pub enum TokenProvider {
41 Static(String),
43 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
58pub trait IntoTokenProvider {
68 fn into_token_provider(self) -> Option<TokenProvider>;
70}
71
72impl<T> IntoTokenProvider for Option<T>
73where
74 T: IntoTokenProvider,
75{
76 fn into_token_provider(self) -> Option<TokenProvider> {
77 self.and_then(|t| t.into_token_provider())
78 }
79}
80
81impl IntoTokenProvider for TokenGenerator {
82 fn into_token_provider(self) -> Option<TokenProvider> {
83 Some(TokenProvider::Generator(self))
84 }
85}
86
87impl IntoTokenProvider for String {
88 fn into_token_provider(self) -> Option<TokenProvider> {
89 Some(TokenProvider::Static(self))
90 }
91}
92
93impl IntoTokenProvider for &str {
94 fn into_token_provider(self) -> Option<TokenProvider> {
95 Some(TokenProvider::Static(self.to_owned()))
96 }
97}
98
99#[derive(Debug)]
109pub struct TokenGenerator {
110 kid: String,
111 encoding_key: EncodingKey,
112 expiry_seconds: u64,
113 permissions: HashSet<Permission>,
114}
115
116#[derive(Serialize, Deserialize)]
117struct JwtRes {
118 #[serde(rename = "os:usecase")]
119 usecase: String,
120
121 #[serde(flatten)]
122 scopes: BTreeMap<String, String>,
123}
124
125#[derive(Serialize, Deserialize)]
126struct JwtClaims {
127 exp: u64,
128 permissions: HashSet<Permission>,
129 res: JwtRes,
130}
131
132impl TokenGenerator {
133 pub fn new(secret_key: SecretKey) -> crate::Result<TokenGenerator> {
135 let encoding_key = EncodingKey::from_ed_pem(secret_key.secret_key.as_bytes())?;
136 Ok(TokenGenerator {
137 kid: secret_key.kid,
138 encoding_key,
139 expiry_seconds: DEFAULT_EXPIRY_SECONDS,
140 permissions: HashSet::from(DEFAULT_PERMISSIONS),
141 })
142 }
143
144 pub fn expiry_seconds(mut self, expiry_seconds: u64) -> Self {
146 self.expiry_seconds = expiry_seconds;
147 self
148 }
149
150 pub fn permissions(mut self, permissions: &[Permission]) -> Self {
152 self.permissions = HashSet::from_iter(permissions.iter().copied());
153 self
154 }
155
156 #[deprecated(note = "Use `create_token(scope).sign()` instead")]
158 pub fn sign(&self, scope: &Scope) -> crate::Result<String> {
159 self.create_token(scope).sign()
160 }
161
162 pub fn create_token<'a>(&'a self, scope: &'a Scope) -> TokenRequest<'a> {
170 TokenRequest {
171 generator: self,
172 scope: scope.0.as_ref(),
173 permissions: None,
174 expiry_seconds: None,
175 }
176 }
177
178 pub(crate) fn request_inner<'a>(&'a self, scope: &'a ScopeInner) -> TokenRequest<'a> {
179 TokenRequest {
180 generator: self,
181 scope: Ok(scope),
182 permissions: None,
183 expiry_seconds: None,
184 }
185 }
186
187 fn resolve_permissions(
190 &self,
191 requested: Option<&[Permission]>,
192 ) -> crate::Result<HashSet<Permission>> {
193 let Some(requested) = requested else {
194 return Ok(self.permissions.clone());
195 };
196
197 let requested: HashSet<Permission> = requested.iter().copied().collect();
198 let mut escalated: Vec<Permission> =
199 requested.difference(&self.permissions).copied().collect();
200 if !escalated.is_empty() {
201 escalated.sort_by_key(Permission::to_string);
202 return Err(crate::Error::PermissionEscalation { escalated });
203 }
204 Ok(requested)
205 }
206}
207
208#[derive(Debug)]
210pub struct TokenRequest<'a> {
211 generator: &'a TokenGenerator,
212 scope: Result<&'a ScopeInner, &'a crate::Error>,
213 permissions: Option<Vec<Permission>>,
214 expiry_seconds: Option<u64>,
215}
216
217impl TokenRequest<'_> {
218 pub fn permissions(mut self, permissions: &[Permission]) -> Self {
221 self.permissions = Some(permissions.to_vec());
222 self
223 }
224
225 pub fn expiry_seconds(mut self, expiry_seconds: u64) -> Self {
227 self.expiry_seconds = Some(expiry_seconds);
228 self
229 }
230
231 pub fn sign(&self) -> crate::Result<String> {
237 let scope = match self.scope {
238 Ok(inner) => inner,
239 Err(crate::Error::InvalidScope(err)) => return Err(err.clone().into()),
240 _ => return Err(scope::InvalidScopeError::Unreachable.into()),
244 };
245
246 let claims = JwtClaims {
247 exp: get_current_timestamp()
248 + self.expiry_seconds.unwrap_or(self.generator.expiry_seconds),
249 permissions: self
250 .generator
251 .resolve_permissions(self.permissions.as_deref())?,
252 res: JwtRes {
253 usecase: scope.usecase().name().into(),
254 scopes: scope
255 .scopes()
256 .iter()
257 .map(|scope| (scope.name().to_string(), scope.value().to_string()))
258 .collect(),
259 },
260 };
261
262 let mut header = Header::new(Algorithm::EdDSA);
263 header.kid = Some(self.generator.kid.clone());
264
265 Ok(encode(&header, &claims, &self.generator.encoding_key)?)
266 }
267}