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
963 changes: 963 additions & 0 deletions crates/cli/src/commands/admin/iam.rs

Large diffs are not rendered by default.

6 changes: 6 additions & 0 deletions crates/cli/src/commands/admin/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ mod diagnostics;
mod expand;
mod group;
mod heal;
mod iam;
mod idp;
mod ilm;
mod info;
Expand Down Expand Up @@ -116,6 +117,10 @@ pub enum AdminCommands {
#[command(name = "bucket-metadata", subcommand)]
BucketMetadata(bucket_metadata::BucketMetadataCommands),

/// Export or import versioned IAM archives
#[command(subcommand)]
Iam(iam::IamCommands),

/// Control the server process (restart, stop, freeze, unfreeze)
#[command(subcommand)]
Service(service::ServiceCommands),
Expand Down Expand Up @@ -158,6 +163,7 @@ pub async fn execute(cmd: AdminCommands, output_config: OutputConfig) -> ExitCod
AdminCommands::BucketMetadata(command) => {
bucket_metadata::execute(command, &formatter).await
}
AdminCommands::Iam(iam_cmd) => iam::execute(iam_cmd, &formatter).await,
AdminCommands::Service(service_cmd) => service::execute(service_cmd, &formatter).await,
AdminCommands::Replicate(replicate_cmd) => {
replicate::execute(replicate_cmd, &formatter).await
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"schema_version":3,"type":"admin_iam_archive","status":"success","data":{"operation":"iam.import","archive_version":1,"dry_run":true,"conflict_policy":"fail","inventory":{"users":0,"groups":0,"policies":0,"service_accounts":0},"conflicts":{"users":0,"groups":0,"policies":0,"service_accounts":0},"result":null}}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"schema_version":3,"type":"admin_iam_archive","status":"error","error":{"type":"conflict","message":"IAM import found existing destination entities","retryable":false,"suggestion":"Review conflicts before selecting overwrite."}}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"schema_version":3,"type":"admin_iam_archive","status":"partial","data":{"operation":"iam.import","archive_version":1,"dry_run":false,"conflict_policy":"overwrite","inventory":{"users":2,"groups":1,"policies":1,"service_accounts":1},"conflicts":{"users":1,"groups":0,"policies":1,"service_accounts":0},"result":{"added":3,"skipped":0,"removed":0,"failed":1,"partial":true}}}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"schema_version":3,"type":"admin_iam_archive","status":"success","data":{"operation":"iam.export","archive_version":1,"entries":4,"bytes":2048,"private":true}}
15 changes: 15 additions & 0 deletions crates/cli/tests/help_contract.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1134,6 +1134,21 @@ fn nested_subcommand_help_contract() {
usage: "Usage: rc admin access-key info [OPTIONS] <ALIAS> <ACCESS_KEY>",
expected_tokens: &[],
},
HelpCase {
args: &["admin", "iam"],
usage: "Usage: rc admin iam [OPTIONS] <COMMAND>",
expected_tokens: &["export", "import"],
},
HelpCase {
args: &["admin", "iam", "export"],
usage: "Usage: rc admin iam export [OPTIONS] --file <FILE> <ALIAS>",
expected_tokens: &["--file"],
},
HelpCase {
args: &["admin", "iam", "import"],
usage: "Usage: rc admin iam import [OPTIONS] --file <FILE> <ALIAS>",
expected_tokens: &["--file", "--dry-run", "--yes", "--conflict"],
},
HelpCase {
args: &["version", "enable"],
usage: "Usage: rc version enable [OPTIONS] <PATH>",
Expand Down
71 changes: 71 additions & 0 deletions crates/core/src/admin/iam.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,77 @@ use serde::{Deserialize, Serialize};

use crate::{Error, Result};

/// Maximum accepted IAM archive size. This matches RustFS's import body limit.
pub const MAX_IAM_ARCHIVE_BYTES: usize = 10 * 1024 * 1024;

/// Maximum accepted structured response from an IAM import.
pub const MAX_IAM_IMPORT_RESPONSE_BYTES: usize = 8 * 1024 * 1024;

/// Names contained in an IAM archive, used for conflict preflight.
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct IamArchiveInventory {
pub users: Vec<String>,
pub groups: Vec<String>,
pub policies: Vec<String>,
pub service_accounts: Vec<String>,
}

/// One category in RustFS's structured IAM import report.
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct IamArchiveResultEntities {
#[serde(default)]
pub policies: Vec<String>,
#[serde(default)]
pub users: Vec<String>,
#[serde(default)]
pub groups: Vec<String>,
#[serde(default)]
pub service_accounts: Vec<String>,
#[serde(default)]
pub user_policies: Vec<serde_json::Value>,
#[serde(default)]
pub group_policies: Vec<serde_json::Value>,
#[serde(default)]
pub sts_policies: Vec<serde_json::Value>,
}

/// A failed IAM entity. Error text is intentionally discarded at the client
/// boundary because a backend error may include credential material.
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct IamArchiveImportSection {
#[serde(default)]
pub name: String,
#[serde(default)]
pub policies: Vec<String>,
}

/// Secret-safe typed summary of an IAM import.
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize)]
pub struct IamArchiveImportResult {
pub skipped: IamArchiveResultEntities,
pub removed: IamArchiveResultEntities,
pub added: IamArchiveResultEntities,
pub failed: Vec<IamArchiveImportSection>,
}

/// Bounded RustFS IAM archive transport.
#[async_trait]
pub trait IamArchiveApi: Send + Sync {
/// Download the server IAM archive.
async fn export_iam_archive(&self) -> Result<Vec<u8>>;

/// Import one already validated IAM archive. Implementations must not retry
/// this mutation because a disconnected response has an unknown outcome.
async fn import_iam_archive(&self, archive: Vec<u8>) -> Result<IamArchiveImportResult>;

/// Return names which already exist on the destination.
async fn iam_archive_conflicts(
&self,
inventory: &IamArchiveInventory,
) -> Result<IamArchiveInventory>;
}

/// Capability name used to guard policy-entity inspection.
pub const IAM_POLICY_ENTITIES_CAPABILITY: &str = "admin.iam.policy-entities";

Expand Down
4 changes: 3 additions & 1 deletion crates/core/src/admin/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,9 @@ pub use diagnostics::{
};
pub use iam::{
GroupPolicyEntities, IAM_POLICY_DETACH_CAPABILITY, IAM_POLICY_ENTITIES_CAPABILITY,
IamMutationApi, IamReadApi, MAX_IAM_POLICY_DETACH_POLICIES,
IamArchiveApi, IamArchiveImportResult, IamArchiveImportSection, IamArchiveInventory,
IamArchiveResultEntities, IamMutationApi, IamReadApi, MAX_IAM_ARCHIVE_BYTES,
MAX_IAM_IMPORT_RESPONSE_BYTES, MAX_IAM_POLICY_DETACH_POLICIES,
MAX_IAM_POLICY_DETACH_REQUEST_BYTES, MAX_IAM_POLICY_DETACH_RESPONSE_BYTES,
MAX_IAM_POLICY_DETACH_SELECTOR_BYTES, MAX_IAM_POLICY_ENTITIES_RESPONSE_BYTES,
MAX_IAM_POLICY_ENTITY_SELECTOR_BYTES, MAX_IAM_POLICY_ENTITY_SELECTORS, PolicyDetachEntity,
Expand Down
Loading
Loading