diff --git a/crates/core/src/database/user/auth_token.rs b/crates/core/src/database/user/auth_token.rs index 2fc63dde..892d9e4f 100644 --- a/crates/core/src/database/user/auth_token.rs +++ b/crates/core/src/database/user/auth_token.rs @@ -5,7 +5,7 @@ use uuid::Uuid; use crate::{ database::DateTime, - user::{permissions::RepositoryActionOptions, scopes::Scopes}, + user::{permissions::RepositoryActions, scopes::Scopes}, }; use super::ReferencesUser; @@ -73,7 +73,7 @@ impl AuthToken { pub async fn has_repository_action( &self, repository_id: Uuid, - repository_action: RepositoryActionOptions, + repository_action: RepositoryActions, database: &PgPool, ) -> sqlx::Result { // Check if the user has the general scope. See RepositoryActions for more info @@ -82,7 +82,7 @@ impl AuthToken { return Ok(true); } // TODO condense this into one query - let Some(actions) = sqlx::query_scalar::<_, Vec>( + let Some(actions) = sqlx::query_scalar::<_, Vec>( r#"SELECT actions FROM user_auth_token_repository_scopes WHERE user_auth_token_id = $1 AND repository_id = $2"#, ) .bind(self.id) diff --git a/crates/core/src/database/user/auth_token/repository_scope.rs b/crates/core/src/database/user/auth_token/repository_scope.rs index 375d4144..a96a3382 100644 --- a/crates/core/src/database/user/auth_token/repository_scope.rs +++ b/crates/core/src/database/user/auth_token/repository_scope.rs @@ -7,7 +7,7 @@ use sqlx::{ use tracing::{debug, instrument, span}; use uuid::Uuid; -use crate::{database::DateTime, user::permissions::RepositoryActionOptions}; +use crate::{database::DateTime, user::permissions::RepositoryActions}; use super::{create_token, hash_token}; /// Represents the actions that can be taken on a repository @@ -22,7 +22,7 @@ pub struct AuthTokenRepositoryScope { pub id: i32, pub user_auth_token_id: i32, pub repository_id: Uuid, - pub action: Vec, + pub action: Vec, pub created_at: DateTime, } @@ -30,7 +30,7 @@ pub struct AuthTokenRepositoryScope { pub struct NewRepositoryToken { pub user_id: i32, pub source: String, - pub repositories: Vec<(Uuid, Vec)>, + pub repositories: Vec<(Uuid, Vec)>, pub expires_at: Option, } impl NewRepositoryToken { @@ -38,7 +38,7 @@ impl NewRepositoryToken { user_id: i32, source: String, repository: Uuid, - actions: Vec, + actions: Vec, ) -> Self { Self { user_id, @@ -47,11 +47,7 @@ impl NewRepositoryToken { expires_at: None, } } - pub fn add_repository( - mut self, - repository: Uuid, - actions: Vec, - ) -> Self { + pub fn add_repository(mut self, repository: Uuid, actions: Vec) -> Self { self.repositories.push((repository, actions)); self } @@ -92,7 +88,7 @@ impl NewRepositoryToken { pub struct NewRepositoryScope { pub token_id: i32, pub repository: Uuid, - pub actions: Vec, + pub actions: Vec, } impl NewRepositoryScope { #[instrument] @@ -103,7 +99,7 @@ impl NewRepositoryScope { actions, } = self; sqlx::query( - r#"INSERT INTO user_auth_token_repository_scopes (user_auth_token_id, repository, actions) VALUES ($1, $2, $3)"#, + r#"INSERT INTO user_auth_token_repository_scopes (user_auth_token_id, repository_id, actions) VALUES ($1, $2, $3)"#, ) .bind(token_id) .bind(repository) diff --git a/crates/core/src/database/user/mod.rs b/crates/core/src/database/user/mod.rs index 04c52357..7412b784 100644 --- a/crates/core/src/database/user/mod.rs +++ b/crates/core/src/database/user/mod.rs @@ -3,13 +3,14 @@ use sqlx::{postgres::PgRow, types::Json, FromRow, PgPool}; use utoipa::ToSchema; use crate::user::{ - permissions::{HasPermissions, RepositoryActionOptions, UserPermissions}, + permissions::{HasPermissions, RepositoryActions, UserPermissions}, Email, Username, }; use super::DateTime; pub mod auth_token; pub mod password_reset; +pub mod permissions; pub mod user_utils; /// Implements on types that references a user in the database. /// @@ -181,12 +182,10 @@ pub struct UserModel { pub require_password_change: bool, pub admin: bool, pub user_manager: bool, - /// Storage Manager will be able to create and delete storage locations - pub storage_manager: bool, /// Repository Manager will be able to create and delete repositories /// Also will have full read/write access to all repositories - pub repository_manager: bool, - pub default_repository_actions: Vec, + pub system_manager: bool, + pub default_repository_actions: Vec, pub updated_at: DateTime, pub created_at: DateTime, } @@ -223,12 +222,10 @@ pub struct UserSafeData { pub active: bool, pub admin: bool, pub user_manager: bool, - /// Storage Manager will be able to create and delete storage locations - pub storage_manager: bool, /// Repository Manager will be able to create and delete repositories /// Also will have full read/write access to all repositories - pub repository_manager: bool, - pub default_repository_actions: Vec, + pub system_manager: bool, + pub default_repository_actions: Vec, pub updated_at: DateTime, pub created_at: DateTime, } @@ -243,8 +240,7 @@ impl UserType for UserSafeData { "active", "admin", "user_manager", - "storage_manager", - "repository_manager", + "system_manager", "default_repository_actions", "updated_at", "created_at", @@ -265,8 +261,7 @@ impl HasPermissions for UserSafeData { id: self.id, admin: self.admin, user_manager: self.user_manager, - storage_manager: self.storage_manager, - repository_manager: self.repository_manager, + system_manager: self.system_manager, default_repository_actions: self.default_repository_actions.clone(), }) } @@ -284,8 +279,7 @@ impl From for UserSafeData { created_at: user.created_at, admin: user.admin, user_manager: user.user_manager, - storage_manager: user.storage_manager, - repository_manager: user.repository_manager, + system_manager: user.system_manager, default_repository_actions: user.default_repository_actions, } } @@ -307,8 +301,7 @@ mod tests { require_password_change: Default::default(), admin: Default::default(), user_manager: Default::default(), - storage_manager: Default::default(), - repository_manager: Default::default(), + system_manager: Default::default(), default_repository_actions: Default::default(), updated_at: Default::default(), created_at: Default::default(), diff --git a/crates/core/src/database/user/permissions.rs b/crates/core/src/database/user/permissions.rs new file mode 100644 index 00000000..a4fa55e7 --- /dev/null +++ b/crates/core/src/database/user/permissions.rs @@ -0,0 +1,125 @@ +use std::collections::HashMap; + +use serde::{Deserialize, Serialize}; +use sqlx::{prelude::FromRow, PgPool}; +use tracing::instrument; +use utoipa::ToSchema; +use uuid::Uuid; + +use crate::{ + database::DateTime, + user::permissions::{RepositoryActions, UserPermissions}, +}; + +#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone, ToSchema, FromRow)] + +pub struct UserRepositoryPermissions { + pub id: i32, + pub user_id: i32, + pub repository_id: Uuid, + pub actions: Vec, + pub updated_at: DateTime, + pub created_at: DateTime, +} +impl UserRepositoryPermissions { + pub async fn has_repository_action( + user_id: i32, + repository: Uuid, + action: RepositoryActions, + database: &PgPool, + ) -> sqlx::Result { + let Some(actions) = sqlx::query_scalar::<_, Vec>( + r#"SELECT * FROM user_repository_permissions WHERE user_id = $1 AND repository_id = $2 "#, + ) + .bind(user_id) + .bind(repository) + .fetch_optional(database) + .await? else{ + return Ok(false); + }; + Ok(actions.contains(&action)) + } + pub async fn get_all_for_user_as_map( + user_id: i32, + database: &PgPool, + ) -> sqlx::Result>> { + let permissions = sqlx::query_scalar::<_, (Uuid, Vec)>( + r#"SELECT (repository_id, actions) FROM user_repository_permissions WHERE user_id = $1"#, + ) + .bind(user_id) + .fetch_all(database) + .await?; + let mut map = HashMap::new(); + for (repository, actions) in permissions { + map.insert(repository, actions); + } + Ok(map) + } + pub async fn delete(user_id: i32, repository_id: Uuid, database: &PgPool) -> sqlx::Result<()> { + sqlx::query( + r#"DELETE FROM user_repository_permissions WHERE user_id = $1 AND repository_id = $2"#, + ) + .bind(user_id) + .bind(repository_id) + .execute(database) + .await?; + Ok(()) + } +} +#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone, ToSchema, FromRow)] + +pub struct NewUserRepositoryPermissions { + pub user_id: i32, + pub repository_id: Uuid, + pub actions: Vec, +} +impl NewUserRepositoryPermissions { + #[instrument] + pub async fn insert(self, database: &PgPool) -> sqlx::Result { + let row:i32 = sqlx::query_scalar( + r#"INSERT INTO user_repository_permissions (user_id, repository_id, actions) VALUES ($1, $2, $3) + ON CONFLICT (user_id, repository_id) DO UPDATE SET actions = $3 + RETURNING id"#, + ) + .bind(self.user_id) + .bind(self.repository_id) + .bind(self.actions) + .fetch_one(database) + .await?; + Ok(row) + } +} +#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone, ToSchema)] +pub struct FullUserPermissions { + pub user_id: i32, + pub admin: bool, + pub user_manager: bool, + /// Repository Manager will be able to create and delete repositories + /// Also will have full read/write access to all repositories + pub system_manager: bool, + pub default_repository_actions: Vec, + pub repository_permissions: HashMap>, +} +impl FullUserPermissions { + pub async fn get_by_id(user_id: i32, database: &PgPool) -> sqlx::Result> { + let permissions = + sqlx::query_as::<_, UserPermissions>(r#"SELECT * FROM users WHERE id = $1"#) + .bind(user_id) + .fetch_optional(database) + .await?; + let Some(permissions) = permissions else { + return Ok(None); + }; + let repository_permissions = + UserRepositoryPermissions::get_all_for_user_as_map(user_id, database).await?; + let permissions = FullUserPermissions { + user_id, + admin: permissions.admin, + user_manager: permissions.user_manager, + system_manager: permissions.system_manager, + default_repository_actions: permissions.default_repository_actions, + repository_permissions, + }; + Ok(Some(permissions)) + } +} diff --git a/crates/core/src/user/permissions.rs b/crates/core/src/user/permissions.rs index b31fb506..22859d47 100644 --- a/crates/core/src/user/permissions.rs +++ b/crates/core/src/user/permissions.rs @@ -3,13 +3,16 @@ use std::{collections::HashMap, fmt::Debug}; use serde::{Deserialize, Serialize}; use sqlx::{ prelude::{FromRow, Type}, - PgPool, + Execute, PgPool, QueryBuilder, }; -use tracing::{debug, instrument, trace}; +use tracing::{debug, info, instrument, trace, warn}; use utoipa::ToSchema; use uuid::Uuid; -use crate::database::{user::auth_token::AuthToken, DateTime}; +use crate::database::user::{ + auth_token::AuthToken, + permissions::{NewUserRepositoryPermissions, UserRepositoryPermissions}, +}; use super::scopes::Scopes; /// User permissions @@ -20,56 +23,10 @@ pub struct UserPermissions { pub id: i32, pub admin: bool, pub user_manager: bool, - /// Storage Manager will be able to create and delete storage locations - pub storage_manager: bool, /// Repository Manager will be able to create and delete repositories /// Also will have full read/write access to all repositories - pub repository_manager: bool, - pub default_repository_actions: Vec, -} -#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone, ToSchema, FromRow)] - -pub struct UserRepositoryPermissions { - pub id: i32, - pub user_id: i32, - pub repository_id: Uuid, - pub actions: Vec, - pub updated_at: DateTime, - pub created_at: DateTime, -} -impl UserRepositoryPermissions { - pub async fn has_repository_action( - user_id: i32, - repository: Uuid, - action: RepositoryActionOptions, - database: &PgPool, - ) -> sqlx::Result { - let Some(actions) = sqlx::query_scalar::<_, Vec>( - r#"SELECT * FROM user_repository_permissions WHERE user_id = $1 AND repository_id = $2 "#, - ) - .bind(user_id) - .bind(repository) - .fetch_optional(database) - .await? else{ - return Ok(false); - }; - Ok(actions.contains(&action)) - } -} -#[derive(Debug, Serialize, Deserialize, Clone, ToSchema)] -pub struct UpdatePermissions { - pub admin: Option, - pub user_manager: Option, - pub storage_manager: Option, - pub repository_manager: Option, - pub default_repository_actions: Option, - pub repository_permissions: Option>, -} - -impl UpdatePermissions { - pub fn apply(self, permissions: &mut UserPermissions) { - todo!() - } + pub system_manager: bool, + pub default_repository_actions: Vec, } impl HasPermissions for UserPermissions { @@ -104,24 +61,19 @@ pub trait HasPermissions { } /// Is the user an admin or repository manager #[inline(always)] - fn is_admin_or_repository_manager(&self) -> bool { - self.get_permissions() - .map(|p| p.admin || p.repository_manager) - .unwrap_or(false) - } - #[inline(always)] - fn is_admin_or_storage_manager(&self) -> bool { + fn is_admin_or_system_manager(&self) -> bool { self.get_permissions() - .map(|p| p.admin || p.storage_manager) + .map(|p| p.admin || p.system_manager) .unwrap_or(false) } + async fn has_action( &self, - action: RepositoryActionOptions, + action: RepositoryActions, repository: Uuid, db: &PgPool, ) -> Result { - if self.is_admin_or_repository_manager() { + if self.is_admin_or_system_manager() { return Ok(true); } let Some(user_id) = self.user_id() else { @@ -137,7 +89,7 @@ pub trait HasPermissions { pub async fn does_user_and_token_have_repository_action( user: &T, token: &AuthToken, - action: RepositoryActionOptions, + action: RepositoryActions, repository: Uuid, database: &PgPool, ) -> sqlx::Result { @@ -150,44 +102,111 @@ pub async fn does_user_and_token_have_repository_action bool { - match action { - RepositoryActionOptions::Read => self.can_read, - RepositoryActionOptions::Write => self.can_write, - RepositoryActionOptions::Edit => self.can_edit, - } - } -} #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Type)] #[sqlx(type_name = "TEXT")] -pub enum RepositoryActionOptions { +pub enum RepositoryActions { Read, Write, Edit, } -impl RepositoryActionOptions { +impl RepositoryActions { pub fn all() -> Vec { vec![Self::Read, Self::Write, Self::Edit] } } -impl From for Scopes { - fn from(action: RepositoryActionOptions) -> Self { +impl From for Scopes { + fn from(action: RepositoryActions) -> Self { match action { - RepositoryActionOptions::Read => Scopes::ReadRepository, - RepositoryActionOptions::Write => Scopes::WriteRepository, - RepositoryActionOptions::Edit => Scopes::EditRepository, + RepositoryActions::Read => Scopes::ReadRepository, + RepositoryActions::Write => Scopes::WriteRepository, + RepositoryActions::Edit => Scopes::EditRepository, + } + } +} + +#[derive(Debug, Deserialize, Clone, ToSchema)] +pub struct UpdatePermissions { + pub admin: Option, + pub user_manager: Option, + pub system_manager: Option, + pub default_repository_actions: Option>, + #[serde(default)] + pub repository_permissions: HashMap>, +} +impl UpdatePermissions { + pub fn has_regular_change(&self) -> bool { + self.admin.is_some() + || self.user_manager.is_some() + || self.system_manager.is_some() + || self.default_repository_actions.is_some() + } + #[instrument(name = "UpdatePermissions::update_regular")] + async fn update_regular(&self, user_id: i32, db: &PgPool) -> Result<(), sqlx::Error> { + let mut query = QueryBuilder::new("UPDATE users SET "); + let mut separated = query.separated(", "); + if let Some(admin) = self.admin { + separated.push("admin = "); + separated.push_bind_unseparated(admin); + } + if let Some(user_manager) = self.user_manager { + separated.push("user_manager = "); + separated.push_bind_unseparated(user_manager); + } + if let Some(system_manager) = self.system_manager { + separated.push("system_manager = "); + separated.push_bind_unseparated(system_manager); + } + if let Some(default_repository_actions) = &self.default_repository_actions { + separated.push("default_repository_actions = "); + separated.push_bind_unseparated(default_repository_actions); + } + query.push(" WHERE id = "); + query.push_bind(user_id); + let query = query.build(); + info!("Updating permissions for user {} {}", user_id, query.sql()); + let result = query.execute(db).await?; + if result.rows_affected() == 0 { + warn!( + "No rows affected when updating permissions for user {}", + user_id + ); + } + Ok(()) + } + #[instrument(name = "UpdatePermissions::update_permissions")] + pub async fn update_permissions(self, user_id: i32, db: &PgPool) -> Result<(), sqlx::Error> { + if self.has_regular_change() { + self.update_regular(user_id, db).await?; + } else { + info!("No regular permissions to update"); + } + if self.repository_permissions.is_empty() { + info!("No repository permissions to update"); + return Ok(()); + } + let span = tracing::span!( + tracing::Level::DEBUG, + "UpdatePermissions::update_permissions::repository_permissions" + ); + let _guard = span.enter(); + for (repository, actions) in self.repository_permissions { + if actions.is_empty() { + debug!( + "Removing entry for repository {} for user {}. Because actions is empty", + repository, user_id + ); + UserRepositoryPermissions::delete(user_id, repository, db).await?; + continue; + } + + let permissions = NewUserRepositoryPermissions { + user_id, + repository_id: repository, + actions, + }; + permissions.insert(db).await?; } + return Ok(()); } } diff --git a/nitro_repo/migrations/20240727231504_ignore_case.down.sql b/nitro_repo/migrations/20240727231504_ignore_case.down.sql index d2f607c5..a53c4fe2 100644 --- a/nitro_repo/migrations/20240727231504_ignore_case.down.sql +++ b/nitro_repo/migrations/20240727231504_ignore_case.down.sql @@ -1 +1,2 @@ -- Add down migration script here +DROP COLLATION IF EXISTS ignoreCase; \ No newline at end of file diff --git a/nitro_repo/migrations/20240813200230_create_projects.down.sql b/nitro_repo/migrations/20240813200230_create_projects.down.sql deleted file mode 100644 index 48aad9dc..00000000 --- a/nitro_repo/migrations/20240813200230_create_projects.down.sql +++ /dev/null @@ -1,4 +0,0 @@ --- Add down migration script here -DROP TABLE project_versions; -DROP TABLE project_members; -DROP TABLE projects; \ No newline at end of file diff --git a/nitro_repo/migrations/20240816220200_create_user_extras.down.sql b/nitro_repo/migrations/20240816220200_create_user_extras.down.sql deleted file mode 100644 index 656da79b..00000000 --- a/nitro_repo/migrations/20240816220200_create_user_extras.down.sql +++ /dev/null @@ -1,3 +0,0 @@ --- Add down migration script here -DROP TABLE IF EXISTS user_auth_tokens; -DROP TABLE IF EXISTS user_password_reset_tokens; \ No newline at end of file diff --git a/nitro_repo/migrations/20240727232431_create_storages_and_repositories.down.sql b/nitro_repo/migrations/20240823113130_create_storages_and_repositories.down.sql similarity index 75% rename from nitro_repo/migrations/20240727232431_create_storages_and_repositories.down.sql rename to nitro_repo/migrations/20240823113130_create_storages_and_repositories.down.sql index 2d1f7937..61784680 100644 --- a/nitro_repo/migrations/20240727232431_create_storages_and_repositories.down.sql +++ b/nitro_repo/migrations/20240823113130_create_storages_and_repositories.down.sql @@ -1,4 +1,3 @@ --- Add down migration script here DROP TABLE IF EXISTS repository_configs; DROP TABLE IF EXISTS repositories; DROP TABLE IF EXISTS storages; \ No newline at end of file diff --git a/nitro_repo/migrations/20240727232431_create_storages_and_repositories.up.sql b/nitro_repo/migrations/20240823113130_create_storages_and_repositories.up.sql similarity index 99% rename from nitro_repo/migrations/20240727232431_create_storages_and_repositories.up.sql rename to nitro_repo/migrations/20240823113130_create_storages_and_repositories.up.sql index 5eeff4ae..03b19bab 100644 --- a/nitro_repo/migrations/20240727232431_create_storages_and_repositories.up.sql +++ b/nitro_repo/migrations/20240823113130_create_storages_and_repositories.up.sql @@ -18,7 +18,6 @@ CREATE TABLE IF NOT EXISTS repositories ( active BOOLEAN NOT NULL DEFAULT TRUE, updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP, created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP, - CONSTRAINT fk_repositories_storage_id FOREIGN KEY (storage_id) REFERENCES storages (id) ON DELETE CASCADE, CONSTRAINT unique_repository_name UNIQUE (storage_id, name) ); diff --git a/nitro_repo/migrations/20240822193610_create_user_tokens.down.sql b/nitro_repo/migrations/20240823113222_users.down.sql similarity index 100% rename from nitro_repo/migrations/20240822193610_create_user_tokens.down.sql rename to nitro_repo/migrations/20240823113222_users.down.sql diff --git a/nitro_repo/migrations/20240727231515_create_users.up.sql b/nitro_repo/migrations/20240823113222_users.up.sql similarity index 85% rename from nitro_repo/migrations/20240727231515_create_users.up.sql rename to nitro_repo/migrations/20240823113222_users.up.sql index a1a43361..2b8de7fe 100644 --- a/nitro_repo/migrations/20240727231515_create_users.up.sql +++ b/nitro_repo/migrations/20240823113222_users.up.sql @@ -1,3 +1,4 @@ +-- Add up migration script here CREATE TABLE IF NOT EXISTS users ( id SERIAL PRIMARY KEY, name TEXT NOT NULL, @@ -8,14 +9,12 @@ CREATE TABLE IF NOT EXISTS users ( require_password_change BOOLEAN DEFAULT FALSE, admin BOOLEAN NOT NULL DEFAULT FALSE, user_manager BOOLEAN NOT NULL DEFAULT FALSE, - storage_manager BOOLEAN NOT NULL DEFAULT FALSE, - repository_manager BOOLEAN NOT NULL DEFAULT FALSE, + system_manager BOOLEAN NOT NULL DEFAULT FALSE, default_repository_actions TEXT[] DEFAULT ARRAY []::text[] not null, password_last_changed TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP, created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP ); - CREATE TABLE IF NOT EXISTS user_repository_permissions( id SERIAL PRIMARY KEY, user_id INTEGER NOT NULL, @@ -23,11 +22,12 @@ CREATE TABLE IF NOT EXISTS user_repository_permissions( FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE, - repository UUID NOT NULL, + repository_id UUID NOT NULL, CONSTRAINT fk_user_repository_permissions_repository - FOREIGN KEY (repository) + FOREIGN KEY (repository_id) REFERENCES repositories (id) ON DELETE CASCADE, + CONSTRAINT unique_repository_and_user UNIQUE (user_id, repository_id), actions TEXT[] NOT NULL, updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP, created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP diff --git a/nitro_repo/migrations/20240815120912_create_stages.down.sql b/nitro_repo/migrations/20240823113259_user_extras.down.sql similarity index 65% rename from nitro_repo/migrations/20240815120912_create_stages.down.sql rename to nitro_repo/migrations/20240823113259_user_extras.down.sql index 5fcc35b1..d2f607c5 100644 --- a/nitro_repo/migrations/20240815120912_create_stages.down.sql +++ b/nitro_repo/migrations/20240823113259_user_extras.down.sql @@ -1,2 +1 @@ -- Add down migration script here -DROP TABLE stages; \ No newline at end of file diff --git a/nitro_repo/migrations/20240816220200_create_user_extras.up.sql b/nitro_repo/migrations/20240823113259_user_extras.up.sql similarity index 99% rename from nitro_repo/migrations/20240816220200_create_user_extras.up.sql rename to nitro_repo/migrations/20240823113259_user_extras.up.sql index 1dfe347a..0c35d3c6 100644 --- a/nitro_repo/migrations/20240816220200_create_user_extras.up.sql +++ b/nitro_repo/migrations/20240823113259_user_extras.up.sql @@ -26,3 +26,4 @@ CREATE TABLE IF NOT EXISTS user_events( event_details JSONB NOT NULL, created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP ); + diff --git a/nitro_repo/migrations/20240727231515_create_users.down.sql b/nitro_repo/migrations/20240823113321_user_auth_tokens.down.sql similarity index 55% rename from nitro_repo/migrations/20240727231515_create_users.down.sql rename to nitro_repo/migrations/20240823113321_user_auth_tokens.down.sql index 329be047..d2f607c5 100644 --- a/nitro_repo/migrations/20240727231515_create_users.down.sql +++ b/nitro_repo/migrations/20240823113321_user_auth_tokens.down.sql @@ -1,2 +1 @@ -- Add down migration script here -DROP TABLE IF EXISTS users; \ No newline at end of file diff --git a/nitro_repo/migrations/20240822193610_create_user_tokens.up.sql b/nitro_repo/migrations/20240823113321_user_auth_tokens.up.sql similarity index 94% rename from nitro_repo/migrations/20240822193610_create_user_tokens.up.sql rename to nitro_repo/migrations/20240823113321_user_auth_tokens.up.sql index fe4ce002..980bcbd8 100644 --- a/nitro_repo/migrations/20240822193610_create_user_tokens.up.sql +++ b/nitro_repo/migrations/20240823113321_user_auth_tokens.up.sql @@ -1,3 +1,4 @@ +-- Add up migration script here -- This migration creates the tables for user tokens CREATE TABLE IF NOT EXISTS user_auth_tokens ( id SERIAL PRIMARY KEY, @@ -33,9 +34,9 @@ CREATE TABLE IF NOT EXISTS user_auth_token_repository_scopes( FOREIGN KEY (user_auth_token_id) REFERENCES user_auth_tokens (id) ON DELETE CASCADE, - repository UUID NOT NULL, + repository_id UUID NOT NULL, CONSTRAINT fk_user_auth_token_repository_scopes_repository - FOREIGN KEY (repository) + FOREIGN KEY (repository_id) REFERENCES repositories (id) ON DELETE CASCADE, actions TEXT[] NOT NULL, diff --git a/nitro_repo/migrations/20240823113359_create_stages.down.sql b/nitro_repo/migrations/20240823113359_create_stages.down.sql new file mode 100644 index 00000000..d2f607c5 --- /dev/null +++ b/nitro_repo/migrations/20240823113359_create_stages.down.sql @@ -0,0 +1 @@ +-- Add down migration script here diff --git a/nitro_repo/migrations/20240815120912_create_stages.up.sql b/nitro_repo/migrations/20240823113359_create_stages.up.sql similarity index 93% rename from nitro_repo/migrations/20240815120912_create_stages.up.sql rename to nitro_repo/migrations/20240823113359_create_stages.up.sql index 17cb4d85..2d2bcc98 100644 --- a/nitro_repo/migrations/20240815120912_create_stages.up.sql +++ b/nitro_repo/migrations/20240823113359_create_stages.up.sql @@ -3,7 +3,7 @@ CREATE TABLE IF NOT EXISTS stages ( id UUID default gen_random_uuid() not null constraint stages_pk primary key, - repository UUID not null + repository_id UUID not null constraint fk_repositories references repositories on delete cascade, diff --git a/nitro_repo/migrations/20240823113416_create_projects.down.sql b/nitro_repo/migrations/20240823113416_create_projects.down.sql new file mode 100644 index 00000000..d2f607c5 --- /dev/null +++ b/nitro_repo/migrations/20240823113416_create_projects.down.sql @@ -0,0 +1 @@ +-- Add down migration script here diff --git a/nitro_repo/migrations/20240813200230_create_projects.up.sql b/nitro_repo/migrations/20240823113416_create_projects.up.sql similarity index 94% rename from nitro_repo/migrations/20240813200230_create_projects.up.sql rename to nitro_repo/migrations/20240823113416_create_projects.up.sql index 16e7c187..8171020c 100644 --- a/nitro_repo/migrations/20240813200230_create_projects.up.sql +++ b/nitro_repo/migrations/20240823113416_create_projects.up.sql @@ -1,4 +1,5 @@ -- Add up migration script here +-- Add up migration script here create TABLE IF NOT EXISTS projects ( id UUID default gen_random_uuid() not null @@ -11,7 +12,7 @@ create TABLE IF NOT EXISTS projects latest_pre_release TEXT, description TEXT, tags TEXT[] default array []::text[] not null, - repository UUID not null + repository_id UUID not null constraint fk_repositories references repositories on delete cascade, @@ -21,7 +22,7 @@ create TABLE IF NOT EXISTS projects ); create TABLE IF NOT EXISTS project_members ( - id serial + id serial constraint project_members_pk primary key, project_id UUID not null diff --git a/nitro_repo/src/app/api/repository.rs b/nitro_repo/src/app/api/repository.rs index 11c87e4c..cda7fc9b 100644 --- a/nitro_repo/src/app/api/repository.rs +++ b/nitro_repo/src/app/api/repository.rs @@ -12,7 +12,7 @@ use nr_core::{ config::repository_page::{PageType, RepositoryPage}, Visibility, }, - user::permissions::{HasPermissions, RepositoryActionOptions}, + user::permissions::{HasPermissions, RepositoryActions}, }; use tracing::instrument; @@ -93,7 +93,7 @@ pub async fn get_repository( }; if config.visibility.is_private() { if !auth - .has_action(RepositoryActionOptions::Read, repository, site.as_ref()) + .has_action(RepositoryActions::Read, repository, site.as_ref()) .await? { return Ok(MissingPermission::ReadRepository(repository).into_response()); diff --git a/nitro_repo/src/app/api/repository/management.rs b/nitro_repo/src/app/api/repository/management.rs index 1bf9bf83..a5a08cb0 100644 --- a/nitro_repo/src/app/api/repository/management.rs +++ b/nitro_repo/src/app/api/repository/management.rs @@ -9,7 +9,7 @@ use axum::{ use http::{header::CONTENT_TYPE, StatusCode}; use nr_core::{ database::repository::{DBRepository, GenericDBRepositoryConfig}, - user::permissions::{HasPermissions, RepositoryActionOptions}, + user::permissions::{HasPermissions, RepositoryActions}, }; use serde::Deserialize; use serde_json::Value; @@ -62,7 +62,7 @@ pub async fn new_repository( Path(repository_type): Path, Json(request): Json, ) -> Result { - if !auth.is_admin_or_repository_manager() { + if !auth.is_admin_or_system_manager() { return Ok(MissingPermission::RepositoryManager.into_response()); } let NewRepositoryRequest { @@ -138,7 +138,7 @@ pub async fn get_configs_for_repository( Path(repository): Path, ) -> Result { if !auth - .has_action(RepositoryActionOptions::Edit, repository, &site.database) + .has_action(RepositoryActions::Edit, repository, &site.database) .await? { return Ok(MissingPermission::EditRepository(repository).into_response()); @@ -173,7 +173,7 @@ pub async fn get_config( Path((repository, config)): Path<(Uuid, String)>, ) -> Result { if !auth - .has_action(RepositoryActionOptions::Edit, repository, &site.database) + .has_action(RepositoryActions::Edit, repository, &site.database) .await? { return Ok(MissingPermission::EditRepository(repository).into_response()); @@ -223,7 +223,7 @@ pub async fn update_config( Json(config): Json, ) -> Result { if !auth - .has_action(RepositoryActionOptions::Edit, repository, &site.database) + .has_action(RepositoryActions::Edit, repository, &site.database) .await? { return Ok(MissingPermission::EditRepository(repository).into_response()); @@ -274,7 +274,7 @@ pub async fn delete_repository( auth: Authentication, Path(repository): Path, ) -> Result { - if !auth.is_admin_or_repository_manager() { + if !auth.is_admin_or_system_manager() { return Ok(MissingPermission::RepositoryManager.into_response()); } let Some(db_repository) = DBRepository::get_by_id(repository, site.as_ref()).await? else { diff --git a/nitro_repo/src/app/api/repository/page.rs b/nitro_repo/src/app/api/repository/page.rs index 24cc6707..2d3b4133 100644 --- a/nitro_repo/src/app/api/repository/page.rs +++ b/nitro_repo/src/app/api/repository/page.rs @@ -9,7 +9,7 @@ use nr_core::{ repository_page::{RepositoryPage, RepositoryPageType}, RepositoryConfigType, }, - user::permissions::{HasPermissions, RepositoryActionOptions}, + user::permissions::{HasPermissions, RepositoryActions}, }; use tracing::instrument; use uuid::Uuid; @@ -45,7 +45,7 @@ pub async fn get_repository_page( if repository.visibility().is_private() { if !auth .has_action( - RepositoryActionOptions::Read, + RepositoryActions::Read, repository.id(), &site.database, ) diff --git a/nitro_repo/src/app/api/storage.rs b/nitro_repo/src/app/api/storage.rs index 64e30003..d742c3f6 100644 --- a/nitro_repo/src/app/api/storage.rs +++ b/nitro_repo/src/app/api/storage.rs @@ -52,7 +52,7 @@ pub struct StorageListRequest { get, path = "/list", responses( - (status = 200, description = "All Storages registered to the system. Config will be null if you are a repository manager but not a StorageManager", body = [DBStorage]), + (status = 200, description = "All Storages registered to the system.", body = [DBStorage]), (status = 403, description = "Does not have permission to view storages") ) )] @@ -62,7 +62,7 @@ pub async fn list_storages( auth: Authentication, Query(request): Query, ) -> Result { - if auth.is_admin_or_storage_manager() { + if auth.is_admin_or_system_manager() { if request.include_config { let storages = DBStorage::get_all(&site.database).await?; Response::builder().status(200).json_body(&storages) @@ -70,9 +70,6 @@ pub async fn list_storages( let storages = DBStorageNoConfig::get_all(&site.database).await?; Response::builder().status(200).json_body(&storages) } - } else if auth.is_admin_or_repository_manager() { - let storages = DBStorageNoConfig::get_all(&site.database).await?; - return Response::builder().status(200).json_body(&storages); } else { Ok(MissingPermission::StorageManager.into_response()) } @@ -100,7 +97,7 @@ pub async fn local_storage_path_helper( auth: Authentication, Json(request): Json, ) -> Result { - if !auth.is_admin_or_storage_manager() { + if !auth.is_admin_or_system_manager() { return Ok(MissingPermission::StorageManager.into_response()); } let path = request.path.unwrap_or_default().trim().to_owned(); @@ -166,7 +163,7 @@ pub async fn new_storage( Path(storage_type): Path, Json(request): Json, ) -> Result { - if !auth.is_admin_or_storage_manager() { + if !auth.is_admin_or_system_manager() { return Ok(MissingPermission::StorageManager.into_response()); } if !DBStorage::is_name_available(&request.name, site.as_ref()).await? { @@ -234,7 +231,7 @@ pub async fn get_storage( Path(id): Path, State(site): State, ) -> Result { - if !auth.is_admin_or_storage_manager() { + if !auth.is_admin_or_system_manager() { return Ok(MissingPermission::StorageManager.into_response()); } let storage = DBStorage::get_by_id(id, &site.database).await?; diff --git a/nitro_repo/src/app/api/user_management.rs b/nitro_repo/src/app/api/user_management.rs index bbd58139..c30e9301 100644 --- a/nitro_repo/src/app/api/user_management.rs +++ b/nitro_repo/src/app/api/user_management.rs @@ -1,3 +1,4 @@ +use ahash::HashMap; use axum::{ body::Body, extract::{Path, State}, @@ -6,8 +7,10 @@ use axum::{ }; use http::StatusCode; use serde::Deserialize; +use sqlx::PgPool; use tracing::instrument; use utoipa::{OpenApi, ToSchema}; +use uuid::Uuid; use crate::{ app::{ @@ -19,10 +22,11 @@ use crate::{ }; use nr_core::{ database::user::{ - user_utils, ChangePasswordNoCheck, NewUserRequest, UserSafeData, UserType as _, + permissions::FullUserPermissions, user_utils, ChangePasswordNoCheck, NewUserRequest, + UserSafeData, UserType as _, }, user::{ - permissions::{HasPermissions, UpdatePermissions, UserPermissions}, + permissions::{HasPermissions, RepositoryActions, UpdatePermissions, UserPermissions}, Email, Username, }, }; @@ -44,6 +48,10 @@ pub fn user_management_routes() -> axum::Router { axum::Router::new() .route("/list", axum::routing::get(list_users)) .route("/get/:user_id", axum::routing::get(get_user)) + .route( + "/get/:user_id/permissions", + axum::routing::get(get_user_permissions), + ) .route("/create", axum::routing::post(create_user)) .route("/is-taken", axum::routing::post(is_taken)) .route( @@ -97,6 +105,31 @@ pub async fn get_user( }; Ok(Json(user).into_response()) } + +#[utoipa::path( + get, + path = "/get/{user_id}/permissions", + responses( + (status = 200, description = "User Info", body = UserSafeData), + (status = 404, description = "User not found") + ) +)] +pub async fn get_user_permissions( + auth: Authentication, + State(site): State, + Path(user_id): Path, +) -> Result { + if !auth.is_admin_or_user_manager() { + return Ok(MissingPermission::UserManager.into_response()); + } + let Some(user) = FullUserPermissions::get_by_id(user_id, site.as_ref()).await? else { + return Ok(Response::builder() + .status(http::StatusCode::NOT_FOUND) + .body("User not found".into()) + .unwrap()); + }; + Ok(Json(user).into_response()) +} #[utoipa::path( post, request_body = NewUserRequest, @@ -215,7 +248,9 @@ pub async fn update_permissions( .body("User not found".into()) .unwrap()); }; - todo!(); + permissions + .update_permissions(user.id, &site.database) + .await?; Ok(Response::builder() .status(StatusCode::NO_CONTENT) .body(Body::empty()) diff --git a/nitro_repo/src/app/open_api.rs b/nitro_repo/src/app/open_api.rs index 78510988..f330f086 100644 --- a/nitro_repo/src/app/open_api.rs +++ b/nitro_repo/src/app/open_api.rs @@ -9,7 +9,7 @@ use axum::{ Json, Router, }; use nr_core::database::user::NewUserRequest; -use nr_core::user::permissions::{RepositoryActions, UserPermissions}; +use nr_core::user::permissions::UserPermissions; use utoipa::openapi::security::{ApiKey, ApiKeyValue, HttpAuthScheme, HttpBuilder, SecurityScheme}; use utoipa::{Modify, OpenApi}; @@ -30,7 +30,6 @@ use utoipa::{Modify, OpenApi}; components( schemas( super::Instance, - RepositoryActions, UserPermissions, api::InstallRequest, NewUserRequest diff --git a/nitro_repo/src/repository/maven/hosted.rs b/nitro_repo/src/repository/maven/hosted.rs index de88d6da..7394245a 100644 --- a/nitro_repo/src/repository/maven/hosted.rs +++ b/nitro_repo/src/repository/maven/hosted.rs @@ -24,7 +24,7 @@ use nr_core::{ Visibility, }, storage::StoragePath, - user::permissions::{HasPermissions, RepositoryActionOptions}, + user::permissions::{HasPermissions, RepositoryActions}, }; use nr_storage::{DynStorage, Storage}; use parking_lot::RwLock; @@ -163,7 +163,7 @@ impl MavenHosted { if authentication.is_no_identification() { return Ok(Some(RepoResponse::www_authenticate("Basic"))); } else if !(authentication - .has_action(RepositoryActionOptions::Read, self.id, self.site.as_ref()) + .has_action(RepositoryActions::Read, self.id, self.site.as_ref()) .await?) { return Ok(Some(RepoResponse::forbidden())); @@ -322,7 +322,7 @@ impl Repository for MavenHosted { if file.is_directory() && visibility.is_hidden() && !authentication - .has_action(RepositoryActionOptions::Read, self.id, self.site.as_ref()) + .has_action(RepositoryActions::Read, self.id, self.site.as_ref()) .await? { return Ok(RepoResponse::indexing_not_allowed()); @@ -350,7 +350,7 @@ impl Repository for MavenHosted { if file.is_directory() && visibility.is_hidden() && !authentication - .has_action(RepositoryActionOptions::Read, self.id, self.site.as_ref()) + .has_action(RepositoryActions::Read, self.id, self.site.as_ref()) .await? { return Ok(RepoResponse::indexing_not_allowed()); @@ -374,14 +374,14 @@ impl Repository for MavenHosted { let Some(user) = request .authentication - .get_user_if_has_action(RepositoryActionOptions::Write, self.id, self.site.as_ref()) + .get_user_if_has_action(RepositoryActions::Write, self.id, self.site.as_ref()) .await? else { info!("No acceptable user authentication provided"); return Ok(RepoResponse::unauthorized()); }; if !user - .has_action(RepositoryActionOptions::Write, self.id, self.site.as_ref()) + .has_action(RepositoryActions::Write, self.id, self.site.as_ref()) .await? { info!(?self.id, ?user, "User does not have write permissions"); diff --git a/nitro_repo/src/repository/npm/mod.rs b/nitro_repo/src/repository/npm/mod.rs index 9f616acb..8c34a008 100644 --- a/nitro_repo/src/repository/npm/mod.rs +++ b/nitro_repo/src/repository/npm/mod.rs @@ -13,7 +13,7 @@ use derive_more::derive::Deref; use http::StatusCode; use nr_core::{ database::{repository::DBRepository, user::auth_token::NewRepositoryToken}, - user::permissions::RepositoryActionOptions, + user::permissions::RepositoryActions, }; use nr_storage::DynStorage; use tracing::{debug, instrument}; @@ -115,7 +115,7 @@ impl Repository for NpmRegistry { user.id, "NPM CLI".to_owned(), self.id, - RepositoryActionOptions::all(), + RepositoryActions::all(), ) .insert(self.site.as_ref()) .await?; diff --git a/nitro_repo/src/repository/repo_http/repo_auth.rs b/nitro_repo/src/repository/repo_http/repo_auth.rs index bd76cded..06b2aebf 100644 --- a/nitro_repo/src/repository/repo_http/repo_auth.rs +++ b/nitro_repo/src/repository/repo_http/repo_auth.rs @@ -6,7 +6,7 @@ use http::request::Parts; use nr_core::{ database::user::{auth_token::AuthToken, UserSafeData, UserType}, user::permissions::{ - does_user_and_token_have_repository_action, HasPermissions, RepositoryActionOptions, + does_user_and_token_have_repository_action, HasPermissions, RepositoryActions, UserPermissions, }, }; @@ -37,7 +37,7 @@ impl RepositoryAuthentication { #[instrument] pub async fn can_access_repository( &self, - action: RepositoryActionOptions, + action: RepositoryActions, repository_id: Uuid, database: &PgPool, ) -> Result { @@ -65,7 +65,7 @@ impl RepositoryAuthentication { #[instrument] pub async fn get_user_if_has_action( &self, - action: RepositoryActionOptions, + action: RepositoryActions, repository_id: Uuid, database: &PgPool, ) -> Result, AuthenticationError> { diff --git a/nitro_repo/src/repository/utils.rs b/nitro_repo/src/repository/utils.rs index 07a0b4aa..08101516 100644 --- a/nitro_repo/src/repository/utils.rs +++ b/nitro_repo/src/repository/utils.rs @@ -1,6 +1,6 @@ use nr_core::{ repository::Visibility, - user::permissions::{HasPermissions, RepositoryActionOptions}, + user::permissions::{HasPermissions, RepositoryActions}, }; use sqlx::PgPool; use uuid::Uuid; @@ -16,7 +16,7 @@ pub async fn can_read_repository( match visibility { Visibility::Public => Ok(true), Visibility::Private | Visibility::Hidden => Ok(auth - .has_action(RepositoryActionOptions::Read, repository_id, database) + .has_action(RepositoryActions::Read, repository_id, database) .await?), } } diff --git a/site/src/components/admin/user/AdminUserPage.vue b/site/src/components/admin/user/AdminUserPage.vue index d9fb507e..6c898f33 100644 --- a/site/src/components/admin/user/AdminUserPage.vue +++ b/site/src/components/admin/user/AdminUserPage.vue @@ -54,6 +54,9 @@
+
+ +
diff --git a/site/src/components/admin/user/RepositoryPermissions.vue b/site/src/components/admin/user/RepositoryPermissions.vue index 039987ef..e0a37fee 100644 --- a/site/src/components/admin/user/RepositoryPermissions.vue +++ b/site/src/components/admin/user/RepositoryPermissions.vue @@ -31,13 +31,13 @@
- +
- +
- +