Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 43 additions & 24 deletions crates/cli/src/commands/admin/group.rs
Original file line number Diff line number Diff line change
Expand Up @@ -210,31 +210,50 @@ async fn execute_add(args: AddArgs, formatter: &Formatter) -> ExitCode {
return ExitCode::UsageError;
}

let members: Option<Vec<String>> = 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<String> = 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
}
}
}
}
Expand Down
11 changes: 11 additions & 0 deletions crates/cli/src/commands/admin/service_account.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<String>,
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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);
Expand Down
3 changes: 2 additions & 1 deletion crates/core/src/admin/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
55 changes: 46 additions & 9 deletions crates/core/src/admin/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<String>,

/// Parent user (owner of this service account)
#[serde(skip_serializing_if = "Option::is_none")]
pub parent_user: Option<String>,

/// Policy attached to this service account
#[serde(skip_serializing_if = "Option::is_none")]
pub policy: Option<String>,

/// Account status
#[serde(skip_serializing_if = "Option::is_none")]
pub account_status: Option<String>,

/// Expiration time (if any)
#[serde(skip_serializing_if = "Option::is_none")]
pub expiration: Option<String>,

#[serde(skip_serializing_if = "Option::is_none")]
pub name: Option<String>,

#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,

#[serde(skip_serializing_if = "Option::is_none")]
pub implied_policy: Option<bool>,
}

impl ServiceAccount {
/// Create a new service account with the given access key
pub fn new(access_key: impl Into<String>) -> Self {
Self {
access_key: access_key.into(),
Expand All @@ -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<String>,

#[serde(skip_serializing_if = "Option::is_none")]
pub session_token: Option<String>,
}

/// Entity type for policy attachment
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
Expand Down Expand Up @@ -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
Expand All @@ -286,8 +315,8 @@ pub struct CreateServiceAccountRequest {
#[serde(skip_serializing_if = "Option::is_none")]
pub policy: Option<String>,

/// 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<String>,

/// Optional name/description
Expand All @@ -297,6 +326,14 @@ pub struct CreateServiceAccountRequest {
/// Optional description
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,

/// 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
Expand Down
39 changes: 33 additions & 6 deletions crates/s3/src/admin.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down Expand Up @@ -333,6 +333,12 @@ struct ServiceAccountInfo {
account_status: Option<String>,
#[serde(default)]
expiration: Option<String>,
#[serde(default)]
name: Option<String>,
#[serde(default)]
description: Option<String>,
#[serde(default)]
implied_policy: Option<bool>,
}

/// Request body for setting bucket quota
Expand Down Expand Up @@ -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
}

Expand Down Expand Up @@ -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)?;

Expand All @@ -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)?;

Expand Down Expand Up @@ -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())
}
Expand All @@ -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)
}

Expand All @@ -629,17 +645,28 @@ impl AdminApi for AdminClient {
request: CreateServiceAccountRequest,
) -> Result<ServiceAccount> {
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,
)
Expand Down