Redo auth once again to add a proper 'auth gate' middleware
This commit is contained in:
parent
2ecebab330
commit
618c04c29b
|
@ -24,17 +24,20 @@ $ htpasswd -nB
|
||||||
```
|
```
|
||||||
|
|
||||||
3. Insert the new user's email, password hash into the `user_logins` table. The salt is not used, so you can put whatever there
|
3. Insert the new user's email, password hash into the `user_logins` table. The salt is not used, so you can put whatever there
|
||||||
|
> WARNING: Ensure that the username is all lowercase!!!
|
||||||
```sql
|
```sql
|
||||||
INSERT INTO user_logins (email, password_hash, password_salt) VALUES ("example@email.com", "some password", "random salt")
|
INSERT INTO user_logins (email, password_hash, password_salt) VALUES ("example@email.com", "some password", "random salt")
|
||||||
```
|
```
|
||||||
|
|
||||||
4. Insert the new user into another table, `users` so the registry knows the source of the user
|
4. Insert the new user into another table, `users` so the registry knows the source of the user
|
||||||
|
> WARNING: Ensure that the username is all lowercase!!!
|
||||||
```sql
|
```sql
|
||||||
INSERT INTO users (username, email, login_source) VALUES ("example", "example@email.com", 0)
|
INSERT INTO users (username, email, login_source) VALUES ("example", "example@email.com", 0)
|
||||||
```
|
```
|
||||||
a `login_source` of `0` means database
|
a `login_source` of `0` means database
|
||||||
|
|
||||||
5. Give the user registry permissions
|
1. Give the user registry permissions
|
||||||
|
> WARNING: Ensure that the username is all lowercase!!!
|
||||||
```sql
|
```sql
|
||||||
INSERT INTO user_registry_permissions (email, user_type) VALUES ("example@email.com", 1)
|
INSERT INTO user_registry_permissions (email, user_type) VALUES ("example@email.com", 1)
|
||||||
```
|
```
|
||||||
|
|
|
@ -15,7 +15,7 @@ use rand::Rng;
|
||||||
use crate::{dto::{scope::Scope, user::TokenInfo}, app_state::AppState};
|
use crate::{dto::{scope::Scope, user::TokenInfo}, app_state::AppState};
|
||||||
use crate::database::Database;
|
use crate::database::Database;
|
||||||
|
|
||||||
use crate::auth::unauthenticated_response;
|
use crate::auth::auth_challenge_response;
|
||||||
|
|
||||||
#[derive(Deserialize, Debug)]
|
#[derive(Deserialize, Debug)]
|
||||||
pub struct TokenAuthRequest {
|
pub struct TokenAuthRequest {
|
||||||
|
@ -180,7 +180,7 @@ pub async fn auth_basic_get(basic_auth: Option<AuthBasic>, state: State<Arc<AppS
|
||||||
debug!("Authentication failed, incorrect password!");
|
debug!("Authentication failed, incorrect password!");
|
||||||
|
|
||||||
// TODO: Dont unwrap, find a way to return multiple scopes
|
// TODO: Dont unwrap, find a way to return multiple scopes
|
||||||
return Ok(unauthenticated_response(&state.config, auth.scope.first().unwrap()));
|
return Ok(auth_challenge_response(&state.config, Some(auth.scope.first().unwrap().clone())));
|
||||||
}
|
}
|
||||||
drop(auth_driver);
|
drop(auth_driver);
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
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};
|
||||||
|
@ -8,28 +7,9 @@ use axum::response::{IntoResponse, Response};
|
||||||
use tokio_util::io::ReaderStream;
|
use tokio_util::io::ReaderStream;
|
||||||
|
|
||||||
use crate::app_state::AppState;
|
use crate::app_state::AppState;
|
||||||
use crate::auth::{access_denied_response, unauthenticated_response};
|
|
||||||
use crate::database::Database;
|
|
||||||
use crate::dto::RepositoryVisibility;
|
|
||||||
use crate::dto::scope::{Scope, ScopeType, Action};
|
|
||||||
use crate::dto::user::{Permission, UserAuth};
|
|
||||||
use crate::error::AppError;
|
use crate::error::AppError;
|
||||||
|
|
||||||
pub async fn digest_exists_head(Path((name, layer_digest)): Path<(String, String)>, state: State<Arc<AppState>>, auth: Option<UserAuth>) -> Result<Response, AppError> {
|
pub async fn digest_exists_head(Path((_name, layer_digest)): Path<(String, String)>, state: State<Arc<AppState>>) -> Result<Response, AppError> {
|
||||||
// Check if the user has permission to pull, or that the repository is public
|
|
||||||
if let Some(auth) = auth {
|
|
||||||
let mut auth_driver = state.auth_checker.lock().await;
|
|
||||||
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));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let storage = state.storage.lock().await;
|
let storage = state.storage.lock().await;
|
||||||
|
|
||||||
if storage.has_digest(&layer_digest).await? {
|
if storage.has_digest(&layer_digest).await? {
|
||||||
|
@ -47,20 +27,7 @@ pub async fn digest_exists_head(Path((name, layer_digest)): Path<(String, String
|
||||||
Ok(StatusCode::NOT_FOUND.into_response())
|
Ok(StatusCode::NOT_FOUND.into_response())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn pull_digest_get(Path((name, layer_digest)): Path<(String, String)>, state: State<Arc<AppState>>, auth: Option<UserAuth>) -> Result<Response, AppError> {
|
pub async fn pull_digest_get(Path((_name, layer_digest)): Path<(String, String)>, state: State<Arc<AppState>>) -> Result<Response, AppError> {
|
||||||
// Check if the user has permission to pull, or that the repository is public
|
|
||||||
if let Some(auth) = auth {
|
|
||||||
let mut auth_driver = state.auth_checker.lock().await;
|
|
||||||
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));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let storage = state.storage.lock().await;
|
let storage = state.storage.lock().await;
|
||||||
|
|
||||||
if let Some(len) = storage.digest_length(&layer_digest).await? {
|
if let Some(len) = storage.digest_length(&layer_digest).await? {
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
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, HeaderName, header};
|
use axum::http::{StatusCode, HeaderName, header};
|
||||||
|
@ -17,12 +16,6 @@ use crate::dto::user::{UserAuth, Permission};
|
||||||
use crate::error::AppError;
|
use crate::error::AppError;
|
||||||
|
|
||||||
pub async fn upload_manifest_put(Path((name, reference)): Path<(String, String)>, state: State<Arc<AppState>>, auth: UserAuth, body: String) -> Result<Response, AppError> {
|
pub async fn upload_manifest_put(Path((name, reference)): Path<(String, String)>, state: State<Arc<AppState>>, auth: UserAuth, body: String) -> Result<Response, AppError> {
|
||||||
let mut auth_driver = state.auth_checker.lock().await;
|
|
||||||
if !auth_driver.user_has_permission(auth.user.username, name.clone(), Permission::PUSH, None).await? {
|
|
||||||
return Ok(access_denied_response(&state.config));
|
|
||||||
}
|
|
||||||
drop(auth_driver);
|
|
||||||
|
|
||||||
// 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);
|
||||||
|
|
|
@ -2,8 +2,7 @@ use std::sync::Arc;
|
||||||
|
|
||||||
use axum::extract::State;
|
use axum::extract::State;
|
||||||
use axum::response::{IntoResponse, Response};
|
use axum::response::{IntoResponse, Response};
|
||||||
use axum::http::{StatusCode, HeaderName, HeaderMap, header};
|
use axum::http::{StatusCode, HeaderName};
|
||||||
use tracing::debug;
|
|
||||||
|
|
||||||
use crate::app_state::AppState;
|
use crate::app_state::AppState;
|
||||||
|
|
||||||
|
@ -14,17 +13,13 @@ pub mod tags;
|
||||||
pub mod catalog;
|
pub mod catalog;
|
||||||
pub mod auth;
|
pub mod auth;
|
||||||
|
|
||||||
use crate::dto::user::UserAuth;
|
|
||||||
|
|
||||||
/// https://docs.docker.com/registry/spec/api/#api-version-check
|
/// https://docs.docker.com/registry/spec/api/#api-version-check
|
||||||
/// full endpoint: `/v2/`
|
/// full endpoint: `/v2/`
|
||||||
pub async fn version_check(_state: State<Arc<AppState>>) -> Response {
|
pub async fn version_check(_state: State<Arc<AppState>>) -> Response {
|
||||||
let bearer = format!("Bearer realm=\"{}/auth\"", _state.config.url());
|
|
||||||
(
|
(
|
||||||
StatusCode::UNAUTHORIZED,
|
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()
|
||||||
}
|
}
|
|
@ -2,7 +2,6 @@ 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};
|
||||||
|
@ -12,47 +11,26 @@ use futures::StreamExt;
|
||||||
use tracing::{debug, warn};
|
use tracing::{debug, warn};
|
||||||
|
|
||||||
use crate::app_state::AppState;
|
use crate::app_state::AppState;
|
||||||
use crate::auth::{access_denied_response, unauthenticated_response};
|
|
||||||
use crate::byte_stream::ByteStream;
|
use crate::byte_stream::ByteStream;
|
||||||
use crate::dto::scope::{Scope, ScopeType, Action};
|
|
||||||
use crate::dto::user::{UserAuth, Permission};
|
|
||||||
use crate::error::AppError;
|
use crate::error::AppError;
|
||||||
|
|
||||||
/// Starting an upload
|
/// Starting an upload
|
||||||
pub async fn start_upload_post(Path((name, )): Path<(String, )>, auth: Option<UserAuth>, state: State<Arc<AppState>>) -> Result<Response, AppError> {
|
pub async fn start_upload_post(Path((name, )): Path<(String, )>) -> Result<Response, AppError> {
|
||||||
if auth.is_none() {
|
debug!("Upload requested");
|
||||||
debug!("atuh was not given, responding with scope");
|
let uuid = uuid::Uuid::new_v4();
|
||||||
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;
|
debug!("Requesting upload of image {}, generated uuid: {}", name, uuid);
|
||||||
if auth_driver.user_has_permission(auth.user.username, name.clone(), Permission::PUSH, None).await? {
|
|
||||||
debug!("Upload requested");
|
|
||||||
let uuid = uuid::Uuid::new_v4();
|
|
||||||
|
|
||||||
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());
|
return Ok((
|
||||||
debug!("Constructed upload url: {}", location);
|
StatusCode::ACCEPTED,
|
||||||
|
[ (header::LOCATION, location) ]
|
||||||
return Ok((
|
).into_response());
|
||||||
StatusCode::ACCEPTED,
|
|
||||||
[ (header::LOCATION, location) ]
|
|
||||||
).into_response());
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(access_denied_response(&state.config))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn chunked_upload_layer_patch(Path((name, layer_uuid)): Path<(String, String)>, auth: UserAuth, state: State<Arc<AppState>>, mut body: BodyStream) -> Result<Response, AppError> {
|
pub async fn chunked_upload_layer_patch(Path((name, layer_uuid)): Path<(String, String)>, state: State<Arc<AppState>>, mut body: BodyStream) -> Result<Response, AppError> {
|
||||||
let mut auth_driver = state.auth_checker.lock().await;
|
|
||||||
if !auth_driver.user_has_permission(auth.user.username, name.clone(), Permission::PUSH, None).await? {
|
|
||||||
return Ok(access_denied_response(&state.config));
|
|
||||||
}
|
|
||||||
drop(auth_driver);
|
|
||||||
|
|
||||||
let storage = state.storage.lock().await;
|
let storage = state.storage.lock().await;
|
||||||
let current_size = storage.digest_length(&layer_uuid).await?;
|
let current_size = storage.digest_length(&layer_uuid).await?;
|
||||||
|
|
||||||
|
@ -105,13 +83,7 @@ pub async fn chunked_upload_layer_patch(Path((name, layer_uuid)): Path<(String,
|
||||||
).into_response())
|
).into_response())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn finish_chunked_upload_put(Path((name, layer_uuid)): Path<(String, String)>, Query(query): Query<HashMap<String, String>>, auth: UserAuth, state: State<Arc<AppState>>, body: Bytes) -> Result<Response, AppError> {
|
pub async fn finish_chunked_upload_put(Path((name, layer_uuid)): Path<(String, String)>, Query(query): Query<HashMap<String, String>>, state: State<Arc<AppState>>, body: Bytes) -> Result<Response, AppError> {
|
||||||
let mut auth_driver = state.auth_checker.lock().await;
|
|
||||||
if !auth_driver.user_has_permission(auth.user.username, name.clone(), Permission::PUSH, None).await? {
|
|
||||||
return Ok(access_denied_response(&state.config));
|
|
||||||
}
|
|
||||||
drop(auth_driver);
|
|
||||||
|
|
||||||
let digest = query.get("digest").unwrap();
|
let digest = query.get("digest").unwrap();
|
||||||
|
|
||||||
let storage = state.storage.lock().await;
|
let storage = state.storage.lock().await;
|
||||||
|
@ -134,13 +106,7 @@ pub async fn finish_chunked_upload_put(Path((name, layer_uuid)): Path<(String, S
|
||||||
).into_response())
|
).into_response())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn cancel_upload_delete(Path((name, layer_uuid)): Path<(String, String)>, state: State<Arc<AppState>>, auth: UserAuth) -> Result<Response, AppError> {
|
pub async fn cancel_upload_delete(Path((_name, layer_uuid)): Path<(String, String)>, state: State<Arc<AppState>>) -> Result<Response, AppError> {
|
||||||
let mut auth_driver = state.auth_checker.lock().await;
|
|
||||||
if !auth_driver.user_has_permission(auth.user.username, name.clone(), Permission::PUSH, None).await? {
|
|
||||||
return Ok(access_denied_response(&state.config));
|
|
||||||
}
|
|
||||||
drop(auth_driver);
|
|
||||||
|
|
||||||
let storage = state.storage.lock().await;
|
let storage = state.storage.lock().await;
|
||||||
storage.delete_digest(&layer_uuid).await?;
|
storage.delete_digest(&layer_uuid).await?;
|
||||||
|
|
||||||
|
@ -148,13 +114,7 @@ pub async fn cancel_upload_delete(Path((name, layer_uuid)): Path<(String, String
|
||||||
Ok(StatusCode::OK.into_response())
|
Ok(StatusCode::OK.into_response())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn check_upload_status_get(Path((name, layer_uuid)): Path<(String, String)>, state: State<Arc<AppState>>, auth: UserAuth) -> Result<Response, AppError> {
|
pub async fn check_upload_status_get(Path((name, layer_uuid)): Path<(String, String)>, state: State<Arc<AppState>>) -> Result<Response, AppError> {
|
||||||
let mut auth_driver = state.auth_checker.lock().await;
|
|
||||||
if !auth_driver.user_has_permission(auth.user.username, name.clone(), Permission::PUSH, None).await? {
|
|
||||||
return Ok(access_denied_response(&state.config));
|
|
||||||
}
|
|
||||||
drop(auth_driver);
|
|
||||||
|
|
||||||
let storage = state.storage.lock().await;
|
let storage = state.storage.lock().await;
|
||||||
let ending = storage.digest_length(&layer_uuid).await?.unwrap_or(0);
|
let ending = storage.digest_length(&layer_uuid).await?.unwrap_or(0);
|
||||||
|
|
||||||
|
|
116
src/auth/mod.rs
116
src/auth/mod.rs
|
@ -2,12 +2,11 @@ pub mod ldap_driver;
|
||||||
|
|
||||||
use std::{ops::Deref, sync::Arc};
|
use std::{ops::Deref, sync::Arc};
|
||||||
|
|
||||||
use axum::{extract::State, http::{StatusCode, HeaderMap, header, HeaderName, Request}, middleware::Next, response::{Response, IntoResponse}};
|
use axum::{extract::State, http::{StatusCode, HeaderMap, header, HeaderName, Request, Method}, middleware::Next, response::{Response, IntoResponse}};
|
||||||
|
|
||||||
use sqlx::{Pool, Sqlite};
|
use tracing::{debug, warn, error};
|
||||||
use tracing::debug;
|
|
||||||
|
|
||||||
use crate::{app_state::AppState, dto::{user::{Permission, RegistryUserType}, RepositoryVisibility, scope::{self, Scope}}, config::Config};
|
use crate::{app_state::AppState, dto::{user::{Permission, RegistryUserType, UserAuth}, RepositoryVisibility, scope::{Scope, ScopeType, Action}}, config::Config};
|
||||||
use crate::database::Database;
|
use crate::database::Database;
|
||||||
|
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
|
@ -31,23 +30,29 @@ where
|
||||||
T: Database + Send + Sync
|
T: Database + Send + Sync
|
||||||
{
|
{
|
||||||
async fn user_has_permission(&mut self, email: String, repository: String, permission: Permission, required_visibility: Option<RepositoryVisibility>) -> anyhow::Result<bool> {
|
async fn user_has_permission(&mut self, email: String, repository: String, permission: Permission, required_visibility: Option<RepositoryVisibility>) -> anyhow::Result<bool> {
|
||||||
let allowed_to: bool = {
|
match self.get_repository_owner(&repository).await? {
|
||||||
if self.get_repository_owner(&repository).await?
|
Some(owner) if owner == email => return Ok(true),
|
||||||
.map_or(false, |owner| owner == email) {
|
Some(_other_owner) => {
|
||||||
|
|
||||||
debug!("Allowing request, user is owner of repository");
|
|
||||||
true
|
|
||||||
} else {
|
|
||||||
match self.get_user_registry_type(email.clone()).await? {
|
match self.get_user_registry_type(email.clone()).await? {
|
||||||
Some(RegistryUserType::Admin) => true,
|
Some(RegistryUserType::Admin) => return Ok(true),
|
||||||
_ => {
|
_ => {
|
||||||
check_user_permissions(self, email, repository, permission, required_visibility).await?
|
return Ok(check_user_permissions(self, email, repository, permission, required_visibility).await?);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
};
|
None => {
|
||||||
|
// If the repository does not exist, see if its the per-user repositories and autocreate it.
|
||||||
Ok(allowed_to)
|
if let Some(user) = self.get_user(email.clone()).await? {
|
||||||
|
let username = user.username.to_lowercase();
|
||||||
|
if repository.starts_with(&username) {
|
||||||
|
self.save_repository(&repository, RepositoryVisibility::Private, Some(email), None).await?;
|
||||||
|
return Ok(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn verify_user_login(&mut self, email: String, password: String) -> anyhow::Result<bool> {
|
async fn verify_user_login(&mut self, email: String, password: String) -> anyhow::Result<bool> {
|
||||||
|
@ -133,9 +138,13 @@ pub async fn require_auth<B>(State(state): State<Arc<AppState>>, mut request: Re
|
||||||
/// Creates a response with an Unauthorized (401) status code.
|
/// Creates a response with an Unauthorized (401) status code.
|
||||||
/// The www-authenticate header is set to notify the client of where to authorize with.
|
/// The www-authenticate header is set to notify the client of where to authorize with.
|
||||||
#[inline(always)]
|
#[inline(always)]
|
||||||
pub fn unauthenticated_response(config: &Config, scope: &Scope) -> Response {
|
pub fn auth_challenge_response(config: &Config, scope: Option<Scope>) -> Response {
|
||||||
let bearer = format!("Bearer realm=\"{}/auth\",service=\"{}\",scope=\"{}\"", config.url(), "localhost:3000", scope);
|
let bearer = match scope {
|
||||||
|
Some(scope) => format!("Bearer realm=\"{}/auth\",scope=\"{}\"", config.url(), scope),
|
||||||
|
None => format!("Bearer realm=\"{}/auth\"", config.url())
|
||||||
|
};
|
||||||
debug!("responding with www-authenticate header of: \"{}\"", bearer);
|
debug!("responding with www-authenticate header of: \"{}\"", bearer);
|
||||||
|
|
||||||
(
|
(
|
||||||
StatusCode::UNAUTHORIZED,
|
StatusCode::UNAUTHORIZED,
|
||||||
[
|
[
|
||||||
|
@ -143,18 +152,83 @@ pub fn unauthenticated_response(config: &Config, scope: &Scope) -> Response {
|
||||||
( header::CONTENT_TYPE, "application/json".to_string() ),
|
( 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\"}]}]}"
|
//"{\"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()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Creates a response with a Forbidden (403) status code.
|
/// Creates a response with a Forbidden (403) status code.
|
||||||
/// No other headers are set.
|
/// No other headers are set.
|
||||||
#[inline(always)]
|
#[inline(always)]
|
||||||
pub fn access_denied_response(config: &Config) -> Response {
|
pub fn access_denied_response(_config: &Config) -> Response {
|
||||||
(
|
(
|
||||||
StatusCode::FORBIDDEN,
|
StatusCode::FORBIDDEN,
|
||||||
[
|
[
|
||||||
( 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()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn check_auth<B>(State(state): State<Arc<AppState>>, auth: Option<UserAuth>, request: Request<B>, next: Next<B>) -> Result<Response, Rejection> {
|
||||||
|
let config = &state.config;
|
||||||
|
// note: url is relative to /v2
|
||||||
|
let url = request.uri().to_string();
|
||||||
|
|
||||||
|
if url == "/" && auth.is_none() {
|
||||||
|
debug!("Responding to /v2/ with an auth challenge");
|
||||||
|
return Ok(auth_challenge_response(config, None));
|
||||||
|
}
|
||||||
|
|
||||||
|
let url_split: Vec<&str> = url.split("/").skip(1).collect();
|
||||||
|
let target_name = url_split[0].replace("%2F", "/");
|
||||||
|
let target_type = url_split[1];
|
||||||
|
|
||||||
|
// check if the request is targeting something inside an image repository
|
||||||
|
if target_type == "blobs" || target_type == "uploads" || target_type == "manifests" {
|
||||||
|
let scope_actions: &[Action] = match request.method().clone() {
|
||||||
|
Method::GET | Method::HEAD => &[Action::Pull],
|
||||||
|
Method::POST | Method::PATCH | Method::PUT => &[Action::Pull, Action::Push],
|
||||||
|
_ => &[],
|
||||||
|
};
|
||||||
|
let scope = Scope::new(ScopeType::Repository, target_name.clone(), scope_actions);
|
||||||
|
|
||||||
|
// respond with an auth challenge if there is no auth header.
|
||||||
|
//if !headers.contains_key(header::AUTHORIZATION) && auth.is_none() {
|
||||||
|
if auth.is_none() {
|
||||||
|
debug!("User is not authenticated, sending challenge");
|
||||||
|
return Ok(auth_challenge_response(config, Some(scope)));
|
||||||
|
}
|
||||||
|
let auth = auth.unwrap();
|
||||||
|
|
||||||
|
let mut auth_checker = state.auth_checker.lock().await;
|
||||||
|
|
||||||
|
// Check permission for each action
|
||||||
|
for action in scope_actions {
|
||||||
|
// action to permission
|
||||||
|
let permission = match action {
|
||||||
|
Action::Pull => Permission::PULL,
|
||||||
|
Action::Push => Permission::PUSH,
|
||||||
|
_ => Permission::NONE,
|
||||||
|
};
|
||||||
|
|
||||||
|
// get optional required visibility from action
|
||||||
|
let vis = match action {
|
||||||
|
Action::Pull => Some(RepositoryVisibility::Public),
|
||||||
|
_ => None,
|
||||||
|
};
|
||||||
|
|
||||||
|
match auth_checker.user_has_permission(auth.user.email.clone(), target_name.clone(), permission, vis).await {
|
||||||
|
Ok(false) => return Ok(auth_challenge_response(config, Some(scope))),
|
||||||
|
Ok(true) => { },
|
||||||
|
Err(e) => {
|
||||||
|
error!("Error when checking user permissions! {}", e);
|
||||||
|
|
||||||
|
return Err((StatusCode::INTERNAL_SERVER_ERROR, HeaderMap::new()));
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
warn!("Unhandled auth check for '{target_type}'!!"); // TODO
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(next.run(request).await)
|
||||||
}
|
}
|
|
@ -550,7 +550,7 @@ impl Database for Pool<Sqlite> {
|
||||||
|
|
||||||
let (expiry, created_at) = (Utc.timestamp_millis_opt(expiry).single(), Utc.timestamp_millis_opt(created_at).single());
|
let (expiry, created_at) = (Utc.timestamp_millis_opt(expiry).single(), Utc.timestamp_millis_opt(created_at).single());
|
||||||
if let (Some(expiry), Some(created_at)) = (expiry, created_at) {
|
if let (Some(expiry), Some(created_at)) = (expiry, created_at) {
|
||||||
let user = User::new(email, user_row.0, LoginSource::try_from(user_row.1)?);
|
let user = User::new(user_row.0, email, LoginSource::try_from(user_row.1)?);
|
||||||
let token = TokenInfo::new(token, expiry, created_at);
|
let token = TokenInfo::new(token, expiry, created_at);
|
||||||
let auth = UserAuth::new(user, token);
|
let auth = UserAuth::new(user, token);
|
||||||
|
|
||||||
|
|
|
@ -3,7 +3,7 @@ use serde::{Deserialize, de::Visitor};
|
||||||
|
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
|
|
||||||
#[derive(Default, Debug)]
|
#[derive(Default, Debug, Clone)]
|
||||||
pub enum ScopeType {
|
pub enum ScopeType {
|
||||||
#[default]
|
#[default]
|
||||||
Unknown,
|
Unknown,
|
||||||
|
@ -37,7 +37,7 @@ impl fmt::Display for Action {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Default, Debug)]
|
#[derive(Default, Debug, Clone)]
|
||||||
pub struct Scope {
|
pub struct Scope {
|
||||||
scope_type: ScopeType,
|
scope_type: ScopeType,
|
||||||
path: String,
|
path: String,
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
use std::{collections::HashMap, sync::Arc};
|
use std::{collections::HashMap, sync::Arc};
|
||||||
|
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use axum::{http::{StatusCode, header, HeaderName, HeaderMap, Request, request::Parts}, extract::{FromRequest, FromRequestParts}};
|
use axum::{http::{StatusCode, header, HeaderName, HeaderMap, request::Parts}, extract::FromRequestParts};
|
||||||
use bitflags::bitflags;
|
use bitflags::bitflags;
|
||||||
use chrono::{DateTime, Utc};
|
use chrono::{DateTime, Utc};
|
||||||
use tracing::{debug, warn};
|
use tracing::debug;
|
||||||
|
|
||||||
use crate::{app_state::AppState, database::Database};
|
use crate::{app_state::AppState, database::Database};
|
||||||
|
|
||||||
|
@ -92,16 +92,9 @@ impl FromRequestParts<Arc<AppState>> for UserAuth {
|
||||||
let auth = String::from(
|
let auth = String::from(
|
||||||
parts.headers
|
parts.headers
|
||||||
.get(header::AUTHORIZATION)
|
.get(header::AUTHORIZATION)
|
||||||
.ok_or(
|
.ok_or((StatusCode::UNAUTHORIZED, failure_headers.clone()))?
|
||||||
{
|
|
||||||
debug!("Client did not send authorization header");
|
|
||||||
(StatusCode::UNAUTHORIZED, failure_headers.clone())
|
|
||||||
})?
|
|
||||||
.to_str()
|
.to_str()
|
||||||
.map_err(|_| {
|
.map_err(|_| (StatusCode::UNAUTHORIZED, failure_headers.clone()))?
|
||||||
warn!("Failure to convert Authorization header to string!");
|
|
||||||
(StatusCode::UNAUTHORIZED, failure_headers.clone())
|
|
||||||
})?
|
|
||||||
);
|
);
|
||||||
|
|
||||||
debug!("got auth header");
|
debug!("got auth header");
|
||||||
|
@ -118,7 +111,7 @@ impl FromRequestParts<Arc<AppState>> for UserAuth {
|
||||||
// If the token is not valid, return an unauthorized response
|
// If the token is not valid, return an unauthorized response
|
||||||
let database = &state.database;
|
let database = &state.database;
|
||||||
if let Ok(Some(user)) = database.verify_user_token(token.to_string()).await {
|
if let Ok(Some(user)) = database.verify_user_token(token.to_string()).await {
|
||||||
debug!("Authenticated user through middleware: {}", user.user.username);
|
debug!("Authenticated user through request extractor: {}", user.user.username);
|
||||||
|
|
||||||
Ok(user)
|
Ok(user)
|
||||||
} else {
|
} else {
|
||||||
|
@ -135,6 +128,7 @@ impl FromRequestParts<Arc<AppState>> for UserAuth {
|
||||||
bitflags! {
|
bitflags! {
|
||||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
|
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
|
||||||
pub struct Permission: u32 {
|
pub struct Permission: u32 {
|
||||||
|
const NONE = 0b0000;
|
||||||
const PULL = 0b0001;
|
const PULL = 0b0001;
|
||||||
const PUSH = 0b0010;
|
const PUSH = 0b0010;
|
||||||
const EDIT = 0b0111;
|
const EDIT = 0b0111;
|
||||||
|
|
|
@ -22,14 +22,13 @@ use axum::ServiceExt;
|
||||||
use axum_server::tls_rustls::RustlsConfig;
|
use axum_server::tls_rustls::RustlsConfig;
|
||||||
use lazy_static::lazy_static;
|
use lazy_static::lazy_static;
|
||||||
use regex::Regex;
|
use regex::Regex;
|
||||||
use sqlx::ConnectOptions;
|
|
||||||
use tokio::fs::File;
|
use tokio::fs::File;
|
||||||
use tower_layer::Layer;
|
use tower_layer::Layer;
|
||||||
|
|
||||||
use sqlx::sqlite::{SqlitePoolOptions, SqliteConnectOptions, SqliteJournalMode};
|
use sqlx::sqlite::{SqlitePoolOptions, SqliteConnectOptions, SqliteJournalMode};
|
||||||
use tokio::sync::Mutex;
|
use tokio::sync::Mutex;
|
||||||
use tower_http::normalize_path::NormalizePathLayer;
|
use tower_http::normalize_path::NormalizePathLayer;
|
||||||
use tracing::{debug, Level, info};
|
use tracing::{debug, info};
|
||||||
|
|
||||||
use app_state::AppState;
|
use app_state::AppState;
|
||||||
use database::Database;
|
use database::Database;
|
||||||
|
@ -117,6 +116,7 @@ async fn main() -> anyhow::Result<()> {
|
||||||
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 auth_middleware = axum::middleware::from_fn_with_state(state.clone(), auth::check_auth);
|
||||||
let path_middleware = axum::middleware::from_fn(change_request_paths);
|
let path_middleware = axum::middleware::from_fn(change_request_paths);
|
||||||
|
|
||||||
let app = Router::new()
|
let app = Router::new()
|
||||||
|
@ -143,7 +143,7 @@ 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());
|
||||||
|
|
Loading…
Reference in New Issue