tandem-server 0.6.0

HTTP server for Tandem engine APIs
use base64::Engine;
use ed25519_dalek::{Signature, Verifier, VerifyingKey};
use std::collections::BTreeMap;
use tandem_types::VerifiedTenantContext;

use crate::AppState;

pub(crate) async fn enrich_verified_context_with_inbound_cross_tenant_grants(
    state: &AppState,
    verified: &mut VerifiedTenantContext,
) {
    if verified.strict_projection.is_none() || verified.tenant_context.is_local_implicit() {
        return;
    }

    let now = crate::now_ms();
    let records = state
        .enterprise
        .cross_tenant_grants
        .read()
        .await
        .values()
        .cloned()
        .collect::<Vec<_>>();
    let Some(strict_projection) = verified.strict_projection.as_mut() else {
        return;
    };
    for record in records {
        if cross_tenant_grant_signature_verifies(&record) {
            record.project_into_strict_context(strict_projection, now);
        }
    }
}

fn cross_tenant_grant_signature_verifies(record: &tandem_types::CrossTenantGrantRecord) -> bool {
    let Some(public_key) = cross_tenant_grant_verifying_key(&record.grant.header.kid) else {
        return false;
    };
    let Ok(encoded_header) = encode_json_base64url(&record.grant.header) else {
        return false;
    };
    let Ok(encoded_claims) = encode_json_base64url(&record.grant.claims) else {
        return false;
    };
    let Some(signature_bytes) = decode_bytes::<64>(&record.grant.signature) else {
        return false;
    };
    let Ok(verifying_key) = VerifyingKey::from_bytes(&public_key) else {
        return false;
    };
    let signature = Signature::from_bytes(&signature_bytes);
    let signing_input = format!("{encoded_header}.{encoded_claims}");
    verifying_key
        .verify(signing_input.as_bytes(), &signature)
        .is_ok()
}

fn cross_tenant_grant_verifying_key(kid: &str) -> Option<[u8; 32]> {
    let kid = kid.trim();
    if kid.is_empty() {
        return None;
    }
    if let Some(raw_keyring) = raw_cross_tenant_grant_public_keyring() {
        return parse_cross_tenant_grant_public_keyring(&raw_keyring)
            .and_then(|keyring| keyring.get(kid).copied());
    }
    let configured_kid = std::env::var("TANDEM_CROSS_TENANT_GRANT_SIGNING_KEY_ID")
        .ok()
        .map(|value| value.trim().to_string())
        .filter(|value| !value.is_empty())
        .unwrap_or_else(|| "cross-tenant-grant-local".to_string());
    if configured_kid != kid {
        return None;
    }
    let raw_key = std::env::var("TANDEM_CROSS_TENANT_GRANT_SIGNING_KEY")
        .ok()
        .or_else(|| {
            let path = std::env::var("TANDEM_CROSS_TENANT_GRANT_SIGNING_KEY_FILE").ok()?;
            std::fs::read_to_string(path).ok()
        })?;
    let key_bytes = decode_bytes::<32>(&raw_key)?;
    Some(
        ed25519_dalek::SigningKey::from_bytes(&key_bytes)
            .verifying_key()
            .to_bytes(),
    )
}

fn raw_cross_tenant_grant_public_keyring() -> Option<String> {
    std::env::var("TANDEM_CROSS_TENANT_GRANT_PUBLIC_KEYS")
        .ok()
        .or_else(|| {
            let path = std::env::var("TANDEM_CROSS_TENANT_GRANT_PUBLIC_KEYS_FILE").ok()?;
            std::fs::read_to_string(path).ok()
        })
}

fn parse_cross_tenant_grant_public_keyring(raw_keys: &str) -> Option<BTreeMap<String, [u8; 32]>> {
    let raw_keys = raw_keys.trim();
    if raw_keys.is_empty() {
        return None;
    }
    if raw_keys.starts_with('{') {
        let entries = serde_json::from_str::<BTreeMap<String, serde_json::Value>>(raw_keys).ok()?;
        let mut decoded = BTreeMap::new();
        for (kid, value) in entries {
            let raw_key = match value {
                serde_json::Value::String(value) => value,
                serde_json::Value::Object(mut object) => object
                    .remove("public_key")
                    .or_else(|| object.remove("publicKey"))?
                    .as_str()?
                    .to_string(),
                _ => return None,
            };
            decoded.insert(kid, decode_bytes::<32>(&raw_key)?);
        }
        return Some(decoded);
    }

    let mut decoded = BTreeMap::new();
    for entry in raw_keys.split([',', ';', '\n']) {
        let entry = entry.trim();
        if entry.is_empty() {
            continue;
        }
        let (kid, raw_key) = entry.split_once('=').or_else(|| entry.split_once(':'))?;
        decoded.insert(kid.trim().to_string(), decode_bytes::<32>(raw_key.trim())?);
    }
    if decoded.is_empty() {
        None
    } else {
        Some(decoded)
    }
}

fn encode_json_base64url<T: serde::Serialize>(value: &T) -> Result<String, serde_json::Error> {
    serde_json::to_vec(value)
        .map(|bytes| base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(bytes))
}

fn decode_bytes<const N: usize>(raw: &str) -> Option<[u8; N]> {
    let raw = raw.trim();
    let decoded = base64::engine::general_purpose::URL_SAFE_NO_PAD
        .decode(raw)
        .or_else(|_| base64::engine::general_purpose::URL_SAFE.decode(raw))
        .or_else(|_| base64::engine::general_purpose::STANDARD.decode(raw))
        .ok()
        .or_else(|| decode_hex(raw))?;
    decoded.as_slice().try_into().ok()
}

fn decode_hex(raw: &str) -> Option<Vec<u8>> {
    let raw = raw.trim();
    if raw.len() % 2 != 0 {
        return None;
    }
    (0..raw.len())
        .step_by(2)
        .map(|idx| u8::from_str_radix(&raw[idx..idx + 2], 16).ok())
        .collect()
}

#[cfg(test)]
mod tests {
    use super::*;
    use ed25519_dalek::Signer;
    use tandem_types::{
        AccessDecision, AccessPermission, AssertionMetadata, AuthorityChain, CrossTenantGrant,
        CrossTenantGrantClaims, CrossTenantGrantHeader, CrossTenantGrantParty,
        CrossTenantGrantRecord, DataBoundary, DataClass, HumanActor, PrincipalRef,
        RequestPrincipal, ResourceKind, ResourceRef, ResourceScope, StrictTenantContext,
        TenantContext, VerifiedTenantContext,
    };

    #[tokio::test]
    async fn enrich_projects_active_inbound_grant_into_strict_context() {
        let signing_key = ed25519_dalek::SigningKey::from_bytes(&[42u8; 32]);
        std::env::set_var(
            "TANDEM_CROSS_TENANT_GRANT_PUBLIC_KEYS",
            format!(
                "grant-key={}",
                base64::engine::general_purpose::URL_SAFE_NO_PAD
                    .encode(signing_key.verifying_key().to_bytes())
            ),
        );
        let state = AppState::new_starting("cross-tenant-grants-test".to_string(), true);
        let issuer =
            TenantContext::explicit_user_workspace("org-a", "workspace-a", None, "admin-a");
        let audience =
            TenantContext::explicit_user_workspace("org-b", "workspace-b", None, "user-b");
        let subject = PrincipalRef::human_user("user-b");
        let resource = ResourceRef::new(
            "org-a",
            "workspace-a",
            ResourceKind::DocumentCollection,
            "finance-drive",
        );
        let claims = CrossTenantGrantClaims::new_v1(
            "grant-finance",
            CrossTenantGrantParty::from_tenant_context(&issuer),
            CrossTenantGrantParty::from_tenant_context(&audience),
            subject.clone(),
            ResourceScope::root(resource.clone()),
            vec![AccessPermission::Read],
            vec![DataClass::FinancialRecord],
            1,
            u64::MAX,
            PrincipalRef::human_user("admin-a"),
        );
        let header = CrossTenantGrantHeader::ed25519("grant-key");
        let signature = sign_test_grant(&header, &claims, &signing_key);
        state.enterprise.cross_tenant_grants.write().await.insert(
            "org-a::workspace-a::local::grant-finance".to_string(),
            CrossTenantGrantRecord::active(CrossTenantGrant::new(header, claims, signature), 1),
        );

        let request_principal = RequestPrincipal::authenticated_user("user-b", "test");
        let strict_context = StrictTenantContext::new(
            audience.clone(),
            subject,
            AuthorityChain::from_request(request_principal.clone()),
            ResourceScope::root(ResourceRef::new(
                "org-b",
                "workspace-b",
                ResourceKind::Workspace,
                "workspace-b",
            )),
            AssertionMetadata::new("issuer", "runtime", 1, u64::MAX, "assertion-b"),
        )
        .with_data_boundary(DataBoundary::allow(vec![DataClass::FinancialRecord]));
        let mut verified = VerifiedTenantContext {
            tenant_context: audience,
            human_actor: HumanActor::tandem_user("user-b"),
            authority_chain: AuthorityChain::from_request(request_principal),
            roles: Vec::new(),
            org_units: Vec::new(),
            capabilities: Vec::new(),
            policy_version: None,
            strict_projection: Some(strict_context),
            issuer: "issuer".to_string(),
            audience: "runtime".to_string(),
            issued_at_ms: 1,
            expires_at_ms: u64::MAX,
            assertion_id: "assertion-b".to_string(),
            assertion_key_id: None,
        };

        enrich_verified_context_with_inbound_cross_tenant_grants(&state, &mut verified).await;

        let strict = verified.strict_projection.expect("strict projection");
        let decision = strict.evaluate_access(
            &resource,
            AccessPermission::Read,
            DataClass::FinancialRecord,
            crate::now_ms(),
        );
        assert_eq!(decision.decision, AccessDecision::Allow);
        assert_eq!(decision.grant_id.as_deref(), Some("grant-finance"));
        std::env::remove_var("TANDEM_CROSS_TENANT_GRANT_PUBLIC_KEYS");
    }

    fn sign_test_grant(
        header: &CrossTenantGrantHeader,
        claims: &CrossTenantGrantClaims,
        signing_key: &ed25519_dalek::SigningKey,
    ) -> String {
        let encoded_header = encode_json_base64url(header).expect("header");
        let encoded_claims = encode_json_base64url(claims).expect("claims");
        let signing_input = format!("{encoded_header}.{encoded_claims}");
        base64::engine::general_purpose::URL_SAFE_NO_PAD
            .encode(signing_key.sign(signing_input.as_bytes()).to_bytes())
    }
}