Implement user auth for pushing and pulling images

This commit is contained in:
SeanOMik 2023-05-06 19:24:03 -04:00
parent 5f74e46607
commit e3a0554823
Signed by: SeanOMik
GPG Key ID: 568F326C7EB33ACB
17 changed files with 555 additions and 98 deletions

17
Cargo.lock generated
View File

@ -119,7 +119,7 @@ checksum = "113713495a32dd0ab52baf5c10044725aa3aec00b31beda84218e469029b72a3"
dependencies = [ dependencies = [
"async-trait", "async-trait",
"axum-core", "axum-core",
"bitflags", "bitflags 1.3.2",
"bytes", "bytes",
"futures-util", "futures-util",
"http", "http",
@ -215,6 +215,12 @@ version = "1.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
[[package]]
name = "bitflags"
version = "2.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "24a6904aef64d73cf10ab17ebace7befb918b82164785cb89907993be7f83813"
[[package]] [[package]]
name = "block-buffer" name = "block-buffer"
version = "0.9.0" version = "0.9.0"
@ -311,7 +317,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "389ca505fd2c00136e0d0cd34bcd8b6bd0b59d5779aab396054b716334230c1c" checksum = "389ca505fd2c00136e0d0cd34bcd8b6bd0b59d5779aab396054b716334230c1c"
dependencies = [ dependencies = [
"atty", "atty",
"bitflags", "bitflags 1.3.2",
"clap_derive", "clap_derive",
"clap_lex", "clap_lex",
"once_cell", "once_cell",
@ -1077,6 +1083,7 @@ dependencies = [
"axum-auth", "axum-auth",
"axum-macros", "axum-macros",
"bcrypt", "bcrypt",
"bitflags 2.2.1",
"bytes", "bytes",
"chrono", "chrono",
"clap", "clap",
@ -1329,7 +1336,7 @@ version = "0.2.16"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a"
dependencies = [ dependencies = [
"bitflags", "bitflags 1.3.2",
] ]
[[package]] [[package]]
@ -1594,7 +1601,7 @@ checksum = "fa8241483a83a3f33aa5fff7e7d9def398ff9990b2752b6c6112b83c6d246029"
dependencies = [ dependencies = [
"ahash", "ahash",
"atoi", "atoi",
"bitflags", "bitflags 1.3.2",
"byteorder", "byteorder",
"bytes", "bytes",
"crc", "crc",
@ -1892,7 +1899,7 @@ version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5d1d42a9b3f3ec46ba828e8d376aec14592ea199f70a06a548587ecd1c4ab658" checksum = "5d1d42a9b3f3ec46ba828e8d376aec14592ea199f70a06a548587ecd1c4ab658"
dependencies = [ dependencies = [
"bitflags", "bitflags 1.3.2",
"bytes", "bytes",
"futures-core", "futures-core",
"futures-util", "futures-util",

View File

@ -50,3 +50,4 @@ hmac = "0.12.1"
sha2 = "0.10.6" sha2 = "0.10.6"
rand = "0.8.5" rand = "0.8.5"
bcrypt = "0.14.0" bcrypt = "0.14.0"
bitflags = "2.2.1"

23
docs/database.md Normal file
View File

@ -0,0 +1,23 @@
## user_permissions table
The field `repository_custom_scope` is an integer created by using bitwise operations.
* pull - `0b0001`
* push - `0b0010`
* edit - `0b0111`
* admin - `0b1111`
### Predefined user permission scopes:
* limited
* pull image
* developer
* pull and push image
* master
* retag images
* project_admin
* configure repository access
## user_registry_permissions
user_type:
* regular user = 0
* admin = 1

View File

@ -2,7 +2,7 @@ use std::{sync::Arc, collections::{HashMap, BTreeMap}, time::{SystemTime, UNIX_E
use axum::{extract::{Query, State}, response::{IntoResponse, Response}, http::{StatusCode, header}, Form}; use axum::{extract::{Query, State}, response::{IntoResponse, Response}, http::{StatusCode, header}, Form};
use axum_auth::AuthBasic; use axum_auth::AuthBasic;
use chrono::{DateTime, Utc}; use chrono::{DateTime, Utc, Duration};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use tracing::{debug, error, info, span, Level}; use tracing::{debug, error, info, span, Level};
@ -12,7 +12,7 @@ use sha2::Sha256;
use rand::Rng; use rand::Rng;
use crate::{dto::scope::Scope, app_state::AppState}; use crate::{dto::{scope::Scope, user::{UserAuth, TokenInfo}}, app_state::AppState};
use crate::database::Database; use crate::database::Database;
#[derive(Deserialize, Debug)] #[derive(Deserialize, Debug)]
@ -41,13 +41,12 @@ pub struct AuthResponse {
issued_at: String, issued_at: String,
} }
fn create_jwt_token(account: String) -> anyhow::Result<String> { /// In the returned UserToken::user, only the username is specified
fn create_jwt_token(account: &str) -> anyhow::Result<TokenInfo> {
let key: Hmac<Sha256> = Hmac::new_from_slice(b"some-secret")?; let key: Hmac<Sha256> = Hmac::new_from_slice(b"some-secret")?;
let now = SystemTime::now(); let now = chrono::offset::Utc::now();
let now_secs = now let now_secs = now.timestamp();
.duration_since(UNIX_EPOCH)?
.as_secs();
// Construct the claims for the token // Construct the claims for the token
let mut claims = BTreeMap::new(); let mut claims = BTreeMap::new();
@ -55,19 +54,23 @@ fn create_jwt_token(account: String) -> anyhow::Result<String> {
claims.insert("subject", &account); claims.insert("subject", &account);
//claims.insert("audience", auth.service); //claims.insert("audience", auth.service);
let notbefore = format!("{}", now_secs - 10); let not_before = format!("{}", now_secs - 10);
let issuedat = format!("{}", now_secs); let issued_at = format!("{}", now_secs);
let expiration = format!("{}", now_secs + 20); let expiration = format!("{}", now_secs + 20);
claims.insert("notbefore", &notbefore); claims.insert("notbefore", &not_before);
claims.insert("issuedat", &issuedat); claims.insert("issuedat", &issued_at);
claims.insert("expiration", &expiration); // TODO: 20 seconds expiry for testing claims.insert("expiration", &expiration); // TODO: 20 seconds expiry for testing
let issued_at = now;
let expiration = now + Duration::seconds(20);
// Create a randomized jwtid // Create a randomized jwtid
let mut rng = rand::thread_rng(); let mut rng = rand::thread_rng();
let jwtid = format!("{}", rng.gen::<u64>()); let jwtid = format!("{}", rng.gen::<u64>());
claims.insert("jwtid", &jwtid); claims.insert("jwtid", &jwtid);
Ok(claims.sign_with_key(&key)?) let token_str = claims.sign_with_key(&key)?;
Ok(TokenInfo::new(token_str, expiration, issued_at))
} }
pub async fn auth_basic_get(basic_auth: Option<AuthBasic>, state: State<Arc<AppState>>, Query(params): Query<HashMap<String, String>>, form: Option<Form<AuthForm>>) -> Response { pub async fn auth_basic_get(basic_auth: Option<AuthBasic>, state: State<Arc<AppState>>, Query(params): Query<HashMap<String, String>>, form: Option<Form<AuthForm>>) -> Response {
@ -142,6 +145,7 @@ pub async fn auth_basic_get(basic_auth: Option<AuthBasic>, state: State<Arc<AppS
// Process all the scopes // Process all the scopes
if let Some(scope) = params.get("scope") { if let Some(scope) = params.get("scope") {
// TODO: Handle multiple scopes // TODO: Handle multiple scopes
auth.scope.push(Scope::try_from(&scope[..]).unwrap()); auth.scope.push(Scope::try_from(&scope[..]).unwrap());
} }
@ -159,7 +163,7 @@ pub async fn auth_basic_get(basic_auth: Option<AuthBasic>, state: State<Arc<AppS
debug!("Constructed auth request"); debug!("Constructed auth request");
if let (Some(account), Some(password)) = (auth.account, auth.password) { if let (Some(account), Some(password)) = (&auth.account, auth.password) {
// Ensure that the password is correct // Ensure that the password is correct
let database = &state.database; let database = &state.database;
if !database.verify_user_login(account.clone(), password).await.unwrap() { if !database.verify_user_login(account.clone(), password).await.unwrap() {
@ -168,11 +172,11 @@ pub async fn auth_basic_get(basic_auth: Option<AuthBasic>, state: State<Arc<AppS
StatusCode::UNAUTHORIZED StatusCode::UNAUTHORIZED
).into_response(); ).into_response();
} }
drop(database);
debug!("User password is correct"); debug!("User password is correct");
let now = SystemTime::now(); let now = SystemTime::now();
let token_str = create_jwt_token(account).unwrap(); let token = create_jwt_token(account).unwrap();
let token_str = token.token;
debug!("Created jwt token"); debug!("Created jwt token");
@ -189,8 +193,8 @@ pub async fn auth_basic_get(basic_auth: Option<AuthBasic>, state: State<Arc<AppS
let json_str = serde_json::to_string(&auth_response).unwrap(); let json_str = serde_json::to_string(&auth_response).unwrap();
let mut auth_storage = state.auth_storage.lock().await; database.store_user_token(token_str.clone(), account.clone(), token.expiry, token.created_at).await.unwrap();
auth_storage.valid_tokens.insert(token_str.clone()); drop(database);
return ( return (
StatusCode::OK, StatusCode::OK,

View File

@ -1,5 +1,6 @@
use std::sync::Arc; use std::sync::Arc;
use axum::Extension;
use axum::body::StreamBody; use axum::body::StreamBody;
use axum::extract::{State, Path}; use axum::extract::{State, Path};
use axum::http::{StatusCode, header, HeaderName}; use axum::http::{StatusCode, header, HeaderName};
@ -7,8 +8,23 @@ 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_storage::{does_user_have_permission, get_unauthenticated_response};
use crate::database::Database;
use crate::dto::RepositoryVisibility;
use crate::dto::user::{Permission, RegistryUserType, UserAuth};
pub async fn digest_exists_head(Path((name, layer_digest)): Path<(String, String)>, state: State<Arc<AppState>>, Extension(auth): Extension<UserAuth>) -> Response {
// Check if the user has permission to pull, or that the repository is public
let database = &state.database;
if !does_user_have_permission(database, auth.user.username, name.clone(), Permission::PULL).await.unwrap()
&& !database.get_repository_visibility(&name).await.unwrap()
.and_then(|v| Some(v == RepositoryVisibility::Public))
.unwrap_or_else(|| false) {
return get_unauthenticated_response(&state.config);
}
drop(database);
pub async fn digest_exists_head(Path((_name, layer_digest)): Path<(String, String)>, state: State<Arc<AppState>>) -> Response {
let storage = state.storage.lock().await; let storage = state.storage.lock().await;
if storage.has_digest(&layer_digest).await.unwrap() { if storage.has_digest(&layer_digest).await.unwrap() {
@ -26,7 +42,18 @@ pub async fn digest_exists_head(Path((_name, layer_digest)): Path<(String, Strin
StatusCode::NOT_FOUND.into_response() StatusCode::NOT_FOUND.into_response()
} }
pub async fn pull_digest_get(Path((_name, layer_digest)): Path<(String, String)>, state: State<Arc<AppState>>) -> Response { pub async fn pull_digest_get(Path((name, layer_digest)): Path<(String, String)>, state: State<Arc<AppState>>, Extension(auth): Extension<UserAuth>) -> Response {
// Check if the user has permission to pull, or that the repository is public
let database = &state.database;
if !does_user_have_permission(database, auth.user.username, name.clone(), Permission::PULL).await.unwrap()
&& !database.get_repository_visibility(&name).await.unwrap()
.and_then(|v| Some(v == RepositoryVisibility::Public))
.unwrap_or_else(|| false) {
return get_unauthenticated_response(&state.config);
}
drop(database);
let storage = state.storage.lock().await; let storage = state.storage.lock().await;
if let Some(len) = storage.digest_length(&layer_digest).await.unwrap() { if let Some(len) = storage.digest_length(&layer_digest).await.unwrap() {

View File

@ -36,7 +36,7 @@ pub async fn list_repositories(Query(params): Query<ListRepositoriesParams>, sta
let last_repo = repos.last().and_then(|s| Some(s.clone())); let last_repo = repos.last().and_then(|s| Some(s.clone()));
// Construct the link header // Construct the link header
let url = &state.config.url; let url = &state.config.get_url();
let mut url = format!("<{}/v2/_catalog?n={}", url, limit); let mut url = format!("<{}/v2/_catalog?n={}", url, limit);
if let Some(last_repo) = last_repo { if let Some(last_repo) = last_repo {
url += &format!("&limit={}", last_repo); url += &format!("&limit={}", last_repo);

View File

@ -1,26 +1,33 @@
use std::sync::Arc; use std::sync::Arc;
use axum::Extension;
use axum::extract::{Path, State}; use axum::extract::{Path, State};
use axum::response::{Response, IntoResponse}; use axum::response::{Response, IntoResponse};
use axum::http::{StatusCode, HeaderMap, HeaderName, header}; use axum::http::{StatusCode, HeaderMap, HeaderName, header};
use tracing::log::warn; use tracing::log::warn;
use tracing::{debug, info}; use tracing::{debug, info};
use crate::auth_storage::{does_user_have_permission, get_unauthenticated_response};
use crate::app_state::AppState; use crate::app_state::AppState;
use crate::database::Database; use crate::database::Database;
use crate::dto::RepositoryVisibility;
use crate::dto::digest::Digest; use crate::dto::digest::Digest;
use crate::dto::manifest::Manifest; use crate::dto::manifest::Manifest;
use crate::dto::user::{UserAuth, Permission};
pub async fn upload_manifest_put(Path((name, reference)): Path<(String, String)>, state: State<Arc<AppState>>, Extension(auth): Extension<UserAuth>, body: String) -> Response {
if !does_user_have_permission(&state.database, auth.user.username, name.clone(), Permission::PUSH).await.unwrap() {
return get_unauthenticated_response(&state.config);
}
pub async fn upload_manifest_put(Path((name, reference)): Path<(String, String)>, state: State<Arc<AppState>>, body: String) -> Response {
// Calculate the sha256 digest for the manifest. // Calculate the sha256 digest for the manifest.
let calculated_hash = sha256::digest(body.clone()); let calculated_hash = sha256::digest(body.clone());
let calculated_digest = format!("sha256:{}", calculated_hash); let calculated_digest = format!("sha256:{}", calculated_hash);
let database = &state.database; let database = &state.database;
// Create the image repository and save the image manifest. // Create the image repository and save the image manifest. This repository will be private by default
database.save_repository(&name).await.unwrap(); database.save_repository(&name, RepositoryVisibility::Private, None).await.unwrap();
database.save_manifest(&name, &calculated_digest, &body).await.unwrap(); database.save_manifest(&name, &calculated_digest, &body).await.unwrap();
// If the reference is not a digest, then it must be a tag name. // If the reference is not a digest, then it must be a tag name.
@ -54,12 +61,23 @@ 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>>) -> Response { pub async fn pull_manifest_get(Path((name, reference)): Path<(String, String)>, state: State<Arc<AppState>>, Extension(auth): Extension<UserAuth>) -> Response {
// Check if the user has permission to pull, or that the repository is public
let database = &state.database;
if !does_user_have_permission(database, auth.user.username, name.clone(), Permission::PULL).await.unwrap()
&& !database.get_repository_visibility(&name).await.unwrap()
.and_then(|v| Some(v == RepositoryVisibility::Public))
.unwrap_or_else(|| false) {
return get_unauthenticated_response(&state.config);
}
drop(database);
let database = &state.database; let database = &state.database;
let digest = match Digest::is_digest(&reference) { let digest = match Digest::is_digest(&reference) {
true => reference.clone(), true => reference.clone(),
false => { false => {
debug!("Attempting to get manifest digest using tag (name={}, reference={})", name, reference); debug!("Attempting to get manifest digest using tag (repository={}, reference={})", name, reference);
if let Some(tag) = database.get_tag(&name, &reference).await.unwrap() { if let Some(tag) = database.get_tag(&name, &reference).await.unwrap() {
tag.manifest_digest tag.manifest_digest
} else { } else {
@ -90,7 +108,18 @@ 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>>) -> Response { pub async fn manifest_exists_head(Path((name, reference)): Path<(String, String)>, state: State<Arc<AppState>>, Extension(auth): Extension<UserAuth>) -> Response {
// Check if the user has permission to pull, or that the repository is public
let database = &state.database;
if !does_user_have_permission(database, auth.user.username, name.clone(), Permission::PULL).await.unwrap()
&& !database.get_repository_visibility(&name).await.unwrap()
.and_then(|v| Some(v == RepositoryVisibility::Public))
.unwrap_or_else(|| false) {
return get_unauthenticated_response(&state.config);
}
drop(database);
// Get the digest from the reference path. // Get the digest from the reference path.
let database = &state.database; let database = &state.database;
let digest = match Digest::is_digest(&reference) { let digest = match Digest::is_digest(&reference) {
@ -124,7 +153,11 @@ 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)>, headers: HeaderMap, state: State<Arc<AppState>>) -> Response { pub async fn delete_manifest(Path((name, reference)): Path<(String, String)>, headers: HeaderMap, state: State<Arc<AppState>>, Extension(auth): Extension<UserAuth>) -> Response {
if !does_user_have_permission(&state.database, auth.user.username, name.clone(), Permission::PUSH).await.unwrap() {
return get_unauthenticated_response(&state.config);
}
let _authorization = headers.get("Authorization").unwrap(); // TODO: use authorization header let _authorization = headers.get("Authorization").unwrap(); // TODO: use authorization header
let database = &state.database; let database = &state.database;

View File

@ -15,10 +15,11 @@ pub mod catalog;
pub mod auth; pub mod auth;
use crate::auth_storage::AuthToken; use crate::auth_storage::AuthToken;
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(AuthToken(_token)): Extension<AuthToken>, _state: State<Arc<AppState>>) -> Response { pub async fn version_check(Extension(_auth): Extension<UserAuth>, _state: State<Arc<AppState>>) -> Response {
( (
StatusCode::OK, StatusCode::OK,
[( HeaderName::from_static("docker-distribution-api-version"), "registry/2.0" )] [( HeaderName::from_static("docker-distribution-api-version"), "registry/2.0" )]

View File

@ -37,7 +37,7 @@ pub async fn list_tags(Path((name, )): Path<(String, )>, Query(params): Query<Li
let last_tag = tags.last(); let last_tag = tags.last();
// Construct the link header // Construct the link header
let url = &state.config.url; let url = &state.config.get_url();
let mut url = format!("<{}/v2/{}/tags/list?n={}", url, name, limit); let mut url = format!("<{}/v2/{}/tags/list?n={}", url, name, limit);
if let Some(last_tag) = last_tag { if let Some(last_tag) = last_tag {
url += &format!("&limit={}", last_tag.name); url += &format!("&limit={}", last_tag.name);

View File

@ -2,6 +2,7 @@ use std::collections::HashMap;
use std::io::ErrorKind; use std::io::ErrorKind;
use std::sync::Arc; use std::sync::Arc;
use axum::Extension;
use axum::http::{StatusCode, header, HeaderName}; use axum::http::{StatusCode, header, HeaderName};
use axum::extract::{Path, BodyStream, State, Query}; use axum::extract::{Path, BodyStream, State, Query};
use axum::response::{IntoResponse, Response}; use axum::response::{IntoResponse, Response};
@ -11,11 +12,15 @@ use futures::StreamExt;
use tracing::{debug, warn}; use tracing::{debug, warn};
use crate::app_state::AppState; use crate::app_state::AppState;
use crate::auth_storage::{does_user_have_permission, get_unauthenticated_response};
use crate::byte_stream::ByteStream; use crate::byte_stream::ByteStream;
use crate::database::Database;
use crate::dto::user::{UserAuth, Permission, RegistryUser, RegistryUserType};
/// Starting an upload /// Starting an upload
pub async fn start_upload_post(Path((name, )): Path<(String, )>) -> impl IntoResponse { pub async fn start_upload_post(Path((name, )): Path<(String, )>, Extension(auth): Extension<UserAuth>, state: State<Arc<AppState>>) -> Response {
debug!("Upload starting"); if does_user_have_permission(&state.database, auth.user.username, name.clone(), Permission::PUSH).await.unwrap() {
debug!("Upload requested");
let uuid = uuid::Uuid::new_v4(); let uuid = uuid::Uuid::new_v4();
debug!("Requesting upload of image {}, generated uuid: {}", name, uuid); debug!("Requesting upload of image {}, generated uuid: {}", name, uuid);
@ -23,13 +28,20 @@ pub async fn start_upload_post(Path((name, )): Path<(String, )>) -> impl IntoRes
let location = format!("/v2/{}/blobs/uploads/{}", name, uuid.to_string()); let location = format!("/v2/{}/blobs/uploads/{}", name, uuid.to_string());
debug!("Constructed upload url: {}", location); debug!("Constructed upload url: {}", location);
( return (
StatusCode::ACCEPTED, StatusCode::ACCEPTED,
[ (header::LOCATION, location) ] [ (header::LOCATION, location) ]
) ).into_response();
}
get_unauthenticated_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) -> Response {
if !does_user_have_permission(&state.database, auth.user.username, name.clone(), Permission::PUSH).await.unwrap() {
return get_unauthenticated_response(&state.config);
} }
pub async fn chunked_upload_layer_patch(Path((name, layer_uuid)): Path<(String, String)>, state: State<Arc<AppState>>, mut body: BodyStream) -> Response {
let storage = state.storage.lock().await; let storage = state.storage.lock().await;
let current_size = storage.digest_length(&layer_uuid).await.unwrap(); let current_size = storage.digest_length(&layer_uuid).await.unwrap();
@ -70,7 +82,7 @@ pub async fn chunked_upload_layer_patch(Path((name, layer_uuid)): Path<(String,
(0, written_size) (0, written_size)
}; };
let full_uri = format!("{}/v2/{}/blobs/uploads/{}", &state.config.url, name, layer_uuid); let full_uri = format!("{}/v2/{}/blobs/uploads/{}", state.config.get_url(), name, layer_uuid);
( (
StatusCode::ACCEPTED, StatusCode::ACCEPTED,
[ [
@ -82,7 +94,11 @@ 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>>, state: State<Arc<AppState>>, body: Bytes) -> impl IntoResponse { 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) -> Response {
if !does_user_have_permission(&state.database, auth.user.username, name.clone(), Permission::PUSH).await.unwrap() {
return get_unauthenticated_response(&state.config);
}
let digest = query.get("digest").unwrap(); let digest = query.get("digest").unwrap();
let storage = state.storage.lock().await; let storage = state.storage.lock().await;
@ -102,18 +118,26 @@ pub async fn finish_chunked_upload_put(Path((name, layer_uuid)): Path<(String, S
(header::CONTENT_LENGTH, "0".to_string()), (header::CONTENT_LENGTH, "0".to_string()),
(HeaderName::from_static("docker-upload-digest"), digest.to_owned()) (HeaderName::from_static("docker-upload-digest"), digest.to_owned())
] ]
) ).into_response()
}
pub async fn cancel_upload_delete(Path((name, layer_uuid)): Path<(String, String)>, state: State<Arc<AppState>>, Extension(auth): Extension<UserAuth>) -> Response {
if !does_user_have_permission(&state.database, auth.user.username, name.clone(), Permission::PUSH).await.unwrap() {
return get_unauthenticated_response(&state.config);
} }
pub async fn cancel_upload_delete(Path((_name, layer_uuid)): Path<(String, String)>, state: State<Arc<AppState>>) -> impl IntoResponse {
let storage = state.storage.lock().await; let storage = state.storage.lock().await;
storage.delete_digest(&layer_uuid).await.unwrap(); storage.delete_digest(&layer_uuid).await.unwrap();
// I'm not sure what this response should be, its not specified in the registry spec. // I'm not sure what this response should be, its not specified in the registry spec.
StatusCode::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>) -> Response {
if !does_user_have_permission(&state.database, auth.user.username, name.clone(), Permission::PUSH).await.unwrap() {
return get_unauthenticated_response(&state.config);
} }
pub async fn check_upload_status_get(Path((name, layer_uuid)): Path<(String, String)>, state: State<Arc<AppState>>) -> impl IntoResponse {
let storage = state.storage.lock().await; let storage = state.storage.lock().await;
let ending = storage.digest_length(&layer_uuid).await.unwrap().unwrap_or(0); let ending = storage.digest_length(&layer_uuid).await.unwrap().unwrap_or(0);
@ -124,5 +148,5 @@ pub async fn check_upload_status_get(Path((name, layer_uuid)): Path<(String, Str
(header::RANGE, format!("0-{}", ending)), (header::RANGE, format!("0-{}", ending)),
(HeaderName::from_static("docker-upload-digest"), layer_uuid) (HeaderName::from_static("docker-upload-digest"), layer_uuid)
] ]
) ).into_response()
} }

View File

@ -1,10 +1,11 @@
use std::{collections::HashSet, ops::Deref, sync::Arc}; use std::{collections::HashSet, ops::Deref, sync::Arc};
use axum::{extract::State, http::{StatusCode, HeaderMap, header, HeaderName, Request}, middleware::Next, response::{Response, IntoResponse}}; use axum::{extract::{State, Path}, http::{StatusCode, HeaderMap, header, HeaderName, Request}, middleware::Next, response::{Response, IntoResponse}};
use tracing::debug; use tracing::debug;
use crate::app_state::AppState; use crate::{app_state::AppState, dto::user::{Permission, RegistryUserType}, config::Config};
use crate::database::Database;
/// Temporary struct for storing auth information in memory. /// Temporary struct for storing auth information in memory.
pub struct MemoryAuthStorage { pub struct MemoryAuthStorage {
@ -33,7 +34,7 @@ impl Deref for AuthToken {
type Rejection = (StatusCode, HeaderMap); type Rejection = (StatusCode, HeaderMap);
pub async fn require_auth<B>(State(state): State<Arc<AppState>>, mut request: Request<B>, next: Next<B>) -> Result<Response, Rejection> { pub async fn require_auth<B>(State(state): State<Arc<AppState>>, mut request: Request<B>, next: Next<B>) -> Result<Response, Rejection> {
let bearer = format!("Bearer realm=\"http://localhost:3000/auth\""); let bearer = format!("Bearer realm=\"{}/auth\"", state.config.get_url());
let mut failure_headers = HeaderMap::new(); let mut failure_headers = HeaderMap::new();
failure_headers.append(header::WWW_AUTHENTICATE, bearer.parse().unwrap()); failure_headers.append(header::WWW_AUTHENTICATE, bearer.parse().unwrap());
failure_headers.append(HeaderName::from_static("docker-distribution-api-version"), "registry/2.0".parse().unwrap()); failure_headers.append(HeaderName::from_static("docker-distribution-api-version"), "registry/2.0".parse().unwrap());
@ -43,7 +44,7 @@ pub async fn require_auth<B>(State(state): State<Arc<AppState>>, mut request: Re
.ok_or((StatusCode::UNAUTHORIZED, failure_headers.clone()))? .ok_or((StatusCode::UNAUTHORIZED, failure_headers.clone()))?
.to_str() .to_str()
.map_err(|_| (StatusCode::UNAUTHORIZED, failure_headers.clone()))? .map_err(|_| (StatusCode::UNAUTHORIZED, failure_headers.clone()))?
); // TODO: Don't unwrap );
let token = match auth.split_once(' ') { let token = match auth.split_once(' ') {
Some((auth, token)) if auth == "Bearer" => token, Some((auth, token)) if auth == "Bearer" => token,
@ -53,23 +54,46 @@ pub async fn require_auth<B>(State(state): State<Arc<AppState>>, mut request: Re
}; };
// If the token is not valid, return an unauthorized response // If the token is not valid, return an unauthorized response
let auth_storage = state.auth_storage.lock().await; let database = &state.database;
if !auth_storage.valid_tokens.contains(token) { if let Some(user) = database.verify_user_token(token.to_string()).await.unwrap() {
let bearer = format!("Bearer realm=\"http://localhost:3000/auth\""); debug!("Authenticated user through middleware: {}", user.user.username);
return Ok((
request.extensions_mut().insert(user);
Ok(next.run(request).await)
} else {
let bearer = format!("Bearer realm=\"{}/auth\"", state.config.get_url());
Ok((
StatusCode::UNAUTHORIZED, StatusCode::UNAUTHORIZED,
[ [
( header::WWW_AUTHENTICATE, bearer ), ( header::WWW_AUTHENTICATE, bearer ),
( HeaderName::from_static("docker-distribution-api-version"), "registry/2.0".to_string() ) ( HeaderName::from_static("docker-distribution-api-version"), "registry/2.0".to_string() )
] ]
).into_response()); ).into_response())
} else {
debug!("Client successfully authenticated!");
} }
drop(auth_storage); }
request.extensions_mut().insert(AuthToken(String::from(token))); pub async fn does_user_have_permission(database: &impl Database, username: String, repository: String, permission: Permission) -> sqlx::Result<bool> {
let allowed_to = {
Ok(next.run(request).await) match database.get_user_registry_type(username.clone()).await.unwrap() {
Some(RegistryUserType::Admin) => true,
_ => match database.get_user_repo_permissions(username, repository).await.unwrap() {
Some(perms) => if perms.has_permission(permission) { true } else { false },
_ => false,
}
}
};
Ok(allowed_to)
}
pub fn get_unauthenticated_response(config: &Config) -> Response {
let bearer = format!("Bearer realm=\"{}/auth\"", config.get_url());
(
StatusCode::UNAUTHORIZED,
[
( header::WWW_AUTHENTICATE, bearer ),
( HeaderName::from_static("docker-distribution-api-version"), "registry/2.0".to_string() )
]
).into_response()
} }

View File

@ -8,7 +8,7 @@ use std::env;
pub struct Config { pub struct Config {
pub listen_address: String, pub listen_address: String,
pub listen_port: String, pub listen_port: String,
pub url: String, pub url: Option<String>,
} }
#[allow(dead_code)] #[allow(dead_code)]
@ -37,11 +37,19 @@ impl Config {
.join(Toml::file(format!("{}", path))); .join(Toml::file(format!("{}", path)));
let mut config: Config = figment.extract()?; let mut config: Config = figment.extract()?;
if let Some(url) = config.url.as_mut() {
if config.url.ends_with("/") { if url.ends_with("/") {
config.url = config.url[..config.url.len() - 1].to_string(); *url = url[..url.len() - 1].to_string();
}
} }
Ok(config) Ok(config)
} }
pub fn get_url(&self) -> String {
match &self.url {
Some(u) => u.clone(),
None => format!("http://{}:{}", self.listen_address, self.listen_port)
}
}
} }

View File

@ -2,9 +2,9 @@ use async_trait::async_trait;
use sqlx::{Sqlite, Pool}; use sqlx::{Sqlite, Pool};
use tracing::debug; use tracing::debug;
use chrono::{DateTime, Utc, NaiveDateTime}; use chrono::{DateTime, Utc, NaiveDateTime, TimeZone};
use crate::dto::{Tag, user::User}; use crate::dto::{Tag, user::{User, RepositoryPermissions, RegistryUserType, Permission, UserAuth, TokenInfo}, RepositoryVisibility};
#[async_trait] #[async_trait]
pub trait Database { pub trait Database {
@ -40,8 +40,10 @@ pub trait Database {
// Repository related functions // Repository related functions
async fn has_repository(&self, repository: &str) -> sqlx::Result<bool>;
async fn get_repository_visibility(&self, repository: &str) -> anyhow::Result<Option<RepositoryVisibility>>;
/// Create a repository /// Create a repository
async fn save_repository(&self, repository: &str) -> sqlx::Result<()>; async fn save_repository(&self, repository: &str, visibility: RepositoryVisibility, owning_project: Option<String>) -> sqlx::Result<()>;
/// List all repositories. /// List all repositories.
/// If limit is not specified, a default limit of 1000 will be returned. /// If limit is not specified, a default limit of 1000 will be returned.
async fn list_repositories(&self, limit: Option<u32>, last_repo: Option<String>) -> sqlx::Result<Vec<String>>; async fn list_repositories(&self, limit: Option<u32>, last_repo: Option<String>) -> sqlx::Result<Vec<String>>;
@ -50,6 +52,11 @@ pub trait Database {
/// User stuff /// User stuff
async fn create_user(&self, username: String, email: String, password_hash: String, password_salt: String) -> sqlx::Result<User>; async fn create_user(&self, username: String, email: String, password_hash: String, password_salt: String) -> sqlx::Result<User>;
async fn verify_user_login(&self, username: String, password: String) -> anyhow::Result<bool>; async fn verify_user_login(&self, username: String, password: String) -> anyhow::Result<bool>;
async fn get_user_registry_type(&self, username: String) -> anyhow::Result<Option<RegistryUserType>>;
async fn get_user_repo_permissions(&self, username: String, repository: String) -> anyhow::Result<Option<RepositoryPermissions>>;
async fn get_user_registry_usertype(&self, username: String) -> anyhow::Result<Option<RegistryUserType>>;
async fn store_user_token(&self, token: String, username: String, expiry: DateTime<Utc>, created_at: DateTime<Utc>) -> anyhow::Result<()>;
async fn verify_user_token(&self, token: String) -> anyhow::Result<Option<UserAuth>>;
} }
#[async_trait] #[async_trait]
@ -127,6 +134,7 @@ impl Database for Pool<Sqlite> {
} }
async fn get_tag(&self, repository: &str, tag: &str) -> sqlx::Result<Option<Tag>> { async fn get_tag(&self, repository: &str, tag: &str) -> sqlx::Result<Option<Tag>> {
debug!("get tag");
let row: (String, i64, ) = match sqlx::query_as("SELECT image_manifest, last_updated FROM image_tags WHERE name = ? AND repository = ?") let row: (String, i64, ) = match sqlx::query_as("SELECT image_manifest, last_updated FROM image_tags WHERE name = ? AND repository = ?")
.bind(tag) .bind(tag)
.bind(repository) .bind(repository)
@ -159,7 +167,7 @@ impl Database for Pool<Sqlite> {
} }
async fn delete_tag(&self, repository: &str, tag: &str) -> sqlx::Result<()> { async fn delete_tag(&self, repository: &str, tag: &str) -> sqlx::Result<()> {
sqlx::query("DELETE FROM image_tags WHERE name = ? AND repository = ?") sqlx::query("DELETE FROM image_tags WHERE 'name' = ? AND repository = ?")
.bind(tag) .bind(tag)
.bind(repository) .bind(repository)
.execute(self).await?; .execute(self).await?;
@ -224,10 +232,66 @@ impl Database for Pool<Sqlite> {
Ok(digests) Ok(digests)
} }
async fn save_repository(&self, repository: &str) -> sqlx::Result<()> { async fn has_repository(&self, repository: &str) -> sqlx::Result<bool> {
sqlx::query("INSERT INTO repositories (name) VALUES (?)") debug!("before query ig");
let row: (u32, ) = match sqlx::query_as("SELECT COUNT(1) repositories WHERE 'name' = ?")
.bind(repository) .bind(repository)
.fetch_one(self).await {
Ok(row) => row,
Err(e) => match e {
sqlx::Error::RowNotFound => {
return Ok(false)
},
_ => {
return Err(e);
}
}
};
Ok(row.0 > 0)
}
async fn get_repository_visibility(&self, repository: &str) -> anyhow::Result<Option<RepositoryVisibility>> {
let row: (u32, ) = match sqlx::query_as("SELECT visibility FROM repositories WHERE 'name' = ?")
.bind(repository)
.fetch_one(self).await {
Ok(row) => row,
Err(e) => match e {
sqlx::Error::RowNotFound => {
return Ok(None)
},
_ => {
return Err(anyhow::Error::new(e));
}
}
};
Ok(Some(RepositoryVisibility::try_from(row.0)?))
}
async fn save_repository(&self, repository: &str, visibility: RepositoryVisibility, owning_project: Option<String>) -> sqlx::Result<()> {
// ensure that the repository was not already created
if self.has_repository(repository).await? {
debug!("repo exists");
return Ok(());
}
debug!("repo does not exist");
match owning_project {
Some(owner) => {
sqlx::query("INSERT INTO repositories (name, visibility, owning_project) VALUES (?, ?, ?)")
.bind(repository)
.bind(visibility as u32)
.bind(owner)
.execute(self).await?; .execute(self).await?;
},
None => {
sqlx::query("INSERT INTO repositories (name, visibility) VALUES (?, ?)")
.bind(repository)
.bind(visibility as u32)
.execute(self).await?;
}
}
Ok(()) Ok(())
} }
@ -278,4 +342,95 @@ impl Database for Pool<Sqlite> {
Ok(bcrypt::verify(password, &row.0)?) Ok(bcrypt::verify(password, &row.0)?)
} }
async fn get_user_registry_type(&self, username: String) -> anyhow::Result<Option<RegistryUserType>> {
let username = username.to_lowercase();
let row: (u32, ) = match sqlx::query_as("SELECT user_type FROM user_registry_permissions WHERE username = ?")
.bind(username)
.fetch_one(self).await {
Ok(row) => row,
Err(e) => match e {
sqlx::Error::RowNotFound => {
return Ok(None)
},
_ => {
return Err(anyhow::Error::new(e));
}
}
};
Ok(Some(RegistryUserType::try_from(row.0)?))
}
async fn get_user_repo_permissions(&self, username: String, repository: String) -> anyhow::Result<Option<RepositoryPermissions>> {
let username = username.to_lowercase();
let row: (u32, ) = match sqlx::query_as("SELECT repository_permissions FROM user_repo_permissions WHERE username = ? AND repository_name = ?")
.bind(username.clone())
.bind(repository.clone())
.fetch_one(self).await {
Ok(row) => row,
Err(e) => match e {
sqlx::Error::RowNotFound => {
return Ok(None)
},
_ => {
return Err(anyhow::Error::new(e));
}
}
};
let vis = self.get_repository_visibility(&repository).await?.unwrap();
// Also get the user type for the registry, if its admin return admin repository permissions
let utype = self.get_user_registry_usertype(username).await?.unwrap(); // unwrap should be safe
if utype == RegistryUserType::Admin {
Ok(Some(RepositoryPermissions::new(Permission::ADMIN.bits(), vis)))
} else {
Ok(Some(RepositoryPermissions::new(row.0, vis)))
}
}
async fn get_user_registry_usertype(&self, username: String) -> anyhow::Result<Option<RegistryUserType>> {
let username = username.to_lowercase();
let row: (u32, ) = sqlx::query_as("SELECT user_type FROM user_registry_permissions WHERE username = ?")
.bind(username)
.fetch_one(self).await?;
Ok(Some(RegistryUserType::try_from(row.0)?))
}
async fn store_user_token(&self, token: String, username: String, expiry: DateTime<Utc>, created_at: DateTime<Utc>) -> anyhow::Result<()> {
let username = username.to_lowercase();
let expiry = expiry.timestamp();
let created_at = created_at.timestamp();
sqlx::query("INSERT INTO user_tokens (token, username, expiry, created_at) VALUES (?, ?, ?, ?)")
.bind(token)
.bind(username)
.bind(expiry)
.bind(created_at)
.execute(self).await?;
Ok(())
}
async fn verify_user_token(&self, token: String) -> anyhow::Result<Option<UserAuth>> {
let token_row: (String, i64, i64, ) = sqlx::query_as("SELECT username, expiry, created_at FROM user_tokens WHERE token = ?")
.bind(token.clone())
.fetch_one(self).await?;
let (username, expiry, created_at) = (token_row.0, token_row.1, token_row.2);
let user_row: (String, ) = sqlx::query_as("SELECT email FROM users WHERE username = ?")
.bind(username.clone())
.fetch_one(self).await?;
let (expiry, created_at) = (Utc.timestamp_millis_opt(expiry).unwrap(), Utc.timestamp_millis_opt(created_at).unwrap());
let user = User::new(username, user_row.0);
let token = TokenInfo::new(token, expiry, created_at);
let auth = UserAuth::new(user, token);
Ok(Some(auth))
}
} }

View File

@ -1,5 +1,14 @@
CREATE TABLE IF NOT EXISTS projects (
name TEXT NOT NULL UNIQUE PRIMARY KEY,
-- 0 = private, 1 = public
visibility INTEGER NOT NULL
);
CREATE TABLE IF NOT EXISTS repositories ( CREATE TABLE IF NOT EXISTS repositories (
name TEXT NOT NULL UNIQUE PRIMARY KEY name TEXT NOT NULL UNIQUE PRIMARY KEY,
owning_project TEXT,
-- 0 = private, 1 = public
visibility INTEGER NOT NULL
); );
CREATE TABLE IF NOT EXISTS image_manifests ( CREATE TABLE IF NOT EXISTS image_manifests (
@ -11,13 +20,16 @@ CREATE TABLE IF NOT EXISTS image_manifests (
CREATE TABLE IF NOT EXISTS image_tags ( CREATE TABLE IF NOT EXISTS image_tags (
name TEXT NOT NULL, name TEXT NOT NULL,
repository TEXT NOT NULL, repository TEXT NOT NULL,
-- the image manifest for this tag
image_manifest TEXT NOT NULL, image_manifest TEXT NOT NULL,
-- the epoch timestamp fo when this image tag was last updated
last_updated BIGINT NOT NULL, last_updated BIGINT NOT NULL,
PRIMARY KEY (name, repository) PRIMARY KEY (name, repository)
); );
CREATE TABLE IF NOT EXISTS manifest_layers ( CREATE TABLE IF NOT EXISTS manifest_layers (
manifest TEXT NOT NULL, manifest TEXT NOT NULL,
-- the digest of the layer for this manifest
layer_digest TEXT NOT NULL, layer_digest TEXT NOT NULL,
PRIMARY KEY (manifest, layer_digest) PRIMARY KEY (manifest, layer_digest)
); );
@ -25,6 +37,29 @@ CREATE TABLE IF NOT EXISTS manifest_layers (
CREATE TABLE IF NOT EXISTS users ( CREATE TABLE IF NOT EXISTS users (
username TEXT NOT NULL UNIQUE PRIMARY KEY, username TEXT NOT NULL UNIQUE PRIMARY KEY,
email TEXT NOT NULL, email TEXT NOT NULL,
-- bcrypt hashed password
password_hash TEXT NOT NULL, password_hash TEXT NOT NULL,
-- the salt generated along side the password hash
password_salt TEXT NOT NULL password_salt TEXT NOT NULL
); );
CREATE TABLE IF NOT EXISTS user_registry_permissions (
username TEXT NOT NULL UNIQUE PRIMARY KEY,
-- 0 = regular user, 1 = admin
user_type INTEGER NOT NULL
);
CREATE TABLE IF NOT EXISTS user_repo_permissions (
username TEXT NOT NULL UNIQUE PRIMARY KEY,
-- name of repository that this user has these permissions in
repository_name TEXT NOT NULL,
-- bitwised integer storing permissions
repository_permissions INTEGER NOT NULL
);
CREATE TABLE IF NOT EXISTS user_tokens (
token TEXT NOT NULL UNIQUE PRIMARY KEY,
username TEXT NOT NULL,
expiry BIGINT NOT NULL,
created_at BIGINT NOT NULL
);

View File

@ -23,3 +23,27 @@ impl Tag {
} }
} }
} }
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum RepositoryVisibility {
Private = 0,
Public = 1
}
impl TryFrom<u32> for RepositoryVisibility {
type Error = anyhow::Error;
fn try_from(value: u32) -> Result<Self, Self::Error> {
match value {
0 => Ok(Self::Private),
1 => Ok(Self::Public),
_ => Err(anyhow::anyhow!("Invalid value for RepositoryVisibility: `{}`", value)),
}
}
}
impl Into<u32> for RepositoryVisibility {
fn into(self) -> u32 {
self as u32
}
}

View File

@ -1,6 +1,14 @@
use std::collections::HashMap;
use bitflags::bitflags;
use chrono::{DateTime, Utc};
use super::RepositoryVisibility;
#[derive(Clone, Debug, PartialEq)]
pub struct User { pub struct User {
username: String, pub username: String,
email: String, pub email: String,
} }
impl User { impl User {
@ -11,3 +19,100 @@ impl User {
} }
} }
} }
#[derive(Clone, Debug, PartialEq)]
pub struct TokenInfo {
pub token: String,
pub expiry: DateTime<Utc>,
pub created_at: DateTime<Utc>,
}
impl TokenInfo {
pub fn new(token: String, expiry: DateTime<Utc>, created_at: DateTime<Utc>) -> Self {
Self {
token,
expiry,
created_at
}
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct UserAuth {
pub user: User,
pub token: TokenInfo,
}
impl UserAuth {
pub fn new(user: User, token: TokenInfo) -> Self {
Self {
user,
token,
}
}
}
bitflags! {
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct Permission: u32 {
const PULL = 0b0001;
const PUSH = 0b0010;
const EDIT = 0b0111;
const ADMIN = 0b1111;
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct RepositoryPermissions {
perms: u32,
visibility: RepositoryVisibility
}
impl RepositoryPermissions {
pub fn new(perms: u32, visibility: RepositoryVisibility) -> Self {
Self {
perms,
visibility
}
}
/// Check if this struct has this permission, use `RepositoryPermission`
/// which has constants for the permissions.
pub fn has_permission(&self, perm: Permission) -> bool {
let perm = perm.bits();
self.perms & perm == perm
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum RegistryUserType {
Regular = 0,
Admin = 1
}
impl TryFrom<u32> for RegistryUserType {
type Error = anyhow::Error;
fn try_from(value: u32) -> Result<Self, Self::Error> {
match value {
0 => Ok(Self::Regular),
1 => Ok(Self::Admin),
_ => Err(anyhow::anyhow!("Invalid value for RegistryUserType: `{}`", value)),
}
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct RegistryUser {
user_type: RegistryUserType,
repository_permissions: HashMap<String, RepositoryPermissions>,
}
impl RegistryUser {
pub fn new(user_type: RegistryUserType, repository_permissions: HashMap<String, RepositoryPermissions>) -> Self {
Self {
user_type,
repository_permissions,
}
}
}

View File

@ -57,20 +57,6 @@ async fn change_request_paths<B>(mut request: Request<B>, next: Next<B>) -> Resp
next.run(request).await next.run(request).await
} }
pub async fn auth_failure() -> impl IntoResponse {
let bearer = format!("Bearer realm=\"http://localhost:3000/token\"");
(
StatusCode::UNAUTHORIZED,
[
( header::WWW_AUTHENTICATE, bearer ),
( HeaderName::from_static("docker-distribution-api-version"), "registry/2.0".to_string() )
]
).into_response()
//StatusCode::UNAUTHORIZED
}
#[tokio::main] #[tokio::main]
async fn main() -> std::io::Result<()> { async fn main() -> std::io::Result<()> {
let pool = SqlitePoolOptions::new() let pool = SqlitePoolOptions::new()