From dde0bc820e47a372c9b1042249637c708a323188 Mon Sep 17 00:00:00 2001 From: Ilion Beyst Date: Sun, 12 Jun 2022 21:03:41 +0200 Subject: accept docker push --- planetwars-server/src/modules/registry.rs | 215 ++++++++++++++++++++++++++++++ 1 file changed, 215 insertions(+) create mode 100644 planetwars-server/src/modules/registry.rs (limited to 'planetwars-server/src/modules/registry.rs') diff --git a/planetwars-server/src/modules/registry.rs b/planetwars-server/src/modules/registry.rs new file mode 100644 index 0000000..d63621a --- /dev/null +++ b/planetwars-server/src/modules/registry.rs @@ -0,0 +1,215 @@ +use axum::body::Body; +use axum::extract::{BodyStream, Path, Query}; +use axum::handler::Handler; +use axum::response::{IntoResponse, Response}; +use axum::routing::{get, head, post, put}; +use axum::Router; +use hyper::StatusCode; +use serde::Serialize; +use sha2::{Digest, Sha256}; +use std::path::PathBuf; +use tokio::io::AsyncWriteExt; + +use crate::util::gen_alphanumeric; + +const REGISTRY_PATH: &'static str = "./data/registry"; +pub fn registry_service() -> Router { + Router::new() + .nest("/v2", registry_api_v2()) + .fallback(fallback.into_service()) +} + +fn registry_api_v2() -> Router { + Router::new() + .route("/", get(root_handler)) + .route("/:name/blobs/:digest", head(blob_check).get(blob_check)) + .route("/:name/blobs/uploads/", post(blob_upload)) + .route( + "/:name/blobs/uploads/:uuid", + put(put_handler).patch(handle_upload), + ) + .route("/:name/manifests/:reference", put(put_manifest)) +} + +async fn fallback(request: axum::http::Request) -> impl IntoResponse { + // for debugging + println!("no route for {} {}", request.method(), request.uri()); + StatusCode::NOT_FOUND +} + +// root should return 200 OK to confirm api compliance +async fn root_handler() -> Response { + Response::builder() + .status(StatusCode::OK) + .header("Docker-Distribution-API-Version", "registry/2.0") + .body(Body::empty()) + .unwrap() +} + +#[derive(Serialize)] +pub struct RegistryErrors { + errors: Vec, +} + +#[derive(Serialize)] +pub struct RegistryError { + code: String, + message: String, + detail: serde_json::Value, +} + +async fn blob_check( + Path((_repository_name, raw_digest)): Path<(String, String)>, +) -> impl IntoResponse { + let digest = raw_digest.strip_prefix("sha256:").unwrap(); + let blob_path = PathBuf::from(REGISTRY_PATH).join(&digest); + if blob_path.exists() { + StatusCode::OK + } else { + StatusCode::NOT_FOUND + } +} + +async fn blob_upload(Path(repository_name): Path) -> impl IntoResponse { + // let value = json!({ + // "errors": [ + // { + // "code": "UNSUPPORTED", + // "message": "not implemented yet lol", + // } + // ] + // }); + + let uuid = gen_alphanumeric(16); + tokio::fs::File::create(PathBuf::from(REGISTRY_PATH).join("uploads").join(&uuid)) + .await + .unwrap(); + + Response::builder() + .status(StatusCode::ACCEPTED) + .header( + "Location", + format!("/v2/{}/blobs/uploads/{}", repository_name, uuid), + ) + .header("Docker-Upload-UUID", uuid) + .header("Range", "bytes=0-0") + .body(Body::empty()) + .unwrap() +} + +use futures::StreamExt; + +async fn handle_upload( + Path((repository_name, uuid)): Path<(String, String)>, + mut stream: BodyStream, +) -> impl IntoResponse { + // let content_length = headers.get("Content-Length").unwrap(); + // let content_range = headers.get("Content-Range").unwrap(); + // let content_type = headers.get("Content-Type").unwrap(); + // assert!(content_type == "application/octet-stream"); + let mut len = 0; + let upload_path = PathBuf::from(REGISTRY_PATH).join("uploads").join(&uuid); + let mut file = tokio::fs::OpenOptions::new() + .read(false) + .write(true) + .append(true) + .create(false) + .open(upload_path) + .await + .unwrap(); + while let Some(Ok(chunk)) = stream.next().await { + let n_bytes = file.write(&chunk).await.unwrap(); + len += n_bytes; + } + + Response::builder() + .status(StatusCode::ACCEPTED) + .header( + "Location", + format!("/v2/{}/blobs/uploads/{}", repository_name, uuid), + ) + .header("Docker-Upload-UUID", uuid) + .header("Range", format!("0-{}", len)) + .body(Body::empty()) + .unwrap() +} + +use serde::Deserialize; +#[derive(Deserialize)] +struct UploadParams { + digest: String, +} + +async fn put_handler( + Path((repository_name, uuid)): Path<(String, String)>, + Query(params): Query, + mut stream: BodyStream, +) -> impl IntoResponse { + let mut _len = 0; + let upload_path = PathBuf::from(REGISTRY_PATH).join("uploads").join(&uuid); + let mut file = tokio::fs::OpenOptions::new() + .read(false) + .write(true) + .append(true) + .create(false) + .open(&upload_path) + .await + .unwrap(); + + while let Some(Ok(chunk)) = stream.next().await { + let n_bytes = file.write(&chunk).await.unwrap(); + _len += n_bytes; + } + let digest = params.digest.strip_prefix("sha256:").unwrap(); + // TODO: check the digest + let target_path = PathBuf::from(REGISTRY_PATH).join(&digest); + tokio::fs::rename(&upload_path, &target_path).await.unwrap(); + println!("DIGEST {}", digest); + Response::builder() + .status(StatusCode::CREATED) + .header( + "Location", + format!("/v2/{}/blobs/{}", repository_name, digest), + ) + .header("Docker-Upload-UUID", uuid) + // .header("Range", format!("0-{}", len)) + .header("Docker-Content-Digest", digest) + .body(Body::empty()) + .unwrap() +} + +async fn put_manifest( + Path((repository_name, reference)): Path<(String, String)>, + mut stream: BodyStream, +) -> impl IntoResponse { + let repository_dir = PathBuf::from(REGISTRY_PATH).join(&repository_name); + + tokio::fs::create_dir_all(&repository_dir).await.unwrap(); + + let mut hasher = Sha256::new(); + { + let manifest_path = repository_dir.join(&reference).with_extension("json"); + let mut file = tokio::fs::OpenOptions::new() + .write(true) + .create(true) + .truncate(true) + .open(&manifest_path) + .await + .unwrap(); + while let Some(Ok(chunk)) = stream.next().await { + hasher.update(&chunk); + file.write(&chunk).await.unwrap(); + } + } + let digest = hasher.finalize(); + + Response::builder() + .status(StatusCode::CREATED) + .header( + "Location", + format!("/v2/{}/manifests/{}", repository_name, reference), + ) + .header("Docker-Content-Digest", format!("sha256:{:x}", digest)) + .body(Body::empty()) + .unwrap() +} -- cgit v1.2.3 From b90b3d3635f57bb84450d90544df536bf58e8588 Mon Sep 17 00:00:00 2001 From: Ilion Beyst Date: Fri, 17 Jun 2022 19:01:40 +0200 Subject: store blobs in sha256 directory --- planetwars-server/src/modules/registry.rs | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) (limited to 'planetwars-server/src/modules/registry.rs') diff --git a/planetwars-server/src/modules/registry.rs b/planetwars-server/src/modules/registry.rs index d63621a..d10532a 100644 --- a/planetwars-server/src/modules/registry.rs +++ b/planetwars-server/src/modules/registry.rs @@ -62,7 +62,7 @@ async fn blob_check( Path((_repository_name, raw_digest)): Path<(String, String)>, ) -> impl IntoResponse { let digest = raw_digest.strip_prefix("sha256:").unwrap(); - let blob_path = PathBuf::from(REGISTRY_PATH).join(&digest); + let blob_path = PathBuf::from(REGISTRY_PATH).join("sha256").join(&digest); if blob_path.exists() { StatusCode::OK } else { @@ -162,9 +162,9 @@ async fn put_handler( } let digest = params.digest.strip_prefix("sha256:").unwrap(); // TODO: check the digest - let target_path = PathBuf::from(REGISTRY_PATH).join(&digest); + let target_path = PathBuf::from(REGISTRY_PATH).join("sha256").join(&digest); tokio::fs::rename(&upload_path, &target_path).await.unwrap(); - println!("DIGEST {}", digest); + Response::builder() .status(StatusCode::CREATED) .header( @@ -182,7 +182,9 @@ async fn put_manifest( Path((repository_name, reference)): Path<(String, String)>, mut stream: BodyStream, ) -> impl IntoResponse { - let repository_dir = PathBuf::from(REGISTRY_PATH).join(&repository_name); + let repository_dir = PathBuf::from(REGISTRY_PATH) + .join("manifests") + .join(&repository_name); tokio::fs::create_dir_all(&repository_dir).await.unwrap(); -- cgit v1.2.3 From 2cde7ec673b38b51db0dce8d5e8496ba2d92aa12 Mon Sep 17 00:00:00 2001 From: Ilion Beyst Date: Sat, 18 Jun 2022 12:42:03 +0200 Subject: support docker pull --- planetwars-server/src/modules/registry.rs | 52 ++++++++++++++++++++++++++++--- 1 file changed, 47 insertions(+), 5 deletions(-) (limited to 'planetwars-server/src/modules/registry.rs') diff --git a/planetwars-server/src/modules/registry.rs b/planetwars-server/src/modules/registry.rs index d10532a..6095527 100644 --- a/planetwars-server/src/modules/registry.rs +++ b/planetwars-server/src/modules/registry.rs @@ -1,4 +1,4 @@ -use axum::body::Body; +use axum::body::{Body, StreamBody}; use axum::extract::{BodyStream, Path, Query}; use axum::handler::Handler; use axum::response::{IntoResponse, Response}; @@ -9,6 +9,7 @@ use serde::Serialize; use sha2::{Digest, Sha256}; use std::path::PathBuf; use tokio::io::AsyncWriteExt; +use tokio_util::io::ReaderStream; use crate::util::gen_alphanumeric; @@ -22,13 +23,16 @@ pub fn registry_service() -> Router { fn registry_api_v2() -> Router { Router::new() .route("/", get(root_handler)) - .route("/:name/blobs/:digest", head(blob_check).get(blob_check)) + .route("/:name/blobs/:digest", head(blob_check).get(get_blob)) .route("/:name/blobs/uploads/", post(blob_upload)) .route( "/:name/blobs/uploads/:uuid", put(put_handler).patch(handle_upload), ) - .route("/:name/manifests/:reference", put(put_manifest)) + .route( + "/:name/manifests/:reference", + get(get_manifest).put(put_manifest), + ) } async fn fallback(request: axum::http::Request) -> impl IntoResponse { @@ -70,6 +74,20 @@ async fn blob_check( } } +async fn get_blob( + Path((_repository_name, raw_digest)): Path<(String, String)>, +) -> impl IntoResponse { + let digest = raw_digest.strip_prefix("sha256:").unwrap(); + let blob_path = PathBuf::from(REGISTRY_PATH).join("sha256").join(&digest); + if !blob_path.exists() { + return Err(StatusCode::NOT_FOUND); + } + let file = tokio::fs::File::open(&blob_path).await.unwrap(); + let reader_stream = ReaderStream::new(file); + let stream_body = StreamBody::new(reader_stream); + Ok(stream_body) +} + async fn blob_upload(Path(repository_name): Path) -> impl IntoResponse { // let value = json!({ // "errors": [ @@ -178,6 +196,26 @@ async fn put_handler( .unwrap() } +async fn get_manifest( + Path((repository_name, reference)): Path<(String, String)>, +) -> impl IntoResponse { + let manifest_path = PathBuf::from(REGISTRY_PATH) + .join("manifests") + .join(&repository_name) + .join(&reference) + .with_extension("json"); + let data = tokio::fs::read(&manifest_path).await.unwrap(); + + let manifest: serde_json::Map = + serde_json::from_slice(&data).unwrap(); + let media_type = manifest.get("mediaType").unwrap().as_str().unwrap(); + Response::builder() + .status(StatusCode::OK) + .header("Content-Type", media_type) + .body(axum::body::Full::from(data)) + .unwrap() +} + async fn put_manifest( Path((repository_name, reference)): Path<(String, String)>, mut stream: BodyStream, @@ -189,8 +227,8 @@ async fn put_manifest( tokio::fs::create_dir_all(&repository_dir).await.unwrap(); let mut hasher = Sha256::new(); + let manifest_path = repository_dir.join(&reference).with_extension("json"); { - let manifest_path = repository_dir.join(&reference).with_extension("json"); let mut file = tokio::fs::OpenOptions::new() .write(true) .create(true) @@ -204,6 +242,10 @@ async fn put_manifest( } } let digest = hasher.finalize(); + // TODO: store content-adressable manifests separately + let content_digest = format!("sha256:{:x}", digest); + let digest_path = repository_dir.join(&content_digest).with_extension("json"); + tokio::fs::copy(manifest_path, digest_path).await.unwrap(); Response::builder() .status(StatusCode::CREATED) @@ -211,7 +253,7 @@ async fn put_manifest( "Location", format!("/v2/{}/manifests/{}", repository_name, reference), ) - .header("Docker-Content-Digest", format!("sha256:{:x}", digest)) + .header("Docker-Content-Digest", content_digest) .body(Body::empty()) .unwrap() } -- cgit v1.2.3 From 478094abcf6f79ddb4e13e5763f5827208363ae7 Mon Sep 17 00:00:00 2001 From: Ilion Beyst Date: Sun, 19 Jun 2022 22:33:44 +0200 Subject: basic docker login PoC --- planetwars-server/src/modules/registry.rs | 57 +++++++++++++++++++++++-------- 1 file changed, 42 insertions(+), 15 deletions(-) (limited to 'planetwars-server/src/modules/registry.rs') diff --git a/planetwars-server/src/modules/registry.rs b/planetwars-server/src/modules/registry.rs index 6095527..9d71dd7 100644 --- a/planetwars-server/src/modules/registry.rs +++ b/planetwars-server/src/modules/registry.rs @@ -1,9 +1,11 @@ -use axum::body::{Body, StreamBody}; -use axum::extract::{BodyStream, Path, Query}; +use axum::body::{Body, Bytes, StreamBody}; +use axum::extract::{BodyStream, FromRequest, Path, Query, RequestParts, TypedHeader}; use axum::handler::Handler; +use axum::headers::authorization::Basic; +use axum::headers::Authorization; use axum::response::{IntoResponse, Response}; use axum::routing::{get, head, post, put}; -use axum::Router; +use axum::{async_trait, Router}; use hyper::StatusCode; use serde::Serialize; use sha2::{Digest, Sha256}; @@ -16,7 +18,8 @@ use crate::util::gen_alphanumeric; const REGISTRY_PATH: &'static str = "./data/registry"; pub fn registry_service() -> Router { Router::new() - .nest("/v2", registry_api_v2()) + // The docker API requires this trailing slash + .nest("/v2/", registry_api_v2()) .fallback(fallback.into_service()) } @@ -41,8 +44,41 @@ async fn fallback(request: axum::http::Request) -> impl IntoResponse { StatusCode::NOT_FOUND } -// root should return 200 OK to confirm api compliance -async fn root_handler() -> Response { +type AuthorizationHeader = TypedHeader>; + +struct RegistryAuth; + +#[async_trait] +impl FromRequest for RegistryAuth +where + B: Send, +{ + type Rejection = Response>; + + async fn from_request(req: &mut RequestParts) -> Result { + let TypedHeader(Authorization(_basic)) = + AuthorizationHeader::from_request(req).await.map_err(|_| { + let err = RegistryErrors { + errors: vec![RegistryError { + code: "UNAUTHORIZED".to_string(), + message: "please log in".to_string(), + detail: serde_json::Value::Null, + }], + }; + Response::builder() + .status(StatusCode::UNAUTHORIZED) + .header("Docker-Distribution-API-Version", "registry/2.0") + .header("WWW-Authenticate", "Basic") + .body(axum::body::Full::from(serde_json::to_vec(&err).unwrap())) + .unwrap() + })?; + + Ok(RegistryAuth) + } +} + +async fn root_handler(_auth: RegistryAuth) -> impl IntoResponse { + // root should return 200 OK to confirm api compliance Response::builder() .status(StatusCode::OK) .header("Docker-Distribution-API-Version", "registry/2.0") @@ -89,15 +125,6 @@ async fn get_blob( } async fn blob_upload(Path(repository_name): Path) -> impl IntoResponse { - // let value = json!({ - // "errors": [ - // { - // "code": "UNSUPPORTED", - // "message": "not implemented yet lol", - // } - // ] - // }); - let uuid = gen_alphanumeric(16); tokio::fs::File::create(PathBuf::from(REGISTRY_PATH).join("uploads").join(&uuid)) .await -- cgit v1.2.3 From a2a8a41689ad07eb2236ee438e9d01266946008d Mon Sep 17 00:00:00 2001 From: Ilion Beyst Date: Mon, 20 Jun 2022 20:27:51 +0200 Subject: rename route handler methods --- planetwars-server/src/modules/registry.rs | 27 +++++++++++++++------------ 1 file changed, 15 insertions(+), 12 deletions(-) (limited to 'planetwars-server/src/modules/registry.rs') diff --git a/planetwars-server/src/modules/registry.rs b/planetwars-server/src/modules/registry.rs index 9d71dd7..61652d9 100644 --- a/planetwars-server/src/modules/registry.rs +++ b/planetwars-server/src/modules/registry.rs @@ -25,17 +25,20 @@ pub fn registry_service() -> Router { fn registry_api_v2() -> Router { Router::new() - .route("/", get(root_handler)) - .route("/:name/blobs/:digest", head(blob_check).get(get_blob)) - .route("/:name/blobs/uploads/", post(blob_upload)) - .route( - "/:name/blobs/uploads/:uuid", - put(put_handler).patch(handle_upload), - ) + .route("/", get(get_root)) .route( "/:name/manifests/:reference", get(get_manifest).put(put_manifest), ) + .route( + "/:name/blobs/:digest", + head(check_blob_exists).get(get_blob), + ) + .route("/:name/blobs/uploads/", post(create_upload)) + .route( + "/:name/blobs/uploads/:uuid", + put(put_upload).patch(patch_upload), + ) } async fn fallback(request: axum::http::Request) -> impl IntoResponse { @@ -77,7 +80,7 @@ where } } -async fn root_handler(_auth: RegistryAuth) -> impl IntoResponse { +async fn get_root(_auth: RegistryAuth) -> impl IntoResponse { // root should return 200 OK to confirm api compliance Response::builder() .status(StatusCode::OK) @@ -98,7 +101,7 @@ pub struct RegistryError { detail: serde_json::Value, } -async fn blob_check( +async fn check_blob_exists( Path((_repository_name, raw_digest)): Path<(String, String)>, ) -> impl IntoResponse { let digest = raw_digest.strip_prefix("sha256:").unwrap(); @@ -124,7 +127,7 @@ async fn get_blob( Ok(stream_body) } -async fn blob_upload(Path(repository_name): Path) -> impl IntoResponse { +async fn create_upload(Path(repository_name): Path) -> impl IntoResponse { let uuid = gen_alphanumeric(16); tokio::fs::File::create(PathBuf::from(REGISTRY_PATH).join("uploads").join(&uuid)) .await @@ -144,7 +147,7 @@ async fn blob_upload(Path(repository_name): Path) -> impl IntoResponse { use futures::StreamExt; -async fn handle_upload( +async fn patch_upload( Path((repository_name, uuid)): Path<(String, String)>, mut stream: BodyStream, ) -> impl IntoResponse { @@ -185,7 +188,7 @@ struct UploadParams { digest: String, } -async fn put_handler( +async fn put_upload( Path((repository_name, uuid)): Path<(String, String)>, Query(params): Query, mut stream: BodyStream, -- cgit v1.2.3 From 059cd4fa0e1da6e3d9b2edaae62d2e58e2f37924 Mon Sep 17 00:00:00 2001 From: Ilion Beyst Date: Mon, 20 Jun 2022 22:14:15 +0200 Subject: implement basic auth checking --- planetwars-server/src/modules/registry.rs | 86 ++++++++++++++++++++++--------- 1 file changed, 63 insertions(+), 23 deletions(-) (limited to 'planetwars-server/src/modules/registry.rs') diff --git a/planetwars-server/src/modules/registry.rs b/planetwars-server/src/modules/registry.rs index 61652d9..a866dce 100644 --- a/planetwars-server/src/modules/registry.rs +++ b/planetwars-server/src/modules/registry.rs @@ -1,4 +1,4 @@ -use axum::body::{Body, Bytes, StreamBody}; +use axum::body::{Body, StreamBody}; use axum::extract::{BodyStream, FromRequest, Path, Query, RequestParts, TypedHeader}; use axum::handler::Handler; use axum::headers::authorization::Basic; @@ -14,8 +14,12 @@ use tokio::io::AsyncWriteExt; use tokio_util::io::ReaderStream; use crate::util::gen_alphanumeric; +use crate::DatabaseConnection; + +use crate::db::users::{authenticate_user, Credentials, User}; + +const REGISTRY_PATH: &str = "./data/registry"; -const REGISTRY_PATH: &'static str = "./data/registry"; pub fn registry_service() -> Router { Router::new() // The docker API requires this trailing slash @@ -49,34 +53,61 @@ async fn fallback(request: axum::http::Request) -> impl IntoResponse { type AuthorizationHeader = TypedHeader>; -struct RegistryAuth; +enum RegistryAuth { + User(User), +} + +enum RegistryAuthError { + NoAuthHeader, + InvalidCredentials, +} + +impl IntoResponse for RegistryAuthError { + fn into_response(self) -> Response { + // TODO: create enum for registry errors + let err = RegistryErrors { + errors: vec![RegistryError { + code: "UNAUTHORIZED".to_string(), + message: "please log in".to_string(), + detail: serde_json::Value::Null, + }], + }; + + ( + StatusCode::UNAUTHORIZED, + [ + ("Docker-Distribution-API-Version", "registry/2.0"), + ("WWW-Authenticate", "Basic"), + ], + serde_json::to_string(&err).unwrap(), + ) + .into_response() + } +} #[async_trait] impl FromRequest for RegistryAuth where B: Send, { - type Rejection = Response>; + type Rejection = RegistryAuthError; async fn from_request(req: &mut RequestParts) -> Result { - let TypedHeader(Authorization(_basic)) = - AuthorizationHeader::from_request(req).await.map_err(|_| { - let err = RegistryErrors { - errors: vec![RegistryError { - code: "UNAUTHORIZED".to_string(), - message: "please log in".to_string(), - detail: serde_json::Value::Null, - }], - }; - Response::builder() - .status(StatusCode::UNAUTHORIZED) - .header("Docker-Distribution-API-Version", "registry/2.0") - .header("WWW-Authenticate", "Basic") - .body(axum::body::Full::from(serde_json::to_vec(&err).unwrap())) - .unwrap() - })?; - - Ok(RegistryAuth) + let db_conn = DatabaseConnection::from_request(req).await.unwrap(); + + let TypedHeader(Authorization(basic)) = AuthorizationHeader::from_request(req) + .await + .map_err(|_| RegistryAuthError::NoAuthHeader)?; + + // TODO: Into would be nice + let credentials = Credentials { + username: basic.username(), + password: basic.password(), + }; + let user = authenticate_user(&credentials, &db_conn) + .ok_or(RegistryAuthError::InvalidCredentials)?; + + Ok(RegistryAuth::User(user)) } } @@ -102,6 +133,7 @@ pub struct RegistryError { } async fn check_blob_exists( + _auth: RegistryAuth, Path((_repository_name, raw_digest)): Path<(String, String)>, ) -> impl IntoResponse { let digest = raw_digest.strip_prefix("sha256:").unwrap(); @@ -114,6 +146,7 @@ async fn check_blob_exists( } async fn get_blob( + _auth: RegistryAuth, Path((_repository_name, raw_digest)): Path<(String, String)>, ) -> impl IntoResponse { let digest = raw_digest.strip_prefix("sha256:").unwrap(); @@ -127,7 +160,10 @@ async fn get_blob( Ok(stream_body) } -async fn create_upload(Path(repository_name): Path) -> impl IntoResponse { +async fn create_upload( + _auth: RegistryAuth, + Path(repository_name): Path, +) -> impl IntoResponse { let uuid = gen_alphanumeric(16); tokio::fs::File::create(PathBuf::from(REGISTRY_PATH).join("uploads").join(&uuid)) .await @@ -148,6 +184,7 @@ async fn create_upload(Path(repository_name): Path) -> impl IntoResponse use futures::StreamExt; async fn patch_upload( + _auth: RegistryAuth, Path((repository_name, uuid)): Path<(String, String)>, mut stream: BodyStream, ) -> impl IntoResponse { @@ -189,6 +226,7 @@ struct UploadParams { } async fn put_upload( + _auth: RegistryAuth, Path((repository_name, uuid)): Path<(String, String)>, Query(params): Query, mut stream: BodyStream, @@ -227,6 +265,7 @@ async fn put_upload( } async fn get_manifest( + _auth: RegistryAuth, Path((repository_name, reference)): Path<(String, String)>, ) -> impl IntoResponse { let manifest_path = PathBuf::from(REGISTRY_PATH) @@ -247,6 +286,7 @@ async fn get_manifest( } async fn put_manifest( + _auth: RegistryAuth, Path((repository_name, reference)): Path<(String, String)>, mut stream: BodyStream, ) -> impl IntoResponse { -- cgit v1.2.3 From 381ce040fda929f65c681d4134a03e3143659243 Mon Sep 17 00:00:00 2001 From: Ilion Beyst Date: Tue, 21 Jun 2022 22:45:59 +0200 Subject: add auth to all registry routes --- planetwars-server/src/modules/registry.rs | 106 +++++++++++++++++++++--------- 1 file changed, 75 insertions(+), 31 deletions(-) (limited to 'planetwars-server/src/modules/registry.rs') diff --git a/planetwars-server/src/modules/registry.rs b/planetwars-server/src/modules/registry.rs index a866dce..8bc3a7d 100644 --- a/planetwars-server/src/modules/registry.rs +++ b/planetwars-server/src/modules/registry.rs @@ -6,6 +6,7 @@ use axum::headers::Authorization; use axum::response::{IntoResponse, Response}; use axum::routing::{get, head, post, put}; use axum::{async_trait, Router}; +use futures::StreamExt; use hyper::StatusCode; use serde::Serialize; use sha2::{Digest, Sha256}; @@ -14,7 +15,7 @@ use tokio::io::AsyncWriteExt; use tokio_util::io::ReaderStream; use crate::util::gen_alphanumeric; -use crate::DatabaseConnection; +use crate::{db, DatabaseConnection}; use crate::db::users::{authenticate_user, Credentials, User}; @@ -133,22 +134,28 @@ pub struct RegistryError { } async fn check_blob_exists( - _auth: RegistryAuth, - Path((_repository_name, raw_digest)): Path<(String, String)>, -) -> impl IntoResponse { + db_conn: DatabaseConnection, + auth: RegistryAuth, + Path((repository_name, raw_digest)): Path<(String, String)>, +) -> Result { + check_access(&repository_name, &auth, &db_conn)?; + let digest = raw_digest.strip_prefix("sha256:").unwrap(); let blob_path = PathBuf::from(REGISTRY_PATH).join("sha256").join(&digest); if blob_path.exists() { - StatusCode::OK + Ok(StatusCode::OK) } else { - StatusCode::NOT_FOUND + Err(StatusCode::NOT_FOUND) } } async fn get_blob( - _auth: RegistryAuth, - Path((_repository_name, raw_digest)): Path<(String, String)>, -) -> impl IntoResponse { + db_conn: DatabaseConnection, + auth: RegistryAuth, + Path((repository_name, raw_digest)): Path<(String, String)>, +) -> Result { + check_access(&repository_name, &auth, &db_conn)?; + let digest = raw_digest.strip_prefix("sha256:").unwrap(); let blob_path = PathBuf::from(REGISTRY_PATH).join("sha256").join(&digest); if !blob_path.exists() { @@ -161,15 +168,18 @@ async fn get_blob( } async fn create_upload( - _auth: RegistryAuth, + db_conn: DatabaseConnection, + auth: RegistryAuth, Path(repository_name): Path, -) -> impl IntoResponse { +) -> Result { + check_access(&repository_name, &auth, &db_conn)?; + let uuid = gen_alphanumeric(16); tokio::fs::File::create(PathBuf::from(REGISTRY_PATH).join("uploads").join(&uuid)) .await .unwrap(); - Response::builder() + Ok(Response::builder() .status(StatusCode::ACCEPTED) .header( "Location", @@ -178,16 +188,17 @@ async fn create_upload( .header("Docker-Upload-UUID", uuid) .header("Range", "bytes=0-0") .body(Body::empty()) - .unwrap() + .unwrap()) } -use futures::StreamExt; - async fn patch_upload( - _auth: RegistryAuth, + db_conn: DatabaseConnection, + auth: RegistryAuth, Path((repository_name, uuid)): Path<(String, String)>, mut stream: BodyStream, -) -> impl IntoResponse { +) -> Result { + check_access(&repository_name, &auth, &db_conn)?; + // let content_length = headers.get("Content-Length").unwrap(); // let content_range = headers.get("Content-Range").unwrap(); // let content_type = headers.get("Content-Type").unwrap(); @@ -207,7 +218,7 @@ async fn patch_upload( len += n_bytes; } - Response::builder() + Ok(Response::builder() .status(StatusCode::ACCEPTED) .header( "Location", @@ -216,7 +227,7 @@ async fn patch_upload( .header("Docker-Upload-UUID", uuid) .header("Range", format!("0-{}", len)) .body(Body::empty()) - .unwrap() + .unwrap()) } use serde::Deserialize; @@ -226,11 +237,14 @@ struct UploadParams { } async fn put_upload( - _auth: RegistryAuth, + db_conn: DatabaseConnection, + auth: RegistryAuth, Path((repository_name, uuid)): Path<(String, String)>, Query(params): Query, mut stream: BodyStream, -) -> impl IntoResponse { +) -> Result { + check_access(&repository_name, &auth, &db_conn)?; + let mut _len = 0; let upload_path = PathBuf::from(REGISTRY_PATH).join("uploads").join(&uuid); let mut file = tokio::fs::OpenOptions::new() @@ -251,7 +265,7 @@ async fn put_upload( let target_path = PathBuf::from(REGISTRY_PATH).join("sha256").join(&digest); tokio::fs::rename(&upload_path, &target_path).await.unwrap(); - Response::builder() + Ok(Response::builder() .status(StatusCode::CREATED) .header( "Location", @@ -261,13 +275,16 @@ async fn put_upload( // .header("Range", format!("0-{}", len)) .header("Docker-Content-Digest", digest) .body(Body::empty()) - .unwrap() + .unwrap()) } async fn get_manifest( - _auth: RegistryAuth, + db_conn: DatabaseConnection, + auth: RegistryAuth, Path((repository_name, reference)): Path<(String, String)>, -) -> impl IntoResponse { +) -> Result { + check_access(&repository_name, &auth, &db_conn)?; + let manifest_path = PathBuf::from(REGISTRY_PATH) .join("manifests") .join(&repository_name) @@ -278,18 +295,21 @@ async fn get_manifest( let manifest: serde_json::Map = serde_json::from_slice(&data).unwrap(); let media_type = manifest.get("mediaType").unwrap().as_str().unwrap(); - Response::builder() + Ok(Response::builder() .status(StatusCode::OK) .header("Content-Type", media_type) .body(axum::body::Full::from(data)) - .unwrap() + .unwrap()) } async fn put_manifest( - _auth: RegistryAuth, + db_conn: DatabaseConnection, + auth: RegistryAuth, Path((repository_name, reference)): Path<(String, String)>, mut stream: BodyStream, -) -> impl IntoResponse { +) -> Result { + check_access(&repository_name, &auth, &db_conn)?; + let repository_dir = PathBuf::from(REGISTRY_PATH) .join("manifests") .join(&repository_name); @@ -317,7 +337,7 @@ async fn put_manifest( let digest_path = repository_dir.join(&content_digest).with_extension("json"); tokio::fs::copy(manifest_path, digest_path).await.unwrap(); - Response::builder() + Ok(Response::builder() .status(StatusCode::CREATED) .header( "Location", @@ -325,5 +345,29 @@ async fn put_manifest( ) .header("Docker-Content-Digest", content_digest) .body(Body::empty()) - .unwrap() + .unwrap()) +} + +fn check_access( + repository_name: &str, + auth: &RegistryAuth, + db_conn: &DatabaseConnection, +) -> Result<(), StatusCode> { + use diesel::OptionalExtension; + + let res = db::bots::find_bot_by_name(repository_name, db_conn) + .optional() + .expect("could not run query"); + + match res { + None => Ok(()), // name has not been claimed yet (TODO: verify its validity) + Some(existing_bot) => { + let RegistryAuth::User(user) = auth; + if existing_bot.owner_id == Some(user.id) { + Ok(()) + } else { + Err(StatusCode::FORBIDDEN) + } + } + } } -- cgit v1.2.3 From f6fca3818a5f5e32afd02280c04fdbe77972075f Mon Sep 17 00:00:00 2001 From: Ilion Beyst Date: Fri, 24 Jun 2022 19:32:22 +0200 Subject: don't allow accessing non-existing repositories --- planetwars-server/src/modules/registry.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'planetwars-server/src/modules/registry.rs') diff --git a/planetwars-server/src/modules/registry.rs b/planetwars-server/src/modules/registry.rs index 8bc3a7d..c0e12d0 100644 --- a/planetwars-server/src/modules/registry.rs +++ b/planetwars-server/src/modules/registry.rs @@ -360,7 +360,7 @@ fn check_access( .expect("could not run query"); match res { - None => Ok(()), // name has not been claimed yet (TODO: verify its validity) + None => Err(StatusCode::FORBIDDEN), Some(existing_bot) => { let RegistryAuth::User(user) = auth; if existing_bot.owner_id == Some(user.id) { -- cgit v1.2.3 From d7e4a1fd5cb1ab7438d281de6dfe26013623dc6b Mon Sep 17 00:00:00 2001 From: Ilion Beyst Date: Mon, 27 Jun 2022 21:20:05 +0200 Subject: implement admin login --- planetwars-server/src/modules/registry.rs | 41 ++++++++++++++++++++++--------- 1 file changed, 29 insertions(+), 12 deletions(-) (limited to 'planetwars-server/src/modules/registry.rs') diff --git a/planetwars-server/src/modules/registry.rs b/planetwars-server/src/modules/registry.rs index c0e12d0..346f5d9 100644 --- a/planetwars-server/src/modules/registry.rs +++ b/planetwars-server/src/modules/registry.rs @@ -52,10 +52,15 @@ async fn fallback(request: axum::http::Request) -> impl IntoResponse { StatusCode::NOT_FOUND } +const ADMIN_USERNAME: &str = "admin"; +// TODO: put this in some configuration +const ADMIN_PASSWORD: &str = "supersecretpassword"; + type AuthorizationHeader = TypedHeader>; enum RegistryAuth { User(User), + Admin, } enum RegistryAuthError { @@ -94,8 +99,6 @@ where type Rejection = RegistryAuthError; async fn from_request(req: &mut RequestParts) -> Result { - let db_conn = DatabaseConnection::from_request(req).await.unwrap(); - let TypedHeader(Authorization(basic)) = AuthorizationHeader::from_request(req) .await .map_err(|_| RegistryAuthError::NoAuthHeader)?; @@ -105,10 +108,20 @@ where username: basic.username(), password: basic.password(), }; - let user = authenticate_user(&credentials, &db_conn) - .ok_or(RegistryAuthError::InvalidCredentials)?; - Ok(RegistryAuth::User(user)) + if credentials.username == ADMIN_USERNAME { + if credentials.password == ADMIN_PASSWORD { + Ok(RegistryAuth::Admin) + } else { + Err(RegistryAuthError::InvalidCredentials) + } + } else { + let db_conn = DatabaseConnection::from_request(req).await.unwrap(); + let user = authenticate_user(&credentials, &db_conn) + .ok_or(RegistryAuthError::InvalidCredentials)?; + + Ok(RegistryAuth::User(user)) + } } } @@ -348,6 +361,8 @@ async fn put_manifest( .unwrap()) } +/// Ensure that the accessed repository exists +/// and the user is allowed to access ti fn check_access( repository_name: &str, auth: &RegistryAuth, @@ -355,15 +370,17 @@ fn check_access( ) -> Result<(), StatusCode> { use diesel::OptionalExtension; - let res = db::bots::find_bot_by_name(repository_name, db_conn) + // TODO: it would be nice to provide the found repository + // to the route handlers + let bot = db::bots::find_bot_by_name(repository_name, db_conn) .optional() - .expect("could not run query"); + .expect("could not run query") + .ok_or(StatusCode::NOT_FOUND)?; - match res { - None => Err(StatusCode::FORBIDDEN), - Some(existing_bot) => { - let RegistryAuth::User(user) = auth; - if existing_bot.owner_id == Some(user.id) { + match &auth { + RegistryAuth::Admin => Ok(()), + RegistryAuth::User(user) => { + if bot.owner_id == Some(user.id) { Ok(()) } else { Err(StatusCode::FORBIDDEN) -- cgit v1.2.3 From 4d1c0a3289a295ea27eea51ec0a91c4229a92edc Mon Sep 17 00:00:00 2001 From: Ilion Beyst Date: Thu, 30 Jun 2022 20:28:37 +0200 Subject: make sure that all pushed data is actually written --- planetwars-server/src/modules/registry.rs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) (limited to 'planetwars-server/src/modules/registry.rs') diff --git a/planetwars-server/src/modules/registry.rs b/planetwars-server/src/modules/registry.rs index 346f5d9..7adb764 100644 --- a/planetwars-server/src/modules/registry.rs +++ b/planetwars-server/src/modules/registry.rs @@ -227,8 +227,8 @@ async fn patch_upload( .await .unwrap(); while let Some(Ok(chunk)) = stream.next().await { - let n_bytes = file.write(&chunk).await.unwrap(); - len += n_bytes; + file.write_all(&chunk).await.unwrap(); + len += chunk.len(); } Ok(Response::builder() @@ -270,9 +270,10 @@ async fn put_upload( .unwrap(); while let Some(Ok(chunk)) = stream.next().await { - let n_bytes = file.write(&chunk).await.unwrap(); - _len += n_bytes; + file.write_all(&chunk).await.unwrap(); + _len += chunk.len(); } + let digest = params.digest.strip_prefix("sha256:").unwrap(); // TODO: check the digest let target_path = PathBuf::from(REGISTRY_PATH).join("sha256").join(&digest); -- cgit v1.2.3 From 419029738dd914bd0c8edd9c8d4365cac2d53ad7 Mon Sep 17 00:00:00 2001 From: Ilion Beyst Date: Thu, 30 Jun 2022 20:49:10 +0200 Subject: verify blob digest on upload --- planetwars-server/src/modules/registry.rs | 26 +++++++++++++++++++++----- 1 file changed, 21 insertions(+), 5 deletions(-) (limited to 'planetwars-server/src/modules/registry.rs') diff --git a/planetwars-server/src/modules/registry.rs b/planetwars-server/src/modules/registry.rs index 7adb764..6e29878 100644 --- a/planetwars-server/src/modules/registry.rs +++ b/planetwars-server/src/modules/registry.rs @@ -125,6 +125,15 @@ where } } +// Since async file io just calls spawn_blocking internally, it does not really make sense +// to make this an async function +fn file_sha256_digest(path: &std::path::Path) -> std::io::Result { + let mut file = std::fs::File::open(path)?; + let mut hasher = Sha256::new(); + let _n = std::io::copy(&mut file, &mut hasher)?; + Ok(format!("{:x}", hasher.finalize())) +} + async fn get_root(_auth: RegistryAuth) -> impl IntoResponse { // root should return 200 OK to confirm api compliance Response::builder() @@ -273,9 +282,15 @@ async fn put_upload( file.write_all(&chunk).await.unwrap(); _len += chunk.len(); } + file.flush().await.unwrap(); + + let expected_digest = params.digest.strip_prefix("sha256:").unwrap(); + let digest = file_sha256_digest(&upload_path).unwrap(); + if digest != expected_digest { + // TODO: return a docker error body + return Err(StatusCode::BAD_REQUEST); + } - let digest = params.digest.strip_prefix("sha256:").unwrap(); - // TODO: check the digest let target_path = PathBuf::from(REGISTRY_PATH).join("sha256").join(&digest); tokio::fs::rename(&upload_path, &target_path).await.unwrap(); @@ -286,8 +301,9 @@ async fn put_upload( format!("/v2/{}/blobs/{}", repository_name, digest), ) .header("Docker-Upload-UUID", uuid) - // .header("Range", format!("0-{}", len)) - .header("Docker-Content-Digest", digest) + // TODO: set content-range + // .header("Content-Range", format!("0-{}", len)) + .header("Docker-Content-Digest", params.digest) .body(Body::empty()) .unwrap()) } @@ -342,7 +358,7 @@ async fn put_manifest( .unwrap(); while let Some(Ok(chunk)) = stream.next().await { hasher.update(&chunk); - file.write(&chunk).await.unwrap(); + file.write_all(&chunk).await.unwrap(); } } let digest = hasher.finalize(); -- cgit v1.2.3 From 7b88bb0502f67e913b6e8bca394428fd2df45cc2 Mon Sep 17 00:00:00 2001 From: Ilion Beyst Date: Fri, 1 Jul 2022 20:45:26 +0200 Subject: use file metadata for returning data ranges and lengths --- planetwars-server/src/modules/registry.rs | 30 ++++++++++++++++++------------ 1 file changed, 18 insertions(+), 12 deletions(-) (limited to 'planetwars-server/src/modules/registry.rs') diff --git a/planetwars-server/src/modules/registry.rs b/planetwars-server/src/modules/registry.rs index 6e29878..d73e7e9 100644 --- a/planetwars-server/src/modules/registry.rs +++ b/planetwars-server/src/modules/registry.rs @@ -134,6 +134,13 @@ fn file_sha256_digest(path: &std::path::Path) -> std::io::Result { Ok(format!("{:x}", hasher.finalize())) } +/// Get the index of the last byte in a file +async fn last_byte_pos(file: &tokio::fs::File) -> std::io::Result { + let n_bytes = file.metadata().await?.len(); + let pos = if n_bytes == 0 { 0 } else { n_bytes - 1 }; + Ok(pos) +} + async fn get_root(_auth: RegistryAuth) -> impl IntoResponse { // root should return 200 OK to confirm api compliance Response::builder() @@ -165,7 +172,8 @@ async fn check_blob_exists( let digest = raw_digest.strip_prefix("sha256:").unwrap(); let blob_path = PathBuf::from(REGISTRY_PATH).join("sha256").join(&digest); if blob_path.exists() { - Ok(StatusCode::OK) + let metadata = std::fs::metadata(&blob_path).unwrap(); + Ok((StatusCode::OK, [("Content-Length", metadata.len())])) } else { Err(StatusCode::NOT_FOUND) } @@ -221,11 +229,7 @@ async fn patch_upload( ) -> Result { check_access(&repository_name, &auth, &db_conn)?; - // let content_length = headers.get("Content-Length").unwrap(); - // let content_range = headers.get("Content-Range").unwrap(); - // let content_type = headers.get("Content-Type").unwrap(); - // assert!(content_type == "application/octet-stream"); - let mut len = 0; + // TODO: support content range header in request let upload_path = PathBuf::from(REGISTRY_PATH).join("uploads").join(&uuid); let mut file = tokio::fs::OpenOptions::new() .read(false) @@ -237,9 +241,10 @@ async fn patch_upload( .unwrap(); while let Some(Ok(chunk)) = stream.next().await { file.write_all(&chunk).await.unwrap(); - len += chunk.len(); } + let last_byte = last_byte_pos(&file).await.unwrap(); + Ok(Response::builder() .status(StatusCode::ACCEPTED) .header( @@ -247,7 +252,8 @@ async fn patch_upload( format!("/v2/{}/blobs/uploads/{}", repository_name, uuid), ) .header("Docker-Upload-UUID", uuid) - .header("Range", format!("0-{}", len)) + // range indicating current progress of the upload + .header("Range", format!("0-{}", last_byte)) .body(Body::empty()) .unwrap()) } @@ -267,7 +273,6 @@ async fn put_upload( ) -> Result { check_access(&repository_name, &auth, &db_conn)?; - let mut _len = 0; let upload_path = PathBuf::from(REGISTRY_PATH).join("uploads").join(&uuid); let mut file = tokio::fs::OpenOptions::new() .read(false) @@ -278,11 +283,12 @@ async fn put_upload( .await .unwrap(); + let range_begin = last_byte_pos(&file).await.unwrap(); while let Some(Ok(chunk)) = stream.next().await { file.write_all(&chunk).await.unwrap(); - _len += chunk.len(); } file.flush().await.unwrap(); + let range_end = last_byte_pos(&file).await.unwrap(); let expected_digest = params.digest.strip_prefix("sha256:").unwrap(); let digest = file_sha256_digest(&upload_path).unwrap(); @@ -301,8 +307,8 @@ async fn put_upload( format!("/v2/{}/blobs/{}", repository_name, digest), ) .header("Docker-Upload-UUID", uuid) - // TODO: set content-range - // .header("Content-Range", format!("0-{}", len)) + // content range for bytes that were in the body of this request + .header("Content-Range", format!("{}-{}", range_begin, range_end)) .header("Docker-Content-Digest", params.digest) .body(Body::empty()) .unwrap()) -- cgit v1.2.3 From bbed87755419f97b0ee8967617af0c6573c168af Mon Sep 17 00:00:00 2001 From: Ilion Beyst Date: Mon, 4 Jul 2022 20:11:29 +0200 Subject: cleanup and comments --- planetwars-server/src/modules/registry.rs | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) (limited to 'planetwars-server/src/modules/registry.rs') diff --git a/planetwars-server/src/modules/registry.rs b/planetwars-server/src/modules/registry.rs index d73e7e9..c8ec4fa 100644 --- a/planetwars-server/src/modules/registry.rs +++ b/planetwars-server/src/modules/registry.rs @@ -1,6 +1,7 @@ +// TODO: this module is functional, but it needs a good refactor for proper error handling. + use axum::body::{Body, StreamBody}; use axum::extract::{BodyStream, FromRequest, Path, Query, RequestParts, TypedHeader}; -use axum::handler::Handler; use axum::headers::authorization::Basic; use axum::headers::Authorization; use axum::response::{IntoResponse, Response}; @@ -19,13 +20,13 @@ use crate::{db, DatabaseConnection}; use crate::db::users::{authenticate_user, Credentials, User}; +// TODO: put this in a config file const REGISTRY_PATH: &str = "./data/registry"; pub fn registry_service() -> Router { Router::new() // The docker API requires this trailing slash .nest("/v2/", registry_api_v2()) - .fallback(fallback.into_service()) } fn registry_api_v2() -> Router { @@ -46,12 +47,6 @@ fn registry_api_v2() -> Router { ) } -async fn fallback(request: axum::http::Request) -> impl IntoResponse { - // for debugging - println!("no route for {} {}", request.method(), request.uri()); - StatusCode::NOT_FOUND -} - const ADMIN_USERNAME: &str = "admin"; // TODO: put this in some configuration const ADMIN_PASSWORD: &str = "supersecretpassword"; -- cgit v1.2.3 From 7eb02a2efc8f0bb8ec411c5af0f648aeda939226 Mon Sep 17 00:00:00 2001 From: Ilion Beyst Date: Fri, 8 Jul 2022 20:40:20 +0200 Subject: create a new bot verison on docker push --- planetwars-server/src/modules/registry.rs | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) (limited to 'planetwars-server/src/modules/registry.rs') diff --git a/planetwars-server/src/modules/registry.rs b/planetwars-server/src/modules/registry.rs index c8ec4fa..7198a61 100644 --- a/planetwars-server/src/modules/registry.rs +++ b/planetwars-server/src/modules/registry.rs @@ -15,6 +15,7 @@ use std::path::PathBuf; use tokio::io::AsyncWriteExt; use tokio_util::io::ReaderStream; +use crate::db::bots::NewBotVersion; use crate::util::gen_alphanumeric; use crate::{db, DatabaseConnection}; @@ -339,7 +340,7 @@ async fn put_manifest( Path((repository_name, reference)): Path<(String, String)>, mut stream: BodyStream, ) -> Result { - check_access(&repository_name, &auth, &db_conn)?; + let bot = check_access(&repository_name, &auth, &db_conn)?; let repository_dir = PathBuf::from(REGISTRY_PATH) .join("manifests") @@ -368,6 +369,15 @@ async fn put_manifest( let digest_path = repository_dir.join(&content_digest).with_extension("json"); tokio::fs::copy(manifest_path, digest_path).await.unwrap(); + // Register the new image as a bot version + // TODO: how should tags be handled? + let new_version = NewBotVersion { + bot_id: Some(bot.id), + code_bundle_path: None, + container_digest: Some(&content_digest), + }; + db::bots::create_bot_version(&new_version, &db_conn).expect("could not save bot version"); + Ok(Response::builder() .status(StatusCode::CREATED) .header( @@ -380,12 +390,13 @@ async fn put_manifest( } /// Ensure that the accessed repository exists -/// and the user is allowed to access ti +/// and the user is allowed to access it. +/// Returns the associated bot. fn check_access( repository_name: &str, auth: &RegistryAuth, db_conn: &DatabaseConnection, -) -> Result<(), StatusCode> { +) -> Result { use diesel::OptionalExtension; // TODO: it would be nice to provide the found repository @@ -396,10 +407,10 @@ fn check_access( .ok_or(StatusCode::NOT_FOUND)?; match &auth { - RegistryAuth::Admin => Ok(()), + RegistryAuth::Admin => Ok(bot), RegistryAuth::User(user) => { if bot.owner_id == Some(user.id) { - Ok(()) + Ok(bot) } else { Err(StatusCode::FORBIDDEN) } -- cgit v1.2.3 From 0cf7b5299d1085e32760ae9843625724a09c8c29 Mon Sep 17 00:00:00 2001 From: Ilion Beyst Date: Sat, 16 Jul 2022 21:47:22 +0200 Subject: integrate registry with GlobalConfig --- planetwars-server/src/modules/registry.rs | 57 +++++++++++++++++++++---------- 1 file changed, 39 insertions(+), 18 deletions(-) (limited to 'planetwars-server/src/modules/registry.rs') diff --git a/planetwars-server/src/modules/registry.rs b/planetwars-server/src/modules/registry.rs index 7198a61..3f6dad2 100644 --- a/planetwars-server/src/modules/registry.rs +++ b/planetwars-server/src/modules/registry.rs @@ -6,24 +6,22 @@ use axum::headers::authorization::Basic; use axum::headers::Authorization; use axum::response::{IntoResponse, Response}; use axum::routing::{get, head, post, put}; -use axum::{async_trait, Router}; +use axum::{async_trait, Extension, Router}; use futures::StreamExt; use hyper::StatusCode; use serde::Serialize; use sha2::{Digest, Sha256}; use std::path::PathBuf; +use std::sync::Arc; use tokio::io::AsyncWriteExt; use tokio_util::io::ReaderStream; use crate::db::bots::NewBotVersion; use crate::util::gen_alphanumeric; -use crate::{db, DatabaseConnection}; +use crate::{db, DatabaseConnection, GlobalConfig}; use crate::db::users::{authenticate_user, Credentials, User}; -// TODO: put this in a config file -const REGISTRY_PATH: &str = "./data/registry"; - pub fn registry_service() -> Router { Router::new() // The docker API requires this trailing slash @@ -49,8 +47,6 @@ fn registry_api_v2() -> Router { } const ADMIN_USERNAME: &str = "admin"; -// TODO: put this in some configuration -const ADMIN_PASSWORD: &str = "supersecretpassword"; type AuthorizationHeader = TypedHeader>; @@ -105,8 +101,12 @@ where password: basic.password(), }; + let Extension(config) = Extension::>::from_request(req) + .await + .unwrap(); + if credentials.username == ADMIN_USERNAME { - if credentials.password == ADMIN_PASSWORD { + if credentials.password == config.registry_admin_password { Ok(RegistryAuth::Admin) } else { Err(RegistryAuthError::InvalidCredentials) @@ -162,11 +162,14 @@ async fn check_blob_exists( db_conn: DatabaseConnection, auth: RegistryAuth, Path((repository_name, raw_digest)): Path<(String, String)>, + Extension(config): Extension>, ) -> Result { check_access(&repository_name, &auth, &db_conn)?; let digest = raw_digest.strip_prefix("sha256:").unwrap(); - let blob_path = PathBuf::from(REGISTRY_PATH).join("sha256").join(&digest); + let blob_path = PathBuf::from(&config.registry_directory) + .join("sha256") + .join(&digest); if blob_path.exists() { let metadata = std::fs::metadata(&blob_path).unwrap(); Ok((StatusCode::OK, [("Content-Length", metadata.len())])) @@ -179,11 +182,14 @@ async fn get_blob( db_conn: DatabaseConnection, auth: RegistryAuth, Path((repository_name, raw_digest)): Path<(String, String)>, + Extension(config): Extension>, ) -> Result { check_access(&repository_name, &auth, &db_conn)?; let digest = raw_digest.strip_prefix("sha256:").unwrap(); - let blob_path = PathBuf::from(REGISTRY_PATH).join("sha256").join(&digest); + let blob_path = PathBuf::from(&config.registry_directory) + .join("sha256") + .join(&digest); if !blob_path.exists() { return Err(StatusCode::NOT_FOUND); } @@ -197,13 +203,18 @@ async fn create_upload( db_conn: DatabaseConnection, auth: RegistryAuth, Path(repository_name): Path, + Extension(config): Extension>, ) -> Result { check_access(&repository_name, &auth, &db_conn)?; let uuid = gen_alphanumeric(16); - tokio::fs::File::create(PathBuf::from(REGISTRY_PATH).join("uploads").join(&uuid)) - .await - .unwrap(); + tokio::fs::File::create( + PathBuf::from(&config.registry_directory) + .join("uploads") + .join(&uuid), + ) + .await + .unwrap(); Ok(Response::builder() .status(StatusCode::ACCEPTED) @@ -222,11 +233,14 @@ async fn patch_upload( auth: RegistryAuth, Path((repository_name, uuid)): Path<(String, String)>, mut stream: BodyStream, + Extension(config): Extension>, ) -> Result { check_access(&repository_name, &auth, &db_conn)?; // TODO: support content range header in request - let upload_path = PathBuf::from(REGISTRY_PATH).join("uploads").join(&uuid); + let upload_path = PathBuf::from(&config.registry_directory) + .join("uploads") + .join(&uuid); let mut file = tokio::fs::OpenOptions::new() .read(false) .write(true) @@ -266,10 +280,13 @@ async fn put_upload( Path((repository_name, uuid)): Path<(String, String)>, Query(params): Query, mut stream: BodyStream, + Extension(config): Extension>, ) -> Result { check_access(&repository_name, &auth, &db_conn)?; - let upload_path = PathBuf::from(REGISTRY_PATH).join("uploads").join(&uuid); + let upload_path = PathBuf::from(&config.registry_directory) + .join("uploads") + .join(&uuid); let mut file = tokio::fs::OpenOptions::new() .read(false) .write(true) @@ -293,7 +310,9 @@ async fn put_upload( return Err(StatusCode::BAD_REQUEST); } - let target_path = PathBuf::from(REGISTRY_PATH).join("sha256").join(&digest); + let target_path = PathBuf::from(&config.registry_directory) + .join("sha256") + .join(&digest); tokio::fs::rename(&upload_path, &target_path).await.unwrap(); Ok(Response::builder() @@ -314,10 +333,11 @@ async fn get_manifest( db_conn: DatabaseConnection, auth: RegistryAuth, Path((repository_name, reference)): Path<(String, String)>, + Extension(config): Extension>, ) -> Result { check_access(&repository_name, &auth, &db_conn)?; - let manifest_path = PathBuf::from(REGISTRY_PATH) + let manifest_path = PathBuf::from(&config.registry_directory) .join("manifests") .join(&repository_name) .join(&reference) @@ -339,10 +359,11 @@ async fn put_manifest( auth: RegistryAuth, Path((repository_name, reference)): Path<(String, String)>, mut stream: BodyStream, + Extension(config): Extension>, ) -> Result { let bot = check_access(&repository_name, &auth, &db_conn)?; - let repository_dir = PathBuf::from(REGISTRY_PATH) + let repository_dir = PathBuf::from(&config.registry_directory) .join("manifests") .join(&repository_name); -- cgit v1.2.3