Skip to content
Open
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
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,10 @@ rc admin service-account create local/ AKIAIOSFODNN7EXAMPLE wJalrXUtnFEMI/K7MDEN
# Create a service account with inline policy file
rc admin service-account create local/ SAKEY123 SASECRET123 --policy ./service-account-policy.json

# Inspect any access key and resolve whether it belongs to a user, service account, or STS credential
rc admin access-key info local/ AKIAIOSFODNN7EXAMPLE
rc admin access-key info local/ AKIAIOSFODNN7EXAMPLE --json

# Manage bucket event notifications
rc event add local/my-bucket arn:aws:sns:us-east-1:123456789012:topic --event 's3:ObjectCreated:*'
rc event list local/my-bucket
Expand Down Expand Up @@ -306,6 +310,7 @@ For full command documentation, see the [`rc` command reference](docs/reference/
| `admin policy` | Manage IAM policies (create, remove, list, info, attach) |
| `admin group` | Manage IAM groups (add, remove, list, info, enable, disable, add-members, rm-members) |
| `admin service-account` | Manage service accounts (create, remove, list, info) |
| `admin access-key` | Inspect access key identity and metadata (info) |
| `admin info` | Display cluster information (cluster, server, disk) |
| `admin heal` | Manage cluster healing operations (status, start, stop) |
| `admin pool` | List pools and inspect expansion/decommission status |
Expand Down
182 changes: 182 additions & 0 deletions crates/cli/src/commands/admin/access_key.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
//! Access key inspection commands.
//!
//! Commands for resolving an access key to its IAM identity type and metadata.

use clap::Subcommand;

use super::get_admin_client;
use crate::exit_code::ExitCode;
use crate::output::Formatter;
use rc_core::admin::{AccessKeyDetails, AccessKeyInfo, AdminApi, OpenIdAccessKeyInfo};

/// Access key inspection subcommands.
#[derive(Subcommand, Debug)]
pub enum AccessKeyCommands {
/// Get access key information
Info(InfoArgs),
}

#[derive(clap::Args, Debug)]
pub struct InfoArgs {
/// Alias name of the server
pub alias: String,

/// Access key to inspect
pub access_key: String,
}

/// Execute an access key subcommand.
pub async fn execute(cmd: AccessKeyCommands, formatter: &Formatter) -> ExitCode {
match cmd {
AccessKeyCommands::Info(args) => execute_info(args, formatter).await,
}
}

async fn execute_info(args: InfoArgs, formatter: &Formatter) -> ExitCode {
let client = match get_admin_client(&args.alias, formatter) {
Ok(c) => c,
Err(code) => return code,
};

match client.get_access_key_info(&args.access_key).await {
Ok(info) => {
if formatter.is_json() {
formatter.json(&info);
} else {
print_access_key_info(&info, formatter);
}
ExitCode::Success
}
Err(rc_core::Error::NotFound(_)) => {
formatter.error(&format!("Access key '{}' not found", args.access_key));
ExitCode::NotFound
}
Err(e) if is_access_key_not_found_error(&e) => {
formatter.error(&format!("Access key '{}' not found", args.access_key));
ExitCode::NotFound
}
Err(e) => formatter.fail(
ExitCode::GeneralError,
&format!("Failed to get access key info: {e}"),
),
}
}

fn is_access_key_not_found_error(error: &rc_core::Error) -> bool {
error.to_string().contains("access key not exist")
}

fn print_access_key_info(info: &AccessKeyInfo, formatter: &Formatter) {
let styled_key = formatter.style_name(&info.access_key);
formatter.println(&format!("Access Key: {styled_key}"));
formatter.println(&format!("User Type: {}", info.user_type));
formatter.println(&format!("Provider: {}", info.user_provider));

print_common_info(&info.info, formatter);

if let Some(username) = &info.ldap_specific_info.username {
formatter.println(&format!("LDAP Username: {username}"));
}

print_openid_info(&info.open_id_specific_info, formatter);
}

fn print_common_info(info: &AccessKeyDetails, formatter: &Formatter) {
if let Some(parent) = &info.parent_user {
formatter.println(&format!("Parent User: {parent}"));
}

if let Some(status) = &info.account_status {
formatter.println(&format!("Status: {status}"));
}

if let Some(expiration) = &info.expiration {
formatter.println(&format!("Expiration: {expiration}"));
}

if let Some(name) = &info.name {
formatter.println(&format!("Name: {name}"));
}

if let Some(description) = &info.description {
formatter.println(&format!("Description: {description}"));
}

if let Some(implied_policy) = info.implied_policy {
formatter.println(&format!("Implied Policy: {implied_policy}"));
}

if let Some(policy) = &info.policy {
formatter.println("");
formatter.println("Policy:");
formatter.println(policy);
}
}

fn print_openid_info(info: &OpenIdAccessKeyInfo, formatter: &Formatter) {
if let Some(config_name) = &info.config_name {
formatter.println(&format!("OpenID Config: {config_name}"));
}

if let Some(user_id) = &info.user_id {
formatter.println(&format!("OpenID User: {user_id}"));
}

if let Some(user_id_claim) = &info.user_id_claim {
formatter.println(&format!("User Claim: {user_id_claim}"));
}

if let Some(display_name) = &info.display_name {
formatter.println(&format!("Display Name: {display_name}"));
}

if let Some(display_name_claim) = &info.display_name_claim {
formatter.println(&format!("Display Claim: {display_name_claim}"));
}
}

#[cfg(test)]
mod tests {
use super::*;
use rc_core::admin::{AccessKeyDetails, LdapAccessKeyInfo, OpenIdAccessKeyInfo};

#[test]
fn test_access_key_info_serializes_server_shape() {
let info = AccessKeyInfo {
access_key: "svc-ldap".to_string(),
user_type: "Service Account".to_string(),
user_provider: "ldap".to_string(),
info: AccessKeyDetails {
parent_user: Some("ldap-parent".to_string()),
account_status: Some("on".to_string()),
implied_policy: Some(false),
policy: Some("{\"Version\":\"2012-10-17\"}".to_string()),
expiration: None,
name: Some("LDAP Service".to_string()),
description: None,
},
ldap_specific_info: LdapAccessKeyInfo {
username: Some("alice".to_string()),
},
open_id_specific_info: OpenIdAccessKeyInfo::default(),
};

let value = serde_json::to_value(info).expect("serialize access key info");

assert_eq!(value["accessKey"], "svc-ldap");
assert_eq!(value["userType"], "Service Account");
assert_eq!(value["userProvider"], "ldap");
assert_eq!(value["parentUser"], "ldap-parent");
assert_eq!(value["accountStatus"], "on");
assert_eq!(value["ldapSpecificInfo"]["username"], "alice");
assert!(value.get("openIDSpecificInfo").is_none());
assert!(value.get("openIdSpecificInfo").is_none());
}

#[test]
fn test_access_key_not_exist_error_maps_to_not_found() {
let error = rc_core::Error::General("Bad request: access key not exist".to_string());

assert!(is_access_key_not_found_error(&error));
}
}
22 changes: 22 additions & 0 deletions crates/cli/src/commands/admin/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
//! This module provides commands for managing users, policies, groups,
//! service accounts, and cluster operations on RustFS/MinIO-compatible servers.

mod access_key;
mod decommission;
mod expand;
mod group;
Expand Down Expand Up @@ -63,6 +64,10 @@ pub enum AdminCommands {
/// Manage service accounts
#[command(name = "service-account", subcommand)]
ServiceAccount(service_account::ServiceAccountCommands),

/// Inspect access key identities
#[command(name = "access-key", subcommand)]
AccessKey(access_key::AccessKeyCommands),
}

/// Execute an admin subcommand
Expand All @@ -84,6 +89,9 @@ pub async fn execute(cmd: AdminCommands, output_config: OutputConfig) -> ExitCod
AdminCommands::Policy(policy_cmd) => policy::execute(policy_cmd, &formatter).await,
AdminCommands::Group(group_cmd) => group::execute(group_cmd, &formatter).await,
AdminCommands::ServiceAccount(sa_cmd) => service_account::execute(sa_cmd, &formatter).await,
AdminCommands::AccessKey(access_key_cmd) => {
access_key::execute(access_key_cmd, &formatter).await
}
}
}

Expand Down Expand Up @@ -154,6 +162,20 @@ mod tests {
}
}

#[test]
fn test_parse_admin_access_key_info() {
let cli =
TestCli::parse_from(["rc", "access-key", "info", "local", "AKIAIOSFODNN7EXAMPLE"]);

match cli.command {
AdminCommands::AccessKey(access_key::AccessKeyCommands::Info(args)) => {
assert_eq!(args.alias, "local");
assert_eq!(args.access_key, "AKIAIOSFODNN7EXAMPLE");
}
_ => panic!("Unexpected command parsing result"),
}
}

#[test]
fn test_parse_admin_heal_start_options() {
let cli = TestCli::parse_from([
Expand Down
6 changes: 6 additions & 0 deletions crates/cli/tests/help_contract.rs
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,7 @@ fn top_level_command_help_contract() {
"policy",
"group",
"service-account",
"access-key",
],
},
HelpCase {
Expand Down Expand Up @@ -760,6 +761,11 @@ fn nested_subcommand_help_contract() {
usage: "Usage: rc admin service-account rm [OPTIONS] <ALIAS> <ACCESS_KEY>",
expected_tokens: &[],
},
HelpCase {
args: &["admin", "access-key", "info"],
usage: "Usage: rc admin access-key info [OPTIONS] <ALIAS> <ACCESS_KEY>",
expected_tokens: &[],
},
HelpCase {
args: &["version", "enable"],
usage: "Usage: rc version enable [OPTIONS] <PATH>",
Expand Down
8 changes: 6 additions & 2 deletions crates/core/src/admin/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,9 @@ pub use tier::{
TierRustFS, TierS3, TierTencent, TierType,
};
pub use types::{
BucketQuota, CreateServiceAccountRequest, Group, GroupStatus, Policy, PolicyEntity, PolicyInfo,
ServiceAccount, ServiceAccountCreateResponse, ServiceAccountCredentials, SetPolicyRequest,
AccessKeyDetails, AccessKeyInfo, BucketQuota, CreateServiceAccountRequest, Group, GroupStatus,
LdapAccessKeyInfo, OpenIdAccessKeyInfo, Policy, PolicyEntity, PolicyInfo, ServiceAccount,
ServiceAccountCreateResponse, ServiceAccountCredentials, SetPolicyRequest,
UpdateGroupMembersRequest, User, UserStatus,
};

Expand Down Expand Up @@ -156,6 +157,9 @@ pub trait AdminApi: Send + Sync {
/// Delete a service account
async fn delete_service_account(&self, access_key: &str) -> Result<()>;

/// Get information for any access key type.
async fn get_access_key_info(&self, access_key: &str) -> Result<AccessKeyInfo>;

// ==================== Bucket Quota Operations ====================

/// Set bucket quota in bytes
Expand Down
Loading
Loading