sdk-rust 0.1.0

Canonical Rust core for the Lattix metadata-only control-plane SDK
Documentation
use mockito::{Matcher, Server};
use sdk_rust::{
    ArtifactProfile, Client, ProtectionOperation, ResourceDescriptor, SdkProtectionPlanRequest,
    WorkloadDescriptor,
};

#[test]
fn builder_rejects_empty_base_url() {
    let error = Client::builder("   ")
        .build()
        .err()
        .expect("builder should fail");
    assert!(matches!(error, sdk_rust::SdkError::InvalidInput(_)));
}

#[test]
fn capabilities_sends_auth_and_identity_headers() {
    let mut server = Server::new();
    let _mock = server
        .mock("GET", "/v1/sdk/capabilities")
        .match_header("authorization", "Bearer test-token")
        .match_header("x-lattix-tenant-id", "tenant-a")
        .match_header("x-lattix-user-id", "user-a")
        .with_status(200)
        .with_body(
            r#"{
                "service":"lattix-platform-api",
                "status":"ready",
                "auth_mode":"bearer_token",
                "caller":{
                    "tenant_id":"tenant-a",
                    "principal_id":"user-a",
                    "subject":"user-a",
                    "auth_source":"bearer_token",
                    "scopes":["platform-api.access"]
                },
                "default_required_scopes":["platform-api.access"],
                "routes":[
                    {
                        "route":"/v1/sdk/protection-plan",
                        "domain":"policy",
                        "configured":true,
                        "required_scopes":["policy.read"]
                    }
                ]
            }"#,
        )
        .create();

    let client = Client::builder(server.url())
        .with_bearer_token("test-token")
        .with_tenant_id("tenant-a")
        .with_user_id("user-a")
        .build()
        .expect("client");

    let response = client.capabilities().expect("capabilities response");
    assert_eq!(response.service, "lattix-platform-api");
    assert_eq!(response.caller.tenant_id, "tenant-a");
    assert_eq!(response.routes.len(), 1);
}

#[test]
fn sdk_client_credentials_exchange_short_lived_session_and_cache_it() {
    let mut server = Server::new();
    let _session_mock = server
        .mock("POST", "/v1/sdk/session")
        .match_header("content-type", Matcher::Regex("application/json.*".into()))
        .match_body(Matcher::AllOf(vec![
            Matcher::Regex("\"tenant_id\":\"tenant-a\"".into()),
            Matcher::Regex("\"client_id\":\"sdk-client\"".into()),
            Matcher::Regex(r#""requested_scopes":\["platform-api.access"\]"#.into()),
        ]))
        .with_status(200)
        .with_body(
            r#"{
                "access_token":"sdk-session-token",
                "token_type":"Bearer",
                "expires_in":900,
                "scope":"platform-api.access",
                "tenant_id":"tenant-a",
                "client_id":"sdk-client",
                "subject":"sdk:sdk-client"
            }"#,
        )
        .expect(1)
        .create();
    let _capabilities_mock = server
        .mock("GET", "/v1/sdk/capabilities")
        .match_header("authorization", "Bearer sdk-session-token")
        .with_status(200)
        .with_body(
            r#"{
                "service":"lattix-platform-api",
                "status":"ready",
                "auth_mode":"bearer_token",
                "caller":{
                    "tenant_id":"tenant-a",
                    "principal_id":"sdk-client",
                    "subject":"sdk:sdk-client",
                    "auth_source":"sdk_client_credentials",
                    "scopes":["platform-api.access"]
                },
                "default_required_scopes":["platform-api.access"],
                "routes":[]
            }"#,
        )
        .expect(2)
        .create();

    let client = Client::builder(server.url())
        .with_tenant_id("tenant-a")
        .with_client_id("sdk-client")
        .with_client_secret("super-secret")
        .with_requested_scopes(["platform-api.access"])
        .build()
        .expect("client");

    let first = client.capabilities().expect("capabilities response");
    let second = client.capabilities().expect("capabilities response");

    assert_eq!(first.caller.principal_id, "sdk-client");
    assert_eq!(second.caller.subject, "sdk:sdk-client");
}

#[test]
fn bootstrap_returns_typed_response() {
    let mut server = Server::new();
    let _mock = server
        .mock("GET", "/v1/sdk/bootstrap")
        .with_status(200)
        .with_body(
            r#"{
                "service":"lattix-platform-api",
                "status":"ready",
                "auth_mode":"bearer_token",
                "caller":{
                    "tenant_id":"tenant-a",
                    "principal_id":"user-a",
                    "subject":"user-a",
                    "auth_source":"bearer_token",
                    "scopes":["platform-api.access"]
                },
                "enforcement_model":"embedded_local_library",
                "plaintext_to_platform":false,
                "policy_resolution_mode":"metadata_only_control_plane",
                "supported_operations":["protect","access","rewrap"],
                "supported_artifact_profiles":["tdf","envelope"],
                "platform_domains":[
                    {
                        "domain":"policy",
                        "configured":true,
                        "reason":"metadata-only"
                    }
                ]
            }"#,
        )
        .create();

    let client = Client::builder(server.url()).build().expect("client");
    let response = client.bootstrap().expect("bootstrap response");

    assert_eq!(response.enforcement_model, "embedded_local_library");
    assert!(!response.plaintext_to_platform);
    assert_eq!(response.supported_operations.len(), 3);
}

#[test]
fn protection_plan_posts_metadata_only_payload() {
    let mut server = Server::new();
    let _mock = server
        .mock("POST", "/v1/sdk/protection-plan")
        .match_header("content-type", Matcher::Regex("application/json.*".into()))
        .match_body(Matcher::AllOf(vec![
            Matcher::Regex("\"operation\":\"protect\"".into()),
            Matcher::Regex("\"content_digest\":\"sha256:abc123\"".into()),
            Matcher::Regex("\"application\":\"example-app\"".into()),
        ]))
        .with_status(200)
        .with_body(
            r#"{
                "service":"lattix-platform-api",
                "status":"ready",
                "caller":{
                    "tenant_id":"tenant-a",
                    "principal_id":"user-a",
                    "subject":"user-a",
                    "auth_source":"bearer_token",
                    "scopes":["platform-api.access"]
                },
                "request_summary":{
                    "operation":"protect",
                    "workload_application":"example-app",
                    "workload_environment":"dev",
                    "workload_component":"worker",
                    "resource_kind":"document",
                    "resource_id":"doc-123",
                    "mime_type":"application/pdf",
                    "preferred_artifact_profile":"tdf",
                    "content_digest_present":true,
                    "content_size_bytes":1024,
                    "label_count":1,
                    "attribute_count":1,
                    "purpose":"store"
                },
                "decision":{
                    "allow":true,
                    "required_scopes":["platform-api.access"],
                    "handling_mode":"local_embedded_enforcement",
                    "plaintext_transport":"forbidden_by_default"
                },
                "execution":{
                    "protect_locally":true,
                    "local_enforcement_library":"sdk_embedded_library_or_local_sidecar",
                    "send_plaintext_to_platform":false,
                    "send_only":["policy metadata","content digest"],
                    "artifact_profile":"tdf",
                    "key_strategy":"encrypt_locally_then_wrap_or_authorize_key_release",
                    "policy_resolution":"platform_api_resolves_policy_from_metadata_only"
                },
                "platform_domains":[
                    {
                        "domain":"policy",
                        "configured":true,
                        "reason":"metadata-only"
                    }
                ],
                "warnings":[]
            }"#,
        )
        .create();

    let client = Client::builder(server.url()).build().expect("client");
    let response = client
        .protection_plan(&SdkProtectionPlanRequest {
            operation: ProtectionOperation::Protect,
            workload: WorkloadDescriptor {
                application: "example-app".to_string(),
                environment: Some("dev".to_string()),
                component: Some("worker".to_string()),
            },
            resource: ResourceDescriptor {
                kind: "document".to_string(),
                id: Some("doc-123".to_string()),
                mime_type: Some("application/pdf".to_string()),
            },
            preferred_artifact_profile: Some(ArtifactProfile::Tdf),
            content_digest: Some("sha256:abc123".to_string()),
            content_size_bytes: Some(1024),
            purpose: Some("store".to_string()),
            labels: vec!["confidential".to_string()],
            attributes: std::collections::BTreeMap::from([(
                "region".to_string(),
                "us".to_string(),
            )]),
        })
        .expect("protection plan");

    assert!(response.execution.protect_locally);
    assert!(!response.execution.send_plaintext_to_platform);
    assert_eq!(response.request_summary.resource_kind, "document");
}