Merge pull request 'Implemented website hosting authorization endpoint.' (#474) from jpds/garage:bucket-serving-validator into main

Reviewed-on: Deuxfleurs/garage#474
This commit is contained in:
Alex 2023-01-19 12:33:16 +00:00
commit aff9c264c8
4 changed files with 189 additions and 0 deletions

View file

@ -77,6 +77,53 @@ impl AdminApiServer {
.body(Body::empty())?) .body(Body::empty())?)
} }
async fn handle_check_website_enabled(
&self,
req: Request<Body>,
) -> Result<Response<Body>, Error> {
let has_domain_header = req.headers().contains_key("domain");
if !has_domain_header {
return Err(Error::bad_request("No domain header found"));
}
let domain = &req
.headers()
.get("domain")
.ok_or_internal_error("Could not parse domain header")?;
let domain_string = String::from(
domain
.to_str()
.ok_or_bad_request("Invalid characters found in domain header")?,
);
let bucket_id = self
.garage
.bucket_helper()
.resolve_global_bucket_name(&domain_string)
.await?
.ok_or_else(|| HelperError::NoSuchBucket(domain_string))?;
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(Response::builder()
.status(StatusCode::OK)
.body(Body::from("Bucket authorized for website hosting"))?),
None => Err(Error::bad_request(
"Bucket is not authorized for website hosting",
)),
}
}
fn handle_health(&self) -> Result<Response<Body>, Error> { fn handle_health(&self) -> Result<Response<Body>, Error> {
let health = self.garage.system.health(); let health = self.garage.system.health();
@ -174,6 +221,7 @@ impl ApiHandler for AdminApiServer {
match endpoint { match endpoint {
Endpoint::Options => self.handle_options(&req), Endpoint::Options => self.handle_options(&req),
Endpoint::CheckWebsiteEnabled => self.handle_check_website_enabled(req).await,
Endpoint::Health => self.handle_health(), Endpoint::Health => self.handle_health(),
Endpoint::Metrics => self.handle_metrics(), Endpoint::Metrics => self.handle_metrics(),
Endpoint::GetClusterStatus => handle_get_cluster_status(&self.garage).await, Endpoint::GetClusterStatus => handle_get_cluster_status(&self.garage).await,

View file

@ -17,6 +17,7 @@ router_match! {@func
#[derive(Debug, Clone, PartialEq, Eq)] #[derive(Debug, Clone, PartialEq, Eq)]
pub enum Endpoint { pub enum Endpoint {
Options, Options,
CheckWebsiteEnabled,
Health, Health,
Metrics, Metrics,
GetClusterStatus, GetClusterStatus,
@ -91,6 +92,7 @@ impl Endpoint {
let res = router_match!(@gen_path_parser (req.method(), path, query) [ let res = router_match!(@gen_path_parser (req.method(), path, query) [
OPTIONS _ => Options, OPTIONS _ => Options,
GET "/check" => CheckWebsiteEnabled,
GET "/health" => Health, GET "/health" => Health,
GET "/metrics" => Metrics, GET "/metrics" => Metrics,
GET "/v0/status" => GetClusterStatus, GET "/v0/status" => GetClusterStatus,
@ -136,6 +138,7 @@ impl Endpoint {
pub fn authorization_type(&self) -> Authorization { pub fn authorization_type(&self) -> Authorization {
match self { match self {
Self::Health => Authorization::None, Self::Health => Authorization::None,
Self::CheckWebsiteEnabled => Authorization::None,
Self::Metrics => Authorization::MetricsToken, Self::Metrics => Authorization::MetricsToken,
_ => Authorization::AdminToken, _ => Authorization::AdminToken,
} }

View file

@ -25,6 +25,7 @@ pub struct Instance {
pub s3_port: u16, pub s3_port: u16,
pub k2v_port: u16, pub k2v_port: u16,
pub web_port: u16, pub web_port: u16,
pub admin_port: u16,
} }
impl Instance { impl Instance {
@ -105,6 +106,7 @@ api_bind_addr = "127.0.0.1:{admin_port}"
s3_port: port, s3_port: port,
k2v_port: port + 1, k2v_port: port + 1,
web_port: port + 3, web_port: port + 3,
admin_port: port + 4,
} }
} }

View file

@ -1,5 +1,8 @@
use crate::common; use crate::common;
use crate::common::ext::*; use crate::common::ext::*;
use crate::k2v::json_body;
use assert_json_diff::assert_json_eq;
use aws_sdk_s3::{ use aws_sdk_s3::{
model::{CorsConfiguration, CorsRule, ErrorDocument, IndexDocument, WebsiteConfiguration}, model::{CorsConfiguration, CorsRule, ErrorDocument, IndexDocument, WebsiteConfiguration},
types::ByteStream, types::ByteStream,
@ -9,6 +12,7 @@ use hyper::{
body::{to_bytes, Body}, body::{to_bytes, Body},
Client, Client,
}; };
use serde_json::json;
const BODY: &[u8; 16] = b"<h1>bonjour</h1>"; const BODY: &[u8; 16] = b"<h1>bonjour</h1>";
const BODY_ERR: &[u8; 6] = b"erreur"; const BODY_ERR: &[u8; 6] = b"erreur";
@ -49,6 +53,28 @@ async fn test_website() {
BODY.as_ref() BODY.as_ref()
); /* check that we do not leak body */ ); /* check that we do not leak body */
let admin_req = || {
Request::builder()
.method("GET")
.uri(format!("http://127.0.0.1:{}/check", ctx.garage.admin_port))
.header("domain", format!("{}", BCKT_NAME))
.body(Body::empty())
.unwrap()
};
let admin_resp = client.request(admin_req()).await.unwrap();
assert_eq!(admin_resp.status(), StatusCode::BAD_REQUEST);
let res_body = json_body(admin_resp).await;
assert_json_eq!(
res_body,
json!({
"code": "InvalidRequest",
"message": "Bad request: Bucket is not authorized for website hosting",
"region": "garage-integ-test",
"path": "/check",
})
);
ctx.garage ctx.garage
.command() .command()
.args(["bucket", "website", "--allow", BCKT_NAME]) .args(["bucket", "website", "--allow", BCKT_NAME])
@ -62,6 +88,22 @@ async fn test_website() {
BODY.as_ref() BODY.as_ref()
); );
let admin_req = || {
Request::builder()
.method("GET")
.uri(format!("http://127.0.0.1:{}/check", ctx.garage.admin_port))
.header("domain", format!("{}", BCKT_NAME))
.body(Body::empty())
.unwrap()
};
let mut admin_resp = client.request(admin_req()).await.unwrap();
assert_eq!(admin_resp.status(), StatusCode::OK);
assert_eq!(
to_bytes(admin_resp.body_mut()).await.unwrap().as_ref(),
b"Bucket authorized for website hosting"
);
ctx.garage ctx.garage
.command() .command()
.args(["bucket", "website", "--deny", BCKT_NAME]) .args(["bucket", "website", "--deny", BCKT_NAME])
@ -74,6 +116,28 @@ async fn test_website() {
to_bytes(resp.body_mut()).await.unwrap().as_ref(), to_bytes(resp.body_mut()).await.unwrap().as_ref(),
BODY.as_ref() BODY.as_ref()
); /* check that we do not leak body */ ); /* check that we do not leak body */
let admin_req = || {
Request::builder()
.method("GET")
.uri(format!("http://127.0.0.1:{}/check", ctx.garage.admin_port))
.header("domain", format!("{}", BCKT_NAME))
.body(Body::empty())
.unwrap()
};
let admin_resp = client.request(admin_req()).await.unwrap();
assert_eq!(admin_resp.status(), StatusCode::BAD_REQUEST);
let res_body = json_body(admin_resp).await;
assert_json_eq!(
res_body,
json!({
"code": "InvalidRequest",
"message": "Bad request: Bucket is not authorized for website hosting",
"region": "garage-integ-test",
"path": "/check",
})
);
} }
#[tokio::test] #[tokio::test]
@ -322,3 +386,75 @@ async fn test_website_s3_api() {
); );
} }
} }
#[tokio::test]
async fn test_website_check_website_enabled() {
let ctx = common::context();
let client = Client::new();
let admin_req = || {
Request::builder()
.method("GET")
.uri(format!("http://127.0.0.1:{}/check", ctx.garage.admin_port))
.body(Body::empty())
.unwrap()
};
let admin_resp = client.request(admin_req()).await.unwrap();
assert_eq!(admin_resp.status(), StatusCode::BAD_REQUEST);
let res_body = json_body(admin_resp).await;
assert_json_eq!(
res_body,
json!({
"code": "InvalidRequest",
"message": "Bad request: No domain header found",
"region": "garage-integ-test",
"path": "/check",
})
);
let admin_req = || {
Request::builder()
.method("GET")
.uri(format!("http://127.0.0.1:{}/check", ctx.garage.admin_port))
.header("domain", "foobar")
.body(Body::empty())
.unwrap()
};
let admin_resp = client.request(admin_req()).await.unwrap();
assert_eq!(admin_resp.status(), StatusCode::NOT_FOUND);
let res_body = json_body(admin_resp).await;
assert_json_eq!(
res_body,
json!({
"code": "NoSuchBucket",
"message": "Bucket not found: foobar",
"region": "garage-integ-test",
"path": "/check",
})
);
let admin_req = || {
Request::builder()
.method("GET")
.uri(format!("http://127.0.0.1:{}/check", ctx.garage.admin_port))
.header("domain", "")
.body(Body::empty())
.unwrap()
};
let admin_resp = client.request(admin_req()).await.unwrap();
assert_eq!(admin_resp.status(), StatusCode::BAD_REQUEST);
let res_body = json_body(admin_resp).await;
assert_json_eq!(
res_body,
json!({
"code": "InvalidRequest",
"message": "Bad request: Invalid characters found in domain header: failed to convert header to a str",
"region": "garage-integ-test",
"path": "/check",
})
);
}