Implement user auth for pushing and pulling images
This commit is contained in:
parent
5f74e46607
commit
e3a0554823
|
@ -119,7 +119,7 @@ checksum = "113713495a32dd0ab52baf5c10044725aa3aec00b31beda84218e469029b72a3"
|
|||
dependencies = [
|
||||
"async-trait",
|
||||
"axum-core",
|
||||
"bitflags",
|
||||
"bitflags 1.3.2",
|
||||
"bytes",
|
||||
"futures-util",
|
||||
"http",
|
||||
|
@ -215,6 +215,12 @@ version = "1.3.2"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "2.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "24a6904aef64d73cf10ab17ebace7befb918b82164785cb89907993be7f83813"
|
||||
|
||||
[[package]]
|
||||
name = "block-buffer"
|
||||
version = "0.9.0"
|
||||
|
@ -311,7 +317,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "389ca505fd2c00136e0d0cd34bcd8b6bd0b59d5779aab396054b716334230c1c"
|
||||
dependencies = [
|
||||
"atty",
|
||||
"bitflags",
|
||||
"bitflags 1.3.2",
|
||||
"clap_derive",
|
||||
"clap_lex",
|
||||
"once_cell",
|
||||
|
@ -1077,6 +1083,7 @@ dependencies = [
|
|||
"axum-auth",
|
||||
"axum-macros",
|
||||
"bcrypt",
|
||||
"bitflags 2.2.1",
|
||||
"bytes",
|
||||
"chrono",
|
||||
"clap",
|
||||
|
@ -1329,7 +1336,7 @@ version = "0.2.16"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"bitflags 1.3.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -1594,7 +1601,7 @@ checksum = "fa8241483a83a3f33aa5fff7e7d9def398ff9990b2752b6c6112b83c6d246029"
|
|||
dependencies = [
|
||||
"ahash",
|
||||
"atoi",
|
||||
"bitflags",
|
||||
"bitflags 1.3.2",
|
||||
"byteorder",
|
||||
"bytes",
|
||||
"crc",
|
||||
|
@ -1892,7 +1899,7 @@ version = "0.4.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5d1d42a9b3f3ec46ba828e8d376aec14592ea199f70a06a548587ecd1c4ab658"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"bitflags 1.3.2",
|
||||
"bytes",
|
||||
"futures-core",
|
||||
"futures-util",
|
||||
|
|
|
@ -50,3 +50,4 @@ hmac = "0.12.1"
|
|||
sha2 = "0.10.6"
|
||||
rand = "0.8.5"
|
||||
bcrypt = "0.14.0"
|
||||
bitflags = "2.2.1"
|
||||
|
|
|
@ -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
|
|
@ -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_auth::AuthBasic;
|
||||
use chrono::{DateTime, Utc};
|
||||
use chrono::{DateTime, Utc, Duration};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tracing::{debug, error, info, span, Level};
|
||||
|
||||
|
@ -12,7 +12,7 @@ use sha2::Sha256;
|
|||
|
||||
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;
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
|
@ -41,13 +41,12 @@ pub struct AuthResponse {
|
|||
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 now = SystemTime::now();
|
||||
let now_secs = now
|
||||
.duration_since(UNIX_EPOCH)?
|
||||
.as_secs();
|
||||
let now = chrono::offset::Utc::now();
|
||||
let now_secs = now.timestamp();
|
||||
|
||||
// Construct the claims for the token
|
||||
let mut claims = BTreeMap::new();
|
||||
|
@ -55,19 +54,23 @@ fn create_jwt_token(account: String) -> anyhow::Result<String> {
|
|||
claims.insert("subject", &account);
|
||||
//claims.insert("audience", auth.service);
|
||||
|
||||
let notbefore = format!("{}", now_secs - 10);
|
||||
let issuedat = format!("{}", now_secs);
|
||||
let not_before = format!("{}", now_secs - 10);
|
||||
let issued_at = format!("{}", now_secs);
|
||||
let expiration = format!("{}", now_secs + 20);
|
||||
claims.insert("notbefore", ¬before);
|
||||
claims.insert("issuedat", &issuedat);
|
||||
claims.insert("notbefore", ¬_before);
|
||||
claims.insert("issuedat", &issued_at);
|
||||
claims.insert("expiration", &expiration); // TODO: 20 seconds expiry for testing
|
||||
|
||||
let issued_at = now;
|
||||
let expiration = now + Duration::seconds(20);
|
||||
|
||||
// Create a randomized jwtid
|
||||
let mut rng = rand::thread_rng();
|
||||
let jwtid = format!("{}", rng.gen::<u64>());
|
||||
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 {
|
||||
|
@ -142,6 +145,7 @@ pub async fn auth_basic_get(basic_auth: Option<AuthBasic>, state: State<Arc<AppS
|
|||
|
||||
// Process all the scopes
|
||||
if let Some(scope) = params.get("scope") {
|
||||
|
||||
// TODO: Handle multiple scopes
|
||||
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");
|
||||
|
||||
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
|
||||
let database = &state.database;
|
||||
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
|
||||
).into_response();
|
||||
}
|
||||
drop(database);
|
||||
debug!("User password is correct");
|
||||
|
||||
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");
|
||||
|
||||
|
@ -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 mut auth_storage = state.auth_storage.lock().await;
|
||||
auth_storage.valid_tokens.insert(token_str.clone());
|
||||
database.store_user_token(token_str.clone(), account.clone(), token.expiry, token.created_at).await.unwrap();
|
||||
drop(database);
|
||||
|
||||
return (
|
||||
StatusCode::OK,
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
use std::sync::Arc;
|
||||
|
||||
use axum::Extension;
|
||||
use axum::body::StreamBody;
|
||||
use axum::extract::{State, Path};
|
||||
use axum::http::{StatusCode, header, HeaderName};
|
||||
|
@ -7,8 +8,23 @@ use axum::response::{IntoResponse, Response};
|
|||
use tokio_util::io::ReaderStream;
|
||||
|
||||
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;
|
||||
|
||||
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()
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
if let Some(len) = storage.digest_length(&layer_digest).await.unwrap() {
|
||||
|
|
|
@ -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()));
|
||||
|
||||
// Construct the link header
|
||||
let url = &state.config.url;
|
||||
let url = &state.config.get_url();
|
||||
let mut url = format!("<{}/v2/_catalog?n={}", url, limit);
|
||||
if let Some(last_repo) = last_repo {
|
||||
url += &format!("&limit={}", last_repo);
|
||||
|
|
|
@ -1,26 +1,33 @@
|
|||
use std::sync::Arc;
|
||||
|
||||
use axum::Extension;
|
||||
use axum::extract::{Path, State};
|
||||
use axum::response::{Response, IntoResponse};
|
||||
use axum::http::{StatusCode, HeaderMap, HeaderName, header};
|
||||
use tracing::log::warn;
|
||||
use tracing::{debug, info};
|
||||
|
||||
use crate::auth_storage::{does_user_have_permission, get_unauthenticated_response};
|
||||
use crate::app_state::AppState;
|
||||
|
||||
use crate::database::Database;
|
||||
use crate::dto::RepositoryVisibility;
|
||||
use crate::dto::digest::Digest;
|
||||
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.
|
||||
let calculated_hash = sha256::digest(body.clone());
|
||||
let calculated_digest = format!("sha256:{}", calculated_hash);
|
||||
|
||||
let database = &state.database;
|
||||
|
||||
// Create the image repository and save the image manifest.
|
||||
database.save_repository(&name).await.unwrap();
|
||||
// Create the image repository and save the image manifest. This repository will be private by default
|
||||
database.save_repository(&name, RepositoryVisibility::Private, None).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.
|
||||
|
@ -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 digest = match Digest::is_digest(&reference) {
|
||||
true => reference.clone(),
|
||||
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() {
|
||||
tag.manifest_digest
|
||||
} else {
|
||||
|
@ -90,7 +108,18 @@ pub async fn pull_manifest_get(Path((name, reference)): Path<(String, String)>,
|
|||
).into_response()
|
||||
}
|
||||
|
||||
pub async fn manifest_exists_head(Path((name, reference)): Path<(String, String)>, state: State<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.
|
||||
let database = &state.database;
|
||||
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()
|
||||
}
|
||||
|
||||
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 database = &state.database;
|
||||
|
|
|
@ -15,10 +15,11 @@ pub mod catalog;
|
|||
pub mod auth;
|
||||
|
||||
use crate::auth_storage::AuthToken;
|
||||
use crate::dto::user::UserAuth;
|
||||
|
||||
/// https://docs.docker.com/registry/spec/api/#api-version-check
|
||||
/// 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,
|
||||
[( HeaderName::from_static("docker-distribution-api-version"), "registry/2.0" )]
|
||||
|
|
|
@ -37,7 +37,7 @@ pub async fn list_tags(Path((name, )): Path<(String, )>, Query(params): Query<Li
|
|||
let last_tag = tags.last();
|
||||
|
||||
// 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);
|
||||
if let Some(last_tag) = last_tag {
|
||||
url += &format!("&limit={}", last_tag.name);
|
||||
|
|
|
@ -2,6 +2,7 @@ use std::collections::HashMap;
|
|||
use std::io::ErrorKind;
|
||||
use std::sync::Arc;
|
||||
|
||||
use axum::Extension;
|
||||
use axum::http::{StatusCode, header, HeaderName};
|
||||
use axum::extract::{Path, BodyStream, State, Query};
|
||||
use axum::response::{IntoResponse, Response};
|
||||
|
@ -11,25 +12,36 @@ use futures::StreamExt;
|
|||
use tracing::{debug, warn};
|
||||
|
||||
use crate::app_state::AppState;
|
||||
use crate::auth_storage::{does_user_have_permission, get_unauthenticated_response};
|
||||
use crate::byte_stream::ByteStream;
|
||||
use crate::database::Database;
|
||||
use crate::dto::user::{UserAuth, Permission, RegistryUser, RegistryUserType};
|
||||
|
||||
/// Starting an upload
|
||||
pub async fn start_upload_post(Path((name, )): Path<(String, )>) -> impl IntoResponse {
|
||||
debug!("Upload starting");
|
||||
let uuid = uuid::Uuid::new_v4();
|
||||
pub async fn start_upload_post(Path((name, )): Path<(String, )>, Extension(auth): Extension<UserAuth>, state: State<Arc<AppState>>) -> Response {
|
||||
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();
|
||||
|
||||
debug!("Requesting upload of image {}, generated uuid: {}", name, uuid);
|
||||
debug!("Requesting upload of image {}, generated uuid: {}", name, uuid);
|
||||
|
||||
let location = format!("/v2/{}/blobs/uploads/{}", name, uuid.to_string());
|
||||
debug!("Constructed upload url: {}", location);
|
||||
let location = format!("/v2/{}/blobs/uploads/{}", name, uuid.to_string());
|
||||
debug!("Constructed upload url: {}", location);
|
||||
|
||||
(
|
||||
StatusCode::ACCEPTED,
|
||||
[ (header::LOCATION, location) ]
|
||||
)
|
||||
return (
|
||||
StatusCode::ACCEPTED,
|
||||
[ (header::LOCATION, location) ]
|
||||
).into_response();
|
||||
}
|
||||
|
||||
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 {
|
||||
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);
|
||||
}
|
||||
|
||||
let storage = state.storage.lock().await;
|
||||
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)
|
||||
};
|
||||
|
||||
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,
|
||||
[
|
||||
|
@ -82,7 +94,11 @@ pub async fn chunked_upload_layer_patch(Path((name, layer_uuid)): Path<(String,
|
|||
).into_response()
|
||||
}
|
||||
|
||||
pub async fn finish_chunked_upload_put(Path((name, layer_uuid)): Path<(String, String)>, Query(query): Query<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 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()),
|
||||
(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>>) -> impl IntoResponse {
|
||||
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);
|
||||
}
|
||||
|
||||
let storage = state.storage.lock().await;
|
||||
storage.delete_digest(&layer_uuid).await.unwrap();
|
||||
|
||||
// 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>>) -> impl IntoResponse {
|
||||
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);
|
||||
}
|
||||
|
||||
let storage = state.storage.lock().await;
|
||||
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)),
|
||||
(HeaderName::from_static("docker-upload-digest"), layer_uuid)
|
||||
]
|
||||
)
|
||||
).into_response()
|
||||
}
|
|
@ -1,10 +1,11 @@
|
|||
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 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.
|
||||
pub struct MemoryAuthStorage {
|
||||
|
@ -33,7 +34,7 @@ impl Deref for AuthToken {
|
|||
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> {
|
||||
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();
|
||||
failure_headers.append(header::WWW_AUTHENTICATE, bearer.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()))?
|
||||
.to_str()
|
||||
.map_err(|_| (StatusCode::UNAUTHORIZED, failure_headers.clone()))?
|
||||
); // TODO: Don't unwrap
|
||||
);
|
||||
|
||||
let token = match auth.split_once(' ') {
|
||||
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
|
||||
let auth_storage = state.auth_storage.lock().await;
|
||||
if !auth_storage.valid_tokens.contains(token) {
|
||||
let bearer = format!("Bearer realm=\"http://localhost:3000/auth\"");
|
||||
return Ok((
|
||||
let database = &state.database;
|
||||
if let Some(user) = database.verify_user_token(token.to_string()).await.unwrap() {
|
||||
debug!("Authenticated user through middleware: {}", user.user.username);
|
||||
|
||||
request.extensions_mut().insert(user);
|
||||
|
||||
Ok(next.run(request).await)
|
||||
} else {
|
||||
let bearer = format!("Bearer realm=\"{}/auth\"", state.config.get_url());
|
||||
Ok((
|
||||
StatusCode::UNAUTHORIZED,
|
||||
[
|
||||
( header::WWW_AUTHENTICATE, bearer ),
|
||||
( HeaderName::from_static("docker-distribution-api-version"), "registry/2.0".to_string() )
|
||||
]
|
||||
).into_response());
|
||||
|
||||
} else {
|
||||
debug!("Client successfully authenticated!");
|
||||
).into_response())
|
||||
}
|
||||
drop(auth_storage);
|
||||
|
||||
request.extensions_mut().insert(AuthToken(String::from(token)));
|
||||
|
||||
Ok(next.run(request).await)
|
||||
}
|
||||
|
||||
pub async fn does_user_have_permission(database: &impl Database, username: String, repository: String, permission: Permission) -> sqlx::Result<bool> {
|
||||
let allowed_to = {
|
||||
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()
|
||||
}
|
|
@ -8,7 +8,7 @@ use std::env;
|
|||
pub struct Config {
|
||||
pub listen_address: String,
|
||||
pub listen_port: String,
|
||||
pub url: String,
|
||||
pub url: Option<String>,
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
|
@ -37,11 +37,19 @@ impl Config {
|
|||
.join(Toml::file(format!("{}", path)));
|
||||
|
||||
let mut config: Config = figment.extract()?;
|
||||
|
||||
if config.url.ends_with("/") {
|
||||
config.url = config.url[..config.url.len() - 1].to_string();
|
||||
if let Some(url) = config.url.as_mut() {
|
||||
if url.ends_with("/") {
|
||||
*url = url[..url.len() - 1].to_string();
|
||||
}
|
||||
}
|
||||
|
||||
Ok(config)
|
||||
}
|
||||
|
||||
pub fn get_url(&self) -> String {
|
||||
match &self.url {
|
||||
Some(u) => u.clone(),
|
||||
None => format!("http://{}:{}", self.listen_address, self.listen_port)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -2,9 +2,9 @@ use async_trait::async_trait;
|
|||
use sqlx::{Sqlite, Pool};
|
||||
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]
|
||||
pub trait Database {
|
||||
|
@ -40,8 +40,10 @@ pub trait Database {
|
|||
|
||||
// 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
|
||||
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.
|
||||
/// 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>>;
|
||||
|
@ -50,6 +52,11 @@ pub trait Database {
|
|||
/// User stuff
|
||||
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 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]
|
||||
|
@ -127,6 +134,7 @@ impl Database for Pool<Sqlite> {
|
|||
}
|
||||
|
||||
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 = ?")
|
||||
.bind(tag)
|
||||
.bind(repository)
|
||||
|
@ -159,7 +167,7 @@ impl Database for Pool<Sqlite> {
|
|||
}
|
||||
|
||||
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(repository)
|
||||
.execute(self).await?;
|
||||
|
@ -224,10 +232,66 @@ impl Database for Pool<Sqlite> {
|
|||
Ok(digests)
|
||||
}
|
||||
|
||||
async fn save_repository(&self, repository: &str) -> sqlx::Result<()> {
|
||||
sqlx::query("INSERT INTO repositories (name) VALUES (?)")
|
||||
.bind(repository)
|
||||
.execute(self).await?;
|
||||
async fn has_repository(&self, repository: &str) -> sqlx::Result<bool> {
|
||||
debug!("before query ig");
|
||||
let row: (u32, ) = match sqlx::query_as("SELECT COUNT(1) repositories WHERE 'name' = ?")
|
||||
.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?;
|
||||
},
|
||||
None => {
|
||||
sqlx::query("INSERT INTO repositories (name, visibility) VALUES (?, ?)")
|
||||
.bind(repository)
|
||||
.bind(visibility as u32)
|
||||
.execute(self).await?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
@ -278,4 +342,95 @@ impl Database for Pool<Sqlite> {
|
|||
|
||||
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))
|
||||
}
|
||||
}
|
|
@ -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 (
|
||||
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 (
|
||||
|
@ -11,13 +20,16 @@ CREATE TABLE IF NOT EXISTS image_manifests (
|
|||
CREATE TABLE IF NOT EXISTS image_tags (
|
||||
name TEXT NOT NULL,
|
||||
repository TEXT NOT NULL,
|
||||
-- the image manifest for this tag
|
||||
image_manifest TEXT NOT NULL,
|
||||
-- the epoch timestamp fo when this image tag was last updated
|
||||
last_updated BIGINT NOT NULL,
|
||||
PRIMARY KEY (name, repository)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS manifest_layers (
|
||||
manifest TEXT NOT NULL,
|
||||
-- the digest of the layer for this manifest
|
||||
layer_digest TEXT NOT NULL,
|
||||
PRIMARY KEY (manifest, layer_digest)
|
||||
);
|
||||
|
@ -25,6 +37,29 @@ CREATE TABLE IF NOT EXISTS manifest_layers (
|
|||
CREATE TABLE IF NOT EXISTS users (
|
||||
username TEXT NOT NULL UNIQUE PRIMARY KEY,
|
||||
email TEXT NOT NULL,
|
||||
-- bcrypt hashed password
|
||||
password_hash TEXT NOT NULL,
|
||||
-- the salt generated along side the password hash
|
||||
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
|
||||
);
|
|
@ -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
|
||||
}
|
||||
}
|
109
src/dto/user.rs
109
src/dto/user.rs
|
@ -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 {
|
||||
username: String,
|
||||
email: String,
|
||||
pub username: String,
|
||||
pub email: String,
|
||||
}
|
||||
|
||||
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,
|
||||
}
|
||||
}
|
||||
}
|
14
src/main.rs
14
src/main.rs
|
@ -57,20 +57,6 @@ async fn change_request_paths<B>(mut request: Request<B>, next: Next<B>) -> Resp
|
|||
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]
|
||||
async fn main() -> std::io::Result<()> {
|
||||
let pool = SqlitePoolOptions::new()
|
||||
|
|
Loading…
Reference in New Issue