create AuthDriver trait for checking user permissions

This commit is contained in:
SeanOMik 2023-05-28 02:06:49 -04:00
parent b5c770b349
commit d98a8e3790
Signed by: SeanOMik
GPG Key ID: 568F326C7EB33ACB
8 changed files with 123 additions and 96 deletions

View File

@ -15,6 +15,8 @@ use rand::Rng;
use crate::{dto::{scope::Scope, user::{UserAuth, TokenInfo}}, app_state::AppState};
use crate::database::Database;
use crate::auth_storage::{unauthenticated_response, AuthDriver};
#[derive(Deserialize, Debug)]
pub struct TokenAuthRequest {
user: Option<String>,
@ -165,13 +167,14 @@ pub async fn auth_basic_get(basic_auth: Option<AuthBasic>, state: State<Arc<AppS
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() {
let auth_driver = state.auth_checker.lock().await;
if !auth_driver.verify_user_login(account.clone(), password).await.unwrap() {
debug!("Authentication failed, incorrect password!");
return (
StatusCode::UNAUTHORIZED
).into_response();
return unauthenticated_response(&state.config);
}
drop(auth_driver);
debug!("User password is correct");
let now = SystemTime::now();
@ -193,6 +196,7 @@ 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 database = &state.database;
database.store_user_token(token_str.clone(), account.clone(), token.expiry, token.created_at).await.unwrap();
drop(database);

View File

@ -8,25 +8,18 @@ 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, does_user_have_repository_permission};
use crate::auth_storage::{unauthenticated_response, AuthDriver};
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);
} */
if !does_user_have_repository_permission(database, auth.user.username, name.clone(), Permission::PULL, Some(RepositoryVisibility::Public)).await.unwrap() {
return get_unauthenticated_response(&state.config);
let auth_driver = state.auth_checker.lock().await;
if !auth_driver.user_has_permission(auth.user.username, name.clone(), Permission::PULL, Some(RepositoryVisibility::Public)).await.unwrap() {
return unauthenticated_response(&state.config);
}
drop(database);
drop(auth_driver);
let storage = state.storage.lock().await;
@ -47,11 +40,11 @@ pub async fn digest_exists_head(Path((name, layer_digest)): Path<(String, String
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_repository_permission(database, auth.user.username, name.clone(), Permission::PULL, Some(RepositoryVisibility::Public)).await.unwrap() {
return get_unauthenticated_response(&state.config);
let auth_driver = state.auth_checker.lock().await;
if !auth_driver.user_has_permission(auth.user.username, name.clone(), Permission::PULL, Some(RepositoryVisibility::Public)).await.unwrap() {
return unauthenticated_response(&state.config);
}
drop(database);
drop(auth_driver);
let storage = state.storage.lock().await;

View File

@ -7,7 +7,7 @@ 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, does_user_have_repository_permission};
use crate::auth_storage::{unauthenticated_response, AuthDriver};
use crate::app_state::AppState;
use crate::database::Database;
use crate::dto::RepositoryVisibility;
@ -16,9 +16,11 @@ 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);
let auth_driver = state.auth_checker.lock().await;
if !auth_driver.user_has_permission(auth.user.username, name.clone(), Permission::PUSH, None).await.unwrap() {
return unauthenticated_response(&state.config);
}
drop(auth_driver);
// Calculate the sha256 digest for the manifest.
let calculated_hash = sha256::digest(body.clone());
@ -63,11 +65,11 @@ pub async fn upload_manifest_put(Path((name, reference)): Path<(String, String)>
pub async fn pull_manifest_get(Path((name, reference)): Path<(String, String)>, state: State<Arc<AppState>>, Extension(auth): Extension<UserAuth>) -> Response {
// Check if the user has permission to pull, or that the repository is public
let database = &state.database;
if !does_user_have_repository_permission(database, auth.user.username, name.clone(), Permission::PULL, Some(RepositoryVisibility::Public)).await.unwrap() {
return get_unauthenticated_response(&state.config);
let auth_driver = state.auth_checker.lock().await;
if !auth_driver.user_has_permission(auth.user.username, name.clone(), Permission::PULL, Some(RepositoryVisibility::Public)).await.unwrap() {
return unauthenticated_response(&state.config);
}
drop(database);
drop(auth_driver);
let database = &state.database;
let digest = match Digest::is_digest(&reference) {
@ -106,11 +108,11 @@ pub async fn pull_manifest_get(Path((name, reference)): Path<(String, String)>,
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_repository_permission(database, auth.user.username, name.clone(), Permission::PULL, Some(RepositoryVisibility::Public)).await.unwrap() {
return get_unauthenticated_response(&state.config);
let auth_driver = state.auth_checker.lock().await;
if !auth_driver.user_has_permission(auth.user.username, name.clone(), Permission::PULL, Some(RepositoryVisibility::Public)).await.unwrap() {
return unauthenticated_response(&state.config);
}
drop(database);
drop(auth_driver);
// Get the digest from the reference path.
let database = &state.database;
@ -146,9 +148,11 @@ pub async fn manifest_exists_head(Path((name, reference)): Path<(String, String)
}
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 auth_driver = state.auth_checker.lock().await;
if !auth_driver.user_has_permission(auth.user.username, name.clone(), Permission::PUSH, None).await.unwrap() {
return unauthenticated_response(&state.config);
}
drop(auth_driver);
let _authorization = headers.get("Authorization").unwrap(); // TODO: use authorization header

View File

@ -12,14 +12,15 @@ 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::auth_storage::{unauthenticated_response, AuthDriver};
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, )>, 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() {
let auth_driver = state.auth_checker.lock().await;
if auth_driver.user_has_permission(auth.user.username, name.clone(), Permission::PUSH, None).await.unwrap() {
debug!("Upload requested");
let uuid = uuid::Uuid::new_v4();
@ -34,13 +35,15 @@ pub async fn start_upload_post(Path((name, )): Path<(String, )>, Extension(auth)
).into_response();
}
get_unauthenticated_response(&state.config)
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);
let auth_driver = state.auth_checker.lock().await;
if !auth_driver.user_has_permission(auth.user.username, name.clone(), Permission::PUSH, None).await.unwrap() {
return unauthenticated_response(&state.config);
}
drop(auth_driver);
let storage = state.storage.lock().await;
let current_size = storage.digest_length(&layer_uuid).await.unwrap();
@ -95,9 +98,11 @@ pub async fn chunked_upload_layer_patch(Path((name, layer_uuid)): Path<(String,
}
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 auth_driver = state.auth_checker.lock().await;
if !auth_driver.user_has_permission(auth.user.username, name.clone(), Permission::PUSH, None).await.unwrap() {
return unauthenticated_response(&state.config);
}
drop(auth_driver);
let digest = query.get("digest").unwrap();
@ -122,9 +127,11 @@ pub async fn finish_chunked_upload_put(Path((name, layer_uuid)): Path<(String, S
}
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 auth_driver = state.auth_checker.lock().await;
if !auth_driver.user_has_permission(auth.user.username, name.clone(), Permission::PUSH, None).await.unwrap() {
return unauthenticated_response(&state.config);
}
drop(auth_driver);
let storage = state.storage.lock().await;
storage.delete_digest(&layer_uuid).await.unwrap();
@ -134,9 +141,11 @@ pub async fn cancel_upload_delete(Path((name, layer_uuid)): Path<(String, String
}
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 auth_driver = state.auth_checker.lock().await;
if !auth_driver.user_has_permission(auth.user.username, name.clone(), Permission::PUSH, None).await.unwrap() {
return unauthenticated_response(&state.config);
}
drop(auth_driver);
let storage = state.storage.lock().await;
let ending = storage.digest_length(&layer_uuid).await.unwrap().unwrap_or(0);

View File

@ -1,6 +1,6 @@
use sqlx::{Sqlite, Pool};
use crate::auth_storage::MemoryAuthStorage;
use crate::auth_storage::AuthDriver;
use crate::storage::StorageDriver;
use crate::config::Config;
@ -10,17 +10,17 @@ pub struct AppState {
pub database: Pool<Sqlite>,
pub storage: Mutex<Box<dyn StorageDriver>>,
pub config: Config,
pub auth_storage: Mutex<MemoryAuthStorage>,
pub auth_checker: Mutex<Box<dyn AuthDriver>>,
}
impl AppState {
pub fn new(database: Pool<Sqlite>, storage: Mutex<Box<dyn StorageDriver>>, config: Config) -> Self
pub fn new(database: Pool<Sqlite>, storage: Mutex<Box<dyn StorageDriver>>, config: Config, auth_checker: Mutex<Box<dyn AuthDriver>>) -> Self
{
Self {
database,
storage,
config,
auth_storage: Mutex::new(MemoryAuthStorage::new()),
auth_checker,
}
}
}

View File

@ -2,13 +2,66 @@ use std::{collections::HashSet, ops::Deref, sync::Arc};
use axum::{extract::{State, Path}, http::{StatusCode, HeaderMap, header, HeaderName, Request}, middleware::Next, response::{Response, IntoResponse}};
use sqlx::{Pool, Sqlite};
use tracing::debug;
use crate::{app_state::AppState, dto::{user::{Permission, RegistryUserType}, RepositoryVisibility}, config::Config};
use crate::database::Database;
use async_trait::async_trait;
#[async_trait]
pub trait AuthDriver: Send + Sync {
/// Checks if a user has permission to do something in a repository.
///
/// * `username`: Name of the user.
/// * `repository`: Name of the repository.
/// * `permissions`: Permission to check for.
/// * `required_visibility`: Specified if there is a specific visibility of the repository that will give the user permission.
async fn user_has_permission(&self, username: String, repository: String, permission: Permission, required_visibility: Option<RepositoryVisibility>) -> anyhow::Result<bool>;
async fn verify_user_login(&self, username: String, password: String) -> anyhow::Result<bool>;
}
#[async_trait]
impl AuthDriver for Pool<Sqlite> {
async fn user_has_permission(&self, username: String, repository: String, permission: Permission, required_visibility: Option<RepositoryVisibility>) -> anyhow::Result<bool> {
let allowed_to = {
match self.get_user_registry_type(username.clone()).await? {
Some(RegistryUserType::Admin) => true,
_ => {
if let Some(perms) = self.get_user_repo_permissions(username, repository.clone()).await? {
if perms.has_permission(permission) {
return Ok(true);
}
}
if let Some(vis) = required_visibility {
if let Some(repo_vis) = self.get_repository_visibility(&repository).await? {
if vis == repo_vis {
return Ok(true);
}
}
}
false
}
/* match database.get_user_repo_permissions(username, repository).await.unwrap() {
Some(perms) => if perms.has_permission(permission) { true } else { false },
_ => false,
} */
}
};
Ok(allowed_to)
}
async fn verify_user_login(&self, username: String, password: String) -> anyhow::Result<bool> {
Database::verify_user_login(self, username, password).await
}
}
/// Temporary struct for storing auth information in memory.
pub struct MemoryAuthStorage {
/* pub struct MemoryAuthStorage {
pub valid_tokens: HashSet<String>,
}
@ -18,7 +71,7 @@ impl MemoryAuthStorage {
valid_tokens: HashSet::new(),
}
}
}
} */
#[derive(Clone)]
pub struct AuthToken(pub String);
@ -73,49 +126,8 @@ pub async fn require_auth<B>(State(state): State<Arc<AppState>>, mut request: Re
}
}
pub async fn does_user_have_permission(database: &impl Database, username: String, repository: String, permission: Permission) -> anyhow::Result<bool> {
does_user_have_repository_permission(database, username, repository, permission, None).await
}
/// Checks if a user has permission to do something in a repository.
///
/// * `database`: Database connection.
/// * `username`: Name of the user.
/// * `repository`: Name of the repository.
/// * `permissions`: Permission to check for.
/// * `required_visibility`: Specified if there is a specific visibility of the repository that will give the user permission.
pub async fn does_user_have_repository_permission(database: &impl Database, username: String, repository: String, permission: Permission, required_visibility: Option<RepositoryVisibility>) -> anyhow::Result<bool> {
let allowed_to = {
match database.get_user_registry_type(username.clone()).await? {
Some(RegistryUserType::Admin) => true,
_ => {
if let Some(perms) = database.get_user_repo_permissions(username, repository.clone()).await? {
if perms.has_permission(permission) {
return Ok(true);
}
}
if let Some(vis) = required_visibility {
if let Some(repo_vis) = database.get_repository_visibility(&repository).await? {
if vis == repo_vis {
return Ok(true);
}
}
}
false
}
/* 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 {
#[inline(always)]
pub fn unauthenticated_response(config: &Config) -> Response {
let bearer = format!("Bearer realm=\"{}/auth\"", config.get_url());
(
StatusCode::UNAUTHORIZED,

View File

@ -62,4 +62,7 @@ CREATE TABLE IF NOT EXISTS user_tokens (
username TEXT NOT NULL,
expiry BIGINT NOT NULL,
created_at BIGINT NOT NULL
);
);
-- create admin user
INSERT OR IGNORE INTO users (username, email, password_hash, password_salt) VALUES ('admin', 'admin@example.com', '$2b$12$x5ECk0jUmOSfBWxW52wsyOmFxNZkwc2J9FH225if4eBnQYUvYLYYq', 'x5ECk0jUmOSfBWxW52wsyO');

View File

@ -12,6 +12,7 @@ use std::net::SocketAddr;
use std::str::FromStr;
use std::sync::Arc;
use auth_storage::AuthDriver;
use axum::http::{Request, StatusCode, header, HeaderName};
use axum::middleware::Next;
use axum::response::{Response, IntoResponse};
@ -66,11 +67,12 @@ async fn main() -> std::io::Result<()> {
pool.create_schema().await.unwrap();
let storage_driver: Mutex<Box<dyn StorageDriver>> = Mutex::new(Box::new(FilesystemDriver::new("registry/blobs")));
let auth_driver: Mutex<Box<dyn AuthDriver>> = Mutex::new(Box::new(pool.clone()));
let config = Config::new().expect("Failure to parse config!");
let app_addr = SocketAddr::from_str(&format!("{}:{}", config.listen_address, config.listen_port)).unwrap();
let state = Arc::new(AppState::new(pool, storage_driver, config));
let state = Arc::new(AppState::new(pool, storage_driver, config, auth_driver));
tracing_subscriber::fmt()
.with_max_level(Level::DEBUG)