registry: add basic error bodies
This commit is contained in:
parent
7154b16ded
commit
e1c30959df
1 changed files with 109 additions and 51 deletions
|
@ -3,13 +3,14 @@
|
||||||
use axum::body::{Body, StreamBody};
|
use axum::body::{Body, StreamBody};
|
||||||
use axum::extract::{BodyStream, FromRequest, Path, Query, RequestParts, TypedHeader};
|
use axum::extract::{BodyStream, FromRequest, Path, Query, RequestParts, TypedHeader};
|
||||||
use axum::headers::authorization::Basic;
|
use axum::headers::authorization::Basic;
|
||||||
use axum::headers::Authorization;
|
use axum::headers::{Authorization, HeaderName};
|
||||||
|
use axum::http::HeaderValue;
|
||||||
use axum::response::{IntoResponse, Response};
|
use axum::response::{IntoResponse, Response};
|
||||||
use axum::routing::{get, head, post, put};
|
use axum::routing::{get, head, post, put};
|
||||||
use axum::{async_trait, Extension, Router};
|
use axum::{async_trait, Extension, Router};
|
||||||
use futures::StreamExt;
|
use futures::StreamExt;
|
||||||
use hyper::StatusCode;
|
use hyper::{HeaderMap, StatusCode};
|
||||||
use serde::Serialize;
|
use serde_json::json;
|
||||||
use sha2::{Digest, Sha256};
|
use sha2::{Digest, Sha256};
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
@ -62,24 +63,7 @@ enum RegistryAuthError {
|
||||||
|
|
||||||
impl IntoResponse for RegistryAuthError {
|
impl IntoResponse for RegistryAuthError {
|
||||||
fn into_response(self) -> Response {
|
fn into_response(self) -> Response {
|
||||||
// TODO: create enum for registry errors
|
RegistryError::Unauthorized.into_response()
|
||||||
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()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -113,10 +97,9 @@ where
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
let mut db_conn = DatabaseConnection::from_request(req).await.unwrap();
|
let mut db_conn = DatabaseConnection::from_request(req).await.unwrap();
|
||||||
let user = authenticate_user(&credentials, &mut db_conn)
|
authenticate_user(&credentials, &mut db_conn)
|
||||||
.ok_or(RegistryAuthError::InvalidCredentials)?;
|
.map(RegistryAuth::User)
|
||||||
|
.ok_or(RegistryAuthError::InvalidCredentials)
|
||||||
Ok(RegistryAuth::User(user))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -146,25 +129,13 @@ async fn get_root(_auth: RegistryAuth) -> impl IntoResponse {
|
||||||
.unwrap()
|
.unwrap()
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize)]
|
|
||||||
pub struct RegistryErrors {
|
|
||||||
errors: Vec<RegistryError>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Serialize)]
|
|
||||||
pub struct RegistryError {
|
|
||||||
code: String,
|
|
||||||
message: String,
|
|
||||||
detail: serde_json::Value,
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn check_blob_exists(
|
async fn check_blob_exists(
|
||||||
mut db_conn: DatabaseConnection,
|
mut db_conn: DatabaseConnection,
|
||||||
auth: RegistryAuth,
|
auth: RegistryAuth,
|
||||||
Path((repository_name, raw_digest)): Path<(String, String)>,
|
Path((repository_name, raw_digest)): Path<(String, String)>,
|
||||||
Extension(config): Extension<Arc<GlobalConfig>>,
|
Extension(config): Extension<Arc<GlobalConfig>>,
|
||||||
) -> Result<impl IntoResponse, StatusCode> {
|
) -> Result<impl IntoResponse, (StatusCode, HeaderMap)> {
|
||||||
check_access(&repository_name, &auth, &mut db_conn)?;
|
check_access(&repository_name, &auth, &mut db_conn).map_err(|err| err.into_headers())?;
|
||||||
|
|
||||||
let digest = raw_digest.strip_prefix("sha256:").unwrap();
|
let digest = raw_digest.strip_prefix("sha256:").unwrap();
|
||||||
let blob_path = PathBuf::from(&config.registry_directory)
|
let blob_path = PathBuf::from(&config.registry_directory)
|
||||||
|
@ -174,7 +145,7 @@ async fn check_blob_exists(
|
||||||
let metadata = std::fs::metadata(&blob_path).unwrap();
|
let metadata = std::fs::metadata(&blob_path).unwrap();
|
||||||
Ok((StatusCode::OK, [("Content-Length", metadata.len())]))
|
Ok((StatusCode::OK, [("Content-Length", metadata.len())]))
|
||||||
} else {
|
} else {
|
||||||
Err(StatusCode::NOT_FOUND)
|
Err(RegistryError::BlobUnknown.into_headers())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -183,7 +154,7 @@ async fn get_blob(
|
||||||
auth: RegistryAuth,
|
auth: RegistryAuth,
|
||||||
Path((repository_name, raw_digest)): Path<(String, String)>,
|
Path((repository_name, raw_digest)): Path<(String, String)>,
|
||||||
Extension(config): Extension<Arc<GlobalConfig>>,
|
Extension(config): Extension<Arc<GlobalConfig>>,
|
||||||
) -> Result<impl IntoResponse, StatusCode> {
|
) -> Result<impl IntoResponse, RegistryError> {
|
||||||
check_access(&repository_name, &auth, &mut db_conn)?;
|
check_access(&repository_name, &auth, &mut db_conn)?;
|
||||||
|
|
||||||
let digest = raw_digest.strip_prefix("sha256:").unwrap();
|
let digest = raw_digest.strip_prefix("sha256:").unwrap();
|
||||||
|
@ -191,7 +162,7 @@ async fn get_blob(
|
||||||
.join("sha256")
|
.join("sha256")
|
||||||
.join(&digest);
|
.join(&digest);
|
||||||
if !blob_path.exists() {
|
if !blob_path.exists() {
|
||||||
return Err(StatusCode::NOT_FOUND);
|
return Err(RegistryError::BlobUnknown);
|
||||||
}
|
}
|
||||||
let file = tokio::fs::File::open(&blob_path).await.unwrap();
|
let file = tokio::fs::File::open(&blob_path).await.unwrap();
|
||||||
let reader_stream = ReaderStream::new(file);
|
let reader_stream = ReaderStream::new(file);
|
||||||
|
@ -204,7 +175,7 @@ async fn create_upload(
|
||||||
auth: RegistryAuth,
|
auth: RegistryAuth,
|
||||||
Path(repository_name): Path<String>,
|
Path(repository_name): Path<String>,
|
||||||
Extension(config): Extension<Arc<GlobalConfig>>,
|
Extension(config): Extension<Arc<GlobalConfig>>,
|
||||||
) -> Result<impl IntoResponse, StatusCode> {
|
) -> Result<impl IntoResponse, RegistryError> {
|
||||||
check_access(&repository_name, &auth, &mut db_conn)?;
|
check_access(&repository_name, &auth, &mut db_conn)?;
|
||||||
|
|
||||||
let uuid = gen_alphanumeric(16);
|
let uuid = gen_alphanumeric(16);
|
||||||
|
@ -234,7 +205,7 @@ async fn patch_upload(
|
||||||
Path((repository_name, uuid)): Path<(String, String)>,
|
Path((repository_name, uuid)): Path<(String, String)>,
|
||||||
mut stream: BodyStream,
|
mut stream: BodyStream,
|
||||||
Extension(config): Extension<Arc<GlobalConfig>>,
|
Extension(config): Extension<Arc<GlobalConfig>>,
|
||||||
) -> Result<impl IntoResponse, StatusCode> {
|
) -> Result<impl IntoResponse, RegistryError> {
|
||||||
check_access(&repository_name, &auth, &mut db_conn)?;
|
check_access(&repository_name, &auth, &mut db_conn)?;
|
||||||
|
|
||||||
// TODO: support content range header in request
|
// TODO: support content range header in request
|
||||||
|
@ -281,7 +252,7 @@ async fn put_upload(
|
||||||
Query(params): Query<UploadParams>,
|
Query(params): Query<UploadParams>,
|
||||||
mut stream: BodyStream,
|
mut stream: BodyStream,
|
||||||
Extension(config): Extension<Arc<GlobalConfig>>,
|
Extension(config): Extension<Arc<GlobalConfig>>,
|
||||||
) -> Result<impl IntoResponse, StatusCode> {
|
) -> Result<impl IntoResponse, RegistryError> {
|
||||||
check_access(&repository_name, &auth, &mut db_conn)?;
|
check_access(&repository_name, &auth, &mut db_conn)?;
|
||||||
|
|
||||||
let upload_path = PathBuf::from(&config.registry_directory)
|
let upload_path = PathBuf::from(&config.registry_directory)
|
||||||
|
@ -309,7 +280,7 @@ async fn put_upload(
|
||||||
let digest = file_sha256_digest(&upload_path).unwrap();
|
let digest = file_sha256_digest(&upload_path).unwrap();
|
||||||
if digest != expected_digest {
|
if digest != expected_digest {
|
||||||
// TODO: return a docker error body
|
// TODO: return a docker error body
|
||||||
return Err(StatusCode::BAD_REQUEST);
|
return Err(RegistryError::DigestInvalid);
|
||||||
}
|
}
|
||||||
|
|
||||||
let target_path = PathBuf::from(&config.registry_directory)
|
let target_path = PathBuf::from(&config.registry_directory)
|
||||||
|
@ -336,7 +307,7 @@ async fn get_manifest(
|
||||||
auth: RegistryAuth,
|
auth: RegistryAuth,
|
||||||
Path((repository_name, reference)): Path<(String, String)>,
|
Path((repository_name, reference)): Path<(String, String)>,
|
||||||
Extension(config): Extension<Arc<GlobalConfig>>,
|
Extension(config): Extension<Arc<GlobalConfig>>,
|
||||||
) -> Result<impl IntoResponse, StatusCode> {
|
) -> Result<impl IntoResponse, RegistryError> {
|
||||||
check_access(&repository_name, &auth, &mut db_conn)?;
|
check_access(&repository_name, &auth, &mut db_conn)?;
|
||||||
|
|
||||||
let manifest_path = PathBuf::from(&config.registry_directory)
|
let manifest_path = PathBuf::from(&config.registry_directory)
|
||||||
|
@ -362,7 +333,7 @@ async fn put_manifest(
|
||||||
Path((repository_name, reference)): Path<(String, String)>,
|
Path((repository_name, reference)): Path<(String, String)>,
|
||||||
mut stream: BodyStream,
|
mut stream: BodyStream,
|
||||||
Extension(config): Extension<Arc<GlobalConfig>>,
|
Extension(config): Extension<Arc<GlobalConfig>>,
|
||||||
) -> Result<impl IntoResponse, StatusCode> {
|
) -> Result<impl IntoResponse, RegistryError> {
|
||||||
let bot = check_access(&repository_name, &auth, &mut db_conn)?;
|
let bot = check_access(&repository_name, &auth, &mut db_conn)?;
|
||||||
|
|
||||||
let repository_dir = PathBuf::from(&config.registry_directory)
|
let repository_dir = PathBuf::from(&config.registry_directory)
|
||||||
|
@ -422,7 +393,7 @@ fn check_access(
|
||||||
repository_name: &str,
|
repository_name: &str,
|
||||||
auth: &RegistryAuth,
|
auth: &RegistryAuth,
|
||||||
db_conn: &mut DatabaseConnection,
|
db_conn: &mut DatabaseConnection,
|
||||||
) -> Result<db::bots::Bot, StatusCode> {
|
) -> Result<db::bots::Bot, RegistryError> {
|
||||||
use diesel::OptionalExtension;
|
use diesel::OptionalExtension;
|
||||||
|
|
||||||
// TODO: it would be nice to provide the found repository
|
// TODO: it would be nice to provide the found repository
|
||||||
|
@ -430,7 +401,8 @@ fn check_access(
|
||||||
let bot = db::bots::find_bot_by_name(repository_name, db_conn)
|
let bot = db::bots::find_bot_by_name(repository_name, db_conn)
|
||||||
.optional()
|
.optional()
|
||||||
.expect("could not run query")
|
.expect("could not run query")
|
||||||
.ok_or(StatusCode::NOT_FOUND)?;
|
// TODO: return an error message here
|
||||||
|
.ok_or(RegistryError::NameUnknown)?;
|
||||||
|
|
||||||
match &auth {
|
match &auth {
|
||||||
RegistryAuth::Admin => Ok(bot),
|
RegistryAuth::Admin => Ok(bot),
|
||||||
|
@ -438,8 +410,94 @@ fn check_access(
|
||||||
if bot.owner_id == Some(user.id) {
|
if bot.owner_id == Some(user.id) {
|
||||||
Ok(bot)
|
Ok(bot)
|
||||||
} else {
|
} else {
|
||||||
Err(StatusCode::FORBIDDEN)
|
Err(RegistryError::Denied)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum RegistryError {
|
||||||
|
Denied,
|
||||||
|
Unauthorized,
|
||||||
|
|
||||||
|
DigestInvalid,
|
||||||
|
|
||||||
|
BlobUnknown,
|
||||||
|
NameUnknown,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RegistryError {
|
||||||
|
fn into_headers(self) -> (StatusCode, HeaderMap) {
|
||||||
|
let raw = self.into_raw();
|
||||||
|
(raw.status_code, raw.headers)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn into_raw(self) -> RawRegistryError {
|
||||||
|
match self {
|
||||||
|
RegistryError::Unauthorized => RawRegistryError {
|
||||||
|
status_code: StatusCode::UNAUTHORIZED,
|
||||||
|
error_code: "UNAUTHORIZED",
|
||||||
|
message: "Authenticate to continue",
|
||||||
|
headers: HeaderMap::from_iter([(
|
||||||
|
HeaderName::from_static("www-authenticate"),
|
||||||
|
HeaderValue::from_static("Basic"),
|
||||||
|
)]),
|
||||||
|
},
|
||||||
|
RegistryError::Denied => RawRegistryError {
|
||||||
|
status_code: StatusCode::FORBIDDEN,
|
||||||
|
error_code: "DENIED",
|
||||||
|
message: "Access denied",
|
||||||
|
headers: HeaderMap::new(),
|
||||||
|
},
|
||||||
|
RegistryError::BlobUnknown => RawRegistryError {
|
||||||
|
status_code: StatusCode::FORBIDDEN,
|
||||||
|
error_code: "BLOB_UNKNOWN",
|
||||||
|
message: "Blob does not exist",
|
||||||
|
headers: HeaderMap::new(),
|
||||||
|
},
|
||||||
|
RegistryError::NameUnknown => RawRegistryError {
|
||||||
|
status_code: StatusCode::NOT_FOUND,
|
||||||
|
error_code: "NAME_UNKNOWN",
|
||||||
|
message: "Repository does not exist",
|
||||||
|
headers: HeaderMap::new(),
|
||||||
|
},
|
||||||
|
RegistryError::DigestInvalid => RawRegistryError {
|
||||||
|
status_code: StatusCode::UNPROCESSABLE_ENTITY,
|
||||||
|
error_code: "DIGEST_INVALID",
|
||||||
|
message: "Layer digest did not match provided value",
|
||||||
|
headers: HeaderMap::new(),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl IntoResponse for RegistryError {
|
||||||
|
fn into_response(self) -> Response {
|
||||||
|
self.into_raw().into_response()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct RawRegistryError {
|
||||||
|
status_code: StatusCode,
|
||||||
|
error_code: &'static str,
|
||||||
|
message: &'static str,
|
||||||
|
headers: HeaderMap,
|
||||||
|
// currently not used
|
||||||
|
// detail: serde_json::Value,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl IntoResponse for RawRegistryError {
|
||||||
|
fn into_response(self) -> Response {
|
||||||
|
let json_body = json!({
|
||||||
|
"errors": [{
|
||||||
|
"code": self.error_code,
|
||||||
|
"message": self.message,
|
||||||
|
"detail": serde_json::Value::Null,
|
||||||
|
}],
|
||||||
|
});
|
||||||
|
|
||||||
|
let body = serde_json::to_vec(&json_body).unwrap();
|
||||||
|
|
||||||
|
(self.status_code, self.headers, body).into_response()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in a new issue