cedarling 0.0.58

The Cedarling: a high-performance local authorization service powered by the Rust Cedar Engine.
Documentation
// This software is available under the Apache-2.0 license.
// See https://www.apache.org/licenses/LICENSE-2.0.txt for full text.
//
// Copyright (c) 2024, Gluu, Inc.

use serde::{Deserialize, Serialize};
use std::path::PathBuf;

use crate::bootstrap_config::BootstrapConfigLoadingError;

/// `PolicyStoreConfig` - Configuration for the policy store.
///
/// Defines where the policy will be retrieved from.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct PolicyStoreConfig {
    /// Specifies the source from which the policy will be read.
    pub source: PolicyStoreSource,

    /// Base refresh interval in seconds for URL-based policy store sources
    /// (`CjarUrl`, `LockServer`, `Uri`). `0` disables background refresh and preserves
    /// the load-once-at-startup behavior. Ignored for local sources. A server
    /// `Cache-Control: max-age` / `Expires` hint may *shorten* the next
    /// interval but never lengthens it.
    #[serde(default)]
    pub refresh_interval_secs: u64,
}

impl PolicyStoreConfig {
    /// Minimum refresh interval, in seconds — anything smaller is clamped up to
    /// this value to avoid a busy-poll against the upstream.
    pub(crate) const MIN_REFRESH_INTERVAL_SECS: u64 = 5;

    /// True if the source is a remote URL and refresh is enabled.
    #[must_use]
    pub fn refresh_enabled(&self) -> bool {
        self.refresh_interval_secs > 0
            && matches!(
                self.source,
                PolicyStoreSource::CjarUrl(_)
                    | PolicyStoreSource::LockServer(_)
                    | PolicyStoreSource::Uri(_)
            )
    }

    /// Returns the effective refresh interval after applying the
    /// `MIN_REFRESH_INTERVAL_SECS` floor, plus a boolean indicating whether
    /// clamping occurred. `0` (disabled) passes through unchanged. Callers
    /// should emit a `WARN` log when `clamped` is true so operators see the
    /// silent normalization. Single point of normalization so deserializer
    /// inputs (env vars, JSON, dict) can't drift apart.
    #[must_use]
    pub(crate) fn effective_refresh_interval(&self) -> (u64, bool) {
        let raw = self.refresh_interval_secs;
        if raw == 0 || raw >= Self::MIN_REFRESH_INTERVAL_SECS {
            (raw, false)
        } else {
            (Self::MIN_REFRESH_INTERVAL_SECS, true)
        }
    }
}

impl Default for PolicyStoreConfig {
    fn default() -> Self {
        Self {
            source: PolicyStoreSource::Yaml(
                "cedar_version: v4.0.0\npolicy_stores: {}\n".to_string(),
            ),
            refresh_interval_secs: 0,
        }
    }
}

/// Raw policy store config
pub struct PolicyStoreConfigRaw {
    /// Source
    pub source: String,
    /// Path
    pub path: Option<String>,
}

/// `PolicyStoreSource` represents the source from which policies will be retrieved.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum PolicyStoreSource {
    /// Read the policy directly from a raw JSON string.
    ///
    /// The string contains the raw JSON data representing the policy.
    Json(String),

    /// Read the policy directly from a raw YAML string.
    ///
    /// The string contains the raw YAML data representing the policy.
    /// Mostly used only for testing purposes.
    Yaml(String),

    /// Fetch the policies from the Lock Master service using a specified identifier.
    ///
    /// The string contains a URI where the policy store can be retrieved.
    LockServer(String),

    /// Read policy from a JSON File.
    FileJson(PathBuf),

    /// Read policy from a YAML File.
    FileYaml(PathBuf),

    /// Read policy from a Cedar Archive (.cjar) file.
    ///
    /// The path points to a `.cjar` archive containing the policy store
    /// in the new directory structure format.
    CjarFile(PathBuf),

    /// Read policy from a Cedar Archive (.cjar) fetched from a URL.
    ///
    /// The string contains a URL where the `.cjar` archive can be downloaded.
    CjarUrl(String),

    /// Read policy from a directory structure.
    ///
    /// The path points to a directory containing the policy store
    /// in the directory structure format (`metadata.json`, `schema.cedarschema`, `policies/`, etc.).
    Directory(PathBuf),

    /// An unresolved URI whose source type (archive vs lock server) is detected
    /// at load time via magic byte checking.
    ///
    /// During loading, [`load_policy_store`](crate::init::policy_store::load_policy_store)
    /// resolves this by making an HTTP request to determine whether the URI points
    /// to a Cedar Archive (`.cjar`) or a Lock Master endpoint.
    Uri(String),

    /// Read policy from Cedar Archive bytes directly.
    ///
    /// The bytes contain a `.cjar` archive (ZIP format) with the policy store.
    /// This is particularly useful for:
    /// - WASM environments with custom fetch logic
    /// - Embedding archives in applications
    /// - Loading from non-standard sources (databases, S3, etc.)
    ArchiveBytes(Vec<u8>),
}

/// Raw policy store source
pub enum PolicyStoreSourceRaw {
    /// JSON
    Json(String),
    /// YAML
    Yaml(String),
    /// Lock server
    LockServer(String),
    /// File JSON
    FileJson(String),
    /// File YAML
    FileYaml(String),
    /// Cedar Archive file (.cjar)
    CjarFile(String),
    /// Cedar Archive URL (.cjar)
    CjarUrl(String),
    /// Directory structure
    Directory(String),
}

impl TryFrom<PolicyStoreConfigRaw> for PolicyStoreConfig {
    type Error = BootstrapConfigLoadingError;

    fn try_from(raw: PolicyStoreConfigRaw) -> Result<Self, Self::Error> {
        let source = match raw.source.as_str() {
            "json" => PolicyStoreSource::Json(raw.path.unwrap_or_default()),
            "yaml" => PolicyStoreSource::Yaml(raw.path.unwrap_or_default()),

            "lock_server" => PolicyStoreSource::LockServer(raw.path.unwrap_or_default()),
            "file_json" => PolicyStoreSource::FileJson(raw.path.unwrap_or_default().into()),
            "file_yaml" => PolicyStoreSource::FileYaml(raw.path.unwrap_or_default().into()),
            "cjar_file" => PolicyStoreSource::CjarFile(
                raw.path
                    .filter(|p| !p.is_empty())
                    .unwrap_or_else(|| "policy-store.cjar".to_string())
                    .into(),
            ),
            "cjar_url" => {
                let url = raw.path.filter(|p| !p.is_empty()).unwrap_or_default();
                if url.is_empty() {
                    return Err(BootstrapConfigLoadingError::MissingCjarUrl);
                }
                PolicyStoreSource::CjarUrl(url)
            },
            "directory" => PolicyStoreSource::Directory(
                raw.path
                    .filter(|p| !p.is_empty())
                    .unwrap_or_else(|| "policy-store".to_string())
                    .into(),
            ),
            _ => PolicyStoreSource::FileYaml("policy-store.yaml".into()),
        };
        // Explicit field init rather than `..Default::default()`, which would
        // allocate and immediately discard the default YAML source string.
        Ok(Self {
            source,
            refresh_interval_secs: 0,
        })
    }
}