diff --git a/dev-sql/create_user.sql b/dev-sql/create_user.sql new file mode 100644 index 0000000..82b858f --- /dev/null +++ b/dev-sql/create_user.sql @@ -0,0 +1,7 @@ +--- Creates a regular user with the password 'test' +INSERT OR IGNORE INTO users (username, email, login_source) VALUES ('test', 'test@example.com', 0); +INSERT OR IGNORE INTO user_logins (email, password_hash, password_salt) VALUES ('test@example.com', '$2y$05$k3gn.RxGxh59NhtyyiWPeeQ2J9kqVaImiL3GPuBjMsiJ51Bn3js.K', 'x5ECk0jUmOSfBWxW52wsyO'); +INSERT OR IGNORE INTO user_registry_permissions (email, user_type) VALUES ('test@example.com', 0); + +-- example of giving this user pull access to a repository +--INSERT OR IGNORE INTO user_repo_permissions (email, repository_name, repository_permissions) VALUES ('test@example.com', 'admin/alpine', 1); \ No newline at end of file diff --git a/docs/todo.md b/docs/todo.md index 6750d33..ce4b80b 100644 --- a/docs/todo.md +++ b/docs/todo.md @@ -3,8 +3,8 @@ - [x] ldap auth - [ ] permission stuff - [ ] simple way to define users and their permissions through a "users.toml" - - [ ] Only allow users to create repositories if its the same name as their username, or if they're an admin - - [ ] Only allow users to pull from their own repositories + - [x] Only allow users to create repositories if its the same name as their username, or if they're an admin + - [x] Only allow users to pull from their own repositories - [ ] postgresql - [ ] prometheus metrics - [ ] simple webui for managing the registry diff --git a/src/api/auth.rs b/src/api/auth.rs index b24a24b..01f99d7 100644 --- a/src/api/auth.rs +++ b/src/api/auth.rs @@ -187,7 +187,11 @@ pub async fn auth_basic_get(basic_auth: Option, state: State, state: State let database = &state.database; // Create the image repository and save the image manifest. This repository will be private by default - database.save_repository(&name, RepositoryVisibility::Private, None).await?; + database.save_repository(&name, RepositoryVisibility::Private, Some(auth.user.email), None).await?; database.save_manifest(&name, &calculated_digest, &body).await?; // If the reference is not a digest, then it must be a tag name. diff --git a/src/api/uploads.rs b/src/api/uploads.rs index 79a4f47..272bd86 100644 --- a/src/api/uploads.rs +++ b/src/api/uploads.rs @@ -41,6 +41,7 @@ pub async fn start_upload_post(Path((name, )): Path<(String, )>, Extension(auth) pub async fn chunked_upload_layer_patch(Path((name, layer_uuid)): Path<(String, String)>, Extension(auth): Extension, state: State>, mut body: BodyStream) -> Result { let mut auth_driver = state.auth_checker.lock().await; if !auth_driver.user_has_permission(auth.user.username, name.clone(), Permission::PUSH, None).await? { + debug!("user is not authenticated"); return Ok(unauthenticated_response(&state.config)); } drop(auth_driver); diff --git a/src/auth/ldap_driver.rs b/src/auth/ldap_driver.rs index 155e5b1..b66933b 100644 --- a/src/auth/ldap_driver.rs +++ b/src/auth/ldap_driver.rs @@ -3,7 +3,7 @@ use ldap3::{LdapConnAsync, Ldap, Scope, SearchEntry}; use sqlx::{Pool, Sqlite}; use tracing::{debug, warn}; -use crate::{config::LdapConnectionConfig, dto::{user::{Permission, LoginSource, RegistryUserType}, RepositoryVisibility}, database::Database}; +use crate::{config::LdapConnectionConfig, dto::{user::{Permission, LoginSource, RegistryUserType, self}, RepositoryVisibility}, database::Database}; use super::AuthDriver; @@ -60,6 +60,7 @@ impl AuthDriver for LdapAuthDriver { Ok(true) } else { debug!("LDAP is falling back to database"); + // fall back to database auth since this user might be local self.database.user_has_permission(email, repository, permission, required_visibility).await } diff --git a/src/auth/mod.rs b/src/auth/mod.rs index 8ef409e..039b191 100644 --- a/src/auth/mod.rs +++ b/src/auth/mod.rs @@ -24,14 +24,25 @@ pub trait AuthDriver: Send + Sync { async fn verify_user_login(&mut self, email: String, password: String) -> anyhow::Result; } +// Implement AuthDriver for anything the implements Database #[async_trait] -impl AuthDriver for Pool { +impl AuthDriver for T +where + T: Database + Send + Sync +{ async fn user_has_permission(&mut self, email: String, repository: String, permission: Permission, required_visibility: Option) -> anyhow::Result { - let allowed_to = { - match self.get_user_registry_type(email.clone()).await? { - Some(RegistryUserType::Admin) => true, - _ => { - check_user_permissions(self, email, repository, permission, required_visibility).await? + let allowed_to: bool = { + if self.get_repository_owner(&repository).await? + .map_or(false, |owner| owner == email) { + + debug!("Allowing request, user is owner of repository"); + true + } else { + match self.get_user_registry_type(email.clone()).await? { + Some(RegistryUserType::Admin) => true, + _ => { + check_user_permissions(self, email, repository, permission, required_visibility).await? + } } } }; diff --git a/src/database/mod.rs b/src/database/mod.rs index 974770f..42b9f75 100644 --- a/src/database/mod.rs +++ b/src/database/mod.rs @@ -1,6 +1,6 @@ use async_trait::async_trait; use sqlx::{Sqlite, Pool}; -use tracing::debug; +use tracing::{debug, warn}; use chrono::{DateTime, Utc, NaiveDateTime, TimeZone}; @@ -42,8 +42,9 @@ pub trait Database { async fn has_repository(&self, repository: &str) -> anyhow::Result; async fn get_repository_visibility(&self, repository: &str) -> anyhow::Result>; + async fn get_repository_owner(&self, repository: &str) -> anyhow::Result>; /// Create a repository - async fn save_repository(&self, repository: &str, visibility: RepositoryVisibility, owning_project: Option) -> anyhow::Result<()>; + async fn save_repository(&self, repository: &str, visibility: RepositoryVisibility, owner_email: Option, owning_project: Option) -> anyhow::Result<()>; /// List all repositories. /// If limit is not specified, a default limit of 1000 will be returned. async fn list_repositories(&self, limit: Option, last_repo: Option) -> anyhow::Result>; @@ -52,6 +53,7 @@ pub trait Database { /// User stuff async fn does_user_exist(&self, email: String) -> anyhow::Result; async fn create_user(&self, email: String, username: String, login_source: LoginSource) -> anyhow::Result; + async fn get_user(&self, email: String) -> anyhow::Result>; async fn add_user_auth(&self, email: String, password_hash: String, password_salt: String) -> anyhow::Result<()>; async fn set_user_registry_type(&self, email: String, user_type: RegistryUserType) -> anyhow::Result<()>; async fn verify_user_login(&self, email: String, password: String) -> anyhow::Result; @@ -254,7 +256,7 @@ impl Database for Pool { } async fn get_repository_visibility(&self, repository: &str) -> anyhow::Result> { - let row: (u32, ) = match sqlx::query_as("SELECT visibility FROM repositories WHERE 'name' = ?") + let row: (u32, ) = match sqlx::query_as("SELECT visibility FROM repositories WHERE name = ?") .bind(repository) .fetch_one(self).await { Ok(row) => row, @@ -271,29 +273,42 @@ impl Database for Pool { Ok(Some(RepositoryVisibility::try_from(row.0)?)) } - async fn save_repository(&self, repository: &str, visibility: RepositoryVisibility, owning_project: Option) -> anyhow::Result<()> { + async fn get_repository_owner(&self, repository: &str) -> anyhow::Result> { + let row: (String, ) = match sqlx::query_as("SELECT owner_email FROM repositories WHERE name = ?") + .bind(repository) + .fetch_one(self).await { + Ok(row) => row, + Err(e) => match e { + sqlx::Error::RowNotFound => { + return Ok(None) + }, + _ => { + debug!("here's the error: {:?}", e); + return Err(anyhow::Error::new(e)); + } + } + }; + + Ok(Some(row.0)) + } + + async fn save_repository(&self, repository: &str, visibility: RepositoryVisibility, owner_email: Option, owning_project: Option) -> anyhow::Result<()> { // ensure that the repository was not already created if self.has_repository(repository).await? { - debug!("repo exists"); + debug!("Skipping creation of repository since it already 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?; - } - } + // unwrap None values to empty for inserting into database + let owner_email = owner_email.unwrap_or(String::new()); + let owning_project = owning_project.unwrap_or(String::new()); + + sqlx::query("INSERT INTO repositories (name, visibility, owner_email, owning_project) VALUES (?, ?, ?, ?)") + .bind(repository) + .bind(visibility as u32) + .bind(owner_email) + .bind(owning_project) + .execute(self).await?; Ok(()) } @@ -353,6 +368,25 @@ impl Database for Pool { Ok(User::new(username, email, login_source)) } + async fn get_user(&self, email: String) -> anyhow::Result> { + let email = email.to_lowercase(); + let row: (String, u32) = match sqlx::query_as("SELECT username, login_source FROM users WHERE email = ?") + .bind(email.clone()) + .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(User::new(row.0, email, LoginSource::try_from(row.1)?))) + } + async fn add_user_auth(&self, email: String, password_hash: String, password_salt: String) -> anyhow::Result<()> { let email = email.to_lowercase(); sqlx::query("INSERT INTO user_logins (email, password_hash, password_salt) VALUES (?, ?, ?)") @@ -376,7 +410,7 @@ impl Database for Pool { async fn verify_user_login(&self, email: String, password: String) -> anyhow::Result { let email = email.to_lowercase(); - let row: (String, ) = sqlx::query_as("SELECT password_hash FROM users WHERE email = ?") + let row: (String, ) = sqlx::query_as("SELECT password_hash FROM user_logins WHERE email = ?") .bind(email) .fetch_one(self).await?; @@ -405,6 +439,8 @@ impl Database for Pool { async fn get_user_repo_permissions(&self, email: String, repository: String) -> anyhow::Result> { let email = email.to_lowercase(); + + debug!("email: {email}, repo: {repository}"); let row: (u32, ) = match sqlx::query_as("SELECT repository_permissions FROM user_repo_permissions WHERE email = ? AND repository_name = ?") .bind(email.clone()) @@ -423,13 +459,17 @@ impl Database for Pool { let vis = match self.get_repository_visibility(&repository).await? { Some(v) => v, - None => return Ok(None), + None => { + warn!("Failure to find visibility for repository '{}'", repository); + return Ok(None) + }, }; // Also get the user type for the registry, if its admin return admin repository permissions let utype = match self.get_user_registry_usertype(email).await? { Some(t) => t, - None => return Ok(None), + // assume a regular user is their type is not found + None => RegistryUserType::Regular, }; if utype == RegistryUserType::Admin { diff --git a/src/database/schemas/schema.sql b/src/database/schemas/schema.sql index 7af27d4..cb96632 100644 --- a/src/database/schemas/schema.sql +++ b/src/database/schemas/schema.sql @@ -7,6 +7,7 @@ CREATE TABLE IF NOT EXISTS projects ( CREATE TABLE IF NOT EXISTS repositories ( name TEXT NOT NULL UNIQUE PRIMARY KEY, owning_project TEXT, + owner_email TEXT, -- 0 = private, 1 = public visibility INTEGER NOT NULL ); @@ -72,4 +73,5 @@ CREATE TABLE IF NOT EXISTS user_tokens ( -- create admin user INSERT OR IGNORE INTO users (username, email, login_source) VALUES ('admin', 'admin@example.com', 0); -INSERT OR IGNORE INTO user_logins (email, password_hash, password_salt) VALUES ('admin@example.com', '$2b$12$x5ECk0jUmOSfBWxW52wsyOmFxNZkwc2J9FH225if4eBnQYUvYLYYq', 'x5ECk0jUmOSfBWxW52wsyO'); \ No newline at end of file +INSERT OR IGNORE INTO user_logins (email, password_hash, password_salt) VALUES ('admin@example.com', '$2y$05$ZBnzGzctboHkUDMr4W02jOaUuPwmRC2OgWKKBxqiQsYv53OkUrfO6', 'x5ECk0jUmOSfBWxW52wsyO'); +INSERT OR IGNORE INTO user_registry_permissions (email, user_type) VALUES ('admin@example.com', 1); \ No newline at end of file diff --git a/src/storage/filesystem.rs b/src/storage/filesystem.rs index 1fca687..b62c090 100644 --- a/src/storage/filesystem.rs +++ b/src/storage/filesystem.rs @@ -25,6 +25,11 @@ impl FilesystemDriver { fn get_digest_path(&self, digest: &str) -> String { format!("{}/{}", self.storage_path, digest) } + + fn ensure_storage_path(&self) -> std::io::Result<()> + { + std::fs::create_dir_all(&self.storage_path) + } } #[async_trait] @@ -40,6 +45,8 @@ impl StorageDriver for FilesystemDriver { } async fn save_digest_stream(&self, digest: &str, mut stream: ByteStream, append: bool) -> anyhow::Result { + self.ensure_storage_path()?; + let path = self.get_digest_path(digest); let mut file = fs::OpenOptions::new() .write(true)