Skip to content
Open
Show file tree
Hide file tree
Changes from 28 commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
a130aaa
chore(jans-cedarling): refactor and add `IssuerIndex` thread safe str…
olehbozhok Jan 23, 2026
8c89e89
refactor(jans-cedarling): add type `IssClaim` that should hold normal…
olehbozhok Jan 23, 2026
720a0fa
chore(jans-cedarling): fix clippy issues
olehbozhok Jan 23, 2026
cc7c720
chore(jans-cedarling): fix clippy warn `field ___ is never read`
olehbozhok Jan 23, 2026
a0115ce
feat(jans-cedarling): add thread safety to `JwtValidatorCache`
olehbozhok Jan 23, 2026
f844b47
feat(jans-cedarling): add thread safety to `KeyService`
olehbozhok Jan 23, 2026
45d0887
feat(jans-cedarling): add new bootstrap property `CEDARLING_TRUSTED_…
olehbozhok Jan 23, 2026
99ed28f
chore(jans-cedarling): refactor to use `TrustedIssuerLoader` on loadi…
olehbozhok Jan 23, 2026
bbf870c
chore(jans-cedarling): add method `TrustedIssuerLoader::load_trusted_…
olehbozhok Jan 27, 2026
24cc68e
feat(jans-cedarling): implement asynchronous loading trusted issuers
olehbozhok Jan 27, 2026
a183dc3
chore(jans-cedarling): update agent file
olehbozhok Jan 27, 2026
bc62313
test(jans-cedarling): add unit tests to `TrustedIssuerLoader`
olehbozhok Jan 27, 2026
513d158
Merge commit 'main' into jans-cedarling-10004
olehbozhok Jan 29, 2026
9bc827f
chore(jans-cedarling): automatic clippy fixes
olehbozhok Jan 29, 2026
9f0375b
chore(jans-cedarling): fix clippy issuers
olehbozhok Jan 29, 2026
4732690
chore(jans-cedarling): fix clippy issuers
olehbozhok Jan 29, 2026
2636d75
feat(jans-cedarling): updated bootstrap properties to have 2 loader t…
olehbozhok Feb 2, 2026
10c9f7b
docs(jans-cedarling): update bootstrap properties reference with miss…
olehbozhok Feb 2, 2026
9b950e8
Merge commit 'b511d61b20737ebb21d04df49197c6fcfbf329fa' into jans-ced…
olehbozhok Feb 2, 2026
46d3386
chore(jans-cedarling): remove outdated comment
olehbozhok Feb 2, 2026
a4cfec4
chore(jans-cedarling): fix clippy issues
olehbozhok Feb 2, 2026
61b702a
chore(jans-cedarling): fix mistype `retreive` -> `retrieve`
olehbozhok Feb 2, 2026
bc26ed6
chore(jans-cedarling): remove unused import
olehbozhok Feb 2, 2026
5fdd95a
chore(jans-cedarling): add license to file
olehbozhok Feb 2, 2026
48911bc
chore(jans-cedarling): fix using docstring instead of comments
olehbozhok Feb 2, 2026
54b6488
chore(jans-cedarling): simplify using lifetimes
olehbozhok Feb 2, 2026
4b81c8d
chore(jans-cedarling): fix clippy issues
olehbozhok Feb 2, 2026
86ef562
chore(jans-cedarling): remove unwrap and use expect with messages
olehbozhok Feb 2, 2026
95f8932
chore(jans-cedarling): Remove unused _id variable.
olehbozhok Feb 2, 2026
965ac00
chore(jans-cedarling): simplify Issuer equality check
olehbozhok Feb 2, 2026
edf8370
chore(jans-cedarling): fix clippy issues
olehbozhok Feb 2, 2026
c8f230b
docs(jans-cedarling): improve doc message to `CEDARLING_TRUSTED_ISSUE…
olehbozhok Feb 2, 2026
73b0b29
test(jans-cedarling): add tests loading `CEDARLING_TRUSTED_ISSUER_LOA…
olehbozhok Feb 2, 2026
2d4aa2b
chore(jans-cedarling): improvement to use NonZeroUsize::MIN
olehbozhok Feb 4, 2026
7ede7b2
chore(jans-cedarling): remove arc wrapper for `StatusListCache`
olehbozhok Feb 9, 2026
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
24 changes: 14 additions & 10 deletions docs/cedarling/reference/cedarling-properties.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,9 @@ These Bootstrap Properties control default application level behavior.

To enable usage of principals at least one of the following keys must be provided:

- **`CEDARLING_WORKLOAD_AUTHZ`** : When `enabled`, Cedar engine authorization is queried for a Workload principal.
- **`CEDARLING_WORKLOAD_AUTHZ`** : When `enabled`, Cedar engine authorization is queried for a Workload principal. Default is `disabled`.

- **`CEDARLING_USER_AUTHZ`** : When `enabled`, Cedar engine authorization is queried for a User principal.
- **`CEDARLING_USER_AUTHZ`** : When `enabled`, Cedar engine authorization is queried for a User principal. Default is `disabled`.

To load policy store one of the following keys must be provided:

Expand Down Expand Up @@ -58,6 +58,10 @@ All other fields are optional and can be omitted. If a field is not provided, Ce

- **`CEDARLING_TOKEN_CACHE_EARLIEST_EXPIRATION_EVICTION`** : Enables eviction policy based on the earliest expiration time. When the cache reaches its capacity, the entry with the nearest expiration timestamp will be removed to make room for a new one. Default value is `true`.

- **`CEDARLING_TRUSTED_ISSUER_LOADER_TYPE`** : `SYNC` | `ASYNC` -- Type of trusted issuer loader. If not set, synchronous loader is used. Sync loader means that trusted issuers will be loaded on initialization. Async loader means that trusted issuers will be loaded in background. Default is `SYNC`.

- **`CEDARLING_TRUSTED_ISSUER_LOADER_WORKERS`** : Number of concurrent workers used to asynchronously load trusted issuers. Default value is 1. Zero will be treated as default value.

**Cedar Entity Mapping properties**

- **`CEDARLING_MAPPING_USER`** : Name of Cedar User schema entity if we don't want to use default. When specified Cedarling try build defined entity (from schema) as user instead of default `User` entity defined in `cedar` schema. Works in namespace defined in the policy store. Default value: `Jans::User`.
Expand All @@ -68,7 +72,7 @@ All other fields are optional and can be omitted. If a field is not provided, Ce

**The following bootstrap properties are needed to configure log behavior:**

- **`CEDARLING_LOG_TYPE`** : `off`, `memory`, `std_out`
- **`CEDARLING_LOG_TYPE`** : `off`, `memory`, `std_out`. Default is `off`.
- **`CEDARLING_LOG_LEVEL`** : System Log Level [See here](./cedarling-logs.md). Default to `WARN`
- **`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 workload entity, such as ["client_id", "rp_id", ...]
Expand All @@ -84,9 +88,9 @@ All other fields are optional and can be omitted. If a field is not provided, Ce

- **`CEDARLING_LOCAL_JWKS`** : Path to a local file containing a JWKS

- **`CEDARLING_JWT_SIG_VALIDATION`** : `enabled` | `disabled` -- Whether to check the signature of all JWT tokens. This requires an `iss` is present.
- **`CEDARLING_JWT_STATUS_VALIDATION`** : `enabled` | `disabled` -- Whether to check the status of the JWT. On startup, the Cedarling should fetch and retreive the latest Status List JWT from the `.well-known/openid-configuration` via the `status_list_endpoint` claim and cache it. See the [IETF Draft](https://datatracker.ietf.org/doc/draft-ietf-oauth-status-list/) for more info.
- **`CEDARLING_JWT_SIGNATURE_ALGORITHMS_SUPPORTED`** : Only tokens signed with these algorithms are acceptable to the Cedarling.
- **`CEDARLING_JWT_SIG_VALIDATION`** : `enabled` | `disabled` -- Whether to check the signature of all JWT tokens. This requires an `iss` is present. Default is `disabled`.
- **`CEDARLING_JWT_STATUS_VALIDATION`** : `enabled` | `disabled` -- Whether to check the status of the JWT. On startup, the Cedarling should fetch and retrieve the latest Status List JWT from the `.well-known/openid-configuration` via the `status_list_endpoint` claim and cache it. See the [IETF Draft](https://datatracker.ietf.org/doc/draft-ietf-oauth-status-list/) for more info. Default is `disabled`.
- **`CEDARLING_JWT_SIGNATURE_ALGORITHMS_SUPPORTED`** : Only tokens signed with these algorithms are acceptable to the Cedarling. If not specified, all algorithms supported by the underlying library are allowed.
- **`CEDARLING_ID_TOKEN_TRUST_MODE`** : `strict` | `never` | `always` | `ifpresent`. Varying levels of validations based on the preference of the developer.

- **`strict`** (default): Enforces strict validation rules:
Expand All @@ -103,12 +107,12 @@ All other fields are optional and can be omitted. If a field is not provided, Ce

**The following bootstrap properties are only needed for the Lock Server Integration.**

- **`CEDARLING_LOCK`** : `enabled` | `disabled`. If `enabled`, the Cedarling will connect to the Lock Server for policies, and subscribe for SSE events.
- **`CEDARLING_LOCK`** : `enabled` | `disabled`. If `enabled`, the Cedarling will connect to the Lock Server for policies, and subscribe for SSE events. Default is `disabled`.
- **`CEDARLING_LOCK_SERVER_CONFIGURATION_URI`** : Required if `LOCK` == `enabled`. URI where Cedarling can get JSON file with all required metadata about the Lock Server, i.e. `.well-known/lock-master-configuration`.
- **`CEDARLING_LOCK_DYNAMIC_CONFIGURATION`** : `enabled` | `disabled`, controls whether Cedarling should listen for SSE config updates.
- **`CEDARLING_LOCK_DYNAMIC_CONFIGURATION`** : `enabled` | `disabled`, controls whether Cedarling should listen for SSE config updates. Default is `disabled`.
- **`CEDARLING_LOCK_SSA_JWT`** : SSA for DCR in a Lock Server deployment. The Cedarling will validate this SSA JWT prior to DCR.
- **`CEDARLING_LOCK_LOG_INTERVAL`** : How often to send log messages to Lock Server (0 to turn off transmission).
- **`CEDARLING_LOCK_HEALTH_INTERVAL`** : How often to send health messages to Lock Server (0 to turn off transmission).
- **`CEDARLING_LOCK_TELEMETRY_INTERVAL`** : How often to send telemetry messages to Lock Server (0 to turn off transmission).
- **`CEDARLING_LOCK_LISTEN_SSE`** : `enabled` | `disabled`: controls whether Cedarling should listen for updates from the Lock Server.
- **`CEDARLING_LOCK_ACCEPT_INVALID_CERTS`** : `enabled` | `disabled`: Allows interaction with a Lock server with invalid certificates. Mainly used for testing. Doesn't work for WASM builds.
- **`CEDARLING_LOCK_LISTEN_SSE`** : `enabled` | `disabled`: controls whether Cedarling should listen for updates from the Lock Server. Default is `disabled`.
- **`CEDARLING_LOCK_ACCEPT_INVALID_CERTS`** : `enabled` | `disabled`: Allows interaction with a Lock server with invalid certificates. Mainly used for testing. Doesn't work for WASM builds. Default is `disabled`.
8 changes: 5 additions & 3 deletions jans-cedarling/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,9 +79,11 @@
- Use test_utils crate for shared test helpers
- Follow existing test patterns in authorize_*.rs files
- Include both positive and negative test cases
- For error checking, use `assert!(matches!(...), "explicit comment")` instead of `assert!(result.is_err())`
- Use `expect_err("explicit comment")` instead of `panic()`
- Always include explicit comments explaining what error is expected
- For success checking, use `result.expect("descriptive message")` instead of `assert!(result.is_ok())`
- For error checking, use `result.expect_err("descriptive message")` instead of `assert!(result.is_err())`
- For pattern matching errors, use `assert!(matches!(...), "descriptive message")`
- All assertions must include a descriptive message explaining what is being tested
- Use `expect_err("explicit comment")` instead of `panic()` when expecting errors

**File Headers:**
- Each Rust file must contain the Apache 2.0 license header:
Expand Down
5 changes: 4 additions & 1 deletion jans-cedarling/cedarling/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,10 @@ async-trait = "0.1.88"
futures = "0.3.31"
wasm-bindgen-futures = { workspace = true }
config = "0.15.11"
ahash = { version = "0.8.12", default-features = false, features = ["no-rng"] }
ahash = { version = "0.8.12", default-features = false, features = [
"no-rng",
"std",
] }
vfs = "0.12"
hex = "0.4.3"
sha2 = "0.10.8"
Expand Down
8 changes: 8 additions & 0 deletions jans-cedarling/cedarling/src/bootstrap_config/decode.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ use std::collections::{HashMap, HashSet};
use std::env;
use std::fmt::Display;
use std::fs;
use std::num::NonZeroUsize;
use std::path::Path;
use std::str::FromStr;

Expand All @@ -20,6 +21,7 @@ use super::{
MemoryLogConfig, PolicyStoreConfig, PolicyStoreSource,
};
use super::{BootstrapConfigRaw, LockServiceConfig};
use crate::jwt_config::{TrustedIssuerLoaderConfig, TrustedIssuerLoaderTypeRaw};
use crate::log::{LogLevel, StdOutLoggerMode};
use jsonwebtoken::Algorithm;
use serde::{Deserialize, Deserializer, Serialize};
Expand Down Expand Up @@ -112,6 +114,9 @@ impl BootstrapConfig {
})
.transpose()?;

let trusted_issuer_workers = NonZeroUsize::new(raw.trusted_issuer_loader_workers)
.unwrap_or_else(|| unsafe { NonZeroUsize::new_unchecked(1) });
Copy link
Contributor

@coderabbitai coderabbitai bot Feb 2, 2026

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

🧩 Analysis chain

🌐 Web query:

Rust NonZeroUsize::new safe alternative to new_unchecked best practice

💡 Result:

The safe alternative to NonZeroUsize::new_unchecked(x) is almost always:

use std::num::NonZeroUsize;

let nz = NonZeroUsize::new(x); // -> Option<NonZeroUsize>

new_unchecked is unsafe because passing 0 violates NonZero*’s “never zero” validity invariant and is undefined behavior (UB). The standard library explicitly documents this: new returns Option after checking, while new_unchecked “results in undefined behavior if the value is zero.” [1]

Best-practice patterns

1) Prefer new + handle Option

let nz = NonZeroUsize::new(x).ok_or(MyError::Zero)?;

(or if let Some(nz) = ... / match).

2) If “zero is a bug” at a boundary, use expect/unwrap

let nz = NonZeroUsize::new(x).expect("x must be non-zero");

This is also usable in const contexts because Option::expect/unwrap is const (shown in the std docs for NonZero*). [2]

3) Only use new_unchecked when you prove non-zero and need it

Typical justification: you already performed a check and want to avoid a redundant branch in a hot path:

let nz = match NonZeroUsize::new(x) {
    Some(v) => v,
    None => return Err(MyError::Zero),
};

// later, if you must reconstruct and can prove non-zero:
let y = nz.get();              // guaranteed non-zero by the type invariant [3]
let nz2 = unsafe { NonZeroUsize::new_unchecked(y) }; // usually unnecessary

If you do use new_unchecked, put a clear // SAFETY: x != 0 because ... comment, and consider a debug_assert!(x != 0);.

Why the invariant matters: NonZero* enables layout optimizations (e.g., Option<NonZero*> can be the same size as the integer type, using 0 as the niche for None). [3], [4]

Sources:
[1] Rust std docs: NonZero::new and NonZero::new_unchecked safety/UB notes https://doc.rust-lang.org/beta/std/num/struct.NonZero.html
[2] Rust std docs: compile-time creation via NonZero*::new(...).expect(...) (example shown for NonZeroIsize) https://doc.rust-lang.org/std/num/type.NonZeroIsize.html
[3] Rust std docs: NonZeroIsize layout/validity + Option<NonZeroIsize> niche optimization explanation https://doc.rust-lang.org/std/num/type.NonZeroIsize.html
[4] Rust forum explanation of why new_unchecked must be unsafe (niche optimization and invariant) https://users.rust-lang.org/t/why-is-std-new-unchecked-unsafe/23151


Remove unnecessary unsafe when defaulting worker count.

The unsafe { NonZeroUsize::new_unchecked(1) } is unnecessary since 1 is compile-time guaranteed to be non-zero. Use the safe constructor instead:

NonZeroUsize::new(1).expect("1 is non-zero")

This follows Rust best practices and avoids unsafe code when the invariant is trivially satisfied.

🤖 Prompt for AI Agents
In `@jans-cedarling/cedarling/src/bootstrap_config/decode.rs` around lines 117 -
118, Replace the unsafe construction for trusted_issuer_workers: instead of
using NonZeroUsize::new_unchecked(1) in the unwrap_or_else closure, call the
safe constructor NonZeroUsize::new(1).expect("1 is non-zero") so the fallback
remains a guaranteed non-zero value without unsafe code; update the closure
where trusted_issuer_workers is assigned (the expression using
raw.trusted_issuer_loader_workers and NonZeroUsize::new_unchecked).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It is OK in this case. In this case it is safe

Copy link
Contributor

Choose a reason for hiding this comment

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

@olehbozhok, understood! While the safe constructor would avoid unsafe with zero runtime cost, I respect your decision to keep it as is.


🧠 Learnings used
Learnt from: olehbozhok
Repo: JanssenProject/jans PR: 13125
File: jans-cedarling/cedarling/src/jwt/mod.rs:177-186
Timestamp: 2026-02-02T18:28:01.853Z
Learning: In jans-cedarling JWT service (file: jans-cedarling/cedarling/src/jwt/mod.rs), the local JWKS loading is intentionally gated by `loader.jwt_config.trusted_issuer_loader.is_sync()`. This means local JWKS are only loaded in synchronous loader mode, not in async mode. This is by design.

Learnt from: olehbozhok
Repo: JanssenProject/jans PR: 12896
File: jans-cedarling/cedarling/src/entity_builder/trusted_issuer_index.rs:23-30
Timestamp: 2025-12-24T14:16:17.748Z
Learning: In jans-cedarling Rust code targeting WASM, avoid using `eprintln!` for warnings in constructors or initialization functions. Instead, accept an `Option<&Arc<LogStrategy>>` parameter and use `logger.log_any(LogEntry::new_with_data(LogType::System, None).set_message(...).set_log_level(LogLevel::WARN))` to emit warnings that work in WASM environments.

Copy link
Contributor

Choose a reason for hiding this comment

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

i think we should refactor this i dont see the point of using unnecessary unsafe when it could be done safely and i think we can also use .unwrap_or(NonZeroUsize::MIN) // MIN is always 1

Copy link
Contributor

Choose a reason for hiding this comment

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

i think we should also add input validation for worker count NonZeroUsize prevents zero, but no upper bound check exists. A misconfigured value (e.g., 10000) could spawn excessive tasks.

Copy link
Contributor

Choose a reason for hiding this comment

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

Seems like the humans are having a chat. I'll hop back into my burrow for now. If you need me again, just tag @coderabbitai in a new comment, and I'll come hopping out!

Copy link
Contributor

Choose a reason for hiding this comment

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

Seems like the humans are having a chat. I'll hop back into my burrow for now. If you need me again, just tag @coderabbitai in a new comment, and I'll come hopping out!

Copy link
Contributor

Choose a reason for hiding this comment

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

i think we should refactor this i dont see the point of using unnecessary unsafe when it could be done safely and i think we can also use .unwrap_or(NonZeroUsize::MIN) // MIN is always 1

I agree with @haileyesus2433 with this unless there is an explicit reason to use unsafe we should avoid it

Copy link
Contributor Author

Choose a reason for hiding this comment

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

using NonZeroUsize::MIN is a really good idea. Thanks!


// JWT Config
let jwt_config = JwtConfig {
jwks,
Expand All @@ -121,6 +126,9 @@ impl BootstrapConfig {
token_cache_max_ttl_secs: raw.token_cache_max_ttl,
token_cache_capacity: raw.token_cache_capacity,
token_cache_earliest_expiration_eviction: raw.token_cache_earliest_expiration_eviction,
trusted_issuer_loader: raw
.trusted_issuer_loader_type
.to_config(trusted_issuer_workers),
};

let authorization_config = AuthorizationConfig {
Expand Down
62 changes: 61 additions & 1 deletion jans-cedarling/cedarling/src/bootstrap_config/jwt_config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

use jsonwebtoken::Algorithm;
use serde::{Deserialize, Serialize};
use std::collections::HashSet;
use std::{collections::HashSet, num::NonZeroUsize};

/// The set of Bootstrap properties related to JWT validation.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
Expand Down Expand Up @@ -52,6 +52,8 @@ pub struct JwtConfig {
/// When the cache reaches its capacity, the entry with the nearest
/// expiration timestamp will be removed to make room for a new one.
pub token_cache_earliest_expiration_eviction: bool,
/// Configuration for loading trusted issuers.
pub trusted_issuer_loader: TrustedIssuerLoaderConfig,
}

/// Validation options related to JSON Web Tokens (JWT).
Expand Down Expand Up @@ -182,6 +184,7 @@ impl Default for JwtConfig {
token_cache_capacity: 100,
token_cache_earliest_expiration_eviction: true,
token_cache_max_ttl_secs: 60 * 5, // 5min
trusted_issuer_loader: TrustedIssuerLoaderConfig::default(),
}
}
}
Expand Down Expand Up @@ -220,3 +223,60 @@ impl JwtConfig {
self
}
}

/// Raw representation of trusted issuer loader type from environment variable.
/// Is used in [`BootstrapConfigRaw`].
#[derive(Debug, Default, Clone, PartialEq, Serialize, Deserialize)]
pub enum TrustedIssuerLoaderTypeRaw {
/// Synchronous loading
#[default]
#[serde(rename = "SYNC")]
Sync,
/// Asynchronous loading
#[serde(rename = "ASYNC")]
Async,
}

impl TrustedIssuerLoaderTypeRaw {
/// Converts raw representation to `TrustedIssuerLoaderConfig`.
pub(crate) fn to_config(&self, workers: NonZeroUsize) -> TrustedIssuerLoaderConfig {
match self {
TrustedIssuerLoaderTypeRaw::Sync => TrustedIssuerLoaderConfig::Sync { workers },
TrustedIssuerLoaderTypeRaw::Async => TrustedIssuerLoaderConfig::Async { workers },
}
}
}

/// Config structure that define how trusted issuers will be loaded.
///
/// Default is `Sync` with 1 worker.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum TrustedIssuerLoaderConfig {
/// Synchronous loading, on start program.
/// The Cedarling will load all entities on start in "blocking mode" (you need to wait).
Sync {
/// Workers count
workers: NonZeroUsize,
},
/// Asynchronous loading, on start program.
/// The Cedarling will load all entities on the background.
/// You need specify workers count.
Async {
/// Workers count
workers: NonZeroUsize,
},
}

impl TrustedIssuerLoaderConfig {
pub(crate) fn is_sync(&self) -> bool {
matches!(self, TrustedIssuerLoaderConfig::Sync { .. })
}
}

impl Default for TrustedIssuerLoaderConfig {
fn default() -> Self {
Self::Sync {
workers: NonZeroUsize::new(1).unwrap(),
}
}
}
22 changes: 22 additions & 0 deletions jans-cedarling/cedarling/src/bootstrap_config/raw_config/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ use super::feature_types::{FeatureToggle, LoggerType};
use super::json_util::{deserialize_or_parse_string_as_json, parse_option_string};
use crate::UnsignedRoleIdSrc;
use crate::common::json_rules::JsonRule;
use crate::jwt_config::TrustedIssuerLoaderTypeRaw;
use crate::log::LogLevel;
use jsonwebtoken::Algorithm;
use serde::{Deserialize, Serialize};
Expand Down Expand Up @@ -305,6 +306,27 @@ pub struct BootstrapConfigRaw {
default = "default_true"
)]
pub token_cache_earliest_expiration_eviction: bool,
/// Type of trusted issuer loader.
/// If not set, synchronous loader is used.
/// Can be `SYNC` or `ASYNC`.
///
/// Sync loader means that trusted issuers will be loaded on initialization.
/// Async loader means that trusted issuers will be loaded in background.
#[serde(
rename = "CEDARLING_TRUSTED_ISSUER_LOADER_TYPE",
default,
deserialize_with = "deserialize_or_parse_string_as_json"
)]
pub trusted_issuer_loader_type: TrustedIssuerLoaderTypeRaw,
/// Number of concurrent workers used to asynchronously load trusted issuers.
/// Default value is 1.
/// Zero will be treated as default value.
#[serde(
rename = "CEDARLING_TRUSTED_ISSUER_LOADER_WORKERS",
default,
deserialize_with = "deserialize_or_parse_string_as_json"
)]
pub trusted_issuer_loader_workers: usize,
}

impl Default for BootstrapConfigRaw {
Expand Down
65 changes: 64 additions & 1 deletion jans-cedarling/cedarling/src/common/issuer_utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,73 @@
//
// Copyright (c) 2024, Gluu, Inc.

use std::fmt;

use serde::Serialize;
use serde::de::{self, Deserialize, Deserializer, Visitor};

/// Utility structure that holds a normalized issuer string
#[derive(Debug, Clone, PartialEq, Eq, Hash, derive_more::Display)]
pub(crate) struct IssClaim(String);

impl IssClaim {
/// Create a new Issuer with normalized value
pub(crate) fn new(issuer: &str) -> Self {
Self(normalize_issuer(issuer))
}

/// Get the issuer as a &str
pub(crate) fn as_str(&self) -> &str {
&self.0
}
}

impl<'de> Deserialize<'de> for IssClaim {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
struct IssClaimVisitor;

impl Visitor<'_> for IssClaimVisitor {
type Value = IssClaim;

fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
formatter.write_str("a string representing an issuer claim")
}

fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
where
E: de::Error,
{
Ok(IssClaim::new(value))
}

fn visit_string<E>(self, value: String) -> Result<Self::Value, E>
where
E: de::Error,
{
Ok(IssClaim::new(&value))
}
}

deserializer.deserialize_string(IssClaimVisitor)
}
}

impl Serialize for IssClaim {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
serializer.serialize_str(&self.0)
}
}

/// Normalizes an issuer URL by removing trailing slashes
/// This handles cases where IDPs like Auth0 return issuers with trailing slashes
/// but the policy store configuration might not have them
pub(crate) fn normalize_issuer(issuer: &str) -> String {
fn normalize_issuer(issuer: &str) -> String {
// Parse the issuer as a URL be consistent after parsing trusted issuers URL by `url` crate.
if let Ok(url) = url::Url::parse(issuer) {
url.to_string().trim_end_matches('/').to_string()
Expand Down
Loading