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.

use std::sync::RwLock;

use super::LogLevel;
use super::interface::{Indexed, LogStorage, LogWriter, Loggable};
use super::memory_logger::MemoryLogger;
use super::nop_logger::NopLogger;
use super::stdout_logger::StdOutLogger;
use crate::app_types::{ApplicationName, PdpID};
use crate::bootstrap_config::log_config::{LogConfig, LogTypeConfig};
use crate::lock::AuditItem;
use crate::lock::LockService;
use crate::lock::health_registry::HealthRegistry;
use crate::log::BaseLogEntry;
use crate::log::loggable_fn::LoggableFn;
use serde::Serialize;

pub(crate) struct LogStrategy {
    logger: LogStrategyLogger,
    pdp_id: PdpID,
    app_name: Option<ApplicationName>,
    lock_service: RwLock<Option<LockService>>,
    log_level: Option<LogLevel>,
}

/// [`LogStrategy`] implements strategy pattern for logging.
/// It is used to provide a single point of access for logging and same api for different loggers.
pub(crate) enum LogStrategyLogger {
    Off(NopLogger),
    MemoryLogger(Box<MemoryLogger>),
    StdOut(StdOutLogger),
}

impl LogStrategy {
    /// Creates a new [`LogStrategy`] based on the provided configuration.
    /// Initializes the corresponding logger accordingly.
    pub(crate) fn new(
        config: &LogConfig,
        pdp_id: PdpID,
        app_name: Option<ApplicationName>,
    ) -> Self {
        let logger = match &config.log_type {
            LogTypeConfig::Off => LogStrategyLogger::Off(NopLogger),
            LogTypeConfig::Memory(memory_config) => LogStrategyLogger::MemoryLogger(Box::new(
                MemoryLogger::new(*memory_config, config.log_level, pdp_id, app_name.clone()),
            )),
            LogTypeConfig::StdOut(stdout_config) => {
                let logger = StdOutLogger::new(config.log_level, *stdout_config);
                LogStrategyLogger::StdOut(logger)
            },
        };
        Self {
            logger,
            pdp_id,
            app_name,
            lock_service: RwLock::new(None),
            log_level: Some(config.log_level),
        }
    }

    // is used as fallback for memory logger if error happens
    pub(super) fn new_with_logger(
        logger: LogStrategyLogger,
        pdp_id: PdpID,
        app_name: Option<ApplicationName>,
        lock_service: Option<LockService>,
    ) -> Self {
        Self {
            logger,
            pdp_id,
            app_name,
            lock_service: RwLock::new(lock_service),
            log_level: None,
        }
    }

    #[cfg(test)]
    pub(super) fn logger(&self) -> &LogStrategyLogger {
        &self.logger
    }

    /// Returns `true` if any sink would accept a log at the given level.
    /// Use to short-circuit expensive entry construction when logging is off.
    fn would_log(&self, entry_level: Option<LogLevel>) -> bool {
        let has_lock = self
            .lock_service
            .read()
            .expect("obtain lock_service read lock")
            .is_some();
        if has_lock {
            return true;
        }

        // If logger is Off and no lock_service, nothing will consume this entry
        if matches!(self.logger, LogStrategyLogger::Off(_)) {
            return false;
        }

        if let Some(logger_level) = &self.log_level
            && let Some(entry_log_level) = entry_level
        {
            return *logger_level <= entry_log_level;
        }

        true
    }

    fn log_entry<T: Loggable>(&self, entry: T) {
        let entry =
            LogEntryWithClientInfo::from_loggable(entry, self.pdp_id, self.app_name.clone());
        if let Some(lock_service) = self
            .lock_service
            .read()
            .expect("obtain lock_service read lock")
            .as_ref()
            && let Some(payload) = entry.to_audit_payload()
        {
            let item = AuditItem {
                payload,
                pdp_id: self.pdp_id,
                app_name: self.app_name.clone(),
                status: None,
            };
            lock_service.dispatch_audit(item);
        }
        match &self.logger {
            LogStrategyLogger::Off(log) => log.log_any(entry),
            LogStrategyLogger::MemoryLogger(memory_logger) => memory_logger.log_any(entry),
            LogStrategyLogger::StdOut(std_out_logger) => std_out_logger.log_any(entry),
        }
    }

    fn log_fn<F, R>(&self, log_fn: LoggableFn<F>)
    where
        R: Loggable + Indexed,
        for<'a> F: Fn(BaseLogEntry) -> R,
    {
        if !self.would_log(log_fn.get_log_level()) {
            return;
        }

        if let Some(logger_level) = &self.log_level {
            if log_fn.can_log(*logger_level) {
                let entry = log_fn.build();
                self.log_entry(entry);
            }
        } else {
            // this case happens only when call `new_with_logger` method
            let entry = log_fn.build();
            self.log_entry(entry);
        }
    }

    pub(crate) fn set_lock_service(&self, lock_service: LockService) {
        *self
            .lock_service
            .write()
            .expect("obtain lock_service write lock") = Some(lock_service);
    }

    pub(crate) fn health_registry(&self) -> Option<HealthRegistry> {
        self.lock_service
            .read()
            .expect("obtain lock_service read lock")
            .as_ref()
            .and_then(|ls| ls.health_registry())
            .cloned()
    }

    pub(crate) async fn shut_down(&self) {
        let lock = self
            .lock_service
            .write()
            .expect("obtain lock_service write lock")
            .take();

        if let Some(mut lock_service) = lock {
            lock_service.shut_down().await;
        }
    }
}

#[derive(Serialize, PartialEq, Clone)]
pub(crate) struct LogEntryWithClientInfo<Entry: Loggable> {
    #[serde(flatten)]
    entry: Entry,
    pdp_id: PdpID,
    #[serde(rename = "application_id", skip_serializing_if = "Option::is_none")]
    app_name: Option<ApplicationName>,
}

impl<Entry: Loggable> Indexed for LogEntryWithClientInfo<Entry> {
    fn get_id(&self) -> uuid7::Uuid {
        self.entry.get_id()
    }

    fn get_additional_ids(&self) -> Vec<uuid7::Uuid> {
        self.entry.get_additional_ids()
    }

    fn get_tags(&self) -> Vec<&str> {
        self.entry.get_tags()
    }
}

impl<Entry: Loggable + Indexed> Loggable for LogEntryWithClientInfo<Entry> {
    fn get_log_level(&self) -> Option<super::LogLevel> {
        self.entry.get_log_level()
    }

    fn to_audit_payload(&self) -> Option<crate::lock::AuditPayload> {
        self.entry.to_audit_payload()
    }
}

// Implementation of LogWriter
impl LogWriter for LogStrategy {
    fn log_any<T: Loggable>(&self, entry: T) {
        if !self.would_log(entry.get_log_level()) {
            return;
        }
        self.log_entry(entry);
    }

    fn log_fn<F, R>(&self, log_fn: LoggableFn<F>)
    where
        R: Loggable,
        F: Fn(super::BaseLogEntry) -> R,
    {
        self.log_fn(log_fn);
    }
}

impl<E: Loggable> LogEntryWithClientInfo<E> {
    pub(crate) fn from_loggable(
        entry: E,
        pdp_id: PdpID,
        app_name: Option<ApplicationName>,
    ) -> Self {
        Self {
            entry,
            pdp_id,
            app_name,
        }
    }
}

// Implementation of LogStorage
// for cases where we not use memory logger we return default value
impl LogStorage for LogStrategy {
    fn pop_logs(&self) -> Vec<serde_json::Value> {
        match &self.logger {
            LogStrategyLogger::MemoryLogger(memory_logger) => memory_logger.pop_logs(),
            _ => Vec::new(),
        }
    }

    fn get_log_by_id(&self, id: &str) -> Option<serde_json::Value> {
        match &self.logger {
            LogStrategyLogger::MemoryLogger(memory_logger) => memory_logger.get_log_by_id(id),
            _ => None,
        }
    }

    fn get_log_ids(&self) -> Vec<String> {
        match &self.logger {
            LogStrategyLogger::MemoryLogger(memory_logger) => memory_logger.get_log_ids(),
            _ => Vec::new(),
        }
    }

    fn get_logs_by_tag(&self, tag: &str) -> Vec<serde_json::Value> {
        match &self.logger {
            LogStrategyLogger::MemoryLogger(memory_logger) => memory_logger.get_logs_by_tag(tag),
            _ => Vec::new(),
        }
    }

    fn get_logs_by_request_id(&self, request_id: &str) -> Vec<serde_json::Value> {
        match &self.logger {
            LogStrategyLogger::MemoryLogger(memory_logger) => {
                memory_logger.get_logs_by_request_id(request_id)
            },
            // always empty vector for not MemoryLogger
            _ => Vec::new(),
        }
    }

    fn get_logs_by_request_id_and_tag(&self, id: &str, tag: &str) -> Vec<serde_json::Value> {
        match &self.logger {
            LogStrategyLogger::MemoryLogger(memory_logger) => {
                memory_logger.get_logs_by_request_id_and_tag(id, tag)
            },
            _ => Vec::new(),
        }
    }
}