use std::collections::{BTreeMap, HashSet};
use jsonwebtoken::{Algorithm, EncodingKey, Header, encode, get_current_timestamp};
use objectstore_types::scope;
use serde::{Deserialize, Serialize};
use crate::{Scope, ScopeInner};
pub use objectstore_types::auth::Permission;
const DEFAULT_EXPIRY_SECONDS: u64 = 60;
const DEFAULT_PERMISSIONS: [Permission; 3] = [
Permission::ObjectRead,
Permission::ObjectWrite,
Permission::ObjectDelete,
];
pub struct SecretKey {
pub kid: String,
pub secret_key: String,
}
impl std::fmt::Debug for SecretKey {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("SecretKey")
.field("kid", &self.kid)
.field("secret_key", &"[redacted]")
.finish()
}
}
pub enum TokenProvider {
Static(String),
Generator(TokenGenerator),
}
impl std::fmt::Debug for TokenProvider {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
TokenProvider::Static(_) => f.write_str("TokenProvider::Static([redacted])"),
TokenProvider::Generator(g) => {
f.debug_tuple("TokenProvider::Generator").field(g).finish()
}
}
}
}
pub trait IntoTokenProvider {
fn into_token_provider(self) -> Option<TokenProvider>;
}
impl<T> IntoTokenProvider for Option<T>
where
T: IntoTokenProvider,
{
fn into_token_provider(self) -> Option<TokenProvider> {
self.and_then(|t| t.into_token_provider())
}
}
impl IntoTokenProvider for TokenGenerator {
fn into_token_provider(self) -> Option<TokenProvider> {
Some(TokenProvider::Generator(self))
}
}
impl IntoTokenProvider for String {
fn into_token_provider(self) -> Option<TokenProvider> {
Some(TokenProvider::Static(self))
}
}
impl IntoTokenProvider for &str {
fn into_token_provider(self) -> Option<TokenProvider> {
Some(TokenProvider::Static(self.to_owned()))
}
}
#[derive(Debug)]
pub struct TokenGenerator {
kid: String,
encoding_key: EncodingKey,
expiry_seconds: u64,
permissions: HashSet<Permission>,
}
#[derive(Serialize, Deserialize)]
struct JwtRes {
#[serde(rename = "os:usecase")]
usecase: String,
#[serde(flatten)]
scopes: BTreeMap<String, String>,
}
#[derive(Serialize, Deserialize)]
struct JwtClaims {
exp: u64,
permissions: HashSet<Permission>,
res: JwtRes,
}
impl TokenGenerator {
pub fn new(secret_key: SecretKey) -> crate::Result<TokenGenerator> {
let encoding_key = EncodingKey::from_ed_pem(secret_key.secret_key.as_bytes())?;
Ok(TokenGenerator {
kid: secret_key.kid,
encoding_key,
expiry_seconds: DEFAULT_EXPIRY_SECONDS,
permissions: HashSet::from(DEFAULT_PERMISSIONS),
})
}
pub fn expiry_seconds(mut self, expiry_seconds: u64) -> Self {
self.expiry_seconds = expiry_seconds;
self
}
pub fn permissions(mut self, permissions: &[Permission]) -> Self {
self.permissions = HashSet::from_iter(permissions.iter().copied());
self
}
#[deprecated(note = "Use `create_token(scope).sign()` instead")]
pub fn sign(&self, scope: &Scope) -> crate::Result<String> {
self.create_token(scope).sign()
}
pub fn create_token<'a>(&'a self, scope: &'a Scope) -> TokenRequest<'a> {
TokenRequest {
generator: self,
scope: scope.0.as_ref(),
permissions: None,
expiry_seconds: None,
}
}
pub(crate) fn request_inner<'a>(&'a self, scope: &'a ScopeInner) -> TokenRequest<'a> {
TokenRequest {
generator: self,
scope: Ok(scope),
permissions: None,
expiry_seconds: None,
}
}
fn resolve_permissions(
&self,
requested: Option<&[Permission]>,
) -> crate::Result<HashSet<Permission>> {
let Some(requested) = requested else {
return Ok(self.permissions.clone());
};
let requested: HashSet<Permission> = requested.iter().copied().collect();
let mut escalated: Vec<Permission> =
requested.difference(&self.permissions).copied().collect();
if !escalated.is_empty() {
escalated.sort_by_key(Permission::to_string);
return Err(crate::Error::PermissionEscalation { escalated });
}
Ok(requested)
}
}
#[derive(Debug)]
pub struct TokenRequest<'a> {
generator: &'a TokenGenerator,
scope: Result<&'a ScopeInner, &'a crate::Error>,
permissions: Option<Vec<Permission>>,
expiry_seconds: Option<u64>,
}
impl TokenRequest<'_> {
pub fn permissions(mut self, permissions: &[Permission]) -> Self {
self.permissions = Some(permissions.to_vec());
self
}
pub fn expiry_seconds(mut self, expiry_seconds: u64) -> Self {
self.expiry_seconds = Some(expiry_seconds);
self
}
pub fn sign(&self) -> crate::Result<String> {
let scope = match self.scope {
Ok(inner) => inner,
Err(crate::Error::InvalidScope(err)) => return Err(err.clone().into()),
_ => return Err(scope::InvalidScopeError::Unreachable.into()),
};
let claims = JwtClaims {
exp: get_current_timestamp()
+ self.expiry_seconds.unwrap_or(self.generator.expiry_seconds),
permissions: self
.generator
.resolve_permissions(self.permissions.as_deref())?,
res: JwtRes {
usecase: scope.usecase().name().into(),
scopes: scope
.scopes()
.iter()
.map(|scope| (scope.name().to_string(), scope.value().to_string()))
.collect(),
},
};
let mut header = Header::new(Algorithm::EdDSA);
header.kid = Some(self.generator.kid.clone());
Ok(encode(&header, &claims, &self.generator.encoding_key)?)
}
}