diff --git a/Cargo.lock b/Cargo.lock index e2cc8aa..1efd345 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1293,7 +1293,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "778e2ac28f6c47af28e4907f13ffd1e1ddbd400980a9abd7c8df189bf578a5ad" dependencies = [ "libc", - "windows-sys 0.52.0", + "windows-sys 0.60.2", ] [[package]] @@ -1332,7 +1332,7 @@ checksum = "0ce92ff622d6dadf7349484f42c93271a0d49b7cc4d466a936405bacbe10aa78" dependencies = [ "cfg-if", "rustix 1.0.8", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -1924,7 +1924,7 @@ dependencies = [ "libc", "percent-encoding", "pin-project-lite", - "socket2 0.5.10", + "socket2 0.6.0", "system-configuration", "tokio", "tower-service", @@ -2229,7 +2229,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "07033963ba89ebaf1584d767badaa2e8fcec21aedea6b8c0346d487d49c28667" dependencies = [ "cfg-if", - "windows-targets 0.48.5", + "windows-targets 0.53.3", ] [[package]] @@ -2433,6 +2433,7 @@ dependencies = [ "nix", "once_cell", "ouroboros", + "parse-size", "path-clean", "priority-queue", "rand 0.9.2", @@ -2444,7 +2445,7 @@ dependencies = [ "sea-orm-migration", "serde", "serde_json", - "thiserror 2.0.12", + "thiserror 2.0.15", "time", "tokio", "tokio-tar", @@ -2717,6 +2718,12 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "parse-size" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "487f2ccd1e17ce8c1bfab3a65c89525af41cfad4c8659021a1e9a2aacd73b89b" + [[package]] name = "password-hash" version = "0.5.0" @@ -3063,7 +3070,7 @@ checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac" dependencies = [ "getrandom 0.2.16", "libredox", - "thiserror 2.0.12", + "thiserror 2.0.15", ] [[package]] @@ -3293,7 +3300,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys 0.4.15", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -3306,7 +3313,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys 0.9.4", - "windows-sys 0.52.0", + "windows-sys 0.60.2", ] [[package]] @@ -3471,7 +3478,7 @@ dependencies = [ "serde_json", "sqlx", "strum", - "thiserror 2.0.12", + "thiserror 2.0.15", "time", "tracing", "url", @@ -3562,7 +3569,7 @@ dependencies = [ "proc-macro2", "quote", "syn", - "thiserror 2.0.12", + "thiserror 2.0.15", ] [[package]] @@ -3808,7 +3815,7 @@ checksum = "297f631f50729c8c99b84667867963997ec0b50f32b2a7dbcab828ef0541e8bb" dependencies = [ "num-bigint", "num-traits", - "thiserror 2.0.12", + "thiserror 2.0.15", "time", ] @@ -3917,7 +3924,7 @@ dependencies = [ "serde_json", "sha2", "smallvec", - "thiserror 2.0.12", + "thiserror 2.0.15", "time", "tokio", "tokio-stream", @@ -4001,7 +4008,7 @@ dependencies = [ "smallvec", "sqlx-core", "stringprep", - "thiserror 2.0.12", + "thiserror 2.0.15", "time", "tracing", "uuid", @@ -4040,7 +4047,7 @@ dependencies = [ "smallvec", "sqlx-core", "stringprep", - "thiserror 2.0.12", + "thiserror 2.0.15", "time", "tracing", "uuid", @@ -4066,7 +4073,7 @@ dependencies = [ "serde", "serde_urlencoded", "sqlx-core", - "thiserror 2.0.12", + "thiserror 2.0.15", "time", "tracing", "url", @@ -4198,7 +4205,7 @@ dependencies = [ "getrandom 0.3.3", "once_cell", "rustix 1.0.8", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -4212,11 +4219,11 @@ dependencies = [ [[package]] name = "thiserror" -version = "2.0.12" +version = "2.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708" +checksum = "80d76d3f064b981389ecb4b6b7f45a0bf9fdac1d5b9204c7bd6714fecc302850" dependencies = [ - "thiserror-impl 2.0.12", + "thiserror-impl 2.0.15", ] [[package]] @@ -4232,9 +4239,9 @@ dependencies = [ [[package]] name = "thiserror-impl" -version = "2.0.12" +version = "2.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" +checksum = "44d29feb33e986b6ea906bd9c3559a856983f92371b3eaa5e83782a351623de0" dependencies = [ "proc-macro2", "quote", diff --git a/netmito/Cargo.toml b/netmito/Cargo.toml index 88f1923..a81136a 100644 --- a/netmito/Cargo.toml +++ b/netmito/Cargo.toml @@ -36,6 +36,7 @@ md5 = { workspace = true } nix = { version = "0.30.1", features = ["process", "signal"] } once_cell = "1.21.3" ouroboros = "0.18.5" +parse-size = "1.0.0" path-clean = "1.0.1" priority-queue = "2.5.0" rand = "0.9.2" @@ -54,7 +55,7 @@ sea-orm = { version = "1.1.14", default-features = false, features = [ sea-orm-migration = "1.1.14" serde = { workspace = true } serde_json = { workspace = true } -thiserror = "2.0.12" +thiserror = "2.0.15" time = { version = "0.3.41", features = ["serde-human-readable"] } tokio = { workspace = true } tokio-tar = "0.3.1" diff --git a/netmito/src/api/admin.rs b/netmito/src/api/admin.rs index 2d62ca5..7bc556b 100644 --- a/netmito/src/api/admin.rs +++ b/netmito/src/api/admin.rs @@ -27,6 +27,7 @@ pub fn admin_router(st: InfraPool, cancel_token: CancellationToken) -> Router, + State(pool): State, + Json(req): Json, +) -> ApiResult> { + let storage_quota = + service::group::change_group_storage_quota(&pool, req.group_name, req.storage_quota) + .await + .map_err(|e| match e { + crate::error::Error::AuthError(err) => ApiError::AuthError(err), + crate::error::Error::ApiError(e) => e, + _ => { + tracing::error!("{}", e); + ApiError::InternalServerError + } + })?; + Ok(Json(GroupStorageQuotaResp { storage_quota })) +} + pub async fn shutdown_worker( Extension(_): Extension, State(pool): State, diff --git a/netmito/src/api/mod.rs b/netmito/src/api/mod.rs index 23a551c..a3a57aa 100644 --- a/netmito/src/api/mod.rs +++ b/netmito/src/api/mod.rs @@ -70,6 +70,13 @@ pub fn router(st: InfraPool, cancel_token: CancellationToken) -> Router { #[cfg(feature = "debugging")] { Router::new() + .route( + "/auth", + get(user::auth_user).layer(middleware::from_fn_with_state( + st.clone(), + user_auth_with_name_middleware, + )), + ) .route( "/health", get(|| async { (StatusCode::OK, Json(json!({"status": "ok"}))) }), diff --git a/netmito/src/client.rs b/netmito/src/client.rs index 6bd11a3..073b461 100644 --- a/netmito/src/client.rs +++ b/netmito/src/client.rs @@ -874,6 +874,34 @@ impl MitoClient { } } + pub async fn admin_update_group_storage_quota( + &mut self, + args: UpdateGroupStorageQuotaArgs, + ) -> crate::error::Result { + self.url.set_path("admin/group/storage_quota"); + let req = ChangeGroupStorageQuotaReq { + group_name: args.group_name, + storage_quota: args.storage_quota, + }; + let resp = self + .http_client + .put(self.url.as_str()) + .json(&req) + .bearer_auth(&self.credential) + .send() + .await + .map_err(map_reqwest_err)?; + if resp.status().is_success() { + let update_resp = resp + .json::() + .await + .map_err(RequestError::from)?; + Ok(update_resp) + } else { + Err(get_error_from_resp(resp).await.into()) + } + } + pub async fn get_attachment_url( &mut self, args: GetAttachmentArgs, @@ -1459,6 +1487,19 @@ impl MitoClient { } } }, + AdminCommands::GroupStorageQuota(args) => { + match self.admin_update_group_storage_quota(args).await { + Ok(resp) => { + tracing::info!( + "Successfully updated group storage quota to {}", + format_size(resp.storage_quota.max(0) as u64, DECIMAL) + ); + } + Err(e) => { + tracing::error!("{}", e); + } + } + } }, ClientCommand::Auth(args) => { match fill_user_auth(args.username, args.password, args.retain) { diff --git a/netmito/src/config/client.rs b/netmito/src/config/client.rs index d9705ef..76a427a 100644 --- a/netmito/src/config/client.rs +++ b/netmito/src/config/client.rs @@ -176,6 +176,12 @@ pub struct ShutdownArgs { pub secret: String, } +#[derive(Serialize, Debug, Deserialize, Args)] +pub struct UpdateGroupStorageQuotaArgs { + pub group_name: String, + pub storage_quota: String, +} + #[derive(Subcommand, Serialize, Debug, Deserialize)] pub enum ArtifactCommands { Delete(DeleteArtifactArgs), @@ -213,6 +219,8 @@ pub enum AdminCommands { Artifact(ArtifactArgs), /// Delete an attachment Attachment(AttachmentArgs), + /// Update storage quota of a group + GroupStorageQuota(UpdateGroupStorageQuotaArgs), } #[derive(Subcommand, Serialize, Debug, Deserialize)] diff --git a/netmito/src/error.rs b/netmito/src/error.rs index 61289a9..85a9389 100644 --- a/netmito/src/error.rs +++ b/netmito/src/error.rs @@ -52,6 +52,8 @@ pub enum Error { RedisError(#[from] redis::RedisError), #[error("Parse xml error: {0}")] ParseXmlError(#[from] roxmltree::Error), + #[error("Parse bytesize error: {0}")] + ParseSizeError(#[from] parse_size::Error), } #[derive(thiserror::Error, Debug)] diff --git a/netmito/src/schema.rs b/netmito/src/schema.rs index 1f7984c..0d7b572 100644 --- a/netmito/src/schema.rs +++ b/netmito/src/schema.rs @@ -68,6 +68,17 @@ pub struct UserStateResp { pub state: UserState, } +#[derive(Debug, Serialize, Deserialize)] +pub struct ChangeGroupStorageQuotaReq { + pub group_name: String, + pub storage_quota: String, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct GroupStorageQuotaResp { + pub storage_quota: i64, +} + #[derive(Debug, Serialize, Deserialize)] pub struct CreateGroupReq { pub group_name: String, diff --git a/netmito/src/service/group.rs b/netmito/src/service/group.rs index aa4fc90..f49d3b9 100644 --- a/netmito/src/service/group.rs +++ b/netmito/src/service/group.rs @@ -194,6 +194,7 @@ where let group = Group::ActiveModel { id: Set(group.id), state: Set(state), + updated_at: Set(TimeDateTimeWithTimeZone::now_utc()), ..Default::default() }; let g = group.update(txn).await?; @@ -336,3 +337,72 @@ pub async fn query_user_groups( .collect(); Ok(group_relations) } + +enum StorageQuotaOp { + Increase(i64), + Decrease(i64), + Set(i64), +} + +fn parse_storage_quota(quota: &str) -> crate::error::Result { + fn parse_bytesize(s: &str) -> crate::error::Result { + let u = parse_size::parse_size(s)?; + Ok(u as i64) + } + match quota { + s if s.starts_with('+') => { + let v = parse_bytesize(&s[1..])?; + Ok(StorageQuotaOp::Increase(v)) + } + s if s.starts_with('-') => { + let v = parse_bytesize(&s[1..])?; + Ok(StorageQuotaOp::Decrease(v)) + } + s if s.starts_with('=') => { + let v = parse_bytesize(&s[1..])?; + Ok(StorageQuotaOp::Set(v)) + } + s => { + let v = parse_bytesize(s)?; + Ok(StorageQuotaOp::Set(v)) + } + } +} + +pub async fn change_group_storage_quota( + pool: &InfraPool, + group_name: String, + storage_quota: String, +) -> crate::error::Result { + let quota_op = parse_storage_quota(&storage_quota)?; + let updated_quota = pool + .db + .transaction::<_, i64, Error>(|txn| { + Box::pin(async move { + let group = Group::Entity::find() + .filter(Group::Column::GroupName.eq(&group_name)) + .one(txn) + .await?; + if let Some(group) = group { + // change state to the new state + let new_quota = match quota_op { + StorageQuotaOp::Increase(v) => group.storage_quota.saturating_add(v), + StorageQuotaOp::Decrease(v) => group.storage_quota.saturating_sub(v), + StorageQuotaOp::Set(v) => v, + }; + let group = Group::ActiveModel { + id: Set(group.id), + storage_quota: Set(new_quota), + updated_at: Set(TimeDateTimeWithTimeZone::now_utc()), + ..Default::default() + }; + let g = group.update(txn).await?; + Ok(g.storage_quota) + } else { + Err(DbErr::RecordNotUpdated.into()) + } + }) + }) + .await?; + Ok(updated_quota) +} diff --git a/netmito/src/service/task.rs b/netmito/src/service/task.rs index b790467..65876c7 100644 --- a/netmito/src/service/task.rs +++ b/netmito/src/service/task.rs @@ -532,44 +532,40 @@ async fn check_task_list_query( ))); } } - match query.group_name { - Some(ref group_name) => { - let builder = pool.db.get_database_backend(); - let role_stmt = Query::select() - .column((UserGroup::Entity, UserGroup::Column::Role)) - .from(UserGroup::Entity) - .join( - sea_orm::JoinType::Join, - Group::Entity, - Expr::col((Group::Entity, Group::Column::Id)) - .eq(Expr::col((UserGroup::Entity, UserGroup::Column::GroupId))), - ) - .and_where(Expr::col((UserGroup::Entity, UserGroup::Column::UserId)).eq(user_id)) - .and_where( - Expr::col((Group::Entity, Group::Column::GroupName)).eq(group_name.clone()), - ) - .to_owned(); - let role = UserGroupRoleQueryRes::find_by_statement(builder.build(&role_stmt)) - .one(&pool.db) - .await? - .map(|r| r.role); - if role.is_none() { - return Err(Error::ApiError(crate::error::ApiError::InvalidRequest( - format!("Group with name {group_name} not found or user is not in the group"), - ))); - } - } - None => { - let username = User::Entity::find() - .filter(User::Column::Id.eq(user_id)) - .one(&pool.db) - .await? - .ok_or(Error::ApiError(crate::error::ApiError::NotFound( - "User".to_string(), - )))? - .username; - tracing::debug!("No group name specified, use username {} instead", username); - query.group_name = Some(username); + if query.group_name.is_none() { + let username = User::Entity::find() + .filter(User::Column::Id.eq(user_id)) + .one(&pool.db) + .await? + .ok_or(Error::ApiError(crate::error::ApiError::NotFound( + "User".to_string(), + )))? + .username; + tracing::debug!("No group name specified, use username {} instead", username); + query.group_name = Some(username); + } + if let Some(ref group_name) = query.group_name { + let builder = pool.db.get_database_backend(); + let role_stmt = Query::select() + .column((UserGroup::Entity, UserGroup::Column::Role)) + .from(UserGroup::Entity) + .join( + sea_orm::JoinType::Join, + Group::Entity, + Expr::col((Group::Entity, Group::Column::Id)) + .eq(Expr::col((UserGroup::Entity, UserGroup::Column::GroupId))), + ) + .and_where(Expr::col((UserGroup::Entity, UserGroup::Column::UserId)).eq(user_id)) + .and_where(Expr::col((Group::Entity, Group::Column::GroupName)).eq(group_name.clone())) + .to_owned(); + let role = UserGroupRoleQueryRes::find_by_statement(builder.build(&role_stmt)) + .one(&pool.db) + .await? + .map(|r| r.role); + if role.is_none() { + return Err(Error::ApiError(crate::error::ApiError::InvalidRequest( + format!("Group with name {group_name} not found or user is not in the group"), + ))); } } Ok(())