cedarling 0.0.44

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.

//! Error types for policy store operations.

/// Cedar schema-specific errors.
#[derive(Debug, thiserror::Error)]
pub(crate) enum CedarSchemaErrorType {
    /// Schema file is empty
    #[error("Schema file is empty")]
    EmptySchema,

    /// Schema parsing failed
    #[error("Schema parsing failed: {0}")]
    ParseError(String),

    /// Schema validation failed
    #[error("Schema validation failed: {0}")]
    ValidationError(String),
}

/// Cedar entity-specific errors.
#[derive(Debug, thiserror::Error)]
pub(crate) enum CedarEntityErrorType {
    /// Failed to parse entity from JSON
    #[error("Failed to parse entity from JSON: {0}")]
    JsonParseError(String),

    /// Invalid entity type name
    #[error("Invalid entity type name '{0}': {1}")]
    InvalidTypeName(String, String),

    /// Invalid entity ID
    #[error("Invalid entity ID: {0}")]
    InvalidEntityId(String),

    /// Failed to create entity store
    #[error("Failed to create entity store: {0}")]
    EntityStoreCreation(String),
}

/// Trusted issuer-specific errors.
#[derive(Debug, thiserror::Error)]
pub(crate) enum TrustedIssuerErrorType {
    /// Trusted issuer file is not a JSON object
    #[error("Trusted issuer file must be a JSON object")]
    NotAnObject,

    /// Missing required field in issuer configuration
    #[error("Issuer '{issuer_id}': missing required field '{field}'")]
    MissingRequiredField { issuer_id: String, field: String },

    /// Invalid OIDC endpoint URL
    #[error("Issuer '{issuer_id}': invalid OIDC endpoint URL '{url}': {reason}")]
    InvalidOidcEndpoint {
        issuer_id: String,
        url: String,
        reason: String,
    },

    /// Token metadata is not an object
    #[error("Issuer '{issuer_id}': token_metadata must be a JSON object")]
    TokenMetadataNotAnObject { issuer_id: String },

    /// Token metadata entry is not an object
    #[error("Issuer '{issuer_id}': token_metadata.{token_type} must be a JSON object")]
    TokenMetadataEntryNotAnObject {
        issuer_id: String,
        token_type: String,
    },
}

/// Errors that can occur during policy store operations.
#[derive(Debug, thiserror::Error)]
pub(crate) enum PolicyStoreError {
    /// IO error during file operations
    #[error("IO error: {0}")]
    Io(#[from] std::io::Error),

    /// Validation error
    #[error("Validation error: {0}")]
    Validation(#[from] ValidationError),

    /// Archive handling error
    #[error("Archive error: {0}")]
    Archive(#[from] ArchiveError),

    /// JSON parsing error
    #[error("JSON parsing error in '{file}'")]
    JsonParsing {
        file: String,
        #[source]
        source: serde_json::Error,
    },

    /// Cedar parsing error
    #[error("Cedar parsing error in '{file}': {detail}")]
    CedarParsing {
        file: String,
        detail: CedarParseErrorDetail,
    },

    /// Cedar schema error
    #[error("Cedar schema error in '{file}': {err}")]
    CedarSchemaError {
        file: String,
        err: CedarSchemaErrorType,
    },

    /// Cedar entity error
    #[error("Cedar entity error in '{file}': {err}")]
    CedarEntityError {
        file: String,
        err: CedarEntityErrorType,
    },

    /// Trusted issuer error
    #[error("Trusted issuer error in '{file}': {err}")]
    TrustedIssuerError {
        file: String,
        err: TrustedIssuerErrorType,
    },

    /// Path not found
    #[error("Path not found: {path}")]
    PathNotFound { path: String },

    /// Path is not a directory
    #[error("Path is not a directory: {path}")]
    NotADirectory { path: String },

    /// Directory read error
    #[error("Failed to read directory '{path}'")]
    DirectoryReadError {
        path: String,
        #[source]
        source: std::io::Error,
    },

    /// File read error
    #[error("Failed to read file '{path}'")]
    FileReadError {
        path: String,
        #[source]
        source: std::io::Error,
    },
}

/// Details about Cedar parsing errors.
#[derive(Debug, Clone, thiserror::Error)]
pub(crate) enum CedarParseErrorDetail {
    /// Missing `@id()` annotation
    #[error("No @id() annotation found and could not derive ID from filename")]
    MissingIdAnnotation,

    /// A policy in a multi-policy file has no `@id("...")` annotation.
    /// `line` is 1-based; `snippet` is the offending `permit` / `forbid` line.
    #[error(
        "Multi-policy .cedar files require @id(\"...\") on each policy; missing at line {line}: {snippet}"
    )]
    MultiPolicyMissingExplicitId { line: usize, snippet: String },

    /// Two or more policies in the same file share the same `@id("...")` value
    #[error("duplicate @id(\"{id}\") within a single .cedar file")]
    DuplicatePolicyIdInFile { id: String },

    /// A `.cedar` policy file contains one or more templates (slots like `?principal`).
    /// Templates must live in the `templates/` directory, not mixed with policies.
    #[error(
        "policy file contains {count} template(s); templates must be placed in the `templates/` directory, not mixed with policies"
    )]
    TemplatesInPolicyFile { count: usize },

    /// A `.cedar` template file contains one or more policies (no slots).
    /// Policies must live in the `policies/` directory, not mixed with templates.
    #[error(
        "template file contains {count} policy/policies; policies must be placed in the `policies/` directory, not mixed with templates"
    )]
    PoliciesInTemplateFile { count: usize },

    /// A template in a multi-template file has no `@id("...")` annotation.
    #[error(
        "Multi-template .cedar files require @id(\"...\") on each template; missing at line {line}: {snippet}"
    )]
    MultiTemplateMissingExplicitId { line: usize, snippet: String },

    /// Two or more templates in the same file share the same `@id("...")` value.
    #[error("duplicate @id(\"{id}\") within a single template .cedar file")]
    DuplicateTemplateIdInFile { id: String },

    /// The same policy/template id appears in two different files.
    /// Cedar's `PolicySet` namespaces policies and templates together, so any
    /// collision across files (policy↔policy, template↔template, or
    /// policy↔template) is caught here with both file names.
    #[error(
        "duplicate @id(\"{id}\") across files: defined in both '{first_file}' and '{second_file}'"
    )]
    DuplicatePolicyIdAcrossFiles {
        id: String,
        first_file: String,
        second_file: String,
    },

    /// Failed to parse Cedar policy or template
    #[error("{0}")]
    ParseError(String),

    /// Failed to add policy to policy set
    #[error("Failed to add policy to set: {0}")]
    AddPolicyFailed(String),

    /// Failed to add template to policy set
    #[error("Failed to add template to set: {0}")]
    AddTemplateFailed(String),
}

/// Validation errors for policy store components.
#[derive(Debug, thiserror::Error)]
pub(crate) enum ValidationError {
    /// Failed to parse metadata JSON
    #[error("Invalid metadata in file {file}: failed to parse JSON")]
    MetadataJsonParseFailed {
        file: String,
        #[source]
        source: serde_json::Error,
    },

    /// Invalid cedar version format in metadata
    #[error("Invalid metadata in file {file}: invalid cedar_version format")]
    MetadataInvalidCedarVersion {
        file: String,
        #[source]
        source: semver::Error,
    },

    /// Missing required file
    #[error("Missing required file: {file}")]
    MissingRequiredFile { file: String },

    /// Missing required directory
    #[error("Missing required directory: {directory}")]
    MissingRequiredDirectory { directory: String },

    /// Invalid file extension
    #[error("Invalid file extension for {file}: expected {expected}, got {actual}")]
    InvalidFileExtension {
        file: String,
        expected: String,
        actual: String,
    },

    /// Policy ID is empty
    #[error("Invalid policy ID format in {file}: Policy ID cannot be empty")]
    EmptyPolicyId { file: String },

    /// Policy ID contains invalid characters
    #[error(
        "Invalid policy ID format in {file}: Policy ID '{id}' contains invalid characters. Only alphanumeric, '_', '-', and ':' are allowed"
    )]
    InvalidPolicyIdCharacters { file: String, id: String },

    // Specific metadata validation errors
    /// Empty Cedar version
    #[error("Cedar version cannot be empty in metadata.json")]
    EmptyCedarVersion,

    /// Invalid Cedar version format
    #[error("Invalid Cedar version format in metadata.json: '{version}' - {details}")]
    InvalidCedarVersion { version: String, details: String },

    /// Empty policy store name
    #[error("Policy store name cannot be empty in metadata.json")]
    EmptyPolicyStoreName,

    /// Policy store name too long
    #[error("Policy store name too long in metadata.json: {length} chars (max 255)")]
    PolicyStoreNameTooLong { length: usize },

    /// Invalid policy store ID format
    #[error(
        "Invalid policy store ID format in metadata.json: '{id}' must be hexadecimal (8-64 chars)"
    )]
    InvalidPolicyStoreId { id: String },

    /// Invalid policy store version
    #[error("Invalid policy store version in metadata.json: '{version}' - {details}")]
    InvalidPolicyStoreVersion { version: String, details: String },

    /// Policy store description too long
    #[error(
        "Policy store description too long in metadata.json: {length} chars (max {max_length})"
    )]
    DescriptionTooLong { length: usize, max_length: usize },

    /// Invalid timestamp ordering
    #[error(
        "Invalid timestamp ordering in metadata.json: updated_date cannot be before created_date"
    )]
    InvalidTimestampOrdering,

    /// Schema directory exists but is empty (no .cedarschema files)
    #[error("Schema directory '{path}/' exists but contains no .cedarschema files")]
    EmptySchemaDirectory { path: String },

    /// Neither schema.cedarschema file nor schemas/ directory found
    #[error("No schema source found: neither '{searched_file}' nor directory '{searched_dir}/' exists")]
    MissingSchemaSource {
        searched_file: String,
        searched_dir: String,
    },
}

/// Errors related to archive (.cjar) handling.
#[derive(Debug, thiserror::Error)]
pub(crate) enum ArchiveError {
    /// Invalid file extension (expected .cjar)
    #[error("Invalid file extension: expected '{expected}', found '{found}'")]
    #[cfg(not(target_arch = "wasm32"))]
    InvalidExtension { expected: String, found: String },

    /// Cannot read archive file
    #[error("Cannot read archive file '{path}': {source}")]
    #[cfg(not(target_arch = "wasm32"))]
    CannotReadFile {
        path: String,
        #[source]
        source: std::io::Error,
    },

    /// Invalid ZIP format
    #[error("Invalid ZIP archive format: {details}")]
    InvalidZipFormat { details: String },

    /// Corrupted archive entry
    #[error("Corrupted archive entry at index {index}: {details}")]
    CorruptedEntry { index: usize, details: String },

    /// Path traversal attempt detected
    #[error("Path traversal attempt detected in archive: '{path}'")]
    PathTraversal { path: String },

    /// Unsupported operation on this platform
    #[cfg(target_arch = "wasm32")]
    #[error("Archive operations are not supported on this platform")]
    WasmUnsupported,
}