Compare commits
5 Commits
0546b71c5e
...
2ecebab330
Author | SHA1 | Date |
---|---|---|
SeanOMik | 2ecebab330 | |
SeanOMik | 0150a1a11e | |
SeanOMik | 603c0062ba | |
SeanOMik | 12827ca37e | |
SeanOMik | 8b59e2b32a |
|
@ -37,6 +37,12 @@ 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"
|
||||||
|
@ -113,9 +119,9 @@ checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "axum"
|
name = "axum"
|
||||||
version = "0.6.16"
|
version = "0.6.18"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "113713495a32dd0ab52baf5c10044725aa3aec00b31beda84218e469029b72a3"
|
checksum = "f8175979259124331c1d7bf6586ee7e0da434155e4b2d48ec2c8386281d8df39"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"async-trait",
|
"async-trait",
|
||||||
"axum-core",
|
"axum-core",
|
||||||
|
@ -184,6 +190,26 @@ 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"
|
||||||
|
@ -742,6 +768,25 @@ 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"
|
||||||
|
@ -849,6 +894,7 @@ dependencies = [
|
||||||
"futures-channel",
|
"futures-channel",
|
||||||
"futures-core",
|
"futures-core",
|
||||||
"futures-util",
|
"futures-util",
|
||||||
|
"h2",
|
||||||
"http",
|
"http",
|
||||||
"http-body",
|
"http-body",
|
||||||
"httparse",
|
"httparse",
|
||||||
|
@ -1256,6 +1302,7 @@ 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",
|
||||||
|
@ -1267,6 +1314,7 @@ dependencies = [
|
||||||
"hmac",
|
"hmac",
|
||||||
"jws",
|
"jws",
|
||||||
"jwt",
|
"jwt",
|
||||||
|
"lazy_static",
|
||||||
"ldap3",
|
"ldap3",
|
||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
"qstring",
|
"qstring",
|
||||||
|
@ -1572,6 +1620,18 @@ 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"
|
||||||
|
@ -1581,6 +1641,16 @@ 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"
|
||||||
|
@ -1847,7 +1917,7 @@ dependencies = [
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"paste",
|
"paste",
|
||||||
"percent-encoding",
|
"percent-encoding",
|
||||||
"rustls",
|
"rustls 0.20.8",
|
||||||
"rustls-pemfile",
|
"rustls-pemfile",
|
||||||
"sha2 0.10.6",
|
"sha2 0.10.6",
|
||||||
"smallvec",
|
"smallvec",
|
||||||
|
@ -1887,7 +1957,7 @@ checksum = "804d3f245f894e61b1e6263c84b23ca675d96753b5abfd5cc8597d86806e8024"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tokio-rustls",
|
"tokio-rustls 0.23.4",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -2083,11 +2153,21 @@ 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",
|
"rustls 0.20.8",
|
||||||
"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"
|
||||||
|
|
|
@ -32,7 +32,8 @@ 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.16"
|
axum = "0.6.18"
|
||||||
|
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" ] }
|
||||||
|
@ -52,3 +53,4 @@ 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"
|
|
@ -1,4 +1,4 @@
|
||||||
FROM rust:alpine3.14 as builder
|
FROM rust:alpine3.17 as builder
|
||||||
|
|
||||||
# update packages
|
# update packages
|
||||||
RUN apk update
|
RUN apk update
|
||||||
|
@ -34,7 +34,9 @@ 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 chown -R $UID:$GID /app
|
RUN mkdir /data && \
|
||||||
|
chown -R $UID:$GID /data && \
|
||||||
|
chown -R $UID:$GID /app
|
||||||
|
|
||||||
USER $UNAME
|
USER $UNAME
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
# Docker registry
|
# Orca 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)).
|
||||||
|
|
|
@ -2,6 +2,9 @@ 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"
|
||||||
|
|
|
@ -5,9 +5,16 @@
|
||||||
- [ ] 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
|
|
@ -179,7 +179,8 @@ 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!");
|
||||||
|
|
||||||
return Ok(unauthenticated_response(&state.config));
|
// TODO: Dont unwrap, find a way to return multiple scopes
|
||||||
|
return Ok(unauthenticated_response(&state.config, auth.scope.first().unwrap()));
|
||||||
}
|
}
|
||||||
drop(auth_driver);
|
drop(auth_driver);
|
||||||
|
|
||||||
|
|
|
@ -8,18 +8,27 @@ 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;
|
use crate::auth::{access_denied_response, unauthenticated_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>>, Extension(auth): Extension<UserAuth>) -> Result<Response, AppError> {
|
pub async fn digest_exists_head(Path((name, layer_digest)): Path<(String, String)>, state: State<Arc<AppState>>, auth: Option<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
|
||||||
let mut auth_driver = state.auth_checker.lock().await;
|
if let Some(auth) = auth {
|
||||||
if !auth_driver.user_has_permission(auth.user.username, name.clone(), Permission::PULL, Some(RepositoryVisibility::Public)).await? {
|
let mut auth_driver = state.auth_checker.lock().await;
|
||||||
return Ok(access_denied_response(&state.config));
|
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) {
|
||||||
|
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;
|
||||||
|
|
||||||
|
@ -38,13 +47,19 @@ 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>>, Extension(auth): Extension<UserAuth>) -> Result<Response, AppError> {
|
pub async fn pull_digest_get(Path((name, layer_digest)): Path<(String, String)>, state: State<Arc<AppState>>, auth: Option<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
|
||||||
let mut auth_driver = state.auth_checker.lock().await;
|
if let Some(auth) = auth {
|
||||||
if !auth_driver.user_has_permission(auth.user.username, name.clone(), Permission::PULL, Some(RepositoryVisibility::Public)).await? {
|
let mut auth_driver = state.auth_checker.lock().await;
|
||||||
return Ok(access_denied_response(&state.config));
|
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;
|
let storage = state.storage.lock().await;
|
||||||
|
|
||||||
|
|
|
@ -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>>, Extension(auth): Extension<UserAuth>, body: String) -> Result<Response, AppError> {
|
pub async fn upload_manifest_put(Path((name, reference)): Path<(String, String)>, state: State<Arc<AppState>>, auth: 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,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<Arc<AppState>>, Extension(auth): Extension<UserAuth>) -> Result<Response, AppError> {
|
pub async fn pull_manifest_get(Path((name, reference)): Path<(String, String)>, state: State<Arc<AppState>>, auth: Option<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
|
||||||
let mut auth_driver = state.auth_checker.lock().await;
|
if let Some(auth) = auth {
|
||||||
if !auth_driver.user_has_permission(auth.user.username, name.clone(), Permission::PULL, Some(RepositoryVisibility::Public)).await? {
|
let mut auth_driver = state.auth_checker.lock().await;
|
||||||
return Ok(access_denied_response(&state.config));
|
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 database = &state.database;
|
||||||
let digest = match Digest::is_digest(&reference) {
|
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())
|
).into_response())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn manifest_exists_head(Path((name, reference)): Path<(String, String)>, state: State<Arc<AppState>>, Extension(auth): Extension<UserAuth>) -> Result<Response, AppError> {
|
pub async fn manifest_exists_head(Path((name, reference)): Path<(String, String)>, state: State<Arc<AppState>>, auth: Option<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
|
||||||
let mut auth_driver = state.auth_checker.lock().await;
|
if let Some(auth) = auth {
|
||||||
if !auth_driver.user_has_permission(auth.user.username, name.clone(), Permission::PULL, Some(RepositoryVisibility::Public)).await? {
|
let mut auth_driver = state.auth_checker.lock().await;
|
||||||
return Ok(access_denied_response(&state.config));
|
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.
|
// Get the digest from the reference path.
|
||||||
let database = &state.database;
|
let database = &state.database;
|
||||||
|
@ -148,7 +161,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>>, Extension(auth): Extension<UserAuth>) -> Result<Response, AppError> {
|
pub async fn delete_manifest(Path((name, reference)): Path<(String, String)>, state: State<Arc<AppState>>, auth: 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));
|
||||||
|
|
|
@ -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};
|
use axum::http::{StatusCode, HeaderName, HeaderMap, header};
|
||||||
|
use tracing::debug;
|
||||||
|
|
||||||
use crate::app_state::AppState;
|
use crate::app_state::AppState;
|
||||||
|
|
||||||
|
@ -18,9 +18,13 @@ 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(Extension(_auth): Extension<UserAuth>, _state: State<Arc<AppState>>) -> Response {
|
pub async fn version_check(_state: State<Arc<AppState>>) -> Response {
|
||||||
|
let bearer = format!("Bearer realm=\"{}/auth\"", _state.config.url());
|
||||||
(
|
(
|
||||||
StatusCode::OK,
|
StatusCode::UNAUTHORIZED,
|
||||||
[( 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()
|
||||||
}
|
}
|
|
@ -12,13 +12,21 @@ 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;
|
use crate::auth::{access_denied_response, unauthenticated_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, )>, Extension(auth): Extension<UserAuth>, state: State<Arc<AppState>>) -> Result<Response, AppError> {
|
pub async fn start_upload_post(Path((name, )): Path<(String, )>, auth: Option<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");
|
||||||
|
@ -38,7 +46,7 @@ pub async fn start_upload_post(Path((name, )): Path<(String, )>, Extension(auth)
|
||||||
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)>, Extension(auth): Extension<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)>, auth: 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));
|
||||||
|
@ -97,7 +105,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>>, Extension(auth): Extension<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>>, auth: 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));
|
||||||
|
@ -126,7 +134,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>>, Extension(auth): Extension<UserAuth>) -> Result<Response, AppError> {
|
pub async fn cancel_upload_delete(Path((name, layer_uuid)): Path<(String, String)>, state: State<Arc<AppState>>, auth: 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));
|
||||||
|
@ -140,7 +148,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>>, Extension(auth): Extension<UserAuth>) -> Result<Response, AppError> {
|
pub async fn check_upload_status_get(Path((name, layer_uuid)): Path<(String, String)>, state: State<Arc<AppState>>, auth: 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));
|
||||||
|
|
|
@ -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}, config::Config};
|
use crate::{app_state::AppState, dto::{user::{Permission, RegistryUserType}, RepositoryVisibility, scope::{self, Scope}}, config::Config};
|
||||||
use crate::database::Database;
|
use crate::database::Database;
|
||||||
|
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
|
@ -133,14 +133,17 @@ 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) -> Response {
|
pub fn unauthenticated_response(config: &Config, scope: &Scope) -> Response {
|
||||||
let bearer = format!("Bearer realm=\"{}/auth\"", config.url());
|
let bearer = format!("Bearer realm=\"{}/auth\",service=\"{}\",scope=\"{}\"", config.url(), "localhost:3000", scope);
|
||||||
|
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()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
|
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;
|
use serde::{Deserialize, Deserializer};
|
||||||
|
use tracing::Level;
|
||||||
|
|
||||||
use std::env;
|
use std::env;
|
||||||
|
|
||||||
|
@ -45,6 +47,13 @@ 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 {
|
||||||
|
@ -56,9 +65,12 @@ 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)]
|
||||||
|
@ -102,4 +114,30 @@ 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
|
|
@ -410,9 +410,20 @@ 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)?)
|
||||||
}
|
}
|
||||||
|
|
|
@ -71,7 +71,7 @@ CREATE TABLE IF NOT EXISTS user_tokens (
|
||||||
created_at BIGINT NOT NULL
|
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 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);
|
INSERT OR IGNORE INTO user_registry_permissions (email, user_type) VALUES ('admin@example.com', 1);
|
|
@ -19,7 +19,7 @@ impl fmt::Display for ScopeType {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Default, Debug)]
|
#[derive(Default, Debug, Clone)]
|
||||||
pub enum Action {
|
pub enum Action {
|
||||||
#[default]
|
#[default]
|
||||||
None,
|
None,
|
||||||
|
@ -44,6 +44,16 @@ 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
|
||||||
|
|
|
@ -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 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;
|
||||||
|
|
||||||
|
@ -72,6 +77,61 @@ 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 {
|
||||||
|
|
53
src/main.rs
53
src/main.rs
|
@ -9,6 +9,7 @@ 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;
|
||||||
|
|
||||||
|
@ -18,13 +19,17 @@ 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};
|
use tracing::{debug, Level, info};
|
||||||
|
|
||||||
use app_state::AppState;
|
use app_state::AppState;
|
||||||
use database::Database;
|
use database::Database;
|
||||||
|
@ -36,11 +41,14 @@ 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 = regex::Regex::new(r"/v2/([\w/]+)/(blobs|tags|manifests)")
|
let regex = ®ISTRY_URL_REGEX;
|
||||||
.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),
|
||||||
|
@ -62,16 +70,21 @@ 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);
|
||||||
|
@ -100,9 +113,10 @@ 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()
|
||||||
|
@ -129,17 +143,30 @@ 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));
|
||||||
|
|
||||||
debug!("Starting http server, listening on {}", app_addr);
|
match tls_config {
|
||||||
axum::Server::bind(&app_addr)
|
Some(tls) if tls.enable => {
|
||||||
.serve(layered_app.into_make_service())
|
info!("Starting https server, listening on {}", app_addr);
|
||||||
.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(())
|
||||||
}
|
}
|
Loading…
Reference in New Issue