add model for admin key table

This commit is contained in:
Alex Auvolat 2025-03-11 13:09:19 +01:00
parent 576d0d950e
commit 46f620119b
6 changed files with 184 additions and 3 deletions

1
Cargo.lock generated
View file

@ -1467,6 +1467,7 @@ dependencies = [
name = "garage_model"
version = "1.1.0"
dependencies = [
"argon2",
"async-trait",
"base64 0.21.7",
"blake2",

View file

@ -243,9 +243,7 @@ impl AdminApiRequest {
/// Get the kind of authorization which is required to perform the operation.
pub fn authorization_type(&self) -> Authorization {
match self {
Self::Options(_) => Authorization::None,
Self::Health(_) => Authorization::None,
Self::CheckDomain(_) => Authorization::None,
Self::Options(_) | Self::Health(_) | Self::CheckDomain(_) => Authorization::None,
Self::Metrics(_) => Authorization::MetricsToken,
_ => Authorization::AdminToken,
}

View file

@ -21,6 +21,7 @@ garage_block.workspace = true
garage_util.workspace = true
garage_net.workspace = true
argon2.workspace = true
async-trait.workspace = true
blake2.workspace = true
chrono.workspace = true

View file

@ -0,0 +1,167 @@
use garage_util::crdt::{self, Crdt};
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 {
/// An admin API token is a bearer token of the following form:
/// `<prefix>.<suffix>`
/// Only the prefix is saved here, it is used as an identifier.
/// The entire API token is hashed and saved in `token_hash` in `state`.
pub prefix: String,
/// If the token is not deleted, its parameters
pub state: crdt::Deletable<AdminApiTokenParams>,
}
#[derive(PartialEq, Eq, Clone, Debug, Serialize, Deserialize)]
pub struct AdminApiTokenParams {
/// The entire API token hashed as a password
pub token_hash: String,
/// User-defined name
pub name: crdt::Lww<String>,
/// The optional time of expiration of the token
pub expiration: crdt::Lww<Option<u64>>,
/// The scope of the token, i.e. list of authorized admin API calls
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 {
/// Create a new admin API token.
/// Returns the AdminApiToken object, which contains the hashed bearer token,
/// as well as the plaintext bearer token.
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 = hex::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 {
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,
}
}
/// Returns true if this represents a deleted bucket
pub fn is_deleted(&self) -> bool {
self.state.is_deleted()
}
/// Returns an option representing the params (None if in deleted state)
pub fn params(&self) -> Option<&AdminApiTokenParams> {
self.state.as_option()
}
/// Mutable version of `.state()`
pub fn params_mut(&mut self) -> Option<&mut AdminApiTokenParams> {
self.state.as_option_mut()
}
/// Scope, if not deleted, or empty slice
pub fn scope(&self) -> &[String] {
self.state
.as_option()
.map(|x| &x.scope.get().0[..])
.unwrap_or_default()
}
}
impl Entry<EmptyKey, String> for AdminApiToken {
fn partition_key(&self) -> &EmptyKey {
&EmptyKey
}
fn sort_key(&self) -> &String {
&self.prefix
}
}
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)
}
}
}
}

View file

@ -24,6 +24,7 @@ use crate::s3::mpu_table::*;
use crate::s3::object_table::*;
use crate::s3::version_table::*;
use crate::admin_token_table::*;
use crate::bucket_alias_table::*;
use crate::bucket_table::*;
use crate::helper;
@ -50,6 +51,8 @@ pub struct Garage {
/// The block manager
pub block_manager: Arc<BlockManager>,
/// Table containing admin API keys
pub admin_token_table: Arc<Table<AdminApiTokenTable, TableFullReplication>>,
/// Table containing buckets
pub bucket_table: Arc<Table<BucketTable, TableFullReplication>>,
/// Table containing bucket aliases
@ -174,6 +177,14 @@ impl Garage {
block_manager.register_bg_vars(&mut bg_vars);
// ---- admin tables ----
info!("Initialize admin_token_table...");
let admin_token_table = Table::new(
AdminApiTokenTable,
control_rep_param.clone(),
system.clone(),
&db,
);
info!("Initialize bucket_table...");
let bucket_table = Table::new(BucketTable, control_rep_param.clone(), system.clone(), &db);
@ -263,6 +274,7 @@ impl Garage {
db,
system,
block_manager,
admin_token_table,
bucket_table,
bucket_alias_table,
key_table,
@ -282,6 +294,7 @@ impl Garage {
pub fn spawn_workers(self: &Arc<Self>, bg: &BackgroundRunner) -> Result<(), Error> {
self.block_manager.spawn_workers(bg);
self.admin_token_table.spawn_workers(bg);
self.bucket_table.spawn_workers(bg);
self.bucket_alias_table.spawn_workers(bg);
self.key_table.spawn_workers(bg);

View file

@ -5,6 +5,7 @@ pub mod permission;
pub mod index_counter;
pub mod admin_token_table;
pub mod bucket_alias_table;
pub mod bucket_table;
pub mod key_table;