Merge pull request 'Implemented website hosting authorization endpoint.' (#474) from jpds/garage:bucket-serving-validator into main
All checks were successful
continuous-integration/drone/push Build is passing
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #474
This commit is contained in:
commit
aff9c264c8
4 changed files with 189 additions and 0 deletions
|
@ -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,
|
||||||
|
|
|
@ -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,
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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",
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in a new issue