From 0150a1a11e39c915787e36bf4fd7384343f3db4b Mon Sep 17 00:00:00 2001 From: SeanOMik Date: Thu, 22 Jun 2023 23:34:26 -0400 Subject: [PATCH] fix pulling from public repositories when not logged in --- docs/todo.md | 2 +- src/api/blobs.rs | 33 ++++++++++++++------- src/api/manifests.rs | 37 +++++++++++++++-------- src/api/mod.rs | 3 +- src/api/uploads.rs | 10 +++---- src/database/mod.rs | 15 ++++++++-- src/database/schemas/schema.sql | 4 +-- src/dto/user.rs | 52 ++++++++++++++++++++++++++++++++- src/main.rs | 4 +-- 9 files changed, 123 insertions(+), 37 deletions(-) diff --git a/docs/todo.md b/docs/todo.md index 46ca145..e3414bf 100644 --- a/docs/todo.md +++ b/docs/todo.md @@ -14,4 +14,4 @@ - [ ] fix repository list - [ ] its not responding with anything - [ ] make sure private repositories dont show up -- [ ] fix pulling from public repositories when not logged in \ No newline at end of file +- [x] fix pulling from public repositories when not logged in \ No newline at end of file diff --git a/src/api/blobs.rs b/src/api/blobs.rs index de5e573..d440ab1 100644 --- a/src/api/blobs.rs +++ b/src/api/blobs.rs @@ -9,17 +9,24 @@ use tokio_util::io::ReaderStream; use crate::app_state::AppState; use crate::auth::access_denied_response; +use crate::database::Database; use crate::dto::RepositoryVisibility; use crate::dto::user::{Permission, UserAuth}; use crate::error::AppError; -pub async fn digest_exists_head(Path((name, layer_digest)): Path<(String, String)>, state: State>, Extension(auth): Extension) -> Result { +pub async fn digest_exists_head(Path((name, layer_digest)): Path<(String, String)>, state: State>, auth: Option) -> Result { // 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(access_denied_response(&state.config)); + if let Some(auth) = auth { + 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(access_denied_response(&state.config)); + } + } else { + let database = &state.database; + if database.get_repository_visibility(&name).await? != Some(RepositoryVisibility::Public) { + return Ok(access_denied_response(&state.config)); + } } - drop(auth_driver); let storage = state.storage.lock().await; @@ -38,13 +45,19 @@ pub async fn digest_exists_head(Path((name, layer_digest)): Path<(String, String Ok(StatusCode::NOT_FOUND.into_response()) } -pub async fn pull_digest_get(Path((name, layer_digest)): Path<(String, String)>, state: State>, Extension(auth): Extension) -> Result { +pub async fn pull_digest_get(Path((name, layer_digest)): Path<(String, String)>, state: State>, auth: Option) -> Result { // 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(access_denied_response(&state.config)); + if let Some(auth) = auth { + 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(access_denied_response(&state.config)); + } + } else { + let database = &state.database; + if database.get_repository_visibility(&name).await? != Some(RepositoryVisibility::Public) { + return Ok(access_denied_response(&state.config)); + } } - drop(auth_driver); let storage = state.storage.lock().await; diff --git a/src/api/manifests.rs b/src/api/manifests.rs index a256734..c974fd7 100644 --- a/src/api/manifests.rs +++ b/src/api/manifests.rs @@ -16,7 +16,7 @@ use crate::dto::manifest::Manifest; use crate::dto::user::{UserAuth, Permission}; use crate::error::AppError; -pub async fn upload_manifest_put(Path((name, reference)): Path<(String, String)>, state: State>, Extension(auth): Extension, body: String) -> Result { +pub async fn upload_manifest_put(Path((name, reference)): Path<(String, String)>, state: State>, auth: UserAuth, 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(access_denied_response(&state.config)); @@ -64,13 +64,19 @@ pub async fn upload_manifest_put(Path((name, reference)): Path<(String, String)> } } -pub async fn pull_manifest_get(Path((name, reference)): Path<(String, String)>, state: State>, Extension(auth): Extension) -> Result { +pub async fn pull_manifest_get(Path((name, reference)): Path<(String, String)>, state: State>, auth: Option) -> Result { // 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(access_denied_response(&state.config)); + if let Some(auth) = auth { + 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(access_denied_response(&state.config)); + } + } else { + let database = &state.database; + if database.get_repository_visibility(&name).await? != Some(RepositoryVisibility::Public) { + return Ok(access_denied_response(&state.config)); + } } - drop(auth_driver); let database = &state.database; let digest = match Digest::is_digest(&reference) { @@ -107,13 +113,20 @@ pub async fn pull_manifest_get(Path((name, reference)): Path<(String, String)>, ).into_response()) } -pub async fn manifest_exists_head(Path((name, reference)): Path<(String, String)>, state: State>, Extension(auth): Extension) -> Result { +pub async fn manifest_exists_head(Path((name, reference)): Path<(String, String)>, state: State>, auth: Option) -> Result { // 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(access_denied_response(&state.config)); + if let Some(auth) = auth { + 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(access_denied_response(&state.config)); + } + drop(auth_driver); + } else { + let database = &state.database; + if database.get_repository_visibility(&name).await? != Some(RepositoryVisibility::Public) { + return Ok(access_denied_response(&state.config)); + } } - drop(auth_driver); // Get the digest from the reference path. let database = &state.database; @@ -148,7 +161,7 @@ pub async fn manifest_exists_head(Path((name, reference)): Path<(String, String) ).into_response()) } -pub async fn delete_manifest(Path((name, reference)): Path<(String, String)>, state: State>, Extension(auth): Extension) -> Result { +pub async fn delete_manifest(Path((name, reference)): Path<(String, String)>, state: State>, auth: UserAuth) -> 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(access_denied_response(&state.config)); diff --git a/src/api/mod.rs b/src/api/mod.rs index b47ea7c..3418c44 100644 --- a/src/api/mod.rs +++ b/src/api/mod.rs @@ -1,6 +1,5 @@ use std::sync::Arc; -use axum::Extension; use axum::extract::State; use axum::response::{IntoResponse, Response}; use axum::http::{StatusCode, HeaderName}; @@ -18,7 +17,7 @@ use crate::dto::user::UserAuth; /// https://docs.docker.com/registry/spec/api/#api-version-check /// full endpoint: `/v2/` -pub async fn version_check(Extension(_auth): Extension, _state: State>) -> Response { +pub async fn version_check(_state: State>) -> Response { ( StatusCode::OK, [( HeaderName::from_static("docker-distribution-api-version"), "registry/2.0" )] diff --git a/src/api/uploads.rs b/src/api/uploads.rs index db722a6..7239563 100644 --- a/src/api/uploads.rs +++ b/src/api/uploads.rs @@ -18,7 +18,7 @@ use crate::dto::user::{UserAuth, Permission}; use crate::error::AppError; /// Starting an upload -pub async fn start_upload_post(Path((name, )): Path<(String, )>, Extension(auth): Extension, state: State>) -> Result { +pub async fn start_upload_post(Path((name, )): Path<(String, )>, auth: UserAuth, state: State>) -> 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!("Upload requested"); @@ -38,7 +38,7 @@ pub async fn start_upload_post(Path((name, )): Path<(String, )>, Extension(auth) 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 { +pub async fn chunked_upload_layer_patch(Path((name, layer_uuid)): Path<(String, String)>, auth: UserAuth, 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? { return Ok(access_denied_response(&state.config)); @@ -97,7 +97,7 @@ pub async fn chunked_upload_layer_patch(Path((name, layer_uuid)): Path<(String, ).into_response()) } -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 { +pub async fn finish_chunked_upload_put(Path((name, layer_uuid)): Path<(String, String)>, Query(query): Query>, auth: UserAuth, 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(access_denied_response(&state.config)); @@ -126,7 +126,7 @@ pub async fn finish_chunked_upload_put(Path((name, layer_uuid)): Path<(String, S ).into_response()) } -pub async fn cancel_upload_delete(Path((name, layer_uuid)): Path<(String, String)>, state: State>, Extension(auth): Extension) -> Result { +pub async fn cancel_upload_delete(Path((name, layer_uuid)): Path<(String, String)>, state: State>, auth: UserAuth) -> 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(access_denied_response(&state.config)); @@ -140,7 +140,7 @@ pub async fn cancel_upload_delete(Path((name, layer_uuid)): Path<(String, String Ok(StatusCode::OK.into_response()) } -pub async fn check_upload_status_get(Path((name, layer_uuid)): Path<(String, String)>, state: State>, Extension(auth): Extension) -> Result { +pub async fn check_upload_status_get(Path((name, layer_uuid)): Path<(String, String)>, state: State>, auth: UserAuth) -> 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(access_denied_response(&state.config)); diff --git a/src/database/mod.rs b/src/database/mod.rs index 42b9f75..58748d1 100644 --- a/src/database/mod.rs +++ b/src/database/mod.rs @@ -410,9 +410,20 @@ impl Database for Pool { async fn verify_user_login(&self, email: String, password: String) -> anyhow::Result { let email = email.to_lowercase(); - let row: (String, ) = sqlx::query_as("SELECT password_hash FROM user_logins WHERE email = ?") + + let row: (String,) = match sqlx::query_as("SELECT password_hash FROM user_logins WHERE email = ?") .bind(email) - .fetch_one(self).await?; + .fetch_one(self).await { + Ok(row) => row, + Err(e) => match e { + sqlx::Error::RowNotFound => { + return Ok(false) + }, + _ => { + return Err(anyhow::Error::new(e)); + } + } + }; Ok(bcrypt::verify(password, &row.0)?) } diff --git a/src/database/schemas/schema.sql b/src/database/schemas/schema.sql index cb96632..9031dc5 100644 --- a/src/database/schemas/schema.sql +++ b/src/database/schemas/schema.sql @@ -71,7 +71,7 @@ CREATE TABLE IF NOT EXISTS user_tokens ( created_at BIGINT NOT NULL ); --- create admin user +-- create admin user (password is 'admin') INSERT OR IGNORE INTO users (username, email, login_source) VALUES ('admin', 'admin@example.com', 0); -INSERT OR IGNORE INTO user_logins (email, password_hash, password_salt) VALUES ('admin@example.com', '$2y$05$ZBnzGzctboHkUDMr4W02jOaUuPwmRC2OgWKKBxqiQsYv53OkUrfO6', 'x5ECk0jUmOSfBWxW52wsyO'); +INSERT OR IGNORE INTO user_logins (email, password_hash, password_salt) VALUES ('admin@example.com', '$2y$05$v9ND7dQKvfkOtY4XpnKVaOpvV0F5RDnW1Ec.nfkZ0vmEjLX5D5S8e', 'x5ECk0jUmOSfBWxW52wsyO'); INSERT OR IGNORE INTO user_registry_permissions (email, user_type) VALUES ('admin@example.com', 1); \ No newline at end of file diff --git a/src/dto/user.rs b/src/dto/user.rs index 6ba557a..6c31712 100644 --- a/src/dto/user.rs +++ b/src/dto/user.rs @@ -1,7 +1,12 @@ -use std::collections::HashMap; +use std::{collections::HashMap, sync::Arc}; +use async_trait::async_trait; +use axum::{http::{StatusCode, header, HeaderName, HeaderMap, Request, request::Parts}, extract::{FromRequest, FromRequestParts}}; use bitflags::bitflags; use chrono::{DateTime, Utc}; +use tracing::debug; + +use crate::{app_state::AppState, database::Database}; use super::RepositoryVisibility; @@ -72,6 +77,51 @@ impl UserAuth { } } +#[async_trait] +impl FromRequestParts> for UserAuth { + type Rejection = (StatusCode, HeaderMap); + + async fn from_request_parts(parts: &mut Parts, state: &Arc) -> Result { + let bearer = format!("Bearer realm=\"{}/auth\"", state.config.url()); + let mut failure_headers = HeaderMap::new(); + failure_headers.append(header::WWW_AUTHENTICATE, bearer.parse().unwrap()); + failure_headers.append(HeaderName::from_static("docker-distribution-api-version"), "registry/2.0".parse().unwrap()); + + let auth = String::from( + parts.headers + .get(header::AUTHORIZATION) + .ok_or((StatusCode::UNAUTHORIZED, failure_headers.clone()))? + .to_str() + .map_err(|_| (StatusCode::UNAUTHORIZED, failure_headers.clone()))? + ); + + let token = match auth.split_once(' ') { + Some((auth, token)) if auth == "Bearer" => token, + // This line would allow empty tokens + //_ if auth == "Bearer" => Ok(AuthToken(None)), + _ => return Err( (StatusCode::UNAUTHORIZED, failure_headers) ), + }; + + // If the token is not valid, return an unauthorized response + let database = &state.database; + if let Ok(Some(user)) = database.verify_user_token(token.to_string()).await { + debug!("Authenticated user through middleware: {}", user.user.username); + + Ok(user) + } else { + let bearer = format!("Bearer realm=\"{}/auth\"", state.config.url()); + let mut headers = HeaderMap::new(); + headers.insert(header::WWW_AUTHENTICATE, bearer.parse().unwrap()); + headers.insert(HeaderName::from_static("docker-distribution-api-version"), "registry/2.0".to_string().parse().unwrap()); + + Err(( + StatusCode::UNAUTHORIZED, + headers + )) + } + } +} + bitflags! { #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] pub struct Permission: u32 { diff --git a/src/main.rs b/src/main.rs index 203c797..0f42d20 100644 --- a/src/main.rs +++ b/src/main.rs @@ -102,7 +102,7 @@ async fn main() -> anyhow::Result<()> { let state = Arc::new(AppState::new(pool, storage_driver, config, auth_driver)); - let auth_middleware = axum::middleware::from_fn_with_state(state.clone(), auth::require_auth); + //let auth_middleware = axum::middleware::from_fn_with_state(state.clone(), auth::require_auth); let path_middleware = axum::middleware::from_fn(change_request_paths); let app = Router::new() @@ -129,7 +129,7 @@ async fn main() -> anyhow::Result<()> { .put(api::manifests::upload_manifest_put) .head(api::manifests::manifest_exists_head) .delete(api::manifests::delete_manifest)) - .layer(auth_middleware) // require auth for ALL v2 routes + //.layer(auth_middleware) // require auth for ALL v2 routes ) .with_state(state) .layer(TraceLayer::new_for_http());