diff --git a/Cargo.toml b/Cargo.toml index 524141f2..ac8e2f34 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -81,3 +81,24 @@ features = [ [workspace.lints.rust] async_fn_in_trait = "allow" deprecated = "deny" + +[profile.dev.package."argon2"] +opt-level = 3 +[profile.dev.package."tokio"] +opt-level = 3 +[profile.dev.package."sha2"] +opt-level = 3 +[profile.dev.package."tracing"] +opt-level = 3 +[profile.dev.package."tracing-subscriber"] +opt-level = 3 +[profile.dev.package."tracing-appender"] +opt-level = 3 +[profile.dev.package."tracing-opentelemetry"] +opt-level = 3 +[profile.dev.package."opentelemetry"] +opt-level = 3 +[profile.dev.package."opentelemetry_sdk"] +opt-level = 3 +[profile.dev.package."opentelemetry-otlp"] +opt-level = 3 diff --git a/crates/core/src/database/project/mod.rs b/crates/core/src/database/project/mod.rs index 3b6c76fa..0765580c 100644 --- a/crates/core/src/database/project/mod.rs +++ b/crates/core/src/database/project/mod.rs @@ -1,4 +1,5 @@ use derive_builder::Builder; +use http::version; use serde::Serialize; use sqlx::{postgres::PgRow, types::Json, FromRow, PgPool}; use utoipa::ToSchema; @@ -17,13 +18,29 @@ pub trait ProjectDBType: for<'r> FromRow<'r, PgRow> + Unpin + Send + Sync { if let Some(prefix) = prefix { Self::columns() .iter() - .map(|column| format!("{}.`{}`", prefix, column)) + .map(|column| format!("{}.{}", prefix, column)) .collect::>() .join(", ") } else { Self::columns().join(", ") } } + async fn find_by_project_key( + project_key: &str, + repository: Uuid, + database: &PgPool, + ) -> Result, sqlx::Error> { + let columns = Self::format_columns(None); + let project = sqlx::query_as::<_, Self>(&format!( + "SELECT {} FROM projects WHERE repository = $1 AND LOWER(project_key) = $2", + columns + )) + .bind(repository) + .bind(project_key.to_lowercase()) + .fetch_optional(database) + .await?; + Ok(project) + } async fn find_by_project_directory( directory: &str, repository: Uuid, @@ -31,7 +48,7 @@ pub trait ProjectDBType: for<'r> FromRow<'r, PgRow> + Unpin + Send + Sync { ) -> Result, sqlx::Error> { let columns = Self::format_columns(None); let project = sqlx::query_as::<_, Self>(&format!( - "SELECT {} FROM projects WHERE `repository` = $1 AND LOWER(`storage_path`) = $2", + "SELECT {} FROM projects WHERE repository = $1 AND LOWER(storage_path) = $2", columns )) .bind(repository) @@ -141,3 +158,19 @@ pub struct DBProjectVersion { pub updated_at: DateTime, pub created_at: DateTime, } +impl DBProjectVersion { + pub async fn find_by_version_and_project( + version: &str, + project_id: Uuid, + database: &PgPool, + ) -> Result, sqlx::Error> { + let version = sqlx::query_as::<_, Self>( + r#"SELECT * FROM project_versions WHERE project_id = $1 AND version = $2"#, + ) + .bind(project_id) + .bind(version) + .fetch_optional(database) + .await?; + Ok(version) + } +} diff --git a/crates/core/src/database/project/new.rs b/crates/core/src/database/project/new.rs index 666d5ead..24631156 100644 --- a/crates/core/src/database/project/new.rs +++ b/crates/core/src/database/project/new.rs @@ -1,11 +1,16 @@ use derive_builder::Builder; use serde::{Deserialize, Serialize}; +use sqlx::{types::Json, PgPool}; +use tracing::info; use utoipa::ToSchema; use uuid::Uuid; use crate::repository::project::{ReleaseType, VersionData}; + +use super::DBProject; #[derive(Debug, Clone, PartialEq, Eq, Builder)] pub struct NewProject { + #[builder(default)] pub scope: Option, /// Maven will use something like `{groupId}:{artifactId}` /// Cargo will use the `name` field @@ -17,19 +22,55 @@ pub struct NewProject { /// NPM will use the `name` field pub name: String, /// Latest stable release + #[builder(default)] pub latest_release: Option, /// Release is SNAPSHOT in Maven or Alpha, Beta, on any other repository type /// This is the latest release or pre-release + #[builder(default)] pub latest_pre_release: Option, /// A short description of the project + #[builder(default)] pub description: Option, /// Can be empty + #[builder(default)] pub tags: Vec, /// The repository it belongs to pub repository: Uuid, /// Storage Path pub storage_path: String, } +impl NewProject { + pub async fn insert(self, db: &sqlx::PgPool) -> Result { + let Self { + scope, + project_key, + name, + latest_release, + latest_pre_release, + description, + tags, + repository, + storage_path, + } = self; + + let insert = sqlx::query_as::<_,DBProject>( + r#" + INSERT INTO projects (scope, project_key, name, latest_release, latest_pre_release, description, tags, repository, storage_path) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9) RETURNING * + "# + ).bind(scope) + .bind(project_key) + .bind(name) + .bind(latest_release) + .bind(latest_pre_release) + .bind(description) + .bind(tags) + .bind(repository) + .bind(storage_path) + .fetch_one(db).await?; + Ok(insert) + } +} #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, ToSchema)] pub struct NewProjectMember { pub user_id: i32, @@ -46,6 +87,27 @@ impl NewProjectMember { can_manage: true, } } + pub async fn insert_no_return(self, db: &PgPool) -> Result<(), sqlx::Error> { + let Self { + user_id, + project_id, + can_write, + can_manage, + } = self; + sqlx::query( + r#" + INSERT INTO project_members (user_id, project_id, can_write, can_manage) + VALUES ($1, $2, $3, $4) + "#, + ) + .bind(user_id) + .bind(project_id) + .bind(can_write) + .bind(can_manage) + .execute(db) + .await?; + Ok(()) + } } #[derive(Debug, Clone, PartialEq, Eq, Builder)] @@ -60,7 +122,69 @@ pub struct NewVersion { /// The publisher of the version pub publisher: i32, /// The version page. Such as a README + #[builder(default)] pub version_page: Option, /// The version data. More data can be added in the future and the data can be repository dependent pub extra: VersionData, } +impl NewVersion { + pub async fn insert_no_return(self, db: &PgPool) -> Result<(), sqlx::Error> { + let Self { + project_id, + version, + release_type, + version_path, + publisher, + version_page, + extra, + } = self; + sqlx::query( + r#" + INSERT INTO project_versions (project_id, version, release_type, version_path, publisher, version_page, extra) + VALUES ($1, $2, $3, $4, $5, $6, $7) + "#, + ) + .bind(project_id) + .bind(&version) + .bind(release_type.to_string()) + .bind(version_path) + .bind(publisher) + .bind(version_page) + .bind(Json(extra)) + .execute(db) + .await?; + match release_type { + ReleaseType::Stable => { + sqlx::query( + r#" + UPDATE projects + SET latest_release = $1 AND latest_pre_release = $1 + WHERE id = $2 + "#, + ) + .bind(version) + .bind(project_id) + .execute(db) + .await?; + } + ReleaseType::Unknown => { + info!("Unknown release type for version {}", version); + } + _ => { + sqlx::query( + r#" + UPDATE projects + SET latest_pre_release = $1 + WHERE id = $2 + "#, + ) + .bind(version) + .bind(project_id) + .execute(db) + .await?; + } + } + + Ok(()) + } +} diff --git a/crates/core/src/database/project/utils.rs b/crates/core/src/database/project/utils.rs index dc6d6b75..096ce4c1 100644 --- a/crates/core/src/database/project/utils.rs +++ b/crates/core/src/database/project/utils.rs @@ -43,19 +43,19 @@ impl ProjectLookup { } if let Some(scope) = scope { where_values.push(( - format!("LOWER(`scope`) = {}", where_values.len() + 1), + format!("LOWER(scope) = {}", where_values.len() + 1), scope.to_lowercase(), )); } if let Some(name) = name { where_values.push(( - format!("LOWER(`name`) = {}", where_values.len() + 1), + format!("LOWER(name) = {}", where_values.len() + 1), name.to_lowercase(), )); } if let Some(storage_path) = storage_path { where_values.push(( - format!("LOWER(`storage_path`) = {}", where_values.len() + 1), + format!("LOWER(storage_path) = {}", where_values.len() + 1), storage_path.to_lowercase(), )); } @@ -69,7 +69,7 @@ impl ProjectLookup { .join(" OR ") }; let query = format!( - r#"SELECT id, scope, project_key, name, storage_path FROM projects WHERE `repository` = $1 AND ({})"#, + r#"SELECT id, scope, project_key, name, storage_path FROM projects WHERE repository = $1 AND ({})"#, where_clause ); debug!( diff --git a/crates/core/src/repository/project/mod.rs b/crates/core/src/repository/project/mod.rs index 6f2b97f2..69a9a045 100644 --- a/crates/core/src/repository/project/mod.rs +++ b/crates/core/src/repository/project/mod.rs @@ -1,10 +1,27 @@ +use std::str::FromStr; + use derive_builder::Builder; use serde::{Deserialize, Serialize}; +use sqlx::{postgres::PgValueRef, Decode, Encode}; +use strum::{AsRefStr, Display, EnumIs, EnumString, IntoStaticStr}; use utoipa::ToSchema; /// Release type of a project /// /// Can be overridden in the panel. -#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, ToSchema, sqlx::Type)] +#[derive( + Debug, + Clone, + PartialEq, + Eq, + Hash, + Serialize, + Deserialize, + ToSchema, + EnumIs, + IntoStaticStr, + Display, + EnumString, +)] pub enum ReleaseType { /// Stable Release Stable, @@ -15,9 +32,70 @@ pub enum ReleaseType { /// Snapshot Release /// Only really used in Maven Snapshot, + /// .RC Release + ReleaseCandidate, /// The release type could not be determined Unknown, } +impl<'q, DB: ::sqlx::Database> Encode<'q, DB> for ReleaseType +where + &'q str: Encode<'q, DB>, +{ + fn encode_by_ref( + &self, + buf: &mut ::ArgumentBuffer<'q>, + ) -> ::std::result::Result<::sqlx::encode::IsNull, ::sqlx::error::BoxDynError> { + let val: &str = self.into(); + <&str as Encode<'q, DB>>::encode(val, buf) + } + fn size_hint(&self) -> ::std::primitive::usize { + let val = self.into(); + <&str as Encode<'q, DB>>::size_hint(&val) + } +} +#[automatically_derived] +impl<'r> Decode<'r, ::sqlx::postgres::Postgres> for ReleaseType { + fn decode( + value: PgValueRef<'r>, + ) -> Result> { + let value = <&'r str as Decode<'r, ::sqlx::postgres::Postgres>>::decode(value)?; + ReleaseType::from_str(value) + .map_err(|_| format!("invalid value {:?} for enum {}", value, "ReleaseType").into()) + } +} +#[automatically_derived] +impl ::sqlx::Type<::sqlx::Postgres> for ReleaseType { + fn type_info() -> ::sqlx::postgres::PgTypeInfo { + ::sqlx::postgres::PgTypeInfo::with_name("TEXT") + } +} +#[automatically_derived] +impl ::sqlx::postgres::PgHasArrayType for ReleaseType { + fn array_type_info() -> ::sqlx::postgres::PgTypeInfo { + ::sqlx::postgres::PgTypeInfo::array_of("TEXT") + } +} +impl Default for ReleaseType { + fn default() -> Self { + ReleaseType::Unknown + } +} +impl ReleaseType { + pub fn release_type_from_version(version: &str) -> ReleaseType { + let version = version.to_lowercase(); + if version.contains("snapshot") { + ReleaseType::Snapshot + } else if version.contains("beta") { + ReleaseType::Beta + } else if version.contains("alpha") { + ReleaseType::Alpha + } else if version.contains(".rc") { + ReleaseType::ReleaseCandidate + } else { + ReleaseType::Stable + } + } +} #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, ToSchema, sqlx::Type)] pub enum ProjectState { Active, @@ -26,12 +104,18 @@ pub enum ProjectState { #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, ToSchema, Default, Builder)] #[serde(default)] pub struct VersionData { + #[builder(default)] pub documentation_url: Option, + #[builder(default)] pub website: Option, #[serde(default)] + #[builder(default)] pub authors: Vec, + #[builder(default)] pub description: Option, + #[builder(default)] pub source: Option, + #[builder(default)] pub licence: Option, } /// Author of the project diff --git a/crates/core/src/storage/storage_path.rs b/crates/core/src/storage/storage_path.rs index 73eae5fb..399ffebc 100644 --- a/crates/core/src/storage/storage_path.rs +++ b/crates/core/src/storage/storage_path.rs @@ -9,6 +9,13 @@ use tracing::instrument; struct StoragePathComponent(String); #[derive(Debug, Clone, Hash, PartialEq, Eq)] pub struct StoragePath(Vec); +impl StoragePath { + pub fn parent(self) -> Self { + let mut path = self.0; + path.pop(); + StoragePath(path) + } +} impl Default for StoragePath { fn default() -> Self { StoragePath(vec![]) diff --git a/crates/core/src/utils.rs b/crates/core/src/utils.rs index c87d3427..7e1229b1 100644 --- a/crates/core/src/utils.rs +++ b/crates/core/src/utils.rs @@ -1,7 +1,8 @@ pub mod base64_utils { use base64::{engine::general_purpose::STANDARD, DecodeError, Engine}; + use tracing::instrument; + #[instrument(skip(input), name = "base64_utils::decode")] #[inline(always)] - pub fn decode(input: impl AsRef<[u8]>) -> Result, DecodeError> { STANDARD.decode(input) } diff --git a/crates/storage/src/fs/file.rs b/crates/storage/src/fs/file.rs index d872cea7..12478334 100644 --- a/crates/storage/src/fs/file.rs +++ b/crates/storage/src/fs/file.rs @@ -24,7 +24,20 @@ pub enum StorageFile { content: StorageFileReader, }, } - +impl StorageFile { + pub fn meta(&self) -> &StorageFileMeta { + match self { + StorageFile::Directory { meta, .. } => meta, + StorageFile::File { meta, .. } => meta, + } + } + pub fn file(self) -> Option<(StorageFileReader, StorageFileMeta)> { + match self { + StorageFile::File { content, meta } => Some((content, meta)), + _ => None, + } + } +} impl Debug for StorageFile { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { diff --git a/crates/storage/src/local/mod.rs b/crates/storage/src/local/mod.rs index 5cca7179..7f365c27 100644 --- a/crates/storage/src/local/mod.rs +++ b/crates/storage/src/local/mod.rs @@ -8,7 +8,7 @@ use std::{ use nr_core::storage::StoragePath; use serde::{Deserialize, Serialize}; -use tracing::{debug, info, instrument, trace, warn}; +use tracing::{debug, info, instrument, span, trace, warn, Level}; use utils::PathUtils; use utoipa::ToSchema; @@ -100,16 +100,15 @@ impl Storage for LocalStorage { config: BorrowedStorageTypeConfig::Local(&self.config), } } - #[instrument(skip(content, location))] async fn save_file( &self, repository: Uuid, content: FileContent, location: &StoragePath, ) -> Result<(usize, bool), StorageError> { + let span = span!(Level::DEBUG, "local_storage_save", "repository" = ?repository, "location" = ?location, "storage_id" = ?self.storage_config.storage_id); + let _enter = span.enter(); let path = self.get_path(&repository, location); - info!(?path, "Saving File"); - let parent_directory = path.parent_or_err()?; let new_file = !path.exists(); if !parent_directory.exists() { @@ -130,14 +129,14 @@ impl Storage for LocalStorage { } Ok((bytes_written, new_file)) } - #[instrument(skip(location))] async fn delete_file( &self, repository: Uuid, location: &StoragePath, ) -> Result<(), StorageError> { + let span = span!(Level::DEBUG, "local_storage_delete", "repository" = ?repository, "location" = ?location, "storage_id" = ?self.storage_config.storage_id); + let _enter = span.enter(); let path = self.get_path(&repository, location); - info!(?path, "Deleting File"); if path.is_dir() { info!(?path, "Deleting Directory"); fs::remove_dir_all(path)?; @@ -147,13 +146,15 @@ impl Storage for LocalStorage { } Ok(()) } - #[instrument(skip(location))] async fn get_file_information( &self, repository: Uuid, location: &StoragePath, ) -> Result, StorageError> { + let span = span!(Level::DEBUG, "local_storage_get_info", "repository" = ?repository, "location" = ?location, "storage_id" = ?self.storage_config.storage_id); + let _enter = span.enter(); let path = self.get_path(&repository, location); + if !path.exists() { debug!(?path, "File does not exist"); return Ok(None); @@ -161,12 +162,13 @@ impl Storage for LocalStorage { let meta = StorageFileMeta::new_from_file(path)?; Ok(Some(meta)) } - #[instrument(skip(location))] async fn open_file( &self, repository: Uuid, location: &StoragePath, ) -> Result, StorageError> { + let span = span!(Level::DEBUG, "local_storage_open_file", "repository" = ?repository, "location" = ?location, "storage_id" = ?self.storage_config.storage_id); + let _enter = span.enter(); let path = self.get_path(&repository, location); if !path.exists() { debug!(?path, "File does not exist"); @@ -179,7 +181,6 @@ impl Storage for LocalStorage { }; Ok(Some(file)) } - #[instrument] fn unload(&self) -> impl std::future::Future> + Send { info!(?self, "Unloading Local Storage"); // TODO: Implement Unload diff --git a/nitro_repo/src/app/api/mod.rs b/nitro_repo/src/app/api/mod.rs index a1007dfc..d37e42cc 100644 --- a/nitro_repo/src/app/api/mod.rs +++ b/nitro_repo/src/app/api/mod.rs @@ -2,26 +2,28 @@ use axum::{extract::State, Json}; use http::StatusCode; use nr_core::{database::user::NewUserRequest, user::permissions::UserPermissions}; use serde::{Deserialize, Serialize}; +use tower_http::cors::CorsLayer; use tracing::{error, instrument}; use utoipa::ToSchema; pub mod repository; pub mod storage; pub mod user; pub mod user_management; -use crate::{error::InternalError, utils::password::encrypt_password}; +use crate::error::InternalError; -use super::{Instance, NitroRepo, NitroRepoState}; +use super::{authentication::password, Instance, NitroRepo, NitroRepoState}; pub fn api_routes() -> axum::Router { axum::Router::new() - .route("/api/info", axum::routing::get(info)) - .route("/api/install", axum::routing::post(install)) - .nest("/api/user", user::user_routes()) - .nest("/api/storage", storage::storage_routes()) + .route("/info", axum::routing::get(info)) + .route("/install", axum::routing::post(install)) + .nest("/user", user::user_routes()) + .nest("/storage", storage::storage_routes()) .nest( - "/api/user-management", + "/user-management", user_management::user_management_routes(), ) - .nest("/api/repository", repository::repository_routes()) + .nest("/repository", repository::repository_routes()) + .layer(CorsLayer::very_permissive()) } #[utoipa::path( get, @@ -64,7 +66,7 @@ pub async fn install( let password = user .password .as_ref() - .and_then(|password| encrypt_password(password)); + .and_then(|password| password::encrypt_password(password)); if password.is_none() { error!("A Password must exist for the first user."); return Ok(StatusCode::BAD_REQUEST); diff --git a/nitro_repo/src/app/api/user.rs b/nitro_repo/src/app/api/user.rs index 7e4edf78..eefcfe51 100644 --- a/nitro_repo/src/app/api/user.rs +++ b/nitro_repo/src/app/api/user.rs @@ -3,7 +3,7 @@ use std::net::SocketAddr; use axum::{ body::Body, extract::{ConnectInfo, State}, - response::{self, IntoResponse, IntoResponseParts, Response}, + response::{self, IntoResponse, Response}, Json, }; use axum_extra::{ diff --git a/nitro_repo/src/app/authentication/api_middleware.rs b/nitro_repo/src/app/authentication/api_middleware.rs index 9ef770a2..ac54ee32 100644 --- a/nitro_repo/src/app/authentication/api_middleware.rs +++ b/nitro_repo/src/app/authentication/api_middleware.rs @@ -50,7 +50,11 @@ where fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll> { self.inner.poll_ready(cx) } - + #[tracing::instrument( + skip(self, req), + name = "AuthenticationMiddleware", + fields(project_module = "Authentication") + )] fn call(&mut self, req: Request) -> Self::Future { if req.method() == http::Method::OPTIONS { trace!("Options Request"); @@ -61,7 +65,6 @@ where }; } let (mut parts, body) = req.into_parts(); - self.site.update_app_url(&mut parts.uri); let cookie_jar = CookieJar::from_headers(&parts.headers); let authorization_header = parts .headers diff --git a/nitro_repo/src/app/authentication/mod.rs b/nitro_repo/src/app/authentication/mod.rs index d5c4d072..155ee98f 100644 --- a/nitro_repo/src/app/authentication/mod.rs +++ b/nitro_repo/src/app/authentication/mod.rs @@ -16,8 +16,9 @@ use nr_core::user::permissions::{HasPermissions, UserPermissions}; use serde::Serialize; use session::Session; use sqlx::PgPool; +use strum::EnumIs; use thiserror::Error; -use tracing::{error, info, warn}; +use tracing::{error, info, instrument, warn}; use utoipa::ToSchema; use crate::utils::headers::AuthorizationHeader; @@ -76,6 +77,11 @@ where S: Send + Sync, { type Rejection = AuthenticationError; + #[instrument( + name = "api_auth_from_request", + skip(parts, state), + fields(project_module = "Authentication") + )] async fn from_request_parts(parts: &mut Parts, state: &S) -> Result { let raw_extension = parts.extensions.get::().cloned(); let repo = NitroRepo::from_ref(state); @@ -115,7 +121,7 @@ pub struct MeWithSession { user: UserSafeData, } -#[derive(Clone, Debug, PartialEq)] +#[derive(Clone, Debug, PartialEq, EnumIs)] pub enum RepositoryAuthentication { AuthToken(AuthToken, UserSafeData), Session(Session, UserSafeData), @@ -168,6 +174,11 @@ where S: Send + Sync, { type Rejection = AuthenticationError; + #[instrument( + name = "repository_auth_from_request", + skip(parts, state), + fields(project_module = "Authentication") + )] async fn from_request_parts(parts: &mut Parts, state: &S) -> Result { let raw_extension = parts.extensions.get::().cloned(); let repo = NitroRepo::from_ref(state); @@ -216,6 +227,7 @@ pub enum AuthenticationRaw { Basic { username: String, password: String }, } impl AuthenticationRaw { + #[instrument(skip(header, site), fields(project_module = "Authentication"))] pub fn new_from_header(header: AuthorizationHeader, site: &NitroRepo) -> Self { match header { AuthorizationHeader::Basic { username, password } => { @@ -251,6 +263,10 @@ impl AuthenticationRaw { } } #[inline(always)] +#[instrument( + skip(username, password, database), + fields(project_module = "Authentication") +)] pub async fn verify_login( username: impl AsRef, password: impl AsRef, @@ -259,33 +275,13 @@ pub async fn verify_login( let user_found: Option = UserModel::get_by_username_or_email(username, database) .await .map_err(AuthenticationError::DBError)?; - if user_found.is_none() { - return Err(AuthenticationError::Unauthorized); - } - let argon2 = Argon2::default(); - let user = user_found.unwrap(); - let Some(parsed_hash) = user - .password - .as_ref() - .map(|x| PasswordHash::new(x)) - .transpose() - .map_err(|err| { - error!("Failed to parse password hash: {}", err); - AuthenticationError::PasswordVerificationError - })? - else { + let Some(user) = user_found else { return Err(AuthenticationError::Unauthorized); }; - - if argon2 - .verify_password(password.as_ref().as_bytes(), &parsed_hash) - .is_err() - { - return Err(AuthenticationError::Unauthorized); - } + password::verify_password(password.as_ref(), user.password.as_deref())?; Ok(user.into()) } - +#[instrument(skip(token, database), fields(project_module = "Authentication"))] pub async fn get_user_and_auth_token( token: &str, database: &PgPool, @@ -300,3 +296,46 @@ pub async fn get_user_and_auth_token( .ok_or(AuthenticationError::Unauthorized)?; Ok((user, auth_token)) } +pub mod password { + use argon2::{ + password_hash::SaltString, Argon2, PasswordHash, PasswordHasher, PasswordVerifier, + }; + use rand::rngs::OsRng; + use tracing::{error, instrument}; + + use crate::app::authentication::AuthenticationError; + #[instrument(skip(password), fields(project_module = "Authentication"))] + pub fn encrypt_password(password: &str) -> Option { + let salt = SaltString::generate(&mut OsRng); + + let argon2 = Argon2::default(); + + let password = argon2.hash_password(password.as_ref(), &salt); + match password { + Ok(ok) => Some(ok.to_string()), + Err(err) => { + error!("Failed to hash password: {}", err); + None + } + } + } + #[instrument(skip(password, hash), fields(project_module = "Authentication"))] + pub fn verify_password(password: &str, hash: Option<&str>) -> Result<(), AuthenticationError> { + let argon2 = Argon2::default(); + let Some(parsed_hash) = hash.map(PasswordHash::new).transpose().map_err(|err| { + error!("Failed to parse password hash: {}", err); + AuthenticationError::PasswordVerificationError + })? + else { + return Err(AuthenticationError::Unauthorized); + }; + + if argon2 + .verify_password(password.as_bytes(), &parsed_hash) + .is_err() + { + return Err(AuthenticationError::Unauthorized); + } + Ok(()) + } +} diff --git a/nitro_repo/src/app/logging.rs b/nitro_repo/src/app/logging.rs index c9420f70..85501ec4 100644 --- a/nitro_repo/src/app/logging.rs +++ b/nitro_repo/src/app/logging.rs @@ -78,10 +78,14 @@ impl Default for TracingConfig { impl LoggingConfig { pub fn init(&self, mode: Mode) -> anyhow::Result<()> { let base_filter = match mode { - Mode::Debug => "debug,nitro_repo=trace,h2=warn", + Mode::Debug => { + "debug,nitro_repo=trace,nr_storage=trace,nr_core=trace,h2=warn,tower=warn,hyper_util=warn" + } Mode::Release => "info", }; - let otel_filter = format!("debug,nitro_repo=trace,nitro_repo=trace,sqlx=debug"); + let otel_filter = format!( + "debug,nitro_repo=trace,nr_storage=trace,nr_core=trace,tower=warn,hyper_util=warn" + ); let env_filter = EnvFilter::try_from_default_env().unwrap_or_else(|_| base_filter.into()); let file_filter = EnvFilter::try_from_default_env().unwrap_or_else(|_| base_filter.into()); diff --git a/nitro_repo/src/app/mod.rs b/nitro_repo/src/app/mod.rs index b4cace0f..5a7cad5a 100644 --- a/nitro_repo/src/app/mod.rs +++ b/nitro_repo/src/app/mod.rs @@ -86,7 +86,6 @@ impl From<(String, String)> for RepositoryStorageName { } } } -#[derive(Debug)] pub struct NitroRepoInner { pub instance: Mutex, pub storages: RwLock>, @@ -97,7 +96,17 @@ pub struct NitroRepoInner { pub repository_types: Vec, pub general_security_settings: SecuritySettings, } -#[derive(Debug, Clone, AsRef, Deref)] +impl Debug for NitroRepo { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("NitroRepo") + .field("instance", &self.inner.instance.lock()) + .field("active_storages", &self.inner.storages.read().len()) + .field("active_repositories", &self.inner.repositories.read().len()) + .field("database", &self.database) + .finish() + } +} +#[derive(Clone, AsRef, Deref)] pub struct NitroRepo { #[deref(forward)] pub inner: Arc, @@ -158,8 +167,6 @@ impl NitroRepo { nitro_repo.load_repositories().await?; Ok(nitro_repo) } - ///Unloads all storages and reloads them from the database - #[instrument] async fn load_storages(&self) -> anyhow::Result<()> { let mut storages = self.storages.write(); storages.clear(); @@ -188,7 +195,6 @@ impl NitroRepo { info!("Loaded {} storages", storages.len()); Ok(()) } - #[instrument] async fn load_repositories(&self) -> anyhow::Result<()> { let mut repositories = self.repositories.write(); repositories.clear(); @@ -248,20 +254,9 @@ impl NitroRepo { repositories.insert(id, repository); } - #[instrument] pub fn update_app_url(&self, app_url: &Uri) { - let mut instance = self.instance.lock(); - if instance.app_url.is_empty() { - info!("Updating app url to {}", app_url); - let schema = app_url.scheme_str().unwrap_or("http"); - let host = if let Some(authority) = app_url.host() { - authority.to_string() - } else { - warn!("No host found in uri"); - return; - }; - instance.app_url = format!("{}://{}", schema, host); - } + info!(?app_url, "Updating app url"); + // TODO: } /// Checks if a repository name and storage pair are found in the lookup table. If not queries the database. /// If found in the database, adds the pair to the lookup table diff --git a/nitro_repo/src/app/web.rs b/nitro_repo/src/app/web.rs index 54aa1dea..d0f1176d 100644 --- a/nitro_repo/src/app/web.rs +++ b/nitro_repo/src/app/web.rs @@ -7,6 +7,7 @@ use anyhow::Context; use axum::extract::DefaultBodyLimit; use axum::routing::any; use axum::{extract::Request, Router}; +use axum_extra::routing::RouterExt; use futures_util::pin_mut; use http::{HeaderName, HeaderValue}; use hyper::body::Incoming; @@ -59,11 +60,19 @@ pub(crate) async fn start(config: NitroRepoConfig) -> anyhow::Result<()> { "/repositories/:storage/:repository/*path", any(crate::repository::handle_repo_request), ) + .route_with_tsr( + "/repositories/:storage/:repository", + any(crate::repository::handle_repo_request), + ) .route( "/storages/:storage/:repository/*path", any(crate::repository::handle_repo_request), ) - .merge(api::api_routes()) + .route_with_tsr( + "/storages/:storage/:repository", + any(crate::repository::handle_repo_request), + ) + .nest("/api", api::api_routes()) .merge(super::open_api::build_router()) .with_state(site); @@ -76,7 +85,6 @@ pub(crate) async fn start(config: NitroRepoConfig) -> anyhow::Result<()> { .layer(PropagateRequestIdLayer::new(REQUEST_ID_HEADER)) .layer(DefaultBodyLimit::max(max_upload.get_as_bytes())) .layer(SetRequestIdLayer::new(REQUEST_ID_HEADER, MakeRequestUuid)) - .layer(CorsLayer::very_permissive()) .layer(auth_layer); if let Some(tls) = tls { start_app_with_tls(tls, app, bind_address).await?; diff --git a/nitro_repo/src/repository/maven/hosted.rs b/nitro_repo/src/repository/maven/hosted.rs index 9fdd9c39..8e4b6e15 100644 --- a/nitro_repo/src/repository/maven/hosted.rs +++ b/nitro_repo/src/repository/maven/hosted.rs @@ -1,8 +1,14 @@ use std::sync::Arc; +use axum::response::{self, Response}; use derive_more::derive::Deref; +use http::{version, StatusCode}; +use maven_rs::pom::Pom; use nr_core::{ - database::repository::DBRepository, + database::{ + project::{DBProject, DBProjectVersion, NewProjectMember, ProjectDBType}, + repository::DBRepository, + }, repository::{ config::{ frontend::{BadgeSettings, BadgeSettingsType, Frontend, FrontendConfigType}, @@ -11,18 +17,20 @@ use nr_core::{ }, Visibility, }, + storage::StoragePath, user::permissions::HasPermissions, }; use nr_storage::{DynStorage, Storage}; use parking_lot::RwLock; -use tracing::{debug, info, instrument}; +use tokio::io::AsyncReadExt; +use tracing::{debug, error, info, instrument}; use uuid::Uuid; use crate::{ - app::NitroRepo, + app::{authentication::RepositoryAuthentication, NitroRepo}, repository::{ - maven::MavenRepositoryConfigType, Repository, RepositoryFactoryError, - RepositoryHandlerError, + maven::{self, MavenError, MavenRepositoryConfigType}, + Repository, RepositoryFactoryError, RepositoryHandlerError, }, }; @@ -91,6 +99,84 @@ impl MavenHosted { }; Ok(Self(Arc::new(inner))) } + pub fn check_read(&self, authentication: &RepositoryAuthentication) -> Option { + if self.visibility().is_private() { + if authentication.is_no_identification() { + return Some(RepoResponse::www_authenticate("Basic")); + } else if !(authentication.can_read_repository(self.id)) { + return Some(RepoResponse::forbidden()); + } + } + None + } + async fn add_or_update_version( + &self, + version_directory: StoragePath, + project_id: Uuid, + publisher: i32, + pom: Pom, + ) -> Result<(), MavenError> { + let db_version = DBProjectVersion::find_by_version_and_project( + &pom.version, + project_id, + &self.site.database, + ) + .await?; + if let Some(version) = db_version { + info!(?version, "Version already exists"); + // TODO: Update Version + } else { + let version = super::pom_to_db_project_version( + project_id, + version_directory, + publisher, + pom.clone(), + )?; + version.insert_no_return(&self.site.database).await?; + info!("Created Version"); + }; + Ok(()) + } + #[instrument] + async fn post_pom_upload_inner( + &self, + pom_directory: StoragePath, + publisher: i32, + pom: Pom, + ) -> Result<(), MavenError> { + let project_key = format!("{}:{}", pom.group_id, pom.artifact_id); + let version_directory = pom_directory.clone().parent(); + let db_project = + DBProject::find_by_project_key(&project_key, self.id, &self.site.database).await?; + let project_id = if let Some(project) = db_project { + project.id + } else { + let project_directory = version_directory.clone().parent(); + let project = super::pom_to_db_project(project_directory, self.id, pom.clone())?; + let project = project.insert(&self.site.database).await?; + + let new_member = NewProjectMember::new_owner(publisher, project.id); + new_member.insert_no_return(&self.site.database).await?; + info!(?project, "Created Project"); + project.id + }; + + self.add_or_update_version(version_directory, project_id, publisher, pom) + .await?; + Ok(()) + } + + pub async fn post_pom_upload(&self, pom_directory: StoragePath, publisher: i32, pom: Pom) { + match self + .post_pom_upload_inner(pom_directory, publisher, pom) + .await + { + Ok(()) => {} + Err(e) => { + error!(?e, "Failed to handle POM Upload"); + } + } + } } impl Repository for MavenHosted { fn get_storage(&self) -> nr_storage::DynStorage { @@ -160,7 +246,7 @@ impl Repository for MavenHosted { } Ok(()) } - + #[instrument(name = "maven_hosted_get")] async fn handle_get( &self, RepositoryRequest { @@ -174,12 +260,10 @@ impl Repository for MavenHosted { return Ok(RepoResponse::disabled_repository()); } - let visibility = { self.security.read().visibility }; - - if visibility.is_private() && !authentication.can_read_repository(self.id) { - return Ok(RepoResponse::Unauthorized); + if let Some(err) = self.check_read(&authentication) { + return Ok(err); } - + let visibility = self.visibility(); let file = self.0.storage.open_file(self.id, &path).await?; if let Some(file) = &file { if file.is_directory() @@ -191,6 +275,7 @@ impl Repository for MavenHosted { } Ok(RepoResponse::from(file)) } + #[instrument(name = "maven_hosted_head")] async fn handle_head( &self, RepositoryRequest { @@ -205,8 +290,8 @@ impl Repository for MavenHosted { } let visibility = { self.security.read().visibility }; - if visibility.is_private() && !authentication.can_read_repository(self.id) { - return Ok(RepoResponse::Unauthorized); + if let Some(err) = self.check_read(&authentication) { + return Ok(err); } let file = self.storage.get_file_information(self.id, &path).await?; if let Some(file) = &file { @@ -219,7 +304,7 @@ impl Repository for MavenHosted { } Ok(RepoResponse::from(file)) } - #[instrument] + #[instrument(name = "maven_hosted_put")] async fn handle_put( &self, RepositoryRequest { @@ -233,28 +318,36 @@ impl Repository for MavenHosted { "Handling PUT Request for Repository: {} Path: {}", self.id, path ); - let repository_name = { + let (save_path, user_id) = { let repository = self.repository.read(); if !repository.active { return Ok(RepoResponse::disabled_repository()); } - repository.name.clone() - }; - let Some(user) = authentication.get_user() else { - return Ok(RepoResponse::Unauthorized); - }; - { - let security_config = self.security.read(); - if security_config.visibility.is_private() && !user.can_read_repository(self.id) { - return Ok(RepoResponse::Unauthorized); + let security = self.security.read(); + if security.must_use_auth_token_for_push && !authentication.has_auth_token() { + info!("Repository requires an auth token for push"); + return Ok(RepoResponse::require_auth_token()); } - } - { - let push_rules = self.push_rules.read(); - + let Some(user) = authentication.get_user() else { + info!("No acceptable user authentication provided"); + return Ok(RepoResponse::unauthorized()); + }; if !user.can_write_to_repository(self.id) { - return Ok(RepoResponse::Unauthorized); + info!(?repository, ?user, "User does not have write permissions"); + return Ok(RepoResponse::forbidden()); } + + let save_path = format!( + "/repositories/{}/{}/{}", + self.storage.storage_config().storage_config.storage_name, + repository.name, + path + ); + (save_path, user.id) + }; + + { + let push_rules = self.push_rules.read(); } info!("Saving File: {}", path); let body = body.body_as_bytes().await?; @@ -263,14 +356,25 @@ impl Repository for MavenHosted { let (size, created) = self.storage.save_file(self.id, body.into(), &path).await?; // Trigger Push Event if it is the .pom file if path.has_extension("pom") { - // TODO: Trigger Push Event + let file = self + .storage + .open_file(self.id, &path) + .await? + .and_then(|x| x.file()); + let Some((mut file, meta)) = file else { + return Ok(Response::builder() + .status(StatusCode::INTERNAL_SERVER_ERROR) + .body("Failed to open file".into()) + .unwrap() + .into()); + }; + let mut pom_file = String::with_capacity(size); + file.read_to_string(&mut pom_file).await?; + let pom: maven_rs::pom::Pom = + maven_rs::quick_xml::de::from_str(&pom_file).map_err(MavenError::from)?; + debug!(?pom, "Parsed POM File"); + self.post_pom_upload(path.clone(), user_id, pom).await; } - let save_path = format!( - "/repositories/{}/{}/{}", - self.storage.storage_config().storage_config.storage_name, - repository_name, - path - ); Ok(RepoResponse::put_response(created, save_path)) } diff --git a/nitro_repo/src/repository/maven/mod.rs b/nitro_repo/src/repository/maven/mod.rs index 66fb1f30..156e0a4f 100644 --- a/nitro_repo/src/repository/maven/mod.rs +++ b/nitro_repo/src/repository/maven/mod.rs @@ -5,12 +5,23 @@ use ahash::HashMap; use axum::response::IntoResponse; use futures::future::BoxFuture; use hosted::MavenHosted; +use maven_rs::pom::Pom; use nr_core::{ - database::repository::{DBRepository, DBRepositoryConfig}, - repository::config::{ - frontend::{BadgeSettingsType, FrontendConfigType}, - PushRulesConfigType, RepositoryConfigError, RepositoryConfigType, SecurityConfigType, + database::{ + project::{ + NewProject, NewProjectBuilder, NewProjectBuilderError, NewVersion, NewVersionBuilder, + NewVersionBuilderError, + }, + repository::{DBRepository, DBRepositoryConfig}, }, + repository::{ + config::{ + frontend::{BadgeSettingsType, FrontendConfigType}, + PushRulesConfigType, RepositoryConfigError, RepositoryConfigType, SecurityConfigType, + }, + project::{ReleaseType, VersionDataBuilder, VersionDataBuilderError}, + }, + storage::StoragePath, }; use nr_macros::DynRepositoryHandler; use nr_storage::DynStorage; @@ -18,6 +29,7 @@ use proxy::MavenProxy; use schemars::{schema_for, JsonSchema}; use serde::{Deserialize, Serialize}; use serde_json::Value; +use uuid::Uuid; use super::{DynRepository, Repository, RepositoryFactoryError, RepositoryType}; pub mod hosted; @@ -165,23 +177,83 @@ impl MavenRepository { pub enum MavenError { #[error("Error with processing Maven request: {0}")] MavenRS(#[from] maven_rs::Error), + #[error("XML Deserialize Error: {0}")] + XMLDeserialize(#[from] maven_rs::quick_xml::DeError), + #[error("Database Error: {0}")] + Database(#[from] sqlx::Error), + #[error("New Project Error: {0}")] + NewProject(#[from] NewProjectBuilderError), + #[error("New Version Error: {0}")] + NewVersion(#[from] NewVersionBuilderError), + #[error("New Version Error: {0}")] + VersionData(#[from] VersionDataBuilderError), } impl IntoResponse for MavenError { fn into_response(self) -> axum::http::Response { match self { - MavenError::MavenRS(maven_rs::Error::XMLDeserialize(err)) => { - axum::http::Response::builder() - .status(StatusCode::BAD_REQUEST) - .body(axum::body::Body::from(format!( - "XML Deserialize Error: {}", - err - ))) - .unwrap() - } + MavenError::MavenRS(maven_rs::Error::XMLDeserialize(err)) + | MavenError::XMLDeserialize(err) => axum::http::Response::builder() + .status(StatusCode::BAD_REQUEST) + .body(axum::body::Body::from(format!( + "XML Deserialize Error: {}", + err + ))) + .unwrap(), MavenError::MavenRS(e) => axum::http::Response::builder() .status(500) .body(axum::body::Body::from(format!("Maven Error: {}", e))) .unwrap(), + err => axum::http::Response::builder() + .status(500) + .body(axum::body::Body::from(format!( + "Internal Server Error: {}", + err + ))) + .unwrap(), } } } +pub fn get_release_type(version: &str) -> ReleaseType { + let version = version.to_lowercase(); + if version.contains("snapshot") { + ReleaseType::Snapshot + } else { + ReleaseType::Stable + } +} + +pub fn pom_to_db_project( + project_path: StoragePath, + repository: Uuid, + pom: Pom, +) -> Result { + let result = NewProjectBuilder::default() + .project_key(format!("{}:{}", pom.group_id, pom.artifact_id)) + .scope(Some(pom.group_id)) + .name(pom.name.unwrap_or(pom.artifact_id)) + .description(pom.description) + .repository(repository) + .storage_path(project_path.to_string()) + .build()?; + Ok(result) +} +pub fn pom_to_db_project_version( + project_id: Uuid, + version_path: StoragePath, + publisher: i32, + pom: Pom, +) -> Result { + let version_data = VersionDataBuilder::default() + .description(pom.description) + .build()?; + let release_type = ReleaseType::release_type_from_version(&pom.version); + let result = NewVersionBuilder::default() + .project_id(project_id) + .version(pom.version) + .publisher(publisher) + .version_path(version_path.to_string()) + .release_type(release_type) + .extra(version_data) + .build()?; + Ok(result) +} diff --git a/nitro_repo/src/repository/mod.rs b/nitro_repo/src/repository/mod.rs index 58800bce..c0e200f5 100644 --- a/nitro_repo/src/repository/mod.rs +++ b/nitro_repo/src/repository/mod.rs @@ -134,6 +134,8 @@ pub enum RepositoryHandlerError { BadRequest(#[from] BadRequestErrors), #[error("Maven Repository Error: {0}")] MavenError(#[from] maven::MavenError), + #[error("IO Error: {0}")] + IOError(#[from] std::io::Error), } impl IntoResponse for RepositoryHandlerError { fn into_response(self) -> Response { diff --git a/nitro_repo/src/repository/repo_http.rs b/nitro_repo/src/repository/repo_http.rs index 0341ee27..53d06309 100644 --- a/nitro_repo/src/repository/repo_http.rs +++ b/nitro_repo/src/repository/repo_http.rs @@ -28,7 +28,7 @@ use nr_core::storage::{InvalidStoragePath, StoragePath}; use nr_storage::{StorageFile, StorageFileMeta, StorageFileReader}; use serde::Deserialize; use serde_json::Value; -use tracing::{debug, error, info, instrument, trace}; +use tracing::{debug, error, info, instrument, span, trace, Level}; use utoipa::openapi::info; use super::RepositoryHandlerError; @@ -134,7 +134,6 @@ pub enum RepoResponse { FileMetaResponse(StorageFileMeta), Json(Value, StatusCode), Generic(axum::response::Response), - Unauthorized, } impl RepoResponse { /// Default Response Format @@ -184,10 +183,6 @@ impl RepoResponse { .unwrap() } Self::Generic(response) => response, - Self::Unauthorized => Response::builder() - .status(StatusCode::UNAUTHORIZED) - .body(Body::from("Unauthorized")) - .unwrap(), } } pub fn put_response(was_created: bool, location: impl AsRef) -> Self { @@ -233,6 +228,39 @@ impl RepoResponse { "Indexing is not allowed for this repository", ) } + pub fn www_authenticate(value: &str) -> Self { + Response::builder() + .status(StatusCode::UNAUTHORIZED) + .header("WWW-Authenticate", value) + .body(Body::from("Unauthorized")) + .unwrap() + .into() + } + pub fn unauthorized() -> Self { + Response::builder() + .status(StatusCode::UNAUTHORIZED) + .body(Body::from("Unauthorized")) + .unwrap() + .into() + } + pub fn forbidden() -> Self { + Response::builder() + .status(StatusCode::FORBIDDEN) + .body(Body::from( + "You do not have permission to access this repository", + )) + .unwrap() + .into() + } + pub fn require_auth_token() -> Self { + Response::builder() + .status(StatusCode::UNAUTHORIZED) + .body(Body::from( + "Authentication Token is required for this repository.", + )) + .unwrap() + .into() + } pub fn disabled_repository() -> Self { Self::basic_text_response(StatusCode::FORBIDDEN, "Repository is disabled") } @@ -272,6 +300,7 @@ impl From> for RepoResponse { pub struct RepoRequestPath { storage: String, repository: String, + #[serde(default)] path: Option, } #[debug_handler] @@ -304,16 +333,25 @@ pub async fn handle_repo_request( path: path.unwrap_or_default(), authentication, }; - info!("Executing Request"); - let response = match method { - Method::GET => repository.handle_get(request).await, - Method::PUT => repository.handle_put(request).await, - Method::DELETE => repository.handle_delete(request).await, - Method::PATCH => repository.handle_patch(request).await, - Method::HEAD => repository.handle_head(request).await, - _ => repository.handle_other(request).await, + + let response = { + let span = span!( + Level::DEBUG, + "Repository Request", + "repository_type" = repository.get_type(), + ?method, + ?request + ); + let _enter = span.enter(); + match method { + Method::GET => repository.handle_get(request).await, + Method::PUT => repository.handle_put(request).await, + Method::DELETE => repository.handle_delete(request).await, + Method::PATCH => repository.handle_patch(request).await, + Method::HEAD => repository.handle_head(request).await, + _ => repository.handle_other(request).await, + } }; - // TODO: If request is HTML, return HTML, If request is JSON, return JSON, else return text match response { Ok(response) => Ok(response.into_response_default()), Err(err) => { diff --git a/nitro_repo/src/utils.rs b/nitro_repo/src/utils.rs index c42f5521..9312cad7 100644 --- a/nitro_repo/src/utils.rs +++ b/nitro_repo/src/utils.rs @@ -48,41 +48,3 @@ impl Resources { } pub mod headers; -pub mod password { - use argon2::{ - password_hash::SaltString, Argon2, PasswordHash, PasswordHasher, PasswordVerifier, - }; - use rand::rngs::OsRng; - use tracing::error; - - use crate::error::InternalError; - - pub fn encrypt_password(password: &str) -> Option { - let salt = SaltString::generate(&mut OsRng); - - let argon2 = Argon2::default(); - - let password = argon2.hash_password(password.as_ref(), &salt); - match password { - Ok(ok) => Some(ok.to_string()), - Err(err) => { - error!("Failed to hash password: {}", err); - None - } - } - } - pub fn verify_password(password: &str, hash: &str) -> Result { - let argon2 = Argon2::default(); - let password_hash = PasswordHash::new(hash)?; - match argon2.verify_password(password.as_bytes(), &password_hash) { - Ok(_) => Ok(true), - // Password is incorrect - Err(argon2::password_hash::Error::Password) => Ok(false), - // Some other error - Err(err) => { - error!("Failed to verify password: {}", err); - Err(InternalError::from(err)) - } - } - } -} diff --git a/nitro_repo/src/utils/headers.rs b/nitro_repo/src/utils/headers.rs index 84e431ab..88b917d6 100644 --- a/nitro_repo/src/utils/headers.rs +++ b/nitro_repo/src/utils/headers.rs @@ -1,6 +1,6 @@ use http::{header::ToStrError, HeaderValue}; use nr_core::utils::base64_utils; -use tracing::{debug, error}; +use tracing::{debug, error, instrument}; use crate::error::{BadRequestErrors, InvalidAuthorizationHeader}; @@ -38,7 +38,7 @@ pub enum AuthorizationHeader { } impl TryFrom for AuthorizationHeader { type Error = BadRequestErrors; - + #[instrument(skip(value), name = "AuthorizationHeader::try_from")] fn try_from(value: String) -> Result { let parts: Vec<&str> = value.split(' ').collect(); @@ -64,6 +64,7 @@ impl TryFrom for AuthorizationHeader { } } } +#[instrument(skip(header))] fn parse_basic_header(header: &str) -> Result { let decoded = base64_utils::decode(header).map_err(|err| { error!("Failed to decode base64: {}", err);