Compare commits
2 commits
56f2cd180d
...
64cebc63d7
Author | SHA1 | Date | |
---|---|---|---|
64cebc63d7 | |||
42b72160c7 |
2 changed files with 79 additions and 30 deletions
|
@ -1,8 +1,6 @@
|
|||
use std::borrow::Cow;
|
||||
use std::sync::Arc;
|
||||
|
||||
use argon2::password_hash::PasswordHash;
|
||||
|
||||
use http::header::{HeaderValue, ACCESS_CONTROL_ALLOW_ORIGIN, AUTHORIZATION};
|
||||
use hyper::{body::Incoming as IncomingBody, Request, Response};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
@ -15,10 +13,12 @@ use opentelemetry_prometheus::PrometheusExporter;
|
|||
|
||||
use garage_model::garage::Garage;
|
||||
use garage_rpc::{Endpoint as RpcEndpoint, *};
|
||||
use garage_table::EmptyKey;
|
||||
use garage_util::background::BackgroundRunner;
|
||||
use garage_util::data::Uuid;
|
||||
use garage_util::error::Error as GarageError;
|
||||
use garage_util::socket_address::UnixOrTCPSocketAddress;
|
||||
use garage_util::time::now_msec;
|
||||
|
||||
use garage_api_common::generic_server::*;
|
||||
use garage_api_common::helpers::*;
|
||||
|
@ -168,14 +168,13 @@ impl AdminApiServer {
|
|||
},
|
||||
};
|
||||
|
||||
if let Some(password_hash) = required_auth_hash {
|
||||
match auth_header {
|
||||
None => return Err(Error::forbidden("Authorization token must be provided")),
|
||||
Some(authorization) => {
|
||||
verify_bearer_token(&authorization, password_hash)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
verify_authorization(
|
||||
&self.garage,
|
||||
required_auth_hash,
|
||||
auth_header,
|
||||
request.name(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
match request {
|
||||
AdminApiRequest::Options(req) => req.handle(&self.garage, &self).await,
|
||||
|
@ -249,20 +248,65 @@ fn hash_bearer_token(token: &str) -> String {
|
|||
.to_string()
|
||||
}
|
||||
|
||||
fn verify_bearer_token(token: &hyper::http::HeaderValue, password_hash: &str) -> Result<(), Error> {
|
||||
use argon2::{password_hash::PasswordVerifier, Argon2};
|
||||
async fn verify_authorization(
|
||||
garage: &Garage,
|
||||
required_token_hash: Option<&str>,
|
||||
auth_header: Option<hyper::http::HeaderValue>,
|
||||
endpoint_name: &str,
|
||||
) -> Result<(), Error> {
|
||||
use argon2::{password_hash::PasswordHash, password_hash::PasswordVerifier, Argon2};
|
||||
|
||||
let parsed_hash = PasswordHash::new(&password_hash).unwrap();
|
||||
let invalid_msg = "Invalid bearer token";
|
||||
|
||||
token
|
||||
.to_str()?
|
||||
.strip_prefix("Bearer ")
|
||||
.and_then(|token| {
|
||||
Argon2::default()
|
||||
.verify_password(token.trim().as_bytes(), &parsed_hash)
|
||||
.ok()
|
||||
})
|
||||
.ok_or_else(|| Error::forbidden("Invalid authorization token"))?;
|
||||
if let Some(token_hash_str) = required_token_hash {
|
||||
let token = match &auth_header {
|
||||
None => {
|
||||
return Err(Error::forbidden(
|
||||
"Bearer token must be provided in Authorization header",
|
||||
))
|
||||
}
|
||||
Some(authorization) => authorization
|
||||
.to_str()?
|
||||
.strip_prefix("Bearer ")
|
||||
.ok_or_else(|| Error::forbidden("Invalid Authorization header"))?
|
||||
.trim(),
|
||||
};
|
||||
|
||||
let token_hash_string = if let Some((prefix, _)) = token.split_once('.') {
|
||||
garage
|
||||
.admin_key_table
|
||||
.get(&EmptyKey, &prefix.to_string())
|
||||
.await?
|
||||
.and_then(|k| k.state.into_option())
|
||||
.filter(|p| {
|
||||
p.expiration
|
||||
.get()
|
||||
.map(|exp| now_msec() < exp)
|
||||
.unwrap_or(true)
|
||||
})
|
||||
.filter(|p| {
|
||||
p.scope
|
||||
.get()
|
||||
.0
|
||||
.iter()
|
||||
.any(|x| x == "*" || x == endpoint_name)
|
||||
})
|
||||
.ok_or_else(|| Error::forbidden(invalid_msg))?
|
||||
.token_hash
|
||||
} else {
|
||||
token_hash_str.to_string()
|
||||
};
|
||||
|
||||
let token_hash = PasswordHash::new(&token_hash_string)
|
||||
.ok_or_internal_error("Could not parse token hash")?;
|
||||
|
||||
if Argon2::default()
|
||||
.verify_password(token.as_bytes(), &token_hash)
|
||||
.is_err()
|
||||
{
|
||||
return Err(Error::forbidden(invalid_msg));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
@ -23,7 +23,7 @@ mod v2 {
|
|||
#[derive(PartialEq, Eq, Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct AdminApiKeyParams {
|
||||
/// The entire API key hashed as a password
|
||||
pub hash: String,
|
||||
pub token_hash: String,
|
||||
|
||||
/// User-defined name
|
||||
pub name: crdt::Lww<String>,
|
||||
|
@ -36,7 +36,7 @@ mod v2 {
|
|||
}
|
||||
|
||||
#[derive(PartialEq, Eq, Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct AdminApiKeyScope(pub(crate) Vec<String>);
|
||||
pub struct AdminApiKeyScope(pub Vec<String>);
|
||||
|
||||
impl garage_util::migrate::InitialFormat for AdminApiKey {
|
||||
const VERSION_MARKER: &'static [u8] = b"G2admkey";
|
||||
|
@ -66,7 +66,10 @@ impl Crdt for AdminApiKeyScope {
|
|||
}
|
||||
|
||||
impl AdminApiKey {
|
||||
pub fn new(name: &str) -> Self {
|
||||
/// Create a new admin API access key.
|
||||
/// Returns the AdminApiKey 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,
|
||||
|
@ -78,20 +81,22 @@ impl AdminApiKey {
|
|||
|
||||
let salt = SaltString::generate(&mut OsRng);
|
||||
let argon2 = Argon2::default();
|
||||
let hash = argon2
|
||||
.hash_password(token.trim().as_bytes(), &salt)
|
||||
let hashed_token = argon2
|
||||
.hash_password(token.as_bytes(), &salt)
|
||||
.expect("could not hash admin API token")
|
||||
.to_string();
|
||||
|
||||
AdminApiKey {
|
||||
let ret = AdminApiKey {
|
||||
prefix,
|
||||
state: crdt::Deletable::present(AdminApiKeyParams {
|
||||
hash,
|
||||
token_hash: hashed_token,
|
||||
name: crdt::Lww::new(name.to_string()),
|
||||
expiration: crdt::Lww::new(None),
|
||||
scope: crdt::Lww::new(AdminApiKeyScope(vec!["*".to_string()])),
|
||||
}),
|
||||
}
|
||||
};
|
||||
|
||||
(ret, token)
|
||||
}
|
||||
|
||||
pub fn delete(prefix: String) -> Self {
|
||||
|
|
Loading…
Add table
Reference in a new issue