use base64::prelude::*;
use garage_util::crdt::{self, Crdt};
use garage_util::time::now_msec;
use garage_table::{EmptyKey, Entry, TableSchema};
pub use crate::key_table::KeyFilter;
mod v2 {
use garage_util::crdt;
use serde::{Deserialize, Serialize};
#[derive(PartialEq, Eq, Clone, Debug, Serialize, Deserialize)]
pub struct AdminApiToken {
pub prefix: String,
pub state: crdt::Deletable<AdminApiTokenParams>,
}
#[derive(PartialEq, Eq, Clone, Debug, Serialize, Deserialize)]
pub struct AdminApiTokenParams {
pub created: u64,
pub token_hash: String,
pub name: crdt::Lww<String>,
pub expiration: crdt::Lww<Option<u64>>,
pub scope: crdt::Lww<AdminApiTokenScope>,
}
#[derive(PartialEq, Eq, Clone, Debug, Serialize, Deserialize)]
pub struct AdminApiTokenScope(pub Vec<String>);
impl garage_util::migrate::InitialFormat for AdminApiToken {
const VERSION_MARKER: &'static [u8] = b"G2admtok";
}
}
pub use v2::*;
impl Crdt for AdminApiTokenParams {
fn merge(&mut self, o: &Self) {
self.name.merge(&o.name);
self.expiration.merge(&o.expiration);
self.scope.merge(&o.scope);
}
}
impl Crdt for AdminApiToken {
fn merge(&mut self, other: &Self) {
self.state.merge(&other.state);
}
}
impl Crdt for AdminApiTokenScope {
fn merge(&mut self, other: &Self) {
self.0.retain(|x| other.0.contains(x));
}
}
impl AdminApiToken {
pub fn new(name: &str) -> (Self, String) {
use argon2::{
password_hash::{rand_core::OsRng, PasswordHasher, SaltString},
Argon2,
};
let prefix = hex::encode(&rand::random::<[u8; 12]>()[..]);
let secret = BASE64_URL_SAFE_NO_PAD.encode(&rand::random::<[u8; 32]>()[..]);
let token = format!("{}.{}", prefix, secret);
let salt = SaltString::generate(&mut OsRng);
let argon2 = Argon2::default();
let hashed_token = argon2
.hash_password(token.as_bytes(), &salt)
.expect("could not hash admin API token")
.to_string();
let ret = AdminApiToken {
prefix,
state: crdt::Deletable::present(AdminApiTokenParams {
created: now_msec(),
token_hash: hashed_token,
name: crdt::Lww::new(name.to_string()),
expiration: crdt::Lww::new(None),
scope: crdt::Lww::new(AdminApiTokenScope(vec!["*".to_string()])),
}),
};
(ret, token)
}
pub fn delete(prefix: String) -> Self {
Self {
prefix,
state: crdt::Deletable::Deleted,
}
}
pub fn is_deleted(&self) -> bool {
self.state.is_deleted()
}
pub fn params(&self) -> Option<&AdminApiTokenParams> {
self.state.as_option()
}
pub fn params_mut(&mut self) -> Option<&mut AdminApiTokenParams> {
self.state.as_option_mut()
}
pub fn scope(&self) -> &[String] {
self.state
.as_option()
.map(|x| &x.scope.get().0[..])
.unwrap_or_default()
}
}
impl AdminApiTokenParams {
pub fn is_expired(&self, ts_now: u64) -> bool {
match *self.expiration.get() {
None => false,
Some(exp) => ts_now >= exp,
}
}
pub fn has_scope(&self, endpoint: &str) -> bool {
self.scope.get().0.iter().any(|x| x == "*" || x == endpoint)
}
}
impl Entry<EmptyKey, String> for AdminApiToken {
fn partition_key(&self) -> &EmptyKey {
&EmptyKey
}
fn sort_key(&self) -> &String {
&self.prefix
}
fn is_tombstone(&self) -> bool {
self.is_deleted()
}
}
pub struct AdminApiTokenTable;
impl TableSchema for AdminApiTokenTable {
const TABLE_NAME: &'static str = "admin_token";
type P = EmptyKey;
type S = String;
type E = AdminApiToken;
type Filter = KeyFilter;
fn matches_filter(entry: &Self::E, filter: &Self::Filter) -> bool {
match filter {
KeyFilter::Deleted(df) => df.apply(entry.state.is_deleted()),
KeyFilter::MatchesAndNotDeleted(pat) => {
let pat = pat.to_lowercase();
entry
.params()
.map(|p| {
entry.prefix.to_lowercase().starts_with(&pat)
|| p.name.get().to_lowercase() == pat
})
.unwrap_or(false)
}
}
}
}