use silicon_iam_client::{api::members::MemberFilter, models};
use crate::{
cli::MemberCommand,
context::Context,
error::Result,
output::{Format, Table, json, label, next_cursor, or_dash, plain},
};
#[allow(
clippy::too_many_lines,
reason = "one arm per verb; splitting the match would only move it elsewhere"
)]
pub async fn run(context: &Context, command: MemberCommand) -> Result<()> {
let client = context.authenticated().await?;
let org = context.organization()?;
match command {
MemberCommand::List {
principal_type,
tag,
status,
page,
} => {
let listed = client
.members()
.list(
org,
&MemberFilter {
principal_type,
tag_id: tag,
status,
},
&page.paging(),
)
.await?;
match context.format {
Format::Json => json(&listed),
Format::Text => {
let mut table =
Table::new(["membership", "principal", "type", "role", "status", "tags"]);
for member in &listed.items {
table.row([
member.id.to_string(),
member.principal.public_id.clone(),
label(&member.principal.type_field),
label(&member.org_role),
label(&member.status),
member
.tags
.iter()
.map(|tag| tag.name.clone())
.collect::<Vec<_>>()
.join(","),
]);
}
table.print();
next_cursor(listed.page.has_more, listed.page.next_cursor.as_deref());
Ok(())
}
}
}
MemberCommand::Show { membership_id } => {
let member = client.members().get(org, membership_id).await?;
report(context, &member)
}
MemberCommand::Authorization { membership_id } => {
let authorization = client.members().authorization(org, membership_id).await?;
match context.format {
Format::Json => json(&authorization),
Format::Text => {
let mut table = Table::new(["field", "value"]);
table.row(["membership", &authorization.membership_id.to_string()]);
table.row(["role", &label(&authorization.org_role)]);
table.row([
"capabilities",
&authorization
.capabilities
.iter()
.map(label)
.collect::<Vec<_>>()
.join(", "),
]);
table.row(["epoch", &authorization.authorization_epoch.to_string()]);
table.print();
Ok(())
}
}
}
MemberCommand::Update {
membership_id,
first_silicon,
clear_first_silicon,
reports_to,
clear_reports_to,
profile_photo,
clear_profile_photo,
} => {
let current = client.members().get(org, membership_id).await?;
let updated = client
.members()
.update(
org,
membership_id,
current.version,
&models::MembershipDirectoryPatch {
first_silicon_membership_id: nullable_patch(
first_silicon,
clear_first_silicon,
),
extra_silicon_membership_ids: None,
default_trust: None,
reports_to_membership_id: nullable_patch(reports_to, clear_reports_to),
profile_photo: nullable_patch(profile_photo, clear_profile_photo),
},
&context.mutation(),
)
.await?;
report(context, &updated)
}
MemberCommand::Remove {
membership_id,
reassign_reports_to,
} => {
let current = client.members().get(org, membership_id).await?;
client
.members()
.remove(
org,
membership_id,
current.version,
reassign_reports_to,
&context.mutation(),
)
.await?;
println!("Removed {}.", current.principal.public_id);
Ok(())
}
MemberCommand::Promote { membership_id } => {
let current = client.members().get(org, membership_id).await?;
let authorization = client
.members()
.promote_admin(org, membership_id, current.version, &context.mutation())
.await?;
json_or_line(context, &authorization, "Promoted to administrator.")
}
MemberCommand::Demote { membership_id } => {
let current = client.members().get(org, membership_id).await?;
let authorization = client
.members()
.demote_admin(org, membership_id, current.version, &context.mutation())
.await?;
json_or_line(context, &authorization, "Demoted to member.")
}
MemberCommand::Capabilities {
membership_id,
capabilities,
} => {
let current = client.members().get(org, membership_id).await?;
let updated = client
.members()
.replace_capabilities(
org,
membership_id,
current.version,
&models::OrganizationCapabilitiesReplace {
capabilities: capabilities.into_iter().map(capability).collect(),
},
&context.mutation(),
)
.await?;
json_or_line(context, &updated, "Capabilities replaced.")
}
MemberCommand::Directory { fields, page } => {
let listed = client
.members()
.directory(org, fields.as_deref(), &page.paging())
.await?;
match context.format {
Format::Json => json(&listed),
Format::Text => {
let fields = directory_fields(fields.as_deref());
let mut table = Table::new(fields.iter().copied());
for entry in &listed.items {
table.row(fields.iter().map(|field| directory_value(entry, field)));
}
table.print();
next_cursor(listed.page.has_more, listed.page.next_cursor.as_deref());
Ok(())
}
}
}
MemberCommand::Self_ { fields } => {
let entry = client
.members()
.directory_self(org, fields.as_deref())
.await?;
report_directory_member(context, &entry, fields.as_deref())
}
MemberCommand::DirectoryMember {
membership_id,
fields,
} => {
let entry = client
.members()
.directory_member(org, membership_id, fields.as_deref())
.await?;
report_directory_member(context, &entry, fields.as_deref())
}
}
}
fn report_directory_member(
context: &Context,
entry: &models::DirectoryMember,
fields: Option<&str>,
) -> Result<()> {
match context.format {
Format::Json => json(entry),
Format::Text => {
let mut table = Table::new(["field", "value"]);
for field in directory_fields(fields) {
table.row([field.to_owned(), directory_value(entry, field)]);
}
table.print();
Ok(())
}
}
}
const DIRECTORY_FIELDS: [&str; 6] = ["name", "id", "role", "org", "tags", "trust"];
fn directory_fields(fields: Option<&str>) -> Vec<&str> {
fields.map_or_else(
|| DIRECTORY_FIELDS.to_vec(),
|fields| fields.split(',').collect(),
)
}
fn directory_value(entry: &models::DirectoryMember, field: &str) -> String {
match field {
"name" => or_dash(entry.name.as_deref()),
"id" => or_dash(entry.id.as_deref()),
"role" => entry.role.as_ref().map_or_else(
|| "-".to_owned(),
|role| {
if role.job_role.is_empty() {
label(&role.org_role)
} else {
format!("{} ({})", label(&role.org_role), role.job_role)
}
},
),
"org" => entry.org.as_ref().map_or_else(
|| "-".to_owned(),
|organization| format!("{} ({})", organization.id, organization.name),
),
"tags" => entry.tags.as_ref().map_or_else(
|| "-".to_owned(),
|tags| {
let rendered = tags
.iter()
.map(|tag| tag.name.clone())
.collect::<Vec<_>>()
.join(", ");
if rendered.is_empty() {
"-".to_owned()
} else {
rendered
}
},
),
"trust" => entry.trust.as_ref().map_or_else(|| "-".to_owned(), plain),
_ => "-".to_owned(),
}
}
#[allow(
clippy::option_option,
reason = "JSON Merge Patch has distinct omitted, null, and value states"
)]
fn nullable_patch<T>(value: Option<T>, clear: bool) -> Option<Option<T>> {
if clear { Some(None) } else { value.map(Some) }
}
fn capability(name: String) -> models::OrganizationCapability {
serde_json::from_value(serde_json::Value::String(name.clone()))
.unwrap_or(models::OrganizationCapability::Other(name))
}
fn report(context: &Context, member: &models::Membership) -> Result<()> {
match context.format {
Format::Json => json(member),
Format::Text => {
let mut table = Table::new(["field", "value"]);
table.row(["membership", &member.id.to_string()]);
table.row(["principal", &member.principal.public_id]);
table.row(["role", &label(&member.org_role)]);
table.row(["job_role", &member.job_role]);
table.row(["status", &label(&member.status)]);
table.row([
"tags",
&member
.tags
.iter()
.map(|tag| tag.name.clone())
.collect::<Vec<_>>()
.join(", "),
]);
table.row(["version", &member.version.to_string()]);
table.print();
Ok(())
}
}
}
fn json_or_line<T: serde::Serialize>(context: &Context, value: &T, line: &str) -> Result<()> {
match context.format {
Format::Json => json(value),
Format::Text => {
println!("{line}");
Ok(())
}
}
}