aboutsummaryrefslogtreecommitdiff
path: root/src/api/admin/api_server.rs
diff options
context:
space:
mode:
Diffstat (limited to 'src/api/admin/api_server.rs')
-rw-r--r--src/api/admin/api_server.rs237
1 files changed, 42 insertions, 195 deletions
diff --git a/src/api/admin/api_server.rs b/src/api/admin/api_server.rs
index 0e4565bb..d66714db 100644
--- a/src/api/admin/api_server.rs
+++ b/src/api/admin/api_server.rs
@@ -1,10 +1,10 @@
-use std::collections::HashMap;
+use std::borrow::Cow;
use std::sync::Arc;
use argon2::password_hash::PasswordHash;
use async_trait::async_trait;
-use http::header::{ACCESS_CONTROL_ALLOW_METHODS, ACCESS_CONTROL_ALLOW_ORIGIN, ALLOW};
+use http::header::{HeaderValue, ACCESS_CONTROL_ALLOW_ORIGIN, AUTHORIZATION};
use hyper::{body::Incoming as IncomingBody, Request, Response, StatusCode};
use tokio::sync::watch;
@@ -16,18 +16,17 @@ use opentelemetry_prometheus::PrometheusExporter;
use prometheus::{Encoder, TextEncoder};
use garage_model::garage::Garage;
-use garage_rpc::system::ClusterHealthStatus;
use garage_util::error::Error as GarageError;
use garage_util::socket_address::UnixOrTCPSocketAddress;
use crate::generic_server::*;
-use crate::admin::bucket::*;
-use crate::admin::cluster::*;
+use crate::admin::api::*;
use crate::admin::error::*;
-use crate::admin::key::*;
use crate::admin::router_v0;
-use crate::admin::router_v1::{Authorization, Endpoint};
+use crate::admin::router_v1;
+use crate::admin::Authorization;
+use crate::admin::EndpointHandler;
use crate::helpers::*;
pub type ResBody = BoxBody<Error>;
@@ -40,6 +39,11 @@ pub struct AdminApiServer {
admin_token: Option<String>,
}
+pub enum Endpoint {
+ Old(router_v1::Endpoint),
+ New(String),
+}
+
impl AdminApiServer {
pub fn new(
garage: Arc<Garage>,
@@ -68,130 +72,6 @@ impl AdminApiServer {
.await
}
- fn handle_options(&self, _req: &Request<IncomingBody>) -> Result<Response<ResBody>, Error> {
- Ok(Response::builder()
- .status(StatusCode::NO_CONTENT)
- .header(ALLOW, "OPTIONS, GET, POST")
- .header(ACCESS_CONTROL_ALLOW_METHODS, "OPTIONS, GET, POST")
- .header(ACCESS_CONTROL_ALLOW_ORIGIN, "*")
- .body(empty_body())?)
- }
-
- async fn handle_check_domain(
- &self,
- req: Request<IncomingBody>,
- ) -> Result<Response<ResBody>, Error> {
- let query_params: HashMap<String, String> = req
- .uri()
- .query()
- .map(|v| {
- url::form_urlencoded::parse(v.as_bytes())
- .into_owned()
- .collect()
- })
- .unwrap_or_else(HashMap::new);
-
- let has_domain_key = query_params.contains_key("domain");
-
- if !has_domain_key {
- return Err(Error::bad_request("No domain query string found"));
- }
-
- let domain = query_params
- .get("domain")
- .ok_or_internal_error("Could not parse domain query string")?;
-
- if self.check_domain(domain).await? {
- Ok(Response::builder()
- .status(StatusCode::OK)
- .body(string_body(format!(
- "Domain '{domain}' is managed by Garage"
- )))?)
- } else {
- Err(Error::bad_request(format!(
- "Domain '{domain}' is not managed by Garage"
- )))
- }
- }
-
- async fn check_domain(&self, domain: &str) -> Result<bool, Error> {
- // Resolve bucket from domain name, inferring if the website must be activated for the
- // domain to be valid.
- let (bucket_name, must_check_website) = if let Some(bname) = self
- .garage
- .config
- .s3_api
- .root_domain
- .as_ref()
- .and_then(|rd| host_to_bucket(domain, rd))
- {
- (bname.to_string(), false)
- } else if let Some(bname) = self
- .garage
- .config
- .s3_web
- .as_ref()
- .and_then(|sw| host_to_bucket(domain, sw.root_domain.as_str()))
- {
- (bname.to_string(), true)
- } else {
- (domain.to_string(), true)
- };
-
- let bucket_id = match self
- .garage
- .bucket_helper()
- .resolve_global_bucket_name(&bucket_name)
- .await?
- {
- Some(bucket_id) => bucket_id,
- None => return Ok(false),
- };
-
- if !must_check_website {
- return Ok(true);
- }
-
- let bucket = self
- .garage
- .bucket_helper()
- .get_existing_bucket(bucket_id)
- .await?;
-
- let bucket_state = bucket.state.as_option().unwrap();
- let bucket_website_config = bucket_state.website_config.get();
-
- match bucket_website_config {
- Some(_v) => Ok(true),
- None => Ok(false),
- }
- }
-
- fn handle_health(&self) -> Result<Response<ResBody>, Error> {
- let health = self.garage.system.health();
-
- let (status, status_str) = match health.status {
- ClusterHealthStatus::Healthy => (StatusCode::OK, "Garage is fully operational"),
- ClusterHealthStatus::Degraded => (
- StatusCode::OK,
- "Garage is operational but some storage nodes are unavailable",
- ),
- ClusterHealthStatus::Unavailable => (
- StatusCode::SERVICE_UNAVAILABLE,
- "Quorum is not available for some/all partitions, reads and writes will fail",
- ),
- };
- let status_str = format!(
- "{}\nConsult the full health check API endpoint at /v1/health for more details\n",
- status_str
- );
-
- Ok(Response::builder()
- .status(status)
- .header(http::header::CONTENT_TYPE, "text/plain")
- .body(string_body(status_str))?)
- }
-
fn handle_metrics(&self) -> Result<Response<ResBody>, Error> {
#[cfg(feature = "metrics")]
{
@@ -232,9 +112,13 @@ impl ApiHandler for AdminApiServer {
fn parse_endpoint(&self, req: &Request<IncomingBody>) -> Result<Endpoint, Error> {
if req.uri().path().starts_with("/v0/") {
let endpoint_v0 = router_v0::Endpoint::from_request(req)?;
- Endpoint::from_v0(endpoint_v0)
+ let endpoint_v1 = router_v1::Endpoint::from_v0(endpoint_v0)?;
+ Ok(Endpoint::Old(endpoint_v1))
+ } else if req.uri().path().starts_with("/v1/") {
+ let endpoint_v1 = router_v1::Endpoint::from_request(req)?;
+ Ok(Endpoint::Old(endpoint_v1))
} else {
- Endpoint::from_request(req)
+ Ok(Endpoint::New(req.uri().path().to_string()))
}
}
@@ -243,8 +127,15 @@ impl ApiHandler for AdminApiServer {
req: Request<IncomingBody>,
endpoint: Endpoint,
) -> Result<Response<ResBody>, Error> {
+ let auth_header = req.headers().get(AUTHORIZATION).cloned();
+
+ let request = match endpoint {
+ Endpoint::Old(endpoint_v1) => AdminApiRequest::from_v1(endpoint_v1, req).await?,
+ Endpoint::New(_) => AdminApiRequest::from_request(req).await?,
+ };
+
let required_auth_hash =
- match endpoint.authorization_type() {
+ match request.authorization_type() {
Authorization::None => None,
Authorization::MetricsToken => self.metrics_token.as_deref(),
Authorization::AdminToken => match self.admin_token.as_deref() {
@@ -256,7 +147,7 @@ impl ApiHandler for AdminApiServer {
};
if let Some(password_hash) = required_auth_hash {
- match req.headers().get("Authorization") {
+ match auth_header {
None => return Err(Error::forbidden("Authorization token must be provided")),
Some(authorization) => {
verify_bearer_token(&authorization, password_hash)?;
@@ -264,72 +155,28 @@ impl ApiHandler for AdminApiServer {
}
}
- match endpoint {
- Endpoint::Options => self.handle_options(&req),
- Endpoint::CheckDomain => self.handle_check_domain(req).await,
- Endpoint::Health => self.handle_health(),
- Endpoint::Metrics => self.handle_metrics(),
- Endpoint::GetClusterStatus => handle_get_cluster_status(&self.garage).await,
- Endpoint::GetClusterHealth => handle_get_cluster_health(&self.garage).await,
- Endpoint::ConnectClusterNodes => handle_connect_cluster_nodes(&self.garage, req).await,
- // Layout
- Endpoint::GetClusterLayout => handle_get_cluster_layout(&self.garage).await,
- Endpoint::UpdateClusterLayout => handle_update_cluster_layout(&self.garage, req).await,
- Endpoint::ApplyClusterLayout => handle_apply_cluster_layout(&self.garage, req).await,
- Endpoint::RevertClusterLayout => handle_revert_cluster_layout(&self.garage).await,
- // Keys
- Endpoint::ListKeys => handle_list_keys(&self.garage).await,
- Endpoint::GetKeyInfo {
- id,
- search,
- show_secret_key,
- } => {
- let show_secret_key = show_secret_key.map(|x| x == "true").unwrap_or(false);
- handle_get_key_info(&self.garage, id, search, show_secret_key).await
- }
- Endpoint::CreateKey => handle_create_key(&self.garage, req).await,
- Endpoint::ImportKey => handle_import_key(&self.garage, req).await,
- Endpoint::UpdateKey { id } => handle_update_key(&self.garage, id, req).await,
- Endpoint::DeleteKey { id } => handle_delete_key(&self.garage, id).await,
- // Buckets
- Endpoint::ListBuckets => handle_list_buckets(&self.garage).await,
- Endpoint::GetBucketInfo { id, global_alias } => {
- handle_get_bucket_info(&self.garage, id, global_alias).await
+ match request {
+ AdminApiRequest::Options(req) => req.handle(&self.garage).await,
+ AdminApiRequest::CheckDomain(req) => req.handle(&self.garage).await,
+ AdminApiRequest::Health(req) => req.handle(&self.garage).await,
+ AdminApiRequest::Metrics(_req) => self.handle_metrics(),
+ req => {
+ let res = req.handle(&self.garage).await?;
+ let mut res = json_ok_response(&res)?;
+ res.headers_mut()
+ .insert(ACCESS_CONTROL_ALLOW_ORIGIN, HeaderValue::from_static("*"));
+ Ok(res)
}
- Endpoint::CreateBucket => handle_create_bucket(&self.garage, req).await,
- Endpoint::DeleteBucket { id } => handle_delete_bucket(&self.garage, id).await,
- Endpoint::UpdateBucket { id } => handle_update_bucket(&self.garage, id, req).await,
- // Bucket-key permissions
- Endpoint::BucketAllowKey => {
- handle_bucket_change_key_perm(&self.garage, req, true).await
- }
- Endpoint::BucketDenyKey => {
- handle_bucket_change_key_perm(&self.garage, req, false).await
- }
- // Bucket aliasing
- Endpoint::GlobalAliasBucket { id, alias } => {
- handle_global_alias_bucket(&self.garage, id, alias).await
- }
- Endpoint::GlobalUnaliasBucket { id, alias } => {
- handle_global_unalias_bucket(&self.garage, id, alias).await
- }
- Endpoint::LocalAliasBucket {
- id,
- access_key_id,
- alias,
- } => handle_local_alias_bucket(&self.garage, id, access_key_id, alias).await,
- Endpoint::LocalUnaliasBucket {
- id,
- access_key_id,
- alias,
- } => handle_local_unalias_bucket(&self.garage, id, access_key_id, alias).await,
}
}
}
impl ApiEndpoint for Endpoint {
- fn name(&self) -> &'static str {
- Endpoint::name(self)
+ fn name(&self) -> Cow<'static, str> {
+ match self {
+ Self::Old(endpoint_v1) => Cow::Borrowed(endpoint_v1.name()),
+ Self::New(path) => Cow::Owned(path.clone()),
+ }
}
fn add_span_attributes(&self, _span: SpanRef<'_>) {}