greentic-start-dev 1.1.27285499481

Greentic lifecycle runner for start/restart/stop orchestration
Documentation
#![allow(dead_code)]

use serde_json::Value as JsonValue;

use super::types::TokenValidationDecision;

pub(super) fn extract_token_validation_request(payload_bytes: &[u8]) -> Option<JsonValue> {
    let payload: JsonValue = serde_json::from_slice(payload_bytes).ok()?;
    let token = extract_bearer_token(&payload)?;
    let mut request = serde_json::Map::new();
    request.insert("token".to_string(), JsonValue::String(token));
    if let Some(issuer) = first_string_at_paths(
        &payload,
        &["/token_validation/issuer", "/auth/issuer", "/issuer"],
    ) {
        request.insert("issuer".to_string(), JsonValue::String(issuer));
    }
    if let Some(audience) = first_value_at_paths(
        &payload,
        &["/token_validation/audience", "/auth/audience", "/audience"],
    ) {
        request.insert("audience".to_string(), normalize_string_or_array(audience));
    }
    if let Some(scopes) = first_value_at_paths(
        &payload,
        &[
            "/token_validation/scopes",
            "/token_validation/required_scopes",
            "/auth/scopes",
            "/auth/required_scopes",
            "/scopes",
        ],
    ) {
        request.insert("scopes".to_string(), normalize_string_or_array(scopes));
    }
    Some(JsonValue::Object(request))
}

fn extract_bearer_token(payload: &JsonValue) -> Option<String> {
    if let Some(value) = first_string_at_paths(
        payload,
        &[
            "/token_validation/token",
            "/auth/token",
            "/bearer_token",
            "/token",
            "/access_token",
        ],
    ) && let Some(token) = parse_token_value(&value)
    {
        return Some(token);
    }

    if let Some(value) = payload
        .pointer("/authorization")
        .and_then(JsonValue::as_str)
        && let Some(token) = parse_authorization_value(value)
    {
        return Some(token);
    }

    if let Some(headers) = payload.get("headers")
        && let Some(token) = extract_bearer_from_headers(headers)
    {
        return Some(token);
    }

    if let Some(value) = payload
        .pointer("/metadata/authorization")
        .and_then(JsonValue::as_str)
        && let Some(token) = parse_authorization_value(value)
    {
        return Some(token);
    }

    None
}

fn extract_bearer_from_headers(headers: &JsonValue) -> Option<String> {
    match headers {
        JsonValue::Object(map) => {
            for key in ["authorization", "Authorization"] {
                if let Some(value) = map.get(key).and_then(JsonValue::as_str)
                    && let Some(token) = parse_authorization_value(value)
                {
                    return Some(token);
                }
            }
            None
        }
        JsonValue::Array(values) => values.iter().find_map(|entry| {
            let name = entry
                .get("name")
                .or_else(|| entry.get("key"))
                .and_then(JsonValue::as_str)?;
            if !name.eq_ignore_ascii_case("authorization") {
                return None;
            }
            let value = entry.get("value").and_then(JsonValue::as_str)?;
            parse_authorization_value(value)
        }),
        _ => None,
    }
}

fn parse_token_value(raw: &str) -> Option<String> {
    let trimmed = raw.trim();
    if trimmed.is_empty() {
        None
    } else {
        Some(trimmed.to_string())
    }
}

fn parse_authorization_value(raw: &str) -> Option<String> {
    let trimmed = raw.trim();
    let mut parts = trimmed.split_whitespace();
    let scheme = parts.next()?;
    if !scheme.eq_ignore_ascii_case("bearer") {
        return None;
    }
    let token = parts.next()?;
    if token.trim().is_empty() || parts.next().is_some() {
        return None;
    }
    Some(token.to_string())
}

fn first_string_at_paths(payload: &JsonValue, paths: &[&str]) -> Option<String> {
    paths
        .iter()
        .find_map(|path| payload.pointer(path).and_then(JsonValue::as_str))
        .map(str::to_string)
}

fn first_value_at_paths<'a>(payload: &'a JsonValue, paths: &[&str]) -> Option<&'a JsonValue> {
    paths.iter().find_map(|path| payload.pointer(path))
}

fn normalize_string_or_array(value: &JsonValue) -> JsonValue {
    match value {
        JsonValue::String(raw) => {
            let values = raw
                .split_whitespace()
                .filter(|entry| !entry.trim().is_empty())
                .map(|entry| JsonValue::String(entry.to_string()))
                .collect::<Vec<_>>();
            JsonValue::Array(values)
        }
        JsonValue::Array(items) => JsonValue::Array(
            items
                .iter()
                .filter_map(|item| item.as_str())
                .map(|item| JsonValue::String(item.to_string()))
                .collect(),
        ),
        _ => JsonValue::Array(Vec::new()),
    }
}

pub(super) fn evaluate_token_validation_output(output: &JsonValue) -> TokenValidationDecision {
    let valid = output
        .get("valid")
        .and_then(JsonValue::as_bool)
        .or_else(|| output.get("ok").and_then(JsonValue::as_bool))
        .unwrap_or(false);
    if !valid {
        let reason = output
            .get("reason")
            .and_then(JsonValue::as_str)
            .or_else(|| output.get("error").and_then(JsonValue::as_str))
            .unwrap_or("invalid bearer token");
        return TokenValidationDecision::Deny(reason.to_string());
    }
    let claims = output
        .get("claims")
        .filter(|value| value.is_object())
        .cloned()
        .or_else(|| {
            output
                .as_object()
                .is_some_and(|map| map.contains_key("sub"))
                .then(|| output.clone())
        });
    TokenValidationDecision::Allow(claims)
}

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

    #[test]
    fn token_validation_request_extracts_bearer_and_requirements() {
        let payload = json!({
            "headers": {
                "Authorization": "Bearer token-123"
            },
            "token_validation": {
                "issuer": "https://issuer.example",
                "audience": ["api://svc"],
                "required_scopes": "read write"
            }
        });
        let request =
            extract_token_validation_request(&serde_json::to_vec(&payload).expect("payload bytes"))
                .expect("request");
        assert_eq!(
            request.pointer("/token").and_then(JsonValue::as_str),
            Some("token-123")
        );
        assert_eq!(
            request.pointer("/issuer").and_then(JsonValue::as_str),
            Some("https://issuer.example")
        );
        assert_eq!(
            request.pointer("/audience/0").and_then(JsonValue::as_str),
            Some("api://svc")
        );
        assert_eq!(
            request.pointer("/scopes/0").and_then(JsonValue::as_str),
            Some("read")
        );
        assert_eq!(
            request.pointer("/scopes/1").and_then(JsonValue::as_str),
            Some("write")
        );
    }

    #[test]
    fn token_validation_request_accepts_case_insensitive_bearer_authorization() {
        let payload = json!({
            "headers": {
                "authorization": "bearer token-123"
            }
        });
        let request =
            extract_token_validation_request(&serde_json::to_vec(&payload).expect("payload bytes"))
                .expect("request");
        assert_eq!(
            request.pointer("/token").and_then(JsonValue::as_str),
            Some("token-123")
        );
    }

    #[test]
    fn token_validation_request_rejects_non_bearer_authorization_headers() {
        let payload = json!({
            "headers": {
                "Authorization": "Basic Zm9vOmJhcg=="
            }
        });
        assert!(
            extract_token_validation_request(&serde_json::to_vec(&payload).expect("payload bytes"))
                .is_none()
        );
    }

    #[test]
    fn token_validation_request_accepts_explicit_token_fields_without_bearer_prefix() {
        let payload = json!({
            "token": " token-123 "
        });
        let request =
            extract_token_validation_request(&serde_json::to_vec(&payload).expect("payload bytes"))
                .expect("request");
        assert_eq!(
            request.pointer("/token").and_then(JsonValue::as_str),
            Some("token-123")
        );
    }

    #[test]
    fn token_validation_output_deny_when_invalid() {
        let output = json!({
            "valid": false,
            "reason": "issuer mismatch"
        });
        match evaluate_token_validation_output(&output) {
            TokenValidationDecision::Deny(reason) => {
                assert_eq!(reason, "issuer mismatch");
            }
            TokenValidationDecision::Allow(_) => panic!("expected deny"),
        }
    }

    #[test]
    fn token_validation_output_uses_error_fallback_reason() {
        let output = json!({
            "ok": false,
            "error": "token expired"
        });
        match evaluate_token_validation_output(&output) {
            TokenValidationDecision::Deny(reason) => {
                assert_eq!(reason, "token expired");
            }
            TokenValidationDecision::Allow(_) => panic!("expected deny"),
        }
    }

    #[test]
    fn token_validation_output_allows_and_returns_claims() {
        let output = json!({
            "valid": true,
            "claims": {
                "sub": "user-1",
                "scope": "read write",
                "aud": ["api://svc"]
            }
        });
        match evaluate_token_validation_output(&output) {
            TokenValidationDecision::Allow(Some(claims)) => {
                assert_eq!(
                    claims.pointer("/sub").and_then(JsonValue::as_str),
                    Some("user-1")
                );
            }
            TokenValidationDecision::Allow(None) => panic!("expected claims"),
            TokenValidationDecision::Deny(reason) => panic!("unexpected deny: {reason}"),
        }
    }

    #[test]
    fn token_validation_output_uses_root_object_as_claims_when_sub_present() {
        let output = json!({
            "ok": true,
            "sub": "user-1",
            "scope": "read"
        });
        match evaluate_token_validation_output(&output) {
            TokenValidationDecision::Allow(Some(claims)) => {
                assert_eq!(
                    claims.pointer("/sub").and_then(JsonValue::as_str),
                    Some("user-1")
                );
            }
            TokenValidationDecision::Allow(None) => panic!("expected claims"),
            TokenValidationDecision::Deny(reason) => panic!("unexpected deny: {reason}"),
        }
    }
}