diff --git a/Cargo.lock b/Cargo.lock index 20820f7d..37e22f21 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1467,6 +1467,7 @@ dependencies = [ name = "garage_model" version = "1.1.0" dependencies = [ + "argon2", "async-trait", "base64 0.21.7", "blake2", diff --git a/src/api/admin/router_v2.rs b/src/api/admin/router_v2.rs index 9f6106e5..133f9c29 100644 --- a/src/api/admin/router_v2.rs +++ b/src/api/admin/router_v2.rs @@ -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, } diff --git a/src/model/Cargo.toml b/src/model/Cargo.toml index 42ec8537..a990a191 100644 --- a/src/model/Cargo.toml +++ b/src/model/Cargo.toml @@ -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 diff --git a/src/model/admin_token_table.rs b/src/model/admin_token_table.rs new file mode 100644 index 00000000..089c72e2 --- /dev/null +++ b/src/model/admin_token_table.rs @@ -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: + /// `.` + /// 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, + } + + #[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, + + /// The optional time of expiration of the token + pub expiration: crdt::Lww>, + + /// The scope of the token, i.e. list of authorized admin API calls + pub scope: crdt::Lww, + } + + #[derive(PartialEq, Eq, Clone, Debug, Serialize, Deserialize)] + pub struct AdminApiTokenScope(pub Vec); + + 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 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) + } + } + } +} diff --git a/src/model/garage.rs b/src/model/garage.rs index 11c0d90f..95f7b577 100644 --- a/src/model/garage.rs +++ b/src/model/garage.rs @@ -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, + /// Table containing admin API keys + pub admin_token_table: Arc>, /// Table containing buckets pub bucket_table: Arc>, /// 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, 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); diff --git a/src/model/lib.rs b/src/model/lib.rs index 1939a7a9..b4dc1e81 100644 --- a/src/model/lib.rs +++ b/src/model/lib.rs @@ -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;