restrict users access to repositories, fix bugs with pushing and pulling, and database bugs

This commit is contained in:
SeanOMik 2023-06-16 00:20:51 -04:00
parent b09757d382
commit 875a1ed2b7
Signed by: SeanOMik
GPG Key ID: 568F326C7EB33ACB
10 changed files with 114 additions and 37 deletions

7
dev-sql/create_user.sql Normal file
View File

@ -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);

View File

@ -3,8 +3,8 @@
- [x] ldap auth - [x] ldap auth
- [ ] permission stuff - [ ] permission stuff
- [ ] simple way to define users and their permissions through a "users.toml" - [ ] 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 - [x] 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 pull from their own repositories
- [ ] postgresql - [ ] postgresql
- [ ] prometheus metrics - [ ] prometheus metrics
- [ ] simple webui for managing the registry - [ ] simple webui for managing the registry

View File

@ -187,7 +187,11 @@ pub async fn auth_basic_get(basic_auth: Option<AuthBasic>, state: State<Arc<AppS
let now = SystemTime::now(); let now = SystemTime::now();
let token = create_jwt_token(account) let token = create_jwt_token(account)
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; .map_err(|_| {
error!("Failed to create jwt token!");
StatusCode::INTERNAL_SERVER_ERROR
})?;
let token_str = token.token; let token_str = token.token;
debug!("Created jwt token"); debug!("Created jwt token");
@ -208,7 +212,11 @@ pub async fn auth_basic_get(basic_auth: Option<AuthBasic>, state: State<Arc<AppS
let database = &state.database; let database = &state.database;
database.store_user_token(token_str.clone(), account.clone(), token.expiry, token.created_at).await database.store_user_token(token_str.clone(), account.clone(), token.expiry, token.created_at).await
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; .map_err(|_| {
error!("Failed to store user token in database!");
StatusCode::INTERNAL_SERVER_ERROR
})?;
drop(database); drop(database);
return Ok(( return Ok((

View File

@ -30,7 +30,7 @@ pub async fn upload_manifest_put(Path((name, reference)): Path<(String, String)>
let database = &state.database; let database = &state.database;
// Create the image repository and save the image manifest. This repository will be private by default // 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?; database.save_manifest(&name, &calculated_digest, &body).await?;
// If the reference is not a digest, then it must be a tag name. // If the reference is not a digest, then it must be a tag name.

View File

@ -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<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)>, Extension(auth): Extension<UserAuth>, state: State<Arc<AppState>>, mut body: BodyStream) -> Result<Response, AppError> {
let mut auth_driver = state.auth_checker.lock().await; let mut auth_driver = state.auth_checker.lock().await;
if !auth_driver.user_has_permission(auth.user.username, name.clone(), Permission::PUSH, None).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)); return Ok(unauthenticated_response(&state.config));
} }
drop(auth_driver); drop(auth_driver);

View File

@ -3,7 +3,7 @@ use ldap3::{LdapConnAsync, Ldap, Scope, SearchEntry};
use sqlx::{Pool, Sqlite}; use sqlx::{Pool, Sqlite};
use tracing::{debug, warn}; 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; use super::AuthDriver;
@ -60,6 +60,7 @@ impl AuthDriver for LdapAuthDriver {
Ok(true) Ok(true)
} else { } else {
debug!("LDAP is falling back to database"); debug!("LDAP is falling back to database");
// fall back to database auth since this user might be local // fall back to database auth since this user might be local
self.database.user_has_permission(email, repository, permission, required_visibility).await self.database.user_has_permission(email, repository, permission, required_visibility).await
} }

View File

@ -24,14 +24,25 @@ pub trait AuthDriver: Send + Sync {
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>;
} }
// Implement AuthDriver for anything the implements Database
#[async_trait] #[async_trait]
impl AuthDriver for Pool<Sqlite> { impl<T> AuthDriver for T
where
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 = { let allowed_to: bool = {
match self.get_user_registry_type(email.clone()).await? { if self.get_repository_owner(&repository).await?
Some(RegistryUserType::Admin) => true, .map_or(false, |owner| owner == email) {
_ => {
check_user_permissions(self, email, repository, permission, required_visibility).await? 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?
}
} }
} }
}; };

View File

@ -1,6 +1,6 @@
use async_trait::async_trait; use async_trait::async_trait;
use sqlx::{Sqlite, Pool}; use sqlx::{Sqlite, Pool};
use tracing::debug; use tracing::{debug, warn};
use chrono::{DateTime, Utc, NaiveDateTime, TimeZone}; use chrono::{DateTime, Utc, NaiveDateTime, TimeZone};
@ -42,8 +42,9 @@ pub trait Database {
async fn has_repository(&self, repository: &str) -> anyhow::Result<bool>; async fn has_repository(&self, repository: &str) -> anyhow::Result<bool>;
async fn get_repository_visibility(&self, repository: &str) -> anyhow::Result<Option<RepositoryVisibility>>; async fn get_repository_visibility(&self, repository: &str) -> anyhow::Result<Option<RepositoryVisibility>>;
async fn get_repository_owner(&self, repository: &str) -> anyhow::Result<Option<String>>;
/// Create a repository /// Create a repository
async fn save_repository(&self, repository: &str, visibility: RepositoryVisibility, owning_project: Option<String>) -> anyhow::Result<()>; async fn save_repository(&self, repository: &str, visibility: RepositoryVisibility, owner_email: Option<String>, owning_project: Option<String>) -> anyhow::Result<()>;
/// List all repositories. /// List all repositories.
/// If limit is not specified, a default limit of 1000 will be returned. /// If limit is not specified, a default limit of 1000 will be returned.
async fn list_repositories(&self, limit: Option<u32>, last_repo: Option<String>) -> anyhow::Result<Vec<String>>; async fn list_repositories(&self, limit: Option<u32>, last_repo: Option<String>) -> anyhow::Result<Vec<String>>;
@ -52,6 +53,7 @@ pub trait Database {
/// User stuff /// User stuff
async fn does_user_exist(&self, email: String) -> anyhow::Result<bool>; async fn does_user_exist(&self, email: String) -> anyhow::Result<bool>;
async fn create_user(&self, email: String, username: String, login_source: LoginSource) -> anyhow::Result<User>; async fn create_user(&self, email: String, username: String, login_source: LoginSource) -> anyhow::Result<User>;
async fn get_user(&self, email: String) -> anyhow::Result<Option<User>>;
async fn add_user_auth(&self, email: String, password_hash: String, password_salt: 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 set_user_registry_type(&self, email: String, user_type: RegistryUserType) -> anyhow::Result<()>;
async fn verify_user_login(&self, email: String, password: String) -> anyhow::Result<bool>; async fn verify_user_login(&self, email: String, password: String) -> anyhow::Result<bool>;
@ -254,7 +256,7 @@ impl Database for Pool<Sqlite> {
} }
async fn get_repository_visibility(&self, repository: &str) -> anyhow::Result<Option<RepositoryVisibility>> { 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' = ?") let row: (u32, ) = match sqlx::query_as("SELECT visibility FROM repositories WHERE name = ?")
.bind(repository) .bind(repository)
.fetch_one(self).await { .fetch_one(self).await {
Ok(row) => row, Ok(row) => row,
@ -271,29 +273,42 @@ impl Database for Pool<Sqlite> {
Ok(Some(RepositoryVisibility::try_from(row.0)?)) Ok(Some(RepositoryVisibility::try_from(row.0)?))
} }
async fn save_repository(&self, repository: &str, visibility: RepositoryVisibility, owning_project: Option<String>) -> anyhow::Result<()> { async fn get_repository_owner(&self, repository: &str) -> anyhow::Result<Option<String>> {
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<String>, owning_project: Option<String>) -> anyhow::Result<()> {
// ensure that the repository was not already created // ensure that the repository was not already created
if self.has_repository(repository).await? { if self.has_repository(repository).await? {
debug!("repo exists"); debug!("Skipping creation of repository since it already exists");
return Ok(()); return Ok(());
} }
debug!("repo does not exist");
match owning_project { // unwrap None values to empty for inserting into database
Some(owner) => { let owner_email = owner_email.unwrap_or(String::new());
sqlx::query("INSERT INTO repositories (name, visibility, owning_project) VALUES (?, ?, ?)") let owning_project = owning_project.unwrap_or(String::new());
.bind(repository)
.bind(visibility as u32) sqlx::query("INSERT INTO repositories (name, visibility, owner_email, owning_project) VALUES (?, ?, ?, ?)")
.bind(owner) .bind(repository)
.execute(self).await?; .bind(visibility as u32)
}, .bind(owner_email)
None => { .bind(owning_project)
sqlx::query("INSERT INTO repositories (name, visibility) VALUES (?, ?)") .execute(self).await?;
.bind(repository)
.bind(visibility as u32)
.execute(self).await?;
}
}
Ok(()) Ok(())
} }
@ -353,6 +368,25 @@ impl Database for Pool<Sqlite> {
Ok(User::new(username, email, login_source)) Ok(User::new(username, email, login_source))
} }
async fn get_user(&self, email: String) -> anyhow::Result<Option<User>> {
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<()> { async fn add_user_auth(&self, email: String, password_hash: String, password_salt: String) -> anyhow::Result<()> {
let email = email.to_lowercase(); let email = email.to_lowercase();
sqlx::query("INSERT INTO user_logins (email, password_hash, password_salt) VALUES (?, ?, ?)") sqlx::query("INSERT INTO user_logins (email, password_hash, password_salt) VALUES (?, ?, ?)")
@ -376,7 +410,7 @@ impl Database for Pool<Sqlite> {
async fn verify_user_login(&self, email: String, password: String) -> anyhow::Result<bool> { async fn verify_user_login(&self, email: String, password: String) -> anyhow::Result<bool> {
let email = email.to_lowercase(); 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) .bind(email)
.fetch_one(self).await?; .fetch_one(self).await?;
@ -405,6 +439,8 @@ impl Database for Pool<Sqlite> {
async fn get_user_repo_permissions(&self, email: String, repository: String) -> anyhow::Result<Option<RepositoryPermissions>> { async fn get_user_repo_permissions(&self, email: String, repository: String) -> anyhow::Result<Option<RepositoryPermissions>> {
let email = email.to_lowercase(); 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 = ?") let row: (u32, ) = match sqlx::query_as("SELECT repository_permissions FROM user_repo_permissions WHERE email = ? AND repository_name = ?")
.bind(email.clone()) .bind(email.clone())
@ -423,13 +459,17 @@ impl Database for Pool<Sqlite> {
let vis = match self.get_repository_visibility(&repository).await? { let vis = match self.get_repository_visibility(&repository).await? {
Some(v) => v, 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 // 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? { let utype = match self.get_user_registry_usertype(email).await? {
Some(t) => t, Some(t) => t,
None => return Ok(None), // assume a regular user is their type is not found
None => RegistryUserType::Regular,
}; };
if utype == RegistryUserType::Admin { if utype == RegistryUserType::Admin {

View File

@ -7,6 +7,7 @@ CREATE TABLE IF NOT EXISTS projects (
CREATE TABLE IF NOT EXISTS repositories ( CREATE TABLE IF NOT EXISTS repositories (
name TEXT NOT NULL UNIQUE PRIMARY KEY, name TEXT NOT NULL UNIQUE PRIMARY KEY,
owning_project TEXT, owning_project TEXT,
owner_email TEXT,
-- 0 = private, 1 = public -- 0 = private, 1 = public
visibility INTEGER NOT NULL visibility INTEGER NOT NULL
); );
@ -72,4 +73,5 @@ CREATE TABLE IF NOT EXISTS user_tokens (
-- create admin user -- create admin user
INSERT OR IGNORE INTO users (username, email, login_source) VALUES ('admin', 'admin@example.com', 0); 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'); 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);

View File

@ -25,6 +25,11 @@ impl FilesystemDriver {
fn get_digest_path(&self, digest: &str) -> String { fn get_digest_path(&self, digest: &str) -> String {
format!("{}/{}", self.storage_path, digest) format!("{}/{}", self.storage_path, digest)
} }
fn ensure_storage_path(&self) -> std::io::Result<()>
{
std::fs::create_dir_all(&self.storage_path)
}
} }
#[async_trait] #[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<usize> { async fn save_digest_stream(&self, digest: &str, mut stream: ByteStream, append: bool) -> anyhow::Result<usize> {
self.ensure_storage_path()?;
let path = self.get_digest_path(digest); let path = self.get_digest_path(digest);
let mut file = fs::OpenOptions::new() let mut file = fs::OpenOptions::new()
.write(true) .write(true)