Compare commits

..

No commits in common. "2ecebab33004cec50bc3c24c5caa84c04485fdaa" and "0546b71c5e6502eaf8be7f1f777272035c7de383" have entirely different histories.

18 changed files with 70 additions and 354 deletions

90
Cargo.lock generated
View File

@ -37,12 +37,6 @@ version = "1.0.70"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7de8ce5e0f9f8d88245311066a578d72b7af3e7088f32783804676302df237e4" checksum = "7de8ce5e0f9f8d88245311066a578d72b7af3e7088f32783804676302df237e4"
[[package]]
name = "arc-swap"
version = "1.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bddcadddf5e9015d310179a59bb28c4d4b9920ad0f11e8e14dbadf654890c9a6"
[[package]] [[package]]
name = "argmap" name = "argmap"
version = "1.1.2" version = "1.1.2"
@ -119,9 +113,9 @@ checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"
[[package]] [[package]]
name = "axum" name = "axum"
version = "0.6.18" version = "0.6.16"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8175979259124331c1d7bf6586ee7e0da434155e4b2d48ec2c8386281d8df39" checksum = "113713495a32dd0ab52baf5c10044725aa3aec00b31beda84218e469029b72a3"
dependencies = [ dependencies = [
"async-trait", "async-trait",
"axum-core", "axum-core",
@ -190,26 +184,6 @@ dependencies = [
"syn 2.0.15", "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]] [[package]]
name = "base64" name = "base64"
version = "0.13.1" version = "0.13.1"
@ -768,25 +742,6 @@ version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" 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]] [[package]]
name = "hashbrown" name = "hashbrown"
version = "0.12.3" version = "0.12.3"
@ -894,7 +849,6 @@ dependencies = [
"futures-channel", "futures-channel",
"futures-core", "futures-core",
"futures-util", "futures-util",
"h2",
"http", "http",
"http-body", "http-body",
"httparse", "httparse",
@ -1302,7 +1256,6 @@ dependencies = [
"axum", "axum",
"axum-auth", "axum-auth",
"axum-macros", "axum-macros",
"axum-server",
"bcrypt", "bcrypt",
"bitflags 2.2.1", "bitflags 2.2.1",
"bytes", "bytes",
@ -1314,7 +1267,6 @@ dependencies = [
"hmac", "hmac",
"jws", "jws",
"jwt", "jwt",
"lazy_static",
"ldap3", "ldap3",
"pin-project-lite", "pin-project-lite",
"qstring", "qstring",
@ -1620,18 +1572,6 @@ dependencies = [
"webpki", "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]] [[package]]
name = "rustls-pemfile" name = "rustls-pemfile"
version = "1.0.2" version = "1.0.2"
@ -1641,16 +1581,6 @@ dependencies = [
"base64 0.21.0", "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]] [[package]]
name = "rustversion" name = "rustversion"
version = "1.0.12" version = "1.0.12"
@ -1917,7 +1847,7 @@ dependencies = [
"once_cell", "once_cell",
"paste", "paste",
"percent-encoding", "percent-encoding",
"rustls 0.20.8", "rustls",
"rustls-pemfile", "rustls-pemfile",
"sha2 0.10.6", "sha2 0.10.6",
"smallvec", "smallvec",
@ -1957,7 +1887,7 @@ checksum = "804d3f245f894e61b1e6263c84b23ca675d96753b5abfd5cc8597d86806e8024"
dependencies = [ dependencies = [
"once_cell", "once_cell",
"tokio", "tokio",
"tokio-rustls 0.23.4", "tokio-rustls",
] ]
[[package]] [[package]]
@ -2153,21 +2083,11 @@ version = "0.23.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c43ee83903113e03984cb9e5cebe6c04a5116269e900e3ddba8f068a62adda59" checksum = "c43ee83903113e03984cb9e5cebe6c04a5116269e900e3ddba8f068a62adda59"
dependencies = [ dependencies = [
"rustls 0.20.8", "rustls",
"tokio", "tokio",
"webpki", "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]] [[package]]
name = "tokio-stream" name = "tokio-stream"
version = "0.1.12" version = "0.1.12"

View File

@ -32,8 +32,7 @@ sha256 = "1.1.2"
pin-project-lite = "0.2.9" pin-project-lite = "0.2.9"
anyhow = "1.0.70" anyhow = "1.0.70"
async-stream = "0.3.5" async-stream = "0.3.5"
axum = "0.6.18" axum = "0.6.16"
axum-server = { version = "0.5.1", features = [ "tls-rustls" ] }
axum-macros = "0.3.7" axum-macros = "0.3.7"
tower-http = { version = "0.4.0", features = [ "trace", "normalize-path" ] } tower-http = { version = "0.4.0", features = [ "trace", "normalize-path" ] }
@ -53,4 +52,3 @@ rand = "0.8.5"
bcrypt = "0.14.0" bcrypt = "0.14.0"
bitflags = "2.2.1" bitflags = "2.2.1"
ldap3 = "0.11.1" ldap3 = "0.11.1"
lazy_static = "1.4.0"

View File

@ -1,4 +1,4 @@
FROM rust:alpine3.17 as builder FROM rust:alpine3.14 as builder
# update packages # update packages
RUN apk update RUN apk update
@ -34,9 +34,7 @@ ARG GID=1000
RUN adduser --disabled-password --gecos "" $UNAME -s -G $GID -u $UID RUN adduser --disabled-password --gecos "" $UNAME -s -G $GID -u $UID
COPY --from=builder --chown=$UID:$GID /app/src/target/release/orca-registry /app/orca-registry COPY --from=builder --chown=$UID:$GID /app/src/target/release/orca-registry /app/orca-registry
RUN mkdir /data && \ RUN chown -R $UID:$GID /app
chown -R $UID:$GID /data && \
chown -R $UID:$GID /app
USER $UNAME USER $UNAME

View File

@ -1,4 +1,4 @@
# Orca registry # Docker registry
Orca is a pure-rust implementation of a Docker Registry. Orca is a pure-rust implementation of a Docker Registry.
Note: Orca is still in early development ([status](#status)). Note: Orca is still in early development ([status](#status)).

View File

@ -2,9 +2,6 @@ listen_address = "127.0.0.1"
listen_port = "3000" listen_port = "3000"
url = "http://localhost:3000/" url = "http://localhost:3000/"
# error, warn, info, debug, trace
log_level = "debug"
[storage] [storage]
driver = "filesystem" driver = "filesystem"
path = "/app/blobs" path = "/app/blobs"

View File

@ -5,16 +5,9 @@
- [ ] simple way to define users and their permissions through a "users.toml" - [ ] 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 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 - [x] Only allow users to pull from their own repositories
- [ ] token expiry
- [ ] postgresql - [ ] postgresql
- [ ] prometheus metrics - [ ] prometheus metrics
- [ ] simple webui for managing the registry - [ ] simple webui for managing the registry
- [x] streaming layer bytes into providers - [x] streaming layer bytes into providers
- [x] streaming layer bytes from providers - [x] streaming layer bytes from providers
- [ ] better client error messages - [ ] better client error messages
- [ ] 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
- [ ] database table for orca related info (version, etc.)
- [ ] only execute sql schemas if this table is missing or not updated

View File

@ -179,8 +179,7 @@ pub async fn auth_basic_get(basic_auth: Option<AuthBasic>, state: State<Arc<AppS
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)? { .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)? {
debug!("Authentication failed, incorrect password!"); debug!("Authentication failed, incorrect password!");
// TODO: Dont unwrap, find a way to return multiple scopes return Ok(unauthenticated_response(&state.config));
return Ok(unauthenticated_response(&state.config, auth.scope.first().unwrap()));
} }
drop(auth_driver); drop(auth_driver);

View File

@ -8,27 +8,18 @@ use axum::response::{IntoResponse, Response};
use tokio_util::io::ReaderStream; use tokio_util::io::ReaderStream;
use crate::app_state::AppState; use crate::app_state::AppState;
use crate::auth::{access_denied_response, unauthenticated_response}; use crate::auth::access_denied_response;
use crate::database::Database;
use crate::dto::RepositoryVisibility; use crate::dto::RepositoryVisibility;
use crate::dto::scope::{Scope, ScopeType, Action};
use crate::dto::user::{Permission, UserAuth}; use crate::dto::user::{Permission, UserAuth};
use crate::error::AppError; use crate::error::AppError;
pub async fn digest_exists_head(Path((name, layer_digest)): Path<(String, String)>, state: State<Arc<AppState>>, auth: Option<UserAuth>) -> Result<Response, AppError> { pub async fn digest_exists_head(Path((name, layer_digest)): Path<(String, String)>, state: State<Arc<AppState>>, Extension(auth): Extension<UserAuth>) -> Result<Response, AppError> {
// Check if the user has permission to pull, or that the repository is public // Check if the user has permission to pull, or that the repository is public
if let Some(auth) = auth { let mut auth_driver = state.auth_checker.lock().await;
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? {
if !auth_driver.user_has_permission(auth.user.username, name.clone(), Permission::PULL, Some(RepositoryVisibility::Public)).await? { return Ok(access_denied_response(&state.config));
return Ok(access_denied_response(&state.config));
}
} else {
let database = &state.database;
if database.get_repository_visibility(&name).await? != Some(RepositoryVisibility::Public) {
let s = Scope::new(ScopeType::Repository, name, &[Action::Push, Action::Pull]);
return Ok(unauthenticated_response(&state.config, &s));
}
} }
drop(auth_driver);
let storage = state.storage.lock().await; let storage = state.storage.lock().await;
@ -47,19 +38,13 @@ pub async fn digest_exists_head(Path((name, layer_digest)): Path<(String, String
Ok(StatusCode::NOT_FOUND.into_response()) Ok(StatusCode::NOT_FOUND.into_response())
} }
pub async fn pull_digest_get(Path((name, layer_digest)): Path<(String, String)>, state: State<Arc<AppState>>, auth: Option<UserAuth>) -> Result<Response, AppError> { pub async fn pull_digest_get(Path((name, layer_digest)): Path<(String, String)>, state: State<Arc<AppState>>, Extension(auth): Extension<UserAuth>) -> Result<Response, AppError> {
// Check if the user has permission to pull, or that the repository is public // Check if the user has permission to pull, or that the repository is public
if let Some(auth) = auth { let mut auth_driver = state.auth_checker.lock().await;
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? {
if !auth_driver.user_has_permission(auth.user.username, name.clone(), Permission::PULL, Some(RepositoryVisibility::Public)).await? { return Ok(access_denied_response(&state.config));
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; let storage = state.storage.lock().await;

View File

@ -16,7 +16,7 @@ use crate::dto::manifest::Manifest;
use crate::dto::user::{UserAuth, Permission}; use crate::dto::user::{UserAuth, Permission};
use crate::error::AppError; use crate::error::AppError;
pub async fn upload_manifest_put(Path((name, reference)): Path<(String, String)>, state: State<Arc<AppState>>, auth: UserAuth, body: String) -> Result<Response, AppError> { pub async fn upload_manifest_put(Path((name, reference)): Path<(String, String)>, state: State<Arc<AppState>>, Extension(auth): Extension<UserAuth>, body: String) -> Result<Response, AppError> {
let mut auth_driver = state.auth_checker.lock().await; let mut auth_driver = state.auth_checker.lock().await;
if !auth_driver.user_has_permission(auth.user.username, name.clone(), Permission::PUSH, None).await? { if !auth_driver.user_has_permission(auth.user.username, name.clone(), Permission::PUSH, None).await? {
return Ok(access_denied_response(&state.config)); return Ok(access_denied_response(&state.config));
@ -64,19 +64,13 @@ 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<Arc<AppState>>, auth: Option<UserAuth>) -> Result<Response, AppError> { pub async fn pull_manifest_get(Path((name, reference)): Path<(String, String)>, state: State<Arc<AppState>>, Extension(auth): Extension<UserAuth>) -> Result<Response, AppError> {
// Check if the user has permission to pull, or that the repository is public // Check if the user has permission to pull, or that the repository is public
if let Some(auth) = auth { let mut auth_driver = state.auth_checker.lock().await;
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? {
if !auth_driver.user_has_permission(auth.user.username, name.clone(), Permission::PULL, Some(RepositoryVisibility::Public)).await? { return Ok(access_denied_response(&state.config));
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 database = &state.database;
let digest = match Digest::is_digest(&reference) { let digest = match Digest::is_digest(&reference) {
@ -113,20 +107,13 @@ pub async fn pull_manifest_get(Path((name, reference)): Path<(String, String)>,
).into_response()) ).into_response())
} }
pub async fn manifest_exists_head(Path((name, reference)): Path<(String, String)>, state: State<Arc<AppState>>, auth: Option<UserAuth>) -> Result<Response, AppError> { pub async fn manifest_exists_head(Path((name, reference)): Path<(String, String)>, state: State<Arc<AppState>>, Extension(auth): Extension<UserAuth>) -> Result<Response, AppError> {
// Check if the user has permission to pull, or that the repository is public // Check if the user has permission to pull, or that the repository is public
if let Some(auth) = auth { let mut auth_driver = state.auth_checker.lock().await;
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? {
if !auth_driver.user_has_permission(auth.user.username, name.clone(), Permission::PULL, Some(RepositoryVisibility::Public)).await? { return Ok(access_denied_response(&state.config));
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. // Get the digest from the reference path.
let database = &state.database; let database = &state.database;
@ -161,7 +148,7 @@ pub async fn manifest_exists_head(Path((name, reference)): Path<(String, String)
).into_response()) ).into_response())
} }
pub async fn delete_manifest(Path((name, reference)): Path<(String, String)>, state: State<Arc<AppState>>, auth: UserAuth) -> Result<Response, AppError> { pub async fn delete_manifest(Path((name, reference)): Path<(String, String)>, state: State<Arc<AppState>>, Extension(auth): Extension<UserAuth>) -> Result<Response, AppError> {
let mut auth_driver = state.auth_checker.lock().await; let mut auth_driver = state.auth_checker.lock().await;
if !auth_driver.user_has_permission(auth.user.username, name.clone(), Permission::PUSH, None).await? { if !auth_driver.user_has_permission(auth.user.username, name.clone(), Permission::PUSH, None).await? {
return Ok(access_denied_response(&state.config)); return Ok(access_denied_response(&state.config));

View File

@ -1,9 +1,9 @@
use std::sync::Arc; use std::sync::Arc;
use axum::Extension;
use axum::extract::State; use axum::extract::State;
use axum::response::{IntoResponse, Response}; use axum::response::{IntoResponse, Response};
use axum::http::{StatusCode, HeaderName, HeaderMap, header}; use axum::http::{StatusCode, HeaderName};
use tracing::debug;
use crate::app_state::AppState; use crate::app_state::AppState;
@ -18,13 +18,9 @@ use crate::dto::user::UserAuth;
/// https://docs.docker.com/registry/spec/api/#api-version-check /// https://docs.docker.com/registry/spec/api/#api-version-check
/// full endpoint: `/v2/` /// full endpoint: `/v2/`
pub async fn version_check(_state: State<Arc<AppState>>) -> Response { pub async fn version_check(Extension(_auth): Extension<UserAuth>, _state: State<Arc<AppState>>) -> Response {
let bearer = format!("Bearer realm=\"{}/auth\"", _state.config.url());
( (
StatusCode::UNAUTHORIZED, StatusCode::OK,
[ [( HeaderName::from_static("docker-distribution-api-version"), "registry/2.0" )]
( HeaderName::from_static("docker-distribution-api-version"), "registry/2.0" ),
//( header::WWW_AUTHENTICATE, &bearer ),
]
).into_response() ).into_response()
} }

View File

@ -12,21 +12,13 @@ use futures::StreamExt;
use tracing::{debug, warn}; use tracing::{debug, warn};
use crate::app_state::AppState; use crate::app_state::AppState;
use crate::auth::{access_denied_response, unauthenticated_response}; use crate::auth::access_denied_response;
use crate::byte_stream::ByteStream; use crate::byte_stream::ByteStream;
use crate::dto::scope::{Scope, ScopeType, Action};
use crate::dto::user::{UserAuth, Permission}; use crate::dto::user::{UserAuth, Permission};
use crate::error::AppError; use crate::error::AppError;
/// Starting an upload /// Starting an upload
pub async fn start_upload_post(Path((name, )): Path<(String, )>, auth: Option<UserAuth>, state: State<Arc<AppState>>) -> Result<Response, AppError> { pub async fn start_upload_post(Path((name, )): Path<(String, )>, Extension(auth): Extension<UserAuth>, state: State<Arc<AppState>>) -> Result<Response, AppError> {
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; let mut auth_driver = state.auth_checker.lock().await;
if auth_driver.user_has_permission(auth.user.username, name.clone(), Permission::PUSH, None).await? { if auth_driver.user_has_permission(auth.user.username, name.clone(), Permission::PUSH, None).await? {
debug!("Upload requested"); debug!("Upload requested");
@ -46,7 +38,7 @@ pub async fn start_upload_post(Path((name, )): Path<(String, )>, auth: Option<Us
Ok(access_denied_response(&state.config)) Ok(access_denied_response(&state.config))
} }
pub async fn chunked_upload_layer_patch(Path((name, layer_uuid)): Path<(String, String)>, auth: UserAuth, state: State<Arc<AppState>>, mut body: BodyStream) -> Result<Response, AppError> { pub async fn chunked_upload_layer_patch(Path((name, layer_uuid)): Path<(String, String)>, Extension(auth): Extension<UserAuth>, state: State<Arc<AppState>>, mut body: BodyStream) -> Result<Response, AppError> {
let mut auth_driver = state.auth_checker.lock().await; let mut auth_driver = state.auth_checker.lock().await;
if !auth_driver.user_has_permission(auth.user.username, name.clone(), Permission::PUSH, None).await? { if !auth_driver.user_has_permission(auth.user.username, name.clone(), Permission::PUSH, None).await? {
return Ok(access_denied_response(&state.config)); return Ok(access_denied_response(&state.config));
@ -105,7 +97,7 @@ pub async fn chunked_upload_layer_patch(Path((name, layer_uuid)): Path<(String,
).into_response()) ).into_response())
} }
pub async fn finish_chunked_upload_put(Path((name, layer_uuid)): Path<(String, String)>, Query(query): Query<HashMap<String, String>>, auth: UserAuth, state: State<Arc<AppState>>, body: Bytes) -> Result<Response, AppError> { pub async fn finish_chunked_upload_put(Path((name, layer_uuid)): Path<(String, String)>, Query(query): Query<HashMap<String, String>>, Extension(auth): Extension<UserAuth>, state: State<Arc<AppState>>, body: Bytes) -> Result<Response, AppError> {
let mut auth_driver = state.auth_checker.lock().await; let mut auth_driver = state.auth_checker.lock().await;
if !auth_driver.user_has_permission(auth.user.username, name.clone(), Permission::PUSH, None).await? { if !auth_driver.user_has_permission(auth.user.username, name.clone(), Permission::PUSH, None).await? {
return Ok(access_denied_response(&state.config)); return Ok(access_denied_response(&state.config));
@ -134,7 +126,7 @@ pub async fn finish_chunked_upload_put(Path((name, layer_uuid)): Path<(String, S
).into_response()) ).into_response())
} }
pub async fn cancel_upload_delete(Path((name, layer_uuid)): Path<(String, String)>, state: State<Arc<AppState>>, auth: UserAuth) -> Result<Response, AppError> { pub async fn cancel_upload_delete(Path((name, layer_uuid)): Path<(String, String)>, state: State<Arc<AppState>>, Extension(auth): Extension<UserAuth>) -> Result<Response, AppError> {
let mut auth_driver = state.auth_checker.lock().await; let mut auth_driver = state.auth_checker.lock().await;
if !auth_driver.user_has_permission(auth.user.username, name.clone(), Permission::PUSH, None).await? { if !auth_driver.user_has_permission(auth.user.username, name.clone(), Permission::PUSH, None).await? {
return Ok(access_denied_response(&state.config)); return Ok(access_denied_response(&state.config));
@ -148,7 +140,7 @@ pub async fn cancel_upload_delete(Path((name, layer_uuid)): Path<(String, String
Ok(StatusCode::OK.into_response()) Ok(StatusCode::OK.into_response())
} }
pub async fn check_upload_status_get(Path((name, layer_uuid)): Path<(String, String)>, state: State<Arc<AppState>>, auth: UserAuth) -> Result<Response, AppError> { pub async fn check_upload_status_get(Path((name, layer_uuid)): Path<(String, String)>, state: State<Arc<AppState>>, Extension(auth): Extension<UserAuth>) -> Result<Response, AppError> {
let mut auth_driver = state.auth_checker.lock().await; let mut auth_driver = state.auth_checker.lock().await;
if !auth_driver.user_has_permission(auth.user.username, name.clone(), Permission::PUSH, None).await? { if !auth_driver.user_has_permission(auth.user.username, name.clone(), Permission::PUSH, None).await? {
return Ok(access_denied_response(&state.config)); return Ok(access_denied_response(&state.config));

View File

@ -7,7 +7,7 @@ use axum::{extract::State, http::{StatusCode, HeaderMap, header, HeaderName, Req
use sqlx::{Pool, Sqlite}; use sqlx::{Pool, Sqlite};
use tracing::debug; use tracing::debug;
use crate::{app_state::AppState, dto::{user::{Permission, RegistryUserType}, RepositoryVisibility, scope::{self, Scope}}, config::Config}; use crate::{app_state::AppState, dto::{user::{Permission, RegistryUserType}, RepositoryVisibility}, config::Config};
use crate::database::Database; use crate::database::Database;
use async_trait::async_trait; use async_trait::async_trait;
@ -133,17 +133,14 @@ pub async fn require_auth<B>(State(state): State<Arc<AppState>>, mut request: Re
/// Creates a response with an Unauthorized (401) status code. /// Creates a response with an Unauthorized (401) status code.
/// The www-authenticate header is set to notify the client of where to authorize with. /// The www-authenticate header is set to notify the client of where to authorize with.
#[inline(always)] #[inline(always)]
pub fn unauthenticated_response(config: &Config, scope: &Scope) -> Response { pub fn unauthenticated_response(config: &Config) -> Response {
let bearer = format!("Bearer realm=\"{}/auth\",service=\"{}\",scope=\"{}\"", config.url(), "localhost:3000", scope); let bearer = format!("Bearer realm=\"{}/auth\"", config.url());
debug!("responding with www-authenticate header of: \"{}\"", bearer);
( (
StatusCode::UNAUTHORIZED, StatusCode::UNAUTHORIZED,
[ [
( header::WWW_AUTHENTICATE, bearer ), ( header::WWW_AUTHENTICATE, bearer ),
( header::CONTENT_TYPE, "application/json".to_string() ),
( HeaderName::from_static("docker-distribution-api-version"), "registry/2.0".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() ).into_response()
} }

View File

@ -1,8 +1,6 @@
use anyhow::anyhow;
use figment::{Figment, providers::{Env, Toml, Format}}; use figment::{Figment, providers::{Env, Toml, Format}};
use figment_cliarg_provider::FigmentCliArgsProvider; use figment_cliarg_provider::FigmentCliArgsProvider;
use serde::{Deserialize, Deserializer}; use serde::Deserialize;
use tracing::Level;
use std::env; use std::env;
@ -47,13 +45,6 @@ pub struct SqliteDbConfig {
pub path: String, pub path: String,
} }
#[derive(Deserialize, Clone)]
pub struct TlsConfig {
pub enable: bool,
pub key: String,
pub cert: String,
}
#[derive(Deserialize, Clone)] #[derive(Deserialize, Clone)]
#[serde(tag = "type", rename_all = "snake_case")] #[serde(tag = "type", rename_all = "snake_case")]
pub enum DatabaseConfig { pub enum DatabaseConfig {
@ -65,12 +56,9 @@ pub struct Config {
pub listen_address: String, pub listen_address: String,
pub listen_port: String, pub listen_port: String,
url: Option<String>, url: Option<String>,
#[serde(deserialize_with = "serialize_log_level", default = "default_log_level")]
pub log_level: Level,
pub ldap: Option<LdapConnectionConfig>, pub ldap: Option<LdapConnectionConfig>,
pub database: DatabaseConfig, pub database: DatabaseConfig,
pub storage: StorageConfig, pub storage: StorageConfig,
pub tls: Option<TlsConfig>,
} }
#[allow(dead_code)] #[allow(dead_code)]
@ -114,30 +102,4 @@ impl Config {
None => format!("http://{}:{}", self.listen_address, self.listen_port) None => format!("http://{}:{}", self.listen_address, self.listen_port)
} }
} }
} }
fn default_log_level() -> Level {
Level::INFO
}
fn serialize_log_level<'de, D>(deserializer: D) -> Result<Level, D::Error>
where D: Deserializer<'de> {
let s = String::deserialize(deserializer)?.to_lowercase();
let s = s.as_str();
match s {
"error" => Ok(Level::ERROR),
"warn" => Ok(Level::WARN),
"info" => Ok(Level::INFO),
"debug" => Ok(Level::DEBUG),
"trace" => Ok(Level::TRACE),
_ => Err(serde::de::Error::custom(format!("Unknown log level: '{}'", s))),
}
}
/* fn<'de, D> serialize_log_level(D) -> Result<Level, D::Error>
where D: Deserializer<'de>
{
} */
//fn serialize_log_level() -> Level

View File

@ -410,20 +410,9 @@ impl Database for Pool<Sqlite> {
async fn verify_user_login(&self, email: String, password: String) -> anyhow::Result<bool> { async fn verify_user_login(&self, email: String, password: String) -> anyhow::Result<bool> {
let email = email.to_lowercase(); 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) .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)?) Ok(bcrypt::verify(password, &row.0)?)
} }

View File

@ -71,7 +71,7 @@ CREATE TABLE IF NOT EXISTS user_tokens (
created_at BIGINT NOT NULL created_at BIGINT NOT NULL
); );
-- create admin user (password is 'admin') -- create admin user
INSERT OR IGNORE INTO users (username, email, login_source) VALUES ('admin', 'admin@example.com', 0); 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$v9ND7dQKvfkOtY4XpnKVaOpvV0F5RDnW1Ec.nfkZ0vmEjLX5D5S8e', 'x5ECk0jUmOSfBWxW52wsyO'); INSERT OR IGNORE INTO user_logins (email, password_hash, password_salt) VALUES ('admin@example.com', '$2y$05$ZBnzGzctboHkUDMr4W02jOaUuPwmRC2OgWKKBxqiQsYv53OkUrfO6', 'x5ECk0jUmOSfBWxW52wsyO');
INSERT OR IGNORE INTO user_registry_permissions (email, user_type) VALUES ('admin@example.com', 1); INSERT OR IGNORE INTO user_registry_permissions (email, user_type) VALUES ('admin@example.com', 1);

View File

@ -19,7 +19,7 @@ impl fmt::Display for ScopeType {
} }
} }
#[derive(Default, Debug, Clone)] #[derive(Default, Debug)]
pub enum Action { pub enum Action {
#[default] #[default]
None, None,
@ -44,16 +44,6 @@ pub struct Scope {
actions: Vec<Action>, actions: Vec<Action>,
} }
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 { impl fmt::Display for Scope {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let actions = self.actions let actions = self.actions

View File

@ -1,12 +1,7 @@
use std::{collections::HashMap, sync::Arc}; use std::collections::HashMap;
use async_trait::async_trait;
use axum::{http::{StatusCode, header, HeaderName, HeaderMap, Request, request::Parts}, extract::{FromRequest, FromRequestParts}};
use bitflags::bitflags; use bitflags::bitflags;
use chrono::{DateTime, Utc}; use chrono::{DateTime, Utc};
use tracing::{debug, warn};
use crate::{app_state::AppState, database::Database};
use super::RepositoryVisibility; use super::RepositoryVisibility;
@ -77,61 +72,6 @@ impl UserAuth {
} }
} }
#[async_trait]
impl FromRequestParts<Arc<AppState>> for UserAuth {
type Rejection = (StatusCode, HeaderMap);
async fn from_request_parts(parts: &mut Parts, state: &Arc<AppState>) -> Result<Self, Self::Rejection> {
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());
debug!("starting UserAuth request parts");
let auth = String::from(
parts.headers
.get(header::AUTHORIZATION)
.ok_or(
{
debug!("Client did not send authorization header");
(StatusCode::UNAUTHORIZED, failure_headers.clone())
})?
.to_str()
.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
//_ if auth == "Bearer" => Ok(AuthToken(None)),
_ => 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 {
debug!("Authenticated user through middleware: {}", user.user.username);
Ok(user)
} else {
debug!("Failure to verify user token, responding with auth realm");
Err((
StatusCode::UNAUTHORIZED,
failure_headers
))
}
}
}
bitflags! { bitflags! {
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct Permission: u32 { pub struct Permission: u32 {

View File

@ -9,7 +9,6 @@ mod auth;
mod error; mod error;
use std::net::SocketAddr; use std::net::SocketAddr;
use std::path::Path;
use std::str::FromStr; use std::str::FromStr;
use std::sync::Arc; use std::sync::Arc;
@ -19,17 +18,13 @@ use axum::middleware::Next;
use axum::response::Response; use axum::response::Response;
use axum::{Router, routing}; use axum::{Router, routing};
use axum::ServiceExt; use axum::ServiceExt;
use axum_server::tls_rustls::RustlsConfig;
use lazy_static::lazy_static;
use regex::Regex;
use sqlx::ConnectOptions; use sqlx::ConnectOptions;
use tokio::fs::File;
use tower_layer::Layer; use tower_layer::Layer;
use sqlx::sqlite::{SqlitePoolOptions, SqliteConnectOptions, SqliteJournalMode}; use sqlx::sqlite::{SqlitePoolOptions, SqliteConnectOptions, SqliteJournalMode};
use tokio::sync::Mutex; use tokio::sync::Mutex;
use tower_http::normalize_path::NormalizePathLayer; use tower_http::normalize_path::NormalizePathLayer;
use tracing::{debug, Level, info}; use tracing::{debug, Level};
use app_state::AppState; use app_state::AppState;
use database::Database; use database::Database;
@ -41,14 +36,11 @@ use crate::config::{Config, DatabaseConfig, StorageConfig};
use tower_http::trace::TraceLayer; 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 /// Encode the 'name' path parameter in the url
async fn change_request_paths<B>(mut request: Request<B>, next: Next<B>) -> Result<Response, StatusCode> { async fn change_request_paths<B>(mut request: Request<B>, next: Next<B>) -> Result<Response, StatusCode> {
// Attempt to find the name using regex in the url // Attempt to find the name using regex in the url
let regex = &REGISTRY_URL_REGEX; let regex = regex::Regex::new(r"/v2/([\w/]+)/(blobs|tags|manifests)")
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
let captures = match regex.captures(request.uri().path()) { let captures = match regex.captures(request.uri().path()) {
Some(captures) => captures, Some(captures) => captures,
None => return Ok(next.run(request).await), None => return Ok(next.run(request).await),
@ -70,21 +62,16 @@ async fn change_request_paths<B>(mut request: Request<B>, next: Next<B>) -> Resu
#[tokio::main] #[tokio::main]
async fn main() -> anyhow::Result<()> { async fn main() -> anyhow::Result<()> {
tracing_subscriber::fmt()
.with_max_level(Level::DEBUG)
.init();
let config = Config::new() let config = Config::new()
.expect("Failure to parse config!"); .expect("Failure to parse config!");
tracing_subscriber::fmt()
.with_max_level(config.log_level)
.init();
let sqlite_config = match &config.database { let sqlite_config = match &config.database {
DatabaseConfig::Sqlite(sqlite) => sqlite, 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))? let connection_options = SqliteConnectOptions::from_str(&format!("sqlite://{}", &sqlite_config.path))?
.journal_mode(SqliteJournalMode::Wal); .journal_mode(SqliteJournalMode::Wal);
@ -113,10 +100,9 @@ async fn main() -> anyhow::Result<()> {
let app_addr = SocketAddr::from_str(&format!("{}:{}", config.listen_address, config.listen_port))?; 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 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 path_middleware = axum::middleware::from_fn(change_request_paths);
let app = Router::new() let app = Router::new()
@ -143,30 +129,17 @@ async fn main() -> anyhow::Result<()> {
.put(api::manifests::upload_manifest_put) .put(api::manifests::upload_manifest_put)
.head(api::manifests::manifest_exists_head) .head(api::manifests::manifest_exists_head)
.delete(api::manifests::delete_manifest)) .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) .with_state(state)
.layer(TraceLayer::new_for_http()); .layer(TraceLayer::new_for_http());
let layered_app = NormalizePathLayer::trim_trailing_slash().layer(path_middleware.layer(app)); let layered_app = NormalizePathLayer::trim_trailing_slash().layer(path_middleware.layer(app));
match tls_config { debug!("Starting http server, listening on {}", app_addr);
Some(tls) if tls.enable => { axum::Server::bind(&app_addr)
info!("Starting https server, listening on {}", app_addr); .serve(layered_app.into_make_service())
.await?;
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(()) Ok(())
} }