diff --git a/crates/cli/src/commands/admin/group.rs b/crates/cli/src/commands/admin/group.rs index 08bac13..266917d 100644 --- a/crates/cli/src/commands/admin/group.rs +++ b/crates/cli/src/commands/admin/group.rs @@ -210,31 +210,50 @@ async fn execute_add(args: AddArgs, formatter: &Formatter) -> ExitCode { return ExitCode::UsageError; } - let members: Option> = args.members.as_ref().map(|m| { - m.split(',') - .map(|s| s.trim().to_string()) - .filter(|s| !s.is_empty()) - .collect() - }); - - match client.create_group(&args.name, members.as_deref()).await { - Ok(group) => { - if formatter.is_json() { - let output = GroupOperationOutput { - success: true, - name: group.name.clone(), - message: format!("Group '{}' created successfully", group.name), - }; - formatter.json(&output); - } else { - let styled_name = formatter.style_name(&group.name); - formatter.success(&format!("Group '{styled_name}' created successfully.")); - } - ExitCode::Success + let members: Vec = args + .members + .as_ref() + .map(|m| { + m.split(',') + .map(|s| s.trim().to_string()) + .filter(|s| !s.is_empty()) + .collect() + }) + .unwrap_or_default(); + + if members.is_empty() { + if formatter.is_json() { + let output = GroupOperationOutput { + success: true, + name: args.name.clone(), + message: format!("Group '{}' created successfully", args.name), + }; + formatter.json(&output); + } else { + let styled_name = formatter.style_name(&args.name); + formatter.success(&format!("Group '{styled_name}' created successfully.")); } - Err(e) => { - formatter.error(&format!("Failed to create group: {e}")); - ExitCode::GeneralError + ExitCode::Success + } else { + match client.add_group_members(&args.name, &members).await { + Ok(()) => { + if formatter.is_json() { + let output = GroupOperationOutput { + success: true, + name: args.name.clone(), + message: format!("Group '{}' created successfully with members", args.name), + }; + formatter.json(&output); + } else { + let styled_name = formatter.style_name(&args.name); + formatter.success(&format!("Group '{styled_name}' created successfully.")); + } + ExitCode::Success + } + Err(e) => { + formatter.error(&format!("Failed to create group: {e}")); + ExitCode::GeneralError + } } } } diff --git a/crates/cli/src/commands/admin/service_account.rs b/crates/cli/src/commands/admin/service_account.rs index 3fd0213..a4a052e 100644 --- a/crates/cli/src/commands/admin/service_account.rs +++ b/crates/cli/src/commands/admin/service_account.rs @@ -43,6 +43,12 @@ pub struct CreateArgs { /// Alias name of the server pub alias: String, + /// Access key for the service account + pub access_key: String, + + /// Secret key for the service account + pub secret_key: String, + /// Optional name for the service account #[arg(long)] pub name: Option, @@ -204,6 +210,8 @@ async fn execute_create(args: CreateArgs, formatter: &Formatter) -> ExitCode { expiry: args.expiry, name: args.name, description: args.description, + access_key: args.access_key.clone(), + secret_key: args.secret_key.clone(), }; match client.create_service_account(request).await { @@ -328,6 +336,9 @@ mod tests { policy: None, account_status: Some("on".to_string()), expiration: None, + name: None, + description: None, + implied_policy: None, }; let info = ServiceAccountInfo::from(sa); diff --git a/crates/core/src/admin/mod.rs b/crates/core/src/admin/mod.rs index 273b2c4..a753af0 100644 --- a/crates/core/src/admin/mod.rs +++ b/crates/core/src/admin/mod.rs @@ -13,7 +13,8 @@ pub use cluster::{ }; pub use types::{ BucketQuota, CreateServiceAccountRequest, Group, GroupStatus, Policy, PolicyEntity, PolicyInfo, - ServiceAccount, SetPolicyRequest, UpdateGroupMembersRequest, User, UserStatus, + ServiceAccount, ServiceAccountCreateResponse, ServiceAccountCredentials, SetPolicyRequest, + UpdateGroupMembersRequest, User, UserStatus, }; use async_trait::async_trait; diff --git a/crates/core/src/admin/types.rs b/crates/core/src/admin/types.rs index beab87d..f7594f1 100644 --- a/crates/core/src/admin/types.rs +++ b/crates/core/src/admin/types.rs @@ -192,32 +192,35 @@ pub struct PolicyInfo { #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct ServiceAccount { - /// Access key ID + #[serde(default)] pub access_key: String, - /// Secret access key (only present on creation) #[serde(skip_serializing_if = "Option::is_none")] pub secret_key: Option, - /// Parent user (owner of this service account) #[serde(skip_serializing_if = "Option::is_none")] pub parent_user: Option, - /// Policy attached to this service account #[serde(skip_serializing_if = "Option::is_none")] pub policy: Option, - /// Account status #[serde(skip_serializing_if = "Option::is_none")] pub account_status: Option, - /// Expiration time (if any) #[serde(skip_serializing_if = "Option::is_none")] pub expiration: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + pub name: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + pub description: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + pub implied_policy: Option, } impl ServiceAccount { - /// Create a new service account with the given access key pub fn new(access_key: impl Into) -> Self { Self { access_key: access_key.into(), @@ -226,10 +229,32 @@ impl ServiceAccount { policy: None, account_status: None, expiration: None, + name: None, + description: None, + implied_policy: None, } } } +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ServiceAccountCreateResponse { + pub credentials: ServiceAccountCredentials, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ServiceAccountCredentials { + pub access_key: String, + pub secret_key: String, + + #[serde(skip_serializing_if = "Option::is_none")] + pub expiration: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + pub session_token: Option, +} + /// Entity type for policy attachment #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "lowercase")] @@ -276,6 +301,10 @@ pub struct UpdateGroupMembersRequest { /// Whether to remove (true) or add (false) members #[serde(default)] pub is_remove: bool, + + /// Group status + #[serde(rename = "groupStatus", default)] + pub status: String, } /// Request to create a service account @@ -286,8 +315,8 @@ pub struct CreateServiceAccountRequest { #[serde(skip_serializing_if = "Option::is_none")] pub policy: Option, - /// Optional expiration time - #[serde(skip_serializing_if = "Option::is_none")] + /// Optional expiration time (ISO 8601 format) + #[serde(rename = "expiration", skip_serializing_if = "Option::is_none")] pub expiry: Option, /// Optional name/description @@ -297,6 +326,14 @@ pub struct CreateServiceAccountRequest { /// Optional description #[serde(skip_serializing_if = "Option::is_none")] pub description: Option, + + /// Access key (required) + #[serde(rename = "accessKey")] + pub access_key: String, + + /// Secret key (required) + #[serde(rename = "secretKey")] + pub secret_key: String, } /// Bucket quota information returned by Admin API diff --git a/crates/s3/src/admin.rs b/crates/s3/src/admin.rs index 7a7fd21..e6b1364 100644 --- a/crates/s3/src/admin.rs +++ b/crates/s3/src/admin.rs @@ -12,7 +12,7 @@ use aws_sigv4::sign::v4; use rc_core::admin::{ AdminApi, BucketQuota, ClusterInfo, CreateServiceAccountRequest, Group, GroupStatus, HealStartRequest, HealStatus, Policy, PolicyEntity, PolicyInfo, ServiceAccount, - UpdateGroupMembersRequest, User, UserStatus, + ServiceAccountCreateResponse, UpdateGroupMembersRequest, User, UserStatus, }; use rc_core::{Alias, Error, Result}; use reqwest::header::{CONTENT_TYPE, HeaderMap, HeaderName, HeaderValue}; @@ -333,6 +333,12 @@ struct ServiceAccountInfo { account_status: Option, #[serde(default)] expiration: Option, + #[serde(default)] + name: Option, + #[serde(default)] + description: Option, + #[serde(default)] + implied_policy: Option, } /// Request body for setting bucket quota @@ -473,7 +479,7 @@ impl AdminApi for AdminClient { async fn create_policy(&self, name: &str, policy_document: &str) -> Result<()> { let query = [("name", name)]; let body = policy_document.as_bytes(); - self.request_no_response(Method::POST, "/add-canned-policy", Some(&query), Some(body)) + self.request_no_response(Method::PUT, "/add-canned-policy", Some(&query), Some(body)) .await } @@ -572,6 +578,7 @@ impl AdminApi for AdminClient { group: group.to_string(), members: members.to_vec(), is_remove: false, + status: "enabled".to_string(), }) .map_err(Error::Json)?; @@ -584,6 +591,7 @@ impl AdminApi for AdminClient { group: group.to_string(), members: members.to_vec(), is_remove: true, + status: "enabled".to_string(), }) .map_err(Error::Json)?; @@ -612,6 +620,9 @@ impl AdminApi for AdminClient { policy: None, account_status: sa.account_status, expiration: sa.expiration, + name: sa.name, + description: sa.description, + implied_policy: sa.implied_policy, }) .collect()) } @@ -621,6 +632,11 @@ impl AdminApi for AdminClient { let response: ServiceAccount = self .request(Method::GET, "/info-service-account", Some(&query), None) .await?; + + let mut response = response; + if response.access_key.is_empty() { + response.access_key = access_key.to_string(); + } Ok(response) } @@ -629,17 +645,28 @@ impl AdminApi for AdminClient { request: CreateServiceAccountRequest, ) -> Result { let body = serde_json::to_vec(&request).map_err(Error::Json)?; - let response: ServiceAccount = self - .request(Method::PUT, "/add-service-account", None, Some(&body)) + let response: ServiceAccountCreateResponse = self + .request(Method::PUT, "/add-service-accounts", None, Some(&body)) .await?; - Ok(response) + + Ok(ServiceAccount { + access_key: response.credentials.access_key, + secret_key: Some(response.credentials.secret_key), + expiration: response.credentials.expiration, + parent_user: None, + policy: None, + account_status: None, + name: None, + description: None, + implied_policy: None, + }) } async fn delete_service_account(&self, access_key: &str) -> Result<()> { let query = [("accessKey", access_key)]; self.request_no_response( Method::DELETE, - "/delete-service-account", + "/delete-service-accounts", Some(&query), None, )