From 19296b77b8b792ecc8f51cc3417d03fa88ca1545 Mon Sep 17 00:00:00 2001 From: Oleh Date: Thu, 12 Dec 2024 19:40:15 +0200 Subject: [PATCH] feat (jans-cedarling): Add CEDARLING_LOG_LEVEL bootstrap property (#10402) * feat(jans-cedarling): add bootstrap property `CEDARLING_LOG_LEVEL` Signed-off-by: Oleh Bohzok * feat(jans-cedarling): add log level filtering Signed-off-by: Oleh Bohzok * chore(jans-cedarling): add logging level to entry logs Signed-off-by: Oleh Bohzok * chore(jans-cedarling): fix clippy issues Signed-off-by: Oleh Bohzok * chore(jans-cedarling): remove logging log level if field is None Signed-off-by: Oleh Bohzok * chore(jans-cedarling): update python config to log only info level logs and some user and workload claims Signed-off-by: Oleh Bohzok * chore(jans-cedarling): fix bootstrap config serde name `CEDARLING_DECISION_LOG_USER_CLAIMS` Signed-off-by: Oleh Bohzok * chore(jans-cedarling): update DecisionLogEntry, added macro to skip serialize some fields if empty Signed-off-by: Oleh Bohzok * chore(jans-cedarling): add logging level for log message with cedar version Signed-off-by: Oleh Bohzok * chore(jans-cedarling): change log message `configuration parsed successfully` to debug level Signed-off-by: Oleh Bohzok * docs(jans-cedarling): update documentation Signed-off-by: Oleh Bohzok * fix(jans-cedarling): fix error deserialize log level from python with error `invalid type: string "DEBUG", expected a borrowed string` Signed-off-by: Oleh Bohzok --------- Signed-off-by: Oleh Bohzok --- docs/cedarling/cedarling-logs.md | 22 ++++---- docs/cedarling/cedarling-properties.md | 11 ++++ docs/cedarling/python/usage.md | 6 +- .../example_files/sample_bootstrap_props.yaml | 3 + .../src/config/bootstrap_config.rs | 4 +- .../bindings/cedarling_python/tests/config.py | 5 +- .../examples/authorize_with_jwt_validation.rs | 3 +- .../authorize_without_jwt_validation.rs | 3 +- jans-cedarling/cedarling/examples/log_init.rs | 7 ++- jans-cedarling/cedarling/src/authz/mod.rs | 12 ++-- .../cedarling/src/bootstrap_config/decode.rs | 22 +++++--- .../src/bootstrap_config/log_config.rs | 6 ++ .../cedarling/src/bootstrap_config/mod.rs | 2 + jans-cedarling/cedarling/src/lib.rs | 4 +- jans-cedarling/cedarling/src/log/interface.rs | 25 +++++++++ jans-cedarling/cedarling/src/log/log_entry.rs | 35 +++++++++++- jans-cedarling/cedarling/src/log/log_level.rs | 56 +++++++++++++++++++ .../cedarling/src/log/log_strategy.rs | 6 +- .../cedarling/src/log/memory_logger.rs | 12 +++- jans-cedarling/cedarling/src/log/mod.rs | 2 + .../cedarling/src/log/stdout_logger.rs | 19 +++++-- jans-cedarling/cedarling/src/log/test.rs | 6 +- .../src/tests/utils/cedarling_util.rs | 2 + .../test_files/bootstrap_props.json | 3 +- .../test_files/bootstrap_props.yaml | 1 + 25 files changed, 230 insertions(+), 47 deletions(-) create mode 100644 jans-cedarling/cedarling/src/log/log_level.rs diff --git a/docs/cedarling/cedarling-logs.md b/docs/cedarling/cedarling-logs.md index 91c45029c56..05cf27bf645 100644 --- a/docs/cedarling/cedarling-logs.md +++ b/docs/cedarling/cedarling-logs.md @@ -30,7 +30,6 @@ There are three different log records produced by the Cedarling: * `System` - Startup, debug and other Cedarling messages not related to authz * `Metric`- Performance and usage data - ### System Log Levels This is set by `CEDARLING_LOG_LEVEL` @@ -57,22 +56,24 @@ The JSON in this document is formatted for readability but is not prettified in ```json { - "id": "01937015-462d-7727-b789-ed95f7faf7a4", - "time": 1732752262, + "request_id": "0193b8a8-efc0-77ce-bd90-4a62a2998462", + "timestamp": "2024-12-12T04:18:19.456Z", "log_kind": "System", - "pdp_id": "75f0dc93-0a90-4076-95fa-dc16d3f00375", + "pdp_id": "d47e245e-beaa-4ea4-b899-b8184cd3eb7e", + "level": "DEBUG", "msg": "configuration parsed successfully" } { - "id": "01937015-462f-7cb5-86bb-d06c56dc5ab3", - "time": 1732752262, + "request_id": "0193b8a8-efc1-7e42-9678-b2480268b91f", + "timestamp": "2024-12-12T04:18:19.457Z", "log_kind": "System", - "pdp_id": "75f0dc93-0a90-4076-95fa-dc16d3f00375", + "pdp_id": "d47e245e-beaa-4ea4-b899-b8184cd3eb7e", + "level": "INFO", "msg": "Cedarling Authz initialized successfully", - "application_id": "TestApp", + "application_id": "My App", "cedar_lang_version": "4.1.0", "cedar_sdk_version": "4.2.2" -} +} ``` ### Decision Log @@ -138,7 +139,8 @@ The result of the authorization is quite extensive because we log all `cedar-pol { "id": "01937015-4649-7aad-8df8-4976e4bd8565", "time": 1732752262, - "log_kind": "Decision", + "log_type": "Decision", + "level": "DEBUG", "pdp_id": "75f0dc93-0a90-4076-95fa-dc16d3f00375", "msg": "Result of authorize.", "application_id": "TestApp", diff --git a/docs/cedarling/cedarling-properties.md b/docs/cedarling/cedarling-properties.md index 463318810a2..12d69d721cb 100644 --- a/docs/cedarling/cedarling-properties.md +++ b/docs/cedarling/cedarling-properties.md @@ -29,6 +29,13 @@ These Bootstrap Properties control default application level behavior. * **`CEDARLING_LOG_STORAGE`** : `off`, `memory`, `std_out` * **`CEDARLING_LOG_LEVEL`** : System Log Level [See here](./cedarling-logs.md). Default to `WARN` * **`CEDARLING_LOG_STDOUT_TYPE`** : Either `System`, `Metric`, or `Decision`. Default to System. +* **`CEDARLING_LOG_LEVEL`** : Log level filter for logging. Log level has only `System` log type entries. `TRACE` is lowest. `FATAL` is highest. Possible variants: + * FATAL + * ERROR + * WARN + * INFO + * DEBUG + * TRACE * **`CEDARLING_DECISION_LOG_USER_CLAIMS`** : List of claims to map from user entity, such as ["sub", "email", "username", ...] * **`CEDARLING_DECISION_LOG_WORKLOAD_CLAIMS`** : List of claims to map from user entity, such as ["client_id", "rp_id", ...] * **`CEDARLING_DECISION_LOG_DEFAULT_JWT_ID`** : Token claims that will be used for decision logging. Default is "jti", but perhaps some other claim is needed. @@ -134,6 +141,7 @@ Below is an example of a bootstrap config in JSON format. Not all fields should "CEDARLING_POLICY_STORE_URI": "", "CEDARLING_POLICY_STORE_ID": "840da5d85403f35ea76519ed1a18a33989f855bf1cf8", "CEDARLING_LOG_TYPE": "memory", + "CEDARLING_LOG_LEVEL": "INFO", "CEDARLING_DECISION_LOG_USER_CLAIMS": ["sub", "email", "username"], "CEDARLING_DECISION_LOG_WORKLOAD_CLAIMS": ["client_id", "rp_id"], "CEDARLING_DECISION_LOG_DEFAULT_JWT_ID": "jti", @@ -207,6 +215,9 @@ CEDARLING_APPLICATION_NAME: My App CEDARLING_POLICY_STORE_URI: '' CEDARLING_POLICY_STORE_ID: '840da5d85403f35ea76519ed1a18a33989f855bf1cf8' CEDARLING_LOG_TYPE: 'memory' +CEDARLING_LOG_LEVEL: 'INFO' +CEDARLING_DECISION_LOG_USER_CLAIMS: ["sub","email"] +CEDARLING_DECISION_LOG_WORKLOAD_CLAIMS: ["client_id", "rp_id"] CEDARLING_LOG_TTL: 60 CEDARLING_USER_AUTHZ: 'enabled' CEDARLING_WORKLOAD_AUTHZ: 'enabled' diff --git a/docs/cedarling/python/usage.md b/docs/cedarling/python/usage.md index 167a955168b..9b531d569e0 100644 --- a/docs/cedarling/python/usage.md +++ b/docs/cedarling/python/usage.md @@ -19,9 +19,9 @@ In this example, we will show an example Python script that calls the `cedarling Policy store location not provided, use 'CEDARLING_LOCAL_POLICY_STORE' environment variable Used default policy store path: example_files/policy-store.json -{"id":"0193414e-9672-786a-986c-57f48d41c4e4","time":1731967489,"log_kind":"System","pdp_id":"c0ec33ff-9482-4bdc-83f6-2925a41a3280","msg":"configuration parsed successfully"} -{"id":"0193414e-9672-786a-986c-57f5379086c3","time":1731967489,"log_kind":"System","pdp_id":"c0ec33ff-9482-4bdc-83f6-2925a41a3280","msg":"Cedarling Authz initialized successfully","application_id":"TestApp"} -{"id":"0193414e-9676-7d8a-b55b-3f0097355851","time":1731967489,"log_kind":"Decision","pdp_id":"c0ec33ff-9482-4bdc-83f6-2925a41a3280","msg":"Result of authorize.","application_id":"TestApp","action":"Jans::Action::\"Read\"","resource":"Jans::Application::\"some_id\"","context":{"user_agent":"Linux","operating_system":"Linux","network_type":"Local","network":"127.0.0.1","geolocation":["America"],"fraud_indicators":["Allowed"],"device_health":["Healthy"],"current_time":1731967489},"person_principal":"Jans::User::\"qzxn1Scrb9lWtGxVedMCky-Ql_ILspZaQA6fyuYktw0\"","person_diagnostics":{"reason":["840da5d85403f35ea76519ed1a18a33989f855bf1cf8"],"errors":[]},"person_decision":"ALLOW","workload_principal":"Jans::Workload::\"d7f71bea-c38d-4caf-a1ba-e43c74a11a62\"","workload_diagnostics":{"reason":["444da5d85403f35ea76519ed1a18a33989f855bf1cf8"],"errors":[]},"workload_decision":"ALLOW","authorized":true} +{"id":"0193414e-9672-786a-986c-57f48d41c4e4","time":1731967489,"log_type":"System","pdp_id":"c0ec33ff-9482-4bdc-83f6-2925a41a3280","msg":"configuration parsed successfully"} +{"id":"0193414e-9672-786a-986c-57f5379086c3","time":1731967489,"log_type":"System","pdp_id":"c0ec33ff-9482-4bdc-83f6-2925a41a3280","msg":"Cedarling Authz initialized successfully","application_id":"TestApp"} +{"id":"0193414e-9676-7d8a-b55b-3f0097355851","time":1731967489,"log_type":"Decision","pdp_id":"c0ec33ff-9482-4bdc-83f6-2925a41a3280","msg":"Result of authorize.","application_id":"TestApp","action":"Jans::Action::\"Read\"","resource":"Jans::Application::\"some_id\"","context":{"user_agent":"Linux","operating_system":"Linux","network_type":"Local","network":"127.0.0.1","geolocation":["America"],"fraud_indicators":["Allowed"],"device_health":["Healthy"],"current_time":1731967489},"person_principal":"Jans::User::\"qzxn1Scrb9lWtGxVedMCky-Ql_ILspZaQA6fyuYktw0\"","person_diagnostics":{"reason":["840da5d85403f35ea76519ed1a18a33989f855bf1cf8"],"errors":[]},"person_decision":"ALLOW","workload_principal":"Jans::Workload::\"d7f71bea-c38d-4caf-a1ba-e43c74a11a62\"","workload_diagnostics":{"reason":["444da5d85403f35ea76519ed1a18a33989f855bf1cf8"],"errors":[]},"workload_decision":"ALLOW","authorized":true} Result of workload authorization: ALLOW Policy ID used: 444da5d85403f35ea76519ed1a18a33989f855bf1cf8 diff --git a/jans-cedarling/bindings/cedarling_python/example_files/sample_bootstrap_props.yaml b/jans-cedarling/bindings/cedarling_python/example_files/sample_bootstrap_props.yaml index 4331ca8f837..e200e0e984e 100644 --- a/jans-cedarling/bindings/cedarling_python/example_files/sample_bootstrap_props.yaml +++ b/jans-cedarling/bindings/cedarling_python/example_files/sample_bootstrap_props.yaml @@ -6,7 +6,10 @@ CEDARLING_APPLICATION_NAME: My App CEDARLING_POLICY_STORE_URI: null CEDARLING_POLICY_STORE_ID: gICAgcHJpbmNpcGFsIGlz CEDARLING_LOG_TYPE: std_out +CEDARLING_LOG_LEVEL: INFO CEDARLING_LOG_TTL: null +CEDARLING_DECISION_LOG_USER_CLAIMS: ["sub","email"] +CEDARLING_DECISION_LOG_WORKLOAD_CLAIMS: ["client_id", "rp_id"] CEDARLING_USER_AUTHZ: enabled CEDARLING_WORKLOAD_AUTHZ: enabled CEDARLING_USER_WORKLOAD_BOOLEAN_OPERATION: AND diff --git a/jans-cedarling/bindings/cedarling_python/src/config/bootstrap_config.rs b/jans-cedarling/bindings/cedarling_python/src/config/bootstrap_config.rs index 017bb62b182..0867aab9287 100644 --- a/jans-cedarling/bindings/cedarling_python/src/config/bootstrap_config.rs +++ b/jans-cedarling/bindings/cedarling_python/src/config/bootstrap_config.rs @@ -47,8 +47,8 @@ pub struct BootstrapConfig { #[pymethods] impl BootstrapConfig { #[new] - pub fn new(options: &Bound<'_, PyDict>) -> PyResult { - let source: cedarling::BootstrapConfigRaw = serde_pyobject::from_pyobject(options.clone()) + pub fn new(options: Bound<'_, PyDict>) -> PyResult { + let source: cedarling::BootstrapConfigRaw = serde_pyobject::from_pyobject(options) .map_err(|e| PyValueError::new_err(e.to_string()))?; let inner = cedarling::BootstrapConfig::from_raw_config(&source) .map_err(|e| PyValueError::new_err(e.to_string()))?; diff --git a/jans-cedarling/bindings/cedarling_python/tests/config.py b/jans-cedarling/bindings/cedarling_python/tests/config.py index 4444d2a1eab..0e3cbfe6039 100644 --- a/jans-cedarling/bindings/cedarling_python/tests/config.py +++ b/jans-cedarling/bindings/cedarling_python/tests/config.py @@ -8,6 +8,7 @@ # The human-readable policy and schema file is located in next folder: # `test_files\policy-store_ok` + def load_bootstrap_config(policy_store_location=None, log_type="std_out", log_ttl=None): """ Loads the bootstrap configuration with predefined settings. @@ -15,7 +16,8 @@ def load_bootstrap_config(policy_store_location=None, log_type="std_out", log_tt """ if policy_store_location is None: - policy_store_location = os.path.join(TEST_FILES_PATH, "policy-store_ok.yaml") + policy_store_location = os.path.join( + TEST_FILES_PATH, "policy-store_ok.yaml") return BootstrapConfig({ "CEDARLING_APPLICATION_NAME": "TestApp", @@ -42,4 +44,5 @@ def load_bootstrap_config(policy_store_location=None, log_type="std_out", log_tt "CEDARLING_ID_TOKEN_TRUST_MODE": "none", "CEDARLING_LOG_TYPE": log_type, "CEDARLING_LOG_TTL": log_ttl, + "CEDARLING_LOG_LEVEL": "DEBUG", }) diff --git a/jans-cedarling/cedarling/examples/authorize_with_jwt_validation.rs b/jans-cedarling/cedarling/examples/authorize_with_jwt_validation.rs index 06af6fad5e2..af9c34801d5 100644 --- a/jans-cedarling/cedarling/examples/authorize_with_jwt_validation.rs +++ b/jans-cedarling/cedarling/examples/authorize_with_jwt_validation.rs @@ -7,7 +7,7 @@ use cedarling::{ AuthorizationConfig, BootstrapConfig, Cedarling, IdTokenTrustMode, JwtConfig, LogConfig, - LogTypeConfig, PolicyStoreConfig, PolicyStoreSource, Request, ResourceData, + LogLevel, LogTypeConfig, PolicyStoreConfig, PolicyStoreSource, Request, ResourceData, TokenValidationConfig, WorkloadBoolOp, }; use jsonwebtoken::Algorithm; @@ -43,6 +43,7 @@ fn main() -> Result<(), Box> { application_name: "test_app".to_string(), log_config: LogConfig { log_type: LogTypeConfig::StdOut, + log_level: LogLevel::INFO, }, policy_store_config: PolicyStoreConfig { source: PolicyStoreSource::Yaml(POLICY_STORE_RAW_YAML.to_string()), diff --git a/jans-cedarling/cedarling/examples/authorize_without_jwt_validation.rs b/jans-cedarling/cedarling/examples/authorize_without_jwt_validation.rs index 85f716e017b..c7aad3b4a09 100644 --- a/jans-cedarling/cedarling/examples/authorize_without_jwt_validation.rs +++ b/jans-cedarling/cedarling/examples/authorize_without_jwt_validation.rs @@ -6,7 +6,7 @@ */ use cedarling::{ - AuthorizationConfig, BootstrapConfig, Cedarling, JwtConfig, LogConfig, LogTypeConfig, + AuthorizationConfig, BootstrapConfig, Cedarling, JwtConfig, LogConfig, LogLevel, LogTypeConfig, PolicyStoreConfig, PolicyStoreSource, Request, ResourceData, WorkloadBoolOp, }; use std::collections::HashMap; @@ -18,6 +18,7 @@ fn main() -> Result<(), Box> { application_name: "test_app".to_string(), log_config: LogConfig { log_type: LogTypeConfig::StdOut, + log_level: LogLevel::INFO, }, policy_store_config: PolicyStoreConfig { source: PolicyStoreSource::Yaml(POLICY_STORE_RAW.to_string()), diff --git a/jans-cedarling/cedarling/examples/log_init.rs b/jans-cedarling/cedarling/examples/log_init.rs index 33749c4c530..8954ab522f7 100644 --- a/jans-cedarling/cedarling/examples/log_init.rs +++ b/jans-cedarling/cedarling/examples/log_init.rs @@ -12,7 +12,7 @@ #![cfg(not(target_family = "wasm"))] use cedarling::{ - AuthorizationConfig, BootstrapConfig, Cedarling, JwtConfig, LogConfig, LogStorage, + AuthorizationConfig, BootstrapConfig, Cedarling, JwtConfig, LogConfig, LogLevel, LogStorage, LogTypeConfig, MemoryLogConfig, PolicyStoreConfig, PolicyStoreSource, WorkloadBoolOp, }; use std::env; @@ -48,7 +48,10 @@ fn main() -> Result<(), Box> { println!("Cedarling initialized with log type: {:?}", log_type); let cedarling = Cedarling::new(&BootstrapConfig { application_name: "test_app".to_string(), - log_config: LogConfig { log_type }, + log_config: LogConfig { + log_type, + log_level: LogLevel::INFO, + }, policy_store_config: PolicyStoreConfig { source: PolicyStoreSource::Yaml(POLICY_STORE_RAW.to_string()), }, diff --git a/jans-cedarling/cedarling/src/authz/mod.rs b/jans-cedarling/cedarling/src/authz/mod.rs index d514f8abf8a..425699b5028 100644 --- a/jans-cedarling/cedarling/src/authz/mod.rs +++ b/jans-cedarling/cedarling/src/authz/mod.rs @@ -16,7 +16,7 @@ use std::sync::Arc; use crate::bootstrap_config::AuthorizationConfig; use crate::common::app_types; use crate::common::policy_store::PolicyStoreWithID; -use crate::jwt; +use crate::{jwt, LogLevel}; use crate::log::interface::LogWriter; use crate::log::{ AuthorizationLogInfo, BaseLogEntry, DecisionLogEntry, Diagnostics, LogEntry, LogTokensInfo, LogType, Logger, PersonAuthorizeInfo, PrincipalLogEntry, WorkloadAuthorizeInfo @@ -72,7 +72,7 @@ impl Authz { Some(config.application_name.clone()), LogType::System, ) - .set_cedar_version() + .set_cedar_version().set_level(LogLevel::INFO) .set_message("Cedarling Authz initialized successfully".to_string()), ); @@ -187,8 +187,8 @@ impl Authz { LogEntry::new_with_data( self.config.pdp_id, Some(self.config.application_name.clone()), - LogType::Decision, - ) + LogType::System, + ).set_level(LogLevel::DEBUG) .set_auth_info(AuthorizationLogInfo { action: request.action.clone(), context: request.context.clone(), @@ -448,12 +448,12 @@ pub struct CreateRequestRoleError { /// Get entity claims from list in config // // To get claims we convert entity to json, because no other way to get introspection -fn get_entity_claims(decision_log_claims: &[String], entities: &Entities,principal_user_entity_uid: EntityUid) -> HashMap { +fn get_entity_claims(decision_log_claims: &[String], entities: &Entities, entity_uid: EntityUid) -> HashMap { HashMap::from_iter( decision_log_claims .iter() .filter_map(|claim_key| { entities - .get(&principal_user_entity_uid) + .get(&entity_uid) // convert entity to json and result to option .and_then(|entity| entity.to_json_value().ok()) // JSON structure of entity: diff --git a/jans-cedarling/cedarling/src/bootstrap_config/decode.rs b/jans-cedarling/cedarling/src/bootstrap_config/decode.rs index d48ec794503..206d9943d30 100644 --- a/jans-cedarling/cedarling/src/bootstrap_config/decode.rs +++ b/jans-cedarling/cedarling/src/bootstrap_config/decode.rs @@ -10,6 +10,7 @@ use super::{ IdTokenTrustMode, JwtConfig, LogConfig, LogTypeConfig, MemoryLogConfig, PolicyStoreConfig, PolicyStoreSource, TokenValidationConfig, }; +use crate::log::LogLevel; use jsonwebtoken::Algorithm; use serde::{Deserialize, Deserializer, Serialize}; use std::{collections::HashSet, fmt::Display, fs, path::Path, str::FromStr}; @@ -38,8 +39,17 @@ pub struct BootstrapConfigRaw { #[serde(rename = "CEDARLING_LOG_TYPE", default)] pub log_type: LoggerType, + /// Log level filter for logging. TRACE is lowest. FATAL is highest. + #[serde(rename = "CEDARLING_LOG_LEVEL", default)] + pub log_level: LogLevel, + + /// If `log_type` is set to [`LogType::Memory`], this is the TTL (time to live) of + /// log entities in seconds. + #[serde(rename = "CEDARLING_LOG_TTL", default)] + pub log_ttl: Option, + /// List of claims to map from user entity, such as ["sub", "email", "username", ...] - #[serde(rename = "CEDARLING_DECISION_LOG_USER_CLAIMS ", default)] + #[serde(rename = "CEDARLING_DECISION_LOG_USER_CLAIMS", default)] pub decision_log_user_claims: Vec, /// List of claims to map from user entity, such as ["client_id", "rp_id", ...] @@ -51,11 +61,6 @@ pub struct BootstrapConfigRaw { #[serde(rename = "CEDARLING_DECISION_LOG_DEFAULT_JWT_ID", default)] pub decision_log_default_jwt_id: String, - /// If `log_type` is set to [`LogType::Memory`], this is the TTL (time to live) of - /// log entities in seconds. - #[serde(rename = "CEDARLING_LOG_TTL", default)] - pub log_ttl: Option, - /// When `enabled`, Cedar engine authorization is queried for a User principal. #[serde(rename = "CEDARLING_USER_AUTHZ", default)] pub user_authz: FeatureToggle, @@ -433,7 +438,10 @@ impl BootstrapConfig { LoggerType::StdOut => LogTypeConfig::StdOut, LoggerType::Lock => LogTypeConfig::Lock, }; - let log_config = LogConfig { log_type }; + let log_config = LogConfig { + log_type, + log_level: raw.log_level, + }; // Decode policy store let policy_store_config = match ( diff --git a/jans-cedarling/cedarling/src/bootstrap_config/log_config.rs b/jans-cedarling/cedarling/src/bootstrap_config/log_config.rs index c9c1a817703..eaf6081a85c 100644 --- a/jans-cedarling/cedarling/src/bootstrap_config/log_config.rs +++ b/jans-cedarling/cedarling/src/bootstrap_config/log_config.rs @@ -5,11 +5,17 @@ * Copyright (c) 2024, Gluu, Inc. */ +use crate::log::LogLevel; + /// A set of properties used to configure logging in the `Cedarling` application. #[derive(Debug, Clone, PartialEq)] pub struct LogConfig { /// `CEDARLING_LOG_TYPE` in [bootstrap properties](https://github.com/JanssenProject/jans/wiki/Cedarling-Nativity-Plan#bootstrap-properties) documentation. pub log_type: LogTypeConfig, + + /// Log level filter for logging. TRACE is lowest. FATAL is highest. + /// `CEDARLING_LOG_LEVEL` in [bootstrap properties](https://github.com/JanssenProject/jans/wiki/Cedarling-Nativity-Plan#bootstrap-properties) documentation. + pub log_level: LogLevel, } /// Log type configuration. diff --git a/jans-cedarling/cedarling/src/bootstrap_config/mod.rs b/jans-cedarling/cedarling/src/bootstrap_config/mod.rs index 85e8e9cc84f..9a8332f1d43 100644 --- a/jans-cedarling/cedarling/src/bootstrap_config/mod.rs +++ b/jans-cedarling/cedarling/src/bootstrap_config/mod.rs @@ -157,6 +157,7 @@ mod test { application_name: "My App".to_string(), log_config: LogConfig { log_type: LogTypeConfig::StdOut, + log_level: crate::LogLevel::DEBUG, }, policy_store_config: PolicyStoreConfig { source: crate::PolicyStoreSource::FileJson( @@ -211,6 +212,7 @@ mod test { application_name: "My App".to_string(), log_config: LogConfig { log_type: LogTypeConfig::Memory(MemoryLogConfig { log_ttl: 60 }), + log_level: crate::LogLevel::DEBUG, }, policy_store_config: PolicyStoreConfig { source: crate::PolicyStoreSource::FileJson( diff --git a/jans-cedarling/cedarling/src/lib.rs b/jans-cedarling/cedarling/src/lib.rs index 76fcab35d96..4611528d7d3 100644 --- a/jans-cedarling/cedarling/src/lib.rs +++ b/jans-cedarling/cedarling/src/lib.rs @@ -39,8 +39,8 @@ use init::ServiceFactory; use common::app_types; use log::interface::LogWriter; use log::LogEntry; -pub use log::LogStorage; use log::LogType; +pub use log::{LogLevel, LogStorage}; pub use crate::authz::entities::CedarPolicyCreateTypeError; @@ -85,6 +85,7 @@ impl Cedarling { .inspect(|_| { log.log( LogEntry::new_with_data(pdp_id, None, LogType::System) + .set_level(LogLevel::DEBUG) .set_message("configuration parsed successfully".to_string()), ) }) @@ -92,6 +93,7 @@ impl Cedarling { log.log( LogEntry::new_with_data(pdp_id, None, LogType::System) .set_error(err.to_string()) + .set_level(LogLevel::ERROR) .set_message("configuration parsed with error".to_string()), ) })?; diff --git a/jans-cedarling/cedarling/src/log/interface.rs b/jans-cedarling/cedarling/src/log/interface.rs index 4d73365e0d4..498b915a0cb 100644 --- a/jans-cedarling/cedarling/src/log/interface.rs +++ b/jans-cedarling/cedarling/src/log/interface.rs @@ -9,6 +9,7 @@ //! Contains the interface for logging. And getting log information from storage. use super::LogEntry; +use super::LogLevel; use uuid7::Uuid; /// Log Writer @@ -24,7 +25,31 @@ pub(crate) trait LogWriter { } pub(crate) trait Loggable: serde::Serialize { + /// get unique request ID fn get_request_id(&self) -> Uuid; + /// get log level for entity + /// not all log entities have log level, only when `log_kind` == `System` + fn get_log_level(&self) -> Option; + + /// check if entry can log to logger + /// + // default implementation of method + // is used to avoid boilerplate code + fn can_log(&self, logger_level: LogLevel) -> bool { + if let Some(entry_log_level) = self.get_log_level() { + if entry_log_level < logger_level { + // entry log level lower than logger level + false + } else { + // entry log higher or equal than logger level + true + } + } else { + // if `.get_log_level` return None + // it means that `log_kind` != `System` and we should log it + true + } + } } /// Log Storage diff --git a/jans-cedarling/cedarling/src/log/log_entry.rs b/jans-cedarling/cedarling/src/log/log_entry.rs index d5fddde3547..b60f64404fd 100644 --- a/jans-cedarling/cedarling/src/log/log_entry.rs +++ b/jans-cedarling/cedarling/src/log/log_entry.rs @@ -22,6 +22,7 @@ use crate::common::app_types::{self, ApplicationName}; use crate::common::policy_store::PoliciesContainer; use super::interface::Loggable; +use super::LogLevel; /// ISO-8601 time format for [`chrono`] /// example: 2024-11-27T10:10:50.654Z @@ -94,12 +95,21 @@ impl LogEntry { self.cedar_sdk_version = Some(cedar_policy::get_sdk_version()); self } + + pub(crate) fn set_level(mut self, level: LogLevel) -> Self { + self.base.level = Some(level); + self + } } impl Loggable for LogEntry { fn get_request_id(&self) -> Uuid { self.base.get_request_id() } + + fn get_log_level(&self) -> Option { + self.base.get_log_level() + } } /// Type of log entry @@ -271,11 +281,14 @@ pub struct DecisionLogEntry<'a> { pub principal: PrincipalLogEntry, /// A list of claims, specified by the CEDARLING_DECISION_LOG_USER_CLAIMS property, that must be present in the Cedar User entity #[serde(rename = "User")] + #[serde(skip_serializing_if = "HashMap::is_empty")] pub user: HashMap, /// A list of claims, specified by the CEDARLING_DECISION_LOG_WORKLOAD_CLAIMS property, that must be present in the Cedar Workload entity #[serde(rename = "Workload")] + #[serde(skip_serializing_if = "HashMap::is_empty")] pub workload: HashMap, /// If this Cedarling has registered with a Lock Server, what is the client_id it received + #[serde(skip_serializing_if = "Option::is_none")] pub lock_client_id: Option, /// action UID for request pub action: String, @@ -293,6 +306,10 @@ impl Loggable for &DecisionLogEntry<'_> { fn get_request_id(&self) -> Uuid { self.base.get_request_id() } + + fn get_log_level(&self) -> Option { + self.base.get_log_level() + } } #[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] @@ -304,23 +321,33 @@ pub struct BaseLogEntry { /// or it is not specified in context pub timestamp: Option, /// kind of log entry - pub log_type: LogType, + pub log_kind: LogType, /// unique id of cedarling pub pdp_id: Uuid, + /// log level of entry + #[serde(skip_serializing_if = "Option::is_none")] + pub level: Option, } impl BaseLogEntry { pub(crate) fn new(pdp_id: app_types::PdpID, log_type: LogType) -> Self { let local_time_string = chrono::Local::now().format(ISO8601).to_string(); + let default_log_level = if log_type == LogType::System { + Some(LogLevel::TRACE) + } else { + None + }; + Self { // We use uuid v7 because it is generated based on the time and sortable. // and we need sortable ids to use it in the sparkv database. // Sparkv store data in BTree. So we need have correct order of ids. request_id: uuid7(), timestamp: Some(local_time_string), - log_type, + log_kind: log_type, pdp_id: pdp_id.0, + level: default_log_level, } } } @@ -329,6 +356,10 @@ impl Loggable for BaseLogEntry { fn get_request_id(&self) -> Uuid { self.request_id } + + fn get_log_level(&self) -> Option { + self.level + } } /// Describes what principal is was executed diff --git a/jans-cedarling/cedarling/src/log/log_level.rs b/jans-cedarling/cedarling/src/log/log_level.rs new file mode 100644 index 00000000000..e3b47e7d40f --- /dev/null +++ b/jans-cedarling/cedarling/src/log/log_level.rs @@ -0,0 +1,56 @@ +/* + * 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::str::FromStr; + +/// Log levels +/// Fatal level is the highest, trace is lowest +#[derive( + Debug, + PartialEq, + PartialOrd, + Eq, + Ord, + Copy, + Clone, + Default, + derive_more::Display, + Deserialize, + Serialize, +)] +pub enum LogLevel { + /// Fatal level + FATAL = 5, + /// Error level + ERROR = 4, + /// Warn level + #[default] + WARN = 3, + /// Info level + INFO = 2, + /// Debug level + DEBUG = 1, + /// Trace level + TRACE = 0, +} + +impl FromStr for LogLevel { + type Err = String; + + fn from_str(s: &str) -> Result { + match s { + "FATAL" => Ok(LogLevel::FATAL), + "ERROR" => Ok(LogLevel::ERROR), + "WARN" => Ok(LogLevel::WARN), + "INFO" => Ok(LogLevel::INFO), + "DEBUG" => Ok(LogLevel::DEBUG), + "TRACE" => Ok(LogLevel::TRACE), + _ => Err(format!("Invalid log level: {}", s)), + } + } +} diff --git a/jans-cedarling/cedarling/src/log/log_strategy.rs b/jans-cedarling/cedarling/src/log/log_strategy.rs index 964ffca6232..f3627a47f71 100644 --- a/jans-cedarling/cedarling/src/log/log_strategy.rs +++ b/jans-cedarling/cedarling/src/log/log_strategy.rs @@ -27,8 +27,10 @@ impl LogStrategy { pub fn new(config: &LogConfig) -> Self { match config.log_type { LogTypeConfig::Off => Self::Off(NopLogger), - LogTypeConfig::Memory(config) => Self::MemoryLogger(MemoryLogger::new(config)), - LogTypeConfig::StdOut => Self::StdOut(StdOutLogger::new()), + LogTypeConfig::Memory(memory_config) => { + Self::MemoryLogger(MemoryLogger::new(memory_config, config.log_level)) + }, + LogTypeConfig::StdOut => Self::StdOut(StdOutLogger::new(config.log_level)), LogTypeConfig::Lock => todo!(), } } diff --git a/jans-cedarling/cedarling/src/log/memory_logger.rs b/jans-cedarling/cedarling/src/log/memory_logger.rs index a35b4577f89..e3d771eb060 100644 --- a/jans-cedarling/cedarling/src/log/memory_logger.rs +++ b/jans-cedarling/cedarling/src/log/memory_logger.rs @@ -7,6 +7,7 @@ use super::interface::{LogStorage, LogWriter, Loggable}; use super::LogEntry; +use super::LogLevel; use crate::bootstrap_config::log_config::MemoryLogConfig; use sparkv::{Config as ConfigSparKV, SparKV}; use std::{sync::Mutex, time::Duration}; @@ -18,10 +19,11 @@ const STORAGE_JSON_PARSE_EXPECT_MESSAGE: &str = /// A logger that store logs in-memory. pub(crate) struct MemoryLogger { storage: Mutex, + log_level: LogLevel, } impl MemoryLogger { - pub fn new(config: MemoryLogConfig) -> Self { + pub fn new(config: MemoryLogConfig, log_level: LogLevel) -> Self { let sparkv_config = ConfigSparKV { default_ttl: Duration::from_secs(config.log_ttl), ..Default::default() @@ -29,6 +31,7 @@ impl MemoryLogger { MemoryLogger { storage: Mutex::new(SparKV::with_config(sparkv_config)), + log_level, } } } @@ -36,6 +39,11 @@ impl MemoryLogger { // Implementation of LogWriter impl LogWriter for MemoryLogger { fn log_any(&self, entry: T) { + if !entry.can_log(self.log_level) { + // do nothing + return; + } + let json_string = serde_json::json!(entry).to_string(); let result = self @@ -92,7 +100,7 @@ mod tests { fn create_memory_logger() -> MemoryLogger { let config = MemoryLogConfig { log_ttl: 60 }; - MemoryLogger::new(config) + MemoryLogger::new(config, LogLevel::TRACE) } #[test] diff --git a/jans-cedarling/cedarling/src/log/mod.rs b/jans-cedarling/cedarling/src/log/mod.rs index 4bb2da51940..a3d20f39582 100644 --- a/jans-cedarling/cedarling/src/log/mod.rs +++ b/jans-cedarling/cedarling/src/log/mod.rs @@ -53,12 +53,14 @@ pub mod interface; mod log_entry; +mod log_level; pub(crate) mod log_strategy; mod memory_logger; mod nop_logger; mod stdout_logger; pub use log_entry::*; +pub use log_level::*; #[cfg(test)] mod test; diff --git a/jans-cedarling/cedarling/src/log/stdout_logger.rs b/jans-cedarling/cedarling/src/log/stdout_logger.rs index 1914c06b627..b1b8c1748d6 100644 --- a/jans-cedarling/cedarling/src/log/stdout_logger.rs +++ b/jans-cedarling/cedarling/src/log/stdout_logger.rs @@ -5,7 +5,8 @@ * Copyright (c) 2024, Gluu, Inc. */ -use super::interface::LogWriter; +use super::interface::{LogWriter, Loggable}; +use super::LogLevel; use std::{ io::Write, sync::{Arc, Mutex}, @@ -15,28 +16,36 @@ use std::{ pub(crate) struct StdOutLogger { // we use `dyn Write`` trait to make it testable and mockable. writer: Mutex>, + log_level: LogLevel, } impl StdOutLogger { - pub(crate) fn new() -> Self { + pub(crate) fn new(log_level: LogLevel) -> Self { Self { writer: Mutex::new(Box::new(std::io::stdout())), + log_level, } } // Create a new StdOutLogger with custom writer. // is used in tests. #[allow(dead_code)] - pub(crate) fn new_with(writer: Box) -> Self { + pub(crate) fn new_with(writer: Box, log_level: LogLevel) -> Self { Self { writer: Mutex::new(Box::new(writer)), + log_level, } } } // Implementation of LogWriter impl LogWriter for StdOutLogger { - fn log_any(&self, entry: T) { + fn log_any(&self, entry: T) { + if !entry.can_log(self.log_level) { + // do nothing + return; + } + let json_str = serde_json::json!(&entry).to_string(); // we can't handle error here or test it so we just panic if it happens. // we should have specific platform to get error @@ -113,7 +122,7 @@ mod tests { let buffer = Box::new(test_writer.clone()) as Box; // Create logger with test writer - let logger = StdOutLogger::new_with(buffer); + let logger = StdOutLogger::new_with(buffer, LogLevel::TRACE); // Log the entry logger.log(log_entry); diff --git a/jans-cedarling/cedarling/src/log/test.rs b/jans-cedarling/cedarling/src/log/test.rs index 6dac75c3f25..17b3f3024bd 100644 --- a/jans-cedarling/cedarling/src/log/test.rs +++ b/jans-cedarling/cedarling/src/log/test.rs @@ -17,6 +17,7 @@ fn test_new_log_strategy_off() { // Arrange let config = LogConfig { log_type: log_config::LogTypeConfig::Off, + log_level: crate::LogLevel::DEBUG, }; // Act @@ -31,6 +32,7 @@ fn test_new_log_strategy_memory() { // Arrange let config = LogConfig { log_type: log_config::LogTypeConfig::Memory(log_config::MemoryLogConfig { log_ttl: 60 }), + log_level: crate::LogLevel::DEBUG, }; // Act @@ -45,6 +47,7 @@ fn test_new_logstrategy_stdout() { // Arrange let config = LogConfig { log_type: log_config::LogTypeConfig::StdOut, + log_level: crate::LogLevel::DEBUG, }; // Act @@ -59,6 +62,7 @@ fn test_log_memory_logger() { // Arrange let config = LogConfig { log_type: log_config::LogTypeConfig::Memory(log_config::MemoryLogConfig { log_ttl: 60 }), + log_level: crate::LogLevel::TRACE, }; let strategy = LogStrategy::new(&config); let entry = LogEntry { @@ -152,7 +156,7 @@ fn test_log_stdout_logger() { let test_writer = TestWriter::new(); let buffer = Box::new(test_writer.clone()) as Box; - let logger = StdOutLogger::new_with(buffer); + let logger = StdOutLogger::new_with(buffer, LogLevel::TRACE); let strategy = LogStrategy::StdOut(logger); // Act diff --git a/jans-cedarling/cedarling/src/tests/utils/cedarling_util.rs b/jans-cedarling/cedarling/src/tests/utils/cedarling_util.rs index 56ee5ff0e77..2e2482b0b27 100644 --- a/jans-cedarling/cedarling/src/tests/utils/cedarling_util.rs +++ b/jans-cedarling/cedarling/src/tests/utils/cedarling_util.rs @@ -36,6 +36,7 @@ pub fn get_config(policy_source: PolicyStoreSource) -> BootstrapConfig { application_name: "test_app".to_string(), log_config: LogConfig { log_type: LogTypeConfig::StdOut, + log_level: crate::LogLevel::DEBUG, }, policy_store_config: PolicyStoreConfig { source: policy_source, @@ -65,6 +66,7 @@ pub fn get_cedarling_with_authorization_conf( application_name: "test_app".to_string(), log_config: LogConfig { log_type: LogTypeConfig::StdOut, + log_level: crate::LogLevel::DEBUG, }, policy_store_config: PolicyStoreConfig { source: policy_source, diff --git a/jans-cedarling/test_files/bootstrap_props.json b/jans-cedarling/test_files/bootstrap_props.json index 41f73f1b809..d2c089e9553 100644 --- a/jans-cedarling/test_files/bootstrap_props.json +++ b/jans-cedarling/test_files/bootstrap_props.json @@ -3,6 +3,7 @@ "CEDARLING_POLICY_STORE_URI": "", "CEDARLING_POLICY_STORE_ID": "840da5d85403f35ea76519ed1a18a33989f855bf1cf8", "CEDARLING_LOG_TYPE": "std_out", + "CEDARLING_LOG_LEVEL": "DEBUG", "CEDARLING_LOG_TTL": null, "CEDARLING_USER_AUTHZ": "enabled", "CEDARLING_WORKLOAD_AUTHZ": "enabled", @@ -37,4 +38,4 @@ "CEDARLING_AUDIT_HEALTH_INTERVAL": 0, "CEDARLING_AUDIT_TELEMETRY_INTERVAL": 0, "CEDARLING_LISTEN_SSE": "disabled" -} +} \ No newline at end of file diff --git a/jans-cedarling/test_files/bootstrap_props.yaml b/jans-cedarling/test_files/bootstrap_props.yaml index 43f39d0c649..a1cc509e581 100644 --- a/jans-cedarling/test_files/bootstrap_props.yaml +++ b/jans-cedarling/test_files/bootstrap_props.yaml @@ -6,6 +6,7 @@ CEDARLING_APPLICATION_NAME: My App CEDARLING_POLICY_STORE_URI: '' CEDARLING_POLICY_STORE_ID: '840da5d85403f35ea76519ed1a18a33989f855bf1cf8' CEDARLING_LOG_TYPE: 'memory' +CEDARLING_LOG_LEVEL: 'DEBUG' CEDARLING_LOG_TTL: 60 CEDARLING_USER_AUTHZ: 'enabled' CEDARLING_WORKLOAD_AUTHZ: 'enabled'