use super::*;
impl BucketWarden {
pub fn console_api_overview(
&mut self,
access_key_id: &str,
) -> Result<ConsoleApiOverviewReport, RuntimeError> {
let session = self.console_api_current_user(access_key_id)?;
let principal = session.principal_id.clone();
let report = self.ops_console_report(&principal, None)?;
Ok(ConsoleApiOverviewReport {
generated_at_epoch_seconds: self.clock_epoch_seconds,
session,
health: report.admin_surface_health(&report),
metrics: report.metrics,
retention_bucket_count: report.retention_bucket_count,
retained_version_count: report.retained_version_count,
lifecycle_bucket_count: report.lifecycle_bucket_count,
lifecycle_rule_count: report.lifecycle_rule_count,
security_governance_findings: report.security_governance_findings,
failure_modes: report.failure_modes,
})
}
pub fn console_api_reports_dashboard(
&mut self,
access_key_id: &str,
) -> Result<ConsoleApiReportsDashboard, RuntimeError> {
let principal = self.console_api_principal(access_key_id)?;
let health = self.ops_health_report(&principal, None)?;
let config = self.ops_config_report(&principal, None)?;
let report = self.ops_console_report(&principal, None)?;
let generated_at_epoch_seconds = self.clock_epoch_seconds;
Ok(ConsoleApiReportsDashboard {
generated_at_epoch_seconds,
reports: vec![
ConsoleApiReportSummary {
id: "health".to_string(),
title: "Health report".to_string(),
category: "operations".to_string(),
status: health.status,
endpoint: "/api/v1/reports/health".to_string(),
export_endpoint: Some("/api/v1/reports/health".to_string()),
generated_at_epoch_seconds: health.generated_at_epoch_seconds,
},
ConsoleApiReportSummary {
id: "configuration".to_string(),
title: "Configuration report".to_string(),
category: "configuration".to_string(),
status: if config.unsupported_storage_backends.is_empty() {
"ok".to_string()
} else {
"tracked".to_string()
},
endpoint: "/api/v1/reports/config".to_string(),
export_endpoint: Some("/api/v1/reports/config".to_string()),
generated_at_epoch_seconds: config.generated_at_epoch_seconds,
},
ConsoleApiReportSummary {
id: "overview".to_string(),
title: "Overview dashboard".to_string(),
category: "dashboard".to_string(),
status: if report.failure_modes.is_empty() {
"ok".to_string()
} else {
"degraded".to_string()
},
endpoint: "/api/v1/overview".to_string(),
export_endpoint: None,
generated_at_epoch_seconds: report.generated_at_epoch_seconds,
},
ConsoleApiReportSummary {
id: "storage".to_string(),
title: "Storage report".to_string(),
category: "storage".to_string(),
status: report.config.active_storage_backend.clone(),
endpoint: "/api/v1/reports/storage".to_string(),
export_endpoint: Some("/api/v1/reports/storage".to_string()),
generated_at_epoch_seconds,
},
ConsoleApiReportSummary {
id: "governance".to_string(),
title: "Governance report".to_string(),
category: "governance".to_string(),
status: if report.security_governance_findings.is_empty() {
"ok".to_string()
} else {
"findings".to_string()
},
endpoint: "/api/v1/reports/governance".to_string(),
export_endpoint: Some("/api/v1/reports/governance".to_string()),
generated_at_epoch_seconds: report.generated_at_epoch_seconds,
},
ConsoleApiReportSummary {
id: "incident".to_string(),
title: "Incident report".to_string(),
category: "incident".to_string(),
status: "tracked".to_string(),
endpoint: "/api/v1/reports/incident".to_string(),
export_endpoint: Some("/api/v1/reports/incident".to_string()),
generated_at_epoch_seconds,
},
],
})
}
pub fn console_api_health_report(
&mut self,
access_key_id: &str,
) -> Result<OpsHealthReport, RuntimeError> {
let principal = self.console_api_principal(access_key_id)?;
self.ops_health_report(&principal, None)
}
pub fn console_api_configuration_report(
&mut self,
access_key_id: &str,
) -> Result<RuntimeConfigReport, RuntimeError> {
let principal = self.console_api_principal(access_key_id)?;
self.ops_config_report(&principal, None)
}
pub fn console_api_redacted_configuration_report(
&mut self,
access_key_id: &str,
) -> Result<serde_json::Value, RuntimeError> {
let report = self.console_api_configuration_report(access_key_id)?;
let mut value = serde_json::to_value(report).map_err(RuntimeError::SnapshotSerialize)?;
redact_config_value(&mut value);
Ok(value)
}
pub fn console_api_storage_report(
&mut self,
access_key_id: &str,
) -> Result<StorageBackendSupportReport, RuntimeError> {
let principal = self.console_api_principal(access_key_id)?;
self.require_operator_action(
&principal,
OperatorAction::ReadDiagnostics,
"*",
"ui:GetStorageReport",
)?;
Ok(self.storage_backend_support_report())
}
pub fn console_api_governance_report(
&mut self,
access_key_id: &str,
) -> Result<serde_json::Value, RuntimeError> {
let principal = self.console_api_principal(access_key_id)?;
self.require_operator_action(
&principal,
OperatorAction::ReadDiagnostics,
"*",
"ui:GetGovernanceReport",
)?;
let report = self.ops_console_report(&principal, None)?;
let total_bucket_count = report.metrics.bucket_count;
let retention_exception_bucket_count =
total_bucket_count.saturating_sub(report.retention_bucket_count);
let retention_coverage_percent = if total_bucket_count == 0 {
0
} else {
(report.retention_bucket_count * 100) / total_bucket_count
};
let total_object_count = report.metrics.object_count;
let legal_hold_object_count = report
.objects
.iter()
.filter(|object| object.legal_hold)
.count();
let legal_hold_exception_object_count =
total_object_count.saturating_sub(legal_hold_object_count);
let legal_hold_coverage_percent = if total_object_count == 0 {
0
} else {
(legal_hold_object_count * 100) / total_object_count
};
Ok(serde_json::json!({
"generated_at_epoch_seconds": report.generated_at_epoch_seconds,
"total_bucket_count": total_bucket_count,
"total_object_count": total_object_count,
"retention_bucket_count": report.retention_bucket_count,
"retention_exception_bucket_count": retention_exception_bucket_count,
"retention_coverage_percent": retention_coverage_percent,
"retained_version_count": report.retained_version_count,
"lifecycle_bucket_count": report.lifecycle_bucket_count,
"lifecycle_rule_count": report.lifecycle_rule_count,
"retention_scope": "tenant",
"retention_remediation_surface": "/ui/v1/buckets",
"legal_hold_scope": "tenant",
"legal_hold_object_count": legal_hold_object_count,
"legal_hold_exception_object_count": legal_hold_exception_object_count,
"legal_hold_coverage_percent": legal_hold_coverage_percent,
"legal_hold_remediation_surface": "/ui/v1/buckets",
"legal_hold_mutation_surface": "object-detail",
"security_governance_findings": report.security_governance_findings,
"buckets_with_object_lock": report.config.buckets_with_object_lock,
"buckets_with_lifecycle": report.config.buckets_with_lifecycle,
"buckets_with_replication": report.config.buckets_with_replication,
}))
}
pub fn console_api_incident_report(
&mut self,
access_key_id: &str,
) -> Result<OpsIncidentReport, RuntimeError> {
let principal = self.console_api_principal(access_key_id)?;
self.ops_incident_report(&principal, None, "replication_failure")
}
pub fn console_api_bucket_list(
&mut self,
access_key_id: &str,
query: ConsoleApiListQuery,
) -> Result<ConsoleApiBucketList, RuntimeError> {
let principal = self.console_api_principal(access_key_id)?;
let mut rows = self.ops_console_report(&principal, None)?.buckets;
match query.sort.as_deref().unwrap_or("name") {
"name" => rows.sort_by(|left, right| left.name.cmp(&right.name)),
"-name" => rows.sort_by(|left, right| right.name.cmp(&left.name)),
"object_count" => rows.sort_by_key(|row| row.object_count),
"-object_count" => {
rows.sort_by(|left, right| right.object_count.cmp(&left.object_count))
}
value => {
return Err(RuntimeError::InvalidListParameter {
name: "sort".to_string(),
value: value.to_string(),
})
}
}
rows.retain(|row| matches_text(&[&row.name, &row.owner, &row.tenant_id], &query.q));
let (page, buckets) = page_vec(rows, &query, "name")?;
Ok(ConsoleApiBucketList { page, buckets })
}
pub fn console_api_bucket_detail(
&mut self,
access_key_id: &str,
bucket: &str,
) -> Result<ConsoleApiBucketDetail, RuntimeError> {
let principal = self.console_api_principal(access_key_id)?;
let report = self.ops_console_report(&principal, Some(bucket))?;
let bucket = report
.buckets
.into_iter()
.next()
.ok_or_else(|| RuntimeError::NoSuchBucket(bucket.to_string()))?;
Ok(ConsoleApiBucketDetail {
bucket,
policies: report.policies,
findings: report.security_governance_findings,
})
}
pub fn console_api_object_list(
&mut self,
access_key_id: &str,
bucket: &str,
query: ConsoleApiListQuery,
) -> Result<ConsoleApiObjectList, RuntimeError> {
let principal = self.console_api_principal(access_key_id)?;
let mut rows = self.ops_console_report(&principal, Some(bucket))?.objects;
match query.sort.as_deref().unwrap_or("key") {
"key" => rows.sort_by(|left, right| left.key.cmp(&right.key)),
"-key" => rows.sort_by(|left, right| right.key.cmp(&left.key)),
"latest_modified" => rows.sort_by_key(|row| row.latest_modified_epoch_seconds),
"-latest_modified" => rows.sort_by(|left, right| {
right
.latest_modified_epoch_seconds
.cmp(&left.latest_modified_epoch_seconds)
}),
value => {
return Err(RuntimeError::InvalidListParameter {
name: "sort".to_string(),
value: value.to_string(),
})
}
}
if let Some(prefix) = query.prefix.as_deref() {
rows.retain(|row| row.key.starts_with(prefix));
}
rows.retain(|row| matches_text(&[&row.bucket, &row.key, &row.owner], &query.q));
let (page, objects) = page_vec(rows, &query, "key")?;
Ok(ConsoleApiObjectList { page, objects })
}
pub fn console_api_object_version_history(
&mut self,
access_key_id: &str,
bucket: &str,
key: &str,
query: ConsoleApiListQuery,
) -> Result<ConsoleApiObjectVersionHistory, RuntimeError> {
let principal = self.console_api_principal(access_key_id)?;
self.require_operator_action(
&principal,
OperatorAction::ReadDiagnostics,
bucket,
"ui:GetObjectVersionHistory",
)?;
let rows = self.object_version_rows(bucket, key)?;
let (page, versions) = page_vec(rows, &query, "-latest_modified")?;
Ok(ConsoleApiObjectVersionHistory { page, versions })
}
pub fn console_api_object_governance_summary(
&mut self,
access_key_id: &str,
bucket: &str,
key: &str,
version_id: Option<&str>,
) -> Result<ConsoleApiObjectGovernanceSummary, RuntimeError> {
let principal = self.console_api_principal(access_key_id)?;
self.require_operator_action(
&principal,
OperatorAction::ReadDiagnostics,
bucket,
"ui:GetObjectGovernanceSummary",
)?;
self.object_governance_summary(bucket, key, version_id)
}
pub fn console_api_object_lock_retention(
&mut self,
access_key_id: &str,
bucket: &str,
key: &str,
version_id: Option<&str>,
) -> Result<ConsoleApiObjectGovernanceSummary, RuntimeError> {
self.console_api_object_governance_summary(access_key_id, bucket, key, version_id)
}
}
fn redact_config_value(value: &mut serde_json::Value) {
match value {
serde_json::Value::Object(map) => {
for (key, value) in map.iter_mut() {
if is_secret_config_key(key) {
*value = serde_json::Value::String("[redacted]".to_string());
} else {
redact_config_value(value);
}
}
}
serde_json::Value::Array(values) => {
for value in values {
redact_config_value(value);
}
}
_ => {}
}
}
fn is_secret_config_key(key: &str) -> bool {
let key = key.to_ascii_lowercase();
key.contains("secret")
|| key.contains("password")
|| key.contains("token")
|| key == "key_id"
|| key.ends_with("_key")
}
trait ConsoleOverviewHealth {
fn admin_surface_health(&self, report: &ConsoleRuntimeReport) -> OpsHealthReport;
}
impl ConsoleOverviewHealth for ConsoleRuntimeReport {
fn admin_surface_health(&self, report: &ConsoleRuntimeReport) -> OpsHealthReport {
OpsHealthReport {
scope: report.scope.clone(),
target: report.target.clone(),
generated_at_epoch_seconds: report.generated_at_epoch_seconds,
status: if report.failure_modes.is_empty() {
"ok".to_string()
} else {
"degraded".to_string()
},
live: true,
ready: report.failure_modes.is_empty(),
bucket_count: report.metrics.bucket_count,
object_version_count: report.metrics.version_count,
multipart_upload_count: report.metrics.multipart_upload_count,
audit_event_count: report.metrics.audit_event_count,
notification_event_count: report.metrics.notification_event_count,
replication_record_count: report.metrics.replication_record_count,
credential_count: report.users.len(),
first_audit_sequence: report.audit.first_audit_sequence,
last_audit_sequence: report.audit.last_audit_sequence,
last_replication_sequence: report.audit.last_replication_sequence,
active_storage_backend: report.config.active_storage_backend.clone(),
unsupported_storage_backends: report.config.unsupported_storage_backends.clone(),
active_replication_strategy: report.config.active_replication_strategy.clone(),
unsupported_replication_strategies: report
.config
.unsupported_replication_strategies
.clone(),
active_erasure_coding_profile: report.config.active_erasure_coding_profile.clone(),
unsupported_erasure_coding_profiles: report
.config
.unsupported_erasure_coding_profiles
.clone(),
active_placement_profile: report.config.active_placement_profile.clone(),
unsupported_placement_domains: report.config.unsupported_placement_domains.clone(),
active_consistency_model: report.config.active_consistency_model.clone(),
unsupported_consistency_models: report.config.unsupported_consistency_models.clone(),
active_metadata_architecture: report.config.active_metadata_architecture.clone(),
unsupported_metadata_architectures: report
.config
.unsupported_metadata_architectures
.clone(),
active_object_layout: report.config.active_object_layout.clone(),
unsupported_object_layouts: report.config.unsupported_object_layouts.clone(),
active_small_object_mode: report.config.active_small_object_mode.clone(),
unsupported_small_object_modes: report.config.unsupported_small_object_modes.clone(),
active_large_object_mode: report.config.active_large_object_mode.clone(),
unsupported_large_object_modes: report.config.unsupported_large_object_modes.clone(),
issues: report.failure_modes.clone(),
}
}
}