use super::*;
use crate::ops_health_diagnostics::{ops_scope_and_target, scoped_buckets};
impl BucketWarden {
pub fn ops_console_report(
&mut self,
principal: &str,
bucket: Option<&str>,
) -> Result<ConsoleRuntimeReport, RuntimeError> {
let (scope, target) = ops_scope_and_target(bucket);
self.require_operator_action(
principal,
OperatorAction::ReadDiagnostics,
&target,
"ops:GetConsoleReport",
)?;
self.require_operator_action(
principal,
OperatorAction::ReadAudit,
&target,
"ops:GetConsoleReport",
)?;
self.require_ops_bucket_scope(bucket)?;
let config = self.ops_config_report(principal, bucket)?;
let admin_surface = self.ops_admin_surface_report(principal, bucket)?;
let audit = self.ops_evidence_export_report(principal, bucket)?;
let health = self.ops_health_report(principal, bucket)?;
let bucket_rows = console_bucket_rows(self, bucket);
let object_rows = console_object_rows(self, bucket);
let user_rows = if bucket.is_none() {
self.auth
.principal_console_rows()
.into_iter()
.map(
|(principal_id, tenant_id, kind, enabled, operator_role_count)| {
ConsoleUserRow {
principal_id,
tenant_id,
kind,
enabled,
operator_role_count,
}
},
)
.collect()
} else {
Vec::new()
};
let policy_rows = console_policy_rows(self, bucket);
let metrics = console_metric_summary(
&bucket_rows,
audit.audit_event_count,
audit.notification_event_count,
audit.replication_record_count,
health.multipart_upload_count,
);
let retained_version_count = object_rows
.iter()
.filter(|object| object.retain_until_epoch_seconds.is_some())
.count();
let retention_bucket_count = bucket_rows
.iter()
.filter(|bucket| bucket.has_object_lock)
.count();
let lifecycle_bucket_count = bucket_rows
.iter()
.filter(|bucket| bucket.has_lifecycle)
.count();
let lifecycle_rule_count = scoped_buckets(self, bucket)
.iter()
.map(|(_, bucket_state)| bucket_state.lifecycle_rules.len())
.sum();
let support = console_support_features();
let product_caveats = console_product_caveats(&config, &admin_surface);
let report = ConsoleRuntimeReport {
scope: scope.to_string(),
target: target.clone(),
generated_at_epoch_seconds: self.clock_epoch_seconds,
buckets: bucket_rows,
objects: object_rows,
users: user_rows,
policies: policy_rows,
metrics,
audit,
retention_bucket_count,
retained_version_count,
lifecycle_bucket_count,
lifecycle_rule_count,
native_support: support.clone(),
semantic_parity: support,
config,
admin_surface,
security_governance_findings: console_security_governance_findings(self, bucket),
observability_evidence: vec![
"ops:GetHealthReport".to_string(),
"ops:GetConfigReport".to_string(),
"ops:GetAdminSurfaceReport".to_string(),
"ops:GetEvidenceExportReport".to_string(),
"ops:GetConsoleReport".to_string(),
],
failure_modes: health.issues,
validation_tests: vec![
"crates/bucketwarden-server/tests/ops_console.rs".to_string(),
"tests/ssot/verify_future_console_ui_boundary.py".to_string(),
],
product_caveats,
};
self.audit.append(
principal,
"ops:GetConsoleReport",
&target,
AuditOutcome::Allowed,
Some(format!(
"buckets={},objects={},users={},policies={}",
report.buckets.len(),
report.objects.len(),
report.users.len(),
report.policies.len()
)),
);
Ok(report)
}
}
fn console_bucket_rows(runtime: &BucketWarden, bucket: Option<&str>) -> Vec<ConsoleBucketRow> {
scoped_buckets(runtime, bucket)
.into_iter()
.map(|(bucket_name, bucket_state)| {
let mut object_count = 0usize;
let mut version_count = 0usize;
let mut delete_marker_count = 0usize;
let mut total_bytes = 0usize;
for object in bucket_state.objects.values() {
if object.has_current_version() {
object_count += 1;
}
for version in &object.versions {
version_count += 1;
if version.delete_marker {
delete_marker_count += 1;
}
total_bytes += version.content_length();
}
}
ConsoleBucketRow {
name: bucket_name.clone(),
tenant_id: bucket_state.tenant_id.clone(),
owner: bucket_state.owner.clone(),
region: bucket_state.region.clone(),
versioning_status: format!("{:?}", bucket_state.versioning),
object_count,
version_count,
delete_marker_count,
total_bytes,
request_count: bucket_state.request_count,
has_policy: bucket_state.policy.is_some(),
has_lifecycle: !bucket_state.lifecycle_rules.is_empty(),
has_object_lock: bucket_state.object_lock.enabled,
has_encryption: bucket_state.encryption.is_some(),
has_replication: !bucket_state.replication.rules.is_empty(),
has_metrics: !bucket_state.metrics_configurations.is_empty(),
}
})
.collect()
}
fn console_object_rows(runtime: &BucketWarden, bucket: Option<&str>) -> Vec<ConsoleObjectRow> {
let mut rows = Vec::new();
for (bucket_name, bucket_state) in scoped_buckets(runtime, bucket) {
for (key, object) in &bucket_state.objects {
let current = object.current_version();
rows.push(ConsoleObjectRow {
bucket: bucket_name.clone(),
key: key.clone(),
current_version_id: current.map(|version| version.version_id.clone()),
version_count: object.versions.len(),
delete_marker_count: object
.versions
.iter()
.filter(|version| version.delete_marker)
.count(),
total_bytes: object
.versions
.iter()
.map(StoredVersion::content_length)
.sum(),
latest_modified_epoch_seconds: object
.versions
.iter()
.map(|version| version.last_modified_epoch_seconds)
.max(),
owner: current
.map(|version| stored_owner(bucket_state, version))
.unwrap_or_else(|| bucket_state.owner.clone()),
legal_hold: current
.map(|version| version.lock.legal_hold)
.unwrap_or_default(),
retention_mode: current.and_then(|version| {
version.lock.retention_mode.map(|mode| format!("{mode:?}"))
}),
retain_until_epoch_seconds: current
.and_then(|version| version.lock.retain_until_epoch_seconds),
encryption: current.and_then(|version| version.metadata.encryption.clone()),
replication_status: current.and_then(|version| version.replication_status.clone()),
});
}
}
rows.sort_by(|left, right| {
left.bucket
.cmp(&right.bucket)
.then(left.key.cmp(&right.key))
});
rows
}
fn console_policy_rows(runtime: &BucketWarden, bucket: Option<&str>) -> Vec<ConsolePolicyRow> {
scoped_buckets(runtime, bucket)
.into_iter()
.filter_map(|(bucket_name, bucket_state)| {
bucket_state.policy.as_ref().map(|policy| ConsolePolicyRow {
scope: "bucket".to_string(),
target: bucket_name.clone(),
statement_count: bucket_policy_statement_count(&policy.json),
byte_length: policy.json.len(),
})
})
.collect()
}
fn bucket_policy_statement_count(policy_json: &str) -> usize {
let Ok(value) = serde_json::from_str::<serde_json::Value>(policy_json) else {
return 0;
};
match value.get("Statement") {
Some(serde_json::Value::Array(statements)) => statements.len(),
Some(_) => 1,
None => 0,
}
}
fn console_metric_summary(
buckets: &[ConsoleBucketRow],
audit_event_count: usize,
notification_event_count: usize,
replication_record_count: usize,
multipart_upload_count: usize,
) -> ConsoleMetricSummary {
ConsoleMetricSummary {
bucket_count: buckets.len(),
object_count: buckets.iter().map(|bucket| bucket.object_count).sum(),
version_count: buckets.iter().map(|bucket| bucket.version_count).sum(),
delete_marker_count: buckets
.iter()
.map(|bucket| bucket.delete_marker_count)
.sum(),
total_bytes: buckets.iter().map(|bucket| bucket.total_bytes).sum(),
request_count: buckets.iter().map(|bucket| bucket.request_count).sum(),
audit_event_count,
notification_event_count,
replication_record_count,
multipart_upload_count,
}
}
fn console_security_governance_findings(
runtime: &BucketWarden,
bucket: Option<&str>,
) -> Vec<String> {
let mut findings = Vec::new();
for (bucket_name, bucket_state) in scoped_buckets(runtime, bucket) {
if bucket_state.policy.is_none() {
findings.push(format!("bucket:{bucket_name}:policy:not-configured"));
}
if bucket_state.encryption.is_none() {
findings.push(format!("bucket:{bucket_name}:encryption:not-configured"));
}
if !bucket_state.object_lock.enabled {
findings.push(format!("bucket:{bucket_name}:object-lock:not-enabled"));
}
if bucket_state.public_access_block.is_none() {
findings.push(format!(
"bucket:{bucket_name}:public-access-block:not-configured"
));
}
}
findings.sort();
findings
}
fn console_support_features() -> Vec<ConsoleSupportFeature> {
[
("001", "Buckets", "console.bucket.index"),
("002", "Objects", "console.object.index"),
("003", "Users", "console.identity.index"),
("004", "Policies", "console.policy.index"),
("005", "Metrics", "console.metrics.summary"),
("006", "Audit", "console.audit.export"),
("007", "Retention", "console.retention.summary"),
("008", "Lifecycle", "console.lifecycle.summary"),
(
"009",
"Native support state",
"console.native-support.summary",
),
("010", "Semantic parity", "console.semantic-parity.summary"),
(
"011",
"Configuration and admin surface",
"console.config-admin.summary",
),
(
"012",
"Security and governance impact",
"console.security-governance.findings",
),
(
"013",
"Observability and evidence",
"console.observability-evidence.summary",
),
(
"014",
"Failure-mode behavior",
"console.failure-modes.summary",
),
(
"015",
"Validation test coverage",
"console.validation-tests.summary",
),
(
"016",
"Product-specific caveats",
"console.product-caveats.summary",
),
]
.into_iter()
.map(|(number, title, runtime_surface)| ConsoleSupportFeature {
feature_id: format!("feat:bucketwarden.req.ops-j08-{number}"),
title: format!("OPS-J08-{number} - {title}"),
runtime_surface: runtime_surface.to_string(),
native_support_state: "implemented".to_string(),
semantic_parity_state: "runtime-console-api".to_string(),
})
.collect()
}
fn console_product_caveats(
config: &RuntimeConfigReport,
admin_surface: &OpsAdminSurfaceReport,
) -> Vec<String> {
let mut caveats = Vec::new();
caveats.extend(config.storage_backend_caveats.iter().cloned());
caveats.extend(config.replication_strategy_caveats.iter().cloned());
caveats.extend(config.erasure_coding_caveats.iter().cloned());
caveats.extend(config.placement_domain_caveats.iter().cloned());
caveats.extend(config.consistency_model_caveats.iter().cloned());
caveats.extend(config.metadata_architecture_caveats.iter().cloned());
caveats.extend(config.object_layout_caveats.iter().cloned());
caveats.extend(config.small_object_optimization_caveats.iter().cloned());
caveats.extend(config.large_object_optimization_caveats.iter().cloned());
caveats.extend(admin_surface.storage_backend_caveats.iter().cloned());
caveats.sort();
caveats.dedup();
caveats
}