diff --git a/Cargo.lock b/Cargo.lock index 8e639cc..0f6d76c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -37,6 +37,12 @@ version = "1.0.70" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7de8ce5e0f9f8d88245311066a578d72b7af3e7088f32783804676302df237e4" +[[package]] +name = "arc-swap" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bddcadddf5e9015d310179a59bb28c4d4b9920ad0f11e8e14dbadf654890c9a6" + [[package]] name = "argmap" version = "1.1.2" @@ -113,9 +119,9 @@ checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" [[package]] name = "axum" -version = "0.6.16" +version = "0.6.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "113713495a32dd0ab52baf5c10044725aa3aec00b31beda84218e469029b72a3" +checksum = "f8175979259124331c1d7bf6586ee7e0da434155e4b2d48ec2c8386281d8df39" dependencies = [ "async-trait", "axum-core", @@ -184,6 +190,26 @@ dependencies = [ "syn 2.0.15", ] +[[package]] +name = "axum-server" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "447f28c85900215cc1bea282f32d4a2f22d55c5a300afdfbc661c8d6a632e063" +dependencies = [ + "arc-swap", + "bytes", + "futures-util", + "http", + "http-body", + "hyper", + "pin-project-lite", + "rustls 0.21.5", + "rustls-pemfile", + "tokio", + "tokio-rustls 0.24.1", + "tower-service", +] + [[package]] name = "base64" version = "0.13.1" @@ -742,6 +768,25 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" +[[package]] +name = "h2" +version = "0.3.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97ec8491ebaf99c8eaa73058b045fe58073cd6be7f596ac993ced0b0a0c01049" +dependencies = [ + "bytes", + "fnv", + "futures-core", + "futures-sink", + "futures-util", + "http", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] + [[package]] name = "hashbrown" version = "0.12.3" @@ -849,6 +894,7 @@ dependencies = [ "futures-channel", "futures-core", "futures-util", + "h2", "http", "http-body", "httparse", @@ -1256,6 +1302,7 @@ dependencies = [ "axum", "axum-auth", "axum-macros", + "axum-server", "bcrypt", "bitflags 2.2.1", "bytes", @@ -1267,6 +1314,7 @@ dependencies = [ "hmac", "jws", "jwt", + "lazy_static", "ldap3", "pin-project-lite", "qstring", @@ -1572,6 +1620,18 @@ dependencies = [ "webpki", ] +[[package]] +name = "rustls" +version = "0.21.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79ea77c539259495ce8ca47f53e66ae0330a8819f67e23ac96ca02f50e7b7d36" +dependencies = [ + "log", + "ring", + "rustls-webpki", + "sct", +] + [[package]] name = "rustls-pemfile" version = "1.0.2" @@ -1581,6 +1641,16 @@ dependencies = [ "base64 0.21.0", ] +[[package]] +name = "rustls-webpki" +version = "0.101.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15f36a6828982f422756984e47912a7a51dcbc2a197aa791158f8ca61cd8204e" +dependencies = [ + "ring", + "untrusted", +] + [[package]] name = "rustversion" version = "1.0.12" @@ -1847,7 +1917,7 @@ dependencies = [ "once_cell", "paste", "percent-encoding", - "rustls", + "rustls 0.20.8", "rustls-pemfile", "sha2 0.10.6", "smallvec", @@ -1887,7 +1957,7 @@ checksum = "804d3f245f894e61b1e6263c84b23ca675d96753b5abfd5cc8597d86806e8024" dependencies = [ "once_cell", "tokio", - "tokio-rustls", + "tokio-rustls 0.23.4", ] [[package]] @@ -2083,11 +2153,21 @@ version = "0.23.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c43ee83903113e03984cb9e5cebe6c04a5116269e900e3ddba8f068a62adda59" dependencies = [ - "rustls", + "rustls 0.20.8", "tokio", "webpki", ] +[[package]] +name = "tokio-rustls" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c28327cf380ac148141087fbfb9de9d7bd4e84ab5d2c28fbc911d753de8a7081" +dependencies = [ + "rustls 0.21.5", + "tokio", +] + [[package]] name = "tokio-stream" version = "0.1.12" diff --git a/Cargo.toml b/Cargo.toml index ee6a0ef..a16fa73 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -32,7 +32,8 @@ sha256 = "1.1.2" pin-project-lite = "0.2.9" anyhow = "1.0.70" async-stream = "0.3.5" -axum = "0.6.16" +axum = "0.6.18" +axum-server = { version = "0.5.1", features = [ "tls-rustls" ] } axum-macros = "0.3.7" tower-http = { version = "0.4.0", features = [ "trace", "normalize-path" ] } @@ -52,3 +53,4 @@ rand = "0.8.5" bcrypt = "0.14.0" bitflags = "2.2.1" ldap3 = "0.11.1" +lazy_static = "1.4.0" \ No newline at end of file diff --git a/docs/todo.md b/docs/todo.md index e3414bf..ba2de79 100644 --- a/docs/todo.md +++ b/docs/todo.md @@ -5,6 +5,7 @@ - [ ] simple way to define users and their permissions through a "users.toml" - [x] Only allow users to create repositories if its the same name as their username, or if they're an admin - [x] Only allow users to pull from their own repositories +- [ ] token expiry - [ ] postgresql - [ ] prometheus metrics - [ ] simple webui for managing the registry @@ -14,4 +15,6 @@ - [ ] fix repository list - [ ] its not responding with anything - [ ] make sure private repositories dont show up -- [x] 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 +- [ ] database table for orca related info (version, etc.) + - [ ] only execute sql schemas if this table is missing or not updated \ No newline at end of file diff --git a/src/api/auth.rs b/src/api/auth.rs index 01f99d7..20fa089 100644 --- a/src/api/auth.rs +++ b/src/api/auth.rs @@ -179,7 +179,8 @@ pub async fn auth_basic_get(basic_auth: Option, state: State>) -> Response { + let bearer = format!("Bearer realm=\"{}/auth\"", _state.config.url()); ( - StatusCode::OK, - [( HeaderName::from_static("docker-distribution-api-version"), "registry/2.0" )] + StatusCode::UNAUTHORIZED, + [ + ( HeaderName::from_static("docker-distribution-api-version"), "registry/2.0" ), + //( header::WWW_AUTHENTICATE, &bearer ), + ] ).into_response() } \ No newline at end of file diff --git a/src/api/uploads.rs b/src/api/uploads.rs index 7239563..aa9eae1 100644 --- a/src/api/uploads.rs +++ b/src/api/uploads.rs @@ -12,13 +12,21 @@ use futures::StreamExt; use tracing::{debug, warn}; use crate::app_state::AppState; -use crate::auth::access_denied_response; +use crate::auth::{access_denied_response, unauthenticated_response}; use crate::byte_stream::ByteStream; +use crate::dto::scope::{Scope, ScopeType, Action}; use crate::dto::user::{UserAuth, Permission}; use crate::error::AppError; /// Starting an upload -pub async fn start_upload_post(Path((name, )): Path<(String, )>, auth: UserAuth, state: State>) -> Result { +pub async fn start_upload_post(Path((name, )): Path<(String, )>, auth: Option, state: State>) -> Result { + if auth.is_none() { + debug!("atuh was not given, responding with scope"); + let s = Scope::new(ScopeType::Repository, name, &[Action::Push, Action::Pull]); + return Ok(unauthenticated_response(&state.config, &s)); + } + let auth = auth.unwrap(); + 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"); diff --git a/src/auth/mod.rs b/src/auth/mod.rs index ce0dd6f..123253c 100644 --- a/src/auth/mod.rs +++ b/src/auth/mod.rs @@ -7,7 +7,7 @@ use axum::{extract::State, http::{StatusCode, HeaderMap, header, HeaderName, Req use sqlx::{Pool, Sqlite}; use tracing::debug; -use crate::{app_state::AppState, dto::{user::{Permission, RegistryUserType}, RepositoryVisibility}, config::Config}; +use crate::{app_state::AppState, dto::{user::{Permission, RegistryUserType}, RepositoryVisibility, scope::{self, Scope}}, config::Config}; use crate::database::Database; use async_trait::async_trait; @@ -133,14 +133,17 @@ 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.url()); +pub fn unauthenticated_response(config: &Config, scope: &Scope) -> Response { + let bearer = format!("Bearer realm=\"{}/auth\",service=\"{}\",scope=\"{}\"", config.url(), "localhost:3000", scope); + debug!("responding with www-authenticate header of: \"{}\"", bearer); ( StatusCode::UNAUTHORIZED, [ ( header::WWW_AUTHENTICATE, bearer ), + ( header::CONTENT_TYPE, "application/json".to_string() ), ( HeaderName::from_static("docker-distribution-api-version"), "registry/2.0".to_string() ) - ] + ], + "{\"errors\":[{\"code\":\"UNAUTHORIZED\",\"message\":\"access to the requested resource is not authorized\",\"detail\":[{\"Type\":\"repository\",\"Name\":\"samalba/my-app\",\"Action\":\"pull\"},{\"Type\":\"repository\",\"Name\":\"samalba/my-app\",\"Action\":\"push\"}]}]}" ).into_response() } diff --git a/src/config.rs b/src/config.rs index f03c880..37100d8 100644 --- a/src/config.rs +++ b/src/config.rs @@ -47,6 +47,13 @@ pub struct SqliteDbConfig { pub path: String, } +#[derive(Deserialize, Clone)] +pub struct TlsConfig { + pub enable: bool, + pub key: String, + pub cert: String, +} + #[derive(Deserialize, Clone)] #[serde(tag = "type", rename_all = "snake_case")] pub enum DatabaseConfig { @@ -63,6 +70,7 @@ pub struct Config { pub ldap: Option, pub database: DatabaseConfig, pub storage: StorageConfig, + pub tls: Option, } #[allow(dead_code)] diff --git a/src/dto/scope.rs b/src/dto/scope.rs index 10c338b..f7b11b9 100644 --- a/src/dto/scope.rs +++ b/src/dto/scope.rs @@ -19,7 +19,7 @@ impl fmt::Display for ScopeType { } } -#[derive(Default, Debug)] +#[derive(Default, Debug, Clone)] pub enum Action { #[default] None, @@ -44,6 +44,16 @@ pub struct Scope { actions: Vec, } +impl Scope { + pub fn new(scope_type: ScopeType, path: String, actions: &[Action]) -> Self { + Self { + scope_type, + path, + actions: actions.to_vec(), + } + } +} + impl fmt::Display for Scope { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let actions = self.actions diff --git a/src/dto/user.rs b/src/dto/user.rs index 6c31712..6953c72 100644 --- a/src/dto/user.rs +++ b/src/dto/user.rs @@ -4,7 +4,7 @@ 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 tracing::{debug, warn}; use crate::{app_state::AppState, database::Database}; @@ -87,14 +87,25 @@ impl FromRequestParts> for UserAuth { failure_headers.append(header::WWW_AUTHENTICATE, bearer.parse().unwrap()); failure_headers.append(HeaderName::from_static("docker-distribution-api-version"), "registry/2.0".parse().unwrap()); + debug!("starting UserAuth request parts"); + let auth = String::from( parts.headers .get(header::AUTHORIZATION) - .ok_or((StatusCode::UNAUTHORIZED, failure_headers.clone()))? + .ok_or( + { + debug!("Client did not send authorization header"); + (StatusCode::UNAUTHORIZED, failure_headers.clone()) + })? .to_str() - .map_err(|_| (StatusCode::UNAUTHORIZED, failure_headers.clone()))? + .map_err(|_| { + warn!("Failure to convert Authorization header to string!"); + (StatusCode::UNAUTHORIZED, failure_headers.clone()) + })? ); + debug!("got auth header"); + let token = match auth.split_once(' ') { Some((auth, token)) if auth == "Bearer" => token, // This line would allow empty tokens @@ -102,6 +113,8 @@ impl FromRequestParts> for UserAuth { _ => return Err( (StatusCode::UNAUTHORIZED, failure_headers) ), }; + debug!("got token"); + // 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 { @@ -109,14 +122,11 @@ impl FromRequestParts> for UserAuth { 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()); + debug!("Failure to verify user token, responding with auth realm"); Err(( StatusCode::UNAUTHORIZED, - headers + failure_headers )) } } diff --git a/src/main.rs b/src/main.rs index 0f42d20..f9a976d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -9,6 +9,7 @@ mod auth; mod error; use std::net::SocketAddr; +use std::path::Path; use std::str::FromStr; use std::sync::Arc; @@ -18,7 +19,11 @@ use axum::middleware::Next; use axum::response::Response; use axum::{Router, routing}; use axum::ServiceExt; +use axum_server::tls_rustls::RustlsConfig; +use lazy_static::lazy_static; +use regex::Regex; use sqlx::ConnectOptions; +use tokio::fs::File; use tower_layer::Layer; use sqlx::sqlite::{SqlitePoolOptions, SqliteConnectOptions, SqliteJournalMode}; @@ -36,11 +41,14 @@ use crate::config::{Config, DatabaseConfig, StorageConfig}; use tower_http::trace::TraceLayer; +lazy_static! { + static ref REGISTRY_URL_REGEX: Regex = regex::Regex::new(r"/v2/([\w\-_./]+)/(blobs|tags|manifests)").unwrap(); +} + /// Encode the 'name' path parameter in the url async fn change_request_paths(mut request: Request, next: Next) -> Result { // Attempt to find the name using regex in the url - let regex = regex::Regex::new(r"/v2/([\w/]+)/(blobs|tags|manifests)") - .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; + let regex = ®ISTRY_URL_REGEX; let captures = match regex.captures(request.uri().path()) { Some(captures) => captures, None => return Ok(next.run(request).await), @@ -72,6 +80,11 @@ async fn main() -> anyhow::Result<()> { let sqlite_config = match &config.database { DatabaseConfig::Sqlite(sqlite) => sqlite, }; + + // Create a database file if it doesn't exist already + if !Path::new(&sqlite_config.path).exists() { + File::create(&sqlite_config.path).await?; + } let connection_options = SqliteConnectOptions::from_str(&format!("sqlite://{}", &sqlite_config.path))? .journal_mode(SqliteJournalMode::Wal); @@ -100,6 +113,7 @@ async fn main() -> anyhow::Result<()> { let app_addr = SocketAddr::from_str(&format!("{}:{}", config.listen_address, config.listen_port))?; + let tls_config = config.tls.clone(); 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); @@ -136,10 +150,23 @@ async fn main() -> anyhow::Result<()> { let layered_app = NormalizePathLayer::trim_trailing_slash().layer(path_middleware.layer(app)); - info!("Starting http server, listening on {}", app_addr); - axum::Server::bind(&app_addr) - .serve(layered_app.into_make_service()) - .await?; + match tls_config { + Some(tls) if tls.enable => { + info!("Starting https server, listening on {}", app_addr); + + let config = RustlsConfig::from_pem_file(&tls.cert, &tls.key).await?; + + axum_server::bind_rustls(app_addr, config) + .serve(layered_app.into_make_service()) + .await?; + }, + _ => { + info!("Starting http server, listening on {}", app_addr); + axum::Server::bind(&app_addr) + .serve(layered_app.into_make_service()) + .await?; + } + } Ok(()) } \ No newline at end of file