cufflink-cli 0.16.1

CLI for the Cufflink CRUD microservice platform — deploy, init, and manage services
use crate::config::CliConfig;
use serde_json::Value;
use std::process::Command;

/// One custom route's audited posture.
pub struct RouteAudit {
    pub method: String,
    pub path: String,
    pub handler: String,
    pub label: String,
    pub anonymous: bool,
}

/// Classify a route's declared posture from its manifest JSON. A missing posture
/// is `undeclared` and counts as anonymous-reachable — on an enforcing platform
/// it runs in legacy (unenforced) mode.
pub fn classify_route(route: &Value) -> RouteAudit {
    let str_at = |key: &str| route[key].as_str().unwrap_or("?").to_string();
    let (label, anonymous) = match route.get("posture") {
        None | Some(Value::Null) => ("undeclared".to_string(), true),
        Some(p) => match p["kind"].as_str() {
            Some("public") => (
                format!("public ({})", p["reason"].as_str().unwrap_or("")),
                true,
            ),
            Some("authenticated") => ("authenticated".to_string(), false),
            Some("permission") => (
                format!(
                    "permission {}:{}",
                    p["area"].as_str().unwrap_or("?"),
                    p["operation"].as_str().unwrap_or("?")
                ),
                false,
            ),
            Some("owner") => (
                format!("owner({})", p["field"].as_str().unwrap_or("?")),
                false,
            ),
            _ => ("unknown".to_string(), true),
        },
    };
    RouteAudit {
        method: str_at("method"),
        path: str_at("path"),
        handler: str_at("handler"),
        label,
        anonymous,
    }
}

/// Audit every custom route in a manifest.
pub fn audit_manifest(manifest: &Value) -> Vec<RouteAudit> {
    manifest["custom_routes"]
        .as_array()
        .map(|routes| routes.iter().map(classify_route).collect())
        .unwrap_or_default()
}

fn print_routes(service: &str, audits: &[RouteAudit]) {
    if audits.is_empty() {
        println!("{service}: no custom routes");
        return;
    }
    println!("{service}:");
    for a in audits {
        let flag = if a.anonymous { " ⚠ anonymous" } else { "" };
        println!(
            "  {:<6} {:<38} {:<26} {:<22}{}",
            a.method, a.path, a.handler, a.label, flag
        );
    }
}

pub async fn run(all_services: bool, audit: bool, env: Option<&str>) -> eyre::Result<()> {
    let anonymous = if all_services {
        run_deployed(env).await?
    } else {
        run_local()?
    };

    if audit && anonymous > 0 {
        eyre::bail!("{anonymous} anonymous or undeclared route(s) found");
    }
    Ok(())
}

/// Audit the manifest of the service in the current directory.
fn run_local() -> eyre::Result<usize> {
    let output = Command::new("cargo")
        .args(["run", "--", "--emit-manifest"])
        .output()?;
    if !output.status.success() {
        eyre::bail!("Failed to build service. Run from a cufflink service directory.");
    }
    let manifest: Value = serde_json::from_slice(&output.stdout)?;
    let service = manifest["name"].as_str().unwrap_or("service").to_string();
    let audits = audit_manifest(&manifest);
    let anonymous = audits.iter().filter(|a| a.anonymous).count();
    print_routes(&service, &audits);
    Ok(anonymous)
}

/// Audit every deployed service's stored manifest.
async fn run_deployed(env: Option<&str>) -> eyre::Result<usize> {
    let config = CliConfig::load_with_env(env)?;
    if let Some(ref name) = config.env_name {
        println!("Environment: {name}\n");
    }
    let client = config.http_client();

    let resp = config
        .auth_request(
            &client,
            reqwest::Method::GET,
            &format!("{}/api/services", config.api_url),
        )
        .send()
        .await?;
    if !resp.status().is_success() {
        eyre::bail!("Failed to list services: {}", resp.status());
    }
    let body: Value = resp.json().await?;
    let services = body["services"].as_array().cloned().unwrap_or_default();

    let mut total_anonymous = 0usize;
    for svc in &services {
        let (Some(id), Some(name)) = (svc["id"].as_str(), svc["name"].as_str()) else {
            continue;
        };
        let detail = config
            .auth_request(
                &client,
                reqwest::Method::GET,
                &format!("{}/api/services/{}", config.api_url, id),
            )
            .send()
            .await?;
        if !detail.status().is_success() {
            println!("{name}: unable to fetch manifest ({})", detail.status());
            continue;
        }
        let detail_body: Value = detail.json().await?;
        let audits = audit_manifest(&detail_body["current_manifest"]);
        total_anonymous += audits.iter().filter(|a| a.anonymous).count();
        print_routes(name, &audits);
    }
    Ok(total_anonymous)
}

#[cfg(test)]
mod tests {
    use super::*;
    use serde_json::json;

    #[test]
    fn classifies_each_posture() {
        let public = classify_route(&json!({
            "method": "POST", "path": "/webhook", "handler": "h",
            "posture": {"kind": "public", "reason": "stripe"}
        }));
        assert!(public.anonymous);
        assert!(public.label.contains("public"));

        let perm = classify_route(&json!({
            "method": "GET", "path": "/x", "handler": "h",
            "posture": {"kind": "permission", "area": "items", "operation": "edit"}
        }));
        assert!(!perm.anonymous);
        assert_eq!(perm.label, "permission items:edit");

        let owner = classify_route(&json!({
            "method": "GET", "path": "/x", "handler": "h",
            "posture": {"kind": "owner", "field": "customer_id"}
        }));
        assert!(!owner.anonymous);
        assert_eq!(owner.label, "owner(customer_id)");

        let auth = classify_route(&json!({
            "method": "GET", "path": "/x", "handler": "h",
            "posture": {"kind": "authenticated"}
        }));
        assert!(!auth.anonymous);
    }

    #[test]
    fn missing_posture_is_undeclared_and_flagged() {
        let a = classify_route(&json!({"method": "GET", "path": "/x", "handler": "h"}));
        assert_eq!(a.label, "undeclared");
        assert!(a.anonymous);
    }

    #[test]
    fn audit_counts_only_anonymous() {
        let manifest = json!({
            "custom_routes": [
                {"method": "GET", "path": "/a", "handler": "a", "posture": {"kind": "authenticated"}},
                {"method": "POST", "path": "/b", "handler": "b", "posture": {"kind": "public", "reason": "wh"}},
                {"method": "GET", "path": "/c", "handler": "c"}
            ]
        });
        let audits = audit_manifest(&manifest);
        assert_eq!(audits.len(), 3);
        assert_eq!(audits.iter().filter(|a| a.anonymous).count(), 2);
    }
}