coil-runtime 0.1.1

HTTP runtime and request handling for the Coil framework.
Documentation
use super::*;
use std::future::Future;
use tokio::runtime::Handle;

pub(super) fn runtime_executor_error(
    context: &InvocationContext,
    error: impl ToString,
) -> WasmModelError {
    WasmModelError::EngineTrap {
        handler_id: trace_id(context).to_string(),
        reason: error.to_string(),
    }
}

pub(super) fn trace_id(context: &InvocationContext) -> &str {
    context.trace.request_id.as_deref().unwrap_or("unknown")
}

pub(super) fn storage_class_from_grant(class: StorageClassGrant) -> StorageClass {
    match class {
        StorageClassGrant::PublicAsset => StorageClass::PublicAsset,
        StorageClassGrant::PublicUpload => StorageClass::PublicUpload,
        StorageClassGrant::PrivateShared => StorageClass::PrivateShared,
        StorageClassGrant::LocalOnlySensitive => StorageClass::LocalOnlySensitive,
    }
}

pub(super) fn http_method_to_wasm(method: HttpMethod) -> WasmHttpMethod {
    match method {
        HttpMethod::Get => WasmHttpMethod::Get,
        HttpMethod::Head => WasmHttpMethod::Head,
        HttpMethod::Post => WasmHttpMethod::Post,
        HttpMethod::Put => WasmHttpMethod::Put,
        HttpMethod::Patch => WasmHttpMethod::Patch,
        HttpMethod::Delete => WasmHttpMethod::Delete,
    }
}

pub(super) fn invocation_surface_path(execution: &RequestExecution) -> String {
    let Some(locale) = execution.route.locale.as_deref() else {
        return execution.path.clone();
    };
    let prefix = format!("/{locale}");
    if execution.path == prefix {
        "/".to_string()
    } else if let Some(rest) = execution.path.strip_prefix(&(prefix.clone() + "/")) {
        format!("/{rest}")
    } else {
        execution.path.clone()
    }
}

pub(super) fn runtime_auth_backend_error(tenant_id: i64, reason: impl ToString) -> WasmModelError {
    WasmModelError::EngineTrap {
        handler_id: format!("auth-tenant-{tenant_id}"),
        reason: reason.to_string(),
    }
}

pub(super) fn runtime_data_backend_error(
    context: &InvocationContext,
    reason: impl ToString,
) -> WasmModelError {
    WasmModelError::HostServiceUnavailable {
        handler_id: trace_id(context).to_string(),
        domain: HostServiceDomain::Data,
        reason: reason.to_string(),
    }
}

pub(super) fn runtime_host_service_error(
    context: &InvocationContext,
    domain: HostServiceDomain,
    reason: impl ToString,
) -> WasmModelError {
    WasmModelError::HostServiceUnavailable {
        handler_id: trace_id(context).to_string(),
        domain,
        reason: reason.to_string(),
    }
}

pub(super) fn block_on_auth<T>(
    future: impl Future<Output = Result<T, String>> + Send,
) -> Result<T, String>
where
    T: Send,
{
    match Handle::try_current() {
        Ok(handle) => tokio::task::block_in_place(|| handle.block_on(future)),
        Err(_) => {
            let runtime = tokio::runtime::Builder::new_current_thread()
                .enable_all()
                .build()
                .map_err(|error| error.to_string())?;
            runtime.block_on(future)
        }
    }
}

pub(super) fn subject_for_principal(context: &InvocationContext) -> DefaultSubject {
    match context.principal.id.as_deref() {
        Some(principal_id) => match context.principal.kind {
            PrincipalKind::ServiceAccount => {
                DefaultSubject::entity(Entity::service_account(principal_id.to_string()))
            }
            _ => DefaultSubject::entity(Entity::user(principal_id.to_string())),
        },
        None => DefaultSubject::entity(Entity::any_user()),
    }
}

pub(super) fn tenant_object(context: &InvocationContext, tenant_id: i64) -> Entity {
    let object_id = context
        .customer_app
        .tenant_id
        .clone()
        .unwrap_or_else(|| tenant_id.to_string());
    Entity::tenant(object_id)
}

pub(super) fn auth_sequence(context: &InvocationContext) -> u32 {
    context
        .trace
        .trace_id
        .as_bytes()
        .iter()
        .fold(0u32, |acc, byte| {
            acc.wrapping_mul(31).wrapping_add(u32::from(*byte))
        })
}

pub(super) fn synthetic_auth_execution(
    request: &AuthServiceRequest,
    context: &InvocationContext,
    tenant_id: i64,
) -> AuthServiceExecution {
    let principal_id = context.principal.id.clone();
    let tenant = tenant_object(context, tenant_id);
    let subject = subject_for_principal(context);
    let decision = true;
    let sequence = auth_sequence(context);

    match request {
        AuthServiceRequest::Check => AuthServiceExecution {
            request: request.clone(),
            allowed: decision,
            checks_seen: 1,
            principal_id,
            details: AuthServiceDetails::Check {
                capability: Capability::SystemConfigRead.to_string(),
                object: tenant.to_string(),
                decision,
            },
        },
        AuthServiceRequest::List => AuthServiceExecution {
            request: request.clone(),
            allowed: true,
            checks_seen: 1,
            principal_id,
            details: AuthServiceDetails::List {
                capability: Capability::CmsPageRead.to_string(),
                namespace: Namespace::Page.to_string(),
                object_ids: vec![format!("synthetic-page-{sequence}")],
            },
        },
        AuthServiceRequest::Lookup => AuthServiceExecution {
            request: request.clone(),
            allowed: true,
            checks_seen: 1,
            principal_id,
            details: AuthServiceDetails::Lookup {
                capability: Capability::SystemModuleManage.to_string(),
                object: tenant.to_string(),
                relation: Relation::Manage.to_string(),
                subject_namespace: Namespace::User.to_string(),
                subject_ids: vec![subject_for_description(&subject)],
            },
        },
        AuthServiceRequest::TupleWrite => AuthServiceExecution {
            request: request.clone(),
            allowed: true,
            checks_seen: 1,
            principal_id,
            details: AuthServiceDetails::TupleWrite {
                capability: Capability::SystemConfigWrite.to_string(),
                object: tenant.to_string(),
                relation: Relation::Manage.to_string(),
                subject: subject_for_description(&subject),
                updates: vec![format!("write {}#manage", tenant)],
                written: 1,
            },
        },
    }
}

pub(super) fn subject_for_description(subject: &DefaultSubject) -> String {
    match subject {
        DefaultSubject::Entity(entity) => entity.to_string(),
        DefaultSubject::Userset { object, relation } => format!("{object}#{relation}"),
    }
}

pub(super) fn describe_tuple_update(update: &DefaultTupleUpdate) -> String {
    match update {
        DefaultTupleUpdate::Write(tuple) => format!(
            "write {}#{}@{}",
            tuple.object,
            tuple.relation,
            subject_for_description(&tuple.subject)
        ),
        DefaultTupleUpdate::Delete(tuple) => format!(
            "delete {}#{}@{}",
            tuple.object,
            tuple.relation,
            subject_for_description(&tuple.subject)
        ),
    }
}