ordinary-config 0.8.0

Config for Ordinary
Documentation
// Copyright (C) 2026 Ordinary Labs, LLC.
//
// SPDX-License-Identifier: AGPL-3.0-only

use crate::{MiddlewareMechanism, OrdinaryApiLimits, OrdinaryConfig};
use anyhow::bail;
use hashbrown::HashSet;
use regex::Regex;

#[allow(clippy::too_many_lines)]
pub fn check_config_against_limits(
    config: &OrdinaryConfig,
    limits: &OrdinaryApiLimits,
    privileged_domains: &HashSet<String>,
) -> anyhow::Result<()> {
    if privileged_domains.contains(&config.domain) {
        return Ok(());
    }

    if let Some(canonical) = &config.canonical {
        let mut is_valid = false;

        if canonical == &config.domain {
            is_valid = true;
        }

        if let Some(cnames) = &config.cnames {
            for cname in cnames {
                if cname == canonical {
                    is_valid = true;
                    break;
                }
            }
        }

        if !is_valid {
            bail!("canonical: {canonical}, is not in 'domain' or 'cnames'");
        }
    }

    if let Some(default_timeout) = config.default_timeout
        && default_timeout > limits.max_default_timeout
    {
        bail!(
            "default timeout {} greater than limit {}",
            default_timeout,
            limits.max_default_timeout
        );
    }

    if let Some(proxies) = &config.proxies {
        if proxies.len() > limits.proxy.count as usize {
            bail!(
                "proxy count {} greater than limit {}",
                proxies.len(),
                limits.proxy.count
            );
        }

        let mut disallowed_target_regexes =
            Vec::with_capacity(limits.proxy.disallowed_targets.len());

        for disallowed in &limits.proxy.disallowed_targets {
            disallowed_target_regexes.push(Regex::new(disallowed)?);
        }

        for proxy in proxies {
            for re in &disallowed_target_regexes {
                if re.is_match(&proxy.target) {
                    bail!(
                        "proxy target {} is disallowed by rule {}",
                        proxy.target,
                        re.as_str()
                    )
                }
            }
        }
    }

    if let Some(middlewares) = &config.middlewares {
        if middlewares.len() > limits.middleware.count as usize {
            bail!(
                "middlewares count {} greater than limit {}",
                middlewares.len(),
                limits.middleware.count
            );
        }

        let mut disallowed_endpoint_regexes =
            Vec::with_capacity(limits.middleware.disallowed_endpoints.len());

        for disallowed in &limits.middleware.disallowed_endpoints {
            disallowed_endpoint_regexes.push(Regex::new(disallowed)?);
        }

        for middleware in middlewares {
            match &middleware.mechanism {
                MiddlewareMechanism::Request { endpoint } => {
                    for re in &disallowed_endpoint_regexes {
                        if re.is_match(endpoint) {
                            bail!(
                                "middleware endpoint {endpoint} is disallowed by rule {}",
                                re.as_str()
                            )
                        }
                    }
                }
            }
        }
    }

    if let Some(templates) = &config.templates {
        if templates.len() > limits.template.count as usize {
            bail!(
                "template count {} greater than limit {}",
                templates.len(),
                limits.template.count
            );
        }

        for template in templates {
            if let Some(timeout) = template.timeout
                && timeout > limits.template.max_timeout
            {
                bail!(
                    "template timeout {} greater than limit {} for {}",
                    timeout,
                    limits.template.max_timeout,
                    template.name
                );
            }

            if let Some(cache) = &template.cache
                && let Some(stored_cache) = &cache.stored
            {
                if let Some(max_size) = stored_cache.max_size
                    && (max_size < limits.storage.cache.max_size_range.0
                        || max_size > limits.storage.cache.max_size_range.1)
                {
                    bail!(
                        "template cache max_size {} is not within range [{}, {}] for {}",
                        max_size,
                        limits.storage.cache.max_size_range.0,
                        limits.storage.cache.max_size_range.1,
                        template.name
                    );
                }

                if let Some(max_count) = stored_cache.max_count
                    && (max_count < limits.storage.cache.max_count_range.0
                        || max_count > limits.storage.cache.max_count_range.1)
                {
                    bail!(
                        "template cache max_count {} is not within range [{}, {}] for {}",
                        max_count,
                        limits.storage.cache.max_count_range.0,
                        limits.storage.cache.max_count_range.1,
                        template.name
                    );
                }

                if let Some((clean_interval_min, clean_interval_max)) = stored_cache.clean_interval
                {
                    if clean_interval_min < limits.storage.cache.clean_interval_ranges.0.0
                        || clean_interval_min > limits.storage.cache.clean_interval_ranges.0.1
                    {
                        bail!(
                            "template cache clean_interval min {} is not within range [{}, {}] for {}",
                            clean_interval_min,
                            limits.storage.cache.clean_interval_ranges.0.0,
                            limits.storage.cache.clean_interval_ranges.0.1,
                            template.name
                        );
                    }

                    if clean_interval_max < limits.storage.cache.clean_interval_ranges.1.0
                        || clean_interval_max > limits.storage.cache.clean_interval_ranges.1.1
                    {
                        bail!(
                            "template cache clean_interval min {} is not within range [{}, {}] for {}",
                            clean_interval_max,
                            limits.storage.cache.clean_interval_ranges.1.0,
                            limits.storage.cache.clean_interval_ranges.1.1,
                            template.name
                        );
                    }
                }
            }
        }
    }

    if let Some(integrations) = &config.integrations {
        if integrations.len() > limits.integration.count as usize {
            bail!(
                "integration count {} greater than limit {}",
                integrations.len(),
                limits.integration.count
            );
        }

        for integration in integrations {
            if let Some(timeout) = integration.timeout
                && timeout > limits.integration.max_timeout
            {
                bail!(
                    "integration timeout {} greater than limit {} for {}",
                    timeout,
                    limits.integration.max_timeout,
                    integration.name,
                );
            }
        }
    }

    if let Some(actions) = &config.actions {
        if actions.len() > limits.action.count as usize {
            bail!(
                "action count {} greater than limit {}",
                actions.len(),
                limits.action.count
            );
        }

        for action in actions {
            if action.privileged == Some(true) {
                bail!("action {} is not under a privileged domain", action.name);
            }

            if let Some(timeout) = action.timeout
                && timeout > limits.action.max_timeout
            {
                bail!(
                    "action timeout {} greater than limit {} for {}",
                    timeout,
                    limits.action.max_timeout,
                    action.name
                );
            }
        }
    }

    if let Some(storage_size) = config.storage_size
        && storage_size > limits.storage.max_app_storage
    {
        bail!("storage size is greater than limit");
    }

    if let Some(content) = &config.content {
        if content.definitions.len() > limits.storage.content.max_content_definitions as usize {
            bail!(
                "content definition count {} greater than limit {}",
                content.definitions.len(),
                limits.storage.content.max_content_definitions
            );
        }

        for content_def in &content.definitions {
            if content_def.fields.len() > limits.storage.content.max_content_fields as usize {
                bail!(
                    "content field count {} greater than limit {} for {}",
                    content_def.fields.len(),
                    limits.storage.content.max_content_fields,
                    content_def.name,
                );
            }

            for field in &content_def.fields {
                if field.searchable == Some(true) && !limits.storage.content.search_enabled {
                    bail!(
                        "content field cannot be 'searchable' for field {} on definition {}",
                        field.name,
                        content_def.name,
                    );
                }
            }
        }
    }

    if let Some(models) = &config.models {
        if models.len() > limits.storage.model.max_model_definitions as usize {
            bail!(
                "model definition count {} greater than limit {}",
                models.len(),
                limits.storage.model.max_model_definitions
            );
        }

        for model in models {
            if model.fields.len() > limits.storage.model.max_model_fields as usize {
                bail!(
                    "model field count {} greater than limit {} for {}",
                    model.fields.len(),
                    limits.storage.content.max_content_fields,
                    model.name,
                );
            }

            for field in &model.fields {
                if field.searchable == Some(true) && !limits.storage.content.search_enabled {
                    bail!(
                        "content field cannot be 'searchable' for field {} on definition {}",
                        field.name,
                        model.name,
                    );
                }
            }
        }
    }

    if let Some(secrets) = &config.secrets
        && secrets.len() > limits.storage.secrets.max_count as usize
    {
        bail!(
            "secrets len {} exceeds max count limit {}",
            secrets.len(),
            limits.storage.secrets.max_count
        );
    }

    Ok(())
}