pdk-cors-lib 1.7.0

PDK CORS Library
Documentation
// Copyright (c) 2026, Salesforce, Inc.,
// All rights reserved.
// For full license text, see the LICENSE.txt file

use pdk_core::logger;

use crate::error::CorsError;
use crate::model::request::origins::OriginGroup;

const ACCESS_CONTROL_MAX_AGE: &str = "Access-Control-Max-Age";
const ACCESS_CONTROL_ALLOW_ORIGIN: &str = "Access-Control-Allow-Origin";
const ACCESS_CONTROL_ALLOW_METHODS: &str = "Access-Control-Allow-Methods";
const ACCESS_CONTROL_ALLOW_HEADERS: &str = "Access-Control-Allow-Headers";
const ACCESS_CONTROL_EXPOSE_HEADERS: &str = "Access-Control-Expose-Headers";
const ACCESS_CONTROL_ALLOW_CREDENTIALS: &str = "Access-Control-Allow-Credentials";

const COMMA: &str = ", ";

pub trait CorsHeader {
    fn add(&self) -> Result<Option<(String, String)>, CorsError>;
}

///
/// Adds Access-Control-Allow-Origin header
///
pub struct AllowOrigin<'a> {
    origin: &'a str,
}

impl<'a> AllowOrigin<'a> {
    pub(crate) fn new(origin: &'a str) -> Self {
        Self { origin }
    }
}

impl CorsHeader for AllowOrigin<'_> {
    fn add(&self) -> Result<Option<(String, String)>, CorsError> {
        Ok(Some((
            ACCESS_CONTROL_ALLOW_ORIGIN.to_string(),
            self.origin.to_string(),
        )))
    }
}

///
/// Adds Access-Control-Expose-Headers header.
///
/// If the expose headers configuration is not configured, it will return an empty value.
/// Otherwise it will return all headers appended.
///
pub struct ExposeHeaders<'a> {
    origin_group: &'a OriginGroup<'a>,
}

impl<'a> ExposeHeaders<'a> {
    pub(crate) fn new(origin_group: &'a OriginGroup) -> Self {
        Self { origin_group }
    }
}

impl CorsHeader for ExposeHeaders<'_> {
    fn add(&self) -> Result<Option<(String, String)>, CorsError> {
        match self.origin_group.exposed_headers() {
            None => Ok(None),
            Some(headers) => {
                logger::debug!("Adding Access-Control-Expose-Headers into the request.");
                let result = headers.join(COMMA);
                Ok(Some((ACCESS_CONTROL_EXPOSE_HEADERS.to_string(), result)))
            }
        }
    }
}

///
/// Adds Access-Control-Max-Age header.
///
/// This header MUST be present only in Preflight requests.
///
pub struct MaxAge<'a> {
    origin_group: &'a OriginGroup<'a>,
}

impl<'a> MaxAge<'a> {
    pub(crate) fn new(origin_group: &'a OriginGroup) -> Self {
        Self { origin_group }
    }
}

impl CorsHeader for MaxAge<'_> {
    fn add(&self) -> Result<Option<(String, String)>, CorsError> {
        Ok(Some((
            ACCESS_CONTROL_MAX_AGE.to_string(),
            self.origin_group.max_age().to_string(),
        )))
    }
}

///
/// Adds Access-Control-Allow-Headers header.
///
/// This header is exclusively for Preflight requests, only when headers are requested for
/// making the Main request.
///
pub struct AllowHeaders<'a> {
    origin_group: &'a OriginGroup<'a>,
    headers: &'a [String],
}

impl<'a> AllowHeaders<'a> {
    pub(crate) fn new(origin_group: &'a OriginGroup<'a>, headers: &'a [String]) -> Self {
        Self {
            origin_group,
            headers,
        }
    }
}

impl CorsHeader for AllowHeaders<'_> {
    fn add(&self) -> Result<Option<(String, String)>, CorsError> {
        let headers_are_allowed = self.origin_group.headers_are_allowed(self.headers);

        if !headers_are_allowed {
            logger::debug!("Some of the headers in {:?} are not allowed.", self.headers);
            return Err(CorsError::HeaderNotAllowed);
        } else {
            logger::debug!("All headers are valid: {:?}", self.headers);
            if !self.headers.is_empty() {
                logger::debug!("Adding headers");
                return Ok(Some((
                    ACCESS_CONTROL_ALLOW_HEADERS.to_string(),
                    self.headers.join(COMMA),
                )));
            }
        }

        Ok(None)
    }
}

///
/// Adds Access-Control-Max-Age header.
///
/// This header is exclusively for Preflight requests.
///
pub struct AllowMethods<'a> {
    origin_group: &'a OriginGroup<'a>,
    method: &'a str,
}

impl<'a> AllowMethods<'a> {
    pub(crate) fn new(origin_group: &'a OriginGroup, method: &'a str) -> Self {
        Self {
            origin_group,
            method,
        }
    }
}

impl CorsHeader for AllowMethods<'_> {
    fn add(&self) -> Result<Option<(String, String)>, CorsError> {
        let method_is_allowed = self.origin_group.method_is_allowed(self.method);

        if !method_is_allowed {
            logger::debug!("Method {} is not allowed.", self.method);
            Err(CorsError::MethodNotAllowed)
        } else {
            logger::debug!("Method {} is allowed.", self.method);
            Ok(Some((
                ACCESS_CONTROL_ALLOW_METHODS.to_string(),
                self.method.to_uppercase(),
            )))
        }
    }
}

///
/// Adds the Access-Control-Allow-Credentials if it is configured.
///
pub struct AllowCredentials {
    allows_credentials: bool,
}

impl AllowCredentials {
    pub(crate) fn new(allows_credentials: bool) -> Self {
        Self { allows_credentials }
    }
}

impl CorsHeader for AllowCredentials {
    fn add(&self) -> Result<Option<(String, String)>, CorsError> {
        match self.allows_credentials {
            true => Ok(Some((
                ACCESS_CONTROL_ALLOW_CREDENTIALS.to_string(),
                "true".to_string(),
            ))),
            false => Ok(None),
        }
    }
}