diff --git a/docs/todo.md b/docs/todo.md index ce4b80b..2ed5168 100644 --- a/docs/todo.md +++ b/docs/todo.md @@ -9,4 +9,5 @@ - [ ] prometheus metrics - [ ] simple webui for managing the registry - [x] streaming layer bytes into providers -- [x] streaming layer bytes from providers \ No newline at end of file +- [x] streaming layer bytes from providers +- [ ] better client error messages \ No newline at end of file diff --git a/src/api/blobs.rs b/src/api/blobs.rs index 65d8f73..de5e573 100644 --- a/src/api/blobs.rs +++ b/src/api/blobs.rs @@ -8,7 +8,7 @@ use axum::response::{IntoResponse, Response}; use tokio_util::io::ReaderStream; use crate::app_state::AppState; -use crate::auth::unauthenticated_response; +use crate::auth::access_denied_response; use crate::dto::RepositoryVisibility; use crate::dto::user::{Permission, UserAuth}; use crate::error::AppError; @@ -17,7 +17,7 @@ pub async fn digest_exists_head(Path((name, layer_digest)): Path<(String, String // Check if the user has permission to pull, or that the repository is public let mut auth_driver = state.auth_checker.lock().await; if !auth_driver.user_has_permission(auth.user.username, name.clone(), Permission::PULL, Some(RepositoryVisibility::Public)).await? { - return Ok(unauthenticated_response(&state.config)); + return Ok(access_denied_response(&state.config)); } drop(auth_driver); @@ -42,7 +42,7 @@ pub async fn pull_digest_get(Path((name, layer_digest)): Path<(String, String)>, // Check if the user has permission to pull, or that the repository is public let mut auth_driver = state.auth_checker.lock().await; if !auth_driver.user_has_permission(auth.user.username, name.clone(), Permission::PULL, Some(RepositoryVisibility::Public)).await? { - return Ok(unauthenticated_response(&state.config)); + return Ok(access_denied_response(&state.config)); } drop(auth_driver); diff --git a/src/api/manifests.rs b/src/api/manifests.rs index 9b44d3b..a256734 100644 --- a/src/api/manifests.rs +++ b/src/api/manifests.rs @@ -7,7 +7,7 @@ use axum::http::{StatusCode, HeaderName, header}; use tracing::log::warn; use tracing::{debug, info}; -use crate::auth::unauthenticated_response; +use crate::auth::access_denied_response; use crate::app_state::AppState; use crate::database::Database; use crate::dto::RepositoryVisibility; @@ -19,7 +19,7 @@ use crate::error::AppError; pub async fn upload_manifest_put(Path((name, reference)): Path<(String, String)>, state: State>, Extension(auth): Extension, body: String) -> Result { let mut auth_driver = state.auth_checker.lock().await; if !auth_driver.user_has_permission(auth.user.username, name.clone(), Permission::PUSH, None).await? { - return Ok(unauthenticated_response(&state.config)); + return Ok(access_denied_response(&state.config)); } drop(auth_driver); @@ -68,7 +68,7 @@ pub async fn pull_manifest_get(Path((name, reference)): Path<(String, String)>, // Check if the user has permission to pull, or that the repository is public let mut auth_driver = state.auth_checker.lock().await; if !auth_driver.user_has_permission(auth.user.username, name.clone(), Permission::PULL, Some(RepositoryVisibility::Public)).await? { - return Ok(unauthenticated_response(&state.config)); + return Ok(access_denied_response(&state.config)); } drop(auth_driver); @@ -111,7 +111,7 @@ pub async fn manifest_exists_head(Path((name, reference)): Path<(String, String) // Check if the user has permission to pull, or that the repository is public let mut auth_driver = state.auth_checker.lock().await; if !auth_driver.user_has_permission(auth.user.username, name.clone(), Permission::PULL, Some(RepositoryVisibility::Public)).await? { - return Ok(unauthenticated_response(&state.config)); + return Ok(access_denied_response(&state.config)); } drop(auth_driver); @@ -151,7 +151,7 @@ pub async fn manifest_exists_head(Path((name, reference)): Path<(String, String) pub async fn delete_manifest(Path((name, reference)): Path<(String, String)>, state: State>, Extension(auth): Extension) -> Result { let mut auth_driver = state.auth_checker.lock().await; if !auth_driver.user_has_permission(auth.user.username, name.clone(), Permission::PUSH, None).await? { - return Ok(unauthenticated_response(&state.config)); + return Ok(access_denied_response(&state.config)); } drop(auth_driver); diff --git a/src/api/uploads.rs b/src/api/uploads.rs index 272bd86..17d1e3e 100644 --- a/src/api/uploads.rs +++ b/src/api/uploads.rs @@ -12,7 +12,7 @@ use futures::StreamExt; use tracing::{debug, warn}; use crate::app_state::AppState; -use crate::auth::unauthenticated_response; +use crate::auth::access_denied_response; use crate::byte_stream::ByteStream; use crate::dto::user::{UserAuth, Permission}; use crate::error::AppError; @@ -35,14 +35,13 @@ pub async fn start_upload_post(Path((name, )): Path<(String, )>, Extension(auth) ).into_response()); } - Ok(unauthenticated_response(&state.config)) + Ok(access_denied_response(&state.config)) } pub async fn chunked_upload_layer_patch(Path((name, layer_uuid)): Path<(String, String)>, Extension(auth): Extension, state: State>, mut body: BodyStream) -> Result { let mut auth_driver = state.auth_checker.lock().await; if !auth_driver.user_has_permission(auth.user.username, name.clone(), Permission::PUSH, None).await? { - debug!("user is not authenticated"); - return Ok(unauthenticated_response(&state.config)); + return Ok(access_denied_response(&state.config)); } drop(auth_driver); @@ -101,7 +100,7 @@ pub async fn chunked_upload_layer_patch(Path((name, layer_uuid)): Path<(String, pub async fn finish_chunked_upload_put(Path((name, layer_uuid)): Path<(String, String)>, Query(query): Query>, Extension(auth): Extension, state: State>, body: Bytes) -> Result { let mut auth_driver = state.auth_checker.lock().await; if !auth_driver.user_has_permission(auth.user.username, name.clone(), Permission::PUSH, None).await? { - return Ok(unauthenticated_response(&state.config)); + return Ok(access_denied_response(&state.config)); } drop(auth_driver); @@ -130,7 +129,7 @@ pub async fn finish_chunked_upload_put(Path((name, layer_uuid)): Path<(String, S pub async fn cancel_upload_delete(Path((name, layer_uuid)): Path<(String, String)>, state: State>, Extension(auth): Extension) -> Result { let mut auth_driver = state.auth_checker.lock().await; if !auth_driver.user_has_permission(auth.user.username, name.clone(), Permission::PUSH, None).await? { - return Ok(unauthenticated_response(&state.config)); + return Ok(access_denied_response(&state.config)); } drop(auth_driver); @@ -144,7 +143,7 @@ pub async fn cancel_upload_delete(Path((name, layer_uuid)): Path<(String, String pub async fn check_upload_status_get(Path((name, layer_uuid)): Path<(String, String)>, state: State>, Extension(auth): Extension) -> Result { let mut auth_driver = state.auth_checker.lock().await; if !auth_driver.user_has_permission(auth.user.username, name.clone(), Permission::PUSH, None).await? { - return Ok(unauthenticated_response(&state.config)); + return Ok(access_denied_response(&state.config)); } drop(auth_driver); diff --git a/src/auth/mod.rs b/src/auth/mod.rs index 039b191..da1f530 100644 --- a/src/auth/mod.rs +++ b/src/auth/mod.rs @@ -130,6 +130,8 @@ pub async fn require_auth(State(state): State>, mut request: Re } } +/// Creates a response with an Unauthorized (401) status code. +/// The www-authenticate header is set to notify the client of where to authorize with. #[inline(always)] pub fn unauthenticated_response(config: &Config) -> Response { let bearer = format!("Bearer realm=\"{}/auth\"", config.get_url()); @@ -140,4 +142,16 @@ pub fn unauthenticated_response(config: &Config) -> Response { ( HeaderName::from_static("docker-distribution-api-version"), "registry/2.0".to_string() ) ] ).into_response() +} + +/// Creates a response with a Forbidden (403) status code. +/// No other headers are set. +#[inline(always)] +pub fn access_denied_response(config: &Config) -> Response { + ( + StatusCode::FORBIDDEN, + [ + ( HeaderName::from_static("docker-distribution-api-version"), "registry/2.0".to_string() ) + ] + ).into_response() } \ No newline at end of file