bucketwarden-server 0.1.0

BucketWarden storage server runtime.
Documentation
mod common;

use bucketwarden_auth::OperatorRole;
use bucketwarden_lock::{ObjectLock, RetentionMode};
use bucketwarden_s3::{ObjectMetadata, PutObjectRequest};
use bucketwarden_server::{
    ConsoleApiAuditQuery, ConsoleApiListQuery, ConsoleApiLoginRequest, ConsoleApiPreferences,
    ConsoleApiRequest, RuntimeConfig, RuntimeError,
};
use common::*;
use serde_json::json;
use std::collections::BTreeMap;

fn seeded_console_runtime() -> (bucketwarden_server::BucketWarden, String) {
    let mut runtime = runtime();
    runtime
        .create_custom_identity("root", "shared-secret")
        .expect("custom identity");
    runtime
        .assign_operator_role("root", "root", OperatorRole::ClusterAdmin, "*")
        .expect("cluster admin");
    runtime
        .create_bucket("alice", "archive-001")
        .expect("bucket");
    runtime
        .put_bucket_object_lock_configuration("alice", "archive-001", None)
        .expect("object lock");
    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 session = runtime
        .console_api_login(ConsoleApiLoginRequest {
            principal_id: "root".to_string(),
            shared_secret: "shared-secret".to_string(),
        })
        .expect("login");
    (runtime, session.access_key_id)
}

#[test]
fn console_api_authenticates_sessions_and_expires_or_rejects_invalid_inputs() {
    let (mut runtime, access_key_id) = seeded_console_runtime();

    let current = runtime
        .console_api_current_user(&access_key_id)
        .expect("current user");
    assert_eq!(current.principal_id, "root");
    assert_eq!(current.scope, "*");

    let invalid = runtime.console_api_login(ConsoleApiLoginRequest {
        principal_id: "root".to_string(),
        shared_secret: "wrong-secret".to_string(),
    });
    assert!(matches!(invalid, Err(RuntimeError::Auth(_))));

    runtime.set_clock_epoch_seconds(current.expires_at_epoch_seconds + 1);
    assert!(matches!(
        runtime.console_api_current_user(&access_key_id),
        Err(RuntimeError::Auth(_))
    ));
}

#[test]
fn console_api_reports_buckets_objects_versions_governance_and_audit() {
    let (mut runtime, access_key_id) = seeded_console_runtime();

    let overview = runtime
        .console_api_overview(&access_key_id)
        .expect("overview");
    assert_eq!(overview.session.principal_id, "root");
    assert_eq!(overview.metrics.bucket_count, 1);
    assert_eq!(overview.metrics.version_count, 2);

    let health = runtime
        .console_api_health_report(&access_key_id)
        .expect("health");
    assert_eq!(health.bucket_count, 1);
    let config = runtime
        .console_api_configuration_report(&access_key_id)
        .expect("config");
    assert_eq!(
        config.bucket_regions.get("archive-001").map(String::as_str),
        Some("us-east-1")
    );

    let buckets = runtime
        .console_api_bucket_list(
            &access_key_id,
            ConsoleApiListQuery {
                q: Some("archive".to_string()),
                ..ConsoleApiListQuery::default()
            },
        )
        .expect("bucket list");
    assert_eq!(buckets.page.total, 1);
    assert_eq!(buckets.buckets[0].name, "archive-001");

    let detail = runtime
        .console_api_bucket_detail(&access_key_id, "archive-001")
        .expect("bucket detail");
    assert!(detail.bucket.has_object_lock);

    let objects = runtime
        .console_api_object_list(
            &access_key_id,
            "archive-001",
            ConsoleApiListQuery::default(),
        )
        .expect("object list");
    assert_eq!(objects.objects[0].key, "records/a.txt");
    assert_eq!(objects.objects[0].version_count, 2);

    let versions = runtime
        .console_api_object_version_history(
            &access_key_id,
            "archive-001",
            "records/a.txt",
            ConsoleApiListQuery::default(),
        )
        .expect("versions");
    assert_eq!(versions.page.total, 2);
    assert!(versions.versions[0].is_latest);

    let governance = runtime
        .console_api_object_governance_summary(&access_key_id, "archive-001", "records/a.txt", None)
        .expect("governance");
    assert!(governance.legal_hold);
    assert_eq!(governance.retention_mode.as_deref(), Some("Governance"));

    let audit = runtime
        .console_api_audit_events(
            &access_key_id,
            ConsoleApiAuditQuery {
                outcome: Some("Allowed".to_string()),
                ..ConsoleApiAuditQuery::default()
            },
        )
        .expect("audit");
    assert!(audit.page.total > 0);
    assert!(audit
        .events
        .iter()
        .all(|event| matches!(event.outcome, bucketwarden_audit::AuditOutcome::Allowed)));
}

#[test]
fn console_api_evidence_preferences_and_errors_are_runtime_backed() {
    let (mut runtime, access_key_id) = seeded_console_runtime();

    let evidence = runtime
        .console_api_evidence_list(&access_key_id, ConsoleApiListQuery::default())
        .expect("evidence list");
    assert!(evidence
        .evidence
        .iter()
        .any(|item| item.name == "snapshot_json" && item.bytes > 0));

    let export = runtime
        .console_api_evidence_export(&access_key_id)
        .expect("evidence export");
    assert_eq!(export.content_type, "application/json");
    assert!(export.report.snapshot_json.contains("\"buckets\""));

    let mut values = BTreeMap::new();
    values.insert("bucketwarden.ui.density".to_string(), "compact".to_string());
    let saved = runtime
        .console_api_preferences_write(&access_key_id, ConsoleApiPreferences { values })
        .expect("write preferences");
    assert_eq!(
        saved
            .values
            .get("bucketwarden.ui.density")
            .map(String::as_str),
        Some("compact")
    );

    let restored = bucketwarden_server::BucketWarden::restore(
        RuntimeConfig::development(),
        runtime.snapshot(),
    )
    .expect("restore");
    let mut restored = restored;
    let current = restored
        .console_api_preferences_read(&access_key_id)
        .expect("read preferences");
    assert_eq!(
        current
            .values
            .get("bucketwarden.ui.density")
            .map(String::as_str),
        Some("compact")
    );

    let error = restored
        .console_api_object_list(&access_key_id, "missing", ConsoleApiListQuery::default())
        .expect_err("missing bucket");
    let envelope = restored.console_api_error_envelope(&error, Some("req-1"));
    assert_eq!(envelope.status, 404);
    assert_eq!(envelope.code, "NoSuchBucket");
    assert!(!envelope.retryable);
}

#[test]
fn console_api_route_dispatch_covers_protected_and_unprotected_endpoints() {
    let (mut runtime, access_key_id) = seeded_console_runtime();

    let missing_session = runtime
        .handle_console_api(ConsoleApiRequest {
            method: "GET".to_string(),
            path: "/ui/api/overview".to_string(),
            ..ConsoleApiRequest::default()
        })
        .expect("error response");
    assert_eq!(missing_session.status, 401);
    assert_eq!(missing_session.body["code"], "AuthenticationFailed");

    let login = runtime
        .handle_console_api(ConsoleApiRequest {
            method: "POST".to_string(),
            path: "/ui/api/login".to_string(),
            body: Some(json!({
                "principal_id": "root",
                "shared_secret": "shared-secret"
            })),
            ..ConsoleApiRequest::default()
        })
        .expect("login route");
    assert_eq!(login.status, 200);
    assert_eq!(login.body["principal_id"], "root");

    let mut query = BTreeMap::new();
    query.insert("bucket".to_string(), "archive-001".to_string());
    query.insert("key".to_string(), "records/a.txt".to_string());
    let versions = runtime
        .handle_console_api(ConsoleApiRequest {
            method: "GET".to_string(),
            path: "/ui/api/object-versions".to_string(),
            access_key_id: Some(access_key_id.clone()),
            query,
            body: None,
        })
        .expect("versions route");
    assert_eq!(versions.status, 200);
    assert_eq!(versions.body["page"]["total"], 2);

    runtime
        .handle_console_api(ConsoleApiRequest {
            method: "POST".to_string(),
            path: "/ui/api/logout".to_string(),
            access_key_id: Some(access_key_id.clone()),
            ..ConsoleApiRequest::default()
        })
        .expect("logout route");
    let after_logout = runtime
        .handle_console_api(ConsoleApiRequest {
            method: "GET".to_string(),
            path: "/ui/api/current-user".to_string(),
            access_key_id: Some(access_key_id),
            ..ConsoleApiRequest::default()
        })
        .expect("revoked route");
    assert_eq!(after_logout.status, 401);
}