Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(jans-cedarling): Load bootstrap properties from environment variables #10692

Merged
merged 28 commits into from
Jan 22, 2025
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
bf2344c
chore(jans-cedarling): implement custom deserializer `fallback_deseri…
olehbozhok Jan 17, 2025
cbfe9f1
feat(jans-cedarling): add to `BootstrapConfigRaw` way to load from env
olehbozhok Jan 17, 2025
625afff
chore(jans-cedarling): add loading `Cedarling` from env
olehbozhok Jan 17, 2025
4540f8c
chore(jans-cedarling): improve `fallback_deserialize` function and im…
olehbozhok Jan 17, 2025
4e65439
chore(jans-cedarling): add to python `BootstrapConfig` method `from_env`
olehbozhok Jan 17, 2025
17934e4
chore(jans-cedarling): update python docs and examples
olehbozhok Jan 17, 2025
2419b7c
chore(jans-cedarling): update sidecar to load Cedaring from env vars
olehbozhok Jan 17, 2025
fb1eb47
docs(jans-cedarling): update docs
olehbozhok Jan 17, 2025
90a3a81
chore(jans-cedarling): fix clippy issues
olehbozhok Jan 18, 2025
58fd69f
chore(jans-cedarling): fix rust tests
olehbozhok Jan 18, 2025
b2df993
chore(jans-cedarling): fix wasm
olehbozhok Jan 18, 2025
d390d9e
Merge branch 'main' into jans-cedaling-issue-10648
olehbozhok Jan 18, 2025
a53c527
Merge commit '040ff17942019bc10433ce17d819b8d8474f13c8' into jans-ced…
olehbozhok Jan 20, 2025
54895de
chore(jans-cedarling): improve comments to `to_json` function
olehbozhok Jan 20, 2025
cffa5ac
chore(jans-cedarling): rename `fallback_deserialize` to `deserialize_…
olehbozhok Jan 20, 2025
03841ad
chore(jans-cedarling): rename `cedarling_env_vars` to `get_cedarling_…
olehbozhok Jan 20, 2025
99859c7
chore(jans-cedarling): improve comments for `from_raw_config_and_env`
olehbozhok Jan 20, 2025
95e52b1
feat(jans-cedarling): add to `BootstrapConfig::from_env` method
olehbozhok Jan 20, 2025
870d869
docs(jans-cedarling): improve doc with loading bootstrap config from env
olehbozhok Jan 20, 2025
78cf11e
Merge branch 'main' into jans-cedaling-issue-10648
olehbozhok Jan 20, 2025
581745a
chore(jans-cedarling): add test cases for load raw config environment
olehbozhok Jan 21, 2025
eccab0b
chore(jans-cedarling): refactor raw config
olehbozhok Jan 21, 2025
452b8c3
chore(jans-cedarling): fix test cases
olehbozhok Jan 21, 2025
af19b04
Merge branch 'main' into jans-cedaling-issue-10648
olehbozhok Jan 22, 2025
5b9a4be
chore(jans-cedarling): add initialize `CEDARLING_BOOTSTRAP_CONFIG` cl…
olehbozhok Jan 22, 2025
3ffafc7
chore(jans-cedarling): update docker composer file for sidecar
olehbozhok Jan 22, 2025
920a7ec
chore(jans-cedarling): revert all changes related to the `sidecar`
olehbozhok Jan 22, 2025
a350979
Merge branch 'main' into jans-cedaling-issue-10648
olehbozhok Jan 22, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions docs/cedarling/cedarling-properties.md
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,11 @@ use cedarling::BootstrapConfig;

let config =
BootstrapConfig::load_from_file("./path/to/your/config.json").unwrap();

// OR from environment variables
let config =
BootstrapConfig::from_raw_config_and_env(None).unwrap();

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So if None is passed, it will just used defaults... should we also provide a function where the behavior is like passing in None?

Suggested change
// OR from environment variables
let config =
BootstrapConfig::from_raw_config_and_env(None).unwrap();
// Load the bootstrap config from the environment variables. Properties that are not defined will be assigned a default value.
let config = BootstrapConfig::from_env().unwrap();
// Load the bootstrap config from the environment variables and a given config.
let config = BootstrapConfig::from_raw_config_and_env(None).unwrap();

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If passed None, will be used values from env. And if some values are not present in env it does not get values from config structure.
Ok I will add additional method from_env

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Resolved in 870d869

```

### Loading From JSON
Expand Down
8 changes: 8 additions & 0 deletions jans-cedarling/bindings/cedarling_python/PYTHON_TYPES.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,14 @@ Methods
:returns: A BootstrapConfig instance

:raises ValueError: If a provided value is invalid or decoding fails.

.. method:: from_env(config=None) -> BootstrapConfig

Loads the bootstrap config from environment variables, optionally merging with provided config.

:param config: Optional dictionary with additional configuration to merge with environment variables.
:returns: A BootstrapConfig instance
:raises ValueError: If a provided value is invalid or decoding fails.
___

Cedarling
Expand Down
20 changes: 20 additions & 0 deletions jans-cedarling/bindings/cedarling_python/cedarling_python.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,25 @@ class BootstrapConfig:
"""
...

@staticmethod
def from_env(options: Dict | None = None) -> BootstrapConfig:
"""
Loads the configuration from environment variables.

Reads environment variables matching the configuration keys listed in the
class documentation. All required keys must be present in the environment.
You can specify dict, but keys from environment variables have bigger priority.

Returns:
BootstrapConfig: An instance of the configuration class.

Raises:
KeyError: If any required environment variable is missing.
ValueError: If a provided value is invalid or extraction fails.
"""
...


@final
class Cedarling:

Expand Down Expand Up @@ -139,6 +158,7 @@ class Request:
resource: ResourceData,
context: Dict[str, Any]) -> None: ...


@final
class Tokens:
access_token: str | None
Expand Down
18 changes: 17 additions & 1 deletion jans-cedarling/bindings/cedarling_python/example.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,24 @@
from cedarling_python import Cedarling
from cedarling_python import ResourceData, Request
import time
import yaml
import os

bootstrap_config = BootstrapConfig.load_from_file("./example_files/sample_bootstrap_props.yaml")

def load_yaml_to_env(yaml_path):
with open(yaml_path, 'r') as file:
config = yaml.safe_load(file)

for key, value in config.items():
if value is not None: # Skip null values
os.environ[key] = str(value)


bootstrap_config = BootstrapConfig.load_from_file(
"./example_files/sample_bootstrap_props.yaml")

# Create config from environment variables
# bootstrap_config = BootstrapConfig.from_env()

# initialize cedarling instance
# all values in the bootstrap_config is parsed and validated at this step.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,6 @@ CEDARLING_ID_TOKEN_TRUST_MODE: strict
CEDARLING_LOCK: disabled
CEDARLING_LOCK_MASTER_CONFIGURATION_URI: null
CEDARLING_DYNAMIC_CONFIGURATION: disabled
CEDARLING_LOCK_SSA_JWT: 0
CEDARLING_AUDIT_HEALTH_INTERVAL: 0
CEDARLING_AUDIT_TELEMETRY_INTERVAL: 0
CEDARLING_LISTEN_SSE: disabled
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,14 @@ use pyo3::types::PyDict;
/// :returns: A BootstrapConfig instance
///
/// :raises ValueError: If a provided value is invalid or decoding fails.
///
/// .. method:: from_env(config=None) -> BootstrapConfig
///
/// Loads the bootstrap config from environment variables, optionally merging with provided config.
///
/// :param config: Optional dictionary with additional configuration to merge with environment variables.
/// :returns: A BootstrapConfig instance
/// :raises ValueError: If a provided value is invalid or decoding fails.
#[pyclass]
pub struct BootstrapConfig {
inner: cedarling::BootstrapConfig,
Expand All @@ -55,6 +63,23 @@ impl BootstrapConfig {
Ok(Self { inner })
}

#[staticmethod]
#[pyo3(signature = (config=None))]
fn from_env(config: Option<Bound<'_, PyDict>>) -> PyResult<Self> {
let inner = if let Some(c) = config {
let source: cedarling::BootstrapConfigRaw = serde_pyobject::from_pyobject(c)
.map_err(|e| PyValueError::new_err(e.to_string()))?;

cedarling::BootstrapConfig::from_raw_config_and_env(Some(source))
.map_err(|e| PyValueError::new_err(e.to_string()))?
} else {
cedarling::BootstrapConfig::from_raw_config_and_env(None)
.map_err(|e| PyValueError::new_err(e.to_string()))?
};

Ok(Self { inner })
}

#[staticmethod]
pub fn load_from_file(path: &str) -> PyResult<Self> {
let inner = cedarling::BootstrapConfig::load_from_file(path).map_err(|e| match e {
Expand Down
14 changes: 12 additions & 2 deletions jans-cedarling/cedarling/src/blocking.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,10 @@

//! Blocking client of Cedarling

use crate::Cedarling as AsyncCedarling;
use crate::{
AuthorizeError, AuthorizeResult, BootstrapConfig, InitCedarlingError, LogStorage, Request,
};
use crate::{BootstrapConfigRaw, Cedarling as AsyncCedarling};
use std::sync::Arc;
use tokio::runtime::Runtime;

Expand All @@ -23,7 +23,17 @@ pub struct Cedarling {
}

impl Cedarling {
/// Builder
/// Create a new instance of the Cedarling application.
/// Initialize instance from enviroment variables and from config.
/// Configuration structure has lower priority.
pub fn new_with_env(
raw_config: Option<BootstrapConfigRaw>,
) -> Result<Cedarling, InitCedarlingError> {
let config = BootstrapConfig::from_raw_config_and_env(raw_config)?;
Self::new(&config)
}

/// Create a new instance of the Cedarling application.
pub fn new(config: &BootstrapConfig) -> Result<Cedarling, InitCedarlingError> {
let rt = Runtime::new().map_err(InitCedarlingError::RuntimeInit)?;

Expand Down
69 changes: 64 additions & 5 deletions jans-cedarling/cedarling/src/bootstrap_config/decode.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,11 @@
//
// Copyright (c) 2024, Gluu, Inc.

use std::collections::HashSet;
// to avoid a lot of `cfg` macros
#![allow(unused_imports)]

use std::collections::{HashMap, HashSet};
use std::env;
use std::fmt::Display;
use std::fs;
use std::path::Path;
Expand All @@ -13,15 +17,20 @@ use jsonwebtoken::Algorithm;
use serde::{Deserialize, Deserializer, Serialize};

use super::authorization_config::AuthorizationConfig;
use super::json_util::fallback_deserialize;
use super::{
BootstrapConfig, BootstrapConfigLoadingError, IdTokenTrustMode, JwtConfig, LogConfig,
LogTypeConfig, MemoryLogConfig, PolicyStoreConfig, PolicyStoreSource, TokenValidationConfig,
};
use crate::log::LogLevel;

#[derive(Deserialize, PartialEq, Debug, Default)]
#[derive(Deserialize, Serialize, PartialEq, Debug, Default)]
/// Struct that represent mapping mapping `Bootstrap properties` to be JSON and YAML compatible
/// from [link](https://github.com/JanssenProject/jans/wiki/Cedarling-Nativity-Plan#bootstrap-properties)
///
/// This structure is used to deserialize values from ENV VARS so json keys is same as keys in environment variables
//
// All fields should be available to parse from string, because env vars always string.
pub struct BootstrapConfigRaw {
/// Human friendly identifier for the application
#[serde(rename = "CEDARLING_APPLICATION_NAME")]
Expand Down Expand Up @@ -50,14 +59,17 @@ pub struct BootstrapConfigRaw {
/// 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)]
#[serde(deserialize_with = "fallback_deserialize")]
pub log_ttl: Option<u64>,

/// List of claims to map from user entity, such as ["sub", "email", "username", ...]
#[serde(rename = "CEDARLING_DECISION_LOG_USER_CLAIMS", default)]
#[serde(deserialize_with = "fallback_deserialize")]
pub decision_log_user_claims: Vec<String>,

/// List of claims to map from user entity, such as ["client_id", "rp_id", ...]
#[serde(rename = "CEDARLING_DECISION_LOG_WORKLOAD_CLAIMS", default)]
#[serde(deserialize_with = "fallback_deserialize")]
pub decision_log_workload_claims: Vec<String>,

/// Token claims that will be used for decision logging.
Expand All @@ -84,22 +96,27 @@ pub struct BootstrapConfigRaw {

/// Mapping name of cedar schema User entity
#[serde(rename = "CEDARLING_MAPPING_USER", default)]
#[serde(deserialize_with = "fallback_deserialize")]
pub mapping_user: Option<String>,

/// Mapping name of cedar schema Workload entity.
#[serde(rename = "CEDARLING_MAPPING_WORKLOAD", default)]
#[serde(deserialize_with = "fallback_deserialize")]
pub mapping_workload: Option<String>,

/// Mapping name of cedar schema id_token entity.
#[serde(rename = "CEDARLING_MAPPING_ID_TOKEN", default)]
#[serde(deserialize_with = "fallback_deserialize")]
pub mapping_id_token: Option<String>,

/// Mapping name of cedar schema access_token entity.
#[serde(rename = "CEDARLING_MAPPING_ACCESS_TOKEN", default)]
#[serde(deserialize_with = "fallback_deserialize")]
pub mapping_access_token: Option<String>,

/// Mapping name of cedar schema userinfo_token entity.
#[serde(rename = "CEDARLING_MAPPING_USERINFO_TOKEN", default)]
#[serde(deserialize_with = "fallback_deserialize")]
pub mapping_userinfo_token: Option<String>,

/// Path to a local file pointing containing a JWKS.
Expand All @@ -112,6 +129,7 @@ pub struct BootstrapConfigRaw {

/// JSON object with policy store
#[serde(rename = "CEDARLING_LOCAL_POLICY_STORE", default)]
#[serde(deserialize_with = "fallback_deserialize")]
pub local_policy_store: Option<String>,

/// Path to a Policy Store JSON file
Expand All @@ -126,6 +144,7 @@ pub struct BootstrapConfigRaw {
///
/// This requires that an `iss` (Issuer) claim is present on each token.
#[serde(rename = "CEDARLING_JWT_SIG_VALIDATION", default)]
#[serde(deserialize_with = "fallback_deserialize")]
pub jwt_sig_validation: FeatureToggle,

/// Whether to check the status of the JWT. On startup.
Expand All @@ -136,10 +155,12 @@ pub struct BootstrapConfigRaw {
///
/// [`IETF Draft`]: https://datatracker.ietf.org/doc/draft-ietf-oauth-status-list/
#[serde(rename = "CEDARLING_JWT_STATUS_VALIDATION", default)]
#[serde(deserialize_with = "fallback_deserialize")]
pub jwt_status_validation: FeatureToggle,

/// Cedarling will only accept tokens signed with these algorithms.
#[serde(rename = "CEDARLING_JWT_SIGNATURE_ALGORITHMS_SUPPORTED", default)]
#[serde(deserialize_with = "fallback_deserialize")]
pub jwt_signature_algorithms_supported: HashSet<Algorithm>,

/// When enabled, the `iss` (Issuer) claim must be present in the Access Token and
Expand Down Expand Up @@ -222,6 +243,7 @@ pub struct BootstrapConfigRaw {
///
/// ***Required*** if `LOCK == Enabled`.
#[serde(rename = "CEDARLING_LOCK_MASTER_CONFIGURATION_URI", default)]
#[serde(deserialize_with = "fallback_deserialize")]
pub lock_master_configuration_uri: Option<String>,

/// Controls whether Cedarling should listen for SSE config updates.
Expand All @@ -239,14 +261,17 @@ pub struct BootstrapConfigRaw {

/// How often to send log messages to Lock Master (0 to turn off trasmission).
#[serde(rename = "CEDARLING_AUDIT_LOG_INTERVAL", default)]
#[serde(deserialize_with = "fallback_deserialize")]
pub audit_log_interval: u64,

/// How often to send health messages to Lock Master (0 to turn off transmission).
#[serde(rename = "CEDARLING_AUDIT_HEALTH_INTERVAL", default)]
#[serde(deserialize_with = "fallback_deserialize")]
pub audit_health_interval: u64,

/// How often to send telemetry messages to Lock Master (0 to turn off transmission).
#[serde(rename = "CEDARLING_AUDIT_TELEMETRY_INTERVAL", default)]
#[serde(deserialize_with = "fallback_deserialize")]
pub audit_health_telemetry_interval: u64,

/// Controls whether Cedarling should listen for updates from the Lock Server.
Expand All @@ -255,7 +280,7 @@ pub struct BootstrapConfigRaw {
}

/// Type of logger
#[derive(Debug, PartialEq, Deserialize, Default)]
#[derive(Debug, PartialEq, Deserialize, Serialize, Default)]
#[serde(rename_all = "lowercase")]
pub enum LoggerType {
/// Disabled logger
Expand Down Expand Up @@ -300,7 +325,7 @@ impl Display for LoggerType {
}

/// Enum varians that represent if feature is enabled or disabled
#[derive(Debug, PartialEq, Deserialize, Default, Copy, Clone)]
#[derive(Debug, PartialEq, Deserialize, Serialize, Default, Copy, Clone)]
#[serde(rename_all = "lowercase")]
pub enum FeatureToggle {
/// Represent as disabled.
Expand Down Expand Up @@ -428,7 +453,41 @@ pub struct ParseFeatureToggleError {
value: String,
}

/// Get environment variables related to `Cedarling`
#[cfg(not(target_arch = "wasm32"))]
fn cedarling_env_vars() -> HashMap<String, serde_json::Value> {
env::vars()
.filter_map(|(k, v)| {
k.starts_with("CEDARLING_")
.then_some((k, serde_json::json!(v)))
})
.collect()
}

impl BootstrapConfig {
/// Construct `BootstrapConfig` from environment variables and `BootstrapConfigRaw` config
//
// Simple implementation that map input structure to JSON map
// and map environment variables with prefix `CEDARLING_` to JSON map. And merge it.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also document which config source has a higher priority here.

Copy link
Contributor Author

@olehbozhok olehbozhok Jan 20, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't really get. But environment variables have bigger priority. The comment was updated.

#[cfg(not(target_arch = "wasm32"))]
pub fn from_raw_config_and_env(
raw: Option<BootstrapConfigRaw>,
) -> Result<Self, BootstrapConfigLoadingError> {
let mut json_config_params = serde_json::json!(raw.unwrap_or_default())
.as_object()
.map(|v| v.to_owned())
.unwrap_or_default();

cedarling_env_vars().into_iter().for_each(|(k, v)| {
// update map with values from env variables
json_config_params.insert(k, v);
});

let config_raw = BootstrapConfigRaw::deserialize(json_config_params)?;

Self::from_raw_config(&config_raw)
}

/// Construct an instance from BootstrapConfigRaw
pub fn from_raw_config(raw: &BootstrapConfigRaw) -> Result<Self, BootstrapConfigLoadingError> {
if !raw.workload_authz.is_enabled() && !raw.user_authz.is_enabled() {
Expand Down Expand Up @@ -559,7 +618,7 @@ pub fn parse_option_string<'de, D>(deserializer: D) -> Result<Option<String>, D:
where
D: Deserializer<'de>,
{
let value = Option::<String>::deserialize(deserializer)?;
let value: Option<String> = fallback_deserialize(deserializer)?;

Ok(value.filter(|s| !s.is_empty()))
}
Loading
Loading