bucketwarden-server 0.1.0

BucketWarden storage server runtime.
Documentation
mod common;

use bucketwarden_auth::OperatorRole;
use bucketwarden_lock::{ObjectLock, RetentionMode};
use bucketwarden_s3::{
    BucketPolicyRequest, BucketQuotaConfiguration, LifecycleRule, ObjectMetadata, PutObjectRequest,
};
use bucketwarden_server::RuntimeError;
use common::*;

#[test]
fn ops_console_report_covers_all_console_feature_surfaces() {
    let mut runtime = runtime();
    runtime.create_local_user("root");
    runtime
        .assign_operator_role("root", "root", OperatorRole::ClusterAdmin, "*")
        .expect("cluster admin");
    runtime.create_tenant("tenant-a");
    runtime
        .create_local_user_in_tenant("alice", "tenant-a")
        .expect("tenant user");
    runtime
        .create_service_account_in_tenant("svc-a", "tenant-a")
        .expect("service account");
    runtime.allow("alice", "s3:*", "*");
    runtime
        .create_bucket("alice", "archive-001")
        .expect("bucket");
    runtime
        .put_bucket_policy(
            "alice",
            BucketPolicyRequest {
                bucket: "archive-001".to_string(),
                policy_json:
                    r#"{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Principal":"*","Action":"s3:GetObject","Resource":"archive-001/public/*"}]}"#
                        .to_string(),
            },
        )
        .expect("policy");
    runtime
        .put_bucket_lifecycle(
            "alice",
            "archive-001",
            vec![LifecycleRule {
                id: "expire".to_string(),
                prefix: Some("records/".to_string()),
                status: "Enabled".to_string(),
                expiration_days: Some(90),
                ..LifecycleRule::default()
            }],
        )
        .expect("lifecycle");
    runtime
        .put_bucket_object_lock_configuration("alice", "archive-001", None)
        .expect("object lock");
    runtime
        .put_bucket_quota(
            "root",
            "archive-001",
            BucketQuotaConfiguration {
                max_objects: None,
                max_requests: Some(1),
            },
        )
        .expect("bucket quota");
    runtime
        .put_object(
            "alice",
            PutObjectRequest {
                bucket: "archive-001".to_string(),
                key: "records/a.txt".to_string(),
                body: b"payload-v1".to_vec(),
                metadata: ObjectMetadata::default(),
            },
            ObjectLock::none(),
        )
        .expect("put v1");
    runtime
        .put_object(
            "alice",
            PutObjectRequest {
                bucket: "archive-001".to_string(),
                key: "records/a.txt".to_string(),
                body: b"payload-v2".to_vec(),
                metadata: ObjectMetadata::default(),
            },
            ObjectLock {
                legal_hold: true,
                retention_mode: Some(RetentionMode::Governance),
                retain_until_epoch_seconds: Some(100),
            },
        )
        .expect("put v2");

    let report = runtime
        .ops_console_report("root", None)
        .expect("console report");

    assert_eq!(report.scope, "runtime");
    assert_eq!(report.buckets.len(), 1);
    assert_eq!(report.buckets[0].name, "archive-001");
    assert!(report.buckets[0].has_policy);
    assert!(report.buckets[0].has_lifecycle);
    assert!(report.buckets[0].has_object_lock);
    assert_eq!(report.objects.len(), 1);
    assert_eq!(report.objects[0].version_count, 2);
    assert!(report.objects[0].legal_hold);
    assert_eq!(
        report.objects[0].retention_mode.as_deref(),
        Some("Governance")
    );
    assert!(report
        .users
        .iter()
        .any(|user| user.principal_id == "alice" && user.kind == "LocalUser"));
    assert!(report
        .users
        .iter()
        .any(|user| user.principal_id == "svc-a" && user.kind == "ServiceAccount"));
    assert_eq!(report.policies.len(), 1);
    assert_eq!(report.policies[0].statement_count, 1);
    assert_eq!(report.metrics.bucket_count, 1);
    assert_eq!(report.metrics.object_count, 1);
    assert_eq!(report.metrics.version_count, 2);
    assert!(report.audit.audit_event_count >= 4);
    assert_eq!(report.retention_bucket_count, 1);
    assert_eq!(report.retained_version_count, 1);
    assert_eq!(report.lifecycle_bucket_count, 1);
    assert_eq!(report.lifecycle_rule_count, 1);
    assert_eq!(report.native_support.len(), 16);
    assert_eq!(report.semantic_parity.len(), 16);
    assert!(report
        .native_support
        .iter()
        .all(|feature| feature.native_support_state == "implemented"));
    assert!(report
        .observability_evidence
        .contains(&"ops:GetConsoleReport".to_string()));
    assert!(report
        .validation_tests
        .contains(&"crates/bucketwarden-server/tests/ops_console.rs".to_string()));
    assert!(runtime
        .audit_events()
        .iter()
        .any(|event| event.action == "ops:GetConsoleReport"));
}

#[test]
fn ops_console_report_supports_bucket_scope_and_fail_closed_authorization() {
    let mut runtime = runtime();
    runtime
        .create_bucket("alice", "archive-001")
        .expect("bucket");
    runtime.create_bucket("alice", "logs-001").expect("bucket");
    runtime.create_local_user("ops");
    runtime
        .assign_operator_role(
            "alice",
            "ops",
            OperatorRole::ReadOnlyOperator,
            "archive-001",
        )
        .expect("read only");
    runtime
        .put_object(
            "alice",
            PutObjectRequest {
                bucket: "archive-001".to_string(),
                key: "records/a.txt".to_string(),
                body: b"payload".to_vec(),
                metadata: ObjectMetadata::default(),
            },
            ObjectLock::none(),
        )
        .expect("put archive");
    runtime
        .put_object(
            "alice",
            PutObjectRequest {
                bucket: "logs-001".to_string(),
                key: "logs/a.txt".to_string(),
                body: b"payload".to_vec(),
                metadata: ObjectMetadata::default(),
            },
            ObjectLock::none(),
        )
        .expect("put logs");

    let report = runtime
        .ops_console_report("ops", Some("archive-001"))
        .expect("bucket console report");

    assert_eq!(report.scope, "bucket");
    assert_eq!(report.target, "archive-001");
    assert_eq!(report.buckets.len(), 1);
    assert_eq!(report.buckets[0].name, "archive-001");
    assert_eq!(report.objects.len(), 1);
    assert_eq!(report.objects[0].bucket, "archive-001");
    assert!(report.users.is_empty());

    assert!(runtime.ops_console_report("ops", Some("logs-001")).is_err());
}

#[test]
fn ops_console_report_rejects_invalid_principals_permissions_and_bucket_inputs() {
    let mut runtime = runtime();
    runtime
        .create_bucket("alice", "archive-001")
        .expect("bucket");
    runtime.create_local_user("diag-only");
    runtime
        .assign_operator_role(
            "alice",
            "diag-only",
            OperatorRole::BucketAdmin,
            "archive-001",
        )
        .expect("bucket admin");
    runtime.create_local_user("root");
    runtime
        .assign_operator_role("root", "root", OperatorRole::ClusterAdmin, "*")
        .expect("cluster admin");

    assert!(matches!(
        runtime.ops_console_report("missing-principal", Some("archive-001")),
        Err(RuntimeError::Auth(_))
    ));
    assert!(matches!(
        runtime.ops_console_report("diag-only", Some("archive-001")),
        Err(RuntimeError::OperatorActionDenied { principal, .. }) if principal == "diag-only"
    ));
    assert!(matches!(
        runtime.ops_console_report("root", Some("missing-bucket")),
        Err(RuntimeError::NoSuchBucket(bucket)) if bucket == "missing-bucket"
    ));
}

#[test]
fn ops_console_report_handles_empty_valid_runtime_without_panics() {
    let mut runtime = runtime();
    runtime.create_local_user("root");
    runtime
        .assign_operator_role("root", "root", OperatorRole::ClusterAdmin, "*")
        .expect("cluster admin");

    let report = runtime
        .ops_console_report("root", None)
        .expect("empty runtime console report");

    assert_eq!(report.scope, "runtime");
    assert!(report.buckets.is_empty());
    assert!(report.objects.is_empty());
    assert_eq!(report.metrics.bucket_count, 0);
    assert_eq!(report.metrics.object_count, 0);
    assert_eq!(report.metrics.version_count, 0);
    assert!(report
        .failure_modes
        .contains(&"runtime has no buckets".to_string()));
    assert_eq!(report.native_support.len(), 16);
    assert_eq!(report.semantic_parity.len(), 16);
}

#[test]
fn ops_console_report_aggregates_worst_case_versions_delete_markers_and_findings() {
    let mut runtime = runtime();
    runtime.create_local_user("root");
    runtime
        .assign_operator_role("root", "root", OperatorRole::ClusterAdmin, "*")
        .expect("cluster admin");

    for bucket_index in 0..3 {
        let bucket = format!("archive-{bucket_index:03}");
        runtime.create_bucket("alice", &bucket).expect("bucket");
        if bucket_index == 0 {
            runtime
                .put_bucket_policy(
                    "alice",
                    BucketPolicyRequest {
                        bucket: bucket.clone(),
                        policy_json:
                            r#"{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Principal":"*","Action":"s3:GetObject","Resource":"archive-000/*"},{"Effect":"Deny","Principal":"*","Action":"s3:DeleteObject","Resource":"archive-000/protected/*"}]}"#
                                .to_string(),
                    },
                )
                .expect("policy");
        }
        if bucket_index == 1 {
            runtime
                .put_bucket_lifecycle(
                    "alice",
                    &bucket,
                    vec![LifecycleRule {
                        id: "expire-noncurrent".to_string(),
                        prefix: Some("records/".to_string()),
                        status: "Enabled".to_string(),
                        noncurrent_expiration_days: Some(30),
                        ..LifecycleRule::default()
                    }],
                )
                .expect("lifecycle");
        }
        if bucket_index == 2 {
            runtime
                .put_bucket_object_lock_configuration("alice", &bucket, None)
                .expect("object lock");
        }

        for key_index in 0..4 {
            let key = format!("records/{key_index:03}.txt");
            for version_index in 0..3 {
                runtime
                    .put_object(
                        "alice",
                        PutObjectRequest {
                            bucket: bucket.clone(),
                            key: key.clone(),
                            body: format!("{bucket}:{key}:{version_index}").into_bytes(),
                            metadata: ObjectMetadata::default(),
                        },
                        ObjectLock::none(),
                    )
                    .expect("put object version");
            }
            if key_index % 2 == 0 {
                runtime
                    .delete_object("alice", &bucket, &key, false)
                    .expect("delete marker");
            }
        }
    }

    let report = runtime
        .ops_console_report("root", None)
        .expect("worst-case console report");

    assert_eq!(report.buckets.len(), 3);
    assert_eq!(report.objects.len(), 12);
    assert_eq!(report.metrics.bucket_count, 3);
    assert_eq!(report.metrics.version_count, 42);
    assert_eq!(report.metrics.delete_marker_count, 6);
    assert_eq!(
        report
            .buckets
            .iter()
            .map(|bucket| bucket.delete_marker_count)
            .sum::<usize>(),
        6
    );
    assert_eq!(report.policies.len(), 1);
    assert_eq!(report.policies[0].statement_count, 2);
    assert_eq!(report.lifecycle_bucket_count, 1);
    assert_eq!(report.lifecycle_rule_count, 1);
    assert_eq!(report.retention_bucket_count, 1);
    assert!(report.security_governance_findings.iter().any(|finding| {
        finding == "bucket:archive-001:policy:not-configured"
            || finding == "bucket:archive-002:policy:not-configured"
    }));
    assert!(!report.product_caveats.is_empty());
    assert!(report
        .product_caveats
        .iter()
        .any(|caveat| caveat.contains("object")));
}