From 5089a64dc5a4a2d3171b2c140df47dad7c4b86fb Mon Sep 17 00:00:00 2001 From: Oleh Bohzok Date: Thu, 2 Jan 2025 21:40:48 +0200 Subject: [PATCH 01/43] chore(jans-cedarling): rebase to main Signed-off-by: Oleh Bohzok --- jans-cedarling/.cargo/config.toml | 2 + jans-cedarling/Cargo.toml | 3 +- .../bindings/cedarling_python/Cargo.toml | 6 +- .../src/authorize/authorize_result.rs | 2 +- .../cedarling_python/src/cedarling.rs | 4 +- .../bindings/cedarling_python/src/lib.rs | 3 +- .../bindings/cedarling_wasm/Cargo.toml | 25 +++ .../bindings/cedarling_wasm/README.md | 47 +++++ .../bindings/cedarling_wasm/index.html | 194 ++++++++++++++++++ .../bindings/cedarling_wasm/src/lib.rs | 193 +++++++++++++++++ .../bindings/cedarling_wasm/src/tests.rs | 89 ++++++++ jans-cedarling/cedarling/Cargo.toml | 13 +- .../examples/authorize_with_jwt_validation.rs | 38 ++-- .../authorize_without_jwt_validation.rs | 52 ++--- jans-cedarling/cedarling/examples/log_init.rs | 9 +- .../cedarling/src/authz/authorize_result.rs | 86 ++++---- .../cedarling/src/authz/entities/user.rs | 13 +- .../cedarling/src/authz/entities/workload.rs | 13 +- jans-cedarling/cedarling/src/authz/mod.rs | 78 ++++--- jans-cedarling/cedarling/src/blocking.rs | 57 +++++ .../cedarling/src/bootstrap_config/decode.rs | 5 +- .../cedarling/src/bootstrap_config/mod.rs | 13 +- jans-cedarling/cedarling/src/http/blocking.rs | 77 ------- jans-cedarling/cedarling/src/http/mod.rs | 139 ++++++++----- jans-cedarling/cedarling/src/http/wasm.rs | 36 ---- .../cedarling/src/init/policy_store.rs | 25 ++- .../cedarling/src/init/service_config.rs | 19 +- .../cedarling/src/init/service_factory.rs | 25 ++- jans-cedarling/cedarling/src/jwt/jwk_store.rs | 24 ++- .../cedarling/src/jwt/key_service.rs | 57 +++-- jans-cedarling/cedarling/src/jwt/mod.rs | 12 +- jans-cedarling/cedarling/src/lib.rs | 26 ++- jans-cedarling/cedarling/src/log/log_entry.rs | 27 ++- .../cedarling/src/log/log_strategy.rs | 2 +- .../cedarling/src/log/memory_logger.rs | 13 +- jans-cedarling/cedarling/src/log/test.rs | 1 + .../cases_authorize_different_principals.rs | 80 +++++--- .../tests/cases_authorize_namespace_jans2.rs | 10 +- .../cases_authorize_without_check_jwt.rs | 86 ++++---- .../cedarling/src/tests/mapping_entities.rs | 64 ++++-- .../src/tests/schema_type_mapping.rs | 8 +- .../cedarling/src/tests/success_test_json.rs | 8 +- .../src/tests/utils/cedarling_util.rs | 6 +- jans-cedarling/flask-sidecar/Dockerfile | 4 +- jans-cedarling/flask-sidecar/pyproject.toml | 2 +- jans-cedarling/sparkv/Cargo.toml | 1 + jans-cedarling/sparkv/README.md | 2 +- jans-cedarling/sparkv/src/config.rs | 20 +- jans-cedarling/sparkv/src/expentry.rs | 27 +-- jans-cedarling/sparkv/src/kventry.rs | 18 +- jans-cedarling/sparkv/src/lib.rs | 145 +++++++++---- 51 files changed, 1344 insertions(+), 565 deletions(-) create mode 100644 jans-cedarling/.cargo/config.toml create mode 100644 jans-cedarling/bindings/cedarling_wasm/Cargo.toml create mode 100644 jans-cedarling/bindings/cedarling_wasm/README.md create mode 100644 jans-cedarling/bindings/cedarling_wasm/index.html create mode 100644 jans-cedarling/bindings/cedarling_wasm/src/lib.rs create mode 100644 jans-cedarling/bindings/cedarling_wasm/src/tests.rs create mode 100644 jans-cedarling/cedarling/src/blocking.rs delete mode 100644 jans-cedarling/cedarling/src/http/blocking.rs delete mode 100644 jans-cedarling/cedarling/src/http/wasm.rs diff --git a/jans-cedarling/.cargo/config.toml b/jans-cedarling/.cargo/config.toml new file mode 100644 index 00000000000..4ec2f3b8620 --- /dev/null +++ b/jans-cedarling/.cargo/config.toml @@ -0,0 +1,2 @@ +[target.wasm32-unknown-unknown] +runner = 'wasm-bindgen-test-runner' diff --git a/jans-cedarling/Cargo.toml b/jans-cedarling/Cargo.toml index 623620c7158..73fde5c5c0b 100644 --- a/jans-cedarling/Cargo.toml +++ b/jans-cedarling/Cargo.toml @@ -7,13 +7,14 @@ serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" thiserror = "1.0" sparkv = { path = "sparkv" } +chrono = "0.4" cedarling = { path = "cedarling" } test_utils = { path = "test_utils" } [profile.release] strip = "symbols" -debug-assertions = true +debug-assertions = false lto = "thin" opt-level = "s" codegen-units = 1 diff --git a/jans-cedarling/bindings/cedarling_python/Cargo.toml b/jans-cedarling/bindings/cedarling_python/Cargo.toml index 2ad9e97c089..4424b1d2527 100644 --- a/jans-cedarling/bindings/cedarling_python/Cargo.toml +++ b/jans-cedarling/bindings/cedarling_python/Cargo.toml @@ -7,10 +7,10 @@ edition = "2021" name = "cedarling_python" crate-type = ["cdylib"] -[dependencies] +# dependency for NOT wasm target +[target.'cfg(not(target_arch = "wasm32"))'.dependencies] pyo3 = { version = "0.22.5", features = ["extension-module", "gil-refs"] } -cedarling = { workspace = true } +cedarling = { workspace = true, features = ["blocking"] } serde = { workspace = true } serde_json = { workspace = true } serde-pyobject = "0.4.0" -jsonwebtoken = "9.3.0" diff --git a/jans-cedarling/bindings/cedarling_python/src/authorize/authorize_result.rs b/jans-cedarling/bindings/cedarling_python/src/authorize/authorize_result.rs index fdd607bb0a5..a83372f48d1 100644 --- a/jans-cedarling/bindings/cedarling_python/src/authorize/authorize_result.rs +++ b/jans-cedarling/bindings/cedarling_python/src/authorize/authorize_result.rs @@ -30,7 +30,7 @@ pub struct AuthorizeResult { impl AuthorizeResult { /// Returns true if request is allowed fn is_allowed(&self) -> bool { - self.inner.is_allowed() + self.inner.decision } /// Get the decision value for workload diff --git a/jans-cedarling/bindings/cedarling_python/src/cedarling.rs b/jans-cedarling/bindings/cedarling_python/src/cedarling.rs index 8de129b1e77..6ca9cffb2f7 100644 --- a/jans-cedarling/bindings/cedarling_python/src/cedarling.rs +++ b/jans-cedarling/bindings/cedarling_python/src/cedarling.rs @@ -62,14 +62,14 @@ use serde_pyobject::to_pyobject; #[derive(Clone)] #[pyclass] pub struct Cedarling { - inner: cedarling::Cedarling, + inner: cedarling::blocking::Cedarling, } #[pymethods] impl Cedarling { #[new] fn new(config: &BootstrapConfig) -> PyResult { - let inner = cedarling::Cedarling::new(config.inner()) + let inner = cedarling::blocking::Cedarling::new(config.inner()) .map_err(|err| PyValueError::new_err(err.to_string()))?; Ok(Self { inner }) } diff --git a/jans-cedarling/bindings/cedarling_python/src/lib.rs b/jans-cedarling/bindings/cedarling_python/src/lib.rs index 685260123e0..26f92fd5604 100644 --- a/jans-cedarling/bindings/cedarling_python/src/lib.rs +++ b/jans-cedarling/bindings/cedarling_python/src/lib.rs @@ -4,9 +4,10 @@ * * Copyright (c) 2024, Gluu, Inc. */ +#![cfg(not(target_arch = "wasm32"))] -use pyo3::prelude::*; use pyo3::Bound; +use pyo3::prelude::*; mod authorize; mod cedarling; diff --git a/jans-cedarling/bindings/cedarling_wasm/Cargo.toml b/jans-cedarling/bindings/cedarling_wasm/Cargo.toml new file mode 100644 index 00000000000..9b86e72eea5 --- /dev/null +++ b/jans-cedarling/bindings/cedarling_wasm/Cargo.toml @@ -0,0 +1,25 @@ +[package] +name = "cedarling_wasm" +version = "0.0.0" +edition = "2021" + +[lib] +crate-type = ["cdylib"] # Required for WASM output + +[dependencies] +# Common dependency for WASM interop +wasm-bindgen = { version = "0.2", features = [ + "serde-serialize", + "enable-interning", +] } +cedarling = { workspace = true } +serde_json = { workspace = true } +serde-wasm-bindgen = "0.6" +wasm-bindgen-futures = "0.4" +wasm-bindgen-test = "0.3.49" + +[profile.release] +lto = true + +[package.metadata.wasm-pack.profile.release] +wasm-opt = ['-O4', '--enable-reference-types', '--enable-gc'] diff --git a/jans-cedarling/bindings/cedarling_wasm/README.md b/jans-cedarling/bindings/cedarling_wasm/README.md new file mode 100644 index 00000000000..d85679de3d8 --- /dev/null +++ b/jans-cedarling/bindings/cedarling_wasm/README.md @@ -0,0 +1,47 @@ +# Cedarling WASM + +This module is designed to build cedarling for browser wasm. + +## Building + +For building we use [`wasm-pack`](https://developer.mozilla.org/en-US/docs/WebAssembly/Rust_to_Wasm) for install you can use command `cargo install wasm-pack` + +Build cedarling in release: + +```bash +wasm-pack build --release --target web +``` + +Build cedarling in dev mode + +```bash +wasm-pack build --target web --dev +``` + +Result files will be in `pkg` folder. + +## Testing + +For WASM testing we use `wasm-pack` and it allows to make test in `node`, `chrome`, `firefox`, `safari`. You just need specify appropriate flag. + +Example for firefox. + +```bash +wasm-pack test --firefox +``` + +## Run browser example + +To run example using `index.html` you need execute following steps: + +1. Build wasm cedarling. +2. Run webserver using `python3 -m http.server` or any other. +3. Visit [localhost](http://localhost:8000/). + +## Optimization wasm binary + +You can try to use `wasm-opt`, a C++ tool for optimize WebAssembly, you can make it even smaller too! `WASM` file holds in the `pkg` folder. + +```bash +wasm-opt -Os cedarling_wasm_bg.wasm -o cedarling_wasm.wasm +``` diff --git a/jans-cedarling/bindings/cedarling_wasm/index.html b/jans-cedarling/bindings/cedarling_wasm/index.html new file mode 100644 index 00000000000..246eeae9de3 --- /dev/null +++ b/jans-cedarling/bindings/cedarling_wasm/index.html @@ -0,0 +1,194 @@ + + + + + + hello-wasm example + + + + + + + \ No newline at end of file diff --git a/jans-cedarling/bindings/cedarling_wasm/src/lib.rs b/jans-cedarling/bindings/cedarling_wasm/src/lib.rs new file mode 100644 index 00000000000..c3a54f5c760 --- /dev/null +++ b/jans-cedarling/bindings/cedarling_wasm/src/lib.rs @@ -0,0 +1,193 @@ +// 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. + +// #![cfg(target_arch = "wasm32")] + +use cedarling::bindings::cedar_policy; +use cedarling::{BootstrapConfig, BootstrapConfigRaw, Request}; +use serde_wasm_bindgen::Error; +use std::rc::Rc; +use wasm_bindgen::prelude::*; +use wasm_bindgen_futures::js_sys::{Map, Object}; +use wasm_bindgen_test::console_log; + +#[cfg(test)] +mod tests; + +/// The instance of the Cedarling application. +#[wasm_bindgen] +#[derive(Clone)] +pub struct Cedarling { + instance: cedarling::Cedarling, +} + +/// Create a new instance of the Cedarling application. +/// This function can take as config parameter the eather `Map` other `Object` +#[wasm_bindgen] +pub async fn init(config: JsValue) -> Result { + if config.is_instance_of::() { + // convert to map + let config_map: Map = config.unchecked_into(); + Cedarling::new_from_map(config_map).await + } else if let Some(config_object) = Object::try_from(&config) { + console_log!("init as object"); + Cedarling::new(config_object).await + } else { + Err(Error::new("config should be Map or Object")) + } +} + +#[wasm_bindgen] +impl Cedarling { + /// Create a new instance of the Cedarling application. + /// Assume that config is `Object` + pub async fn new(config: &Object) -> Result { + let config: BootstrapConfigRaw = serde_wasm_bindgen::from_value(config.into())?; + + let config = BootstrapConfig::from_raw_config(&config).map_err(Error::new)?; + + cedarling::Cedarling::new(&config) + .await + .map(|instance| Cedarling { instance }) + .map_err(Error::new) + } + + /// Create a new instance of the Cedarling application. + /// Assume that config is `Object` + pub async fn new_from_map(config: Map) -> Result { + let conf_js_val = config.unchecked_into(); + + let conf_object = Object::from_entries(&conf_js_val)?; + Self::new(&conf_object).await + } + + /// Authorize request + /// makes authorization decision based on the [`Request`] + pub async fn authorize(&self, request: JsValue) -> Result { + let request: Request = serde_wasm_bindgen::from_value(request)?; + + let result = self.instance.authorize(request).await.map_err(Error::new)?; + Ok(result.into()) + } +} + +/// A WASM wrapper for the Rust `cedarling::AuthorizeResult` struct. +/// Represents the result of an authorization request. +#[wasm_bindgen] +pub struct AuthorizeResult { + /// Result of authorization where principal is `Jans::Workload` + #[wasm_bindgen(getter_with_clone)] + pub workload: Option, + /// Result of authorization where principal is `Jans::User` + #[wasm_bindgen(getter_with_clone)] + pub person: Option, + + /// Result of authorization + /// true means `ALLOW` + /// false means `Deny` + /// + /// this field is [`bool`] type to be compatible with [authzen Access Evaluation Decision](https://openid.github.io/authzen/#section-6.2.1). + pub decision: bool, +} + +impl From for AuthorizeResult { + fn from(value: cedarling::AuthorizeResult) -> Self { + Self { + workload: value + .workload + .map(|v| AuthorizeResultResponse { inner: Rc::new(v) }), + person: value + .person + .map(|v| AuthorizeResultResponse { inner: Rc::new(v) }), + decision: value.decision, + } + } +} + +/// A WASM wrapper for the Rust `cedar_policy::Response` struct. +/// Represents the result of an authorization request. +#[wasm_bindgen] +#[derive(Clone)] +pub struct AuthorizeResultResponse { + // It can be premature optimization, but RC allows avoiding clone actual structure + inner: Rc, +} + +#[wasm_bindgen] +impl AuthorizeResultResponse { + /// Authorization decision + #[wasm_bindgen(getter)] + pub fn decision(&self) -> bool { + self.inner.decision() == cedar_policy::Decision::Allow + } + + /// Diagnostics providing more information on how this decision was reached + #[wasm_bindgen(getter)] + pub fn diagnostics(&self) -> Diagnostics { + Diagnostics { + inner: self.inner.diagnostics().clone(), + } + } +} + +/// Diagnostics +/// =========== +/// +/// Provides detailed information about how a policy decision was made, including policies that contributed to the decision and any errors encountered during evaluation. +#[wasm_bindgen] +pub struct Diagnostics { + inner: cedar_policy::Diagnostics, +} + +#[wasm_bindgen] +impl Diagnostics { + /// `PolicyId`s of the policies that contributed to the decision. + /// If no policies applied to the request, this set will be empty. + /// + /// The ids should be treated as unordered, + #[wasm_bindgen(getter)] + pub fn reason(&self) -> Vec { + self.inner.reason().map(|v| v.to_string()).collect() + } + + /// Errors that occurred during authorization. The errors should be + /// treated as unordered, since policies may be evaluated in any order. + #[wasm_bindgen(getter)] + pub fn errors(&self) -> Vec { + self.inner + .errors() + .map(|err| { + let mapped_error: cedarling::bindings::PolicyEvaluationError = err.into(); + PolicyEvaluationError { + inner: mapped_error, + } + }) + .collect() + } +} + +/// PolicyEvaluationError +/// ===================== +/// +/// Represents an error that occurred when evaluating a Cedar policy. +#[wasm_bindgen] +pub struct PolicyEvaluationError { + inner: cedarling::bindings::PolicyEvaluationError, +} + +#[wasm_bindgen] +impl PolicyEvaluationError { + /// Id of the policy with an error + #[wasm_bindgen(getter)] + pub fn id(&self) -> String { + self.inner.id.clone() + } + + /// Underlying evaluation error string representation + #[wasm_bindgen(getter)] + pub fn error(&self) -> String { + self.inner.error.clone() + } +} diff --git a/jans-cedarling/bindings/cedarling_wasm/src/tests.rs b/jans-cedarling/bindings/cedarling_wasm/src/tests.rs new file mode 100644 index 00000000000..7c62573ead3 --- /dev/null +++ b/jans-cedarling/bindings/cedarling_wasm/src/tests.rs @@ -0,0 +1,89 @@ +// allow dead code to avoid highlight test functions (by linter) that is used only using WASM +#![allow(dead_code)] + +use std::sync::LazyLock; + +use crate::*; + +use wasm_bindgen_test::*; + +wasm_bindgen_test_configure!(run_in_browser); + +// Reuse json policy store file from python example. +// Because for `BootstrapConfigRaw` we need to use JSON +static POLICY_STORE_RAW_YAML: &str = + include_str!("../../../bindings/cedarling_python/example_files/policy-store.json"); + +static BOOTSTRAP_CONFIG: LazyLock = LazyLock::new(|| { + serde_json::json!({ + "CEDARLING_APPLICATION_NAME": "My App", + "CEDARLING_LOCAL_POLICY_STORE": POLICY_STORE_RAW_YAML, + "CEDARLING_LOG_TYPE": "std_out", + "CEDARLING_LOG_LEVEL": "INFO", + "CEDARLING_USER_AUTHZ": "enabled", + "CEDARLING_WORKLOAD_AUTHZ": "enabled", + "CEDARLING_USER_WORKLOAD_BOOLEAN_OPERATION": "AND", + "CEDARLING_ID_TOKEN_TRUST_MODE": "strict", + + }) +}); + +/// test init with map value using `Cedarling::new_from_map` +#[wasm_bindgen_test] +async fn test_cedarling_new_from_map() { + let bootstrap_config_json = BOOTSTRAP_CONFIG.clone(); + let conf_map_js_value = serde_wasm_bindgen::to_value(&bootstrap_config_json) + .expect("serde json value should be converted to JsValue"); + console_log!("conf_map_js_value: {conf_map_js_value:?}"); + + let conf_js_map: Map = conf_map_js_value.unchecked_into(); + console_log!("conf_js_map: {conf_js_map:?}"); + let _instance = Cedarling::new_from_map(conf_js_map.clone()) + .await + .inspect(|_| console_log!("Cedarling::new_from_map initialized successfully")) + .expect("Cedarling::new_from_map should be initialized"); +} + +/// test init with map value using `init` +#[wasm_bindgen_test] +async fn test_init_conf_as_map() { + let bootstrap_config_json = BOOTSTRAP_CONFIG.clone(); + let conf_map_js_value = serde_wasm_bindgen::to_value(&bootstrap_config_json) + .expect("serde json value should be converted to JsValue"); + console_log!("conf_map_js_value: {conf_map_js_value:?}"); + + let _instance = init(conf_map_js_value) + .await + .inspect(|_| console_log!("init initialized successfully")) + .expect("init function should be initialized with js map"); +} + +/// test init with object value using `Cedarling::new` +#[wasm_bindgen_test] +async fn test_cedarling_new_from_object() { + let bootstrap_config_json = BOOTSTRAP_CONFIG.clone(); + let conf_map_js_value = serde_wasm_bindgen::to_value(&bootstrap_config_json) + .expect("serde json value should be converted to JsValue"); + + let conf_object = + Object::from_entries(&conf_map_js_value).expect("map value should be converted to object"); + + let _instance = Cedarling::new(&conf_object) + .await + .expect("Cedarling::new_from_map should be initialized"); +} + +/// test init with object value using `init` +#[wasm_bindgen_test] +async fn test_init_conf_as_object() { + let bootstrap_config_json = BOOTSTRAP_CONFIG.clone(); + let conf_map_js_value = serde_wasm_bindgen::to_value(&bootstrap_config_json) + .expect("serde json value should be converted to JsValue"); + + let conf_object = + Object::from_entries(&conf_map_js_value).expect("map value should be converted to object"); + + let _instance = init(conf_object.into()) + .await + .expect("init function should be initialized with js map"); +} diff --git a/jans-cedarling/cedarling/Cargo.toml b/jans-cedarling/cedarling/Cargo.toml index bad3ee54ef6..6e9228fbe1e 100644 --- a/jans-cedarling/cedarling/Cargo.toml +++ b/jans-cedarling/cedarling/Cargo.toml @@ -3,6 +3,13 @@ name = "cedarling" version = "0.0.0-nightly" edition = "2021" +[features] +# blocking with default is used only for local testing, next line should be commented +# default = ["blocking"] + +# blocking feature allows to use blocking cedarling client +blocking = ["tokio/rt-multi-thread"] + [dependencies] serde = { workspace = true } serde_json = { workspace = true } @@ -15,7 +22,7 @@ base64 = "0.22.1" url = "2.5.2" lazy_static = "1.5.0" jsonwebtoken = "9.3.0" -reqwest = { version = "0.12.8", features = ["blocking", "json"] } +reqwest = { version = "0.12.8", features = ["json"] } bytes = "1.7.2" typed-builder = "0.20.0" semver = { version = "1.0.23", features = ["serde"] } @@ -27,7 +34,9 @@ derive_more = { version = "1.0.0", features = [ ] } time = { version = "0.3.36", features = ["wasm-bindgen"] } regex = "1.11.1" -chrono = "0.4.38" +chrono = { workspace = true } +tokio = { version = "1.42.0", features = ["macros", "time"] } +rand = "0.8.5" [dev-dependencies] # is used in testing diff --git a/jans-cedarling/cedarling/examples/authorize_with_jwt_validation.rs b/jans-cedarling/cedarling/examples/authorize_with_jwt_validation.rs index 802712a49f7..ba3a63ea27a 100644 --- a/jans-cedarling/cedarling/examples/authorize_with_jwt_validation.rs +++ b/jans-cedarling/cedarling/examples/authorize_with_jwt_validation.rs @@ -15,7 +15,8 @@ use jsonwebtoken::Algorithm; static POLICY_STORE_RAW_YAML: &str = include_str!("../../test_files/policy-store_with_trusted_issuers_ok.yaml"); -fn main() -> Result<(), Box> { +#[tokio::main(flavor = "current_thread")] +async fn main() -> Result<(), Box> { // Configure JWT validation settings. Enable the JwtService to validate JWT tokens // using specific algorithms: `HS256` and `RS256`. Only tokens signed with these algorithms // will be accepted; others will be marked as invalid during validation. @@ -54,27 +55,30 @@ fn main() -> Result<(), Box> { user_workload_operator: WorkloadBoolOp::And, ..Default::default() }, - })?; + }) + .await?; // Perform an authorization request to Cedarling. // This request checks if the provided tokens have sufficient permission to perform an action // on a specific resource. Each token (access, ID, and userinfo) is required for the // authorization process, alongside resource and action details. - let result = cedarling.authorize(Request { - access_token: Some(access_token), - id_token: Some(id_token), - userinfo_token: Some(userinfo_token), - action: "Jans::Action::\"Update\"".to_string(), - context: serde_json::json!({}), - resource: ResourceData { - id: "random_id".to_string(), - resource_type: "Jans::Issue".to_string(), - payload: HashMap::from_iter([( - "org_id".to_string(), - serde_json::Value::String("some_long_id".to_string()), - )]), - }, - }); + let result = cedarling + .authorize(Request { + access_token: Some(access_token), + id_token: Some(id_token), + userinfo_token: Some(userinfo_token), + action: "Jans::Action::\"Update\"".to_string(), + context: serde_json::json!({}), + resource: ResourceData { + id: "random_id".to_string(), + resource_type: "Jans::Issue".to_string(), + payload: HashMap::from_iter([( + "org_id".to_string(), + serde_json::Value::String("some_long_id".to_string()), + )]), + }, + }) + .await; // Handle authorization result. If there's an error, print it. if let Err(ref e) = &result { diff --git a/jans-cedarling/cedarling/examples/authorize_without_jwt_validation.rs b/jans-cedarling/cedarling/examples/authorize_without_jwt_validation.rs index af31d0f2f78..d151f997a87 100644 --- a/jans-cedarling/cedarling/examples/authorize_without_jwt_validation.rs +++ b/jans-cedarling/cedarling/examples/authorize_without_jwt_validation.rs @@ -12,7 +12,8 @@ use cedarling::{ static POLICY_STORE_RAW: &str = include_str!("../../test_files/policy-store_ok.yaml"); -fn main() -> Result<(), Box> { +#[tokio::main(flavor = "current_thread")] +async fn main() -> Result<(), Box> { let cedarling = Cedarling::new(&BootstrapConfig { application_name: "test_app".to_string(), log_config: LogConfig { @@ -32,7 +33,8 @@ fn main() -> Result<(), Box> { decision_log_workload_claims: vec!["org_id".to_string()], ..Default::default() }, - })?; + }) + .await?; // the following tokens are expired // access_token claims: @@ -111,31 +113,33 @@ fn main() -> Result<(), Box> { // } let userinfo_token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJodHRwczovL2FkbWluLXVpLXRlc3QuZ2x1dS5vcmciLCJzdWIiOiJib0c4ZGZjNU1LVG4zN283Z3NkQ2V5cUw4THBXUXRnb080MW0xS1p3ZHEwIiwiY2xpZW50X2lkIjoiNWI0NDg3YzQtOGRiMS00MDlkLWE2NTMtZjkwN2I4MDk0MDM5IiwiYXVkIjoiNWI0NDg3YzQtOGRiMS00MDlkLWE2NTMtZjkwN2I4MDk0MDM5IiwidXNlcm5hbWUiOiJhZG1pbkBnbHV1Lm9yZyIsIm5hbWUiOiJEZWZhdWx0IEFkbWluIFVzZXIiLCJlbWFpbCI6ImFkbWluQGdsdXUub3JnIiwiY291bnRyeSI6IlVTIiwianRpIjoidXNyaW5mb190a25fanRpIn0.NoR53vPZFpfb4vFk85JH9RPx7CHsaJMZwrH3fnB-N60".to_string(); - let result = cedarling.authorize(Request { - access_token: Some(access_token), - id_token: Some(id_token), - userinfo_token: Some(userinfo_token), - action: "Jans::Action::\"Update\"".to_string(), - context: serde_json::json!({}), - resource: ResourceData { - id: "random_id".to_string(), - resource_type: "Jans::Issue".to_string(), - payload: HashMap::from_iter([ - ( - "org_id".to_string(), - serde_json::Value::String("some_long_id".to_string()), - ), - ( - "country".to_string(), - serde_json::Value::String("US".to_string()), - ), - ]), - }, - }); + let result = cedarling + .authorize(Request { + access_token: Some(access_token), + id_token: Some(id_token), + userinfo_token: Some(userinfo_token), + action: "Jans::Action::\"Update\"".to_string(), + context: serde_json::json!({}), + resource: ResourceData { + id: "random_id".to_string(), + resource_type: "Jans::Issue".to_string(), + payload: HashMap::from_iter([ + ( + "org_id".to_string(), + serde_json::Value::String("some_long_id".to_string()), + ), + ( + "country".to_string(), + serde_json::Value::String("US".to_string()), + ), + ]), + }, + }) + .await; match result { Ok(result) => { - println!("\n\nis allowed: {}", result.is_allowed()); + println!("\n\nis allowed: {}", result.decision); }, Err(e) => eprintln!("Error while authorizing: {}\n {:?}\n\n", e, e), } diff --git a/jans-cedarling/cedarling/examples/log_init.rs b/jans-cedarling/cedarling/examples/log_init.rs index 585beb5f406..d8f1538b755 100644 --- a/jans-cedarling/cedarling/examples/log_init.rs +++ b/jans-cedarling/cedarling/examples/log_init.rs @@ -9,18 +9,18 @@ // and `use std::env` prevents that compilation. #![cfg(not(target_family = "wasm"))] -use std::env; - use cedarling::{ AuthorizationConfig, BootstrapConfig, Cedarling, JwtConfig, LogConfig, LogLevel, LogStorage, LogTypeConfig, MemoryLogConfig, PolicyStoreConfig, PolicyStoreSource, WorkloadBoolOp, }; +use std::env; // The human-readable policy and schema file is located in next folder: // `test_files\policy-store_ok` static POLICY_STORE_RAW: &str = include_str!("../../test_files/policy-store_ok.yaml"); -fn main() -> Result<(), Box> { +#[tokio::main(flavor = "current_thread")] +async fn main() -> Result<(), Box> { // Collect command-line arguments let args: Vec = env::args().collect(); @@ -61,7 +61,8 @@ fn main() -> Result<(), Box> { user_workload_operator: WorkloadBoolOp::And, ..Default::default() }, - })?; + }) + .await?; println!("Stage 1:"); let logs_ids = cedarling.get_log_ids(); diff --git a/jans-cedarling/cedarling/src/authz/authorize_result.rs b/jans-cedarling/cedarling/src/authz/authorize_result.rs index 8d5da5cfa59..86f0ebf9bc8 100644 --- a/jans-cedarling/cedarling/src/authz/authorize_result.rs +++ b/jans-cedarling/cedarling/src/authz/authorize_result.rs @@ -1,13 +1,14 @@ -// 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::collections::HashSet; +/* + * 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 cedar_policy::Decision; use serde::ser::SerializeStruct; use serde::{Serialize, Serializer}; +use std::collections::HashSet; use crate::bootstrap_config::WorkloadBoolOp; @@ -15,13 +16,19 @@ use crate::bootstrap_config::WorkloadBoolOp; /// based on the [Request](crate::models::request::Request) and policy store #[derive(Debug, Clone, Serialize)] pub struct AuthorizeResult { - user_workload_operator: WorkloadBoolOp, /// Result of authorization where principal is `Jans::Workload` #[serde(serialize_with = "serialize_opt_response")] pub workload: Option, /// Result of authorization where principal is `Jans::User` #[serde(serialize_with = "serialize_opt_response")] pub person: Option, + + /// Result of authorization + /// true means `ALLOW` + /// false means `Deny` + /// + /// this field is [`bool`] type to be compatible with [authzen Access Evaluation Decision](https://openid.github.io/authzen/#section-6.2.1). + pub decision: bool, } /// Custom serializer for an Option which converts `None` to an empty string and vice versa. @@ -67,50 +74,47 @@ impl AuthorizeResult { person: Option, ) -> Self { Self { - user_workload_operator, + decision: calc_decision(&user_workload_operator, &workload, &person), workload, person, } } - /// Evaluates the authorization result to determine if the request is allowed. - /// - /// This function checks the decision based on the following rule: - /// - The `workload` must allow the request (PRINCIPAL). - /// - Either the `person` must also allow the request. - /// - /// This approach represents decision-making model, where the - /// `workload` (i.e., primary principal) needs to permit the request and - /// additional conditions `person` must also indicate allowance. - /// - /// If person and wokload is present will be used operator (AND or OR) based on `CEDARLING_USER_WORKLOAD_BOOLEAN_OPERATION` bootstrap property. - pub fn is_allowed(&self) -> bool { - let workload_allowed = self - .workload - .as_ref() - .map(|response| response.decision() == Decision::Allow); - - let person_allowed = self - .person - .as_ref() - .map(|response| response.decision() == Decision::Allow); - - // cover each possible case when any of value is Some or None - match (workload_allowed, person_allowed) { - (None, None) => false, - (None, Some(person)) => person, - (Some(workload), None) => workload, - (Some(workload), Some(person)) => self.user_workload_operator.calc(workload, person), - } - } - /// Decision of result /// works based on [`AuthorizeResult::is_allowed`] - pub fn decision(&self) -> Decision { - if self.is_allowed() { + pub fn cedar_decision(&self) -> Decision { + if self.decision { Decision::Allow } else { Decision::Deny } } } + +/// Evaluates the authorization result to determine if the request is allowed. +/// +/// If present only workload result return true if decision is `ALLOW`. +/// If present only person result return true if decision is `ALLOW`. +/// If person and workload is present will be used operator (AND or OR) based on `CEDARLING_USER_WORKLOAD_BOOLEAN_OPERATION` bootstrap property. +/// If none present return false. +fn calc_decision( + user_workload_operator: &WorkloadBoolOp, + workload: &Option, + person: &Option, +) -> bool { + let workload_allowed = workload + .as_ref() + .map(|response| response.decision() == Decision::Allow); + + let person_allowed = person + .as_ref() + .map(|response| response.decision() == Decision::Allow); + + // cover each possible case when any of value is Some or None + match (workload_allowed, person_allowed) { + (None, None) => false, + (None, Some(person)) => person, + (Some(workload), None) => workload, + (Some(workload), Some(person)) => user_workload_operator.calc(workload, person), + } +} diff --git a/jans-cedarling/cedarling/src/authz/entities/user.rs b/jans-cedarling/cedarling/src/authz/entities/user.rs index da43a4f6480..fd50773bad9 100644 --- a/jans-cedarling/cedarling/src/authz/entities/user.rs +++ b/jans-cedarling/cedarling/src/authz/entities/user.rs @@ -91,6 +91,7 @@ mod test { use cedar_policy::{Entity, RestrictedExpression}; use serde_json::json; use test_utils::assert_eq; + use tokio::test; use super::create_user_entity; use crate::authz::entities::DecodedTokens; @@ -100,13 +101,14 @@ mod test { use crate::{CreateCedarEntityError, PolicyStoreConfig, PolicyStoreSource}; #[test] - fn can_create_from_id_token() { + async fn can_create_from_id_token() { let entity_mapping = None; let policy_store = load_policy_store(&PolicyStoreConfig { source: PolicyStoreSource::FileYaml( Path::new("../test_files/policy-store_ok_2.yaml").into(), ), }) + .await .expect("Should load policy store") .store; @@ -141,13 +143,14 @@ mod test { } #[test] - fn can_create_from_userinfo_token() { + async fn can_create_from_userinfo_token() { let entity_mapping = None; let policy_store = load_policy_store(&PolicyStoreConfig { source: PolicyStoreSource::FileYaml( Path::new("../test_files/policy-store_ok_2.yaml").into(), ), }) + .await .expect("Should load policy store") .store; @@ -182,13 +185,14 @@ mod test { } #[test] - fn errors_when_tokens_have_missing_claims() { + async fn errors_when_tokens_have_missing_claims() { let entity_mapping = None; let policy_store = load_policy_store(&PolicyStoreConfig { source: PolicyStoreSource::FileYaml( Path::new("../test_files/policy-store_ok_2.yaml").into(), ), }) + .await .expect("Should load policy store") .store; @@ -221,13 +225,14 @@ mod test { } #[test] - fn errors_when_tokens_unavailable() { + async fn errors_when_tokens_unavailable() { let entity_mapping = None; let policy_store = load_policy_store(&PolicyStoreConfig { source: PolicyStoreSource::FileYaml( Path::new("../test_files/policy-store_ok_2.yaml").into(), ), }) + .await .expect("Should load policy store") .store; diff --git a/jans-cedarling/cedarling/src/authz/entities/workload.rs b/jans-cedarling/cedarling/src/authz/entities/workload.rs index bb325b7cf59..48feee62a8f 100644 --- a/jans-cedarling/cedarling/src/authz/entities/workload.rs +++ b/jans-cedarling/cedarling/src/authz/entities/workload.rs @@ -87,6 +87,7 @@ mod test { use cedar_policy::{Entity, RestrictedExpression}; use serde_json::json; use test_utils::assert_eq; + use tokio::test; use super::create_workload_entity; use crate::authz::entities::DecodedTokens; @@ -96,13 +97,14 @@ mod test { use crate::{CreateCedarEntityError, PolicyStoreConfig, PolicyStoreSource}; #[test] - fn can_create_from_id_token() { + async fn can_create_from_id_token() { let entity_mapping = None; let policy_store = load_policy_store(&PolicyStoreConfig { source: PolicyStoreSource::FileYaml( Path::new("../test_files/policy-store_ok_2.yaml").into(), ), }) + .await .expect("Should load policy store") .store; @@ -137,13 +139,14 @@ mod test { } #[test] - fn can_create_from_access_token() { + async fn can_create_from_access_token() { let entity_mapping = None; let policy_store = load_policy_store(&PolicyStoreConfig { source: PolicyStoreSource::FileYaml( Path::new("../test_files/policy-store_ok_2.yaml").into(), ), }) + .await .expect("Should load policy store") .store; @@ -178,13 +181,14 @@ mod test { } #[test] - fn errors_when_tokens_have_missing_claims() { + async fn errors_when_tokens_have_missing_claims() { let entity_mapping = None; let policy_store = load_policy_store(&PolicyStoreConfig { source: PolicyStoreSource::FileYaml( Path::new("../test_files/policy-store_ok_2.yaml").into(), ), }) + .await .expect("Should load policy store") .store; @@ -213,13 +217,14 @@ mod test { } #[test] - fn errors_when_tokens_unavailable() { + async fn errors_when_tokens_unavailable() { let entity_mapping = None; let policy_store = load_policy_store(&PolicyStoreConfig { source: PolicyStoreSource::FileYaml( Path::new("../test_files/policy-store_ok_2.yaml").into(), ), }) + .await .expect("Should load policy store") .store; diff --git a/jans-cedarling/cedarling/src/authz/mod.rs b/jans-cedarling/cedarling/src/authz/mod.rs index 8f3b0fa217b..b0d346b43c0 100644 --- a/jans-cedarling/cedarling/src/authz/mod.rs +++ b/jans-cedarling/cedarling/src/authz/mod.rs @@ -18,6 +18,7 @@ use crate::common::app_types; use crate::common::cedar_schema::cedar_json::{BuildJsonCtxError, FindActionError}; use crate::common::policy_store::PolicyStoreWithID; use crate::jwt::{self, TokenStr}; + use crate::log::interface::LogWriter; use crate::log::{ AuthorizationLogInfo, BaseLogEntry, DecisionLogEntry, Diagnostics, LogEntry, LogLevel, @@ -29,10 +30,10 @@ mod merge_json; pub(crate) mod entities; pub(crate) mod request; -use std::time::Instant; pub use authorize_result::AuthorizeResult; use cedar_policy::{ContextJsonError, Entities, Entity, EntityUid}; +use chrono::Utc; use entities::{ CEDAR_POLICY_SEPARATOR, CreateCedarEntityError, CreateUserEntityError, CreateWorkloadEntityError, DecodedTokens, ResourceEntityError, RoleEntityError, @@ -82,29 +83,42 @@ impl Authz { } // decode JWT tokens to structs AccessTokenData, IdTokenData, UserInfoTokenData using jwt service - pub(crate) fn decode_tokens<'a>( + pub(crate) async fn decode_tokens<'a>( &'a self, request: &'a Request, ) -> Result, AuthorizeError> { - let access_token = request - .access_token - .as_ref() - .map(|tkn| self.config.jwt_service.process_token(TokenStr::Access(tkn))) - .transpose()?; - let id_token = request - .id_token - .as_ref() - .map(|tkn| self.config.jwt_service.process_token(TokenStr::Id(tkn))) - .transpose()?; - let userinfo_token = request - .userinfo_token - .as_ref() - .map(|tkn| { + let access_token = if let Some(tkn) = request.access_token.as_ref() { + Some( self.config .jwt_service - .process_token(TokenStr::Userinfo(tkn)) - }) - .transpose()?; + .process_token(TokenStr::Access(tkn.as_str())) + .await?, + ) + } else { + None + }; + + let id_token = if let Some(tkn) = request.id_token.as_ref() { + Some( + self.config + .jwt_service + .process_token(TokenStr::Id(tkn.as_str())) + .await?, + ) + } else { + None + }; + + let userinfo_token = if let Some(tkn) = request.userinfo_token.as_ref() { + Some( + self.config + .jwt_service + .process_token(TokenStr::Userinfo(tkn.as_str())) + .await?, + ) + } else { + None + }; Ok(DecodedTokens { access_token, @@ -116,17 +130,19 @@ impl Authz { /// Evaluate Authorization Request /// - evaluate if authorization is granted for *person* /// - evaluate if authorization is granted for *workload* - pub fn authorize(&self, request: Request) -> Result { - let start_time = Instant::now(); + pub async fn authorize(&self, request: Request) -> Result { + let start_time = Utc::now(); + let schema = &self.config.policy_store.schema; - let tokens = self.decode_tokens(&request)?; + + let tokens = self.decode_tokens(&request).await?; // Parse action UID. let action = cedar_policy::EntityUid::from_str(request.action.as_str()) .map_err(AuthorizeError::Action)?; // Parse [`cedar_policy::Entity`]-s to [`AuthorizeEntitiesData`] that hold all entities (for usability). - let entities_data: AuthorizeEntitiesData = self.build_entities(&request, &tokens)?; + let entities_data: AuthorizeEntitiesData = self.build_entities(&request, &tokens).await?; // Get entity UIDs what we will be used on authorize check let resource_uid = entities_data.resource.uid(); @@ -236,7 +252,9 @@ impl Authz { ); // measure time how long request executes - let elapsed_ms = start_time.elapsed().as_millis(); + let elapsed_ms = Utc::now() + .signed_duration_since(start_time) + .num_milliseconds(); // FROM THIS POINT WE ONLY MAKE LOGS @@ -265,7 +283,7 @@ impl Authz { entities: entities_json, person_authorize_info: user_authz_info, workload_authorize_info: workload_authz_info, - authorized: result.is_allowed(), + authorized: result.decision, }) .set_message("Result of authorize.".to_string()), ); @@ -308,7 +326,7 @@ impl Authz { lock_client_id: None, action: request.action.clone(), resource: resource_uid.to_string(), - decision: result.decision().into(), + decision: result.decision.into(), tokens: tokens_logging_info, decision_time_ms: elapsed_ms, }); @@ -342,10 +360,10 @@ impl Authz { /// Build all the Cedar [`Entities`] from a [`Request`] /// /// [`Entities`]: Entity - pub fn build_entities( + pub async fn build_entities( &self, request: &Request, - tokens: &DecodedTokens, + tokens: &DecodedTokens<'_>, ) -> Result { let policy_store = &self.config.policy_store; let auth_conf = &self.config.authorization; @@ -427,7 +445,9 @@ fn build_context( id_mapping.insert(type_name, type_id.to_string()); } - let entities_context = action_schema.build_ctx_entity_refs_json(id_mapping)?; + let entities_context = action_schema + .build_ctx_entity_refs_json(id_mapping) + .unwrap(); let context = merge_json_values(entities_context, request_context)?; diff --git a/jans-cedarling/cedarling/src/blocking.rs b/jans-cedarling/cedarling/src/blocking.rs new file mode 100644 index 00000000000..203a5647a64 --- /dev/null +++ b/jans-cedarling/cedarling/src/blocking.rs @@ -0,0 +1,57 @@ +/* + * 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. + */ + +//! Blocking client of Cedarling + +use crate::Cedarling as AsyncCedarling; +use crate::{ + AuthorizeError, AuthorizeResult, BootstrapConfig, InitCedarlingError, LogEntry, LogStorage, + Request, +}; +use std::sync::Arc; +use tokio::runtime::Runtime; + +/// The blocking instance of the Cedarling application. +/// It is safe to share between threads. +#[derive(Clone)] +pub struct Cedarling { + runtime: Arc, + instance: AsyncCedarling, +} + +impl Cedarling { + /// Builder + pub fn new(config: &BootstrapConfig) -> Result { + let rt = Runtime::new().unwrap(); + + rt.block_on(AsyncCedarling::new(config)) + .map(|async_instance| Cedarling { + instance: async_instance, + runtime: Arc::new(rt), + }) + } + + /// Authorize request + /// makes authorization decision based on the [`Request`] + pub fn authorize(&self, request: Request) -> Result { + self.runtime.block_on(self.instance.authorize(request)) + } +} + +impl LogStorage for Cedarling { + fn pop_logs(&self) -> Vec { + self.instance.pop_logs() + } + + fn get_log_by_id(&self, id: &str) -> Option { + self.instance.get_log_by_id(id) + } + + fn get_log_ids(&self) -> Vec { + self.instance.get_log_ids() + } +} diff --git a/jans-cedarling/cedarling/src/bootstrap_config/decode.rs b/jans-cedarling/cedarling/src/bootstrap_config/decode.rs index be2b1726e35..5b9d688c2a9 100644 --- a/jans-cedarling/cedarling/src/bootstrap_config/decode.rs +++ b/jans-cedarling/cedarling/src/bootstrap_config/decode.rs @@ -493,8 +493,9 @@ impl BootstrapConfig { .local_jwks .as_ref() .map(|path| { - fs::read_to_string(path) - .map_err(|e| BootstrapConfigLoadingError::LoadLocalJwks(path.to_string(), e)) + fs::read_to_string(path).map_err(|e| { + BootstrapConfigLoadingError::LoadLocalJwks(path.to_string(), e.to_string()) + }) }) .transpose()?; diff --git a/jans-cedarling/cedarling/src/bootstrap_config/mod.rs b/jans-cedarling/cedarling/src/bootstrap_config/mod.rs index 59720f4a1a6..91e6065352d 100644 --- a/jans-cedarling/cedarling/src/bootstrap_config/mod.rs +++ b/jans-cedarling/cedarling/src/bootstrap_config/mod.rs @@ -11,8 +11,8 @@ pub(crate) mod jwt_config; pub(crate) mod log_config; pub(crate) mod policy_store_config; -use std::path::Path; -use std::{fs, io}; +#[cfg(not(target_arch = "wasm32"))] +use std::{fs, io, path::Path}; pub use authorization_config::AuthorizationConfig; // reimport to useful import values in root module @@ -53,6 +53,7 @@ impl BootstrapConfig { /// /// let config = BootstrapConfig::load_from_file("../test_files/bootstrap_props.json").unwrap(); /// ``` + #[cfg(not(target_arch = "wasm32"))] pub fn load_from_file(path: &str) -> Result { let file_ext = Path::new(path) .extension() @@ -94,12 +95,14 @@ pub enum BootstrapConfigLoadingError { /// Supported formats include: /// - `.json` /// - `.yaml` or `.yml` + #[cfg(not(target_arch = "wasm32"))] #[error( "Unsupported bootstrap config file format for: {0}. Supported formats include: JSON, YAML" )] InvalidFileFormat(String), /// Error returned when the file cannot be read. + #[cfg(not(target_arch = "wasm32"))] #[error("Failed to read {0}: {1}")] ReadFile(String, io::Error), @@ -130,14 +133,12 @@ pub enum BootstrapConfigLoadingError { MissingPolicyStore, /// Error returned when the policy store file is in an unsupported format. - #[error( - "Unsupported policy store file format for: {0}. Supported formats include: JSON, YAML" - )] + #[error("Unsupported policy store file format for: {0}. Supported formats include: JSON, YAML")] UnsupportedPolicyStoreFileFormat(String), /// Error returned when failing to load a local JWKS #[error("Failed to load local JWKS from {0}: {1}")] - LoadLocalJwks(String, std::io::Error), + LoadLocalJwks(String, String), /// Error returned when both `CEDARLING_USER_AUTHZ` and `CEDARLING_WORKLOAD_AUTHZ` are disabled. /// These two authentication configurations cannot be disabled at the same time. diff --git a/jans-cedarling/cedarling/src/http/blocking.rs b/jans-cedarling/cedarling/src/http/blocking.rs deleted file mode 100644 index 90a8a0ad1fb..00000000000 --- a/jans-cedarling/cedarling/src/http/blocking.rs +++ /dev/null @@ -1,77 +0,0 @@ -// 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::thread::sleep; -use std::time::Duration; - -use reqwest::blocking::Client; - -use super::{HttpClientError, HttpGet, Response}; - -/// A wrapper around `reqwest::blocking::Client` providing HTTP request functionality -/// with retry logic. -/// -/// The `HttpClient` struct allows for sending GET requests with a retry mechanism -/// that attempts to fetch the requested resource up to a maximum number of times -/// if an error occurs. -#[derive(Debug)] -pub struct BlockingHttpClient { - client: reqwest::blocking::Client, - max_retries: u32, - retry_delay: Duration, -} - -impl BlockingHttpClient { - pub fn new(max_retries: u32, retry_delay: Duration) -> Result { - let client = Client::builder() - .build() - .map_err(HttpClientError::Initialization)?; - - Ok(Self { - client, - max_retries, - retry_delay, - }) - } -} - -impl HttpGet for BlockingHttpClient { - /// Sends a GET request to the specified URI with retry logic. - /// - /// This method will attempt to fetch the resource up to 3 times, with an increasing delay - /// between each attempt. - fn get(&self, uri: &str) -> Result { - // Fetch the JWKS from the jwks_uri - let mut attempts = 0; - let response = loop { - match self.client.get(uri).send() { - // Exit loop on success - Ok(response) => break response, - - Err(e) if attempts < self.max_retries => { - attempts += 1; - // TODO: pass this message to the logger - eprintln!( - "Request failed (attempt {} of {}): {}. Retrying...", - attempts, self.max_retries, e - ); - sleep(self.retry_delay * attempts); - }, - // Exit if max retries exceeded - Err(e) => return Err(HttpClientError::MaxHttpRetriesReached(e)), - } - }; - - let response = response - .error_for_status() - .map_err(HttpClientError::HttpStatus)?; - - Ok(Response { - text: response - .text() - .map_err(HttpClientError::DecodeResponseUtf8)?, - }) - } -} diff --git a/jans-cedarling/cedarling/src/http/mod.rs b/jans-cedarling/cedarling/src/http/mod.rs index 85cc01ef862..09ad396d7ef 100644 --- a/jans-cedarling/cedarling/src/http/mod.rs +++ b/jans-cedarling/cedarling/src/http/mod.rs @@ -1,40 +1,78 @@ -// 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. - -#[cfg(not(target_family = "wasm"))] -mod blocking; -#[cfg(target_family = "wasm")] -mod wasm; - -use std::time::Duration; - +/* +* 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. +cfg*/ + +use reqwest::Client; use serde::Deserialize; +use std::time::Duration; -trait HttpGet { - /// Sends a GET request to the specified URI - fn get(&self, uri: &str) -> Result; -} - +/// A wrapper around `reqwest::blocking::Client` providing HTTP request functionality +/// with retry logic. +/// +/// The `HttpClient` struct allows for sending GET requests with a retry mechanism +/// that attempts to fetch the requested resource up to a maximum number of times +/// if an error occurs. +#[derive(Debug)] pub struct HttpClient { - client: Box, + client: reqwest::Client, + max_retries: u32, + retry_delay: Duration, } impl HttpClient { pub fn new(max_retries: u32, retry_delay: Duration) -> Result { - #[cfg(not(target_family = "wasm"))] - let client = blocking::BlockingHttpClient::new(max_retries, retry_delay)?; - #[cfg(target_family = "wasm")] - let client = wasm::WasmHttpClient::new(max_retries, retry_delay)?; + let client = Client::builder() + .build() + .map_err(HttpClientError::Initialization)?; Ok(Self { - client: Box::new(client), + client, + max_retries, + retry_delay, }) } +} - pub fn get(&self, uri: &str) -> Result { - self.client.get(uri) +impl HttpClient { + /// Sends a GET request to the specified URI with retry logic. + /// + /// This method will attempt to fetch the resource up to 3 times, with an increasing delay + /// between each attempt. + pub async fn get(&self, uri: &str) -> Result { + // Fetch the JWKS from the jwks_uri + let mut attempts = 0; + let response = loop { + match self.client.get(uri).send().await { + // Exit loop on success + Ok(response) => break response, + + Err(e) if attempts < self.max_retries => { + attempts += 1; + // TODO: pass this message to the logger + eprintln!( + "Request failed (attempt {} of {}): {}. Retrying...", + attempts, self.max_retries, e + ); + tokio::time::sleep(self.retry_delay * attempts).await; + }, + // Exit if max retries exceeded + Err(e) => return Err(HttpClientError::MaxHttpRetriesReached(e)), + } + }; + + let response = response + .error_for_status() + .map_err(HttpClientError::HttpStatus)?; + + Ok(Response { + text: response + .text() + .await + .map_err(HttpClientError::DecodeResponseUtf8)?, + }) } } @@ -75,17 +113,18 @@ pub enum HttpClientError { #[cfg(test)] mod test { - use std::time::Duration; + use crate::http::{HttpClient, HttpClientError}; use mockito::Server; use serde_json::json; + use std::time::Duration; use test_utils::assert_eq; + use tokio; + use tokio::join; - use crate::http::{HttpClient, HttpClientError}; - - #[test] - fn can_fetch() { - let mut mock_server = Server::new(); + #[tokio::test] + async fn can_fetch() { + let mut mock_server = Server::new_async().await; let expected = json!({ "issuer": mock_server.url(), @@ -98,16 +137,16 @@ mod test { .with_header("content-type", "application/json") .with_body(expected.to_string()) .expect(1) - .create(); + .create_async(); let client = HttpClient::new(3, Duration::from_millis(1)).expect("Should create HttpClient."); - let response = client - .get(&format!( - "{}/.well-known/openid-configuration", - mock_server.url() - )) + let link = &format!("{}/.well-known/openid-configuration", mock_server.url()); + let req_fut = client.get(link); + let (req_result, mock_result) = join!(req_fut, mock_endpoint); + + let response = req_result .expect("Should get response") .json::() .expect("Should deserialize JSON response."); @@ -117,14 +156,14 @@ mod test { "Expected: {expected:?}\nBut got: {response:?}" ); - mock_endpoint.assert(); + mock_result.assert(); } - #[test] - fn errors_when_max_http_retries_exceeded() { + #[tokio::test] + async fn errors_when_max_http_retries_exceeded() { let client = HttpClient::new(3, Duration::from_millis(1)).expect("Should create HttpClient"); - let response = client.get("0.0.0.0"); + let response = client.get("0.0.0.0").await; assert!( matches!(response, Err(HttpClientError::MaxHttpRetriesReached(_))), @@ -132,23 +171,23 @@ mod test { ); } - #[test] - fn errors_on_http_error_status() { - let mut mock_server = Server::new(); + #[tokio::test] + async fn errors_on_http_error_status() { + let mut mock_server = Server::new_async().await; - let mock_endpoint = mock_server + let mock_endpoint_fut = mock_server .mock("GET", "/.well-known/openid-configuration") .with_status(500) .expect(1) - .create(); + .create_async(); let client = HttpClient::new(3, Duration::from_millis(1)).expect("Should create HttpClient."); - let response = client.get(&format!( - "{}/.well-known/openid-configuration", - mock_server.url() - )); + let link = &format!("{}/.well-known/openid-configuration", mock_server.url()); + let client_fut = client.get(link); + + let (mock_endpoint, response) = join!(mock_endpoint_fut, client_fut); assert!( matches!(response, Err(HttpClientError::HttpStatus(_))), diff --git a/jans-cedarling/cedarling/src/http/wasm.rs b/jans-cedarling/cedarling/src/http/wasm.rs deleted file mode 100644 index b17d28a5c58..00000000000 --- a/jans-cedarling/cedarling/src/http/wasm.rs +++ /dev/null @@ -1,36 +0,0 @@ -// 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::time::Duration; - -use super::{HttpClientError, HttpGet, Response}; - -/// A wrapper around `reqwest::blocking::Client` providing HTTP request functionality -/// with retry logic. -/// -/// The `HttpClient` struct allows for sending GET requests with a retry mechanism -/// that attempts to fetch the requested resource up to a maximum number of times -/// if an error occurs. -#[derive(Debug)] -pub struct WasmHttpClient { - _max_retries: u32, - _retry_delay: Duration, -} - -impl WasmHttpClient { - pub fn new(_max_retries: u32, _retry_delay: Duration) -> Result { - todo!(); - } -} - -impl HttpGet for WasmHttpClient { - /// Sends a GET request to the specified URI with retry logic. - /// - /// This method will attempt to fetch the resource up to 3 times, with an increasing delay - /// between each attempt. - fn get(&self, _uri: &str) -> Result { - todo!() - } -} diff --git a/jans-cedarling/cedarling/src/init/policy_store.rs b/jans-cedarling/cedarling/src/init/policy_store.rs index c4259c872e6..6a9af2a8721 100644 --- a/jans-cedarling/cedarling/src/init/policy_store.rs +++ b/jans-cedarling/cedarling/src/init/policy_store.rs @@ -61,7 +61,7 @@ fn extract_first_policy_store( /// Loads the policy store based on the provided configuration. /// /// This function supports multiple sources for loading policies. -pub(crate) fn load_policy_store( +pub(crate) async fn load_policy_store( config: &PolicyStoreConfig, ) -> Result { let policy_store = match &config.source { @@ -76,7 +76,7 @@ pub(crate) fn load_policy_store( extract_first_policy_store(&agama_policy_store)? }, PolicyStoreSource::LockMaster(policy_store_uri) => { - load_policy_store_from_lock_master(policy_store_uri)? + load_policy_store_from_lock_master(policy_store_uri).await? }, PolicyStoreSource::FileJson(path) => { let policy_json = fs::read_to_string(path) @@ -98,11 +98,11 @@ pub(crate) fn load_policy_store( /// Loads the policy store from the Lock Master. /// /// The URI is from the `CEDARLING_POLICY_STORE_URI` bootstrap property. -fn load_policy_store_from_lock_master( +async fn load_policy_store_from_lock_master( uri: &str, ) -> Result { let client = HttpClient::new(3, Duration::from_secs(3))?; - let agama_policy_store = client.get(uri)?.json::()?; + let agama_policy_store = client.get(uri).await?.json::()?; extract_first_policy_store(&agama_policy_store) } @@ -119,29 +119,31 @@ mod test { // works correctly anymore here since we already have tests for those in // src/common/policy_store/test.rs... - #[test] - fn can_load_from_json_file() { + #[tokio::test] + async fn can_load_from_json_file() { load_policy_store(&PolicyStoreConfig { source: crate::PolicyStoreSource::FileJson( Path::new("../test_files/policy-store_generated.json").into(), ), }) + .await .expect("Should load policy store from JSON file"); } - #[test] - fn can_load_from_yaml_file() { + #[tokio::test] + async fn can_load_from_yaml_file() { load_policy_store(&PolicyStoreConfig { source: crate::PolicyStoreSource::FileYaml( Path::new("../test_files/policy-store_ok.yaml").into(), ), }) + .await .expect("Should load policy store from YAML file"); } - #[test] - fn can_load_from_lock_master() { - let mut mock_server = Server::new(); + #[tokio::test] + async fn can_load_from_lock_master() { + let mut mock_server = Server::new_async().await; let policy_store_json = include_str!("../../../test_files/policy-store_lock_master_ok.json").to_string(); @@ -159,6 +161,7 @@ mod test { load_policy_store(&PolicyStoreConfig { source: crate::PolicyStoreSource::LockMaster(uri), }) + .await .expect("Should load policy store from Lock Master file"); mock_endpoint.assert(); diff --git a/jans-cedarling/cedarling/src/init/service_config.rs b/jans-cedarling/cedarling/src/init/service_config.rs index 9abbdd643e8..f6de2deb7ef 100644 --- a/jans-cedarling/cedarling/src/init/service_config.rs +++ b/jans-cedarling/cedarling/src/init/service_config.rs @@ -1,13 +1,14 @@ -// 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. +/* + * 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 bootstrap_config::BootstrapConfig; - -use super::policy_store::{load_policy_store, PolicyStoreLoadError}; +use super::policy_store::{PolicyStoreLoadError, load_policy_store}; use crate::bootstrap_config; use crate::common::policy_store::PolicyStoreWithID; +use bootstrap_config::BootstrapConfig; /// Configuration that hold validated infomation from bootstrap config #[derive(Clone)] @@ -23,8 +24,8 @@ pub enum ServiceConfigError { } impl ServiceConfig { - pub fn new(bootstrap: &BootstrapConfig) -> Result { - let policy_store = load_policy_store(&bootstrap.policy_store_config)?; + pub async fn new(bootstrap: &BootstrapConfig) -> Result { + let policy_store = load_policy_store(&bootstrap.policy_store_config).await?; Ok(Self { policy_store }) } diff --git a/jans-cedarling/cedarling/src/init/service_factory.rs b/jans-cedarling/cedarling/src/init/service_factory.rs index 1403a34f245..9b7ea155cd9 100644 --- a/jans-cedarling/cedarling/src/init/service_factory.rs +++ b/jans-cedarling/cedarling/src/init/service_factory.rs @@ -1,18 +1,21 @@ -// 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. +/* + * 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. + */ //! Module to lazily initialize internal cedarling services use std::sync::Arc; -use super::service_config::ServiceConfig; -use crate::authz::{Authz, AuthzConfig}; use crate::bootstrap_config::BootstrapConfig; -use crate::common::app_types; use crate::common::policy_store::PolicyStoreWithID; use crate::jwt::{JwtService, JwtServiceInitError}; + +use super::service_config::ServiceConfig; +use crate::authz::{Authz, AuthzConfig}; +use crate::common::app_types; use crate::log; #[derive(Clone)] @@ -71,20 +74,20 @@ impl<'a> ServiceFactory<'a> { } // get jwt service - pub fn jwt_service(&mut self) -> Result, ServiceInitError> { + pub async fn jwt_service(&mut self) -> Result, ServiceInitError> { if let Some(jwt_service) = &self.container.jwt_service { Ok(jwt_service.clone()) } else { let config = &self.bootstrap_config.jwt_config; let trusted_issuers = self.policy_store().trusted_issuers.clone(); - let service = Arc::new(JwtService::new(config, trusted_issuers)?); + let service = Arc::new(JwtService::new(config, trusted_issuers).await?); self.container.jwt_service = Some(service.clone()); Ok(service) } } // get authz service - pub fn authz_service(&mut self) -> Result, ServiceInitError> { + pub async fn authz_service(&mut self) -> Result, ServiceInitError> { if let Some(authz) = &self.container.authz_service { Ok(authz.clone()) } else { @@ -93,7 +96,7 @@ impl<'a> ServiceFactory<'a> { pdp_id: self.pdp_id(), application_name: self.application_name(), policy_store: self.policy_store(), - jwt_service: self.jwt_service()?, + jwt_service: self.jwt_service().await?, authorization: self.bootstrap_config.authorization_config.clone(), }; let service = Arc::new(Authz::new(config)); diff --git a/jans-cedarling/cedarling/src/jwt/jwk_store.rs b/jans-cedarling/cedarling/src/jwt/jwk_store.rs index a02b693c595..b964fbf41ea 100644 --- a/jans-cedarling/cedarling/src/jwt/jwk_store.rs +++ b/jans-cedarling/cedarling/src/jwt/jwk_store.rs @@ -7,8 +7,9 @@ use std::collections::{HashMap, HashSet}; use std::fmt::Debug; use std::sync::Arc; -use jsonwebtoken::jwk::Jwk; use jsonwebtoken::DecodingKey; +use jsonwebtoken::jwk::Jwk; + use serde::Deserialize; use serde_json::Value; use time::OffsetDateTime; @@ -153,19 +154,21 @@ impl JwkStore { } /// Creates a JwkStore by fetching the keys from the given [`TrustedIssuer`]. - pub fn new_from_trusted_issuer( + pub async fn new_from_trusted_issuer( store_id: TrustedIssuerId, issuer: &TrustedIssuer, http_client: &HttpClient, ) -> Result { // fetch openid configuration - let response = http_client.get(&issuer.openid_configuration_endpoint)?; + let response = http_client + .get(&issuer.openid_configuration_endpoint) + .await?; let openid_config = response .json::() .map_err(JwkStoreError::FetchOpenIdConfig)?; // fetch jwks - let response = http_client.get(&openid_config.jwks_uri)?; + let response = http_client.get(&openid_config.jwks_uri).await?; let mut store = Self::new_from_jwks_str(store_id, response.text())?; store.issuer = Some(openid_config.issuer.into()); @@ -233,8 +236,8 @@ mod test { use std::collections::HashMap; use std::time::Duration; - use jsonwebtoken::jwk::JwkSet; use jsonwebtoken::DecodingKey; + use jsonwebtoken::jwk::JwkSet; use mockito::Server; use serde_json::json; use time::OffsetDateTime; @@ -313,9 +316,9 @@ mod test { ); } - #[test] - fn can_load_from_trusted_issuers() { - let mut mock_server = Server::new(); + #[tokio::test] + async fn can_load_from_trusted_issuers() { + let mut mock_server = Server::new_async().await; // Setup OpenId config endpoint let openid_config_json = json!({ @@ -349,10 +352,8 @@ mod test { "alg": "RS256", "kty": "RSA", "kid": kid2, - } + }]}); - ] - }); let jwks_endpoint = mock_server .mock("GET", "/jwks") .with_status(200) @@ -375,6 +376,7 @@ mod test { let mut result = JwkStore::new_from_trusted_issuer("test".into(), &source_iss, &http_client) + .await .expect("Should load JwkStore from Trusted Issuer"); // We edit the `last_updated` from the result so that the comparison // wont fail because of the timestamp. diff --git a/jans-cedarling/cedarling/src/jwt/key_service.rs b/jans-cedarling/cedarling/src/jwt/key_service.rs index 0f794d77bad..df36cf3d92a 100644 --- a/jans-cedarling/cedarling/src/jwt/key_service.rs +++ b/jans-cedarling/cedarling/src/jwt/key_service.rs @@ -8,10 +8,10 @@ use std::sync::Arc; use std::time::Duration; use jsonwebtoken::DecodingKey; -use serde_json::{json, Value}; +use serde_json::{Value, json}; -use super::jwk_store::{JwkStore, JwkStoreError}; use super::TrustedIssuerId; +use super::jwk_store::{JwkStore, JwkStoreError}; use crate::common::policy_store::TrustedIssuer; use crate::http::{HttpClient, HttpClientError}; @@ -64,7 +64,7 @@ impl KeyService { /// Loads key stores using a JSON string. /// /// Enables loading key stores from a local JSON file. - pub fn new_from_trusted_issuers( + pub async fn new_from_trusted_issuers( trusted_issuers: &HashMap, ) -> Result { let http_client = HttpClient::new(3, Duration::from_secs(3))?; @@ -74,7 +74,7 @@ impl KeyService { let iss_id: Arc = iss_id.as_str().into(); key_stores.insert( iss_id.clone(), - JwkStore::new_from_trusted_issuer(iss_id, iss, &http_client)?, + JwkStore::new_from_trusted_issuer(iss_id, iss, &http_client).await?, ); } @@ -173,12 +173,12 @@ mod test { ); } - #[test] - fn can_load_jwk_stores_from_multiple_trusted_issuers() { + #[tokio::test] + async fn can_load_jwk_stores_from_multiple_trusted_issuers() { let kid1 = "a50f6e70ef4b548a5fd9142eecd1fb8f54dce9ee"; let kid2 = "73e25f9789119c7875d58087a78ac23f5ef2eda3"; - let mut mock_server = Server::new(); + let mut mock_server = Server::new_async().await; // Setup first OpenID config endpoint let openid_config_endpoint1 = mock_server @@ -247,31 +247,26 @@ mod test { .create(); let key_service = KeyService::new_from_trusted_issuers(&HashMap::from([ - ( - "first".to_string(), - TrustedIssuer { - name: "First IDP".to_string(), - description: "".to_string(), - openid_configuration_endpoint: format!( - "{}/first/.well-known/openid-configuration", - mock_server.url() - ), - ..Default::default() - }, - ), - ( - "second".to_string(), - TrustedIssuer { - name: "Second IDP".to_string(), - description: "".to_string(), - openid_configuration_endpoint: format!( - "{}/second/.well-known/openid-configuration", - mock_server.url() - ), - ..Default::default() - }, - ), + ("first".to_string(), TrustedIssuer { + name: "First IDP".to_string(), + description: "".to_string(), + openid_configuration_endpoint: format!( + "{}/first/.well-known/openid-configuration", + mock_server.url() + ), + ..Default::default() + }), + ("second".to_string(), TrustedIssuer { + name: "Second IDP".to_string(), + description: "".to_string(), + openid_configuration_endpoint: format!( + "{}/second/.well-known/openid-configuration", + mock_server.url() + ), + ..Default::default() + }), ])) + .await .expect("Should load KeyService from trusted issuers"); assert!( diff --git a/jans-cedarling/cedarling/src/jwt/mod.rs b/jans-cedarling/cedarling/src/jwt/mod.rs index 97d88d9d822..9f671ac871e 100644 --- a/jans-cedarling/cedarling/src/jwt/mod.rs +++ b/jans-cedarling/cedarling/src/jwt/mod.rs @@ -99,7 +99,7 @@ pub struct JwtService { } impl JwtService { - pub fn new( + pub async fn new( config: &JwtConfig, trusted_issuers: Option>, ) -> Result { @@ -110,6 +110,7 @@ impl JwtService { // Case: Trusted issuers provided (true, None, Some(issuers)) => Some( KeyService::new_from_trusted_issuers(issuers) + .await .map_err(JwtServiceInitError::KeyService)?, ), // Case: Local JWKS provided @@ -177,7 +178,7 @@ impl JwtService { }) } - pub fn process_token<'a>( + pub async fn process_token<'a>( &'a self, token: TokenStr<'a>, ) -> Result, JwtProcessingError> { @@ -217,13 +218,14 @@ mod test { use jsonwebtoken::Algorithm; use serde_json::json; use test_utils::assert_eq; + use tokio::test; use super::test_utils::*; use super::{JwtService, Token, TokenClaims, TokenStr}; use crate::{IdTokenTrustMode, JwtConfig, TokenValidationConfig}; #[test] - pub fn can_validate_token() { + pub async fn can_validate_token() { // Generate token let keys = generate_keypair_hs256(Some("some_hs256_key")).expect("Should generate keys"); let access_tkn_claims = json!({ @@ -270,11 +272,13 @@ mod test { }, None, ) + .await .expect("Should create JwtService"); // Test access_token let access_tkn = jwt_service .process_token(TokenStr::Access(&access_tkn)) + .await .expect("Should process access_token"); let expected_claims = serde_json::from_value::(access_tkn_claims) .expect("Should create expected access_token claims"); @@ -283,6 +287,7 @@ mod test { // Test id_token let id_tkn = jwt_service .process_token(TokenStr::Id(&id_tkn)) + .await .expect("Should process id_token"); let expected_claims = serde_json::from_value::(id_tkn_claims) .expect("Should create expected id_token claims"); @@ -291,6 +296,7 @@ mod test { // Test userinfo_token let userinfo_tkn = jwt_service .process_token(TokenStr::Userinfo(&userinfo_tkn)) + .await .expect("Should process userinfo_token"); let expected_claims = serde_json::from_value::(userinfo_tkn_claims) .expect("Should create expected userinfo_token claims"); diff --git a/jans-cedarling/cedarling/src/lib.rs b/jans-cedarling/cedarling/src/lib.rs index 465de0eab1b..5e58a5aca38 100644 --- a/jans-cedarling/cedarling/src/lib.rs +++ b/jans-cedarling/cedarling/src/lib.rs @@ -21,22 +21,26 @@ mod jwt; mod lock; mod log; +#[cfg(not(target_arch = "wasm32"))] +#[cfg(feature = "blocking")] +pub mod blocking; + #[doc(hidden)] #[cfg(test)] mod tests; use std::sync::Arc; -pub use authz::request::{Request, ResourceData}; #[cfg(test)] use authz::AuthorizeEntitiesData; use authz::Authz; +pub use authz::request::{Request, ResourceData}; pub use authz::{AuthorizeError, AuthorizeResult}; pub use bootstrap_config::*; use common::app_types; +use init::ServiceFactory; use init::service_config::{ServiceConfig, ServiceConfigError}; use init::service_factory::ServiceInitError; -use init::ServiceFactory; use log::interface::LogWriter; use log::{LogEntry, LogType}; pub use log::{LogLevel, LogStorage}; @@ -62,6 +66,9 @@ pub enum InitCedarlingError { /// Error while initializing a Service #[error(transparent)] ServiceInit(#[from] ServiceInitError), + /// Error while parse [`BootstrapConfigRaw`] + #[error(transparent)] + BootstrapConfigLoading(#[from] BootstrapConfigLoadingError), } /// The instance of the Cedarling application. @@ -74,11 +81,12 @@ pub struct Cedarling { impl Cedarling { /// Create a new instance of the Cedarling application. - pub fn new(config: &BootstrapConfig) -> Result { + pub async fn new(config: &BootstrapConfig) -> Result { let log = log::init_logger(&config.log_config); let pdp_id = app_types::PdpID::new(); let service_config = ServiceConfig::new(config) + .await .inspect(|_| { log.log( LogEntry::new_with_data(pdp_id, None, LogType::System) @@ -99,25 +107,25 @@ impl Cedarling { Ok(Cedarling { log, - authz: service_factory.authz_service()?, + authz: service_factory.authz_service().await?, }) } /// Authorize request /// makes authorization decision based on the [`Request`] - pub fn authorize(&self, request: Request) -> Result { - self.authz.authorize(request) + pub async fn authorize(&self, request: Request) -> Result { + self.authz.authorize(request).await } /// Get entites derived from `cedar-policy` schema and tokens for `authorize` request. #[doc(hidden)] #[cfg(test)] - pub fn authorize_entities_data( + pub async fn authorize_entities_data( &self, request: &Request, ) -> Result { - let tokens = self.authz.decode_tokens(request)?; - self.authz.build_entities(request, &tokens) + let tokens = self.authz.decode_tokens(request).await?; + self.authz.build_entities(request, &tokens).await } } diff --git a/jans-cedarling/cedarling/src/log/log_entry.rs b/jans-cedarling/cedarling/src/log/log_entry.rs index 78639461369..e3586960391 100644 --- a/jans-cedarling/cedarling/src/log/log_entry.rs +++ b/jans-cedarling/cedarling/src/log/log_entry.rs @@ -10,10 +10,10 @@ use std::collections::{HashMap, HashSet}; use std::fmt::Display; use std::hash::Hash; -use uuid7::{uuid7, Uuid}; +use uuid7::Uuid; -use super::interface::Loggable; use super::LogLevel; +use super::interface::Loggable; use crate::bootstrap_config::AuthorizationConfig; use crate::common::app_types::{self, ApplicationName}; use crate::common::policy_store::PoliciesContainer; @@ -29,6 +29,7 @@ pub struct LogEntry { /// it is unwrap to flatten structure #[serde(flatten)] pub base: BaseLogEntry, + /// message of the event pub msg: String, /// name of application from [bootstrap properties](https://github.com/JanssenProject/jans/wiki/Cedarling-Nativity-Plan#bootstrap-properties) @@ -195,6 +196,12 @@ impl From for Decision { } } +impl From for Decision { + fn from(value: bool) -> Self { + if value { Self::Allow } else { Self::Deny } + } +} + /// An error occurred when evaluating a policy #[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] pub struct PolicyEvaluationError { @@ -298,7 +305,7 @@ pub struct DecisionLogEntry<'a> { /// Dictionary with the token type and claims which should be included in the log pub tokens: LogTokensInfo<'a>, /// time in milliseconds spent for decision - pub decision_time_ms: u128, + pub decision_time_ms: i64, } impl Loggable for &DecisionLogEntry<'_> { @@ -311,6 +318,18 @@ impl Loggable for &DecisionLogEntry<'_> { } } +/// custom uuid generation function to avoid using std::time because it makes panic in WASM +// +// TODO: maybe using wasm we can use `js_sys::Date::now()` +// TODO: use global generator +fn gen_uuid7() -> Uuid { + use uuid7::V7Generator; + + let mut g = V7Generator::with_rand08(rand::rngs::OsRng); + let custom_unix_ts_ms = chrono::Utc::now().timestamp_millis(); + g.generate_or_reset_core(custom_unix_ts_ms as u64, 10_000) +} + #[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] pub struct BaseLogEntry { /// unique identifier for this event @@ -342,7 +361,7 @@ impl BaseLogEntry { // 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(), + request_id: gen_uuid7(), timestamp: Some(local_time_string), log_kind: log_type, pdp_id: pdp_id.0, diff --git a/jans-cedarling/cedarling/src/log/log_strategy.rs b/jans-cedarling/cedarling/src/log/log_strategy.rs index c2c96134fc3..c8b960642f3 100644 --- a/jans-cedarling/cedarling/src/log/log_strategy.rs +++ b/jans-cedarling/cedarling/src/log/log_strategy.rs @@ -3,11 +3,11 @@ // // Copyright (c) 2024, Gluu, Inc. +use super::LogEntry; use super::interface::{LogStorage, LogWriter, Loggable}; use super::memory_logger::MemoryLogger; use super::nop_logger::NopLogger; use super::stdout_logger::StdOutLogger; -use super::LogEntry; use crate::bootstrap_config::log_config::{LogConfig, LogTypeConfig}; /// LogStrategy implements strategy pattern for logging. diff --git a/jans-cedarling/cedarling/src/log/memory_logger.rs b/jans-cedarling/cedarling/src/log/memory_logger.rs index bb0f1aacb4b..1f41dac198a 100644 --- a/jans-cedarling/cedarling/src/log/memory_logger.rs +++ b/jans-cedarling/cedarling/src/log/memory_logger.rs @@ -3,8 +3,8 @@ // // Copyright (c) 2024, Gluu, Inc. +use chrono::Duration; use std::sync::Mutex; -use std::time::Duration; use sparkv::{Config as ConfigSparKV, SparKV}; @@ -25,7 +25,11 @@ pub(crate) struct MemoryLogger { impl MemoryLogger { pub fn new(config: MemoryLogConfig, log_level: LogLevel) -> Self { let sparkv_config = ConfigSparKV { - default_ttl: Duration::from_secs(config.log_ttl), + default_ttl: Duration::new( + config.log_ttl.try_into().expect("u64 that fits in a i64"), + 0, + ) + .expect("a valid duration"), ..Default::default() }; @@ -130,6 +134,11 @@ mod tests { LogType::System, ); + assert!( + entry1.base.request_id < entry2.base.request_id, + "entry1.base.request_id should be lower than in entry2" + ); + // log entries logger.log(entry1.clone()); logger.log(entry2.clone()); diff --git a/jans-cedarling/cedarling/src/log/test.rs b/jans-cedarling/cedarling/src/log/test.rs index 10fb54ab8a2..7130e90cd41 100644 --- a/jans-cedarling/cedarling/src/log/test.rs +++ b/jans-cedarling/cedarling/src/log/test.rs @@ -12,6 +12,7 @@ use std::io::Write; use interface::{LogWriter, Loggable}; use nop_logger::NopLogger; use stdout_logger::StdOutLogger; +use test_utils::assert_eq; use super::*; use crate::bootstrap_config::log_config; diff --git a/jans-cedarling/cedarling/src/tests/cases_authorize_different_principals.rs b/jans-cedarling/cedarling/src/tests/cases_authorize_different_principals.rs index 6d38674724d..150f0f167d5 100644 --- a/jans-cedarling/cedarling/src/tests/cases_authorize_different_principals.rs +++ b/jans-cedarling/cedarling/src/tests/cases_authorize_different_principals.rs @@ -6,14 +6,15 @@ //! In this module we test authorize different action //! where not all principals can be applied //! -//! all case scenario should have `result.is_allowed() == true` +//! all case scenario should have `result.decision == true` //! because we have checked different scenarios in `cases_authorize_without_check_jwt.rs` use lazy_static::lazy_static; use test_utils::assert_eq; +use tokio::test; use super::utils::*; -use crate::{cmp_decision, cmp_policy, WorkloadBoolOp}; /* macros is defined in the cedarling\src\tests\utils\cedarling_util.rs */ +use crate::{WorkloadBoolOp, cmp_decision, cmp_policy}; /* macros is defined in the cedarling\src\tests\utils\cedarling_util.rs */ static POLICY_STORE_RAW_YAML: &str = include_str!("../../../test_files/policy-store_ok_2.yaml"); @@ -57,14 +58,15 @@ lazy_static! { /// Check if action executes for next principals: Workload, User #[test] -fn success_test_for_all_principals() { - let cedarling = get_cedarling(PolicyStoreSource::Yaml(POLICY_STORE_RAW_YAML.to_string())); +async fn success_test_for_all_principals() { + let cedarling = get_cedarling(PolicyStoreSource::Yaml(POLICY_STORE_RAW_YAML.to_string())).await; let mut request = AuthRequestBase.clone(); request.action = "Jans::Action::\"Update\"".to_string(); let result = cedarling .authorize(request) + .await .expect("request should be parsed without errors"); cmp_decision!( @@ -90,12 +92,12 @@ fn success_test_for_all_principals() { "reason of permit person should be '2'" ); - assert!(result.is_allowed(), "request result should be allowed"); + assert!(result.decision, "request result should be allowed"); } /// Check if action executes for next principals: Workload #[test] -fn success_test_for_principal_workload() { +async fn success_test_for_principal_workload() { let cedarling = get_cedarling_with_authorization_conf( PolicyStoreSource::Yaml(POLICY_STORE_RAW_YAML.to_string()), crate::AuthorizationConfig { @@ -104,13 +106,15 @@ fn success_test_for_principal_workload() { user_workload_operator: Default::default(), ..Default::default() }, - ); + ) + .await; let mut request = AuthRequestBase.clone(); request.action = "Jans::Action::\"UpdateForWorkload\"".to_string(); let result = cedarling .authorize(request) + .await .expect("request should be parsed without errors"); cmp_decision!( @@ -126,12 +130,12 @@ fn success_test_for_principal_workload() { assert!(result.person.is_none(), "result for person should be none"); - assert!(result.is_allowed(), "request result should be allowed"); + assert!(result.decision, "request result should be allowed"); } /// Check if action executes for next principals: User #[test] -fn success_test_for_principal_user() { +async fn success_test_for_principal_user() { let cedarling = get_cedarling_with_authorization_conf( PolicyStoreSource::Yaml(POLICY_STORE_RAW_YAML.to_string()), crate::AuthorizationConfig { @@ -140,13 +144,15 @@ fn success_test_for_principal_user() { user_workload_operator: Default::default(), ..Default::default() }, - ); + ) + .await; let mut request = AuthRequestBase.clone(); request.action = "Jans::Action::\"UpdateForUser\"".to_string(); let result = cedarling .authorize(request) + .await .expect("request should be parsed without errors"); cmp_decision!( @@ -165,13 +171,13 @@ fn success_test_for_principal_user() { "result for workload should be none" ); - assert!(result.is_allowed(), "request result should be allowed"); + assert!(result.decision, "request result should be allowed"); } /// Check if action executes for next principals: Person (only) /// check for user and role #[test] -fn success_test_for_principal_person_role() { +async fn success_test_for_principal_person_role() { let cedarling = get_cedarling_with_authorization_conf( PolicyStoreSource::Yaml(POLICY_STORE_RAW_YAML.to_string()), crate::AuthorizationConfig { @@ -180,13 +186,15 @@ fn success_test_for_principal_person_role() { user_workload_operator: Default::default(), ..Default::default() }, - ); + ) + .await; let mut request = AuthRequestBase.clone(); request.action = "Jans::Action::\"UpdateForUserAndRole\"".to_string(); let result = cedarling .authorize(request) + .await .expect("request should be parsed without errors"); cmp_policy!( @@ -206,12 +214,12 @@ fn success_test_for_principal_person_role() { "result for workload should be none" ); - assert!(result.is_allowed(), "request result should be allowed"); + assert!(result.decision, "request result should be allowed"); } /// Check if action executes for next principals: Workload AND Person (Role) #[test] -fn success_test_for_principal_workload_role() { +async fn success_test_for_principal_workload_role() { let cedarling = get_cedarling_with_authorization_conf( PolicyStoreSource::Yaml(POLICY_STORE_RAW_YAML.to_string()), crate::AuthorizationConfig { @@ -220,13 +228,15 @@ fn success_test_for_principal_workload_role() { user_workload_operator: WorkloadBoolOp::And, ..Default::default() }, - ); + ) + .await; let mut request = AuthRequestBase.clone(); request.action = "Jans::Action::\"UpdateForWorkloadAndRole\"".to_string(); let result = cedarling .authorize(request) + .await .expect("request should be parsed without errors"); cmp_decision!( @@ -251,13 +261,13 @@ fn success_test_for_principal_workload_role() { "reason of permit person should be '3'" ); - assert!(result.is_allowed(), "request result should be allowed"); + assert!(result.decision, "request result should be allowed"); } /// Check if action executes for next principals: Workload (true) OR Person (false) /// is used operator OR #[test] -fn success_test_for_principal_workload_true_or_user_false() { +async fn success_test_for_principal_workload_true_or_user_false() { let cedarling = get_cedarling_with_authorization_conf( PolicyStoreSource::Yaml(POLICY_STORE_RAW_YAML.to_string()), crate::AuthorizationConfig { @@ -266,13 +276,15 @@ fn success_test_for_principal_workload_true_or_user_false() { user_workload_operator: WorkloadBoolOp::Or, ..Default::default() }, - ); + ) + .await; let mut request = AuthRequestBase.clone(); request.action = "Jans::Action::\"UpdateForWorkload\"".to_string(); let result = cedarling .authorize(request) + .await .expect("request should be parsed without errors"); cmp_decision!( @@ -297,13 +309,13 @@ fn success_test_for_principal_workload_true_or_user_false() { "reason of permit person should be empty" ); - assert!(result.is_allowed(), "request result should be allowed"); + assert!(result.decision, "request result should be allowed"); } /// Check if action executes for next principals: Workload (false) OR Person (true) /// is used operator OR #[test] -fn success_test_for_principal_workload_false_or_user_true() { +async fn success_test_for_principal_workload_false_or_user_true() { let cedarling = get_cedarling_with_authorization_conf( PolicyStoreSource::Yaml(POLICY_STORE_RAW_YAML.to_string()), crate::AuthorizationConfig { @@ -312,13 +324,15 @@ fn success_test_for_principal_workload_false_or_user_true() { user_workload_operator: WorkloadBoolOp::Or, ..Default::default() }, - ); + ) + .await; let mut request = AuthRequestBase.clone(); request.action = "Jans::Action::\"UpdateForUser\"".to_string(); let result = cedarling .authorize(request) + .await .expect("request should be parsed without errors"); cmp_decision!( @@ -343,13 +357,13 @@ fn success_test_for_principal_workload_false_or_user_true() { "reason of permit person should be '2'" ); - assert!(result.is_allowed(), "request result should be allowed"); + assert!(result.decision, "request result should be allowed"); } /// Check if action executes for next principals: Workload (false) OR Person (false) /// is used operator OR #[test] -fn success_test_for_principal_workload_false_or_user_false() { +async fn success_test_for_principal_workload_false_or_user_false() { let cedarling = get_cedarling_with_authorization_conf( PolicyStoreSource::Yaml(POLICY_STORE_RAW_YAML.to_string()), crate::AuthorizationConfig { @@ -358,13 +372,15 @@ fn success_test_for_principal_workload_false_or_user_false() { user_workload_operator: WorkloadBoolOp::Or, ..Default::default() }, - ); + ) + .await; let mut request = AuthRequestBase.clone(); request.action = "Jans::Action::\"AlwaysDeny\"".to_string(); let result = cedarling .authorize(request) + .await .expect("request should be parsed without errors"); cmp_decision!( @@ -389,12 +405,12 @@ fn success_test_for_principal_workload_false_or_user_false() { "reason of permit person should be empty" ); - assert!(!result.is_allowed(), "request result should be not allowed"); + assert!(!result.decision, "request result should be not allowed"); } /// Check if action executes when principal workload can't be applied #[test] -fn test_where_principal_workload_cant_be_applied() { +async fn test_where_principal_workload_cant_be_applied() { let cedarling = get_cedarling_with_authorization_conf( PolicyStoreSource::Yaml(POLICY_STORE_RAW_YAML.to_string()), crate::AuthorizationConfig { @@ -403,13 +419,15 @@ fn test_where_principal_workload_cant_be_applied() { user_workload_operator: Default::default(), ..Default::default() }, - ); + ) + .await; let mut request = AuthRequestBase.clone(); request.action = "Jans::Action::\"NoApplies\"".to_string(); let result = cedarling .authorize(request) + .await .expect_err("request should be parsed with error"); assert!(matches!( @@ -420,7 +438,7 @@ fn test_where_principal_workload_cant_be_applied() { /// Check if action executes when principal user can't be applied #[test] -fn test_where_principal_user_cant_be_applied() { +async fn test_where_principal_user_cant_be_applied() { let cedarling = get_cedarling_with_authorization_conf( PolicyStoreSource::Yaml(POLICY_STORE_RAW_YAML.to_string()), crate::AuthorizationConfig { @@ -429,13 +447,15 @@ fn test_where_principal_user_cant_be_applied() { user_workload_operator: Default::default(), ..Default::default() }, - ); + ) + .await; let mut request = AuthRequestBase.clone(); request.action = "Jans::Action::\"NoApplies\"".to_string(); let result = cedarling .authorize(request) + .await .expect_err("request should be parsed with error"); assert!( diff --git a/jans-cedarling/cedarling/src/tests/cases_authorize_namespace_jans2.rs b/jans-cedarling/cedarling/src/tests/cases_authorize_namespace_jans2.rs index a09753429d2..233872f7a2b 100644 --- a/jans-cedarling/cedarling/src/tests/cases_authorize_namespace_jans2.rs +++ b/jans-cedarling/cedarling/src/tests/cases_authorize_namespace_jans2.rs @@ -4,9 +4,10 @@ // Copyright (c) 2024, Gluu, Inc. use test_utils::assert_eq; +use tokio::test; use super::utils::*; -use crate::{cmp_decision, cmp_policy}; /* macros is defined in the cedarling\src\tests\utils\cedarling_util.rs */ +use crate::{cmp_decision, cmp_policy}; // macros is defined in the cedarling\src\tests\utils\cedarling_util.rs static POLICY_STORE_RAW_YAML: &str = include_str!("../../../test_files/policy-store_ok_namespace_Jans2.yaml"); @@ -15,8 +16,8 @@ static POLICY_STORE_RAW_YAML: &str = /// In previous we hardcoded creating entities in namespace `Jans` /// in `POLICY_STORE_RAW_YAML` is used namespace `Jans2` #[test] -fn test_namespace_jans2() { - let cedarling = get_cedarling(PolicyStoreSource::Yaml(POLICY_STORE_RAW_YAML.to_string())); +async fn test_namespace_jans2() { + let cedarling = get_cedarling(PolicyStoreSource::Yaml(POLICY_STORE_RAW_YAML.to_string())).await; // deserialize `Request` from json let request = Request::deserialize(serde_json::json!( @@ -56,6 +57,7 @@ fn test_namespace_jans2() { let result = cedarling .authorize(request) + .await .expect("request should be parsed without errors"); cmp_decision!( @@ -82,5 +84,5 @@ fn test_namespace_jans2() { "reason of permit person should be '2'" ); - assert!(result.is_allowed(), "request result should be allowed"); + assert!(result.decision, "request result should be allowed"); } diff --git a/jans-cedarling/cedarling/src/tests/cases_authorize_without_check_jwt.rs b/jans-cedarling/cedarling/src/tests/cases_authorize_without_check_jwt.rs index 6503c477b2a..7abb75f418f 100644 --- a/jans-cedarling/cedarling/src/tests/cases_authorize_without_check_jwt.rs +++ b/jans-cedarling/cedarling/src/tests/cases_authorize_without_check_jwt.rs @@ -4,9 +4,10 @@ // Copyright (c) 2024, Gluu, Inc. use test_utils::assert_eq; +use tokio::test; use super::utils::*; -use crate::{cmp_decision, cmp_policy}; /* macros is defined in the cedarling\src\tests\utils\cedarling_util.rs */ +use crate::{cmp_decision, cmp_policy}; // macros is defined in the cedarling\src\tests\utils\cedarling_util.rs static POLICY_STORE_RAW_YAML: &str = include_str!("../../../test_files/policy-store_ok_2.yaml"); static POLICY_STORE_ABAC_YAML: &str = include_str!("../../../test_files/policy-store_ok_abac.yaml"); @@ -18,8 +19,8 @@ static POLICY_STORE_ABAC_YAML: &str = include_str!("../../../test_files/policy-s /// we check here that field are parsed from JWT tokens /// and correctly executed using correct cedar-policy id #[test] -fn success_test_role_string() { - let cedarling = get_cedarling(PolicyStoreSource::Yaml(POLICY_STORE_RAW_YAML.to_string())); +async fn success_test_role_string() { + let cedarling = get_cedarling(PolicyStoreSource::Yaml(POLICY_STORE_RAW_YAML.to_string())).await; // deserialize `Request` from json let request = Request::deserialize(serde_json::json!( @@ -59,6 +60,7 @@ fn success_test_role_string() { let result = cedarling .authorize(request) + .await .expect("request should be parsed without errors"); cmp_decision!( @@ -84,7 +86,7 @@ fn success_test_role_string() { "reason of permit person should be '2','3'" ); - assert!(result.is_allowed(), "request result should be allowed"); + assert!(result.decision, "request result should be allowed"); } /// forbid test case where all check of role is forbid @@ -94,8 +96,8 @@ fn success_test_role_string() { /// we check here that field are parsed from JWT tokens /// and correctly executed using correct cedar-policy id #[test] -fn forbid_test_role_guest() { - let cedarling = get_cedarling(PolicyStoreSource::Yaml(POLICY_STORE_RAW_YAML.to_string())); +async fn forbid_test_role_guest() { + let cedarling = get_cedarling(PolicyStoreSource::Yaml(POLICY_STORE_RAW_YAML.to_string())).await; // deserialize `Request` from json let request = Request::deserialize(serde_json::json!( @@ -135,6 +137,7 @@ fn forbid_test_role_guest() { let result = cedarling .authorize(request) + .await .expect("request should be parsed without errors"); cmp_decision!( @@ -160,7 +163,7 @@ fn forbid_test_role_guest() { "reason of permit person should be '2' and '4'" ); - assert!(!result.is_allowed(), "request result should be not allowed"); + assert!(!result.decision, "request result should be not allowed"); } /// Success test case where all check a successful @@ -170,8 +173,8 @@ fn forbid_test_role_guest() { /// we check here that field are parsed from JWT tokens /// and correctly executed using correct cedar-policy id #[test] -fn success_test_role_array() { - let cedarling = get_cedarling(PolicyStoreSource::Yaml(POLICY_STORE_RAW_YAML.to_string())); +async fn success_test_role_array() { + let cedarling = get_cedarling(PolicyStoreSource::Yaml(POLICY_STORE_RAW_YAML.to_string())).await; // deserialize `Request` from json let request = Request::deserialize(serde_json::json!( @@ -211,6 +214,7 @@ fn success_test_role_array() { let result = cedarling .authorize(request) + .await .expect("request should be parsed without errors"); cmp_decision!( @@ -237,7 +241,7 @@ fn success_test_role_array() { "reason of permit person should be '2','3'" ); - assert!(result.is_allowed(), "request result should be allowed"); + assert!(result.decision, "request result should be allowed"); } /// Success test case where all check a successful @@ -247,8 +251,8 @@ fn success_test_role_array() { /// and correctly executed using correct cedar-policy id /// if role field is not present, just ignore role check #[test] -fn success_test_no_role() { - let cedarling = get_cedarling(PolicyStoreSource::Yaml(POLICY_STORE_RAW_YAML.to_string())); +async fn success_test_no_role() { + let cedarling = get_cedarling(PolicyStoreSource::Yaml(POLICY_STORE_RAW_YAML.to_string())).await; // deserialize `Request` from json let request = Request::deserialize(serde_json::json!( @@ -289,6 +293,7 @@ fn success_test_no_role() { let result = cedarling .authorize(request) + .await .expect("request should be parsed without errors"); cmp_decision!( @@ -316,7 +321,7 @@ fn success_test_no_role() { ); assert!( - result.is_allowed(), + result.decision, "request result should be allowed, because workload and user allowed" ); } @@ -326,8 +331,8 @@ fn success_test_no_role() { /// we check here that field for `Jans::User` is present in `id_token` /// it is `country` field of `Jans::User` and role field is present #[test] -fn success_test_user_data_in_id_token() { - let cedarling = get_cedarling(PolicyStoreSource::Yaml(POLICY_STORE_RAW_YAML.to_string())); +async fn success_test_user_data_in_id_token() { + let cedarling = get_cedarling(PolicyStoreSource::Yaml(POLICY_STORE_RAW_YAML.to_string())).await; // deserialize `Request` from json let request = Request::deserialize(serde_json::json!( @@ -368,6 +373,7 @@ fn success_test_user_data_in_id_token() { let result = cedarling .authorize(request) + .await .expect("request should be parsed without errors"); cmp_decision!( @@ -394,13 +400,13 @@ fn success_test_user_data_in_id_token() { "reason of permit person should be '2','3'" ); - assert!(result.is_allowed(), "request result should be allowed"); + assert!(result.decision, "request result should be allowed"); } // check all forbid #[test] -fn all_forbid() { - let cedarling = get_cedarling(PolicyStoreSource::Yaml(POLICY_STORE_RAW_YAML.to_string())); +async fn all_forbid() { + let cedarling = get_cedarling(PolicyStoreSource::Yaml(POLICY_STORE_RAW_YAML.to_string())).await; // deserialize `Request` from json let request = Request::deserialize(serde_json::json!( @@ -444,6 +450,7 @@ fn all_forbid() { let result = cedarling .authorize(request) + .await .expect("request should be parsed without errors"); cmp_decision!( @@ -470,13 +477,13 @@ fn all_forbid() { "reason of forbid person should empty, no forbid rule" ); - assert!(!result.is_allowed(), "request result should be not allowed"); + assert!(!result.decision, "request result should be not allowed"); } // check only workload permit and other not #[test] -fn only_workload_permit() { - let cedarling = get_cedarling(PolicyStoreSource::Yaml(POLICY_STORE_RAW_YAML.to_string())); +async fn only_workload_permit() { + let cedarling = get_cedarling(PolicyStoreSource::Yaml(POLICY_STORE_RAW_YAML.to_string())).await; // deserialize `Request` from json let request = Request::deserialize(serde_json::json!( @@ -518,6 +525,7 @@ fn only_workload_permit() { let result = cedarling .authorize(request) + .await .expect("request should be parsed without errors"); cmp_decision!( @@ -550,13 +558,13 @@ fn only_workload_permit() { "reason of forbid person should empty, no forbid rule" ); - assert!(!result.is_allowed(), "request result should be not allowed"); + assert!(!result.decision, "request result should be not allowed"); } // check only person permit and other not #[test] -fn only_person_permit() { - let cedarling = get_cedarling(PolicyStoreSource::Yaml(POLICY_STORE_RAW_YAML.to_string())); +async fn only_person_permit() { + let cedarling = get_cedarling(PolicyStoreSource::Yaml(POLICY_STORE_RAW_YAML.to_string())).await; // deserialize `Request` from json let request = Request::deserialize(serde_json::json!( @@ -599,6 +607,7 @@ fn only_person_permit() { let result = cedarling .authorize(request) + .await .expect("request should be parsed without errors"); cmp_decision!( @@ -625,13 +634,13 @@ fn only_person_permit() { "reason of forbid person should '2'" ); - assert!(!result.is_allowed(), "request result should be not allowed"); + assert!(!result.decision, "request result should be not allowed"); } // check only user role permit and other not #[test] -fn only_user_role_permit() { - let cedarling = get_cedarling(PolicyStoreSource::Yaml(POLICY_STORE_RAW_YAML.to_string())); +async fn only_user_role_permit() { + let cedarling = get_cedarling(PolicyStoreSource::Yaml(POLICY_STORE_RAW_YAML.to_string())).await; // deserialize `Request` from json let request = Request::deserialize(serde_json::json!( @@ -673,6 +682,7 @@ fn only_user_role_permit() { let result = cedarling .authorize(request) + .await .expect("request should be parsed without errors"); cmp_decision!( @@ -699,13 +709,13 @@ fn only_user_role_permit() { "reason of forbid person '3', permit for role Admin" ); - assert!(!result.is_allowed(), "request result should be not allowed"); + assert!(!result.decision, "request result should be not allowed"); } // check only workload and person permit and role not #[test] -fn only_workload_and_person_permit() { - let cedarling = get_cedarling(PolicyStoreSource::Yaml(POLICY_STORE_RAW_YAML.to_string())); +async fn only_workload_and_person_permit() { + let cedarling = get_cedarling(PolicyStoreSource::Yaml(POLICY_STORE_RAW_YAML.to_string())).await; // deserialize `Request` from json let request = Request::deserialize(serde_json::json!( @@ -746,6 +756,7 @@ fn only_workload_and_person_permit() { let result = cedarling .authorize(request) + .await .expect("request should be parsed without errors"); cmp_decision!( @@ -772,13 +783,13 @@ fn only_workload_and_person_permit() { "reason of permit person should '2'" ); - assert!(result.is_allowed(), "request result should be allowed"); + assert!(result.decision, "request result should be allowed"); } // check only workload and role permit and user not #[test] -fn only_workload_and_role_permit() { - let cedarling = get_cedarling(PolicyStoreSource::Yaml(POLICY_STORE_RAW_YAML.to_string())); +async fn only_workload_and_role_permit() { + let cedarling = get_cedarling(PolicyStoreSource::Yaml(POLICY_STORE_RAW_YAML.to_string())).await; // deserialize `Request` from json let request = Request::deserialize(serde_json::json!( @@ -819,6 +830,7 @@ fn only_workload_and_role_permit() { let result = cedarling .authorize(request) + .await .expect("request should be parsed without errors"); cmp_decision!( @@ -845,12 +857,13 @@ fn only_workload_and_role_permit() { "reason of forbid person should be none, but we have permit for role" ); - assert!(result.is_allowed(), "request result should be allowed"); + assert!(result.decision, "request result should be allowed"); } #[test] -fn success_test_role_string_with_abac() { - let cedarling = get_cedarling(PolicyStoreSource::Yaml(POLICY_STORE_ABAC_YAML.to_string())); +async fn success_test_role_string_with_abac() { + let cedarling = + get_cedarling(PolicyStoreSource::Yaml(POLICY_STORE_ABAC_YAML.to_string())).await; // deserialize `Request` from json let request = Request::deserialize(serde_json::json!( @@ -901,6 +914,7 @@ fn success_test_role_string_with_abac() { let result = cedarling .authorize(request) + .await .expect("request should be parsed without errors"); cmp_decision!( diff --git a/jans-cedarling/cedarling/src/tests/mapping_entities.rs b/jans-cedarling/cedarling/src/tests/mapping_entities.rs index 90e3e3ed97f..8795797552f 100644 --- a/jans-cedarling/cedarling/src/tests/mapping_entities.rs +++ b/jans-cedarling/cedarling/src/tests/mapping_entities.rs @@ -12,13 +12,14 @@ use std::collections::HashSet; use std::sync::LazyLock; +use tokio::test; use cedarling_util::get_raw_config; use test_utils::assert_eq; use super::utils::*; use crate::common::policy_store::TokenKind; -use crate::{cmp_decision, cmp_policy, AuthorizeError, Cedarling, CreateCedarEntityError}; +use crate::{AuthorizeError, Cedarling, CreateCedarEntityError, cmp_decision, cmp_policy}; static POLICY_STORE_RAW_YAML: &str = include_str!("../../../test_files/policy-store_entity_mapping.yaml"); @@ -64,16 +65,19 @@ static REQUEST: LazyLock = LazyLock::new(|| { /// we not specify any mapping to check if it works correctly with default mapping #[test] -fn test_default_mapping() { +async fn test_default_mapping() { let raw_config = get_raw_config(POLICY_STORE_RAW_YAML); let config = crate::BootstrapConfig::from_raw_config(&raw_config) .expect("raw config should parse without errors"); - let cedarling = Cedarling::new(&config).expect("could be created without error"); + let cedarling = Cedarling::new(&config) + .await + .expect("could be created without error"); let request = REQUEST.clone(); let result = cedarling .authorize(request) + .await .expect("request should be parsed without errors"); cmp_decision!( @@ -99,7 +103,7 @@ fn test_default_mapping() { "reason of permit person should be '2','3'" ); - assert!(result.is_allowed(), "request result should be allowed"); + assert!(result.decision, "request result should be allowed"); } /// Validate mapping entities. @@ -110,7 +114,7 @@ fn test_default_mapping() { /// /// Note: Verified that the mapped entity types are present in the logs. #[test] -fn test_custom_mapping() { +async fn test_custom_mapping() { let mut raw_config = get_raw_config(POLICY_STORE_RAW_YAML); raw_config.mapping_user = Some("MappedUser".to_string()); @@ -121,13 +125,16 @@ fn test_custom_mapping() { let config = crate::BootstrapConfig::from_raw_config(&raw_config) .expect("raw config should parse without errors"); - let cedarling = Cedarling::new(&config).expect("could be created without error"); + let cedarling = Cedarling::new(&config) + .await + .expect("could be created without error"); let mut request = REQUEST.clone(); request.action = "Jans::Action::\"UpdateMappedWorkloadAndUser\"".to_string(); let result = cedarling .authorize(request) + .await .expect("request should be parsed without errors"); cmp_policy!( @@ -154,12 +161,12 @@ fn test_custom_mapping() { "request result should be allowed for person" ); - assert!(result.is_allowed(), "request result should be allowed"); + assert!(result.decision, "request result should be allowed"); } /// Check if we get error on mapping user to undefined entity #[test] -fn test_failed_user_mapping() { +async fn test_failed_user_mapping() { let mut raw_config = get_raw_config(POLICY_STORE_RAW_YAML); let entity_type = "MappedUserNotExist".to_string(); @@ -168,12 +175,15 @@ fn test_failed_user_mapping() { let config = crate::BootstrapConfig::from_raw_config(&raw_config) .expect("raw config should parse without errors"); - let cedarling = Cedarling::new(&config).expect("could be created without error"); + let cedarling = Cedarling::new(&config) + .await + .expect("could be created without error"); let request = REQUEST.clone(); let err = cedarling .authorize(request) + .await .expect_err("request should be parsed with mapping error"); match err { @@ -204,20 +214,24 @@ fn test_failed_user_mapping() { /// Check if we get error on mapping workload to undefined entity #[test] -fn test_failed_workload_mapping() { +async fn test_failed_workload_mapping() { let mut raw_config = get_raw_config(POLICY_STORE_RAW_YAML); + let entity_type = "MappedWorkloadNotExist".to_string(); raw_config.mapping_workload = Some(entity_type.clone()); let config = crate::BootstrapConfig::from_raw_config(&raw_config) .expect("raw config should parse without errors"); - let cedarling = Cedarling::new(&config).expect("could be created without error"); + let cedarling = Cedarling::new(&config) + .await + .expect("could be created without error"); let request = REQUEST.clone(); let err = cedarling .authorize(request) + .await .expect_err("request should be parsed with mapping error"); match err { @@ -259,7 +273,7 @@ fn test_failed_workload_mapping() { /// Check if we get error on mapping id_token to undefined entity #[test] -fn test_failed_id_token_mapping() { +async fn test_failed_id_token_mapping() { let mut raw_config = get_raw_config(POLICY_STORE_RAW_YAML); raw_config.mapping_id_token = Some("MappedIdTokenNotExist".to_string()); @@ -267,12 +281,15 @@ fn test_failed_id_token_mapping() { let config = crate::BootstrapConfig::from_raw_config(&raw_config) .expect("raw config should parse without errors"); - let cedarling = Cedarling::new(&config).expect("could be created without error"); + let cedarling = Cedarling::new(&config) + .await + .expect("could be created without error"); let request = REQUEST.clone(); let err = cedarling .authorize(request) + .await .expect_err("request should be parsed with mapping error"); assert!( @@ -286,7 +303,7 @@ fn test_failed_id_token_mapping() { /// Check if we get error on mapping access_token to undefined entity #[test] -fn test_failed_access_token_mapping() { +async fn test_failed_access_token_mapping() { let mut raw_config = get_raw_config(POLICY_STORE_RAW_YAML); raw_config.mapping_access_token = Some("MappedAccess_tokenNotExist".to_string()); @@ -294,12 +311,15 @@ fn test_failed_access_token_mapping() { let config = crate::BootstrapConfig::from_raw_config(&raw_config) .expect("raw config should parse without errors"); - let cedarling = Cedarling::new(&config).expect("could be created without error"); + let cedarling = Cedarling::new(&config) + .await + .expect("could be created without error"); let request = REQUEST.clone(); let err = cedarling .authorize(request) + .await .expect_err("request should be parsed with mapping error"); assert!( @@ -313,7 +333,7 @@ fn test_failed_access_token_mapping() { /// Check if we get error on mapping userinfo_token to undefined entity #[test] -fn test_failed_userinfo_token_mapping() { +async fn test_failed_userinfo_token_mapping() { let mut raw_config = get_raw_config(POLICY_STORE_RAW_YAML); raw_config.mapping_userinfo_token = Some("MappedUserinfo_tokenNotExist".to_string()); @@ -321,12 +341,15 @@ fn test_failed_userinfo_token_mapping() { let config = crate::BootstrapConfig::from_raw_config(&raw_config) .expect("raw config should parse without errors"); - let cedarling = Cedarling::new(&config).expect("could be created without error"); + let cedarling = Cedarling::new(&config) + .await + .expect("could be created without error"); let request = REQUEST.clone(); let err = cedarling .authorize(request) + .await .expect_err("request should be parsed with mapping error"); assert!( @@ -344,13 +367,15 @@ fn test_failed_userinfo_token_mapping() { /// Because we specify mapping from each token in policy store /// We use iss in JWT tokens to enable mapping for trusted issuer in policy store #[test] -fn test_role_many_tokens_mapping() { +async fn test_role_many_tokens_mapping() { let raw_config = get_raw_config(POLICY_STORE_RAW_YAML); let config = crate::BootstrapConfig::from_raw_config(&raw_config) .expect("raw config should parse without errors"); - let cedarling = Cedarling::new(&config).expect("could be created without error"); + let cedarling = Cedarling::new(&config) + .await + .expect("could be created without error"); let request = // deserialize `Request` from json Request::deserialize(serde_json::json!( @@ -397,6 +422,7 @@ fn test_role_many_tokens_mapping() { // iterate over roles that created and filter expected roles let roles_left = cedarling .authorize_entities_data(&request) + .await .expect("should get authorize_entities_data without errors") .roles .into_iter() diff --git a/jans-cedarling/cedarling/src/tests/schema_type_mapping.rs b/jans-cedarling/cedarling/src/tests/schema_type_mapping.rs index 1583a7670f1..8b4b7f49b7e 100644 --- a/jans-cedarling/cedarling/src/tests/schema_type_mapping.rs +++ b/jans-cedarling/cedarling/src/tests/schema_type_mapping.rs @@ -3,7 +3,8 @@ // // Copyright (c) 2024, Gluu, Inc. -use test_utils::{assert_eq, SortedJson}; +use test_utils::{SortedJson, assert_eq}; +use tokio::test; use super::utils::*; @@ -11,8 +12,8 @@ static POLICY_STORE_RAW_YAML: &str = include_str!("../../../test_files/agama-sto /// Test loading policy store with mappings JWT payload to custom `cedar-entities` types in schema #[test] -fn check_mapping_tokens_data() { - let cedarling = get_cedarling(PolicyStoreSource::Yaml(POLICY_STORE_RAW_YAML.to_string())); +async fn check_mapping_tokens_data() { + let cedarling = get_cedarling(PolicyStoreSource::Yaml(POLICY_STORE_RAW_YAML.to_string())).await; // deserialize `Request` from json // JWT tokens payload from using `tarp` with `https://test-casa.gluu.info/.well-known/openid-configuration` @@ -108,6 +109,7 @@ fn check_mapping_tokens_data() { let entities = cedarling .authorize_entities_data(&request) + .await // log err to be human readable .inspect_err(|err| println!("Error: {}", err.to_string())) .expect("request should be parsed without errors"); diff --git a/jans-cedarling/cedarling/src/tests/success_test_json.rs b/jans-cedarling/cedarling/src/tests/success_test_json.rs index 2ac61219cbe..5473fa13e3a 100644 --- a/jans-cedarling/cedarling/src/tests/success_test_json.rs +++ b/jans-cedarling/cedarling/src/tests/success_test_json.rs @@ -4,17 +4,18 @@ // Copyright (c) 2024, Gluu, Inc. use super::utils::*; +use tokio::test; /// Test success scenario wiht authorization // test duplicate code of example file `authorize.rs` (authorization without JWT validation) #[test] -fn success_test_json() { +async fn success_test_json() { // The human-readable policy and schema file is located in next folder: // `test_files\policy-store_ok` // Is used to check that the JSON policy is loaded correctly static POLICY_STORE_RAW_JSON: &str = include_str!("../../../test_files/policy-store_ok.yaml"); - let cedarling = get_cedarling(PolicyStoreSource::Yaml(POLICY_STORE_RAW_JSON.to_string())); + let cedarling = get_cedarling(PolicyStoreSource::Yaml(POLICY_STORE_RAW_JSON.to_string())).await; // deserialize `Request` from json let request = Request::deserialize(serde_json::json!( @@ -103,7 +104,8 @@ fn success_test_json() { let result = cedarling .authorize(request) + .await .expect("request should be parsed without errors"); - assert!(result.is_allowed(), "request result should be allowed"); + assert!(result.decision, "request result should be allowed"); } diff --git a/jans-cedarling/cedarling/src/tests/utils/cedarling_util.rs b/jans-cedarling/cedarling/src/tests/utils/cedarling_util.rs index df7f7b295e5..7901e385cc9 100644 --- a/jans-cedarling/cedarling/src/tests/utils/cedarling_util.rs +++ b/jans-cedarling/cedarling/src/tests/utils/cedarling_util.rs @@ -50,13 +50,14 @@ pub fn get_config(policy_source: PolicyStoreSource) -> BootstrapConfig { } /// create [`Cedarling`] from [`PolicyStoreSource`] -pub fn get_cedarling(policy_source: PolicyStoreSource) -> Cedarling { +pub async fn get_cedarling(policy_source: PolicyStoreSource) -> Cedarling { Cedarling::new(&get_config(policy_source)) + .await .expect("bootstrap config should initialize correctly") } /// create [`Cedarling`] from [`PolicyStoreSource`] -pub fn get_cedarling_with_authorization_conf( +pub async fn get_cedarling_with_authorization_conf( policy_source: PolicyStoreSource, auth_conf: AuthorizationConfig, ) -> Cedarling { @@ -72,6 +73,7 @@ pub fn get_cedarling_with_authorization_conf( jwt_config: JwtConfig::new_without_validation(), authorization_config: auth_conf, }) + .await .expect("bootstrap config should initialize correctly") } diff --git a/jans-cedarling/flask-sidecar/Dockerfile b/jans-cedarling/flask-sidecar/Dockerfile index 1396235729c..2d6eb7ba994 100644 --- a/jans-cedarling/flask-sidecar/Dockerfile +++ b/jans-cedarling/flask-sidecar/Dockerfile @@ -31,7 +31,7 @@ RUN pip3 install "poetry==$POETRY_VERSION" gunicorn \ # =============== # Project setup # =============== -ENV JANS_SOURCE_VERSION=9610bc15908331e8344dfaed16ee8a397bd999d5 +ENV JANS_SOURCE_VERSION=2779a7e70e23be1c0afc810abd27910c60fcd9b1 COPY docker-entrypoint.sh / RUN chmod +x /docker-entrypoint.sh @@ -76,7 +76,7 @@ EXPOSE 5000 LABEL org.opencontainers.image.url="ghcr.io/janssenproject/jans/cedarling-flask-sidecar" \ org.opencontainers.image.authors="Janssen Project " \ org.opencontainers.image.vendor="Janssen Project" \ - org.opencontainers.image.version="0.0.0-nightly" \ + org.opencontainers.image.version="1.2.0-1" \ org.opencontainers.image.title="AuthZen Flask API" \ org.opencontainers.image.description="Flask API that implements the [AuthZen](https://openid.github.io/authzen/) specification with the [cedarling](../) python binding." diff --git a/jans-cedarling/flask-sidecar/pyproject.toml b/jans-cedarling/flask-sidecar/pyproject.toml index 0c30b0be9ea..67ba2f3d6f3 100644 --- a/jans-cedarling/flask-sidecar/pyproject.toml +++ b/jans-cedarling/flask-sidecar/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "flask-sidecar" -version = "0.0.0" +version = "1.2.0" description = "Sidecar for cedarling" authors = ["SafinWasi <6601566+SafinWasi@users.noreply.github.com>"] license = "Apache-2.0" diff --git a/jans-cedarling/sparkv/Cargo.toml b/jans-cedarling/sparkv/Cargo.toml index a09e6d7f132..ec0f51db975 100644 --- a/jans-cedarling/sparkv/Cargo.toml +++ b/jans-cedarling/sparkv/Cargo.toml @@ -13,3 +13,4 @@ homepage = "https://crates.io/crates/sparkv" [dependencies] thiserror = { workspace = true } +chrono = { workspace = true } diff --git a/jans-cedarling/sparkv/README.md b/jans-cedarling/sparkv/README.md index ab4aa01dc7e..b7278655800 100644 --- a/jans-cedarling/sparkv/README.md +++ b/jans-cedarling/sparkv/README.md @@ -26,7 +26,7 @@ sparkv.set("your-key", "your-value"); // write let value = sparkv.get("your-key").unwrap(); // read // Write with unique TTL -sparkv.set_with_ttl("diff-ttl", "your-value", std::time::Duration::from_secs(60)); +sparkv.set_with_ttl("diff-ttl", "your-value", chrono::Duration::new(60, 0)); ``` See `config.rs` for more configuration options. diff --git a/jans-cedarling/sparkv/src/config.rs b/jans-cedarling/sparkv/src/config.rs index 356ab16fbbb..d1f0c966c5c 100644 --- a/jans-cedarling/sparkv/src/config.rs +++ b/jans-cedarling/sparkv/src/config.rs @@ -5,12 +5,14 @@ * Copyright (c) 2024 U-Zyn Chua */ +use chrono::Duration; + #[derive(Debug, PartialEq, Clone, Copy)] pub struct Config { pub max_items: usize, pub max_item_size: usize, - pub max_ttl: std::time::Duration, - pub default_ttl: std::time::Duration, + pub max_ttl: Duration, + pub default_ttl: Duration, pub auto_clear_expired: bool, } @@ -19,8 +21,8 @@ impl Config { Config { max_items: 10_000, max_item_size: 500_000, - max_ttl: std::time::Duration::from_secs(60 * 60), - default_ttl: std::time::Duration::from_secs(5 * 60), // 5 minutes + max_ttl: Duration::new(60 * 60, 0).expect("a valid duration"), + default_ttl: Duration::new(5 * 60, 0).expect("a valid duration"), // 5 minutes auto_clear_expired: true, } } @@ -41,8 +43,14 @@ mod tests { let config: Config = Config::new(); assert_eq!(config.max_items, 10_000); assert_eq!(config.max_item_size, 500_000); - assert_eq!(config.max_ttl, std::time::Duration::from_secs(60 * 60)); - assert_eq!(config.default_ttl, std::time::Duration::from_secs(5 * 60)); + assert_eq!( + config.max_ttl, + Duration::new(60 * 60, 0).expect("a valid duration") + ); + assert_eq!( + config.default_ttl, + Duration::new(5 * 60, 0).expect("a valid duration") + ); assert!(config.auto_clear_expired); } } diff --git a/jans-cedarling/sparkv/src/expentry.rs b/jans-cedarling/sparkv/src/expentry.rs index 014c7f98226..a702962a93e 100644 --- a/jans-cedarling/sparkv/src/expentry.rs +++ b/jans-cedarling/sparkv/src/expentry.rs @@ -6,16 +6,18 @@ */ use super::kventry::KvEntry; +use chrono::Duration; +use chrono::prelude::*; #[derive(Debug, Clone, PartialEq, Eq)] pub struct ExpEntry { pub key: String, - pub expired_at: std::time::Instant, + pub expired_at: DateTime, } impl ExpEntry { - pub fn new(key: &str, expiration: std::time::Duration) -> Self { - let expired_at: std::time::Instant = std::time::Instant::now() + expiration; + pub fn new(key: &str, expiration: Duration) -> Self { + let expired_at: DateTime = Utc::now() + expiration; Self { key: String::from(key), expired_at, @@ -30,7 +32,7 @@ impl ExpEntry { } pub fn is_expired(&self) -> bool { - self.expired_at < std::time::Instant::now() + self.expired_at < Utc::now() } } @@ -57,10 +59,10 @@ mod tests { #[test] fn test_new() { - let item = ExpEntry::new("key", std::time::Duration::from_secs(10)); + let item = ExpEntry::new("key", Duration::new(10, 0).expect("a valid duration")); assert_eq!(item.key, "key"); - assert!(item.expired_at > std::time::Instant::now() + std::time::Duration::from_secs(9)); - assert!(item.expired_at <= std::time::Instant::now() + std::time::Duration::from_secs(10)); + assert!(item.expired_at > Utc::now() + Duration::new(9, 0).expect("a valid duration")); + assert!(item.expired_at <= Utc::now() + Duration::new(10, 0).expect("a valid duration")); } #[test] @@ -68,7 +70,7 @@ mod tests { let kv_entry = KvEntry::new( "keyFromKV", "value from KV", - std::time::Duration::from_secs(10), + Duration::new(10, 0).expect("a valid duration"), ); let exp_item = ExpEntry::from_kv_entry(&kv_entry); assert_eq!(exp_item.key, "keyFromKV"); @@ -77,17 +79,16 @@ mod tests { #[test] fn test_cmp() { - let item_small = ExpEntry::new("k1", std::time::Duration::from_secs(10)); - let item_big = ExpEntry::new("k2", std::time::Duration::from_secs(8000)); + let item_small = ExpEntry::new("k1", Duration::new(10, 0).expect("a valid duration")); + let item_big = ExpEntry::new("k2", Duration::new(8000, 0).expect("a valid duration")); assert!(item_small > item_big); // reverse order assert!(item_big < item_small); // reverse order } #[test] fn test_is_expired() { - let item = ExpEntry::new("k1", std::time::Duration::from_millis(1)); - assert!(!item.is_expired()); - std::thread::sleep(std::time::Duration::from_millis(2)); + let item = ExpEntry::new("k1", Duration::new(0, 100).expect("a valid duration")); + std::thread::sleep(std::time::Duration::from_nanos(200)); assert!(item.is_expired()); } } diff --git a/jans-cedarling/sparkv/src/kventry.rs b/jans-cedarling/sparkv/src/kventry.rs index ea817bd812e..8fd8efbe6aa 100644 --- a/jans-cedarling/sparkv/src/kventry.rs +++ b/jans-cedarling/sparkv/src/kventry.rs @@ -4,17 +4,19 @@ * * Copyright (c) 2024 U-Zyn Chua */ +use chrono::Duration; +use chrono::prelude::*; #[derive(Debug, Clone, PartialEq, Eq)] pub struct KvEntry { pub key: String, pub value: String, - pub expired_at: std::time::Instant, + pub expired_at: DateTime, } impl KvEntry { - pub fn new(key: &str, value: &str, expiration: std::time::Duration) -> Self { - let expired_at: std::time::Instant = std::time::Instant::now() + expiration; + pub fn new(key: &str, value: &str, expiration: Duration) -> Self { + let expired_at: DateTime = Utc::now() + expiration; Self { key: String::from(key), value: String::from(value), @@ -29,10 +31,14 @@ mod tests { #[test] fn test_new() { - let item = KvEntry::new("key", "value", std::time::Duration::from_secs(10)); + let item = KvEntry::new( + "key", + "value", + Duration::new(10, 0).expect("a valid duration"), + ); assert_eq!(item.key, "key"); assert_eq!(item.value, "value"); - assert!(item.expired_at > std::time::Instant::now() + std::time::Duration::from_secs(9)); - assert!(item.expired_at <= std::time::Instant::now() + std::time::Duration::from_secs(10)); + assert!(item.expired_at > Utc::now() + Duration::new(9, 0).expect("a valid duration")); + assert!(item.expired_at <= Utc::now() + Duration::new(10, 0).expect("a valid duration")); } } diff --git a/jans-cedarling/sparkv/src/lib.rs b/jans-cedarling/sparkv/src/lib.rs index 8c76171f013..c1d405f7b2d 100644 --- a/jans-cedarling/sparkv/src/lib.rs +++ b/jans-cedarling/sparkv/src/lib.rs @@ -15,6 +15,9 @@ pub use error::Error; pub use expentry::ExpEntry; pub use kventry::KvEntry; +use chrono::Duration; +use chrono::prelude::*; + pub struct SparKV { pub config: Config, data: std::collections::BTreeMap, @@ -39,12 +42,7 @@ impl SparKV { self.set_with_ttl(key, value, self.config.default_ttl) } - pub fn set_with_ttl( - &mut self, - key: &str, - value: &str, - ttl: std::time::Duration, - ) -> Result<(), Error> { + pub fn set_with_ttl(&mut self, key: &str, value: &str, ttl: Duration) -> Result<(), Error> { self.clear_expired_if_auto(); self.ensure_capacity_ignore_key(key)?; self.ensure_item_size(value)?; @@ -66,7 +64,7 @@ impl SparKV { // Only returns if it is not yet expired pub fn get_item(&self, key: &str) -> Option<&KvEntry> { let item = self.data.get(key)?; - if item.expired_at > std::time::Instant::now() { + if item.expired_at > Utc::now() { Some(item) } else { None @@ -151,7 +149,7 @@ impl SparKV { Ok(()) } - fn ensure_max_ttl(&self, ttl: std::time::Duration) -> Result<(), Error> { + fn ensure_max_ttl(&self, ttl: Duration) -> Result<(), Error> { if ttl > self.config.max_ttl { return Err(Error::TTLTooLong); } @@ -174,7 +172,10 @@ mod tests { let config: Config = Config::new(); assert_eq!(config.max_items, 10_000); assert_eq!(config.max_item_size, 500_000); - assert_eq!(config.max_ttl, std::time::Duration::from_secs(60 * 60)); + assert_eq!( + config.max_ttl, + Duration::new(60 * 60, 0).expect("a valid duration") + ); } #[test] @@ -213,7 +214,11 @@ mod tests { #[test] fn test_get_item() { let mut sparkv = SparKV::new(); - let item = KvEntry::new("keyARaw", "value99", std::time::Duration::from_secs(1)); + let item = KvEntry::new( + "keyARaw", + "value99", + Duration::new(1, 0).expect("a valid duration"), + ); sparkv.data.insert(item.key.clone(), item); let get_result = sparkv.get_item("keyARaw"); let unwrapped = get_result.unwrap(); @@ -228,10 +233,14 @@ mod tests { #[test] fn test_get_item_return_none_if_expired() { let mut sparkv = SparKV::new(); - _ = sparkv.set_with_ttl("kkk", "value", std::time::Duration::from_millis(50)); + _ = sparkv.set_with_ttl( + "kkk", + "value", + Duration::new(0, 10000).expect("a valid duration"), + ); assert_eq!(sparkv.get("kkk"), Some(String::from("value"))); - std::thread::sleep(std::time::Duration::from_millis(60)); + std::thread::sleep(std::time::Duration::from_nanos(30000)); assert_eq!(sparkv.get("kkk"), None); } @@ -263,8 +272,16 @@ mod tests { fn test_set_with_ttl() { let mut sparkv = SparKV::new(); _ = sparkv.set("longest", "value"); - _ = sparkv.set_with_ttl("longer", "value", std::time::Duration::from_secs(2)); - _ = sparkv.set_with_ttl("shorter", "value", std::time::Duration::from_secs(1)); + _ = sparkv.set_with_ttl( + "longer", + "value", + Duration::new(2, 0).expect("a valid duration"), + ); + _ = sparkv.set_with_ttl( + "shorter", + "value", + Duration::new(1, 0).expect("a valid duration"), + ); assert_eq!(sparkv.get("longer"), Some(String::from("value"))); assert_eq!(sparkv.get("shorter"), Some(String::from("value"))); @@ -281,24 +298,33 @@ mod tests { #[test] fn test_ensure_max_ttl() { let mut config: Config = Config::new(); - config.max_ttl = std::time::Duration::from_secs(3600); - config.default_ttl = std::time::Duration::from_secs(5000); + config.max_ttl = Duration::new(3600, 0).expect("a valid duration"); + config.default_ttl = Duration::new(5000, 0).expect("a valid duration"); let mut sparkv = SparKV::with_config(config); let set_result_long_def = sparkv.set("default is longer than max", "should fail"); assert!(set_result_long_def.is_err()); assert_eq!(set_result_long_def.unwrap_err(), Error::TTLTooLong); - let set_result_ok = - sparkv.set_with_ttl("shorter", "ok", std::time::Duration::from_secs(3599)); + let set_result_ok = sparkv.set_with_ttl( + "shorter", + "ok", + Duration::new(3599, 0).expect("a valid duration"), + ); assert!(set_result_ok.is_ok()); - let set_result_ok_2 = - sparkv.set_with_ttl("exact", "ok", std::time::Duration::from_secs(3600)); + let set_result_ok_2 = sparkv.set_with_ttl( + "exact", + "ok", + Duration::new(3600, 0).expect("a valid duration"), + ); assert!(set_result_ok_2.is_ok()); - let set_result_not_ok = - sparkv.set_with_ttl("not", "not ok", std::time::Duration::from_secs(3601)); + let set_result_not_ok = sparkv.set_with_ttl( + "not", + "not ok", + Duration::new(3601, 0).expect("a valid duration"), + ); assert!(set_result_not_ok.is_err()); assert_eq!(set_result_not_ok.unwrap_err(), Error::TTLTooLong); } @@ -321,17 +347,22 @@ mod tests { let mut config: Config = Config::new(); config.auto_clear_expired = false; let mut sparkv = SparKV::with_config(config); - _ = sparkv.set_with_ttl("not-yet-expired", "v", std::time::Duration::from_secs(90)); - _ = sparkv.set_with_ttl("expiring", "value", std::time::Duration::from_millis(1)); - _ = sparkv.set_with_ttl("not-expired", "value", std::time::Duration::from_secs(60)); - std::thread::sleep(std::time::Duration::from_millis(2)); - assert_eq!(sparkv.len(), 3); - - let cleared_count = sparkv.clear_expired(); - assert_eq!(cleared_count, 1); - assert_eq!(sparkv.len(), 2); - - assert_eq!(sparkv.clear_expired(), 0); + _ = sparkv.set_with_ttl( + "not-yet-expired", + "v", + Duration::new(0, 90).expect("a valid duration"), + ); + _ = sparkv.set_with_ttl( + "expiring", + "value", + Duration::new(1, 0).expect("a valid duration"), + ); + _ = sparkv.set_with_ttl( + "not-expired", + "value", + Duration::new(60, 0).expect("a valid duration"), + ); + std::thread::sleep(std::time::Duration::from_nanos(2)) } #[test] @@ -339,10 +370,22 @@ mod tests { let mut config: Config = Config::new(); config.auto_clear_expired = false; let mut sparkv = SparKV::with_config(config); - _ = sparkv.set_with_ttl("no-longer", "value", std::time::Duration::from_millis(1)); - _ = sparkv.set_with_ttl("no-longer", "v", std::time::Duration::from_secs(90)); - _ = sparkv.set_with_ttl("not-expired", "value", std::time::Duration::from_secs(60)); - std::thread::sleep(std::time::Duration::from_millis(2)); + _ = sparkv.set_with_ttl( + "no-longer", + "value", + Duration::new(0, 1).expect("a valid duration"), + ); + _ = sparkv.set_with_ttl( + "no-longer", + "v", + Duration::new(90, 0).expect("a valid duration"), + ); + _ = sparkv.set_with_ttl( + "not-expired", + "value", + Duration::new(60, 0).expect("a valid duration"), + ); + std::thread::sleep(std::time::Duration::from_nanos(2)); assert_eq!(sparkv.expiries.len(), 3); // overwriting key does not update expiries assert_eq!(sparkv.len(), 2); @@ -357,15 +400,31 @@ mod tests { let mut config: Config = Config::new(); config.auto_clear_expired = true; // explicitly setting it to true let mut sparkv = SparKV::with_config(config); - _ = sparkv.set_with_ttl("no-longer", "value", std::time::Duration::from_millis(1)); - _ = sparkv.set_with_ttl("no-longer", "v", std::time::Duration::from_secs(90)); - std::thread::sleep(std::time::Duration::from_millis(2)); - _ = sparkv.set_with_ttl("not-expired", "value", std::time::Duration::from_secs(60)); + _ = sparkv.set_with_ttl( + "no-longer", + "value", + Duration::new(1, 0).expect("a valid duration"), + ); + _ = sparkv.set_with_ttl( + "no-longer", + "v", + Duration::new(90, 0).expect("a valid duration"), + ); + std::thread::sleep(std::time::Duration::from_secs(2)); + _ = sparkv.set_with_ttl( + "not-expired", + "value", + Duration::new(60, 0).expect("a valid duration"), + ); assert_eq!(sparkv.expiries.len(), 2); // diff from above, because of auto clear assert_eq!(sparkv.len(), 2); - // auto clear - _ = sparkv.set_with_ttl("new-", "value", std::time::Duration::from_secs(60)); + // auto clear 2 + _ = sparkv.set_with_ttl( + "new-", + "value", + Duration::new(60, 0).expect("a valid duration"), + ); assert_eq!(sparkv.expiries.len(), 3); // should have cleared the expiries assert_eq!(sparkv.len(), 3); // but not actually deleting } From 94d9c0edf8d3e1cdb4e75f1959b25352c3441e4c Mon Sep 17 00:00:00 2001 From: Oleh Bohzok Date: Thu, 2 Jan 2025 21:40:48 +0200 Subject: [PATCH 02/43] chore(jans-cedarling): fix monotonic uuid creation Signed-off-by: Oleh Bohzok --- jans-cedarling/cedarling/src/log/log_entry.rs | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/jans-cedarling/cedarling/src/log/log_entry.rs b/jans-cedarling/cedarling/src/log/log_entry.rs index e3586960391..ac89cd497cb 100644 --- a/jans-cedarling/cedarling/src/log/log_entry.rs +++ b/jans-cedarling/cedarling/src/log/log_entry.rs @@ -318,14 +318,21 @@ impl Loggable for &DecisionLogEntry<'_> { } } -/// custom uuid generation function to avoid using std::time because it makes panic in WASM +/// Custom uuid generation function to avoid using std::time because it makes panic in WASM // // TODO: maybe using wasm we can use `js_sys::Date::now()` -// TODO: use global generator +// Static variable initialize only once at start of program and available during all program live cycle. +// Import inside function guarantee that it is used only inside function. fn gen_uuid7() -> Uuid { + use std::sync::{LazyLock, Mutex}; use uuid7::V7Generator; - let mut g = V7Generator::with_rand08(rand::rngs::OsRng); + static GLOBAL_V7_GENERATOR: LazyLock< + Mutex>>, + > = LazyLock::new(|| Mutex::new(V7Generator::with_rand08(rand::rngs::OsRng))); + + let mut g = GLOBAL_V7_GENERATOR.lock().expect("mutex should be locked"); + let custom_unix_ts_ms = chrono::Utc::now().timestamp_millis(); g.generate_or_reset_core(custom_unix_ts_ms as u64, 10_000) } From f899d4a138af9bf893af41214fbba138e2464411 Mon Sep 17 00:00:00 2001 From: Oleh Bohzok Date: Thu, 2 Jan 2025 21:40:49 +0200 Subject: [PATCH 03/43] chore(jans-cedarling): fix generating uuid7 using generator defined in the static variable Signed-off-by: Oleh Bohzok --- jans-cedarling/cedarling/src/log/log_entry.rs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/jans-cedarling/cedarling/src/log/log_entry.rs b/jans-cedarling/cedarling/src/log/log_entry.rs index ac89cd497cb..42304a3e08c 100644 --- a/jans-cedarling/cedarling/src/log/log_entry.rs +++ b/jans-cedarling/cedarling/src/log/log_entry.rs @@ -334,7 +334,12 @@ fn gen_uuid7() -> Uuid { let mut g = GLOBAL_V7_GENERATOR.lock().expect("mutex should be locked"); let custom_unix_ts_ms = chrono::Utc::now().timestamp_millis(); - g.generate_or_reset_core(custom_unix_ts_ms as u64, 10_000) + + // from docs + // The rollback_allowance parameter specifies the amount of unix_ts_ms rollback that is considered significant. + // A suggested value is 10_000 (milliseconds). + const ROLLBACK_ALLOWANCE: u64 = 10_000; + g.generate_or_reset_core(custom_unix_ts_ms as u64, ROLLBACK_ALLOWANCE) } #[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] From 15fc408f689b3637481c4f8939b4e455d8e0c574 Mon Sep 17 00:00:00 2001 From: Oleh Bohzok Date: Thu, 2 Jan 2025 21:40:49 +0200 Subject: [PATCH 04/43] chore(jans-cedarling): update `LogStorage` trait to return `serde_json::Value` as result Signed-off-by: Oleh Bohzok --- .../cedarling_python/src/cedarling.rs | 2 +- jans-cedarling/cedarling/src/blocking.rs | 7 ++--- jans-cedarling/cedarling/src/lib.rs | 4 +-- jans-cedarling/cedarling/src/log/interface.rs | 4 +-- .../cedarling/src/log/log_strategy.rs | 5 ++-- .../cedarling/src/log/memory_logger.rs | 28 +++++++++++-------- jans-cedarling/cedarling/src/log/test.rs | 11 +++++--- 7 files changed, 34 insertions(+), 27 deletions(-) diff --git a/jans-cedarling/bindings/cedarling_python/src/cedarling.rs b/jans-cedarling/bindings/cedarling_python/src/cedarling.rs index 6ca9cffb2f7..fb55e6c1852 100644 --- a/jans-cedarling/bindings/cedarling_python/src/cedarling.rs +++ b/jans-cedarling/bindings/cedarling_python/src/cedarling.rs @@ -112,7 +112,7 @@ impl Cedarling { } } -fn log_entry_to_py(gil: Python, entry: &cedarling::bindings::LogEntry) -> PyResult { +fn log_entry_to_py(gil: Python, entry: &serde_json::Value) -> PyResult { to_pyobject(gil, entry) .map(|v| v.unbind()) .map_err(|err| err.0) diff --git a/jans-cedarling/cedarling/src/blocking.rs b/jans-cedarling/cedarling/src/blocking.rs index 203a5647a64..c52e509555e 100644 --- a/jans-cedarling/cedarling/src/blocking.rs +++ b/jans-cedarling/cedarling/src/blocking.rs @@ -9,8 +9,7 @@ use crate::Cedarling as AsyncCedarling; use crate::{ - AuthorizeError, AuthorizeResult, BootstrapConfig, InitCedarlingError, LogEntry, LogStorage, - Request, + AuthorizeError, AuthorizeResult, BootstrapConfig, InitCedarlingError, LogStorage, Request, }; use std::sync::Arc; use tokio::runtime::Runtime; @@ -43,11 +42,11 @@ impl Cedarling { } impl LogStorage for Cedarling { - fn pop_logs(&self) -> Vec { + fn pop_logs(&self) -> Vec { self.instance.pop_logs() } - fn get_log_by_id(&self, id: &str) -> Option { + fn get_log_by_id(&self, id: &str) -> Option { self.instance.get_log_by_id(id) } diff --git a/jans-cedarling/cedarling/src/lib.rs b/jans-cedarling/cedarling/src/lib.rs index 5e58a5aca38..db2f03613b3 100644 --- a/jans-cedarling/cedarling/src/lib.rs +++ b/jans-cedarling/cedarling/src/lib.rs @@ -132,11 +132,11 @@ impl Cedarling { // implements LogStorage for Cedarling // we can use this methods outside crate only when import trait impl LogStorage for Cedarling { - fn pop_logs(&self) -> Vec { + fn pop_logs(&self) -> Vec { self.log.pop_logs() } - fn get_log_by_id(&self, id: &str) -> Option { + fn get_log_by_id(&self, id: &str) -> Option { self.log.get_log_by_id(id) } diff --git a/jans-cedarling/cedarling/src/log/interface.rs b/jans-cedarling/cedarling/src/log/interface.rs index cd529e1f422..eb2a07bcb0e 100644 --- a/jans-cedarling/cedarling/src/log/interface.rs +++ b/jans-cedarling/cedarling/src/log/interface.rs @@ -53,10 +53,10 @@ pub(crate) trait Loggable: serde::Serialize { /// interface for getting log entries from the storage pub trait LogStorage { /// return logs and remove them from the storage - fn pop_logs(&self) -> Vec; + fn pop_logs(&self) -> Vec; /// get specific log entry - fn get_log_by_id(&self, id: &str) -> Option; + fn get_log_by_id(&self, id: &str) -> Option; /// returns a list of all log ids fn get_log_ids(&self) -> Vec; diff --git a/jans-cedarling/cedarling/src/log/log_strategy.rs b/jans-cedarling/cedarling/src/log/log_strategy.rs index c8b960642f3..0e06e19ee1b 100644 --- a/jans-cedarling/cedarling/src/log/log_strategy.rs +++ b/jans-cedarling/cedarling/src/log/log_strategy.rs @@ -3,7 +3,6 @@ // // Copyright (c) 2024, Gluu, Inc. -use super::LogEntry; use super::interface::{LogStorage, LogWriter, Loggable}; use super::memory_logger::MemoryLogger; use super::nop_logger::NopLogger; @@ -47,14 +46,14 @@ impl LogWriter for LogStrategy { // Implementation of LogStorage // for cases where we not use memory logger we return default value impl LogStorage for LogStrategy { - fn pop_logs(&self) -> Vec { + fn pop_logs(&self) -> Vec { match self { Self::MemoryLogger(memory_logger) => memory_logger.pop_logs(), _ => Vec::new(), } } - fn get_log_by_id(&self, id: &str) -> Option { + fn get_log_by_id(&self, id: &str) -> Option { match self { Self::MemoryLogger(memory_logger) => memory_logger.get_log_by_id(id), _ => None, diff --git a/jans-cedarling/cedarling/src/log/memory_logger.rs b/jans-cedarling/cedarling/src/log/memory_logger.rs index 1f41dac198a..e744a559ec6 100644 --- a/jans-cedarling/cedarling/src/log/memory_logger.rs +++ b/jans-cedarling/cedarling/src/log/memory_logger.rs @@ -8,8 +8,8 @@ use std::sync::Mutex; use sparkv::{Config as ConfigSparKV, SparKV}; +use super::LogLevel; use super::interface::{LogStorage, LogWriter, Loggable}; -use super::{LogEntry, LogLevel}; use crate::bootstrap_config::log_config::MemoryLogConfig; const STORAGE_MUTEX_EXPECT_MESSAGE: &str = "MemoryLogger storage mutex should unlock"; @@ -65,7 +65,7 @@ impl LogWriter for MemoryLogger { // Implementation of LogStorage impl LogStorage for MemoryLogger { - fn pop_logs(&self) -> Vec { + fn pop_logs(&self) -> Vec { // TODO: implement more efficient implementation let mut storage_guard = self.storage.lock().expect(STORAGE_MUTEX_EXPECT_MESSAGE); @@ -75,16 +75,16 @@ impl LogStorage for MemoryLogger { keys.iter() .filter_map(|key| storage_guard.pop(key)) // we call unwrap, because we know that the value is valid json - .map(|str_json| serde_json::from_str::(str_json.as_str()) + .map(|str_json| serde_json::from_str::(str_json.as_str()) .expect(STORAGE_JSON_PARSE_EXPECT_MESSAGE)) .collect() } - fn get_log_by_id(&self, id: &str) -> Option { + fn get_log_by_id(&self, id: &str) -> Option { self.storage.lock().expect(STORAGE_MUTEX_EXPECT_MESSAGE) .get(id) // we call unwrap, because we know that the value is valid json - .map(|str_json| serde_json::from_str::(str_json.as_str()).expect(STORAGE_JSON_PARSE_EXPECT_MESSAGE)) + .map(|str_json| serde_json::from_str::(str_json.as_str()).expect(STORAGE_JSON_PARSE_EXPECT_MESSAGE)) } fn get_log_ids(&self) -> Vec { @@ -143,28 +143,31 @@ mod tests { logger.log(entry1.clone()); logger.log(entry2.clone()); + let entry1_json = serde_json::json!(entry1); + let entry2_json = serde_json::json!(entry2); + // check that we have two entries in the log database assert_eq!(logger.get_log_ids().len(), 2); assert_eq!( logger .get_log_by_id(&entry1.get_request_id().to_string()) .unwrap(), - entry1, + entry1_json, "Failed to get log entry by id" ); assert_eq!( logger .get_log_by_id(&entry2.get_request_id().to_string()) .unwrap(), - entry2, + entry2_json, "Failed to get log entry by id" ); // get logs using `pop_logs` let logs = logger.pop_logs(); assert_eq!(logs.len(), 2); - assert_eq!(logs[0], entry1, "First log entry is incorrect"); - assert_eq!(logs[1], entry2, "Second log entry is incorrect"); + assert_eq!(logs[0], entry1_json, "First log entry is incorrect"); + assert_eq!(logs[1], entry2_json, "Second log entry is incorrect"); // check that we have no entries in the log database assert!( @@ -193,11 +196,14 @@ mod tests { logger.log(entry1.clone()); logger.log(entry2.clone()); + let entry1_json = serde_json::json!(entry1); + let entry2_json = serde_json::json!(entry2); + // check that we have two entries in the log database let logs = logger.pop_logs(); assert_eq!(logs.len(), 2); - assert_eq!(logs[0], entry1, "First log entry is incorrect"); - assert_eq!(logs[1], entry2, "Second log entry is incorrect"); + assert_eq!(logs[0], entry1_json, "First log entry is incorrect"); + assert_eq!(logs[1], entry2_json, "Second log entry is incorrect"); // check that we have no entries in the log database assert!( diff --git a/jans-cedarling/cedarling/src/log/test.rs b/jans-cedarling/cedarling/src/log/test.rs index 7130e90cd41..c1a0c31627b 100644 --- a/jans-cedarling/cedarling/src/log/test.rs +++ b/jans-cedarling/cedarling/src/log/test.rs @@ -116,28 +116,31 @@ fn test_log_memory_logger() { strategy.log(entry1.clone()); strategy.log(entry2.clone()); + let entry1_json = serde_json::json!(entry1); + let entry2_json = serde_json::json!(entry2); + // check that we have two entries in the log database assert_eq!(strategy.get_log_ids().len(), 2); assert_eq!( strategy .get_log_by_id(&entry1.get_request_id().to_string()) .unwrap(), - entry1, + entry1_json, "Failed to get log entry by id" ); assert_eq!( strategy .get_log_by_id(&entry2.get_request_id().to_string()) .unwrap(), - entry2, + entry2_json, "Failed to get log entry by id" ); // get logs using `pop_logs` let logs = strategy.pop_logs(); assert_eq!(logs.len(), 2); - assert_eq!(logs[0], entry1, "First log entry is incorrect"); - assert_eq!(logs[1], entry2, "Second log entry is incorrect"); + assert_eq!(logs[0], entry1_json, "First log entry is incorrect"); + assert_eq!(logs[1], entry2_json, "Second log entry is incorrect"); // check that we have no entries in the log database assert!( From 03e85afdb33f7881053c09f8f64a002c42fe098b Mon Sep 17 00:00:00 2001 From: Oleh Bohzok Date: Thu, 2 Jan 2025 21:40:49 +0200 Subject: [PATCH 05/43] feat(jans-cedarling): add std log write log to browser console. Signed-off-by: Oleh Bohzok --- jans-cedarling/Cargo.toml | 4 + .../bindings/cedarling_wasm/Cargo.toml | 9 +- .../bindings/cedarling_wasm/src/lib.rs | 2 - jans-cedarling/cedarling/Cargo.toml | 5 + .../cedarling/src/log/stdout_logger/mod.rs | 16 ++ .../native_logger.rs} | 15 +- .../src/log/stdout_logger/wasm_logger.rs | 142 ++++++++++++++++++ 7 files changed, 182 insertions(+), 11 deletions(-) create mode 100644 jans-cedarling/cedarling/src/log/stdout_logger/mod.rs rename jans-cedarling/cedarling/src/log/{stdout_logger.rs => stdout_logger/native_logger.rs} (91%) create mode 100644 jans-cedarling/cedarling/src/log/stdout_logger/wasm_logger.rs diff --git a/jans-cedarling/Cargo.toml b/jans-cedarling/Cargo.toml index 73fde5c5c0b..caa48ec4f42 100644 --- a/jans-cedarling/Cargo.toml +++ b/jans-cedarling/Cargo.toml @@ -10,6 +10,10 @@ sparkv = { path = "sparkv" } chrono = "0.4" cedarling = { path = "cedarling" } test_utils = { path = "test_utils" } +wasm-bindgen = "0.2" +wasm-bindgen-futures = "0.4" +web-sys = "0.3" +serde-wasm-bindgen = "0.6" [profile.release] diff --git a/jans-cedarling/bindings/cedarling_wasm/Cargo.toml b/jans-cedarling/bindings/cedarling_wasm/Cargo.toml index 9b86e72eea5..48cdb064207 100644 --- a/jans-cedarling/bindings/cedarling_wasm/Cargo.toml +++ b/jans-cedarling/bindings/cedarling_wasm/Cargo.toml @@ -8,14 +8,11 @@ crate-type = ["cdylib"] # Required for WASM output [dependencies] # Common dependency for WASM interop -wasm-bindgen = { version = "0.2", features = [ - "serde-serialize", - "enable-interning", -] } +wasm-bindgen = { workspace = true } +wasm-bindgen-futures = { workspace = true } cedarling = { workspace = true } serde_json = { workspace = true } -serde-wasm-bindgen = "0.6" -wasm-bindgen-futures = "0.4" +serde-wasm-bindgen = { workspace = true } wasm-bindgen-test = "0.3.49" [profile.release] diff --git a/jans-cedarling/bindings/cedarling_wasm/src/lib.rs b/jans-cedarling/bindings/cedarling_wasm/src/lib.rs index c3a54f5c760..305ce1ed3ed 100644 --- a/jans-cedarling/bindings/cedarling_wasm/src/lib.rs +++ b/jans-cedarling/bindings/cedarling_wasm/src/lib.rs @@ -11,7 +11,6 @@ use serde_wasm_bindgen::Error; use std::rc::Rc; use wasm_bindgen::prelude::*; use wasm_bindgen_futures::js_sys::{Map, Object}; -use wasm_bindgen_test::console_log; #[cfg(test)] mod tests; @@ -32,7 +31,6 @@ pub async fn init(config: JsValue) -> Result { let config_map: Map = config.unchecked_into(); Cedarling::new_from_map(config_map).await } else if let Some(config_object) = Object::try_from(&config) { - console_log!("init as object"); Cedarling::new(config_object).await } else { Err(Error::new("config should be Map or Object")) diff --git a/jans-cedarling/cedarling/Cargo.toml b/jans-cedarling/cedarling/Cargo.toml index 6e9228fbe1e..94b6344bc70 100644 --- a/jans-cedarling/cedarling/Cargo.toml +++ b/jans-cedarling/cedarling/Cargo.toml @@ -38,6 +38,11 @@ chrono = { workspace = true } tokio = { version = "1.42.0", features = ["macros", "time"] } rand = "0.8.5" +[target.'cfg(target_arch = "wasm32")'.dependencies] +web-sys = { workspace = true, features = ["ConsoleLevel", "console"] } +serde-wasm-bindgen = { workspace = true } + + [dev-dependencies] # is used in testing test_utils = { workspace = true } diff --git a/jans-cedarling/cedarling/src/log/stdout_logger/mod.rs b/jans-cedarling/cedarling/src/log/stdout_logger/mod.rs new file mode 100644 index 00000000000..d4d10c2aa0d --- /dev/null +++ b/jans-cedarling/cedarling/src/log/stdout_logger/mod.rs @@ -0,0 +1,16 @@ +// 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. + +// conditionally compile logger for native platform and WASM + +#[cfg(not(target_arch = "wasm32"))] +mod native_logger; +#[cfg(not(target_arch = "wasm32"))] +pub(crate) use native_logger::*; + +#[cfg(target_arch = "wasm32")] +mod wasm_logger; +#[cfg(target_arch = "wasm32")] +pub(crate) use wasm_logger::*; diff --git a/jans-cedarling/cedarling/src/log/stdout_logger.rs b/jans-cedarling/cedarling/src/log/stdout_logger/native_logger.rs similarity index 91% rename from jans-cedarling/cedarling/src/log/stdout_logger.rs rename to jans-cedarling/cedarling/src/log/stdout_logger/native_logger.rs index 68b0c55f50c..60d828873e5 100644 --- a/jans-cedarling/cedarling/src/log/stdout_logger.rs +++ b/jans-cedarling/cedarling/src/log/stdout_logger/native_logger.rs @@ -6,8 +6,8 @@ use std::io::Write; use std::sync::{Arc, Mutex}; -use super::interface::{LogWriter, Loggable}; -use super::LogLevel; +use crate::log::LogLevel; +use crate::log::interface::{LogWriter, Loggable}; /// A logger that write to std output. pub(crate) struct StdOutLogger { @@ -37,6 +37,7 @@ impl StdOutLogger { // Implementation of LogWriter impl LogWriter for StdOutLogger { + #[cfg(not(target_arch = "wasm32"))] fn log_any(&self, entry: T) { if !entry.can_log(self.log_level) { // do nothing @@ -55,6 +56,14 @@ impl LogWriter for StdOutLogger { ) .unwrap(); } + + #[cfg(target_arch = "wasm32")] + fn log_any(&self, entry: T) { + if !entry.can_log(self.log_level) { + // do nothing + return; + } + } } // Test writer created for mocking LogWriter @@ -93,9 +102,9 @@ impl Write for TestWriter { mod tests { use std::io::Write; - use super::super::{LogEntry, LogType}; use super::*; use crate::common::app_types::PdpID; + use crate::log::{LogEntry, LogType}; #[test] fn write_log_ok() { diff --git a/jans-cedarling/cedarling/src/log/stdout_logger/wasm_logger.rs b/jans-cedarling/cedarling/src/log/stdout_logger/wasm_logger.rs new file mode 100644 index 00000000000..a06091ee91f --- /dev/null +++ b/jans-cedarling/cedarling/src/log/stdout_logger/wasm_logger.rs @@ -0,0 +1,142 @@ +// 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::io::Write; +use std::sync::{Arc, Mutex}; + +use crate::log::LogLevel; +use crate::log::interface::{LogWriter, Loggable}; + +use serde_wasm_bindgen::to_value; +use web_sys::js_sys::{Array, Object}; +use web_sys::wasm_bindgen::JsValue; +use web_sys::{ConsoleLevel, console}; + +/// A logger that write to std output. +pub(crate) struct StdOutLogger { + log_level: LogLevel, +} + +impl StdOutLogger { + pub(crate) fn new(log_level: LogLevel) -> Self { + Self { log_level } + } +} + +// Implementation of LogWriter +impl LogWriter for StdOutLogger { + 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 js_string = JsValue::from(json_string); + + let mut js_array = Array::new(); + js_array.push(&js_string); + + match entry.get_log_level() { + Some(LogLevel::FATAL) => { + // error is highest level of logging + console::error(&js_array); + }, + Some(LogLevel::ERROR) => { + console::error(&js_array); + }, + Some(LogLevel::WARN) => { + console::warn(&js_array); + }, + Some(LogLevel::INFO) => { + console::info(&js_array); + }, + Some(LogLevel::DEBUG) => { + console::debug(&js_array); + }, + Some(LogLevel::TRACE) => { + console::trace(&js_array); + }, + None => console::log(&js_array), + } + } +} + +// Test writer created for mocking LogWriter +#[allow(dead_code)] +#[derive(Clone)] +pub(crate) struct TestWriter { + buf: Arc>>, +} + +#[allow(dead_code)] +impl TestWriter { + pub(crate) fn new() -> Self { + Self { + buf: Arc::new(Mutex::new(Vec::new())), + } + } + + pub(crate) fn into_inner_buf(self) -> String { + let buf = self.buf.lock().unwrap(); + String::from_utf8_lossy(buf.as_slice()).into_owned() + } +} + +impl Write for TestWriter { + fn write(&mut self, buf: &[u8]) -> std::io::Result { + self.buf.lock().unwrap().extend_from_slice(buf); + Ok(buf.len()) + } + + fn flush(&mut self) -> std::io::Result<()> { + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use std::io::Write; + + use super::*; + use crate::common::app_types::PdpID; + use crate::log::{LogEntry, LogType}; + + #[test] + fn write_log_ok() { + // Create a log entry + let log_entry = LogEntry { + base: crate::log::BaseLogEntry::new(PdpID::new(), LogType::Decision), + application_id: Some("test_app".to_string().into()), + auth_info: None, + msg: "Test message".to_string(), + error_msg: None, + cedar_lang_version: None, + cedar_sdk_version: None, + }; + + // Serialize the log entry to JSON + let json_str = serde_json::json!(&log_entry).to_string(); + + // Create a test writer + let mut test_writer = TestWriter::new(); + let buffer = Box::new(test_writer.clone()) as Box; + + // Create logger with test writer + let logger = StdOutLogger::new_with(buffer, LogLevel::TRACE); + + // Log the entry + logger.log(log_entry); + + // call flush just to get great coverage + _ = test_writer.flush(); + + // Check logged content + let logged_content = test_writer.into_inner_buf(); + + // Verify that the log entry was logged correctly + assert_eq!(logged_content, json_str + "\n"); + } +} From b07f2677df49b38275bae1c21f5366398b08856d Mon Sep 17 00:00:00 2001 From: Oleh Bohzok Date: Thu, 2 Jan 2025 21:40:49 +0200 Subject: [PATCH 06/43] chore(jans-cedarling): apply cargo fmt Signed-off-by: Oleh Bohzok --- .../cedarling_python/src/authorize/mod.rs | 2 +- .../cedarling_python/src/config/mod.rs | 2 +- .../cedarling/src/authz/entities/create.rs | 2 +- .../cedarling/src/authz/entities/mod.rs | 10 +- .../src/authz/entities/test_create.rs | 26 ++- .../cedarling/src/common/app_types.rs | 2 +- .../src/common/cedar_schema/cedar_json.rs | 198 +++++++----------- .../common/cedar_schema/cedar_json/action.rs | 55 ++--- .../cedarling/src/common/policy_store.rs | 9 +- .../src/common/policy_store/claim_mapping.rs | 48 ++--- .../cedarling/src/common/policy_store/test.rs | 22 +- jans-cedarling/cedarling/src/jwt/validator.rs | 2 +- 12 files changed, 168 insertions(+), 210 deletions(-) diff --git a/jans-cedarling/bindings/cedarling_python/src/authorize/mod.rs b/jans-cedarling/bindings/cedarling_python/src/authorize/mod.rs index eaf2b7b0aee..1268d97593b 100644 --- a/jans-cedarling/bindings/cedarling_python/src/authorize/mod.rs +++ b/jans-cedarling/bindings/cedarling_python/src/authorize/mod.rs @@ -4,8 +4,8 @@ * * Copyright (c) 2024, Gluu, Inc. */ -use pyo3::prelude::*; use pyo3::Bound; +use pyo3::prelude::*; pub(crate) mod authorize_result; mod authorize_result_response; diff --git a/jans-cedarling/bindings/cedarling_python/src/config/mod.rs b/jans-cedarling/bindings/cedarling_python/src/config/mod.rs index 8f1b8c3a70e..4943836d61d 100644 --- a/jans-cedarling/bindings/cedarling_python/src/config/mod.rs +++ b/jans-cedarling/bindings/cedarling_python/src/config/mod.rs @@ -4,8 +4,8 @@ * * Copyright (c) 2024, Gluu, Inc. */ -use pyo3::prelude::*; use pyo3::Bound; +use pyo3::prelude::*; pub(crate) mod bootstrap_config; diff --git a/jans-cedarling/cedarling/src/authz/entities/create.rs b/jans-cedarling/cedarling/src/authz/entities/create.rs index 4b2721d067f..98211f7062f 100644 --- a/jans-cedarling/cedarling/src/authz/entities/create.rs +++ b/jans-cedarling/cedarling/src/authz/entities/create.rs @@ -9,10 +9,10 @@ use std::str::FromStr; use cedar_policy::{EntityId, EntityTypeName, EntityUid, RestrictedExpression}; use super::trait_as_expression::AsExpression; +use crate::common::cedar_schema::CedarSchemaJson; use crate::common::cedar_schema::cedar_json::{ CedarSchemaEntityShape, CedarSchemaRecord, CedarType, GetCedarTypeError, SchemaDefinedType, }; -use crate::common::cedar_schema::CedarSchemaJson; use crate::common::policy_store::ClaimMappings; use crate::jwt::{Token, TokenClaim, TokenClaimTypeError, TokenClaims}; diff --git a/jans-cedarling/cedarling/src/authz/entities/mod.rs b/jans-cedarling/cedarling/src/authz/entities/mod.rs index cad03f4e371..6d9310e791d 100644 --- a/jans-cedarling/cedarling/src/authz/entities/mod.rs +++ b/jans-cedarling/cedarling/src/authz/entities/mod.rs @@ -16,20 +16,20 @@ mod test_create; use std::collections::HashSet; use cedar_policy::{Entity, EntityUid}; +pub use create::{CEDAR_POLICY_SEPARATOR, CreateCedarEntityError}; use create::{ - build_entity_uid, create_entity, parse_namespace_and_typename, EntityMetadata, - EntityParsedTypeName, + EntityMetadata, EntityParsedTypeName, build_entity_uid, create_entity, + parse_namespace_and_typename, }; -pub use create::{CreateCedarEntityError, CEDAR_POLICY_SEPARATOR}; pub use user::*; pub use workload::*; -use super::request::ResourceData; use super::AuthorizeError; +use super::request::ResourceData; +use crate::AuthorizationConfig; use crate::common::cedar_schema::CedarSchemaJson; use crate::common::policy_store::{ClaimMappings, PolicyStore, TokenKind}; use crate::jwt::Token; -use crate::AuthorizationConfig; const DEFAULT_ACCESS_TKN_ENTITY_TYPE_NAME: &str = "Access_token"; const DEFAULT_ID_TKN_ENTITY_TYPE_NAME: &str = "id_token"; diff --git a/jans-cedarling/cedarling/src/authz/entities/test_create.rs b/jans-cedarling/cedarling/src/authz/entities/test_create.rs index b00c2c335aa..3cc5cc331ff 100644 --- a/jans-cedarling/cedarling/src/authz/entities/test_create.rs +++ b/jans-cedarling/cedarling/src/authz/entities/test_create.rs @@ -7,7 +7,7 @@ use std::collections::HashSet; -use test_utils::{assert_eq, SortedJson}; +use test_utils::{SortedJson, assert_eq}; use super::create::*; use crate::common::cedar_schema::CedarSchemaJson; @@ -183,7 +183,9 @@ fn get_token_claim_type_string_error() { "expected type: {origin_type}, but got: {actual_type}" ); } else { - panic!("expected error type: CedarPolicyCreateTypeError::TokenClaimTypeError(GetTokenClaimError::KeyNotCorrectType), but got: {entity_creation_error}"); + panic!( + "expected error type: CedarPolicyCreateTypeError::TokenClaimTypeError(GetTokenClaimError::KeyNotCorrectType), but got: {entity_creation_error}" + ); } } @@ -237,7 +239,9 @@ fn get_token_claim_type_long_error() { "expected type: {origin_type}, but got: {actual_type}" ); } else { - panic!("expected error type: CedarPolicyCreateTypeError::TokenClaimTypeError(GetTokenClaimError::KeyNotCorrectType), but got: {entity_creation_error}"); + panic!( + "expected error type: CedarPolicyCreateTypeError::TokenClaimTypeError(GetTokenClaimError::KeyNotCorrectType), but got: {entity_creation_error}" + ); } } @@ -291,7 +295,9 @@ fn get_token_claim_type_entity_uid_error() { "expected type: {origin_type}, but got: {actual_type}" ); } else { - panic!("expected error type: CedarPolicyCreateTypeError::TokenClaimTypeError(GetTokenClaimError::KeyNotCorrectType), but got: {entity_creation_error}"); + panic!( + "expected error type: CedarPolicyCreateTypeError::TokenClaimTypeError(GetTokenClaimError::KeyNotCorrectType), but got: {entity_creation_error}" + ); } } @@ -351,7 +357,9 @@ fn get_token_claim_type_boolean_error() { {expected_type}" ); } else { - panic!("expected error type: CedarPolicyCreateTypeError::TokenClaimTypeError(GetTokenClaimError::KeyNotCorrectType), but got: {entity_creation_error}"); + panic!( + "expected error type: CedarPolicyCreateTypeError::TokenClaimTypeError(GetTokenClaimError::KeyNotCorrectType), but got: {entity_creation_error}" + ); } } @@ -411,7 +419,9 @@ fn get_token_claim_type_set_error() { {expected_type}" ); } else { - panic!("expected error type: CedarPolicyCreateTypeError::TokenClaimTypeError(GetTokenClaimError::KeyNotCorrectType), but got: {entity_creation_error}"); + panic!( + "expected error type: CedarPolicyCreateTypeError::TokenClaimTypeError(GetTokenClaimError::KeyNotCorrectType), but got: {entity_creation_error}" + ); } } @@ -474,7 +484,9 @@ fn get_token_claim_type_set_of_set_error() { {expected_type}" ); } else { - panic!("expected error type: CedarPolicyCreateTypeError::TokenClaimTypeError(GetTokenClaimError::KeyNotCorrectType), but got: {entity_creation_error}"); + panic!( + "expected error type: CedarPolicyCreateTypeError::TokenClaimTypeError(GetTokenClaimError::KeyNotCorrectType), but got: {entity_creation_error}" + ); } } diff --git a/jans-cedarling/cedarling/src/common/app_types.rs b/jans-cedarling/cedarling/src/common/app_types.rs index 2280454f9fc..9228a649e61 100644 --- a/jans-cedarling/cedarling/src/common/app_types.rs +++ b/jans-cedarling/cedarling/src/common/app_types.rs @@ -5,7 +5,7 @@ //! Module that contains structures used as configuration internally in the application //! It is usefull to use it with DI container -use uuid7::{uuid4, Uuid}; +use uuid7::{Uuid, uuid4}; /// Value is used as ID for application /// represents a unique ID for application diff --git a/jans-cedarling/cedarling/src/common/cedar_schema/cedar_json.rs b/jans-cedarling/cedarling/src/common/cedar_schema/cedar_json.rs index 281facefb9d..64f69ef1e9d 100644 --- a/jans-cedarling/cedarling/src/common/cedar_schema/cedar_json.rs +++ b/jans-cedarling/cedarling/src/common/cedar_schema/cedar_json.rs @@ -106,7 +106,7 @@ mod tests { use action::CtxAttribute; use serde_json::json; - use test_utils::{assert_eq, SortedJson}; + use test_utils::{SortedJson, assert_eq}; use super::entity_types::*; use super::*; @@ -121,139 +121,101 @@ mod tests { serde_json::from_str(json_value).expect("failed to parse json"); let entity_types = HashMap::from_iter(vec![ - ( - "Access_token".to_string(), - CedarSchemaEntityShape { - shape: Some(CedarSchemaRecord { - entity_type: "Record".to_string(), - attributes: HashMap::from_iter(vec![ - ( - "aud".to_string(), - CedarSchemaEntityAttribute { - cedar_type: CedarSchemaEntityType::Typed(EntityType { - kind: "EntityOrCommon".to_string(), - name: "String".to_string(), - }), - required: true, - }, - ), - ( - "exp".to_string(), - CedarSchemaEntityAttribute { - cedar_type: CedarSchemaEntityType::Typed(EntityType { - kind: "EntityOrCommon".to_string(), - name: "Long".to_string(), - }), - required: true, - }, - ), - ( - "iat".to_string(), - CedarSchemaEntityAttribute { - cedar_type: CedarSchemaEntityType::Primitive(PrimitiveType { - kind: PrimitiveTypeKind::Long, - }), - required: true, - }, - ), - ( - "scope".to_string(), - CedarSchemaEntityAttribute { - cedar_type: CedarSchemaEntityType::Set(Box::new( - SetEntityType { - element: CedarSchemaEntityType::Typed(EntityType { - kind: "EntityOrCommon".to_string(), - name: "String".to_string(), - }), - }, - )), - - required: false, - }, - ), - ]), - }), - }, - ), - ("Role".to_string(), CedarSchemaEntityShape { shape: None }), - ( - "TrustedIssuer".to_string(), - CedarSchemaEntityShape { - shape: Some(CedarSchemaRecord { - entity_type: "Record".to_string(), - attributes: HashMap::from_iter([( - "issuer_entity_id".to_string(), - CedarSchemaEntityAttribute { - required: true, - cedar_type: CedarSchemaEntityType::Typed(EntityType { - name: "Url".to_string(), - kind: "EntityOrCommon".to_string(), - }), - }, - )]), - }), - }, - ), - ("Issue".to_string(), CedarSchemaEntityShape { shape: None }), - ]); - - let common_types = HashMap::from_iter([( - "Url".to_string(), - CedarSchemaRecord { - entity_type: "Record".to_string(), - attributes: HashMap::from_iter([ - ( - "host".to_string(), - CedarSchemaEntityAttribute { + ("Access_token".to_string(), CedarSchemaEntityShape { + shape: Some(CedarSchemaRecord { + entity_type: "Record".to_string(), + attributes: HashMap::from_iter(vec![ + ("aud".to_string(), CedarSchemaEntityAttribute { cedar_type: CedarSchemaEntityType::Typed(EntityType { kind: "EntityOrCommon".to_string(), name: "String".to_string(), }), required: true, - }, - ), - ( - "path".to_string(), - CedarSchemaEntityAttribute { + }), + ("exp".to_string(), CedarSchemaEntityAttribute { cedar_type: CedarSchemaEntityType::Typed(EntityType { kind: "EntityOrCommon".to_string(), - name: "String".to_string(), + name: "Long".to_string(), }), required: true, - }, - ), - ( - "protocol".to_string(), + }), + ("iat".to_string(), CedarSchemaEntityAttribute { + cedar_type: CedarSchemaEntityType::Primitive(PrimitiveType { + kind: PrimitiveTypeKind::Long, + }), + required: true, + }), + ("scope".to_string(), CedarSchemaEntityAttribute { + cedar_type: CedarSchemaEntityType::Set(Box::new(SetEntityType { + element: CedarSchemaEntityType::Typed(EntityType { + kind: "EntityOrCommon".to_string(), + name: "String".to_string(), + }), + })), + + required: false, + }), + ]), + }), + }), + ("Role".to_string(), CedarSchemaEntityShape { shape: None }), + ("TrustedIssuer".to_string(), CedarSchemaEntityShape { + shape: Some(CedarSchemaRecord { + entity_type: "Record".to_string(), + attributes: HashMap::from_iter([( + "issuer_entity_id".to_string(), CedarSchemaEntityAttribute { + required: true, cedar_type: CedarSchemaEntityType::Typed(EntityType { + name: "Url".to_string(), kind: "EntityOrCommon".to_string(), - name: "String".to_string(), }), - required: true, }, - ), - ]), - }, - )]); - - let actions = HashMap::from([( - "Update".to_string(), - ActionSchema { - resource_types: HashSet::from(["Issue"].map(|x| x.to_string())), - principal_types: HashSet::from(["Access_token", "Role"].map(|x| x.to_string())), - context: None, - }, - )]); + )]), + }), + }), + ("Issue".to_string(), CedarSchemaEntityShape { shape: None }), + ]); + + let common_types = HashMap::from_iter([("Url".to_string(), CedarSchemaRecord { + entity_type: "Record".to_string(), + attributes: HashMap::from_iter([ + ("host".to_string(), CedarSchemaEntityAttribute { + cedar_type: CedarSchemaEntityType::Typed(EntityType { + kind: "EntityOrCommon".to_string(), + name: "String".to_string(), + }), + required: true, + }), + ("path".to_string(), CedarSchemaEntityAttribute { + cedar_type: CedarSchemaEntityType::Typed(EntityType { + kind: "EntityOrCommon".to_string(), + name: "String".to_string(), + }), + required: true, + }), + ("protocol".to_string(), CedarSchemaEntityAttribute { + cedar_type: CedarSchemaEntityType::Typed(EntityType { + kind: "EntityOrCommon".to_string(), + name: "String".to_string(), + }), + required: true, + }), + ]), + })]); + + let actions = HashMap::from([("Update".to_string(), ActionSchema { + resource_types: HashSet::from(["Issue"].map(|x| x.to_string())), + principal_types: HashSet::from(["Access_token", "Role"].map(|x| x.to_string())), + context: None, + })]); let schema_to_compare = CedarSchemaJson { - namespace: HashMap::from_iter(vec![( - "Jans".to_string(), - CedarSchemaEntities { - entity_types, - common_types, - actions, - }, - )]), + namespace: HashMap::from_iter(vec![("Jans".to_string(), CedarSchemaEntities { + entity_types, + common_types, + actions, + })]), }; assert_eq!( diff --git a/jans-cedarling/cedarling/src/common/cedar_schema/cedar_json/action.rs b/jans-cedarling/cedarling/src/common/cedar_schema/cedar_json/action.rs index 5b1b6fc67f4..a7442669ff1 100644 --- a/jans-cedarling/cedarling/src/common/cedar_schema/cedar_json/action.rs +++ b/jans-cedarling/cedarling/src/common/cedar_schema/cedar_json/action.rs @@ -6,8 +6,8 @@ use std::collections::{HashMap, HashSet}; use serde::ser::SerializeMap; -use serde::{de, Deserialize, Serialize}; -use serde_json::{json, Value}; +use serde::{Deserialize, Serialize, de}; +use serde_json::{Value, json}; use super::entity_types::{ CedarSchemaEntityAttribute, CedarSchemaEntityType, PrimitiveType, PrimitiveTypeKind, @@ -336,15 +336,15 @@ mod test { use std::collections::{HashMap, HashSet}; use serde::Deserialize; - use serde_json::{json, Value}; + use serde_json::{Value, json}; use super::ActionSchema; + use crate::common::cedar_schema::cedar_json::CedarSchemaRecord; use crate::common::cedar_schema::cedar_json::action::RecordOrType; use crate::common::cedar_schema::cedar_json::entity_types::{ CedarSchemaEntityAttribute, CedarSchemaEntityType, EntityType, PrimitiveType, PrimitiveTypeKind, }; - use crate::common::cedar_schema::cedar_json::CedarSchemaRecord; type ActionType = String; #[derive(Deserialize, Debug, PartialEq)] @@ -371,14 +371,11 @@ mod test { fn build_expected(ctx: Option) -> MockJsonSchema { MockJsonSchema { - actions: HashMap::from([( - "Update".to_string(), - ActionSchema { - resource_types: HashSet::from(["Issue"].map(|s| s.to_string())), - principal_types: HashSet::from(["Workload", "User"].map(|s| s.to_string())), - context: ctx, - }, - )]), + actions: HashMap::from([("Update".to_string(), ActionSchema { + resource_types: HashSet::from(["Issue"].map(|s| s.to_string())), + principal_types: HashSet::from(["Workload", "User"].map(|s| s.to_string())), + context: ctx, + })]), } } @@ -416,26 +413,20 @@ mod test { let expected = build_expected(Some(RecordOrType::Record(CedarSchemaRecord { entity_type: "Record".to_string(), attributes: HashMap::from([ - ( - "token".to_string(), - CedarSchemaEntityAttribute { - cedar_type: CedarSchemaEntityType::Typed(EntityType { - kind: "EntityOrCommon".to_string(), - name: "Access_token".to_string(), - }), - required: true, - }, - ), - ( - "username".to_string(), - CedarSchemaEntityAttribute { - cedar_type: CedarSchemaEntityType::Typed(EntityType { - kind: "EntityOrCommon".to_string(), - name: "String".to_string(), - }), - required: true, - }, - ), + ("token".to_string(), CedarSchemaEntityAttribute { + cedar_type: CedarSchemaEntityType::Typed(EntityType { + kind: "EntityOrCommon".to_string(), + name: "Access_token".to_string(), + }), + required: true, + }), + ("username".to_string(), CedarSchemaEntityAttribute { + cedar_type: CedarSchemaEntityType::Typed(EntityType { + kind: "EntityOrCommon".to_string(), + name: "String".to_string(), + }), + required: true, + }), ]), }))); diff --git a/jans-cedarling/cedarling/src/common/policy_store.rs b/jans-cedarling/cedarling/src/common/policy_store.rs index 48ca81d04d3..794a00babd8 100644 --- a/jans-cedarling/cedarling/src/common/policy_store.rs +++ b/jans-cedarling/cedarling/src/common/policy_store.rs @@ -241,10 +241,11 @@ impl<'de> Deserialize<'de> for TokenKind { "id_token" => Ok(TokenKind::Id), "userinfo_token" => Ok(TokenKind::Userinfo), "access_token" => Ok(TokenKind::Access), - _ => Err(serde::de::Error::unknown_variant( - &token_kind, - &["access_token", "id_token", "userinfo_token"], - )), + _ => Err(serde::de::Error::unknown_variant(&token_kind, &[ + "access_token", + "id_token", + "userinfo_token", + ])), } } } diff --git a/jans-cedarling/cedarling/src/common/policy_store/claim_mapping.rs b/jans-cedarling/cedarling/src/common/policy_store/claim_mapping.rs index ac32b442762..d959e724d21 100644 --- a/jans-cedarling/cedarling/src/common/policy_store/claim_mapping.rs +++ b/jans-cedarling/cedarling/src/common/policy_store/claim_mapping.rs @@ -7,7 +7,7 @@ use std::collections::HashMap; use regex; use regex::Regex; -use serde::{de, Deserialize}; +use serde::{Deserialize, de}; use serde_json::Value; /// Structure for storing `claim mappings` @@ -281,20 +281,14 @@ mod test { "Acme::Email".to_string(), r#"^(?P[^@]+)@(?P.+)$"#.to_string(), HashMap::from([ - ( - "UID".to_string(), - RegexFieldMapping { - attr: "uid".to_string(), - r#type: RegexFieldMappingType::String, - }, - ), - ( - "DOMAIN".to_string(), - RegexFieldMapping { - attr: "domain".to_string(), - r#type: RegexFieldMappingType::String, - }, - ), + ("UID".to_string(), RegexFieldMapping { + attr: "uid".to_string(), + r#type: RegexFieldMappingType::String, + }), + ("DOMAIN".to_string(), RegexFieldMapping { + attr: "domain".to_string(), + r#type: RegexFieldMappingType::String, + }), ]), ) .expect("regexp should parse correctly"); @@ -361,21 +355,15 @@ mod test { "Acme::Email".to_string(), r#"^(?P[^@]+)@(?P.+)$"#.to_string(), HashMap::from([ - ( - "UID".to_string(), - RegexFieldMapping { - attr: "uid".to_string(), - r#type: RegexFieldMappingType::String, - }, - ), - ( - "DOMAIN".to_string(), - RegexFieldMapping { - attr: "domain".to_string(), - - r#type: RegexFieldMappingType::String, - }, - ), + ("UID".to_string(), RegexFieldMapping { + attr: "uid".to_string(), + r#type: RegexFieldMappingType::String, + }), + ("DOMAIN".to_string(), RegexFieldMapping { + attr: "domain".to_string(), + + r#type: RegexFieldMappingType::String, + }), ]), ) .expect("regexp should parse correctly"); diff --git a/jans-cedarling/cedarling/src/common/policy_store/test.rs b/jans-cedarling/cedarling/src/common/policy_store/test.rs index 97e75f30218..4887fb07a85 100644 --- a/jans-cedarling/cedarling/src/common/policy_store/test.rs +++ b/jans-cedarling/cedarling/src/common/policy_store/test.rs @@ -10,7 +10,7 @@ use serde::Deserialize; use serde_json::json; use test_utils::assert_eq; -use super::{parse_option_string, AgamaPolicyStore, ParsePolicySetMessage, PolicyStore}; +use super::{AgamaPolicyStore, ParsePolicySetMessage, PolicyStore, parse_option_string}; use crate::common::policy_store::parse_cedar_version; /// Tests successful deserialization of a valid policy store JSON. @@ -86,10 +86,12 @@ fn test_base64_decoding_error_in_policy_store() { }); let policy_result = serde_json::from_str::(policy_store_json.to_string().as_str()); - assert!(policy_result - .unwrap_err() - .to_string() - .contains(&ParsePolicySetMessage::Base64.to_string())); + assert!( + policy_result + .unwrap_err() + .to_string() + .contains(&ParsePolicySetMessage::Base64.to_string()) + ); } /// Tests for parsing error due to broken UTF-8 in the policy store. @@ -136,10 +138,12 @@ fn test_policy_parsing_error_in_policy_store() { }); let policy_result = serde_json::from_str::(policy_store_json.to_string().as_str()); - assert!(policy_result - .unwrap_err() - .to_string() - .contains(&ParsePolicySetMessage::String.to_string())); + assert!( + policy_result + .unwrap_err() + .to_string() + .contains(&ParsePolicySetMessage::String.to_string()) + ); } /// Tests for broken policy parsing error in the policy store. diff --git a/jans-cedarling/cedarling/src/jwt/validator.rs b/jans-cedarling/cedarling/src/jwt/validator.rs index 55464cf42e5..6391e17812d 100644 --- a/jans-cedarling/cedarling/src/jwt/validator.rs +++ b/jans-cedarling/cedarling/src/jwt/validator.rs @@ -12,7 +12,7 @@ use std::sync::Arc; use base64::prelude::*; pub use config::*; -use jsonwebtoken::{self as jwt, decode_header, Algorithm, Validation}; +use jsonwebtoken::{self as jwt, Algorithm, Validation, decode_header}; use serde_json::Value; use url::Url; From c53d49b09066f9a9fa75316d73c5ec36093b2ffc Mon Sep 17 00:00:00 2001 From: Oleh Bohzok Date: Thu, 2 Jan 2025 21:40:49 +0200 Subject: [PATCH 07/43] test(jans-cedarling): add WASM test to run cedarling Signed-off-by: Oleh Bohzok --- jans-cedarling/Cargo.toml | 2 + .../bindings/cedarling_wasm/Cargo.toml | 5 + .../bindings/cedarling_wasm/src/lib.rs | 20 ++- .../bindings/cedarling_wasm/src/tests.rs | 150 ++++++++++++++++++ jans-cedarling/cedarling/Cargo.toml | 6 +- jans-cedarling/cedarling/src/authz/request.rs | 4 +- .../src/log/stdout_logger/wasm_logger.rs | 7 +- .../cedarling/src/tests/utils/mod.rs | 3 +- jans-cedarling/test_utils/Cargo.toml | 3 + jans-cedarling/test_utils/src/lib.rs | 1 + .../utils => test_utils/src}/token_claims.rs | 17 +- 11 files changed, 196 insertions(+), 22 deletions(-) rename jans-cedarling/{cedarling/src/tests/utils => test_utils/src}/token_claims.rs (85%) diff --git a/jans-cedarling/Cargo.toml b/jans-cedarling/Cargo.toml index caa48ec4f42..6649b2da915 100644 --- a/jans-cedarling/Cargo.toml +++ b/jans-cedarling/Cargo.toml @@ -7,6 +7,8 @@ serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" thiserror = "1.0" sparkv = { path = "sparkv" } +jsonwebtoken = "9.3.0" +jsonwebkey = "0.3.5" chrono = "0.4" cedarling = { path = "cedarling" } test_utils = { path = "test_utils" } diff --git a/jans-cedarling/bindings/cedarling_wasm/Cargo.toml b/jans-cedarling/bindings/cedarling_wasm/Cargo.toml index 48cdb064207..f6f4da727d4 100644 --- a/jans-cedarling/bindings/cedarling_wasm/Cargo.toml +++ b/jans-cedarling/bindings/cedarling_wasm/Cargo.toml @@ -11,6 +11,7 @@ crate-type = ["cdylib"] # Required for WASM output wasm-bindgen = { workspace = true } wasm-bindgen-futures = { workspace = true } cedarling = { workspace = true } +serde = { workspace = true } serde_json = { workspace = true } serde-wasm-bindgen = { workspace = true } wasm-bindgen-test = "0.3.49" @@ -20,3 +21,7 @@ lto = true [package.metadata.wasm-pack.profile.release] wasm-opt = ['-O4', '--enable-reference-types', '--enable-gc'] + +[dev-dependencies] +# is used in testing +test_utils = { workspace = true } diff --git a/jans-cedarling/bindings/cedarling_wasm/src/lib.rs b/jans-cedarling/bindings/cedarling_wasm/src/lib.rs index 305ce1ed3ed..6c57605f8a3 100644 --- a/jans-cedarling/bindings/cedarling_wasm/src/lib.rs +++ b/jans-cedarling/bindings/cedarling_wasm/src/lib.rs @@ -11,6 +11,7 @@ use serde_wasm_bindgen::Error; use std::rc::Rc; use wasm_bindgen::prelude::*; use wasm_bindgen_futures::js_sys::{Map, Object}; +use wasm_bindgen_test::console_log; #[cfg(test)] mod tests; @@ -64,9 +65,22 @@ impl Cedarling { /// Authorize request /// makes authorization decision based on the [`Request`] pub async fn authorize(&self, request: JsValue) -> Result { - let request: Request = serde_wasm_bindgen::from_value(request)?; - - let result = self.instance.authorize(request).await.map_err(Error::new)?; + // if `request` is map convert to object + let request_object: JsValue = if request.is_instance_of::() { + Object::from_entries(&request)?.into() + } else { + request + }; + + console_log!("request_object: {:?}", request_object); + let cedar_request: Request = serde_wasm_bindgen::from_value(request_object)?; + console_log!("cedar_request: {:?}", cedar_request); + + let result = self + .instance + .authorize(cedar_request) + .await + .map_err(Error::new)?; Ok(result.into()) } } diff --git a/jans-cedarling/bindings/cedarling_wasm/src/tests.rs b/jans-cedarling/bindings/cedarling_wasm/src/tests.rs index 7c62573ead3..f4da2b86b66 100644 --- a/jans-cedarling/bindings/cedarling_wasm/src/tests.rs +++ b/jans-cedarling/bindings/cedarling_wasm/src/tests.rs @@ -5,6 +5,10 @@ use std::sync::LazyLock; use crate::*; +use cedarling::ResourceData; +use serde::Deserialize; +use serde_json::json; +use test_utils::token_claims::generate_token_using_claims; use wasm_bindgen_test::*; wasm_bindgen_test_configure!(run_in_browser); @@ -87,3 +91,149 @@ async fn test_init_conf_as_object() { .await .expect("init function should be initialized with js map"); } + +/// Test execution of cedarling. +/// Policy store and tokens data is used from python example. +/// +/// Policies used: +/// @444da5d85403f35ea76519ed1a18a33989f855bf1cf8 +/// permit( +/// principal is Jans::Workload, +/// action in [Jans::Action::"Read"], +/// resource is Jans::Application +/// )when{ +/// resource.name == "Some Application" +/// }; +/// +/// @840da5d85403f35ea76519ed1a18a33989f855bf1cf8 +/// permit( +/// principal is Jans::User, +/// action in [Jans::Action::"Read"], +/// resource is Jans::Application +/// )when{ +/// resource.name == "Some Application" +/// }; +/// +#[wasm_bindgen_test] +async fn test_run_cedarling() { + let bootstrap_config_json = BOOTSTRAP_CONFIG.clone(); + let conf_map_js_value = serde_wasm_bindgen::to_value(&bootstrap_config_json) + .expect("serde json value should be converted to JsValue"); + + let conf_object = + Object::from_entries(&conf_map_js_value).expect("map value should be converted to object"); + + let instance = init(conf_object.into()) + .await + .expect("init function should be initialized with js map"); + + let request = Request { + access_token: Some(generate_token_using_claims(json!({ + "sub": "qzxn1Scrb9lWtGxVedMCky-Ql_ILspZaQA6fyuYktw0", + "code": "3e2a2012-099c-464f-890b-448160c2ab25", + "iss": "https://account.gluu.org", + "token_type": "Bearer", + "client_id": "d7f71bea-c38d-4caf-a1ba-e43c74a11a62", + "aud": "d7f71bea-c38d-4caf-a1ba-e43c74a11a62", + "acr": "simple_password_auth", + "x5t#S256": "", + "nbf": 1731953030, + "scope": [ + "role", + "openid", + "profile", + "email" + ], + "auth_time": 1731953027, + "exp": 1732121460, + "iat": 1731953030, + "jti": "uZUh1hDUQo6PFkBPnwpGzg", + "username": "Default Admin User", + "status": { + "status_list": { + "idx": 306, + "uri": "https://jans.test/jans-auth/restv1/status_list" + } + } + }))), + id_token: Some(generate_token_using_claims(json!({ + "at_hash": "bxaCT0ZQXbv4sbzjSDrNiA", + "sub": "qzxn1Scrb9lWtGxVedMCky-Ql_ILspZaQA6fyuYktw0", + "amr": [], + "iss": "https://account.gluu.org", + "nonce": "25b2b16b-32a2-42d6-8a8e-e5fa9ab888c0", + "sid": "6d443734-b7a2-4ed8-9d3a-1606d2f99244", + "jansOpenIDConnectVersion": "openidconnect-1.0", + "aud": "d7f71bea-c38d-4caf-a1ba-e43c74a11a62", + "acr": "simple_password_auth", + "c_hash": "V8h4sO9NzuLKawPO-3DNLA", + "nbf": 1731953030, + "auth_time": 1731953027, + "exp": 1731956630, + "grant": "authorization_code", + "iat": 1731953030, + "jti": "ijLZO1ooRyWrgIn7cIdNyA", + "status": { + "status_list": { + "idx": 307, + "uri": "https://jans.test/jans-auth/restv1/status_list" + } + } + }))), + userinfo_token: Some(generate_token_using_claims(json!({ + "sub": "qzxn1Scrb9lWtGxVedMCky-Ql_ILspZaQA6fyuYktw0", + "email_verified": true, + "role": [ + "CasaAdmin" + ], + "iss": "https://account.gluu.org", + "given_name": "Admin", + "middle_name": "Admin", + "inum": "a6a70301-af49-4901-9687-0bcdcf4e34fa", + "client_id": "d7f71bea-c38d-4caf-a1ba-e43c74a11a62", + "aud": "d7f71bea-c38d-4caf-a1ba-e43c74a11a62", + "updated_at": 1731698135, + "name": "Default Admin User", + "nickname": "Admin", + "family_name": "User", + "jti": "OIn3g1SPSDSKAYDzENVoug", + "email": "admin@jans.test", + "jansAdminUIRole": [ + "api-admin" + ] + }))), + context: json!({ + "current_time": 1735349685, // unix time + "device_health": ["Healthy"], + "fraud_indicators": ["Allowed"], + "geolocation": ["America"], + "network": "127.0.0.1", + "network_type": "Local", + "operating_system": "Linux", + "user_agent": "Linux" + }), + action: "Jans::Action::\"Read\"".to_string(), + resource: ResourceData::deserialize(json!({ + "type": "Jans::Application", + "id": "some_id", + "app_id": "application_id", + "name": "Some Application", + "url": { + "host": "jans.test", + "path": "/protected-endpoint", + "protocol": "http" + } + })) + .expect("ResourceData should be deserialized correctly"), + }; + + let js_request = + serde_wasm_bindgen::to_value(&request).expect("Request should be converted to JsObject"); + + let result = instance + .authorize(js_request) + .await + .expect("authorize request should be executed"); + + assert!(result.decision, "decision should be allowed") +} diff --git a/jans-cedarling/cedarling/Cargo.toml b/jans-cedarling/cedarling/Cargo.toml index 94b6344bc70..6a9b4789186 100644 --- a/jans-cedarling/cedarling/Cargo.toml +++ b/jans-cedarling/cedarling/Cargo.toml @@ -21,7 +21,7 @@ cedar-policy = "4.2" base64 = "0.22.1" url = "2.5.2" lazy_static = "1.5.0" -jsonwebtoken = "9.3.0" +jsonwebtoken = { workspace = true } reqwest = { version = "0.12.8", features = ["json"] } bytes = "1.7.2" typed-builder = "0.20.0" @@ -39,7 +39,7 @@ tokio = { version = "1.42.0", features = ["macros", "time"] } rand = "0.8.5" [target.'cfg(target_arch = "wasm32")'.dependencies] -web-sys = { workspace = true, features = ["ConsoleLevel", "console"] } +web-sys = { workspace = true, features = ["console"] } serde-wasm-bindgen = { workspace = true } @@ -47,5 +47,5 @@ serde-wasm-bindgen = { workspace = true } # is used in testing test_utils = { workspace = true } rand = "0.8.5" -jsonwebkey = { version = "0.3.5", features = ["generate", "jwt-convert"] } +jsonwebkey = { workspace = true, features = ["generate", "jwt-convert"] } mockito = "1.5.0" diff --git a/jans-cedarling/cedarling/src/authz/request.rs b/jans-cedarling/cedarling/src/authz/request.rs index 4f5047cc069..59196e25c4d 100644 --- a/jans-cedarling/cedarling/src/authz/request.rs +++ b/jans-cedarling/cedarling/src/authz/request.rs @@ -9,7 +9,7 @@ use std::str::FromStr; use cedar_policy::{EntityId, EntityTypeName, EntityUid, ParseErrors}; /// Box to store authorization data -#[derive(Debug, Clone, serde::Deserialize)] +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub struct Request { /// Access token raw value pub access_token: Option, @@ -27,7 +27,7 @@ pub struct Request { /// Cedar policy resource data /// fields represent EntityUid -#[derive(serde::Deserialize, Debug, Clone)] +#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] pub struct ResourceData { /// entity type name #[serde(rename = "type")] diff --git a/jans-cedarling/cedarling/src/log/stdout_logger/wasm_logger.rs b/jans-cedarling/cedarling/src/log/stdout_logger/wasm_logger.rs index a06091ee91f..4b55a690ee4 100644 --- a/jans-cedarling/cedarling/src/log/stdout_logger/wasm_logger.rs +++ b/jans-cedarling/cedarling/src/log/stdout_logger/wasm_logger.rs @@ -9,10 +9,9 @@ use std::sync::{Arc, Mutex}; use crate::log::LogLevel; use crate::log::interface::{LogWriter, Loggable}; -use serde_wasm_bindgen::to_value; -use web_sys::js_sys::{Array, Object}; +use web_sys::console; +use web_sys::js_sys::Array; use web_sys::wasm_bindgen::JsValue; -use web_sys::{ConsoleLevel, console}; /// A logger that write to std output. pub(crate) struct StdOutLogger { @@ -36,7 +35,7 @@ impl LogWriter for StdOutLogger { let json_string = serde_json::json!(entry).to_string(); let js_string = JsValue::from(json_string); - let mut js_array = Array::new(); + let js_array = Array::new(); js_array.push(&js_string); match entry.get_log_level() { diff --git a/jans-cedarling/cedarling/src/tests/utils/mod.rs b/jans-cedarling/cedarling/src/tests/utils/mod.rs index b31c1d6c11a..31b5c16046f 100644 --- a/jans-cedarling/cedarling/src/tests/utils/mod.rs +++ b/jans-cedarling/cedarling/src/tests/utils/mod.rs @@ -10,6 +10,5 @@ pub use serde_json::json; pub use crate::{PolicyStoreSource, Request}; pub mod cedarling_util; -pub mod token_claims; pub use cedarling_util::{get_cedarling, get_cedarling_with_authorization_conf}; -pub use token_claims::generate_token_using_claims; +pub use test_utils::token_claims::generate_token_using_claims; diff --git a/jans-cedarling/test_utils/Cargo.toml b/jans-cedarling/test_utils/Cargo.toml index b47c9faae77..b03100d9fed 100644 --- a/jans-cedarling/test_utils/Cargo.toml +++ b/jans-cedarling/test_utils/Cargo.toml @@ -6,3 +6,6 @@ edition = "2021" [dependencies] pretty_assertions = "1" serde_json = { workspace = true } +jsonwebtoken = { workspace = true } +jsonwebkey = { workspace = true, features = ["generate", "jwt-convert"] } +serde = { workspace = true } diff --git a/jans-cedarling/test_utils/src/lib.rs b/jans-cedarling/test_utils/src/lib.rs index 175ba73d4e6..5b1b61320f8 100644 --- a/jans-cedarling/test_utils/src/lib.rs +++ b/jans-cedarling/test_utils/src/lib.rs @@ -6,6 +6,7 @@ */ mod sort_json; +pub mod token_claims; pub use pretty_assertions::*; pub use sort_json::SortedJson; diff --git a/jans-cedarling/cedarling/src/tests/utils/token_claims.rs b/jans-cedarling/test_utils/src/token_claims.rs similarity index 85% rename from jans-cedarling/cedarling/src/tests/utils/token_claims.rs rename to jans-cedarling/test_utils/src/token_claims.rs index 7529926f241..d89c8de7cf0 100644 --- a/jans-cedarling/cedarling/src/tests/utils/token_claims.rs +++ b/jans-cedarling/test_utils/src/token_claims.rs @@ -3,15 +3,16 @@ // // Copyright (c) 2024, Gluu, Inc. -use lazy_static::lazy_static; +//! Package for generating JWT tokens for testing purpose. + +use std::sync::LazyLock; + use {jsonwebkey as jwk, jsonwebtoken as jwt}; // Represent meta information about entity from cedar-policy schema. -lazy_static! { - pub(crate) static ref EncodingKeys: GeneratedKeys = generate_keys(); -} +static ENCODING_KEYS: LazyLock = LazyLock::new(generate_keys); -pub(crate) struct GeneratedKeys { +pub struct GeneratedKeys { pub private_key_id: String, pub private_encoding_key: jwt::EncodingKey, } @@ -19,7 +20,7 @@ pub(crate) struct GeneratedKeys { /// Generates a set of private and public keys using ES256 /// /// Returns a tuple: (Vec<(key_id, private_key)>, jwks) -pub fn generate_keys() -> GeneratedKeys { +fn generate_keys() -> GeneratedKeys { let kid = 1; // Generate a private key let mut jwk = jwk::JsonWebKey::new(jwk::Key::generate_p256()); @@ -50,8 +51,8 @@ pub fn generate_keys() -> GeneratedKeys { /// Generates a token string signed with ES256 pub fn generate_token_using_claims(claims: impl serde::Serialize) -> String { - let key_id = EncodingKeys.private_key_id.clone(); - let encoding_key = &EncodingKeys.private_encoding_key; + let key_id = ENCODING_KEYS.private_key_id.clone(); + let encoding_key = &ENCODING_KEYS.private_encoding_key; // select a key from the keyset // for simplicity, were just choosing the second one From 54f6291b644d22762049faed302030ec16c6f462 Mon Sep 17 00:00:00 2001 From: Oleh Bohzok Date: Thu, 2 Jan 2025 21:40:50 +0200 Subject: [PATCH 08/43] feat(jans-cedarling): add memory log to WASM API Signed-off-by: Oleh Bohzok --- .../bindings/cedarling_wasm/src/lib.rs | 35 +++++++++++++++++-- 1 file changed, 33 insertions(+), 2 deletions(-) diff --git a/jans-cedarling/bindings/cedarling_wasm/src/lib.rs b/jans-cedarling/bindings/cedarling_wasm/src/lib.rs index 6c57605f8a3..8fb2a28b2ec 100644 --- a/jans-cedarling/bindings/cedarling_wasm/src/lib.rs +++ b/jans-cedarling/bindings/cedarling_wasm/src/lib.rs @@ -6,11 +6,11 @@ // #![cfg(target_arch = "wasm32")] use cedarling::bindings::cedar_policy; -use cedarling::{BootstrapConfig, BootstrapConfigRaw, Request}; +use cedarling::{BootstrapConfig, BootstrapConfigRaw, LogStorage, Request}; use serde_wasm_bindgen::Error; use std::rc::Rc; use wasm_bindgen::prelude::*; -use wasm_bindgen_futures::js_sys::{Map, Object}; +use wasm_bindgen_futures::js_sys::{Array, Map, Object}; use wasm_bindgen_test::console_log; #[cfg(test)] @@ -83,6 +83,37 @@ impl Cedarling { .map_err(Error::new)?; Ok(result.into()) } + + /// return logs and remove them from the storage + pub fn pop_logs(&self) -> Result { + let result = Array::new(); + for log in self.instance.pop_logs() { + let js_log = serde_wasm_bindgen::to_value(&log)?; + result.push(&js_log); + } + Ok(result) + } + + /// get specific log entry + pub fn get_log_by_id(&self, id: &str) -> Result { + let result = if let Some(log_json_value) = self.instance.get_log_by_id(id) { + let js_log = serde_wasm_bindgen::to_value(&log_json_value)?; + js_log + } else { + JsValue::NULL + }; + Ok(result) + } + + /// returns a list of all log ids + pub fn get_log_ids(&self) -> Array { + let result = Array::new(); + for log_id in self.instance.get_log_ids() { + let js_id = log_id.into(); + result.push(&js_id); + } + result + } } /// A WASM wrapper for the Rust `cedarling::AuthorizeResult` struct. From 0a3f195af00d632165efbf38e3cfcc77b5816b09 Mon Sep 17 00:00:00 2001 From: Oleh Bohzok Date: Thu, 2 Jan 2025 21:40:50 +0200 Subject: [PATCH 09/43] chore(jans-cedarling): remove conditional compiling for WASM, it allows to catch some errors when made changes on different platforms Signed-off-by: Oleh Bohzok --- jans-cedarling/bindings/cedarling_wasm/src/lib.rs | 2 -- 1 file changed, 2 deletions(-) diff --git a/jans-cedarling/bindings/cedarling_wasm/src/lib.rs b/jans-cedarling/bindings/cedarling_wasm/src/lib.rs index 8fb2a28b2ec..18da1698910 100644 --- a/jans-cedarling/bindings/cedarling_wasm/src/lib.rs +++ b/jans-cedarling/bindings/cedarling_wasm/src/lib.rs @@ -3,8 +3,6 @@ // // Copyright (c) 2024, Gluu, Inc. -// #![cfg(target_arch = "wasm32")] - use cedarling::bindings::cedar_policy; use cedarling::{BootstrapConfig, BootstrapConfigRaw, LogStorage, Request}; use serde_wasm_bindgen::Error; From 1b39cc3a0e975319daf21032d78bc798db0221bc Mon Sep 17 00:00:00 2001 From: Oleh Bohzok Date: Thu, 2 Jan 2025 21:40:50 +0200 Subject: [PATCH 10/43] chore(jans-cedarling): improve doc message for memory log interface Signed-off-by: Oleh Bohzok --- jans-cedarling/bindings/cedarling_wasm/src/lib.rs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/jans-cedarling/bindings/cedarling_wasm/src/lib.rs b/jans-cedarling/bindings/cedarling_wasm/src/lib.rs index 18da1698910..2b3a67820c2 100644 --- a/jans-cedarling/bindings/cedarling_wasm/src/lib.rs +++ b/jans-cedarling/bindings/cedarling_wasm/src/lib.rs @@ -82,7 +82,8 @@ impl Cedarling { Ok(result.into()) } - /// return logs and remove them from the storage + /// Get logs and remove them from the storage. + /// Returns `Array` of `Map` pub fn pop_logs(&self) -> Result { let result = Array::new(); for log in self.instance.pop_logs() { @@ -92,7 +93,8 @@ impl Cedarling { Ok(result) } - /// get specific log entry + /// Get specific log entry. + /// Returns `Map` with values or `null`. pub fn get_log_by_id(&self, id: &str) -> Result { let result = if let Some(log_json_value) = self.instance.get_log_by_id(id) { let js_log = serde_wasm_bindgen::to_value(&log_json_value)?; @@ -103,7 +105,8 @@ impl Cedarling { Ok(result) } - /// returns a list of all log ids + /// Returns a list of all log ids. + /// Returns `Array` of `String` pub fn get_log_ids(&self) -> Array { let result = Array::new(); for log_id in self.instance.get_log_ids() { From dcce0b2a23a9626dc28a8f2467f0ee9322ff1ba8 Mon Sep 17 00:00:00 2001 From: Oleh Bohzok Date: Thu, 2 Jan 2025 21:40:50 +0200 Subject: [PATCH 11/43] test(jans-cedarling): add `test_memory_log_interface` Signed-off-by: Oleh Bohzok --- .../bindings/cedarling_wasm/src/tests.rs | 165 +++++++++++++++++- 1 file changed, 164 insertions(+), 1 deletion(-) diff --git a/jans-cedarling/bindings/cedarling_wasm/src/tests.rs b/jans-cedarling/bindings/cedarling_wasm/src/tests.rs index f4da2b86b66..4f21b4546e1 100644 --- a/jans-cedarling/bindings/cedarling_wasm/src/tests.rs +++ b/jans-cedarling/bindings/cedarling_wasm/src/tests.rs @@ -19,7 +19,7 @@ static POLICY_STORE_RAW_YAML: &str = include_str!("../../../bindings/cedarling_python/example_files/policy-store.json"); static BOOTSTRAP_CONFIG: LazyLock = LazyLock::new(|| { - serde_json::json!({ + json!({ "CEDARLING_APPLICATION_NAME": "My App", "CEDARLING_LOCAL_POLICY_STORE": POLICY_STORE_RAW_YAML, "CEDARLING_LOG_TYPE": "std_out", @@ -237,3 +237,166 @@ async fn test_run_cedarling() { assert!(result.decision, "decision should be allowed") } + +/// Test memory log interface. +/// In this scenario we check that memory log interface return some data +#[wasm_bindgen_test] +async fn test_memory_log_interface() { + let bootstrap_config_json = json!({ + "CEDARLING_APPLICATION_NAME": "My App", + "CEDARLING_LOCAL_POLICY_STORE": POLICY_STORE_RAW_YAML, + "CEDARLING_LOG_TYPE": "memory", + "CEDARLING_LOG_TTL": 120, + "CEDARLING_LOG_LEVEL": "INFO", + "CEDARLING_USER_AUTHZ": "enabled", + "CEDARLING_WORKLOAD_AUTHZ": "enabled", + "CEDARLING_USER_WORKLOAD_BOOLEAN_OPERATION": "AND", + "CEDARLING_ID_TOKEN_TRUST_MODE": "strict", + + }); + + let conf_map_js_value = serde_wasm_bindgen::to_value(&bootstrap_config_json) + .expect("serde json value should be converted to JsValue"); + + let conf_object = + Object::from_entries(&conf_map_js_value).expect("map value should be converted to object"); + + let instance = init(conf_object.into()) + .await + .expect("init function should be initialized with js map"); + + let request = Request { + access_token: Some(generate_token_using_claims(json!({ + "sub": "qzxn1Scrb9lWtGxVedMCky-Ql_ILspZaQA6fyuYktw0", + "code": "3e2a2012-099c-464f-890b-448160c2ab25", + "iss": "https://account.gluu.org", + "token_type": "Bearer", + "client_id": "d7f71bea-c38d-4caf-a1ba-e43c74a11a62", + "aud": "d7f71bea-c38d-4caf-a1ba-e43c74a11a62", + "acr": "simple_password_auth", + "x5t#S256": "", + "nbf": 1731953030, + "scope": [ + "role", + "openid", + "profile", + "email" + ], + "auth_time": 1731953027, + "exp": 1732121460, + "iat": 1731953030, + "jti": "uZUh1hDUQo6PFkBPnwpGzg", + "username": "Default Admin User", + "status": { + "status_list": { + "idx": 306, + "uri": "https://jans.test/jans-auth/restv1/status_list" + } + } + }))), + id_token: Some(generate_token_using_claims(json!({ + "at_hash": "bxaCT0ZQXbv4sbzjSDrNiA", + "sub": "qzxn1Scrb9lWtGxVedMCky-Ql_ILspZaQA6fyuYktw0", + "amr": [], + "iss": "https://account.gluu.org", + "nonce": "25b2b16b-32a2-42d6-8a8e-e5fa9ab888c0", + "sid": "6d443734-b7a2-4ed8-9d3a-1606d2f99244", + "jansOpenIDConnectVersion": "openidconnect-1.0", + "aud": "d7f71bea-c38d-4caf-a1ba-e43c74a11a62", + "acr": "simple_password_auth", + "c_hash": "V8h4sO9NzuLKawPO-3DNLA", + "nbf": 1731953030, + "auth_time": 1731953027, + "exp": 1731956630, + "grant": "authorization_code", + "iat": 1731953030, + "jti": "ijLZO1ooRyWrgIn7cIdNyA", + "status": { + "status_list": { + "idx": 307, + "uri": "https://jans.test/jans-auth/restv1/status_list" + } + } + }))), + userinfo_token: Some(generate_token_using_claims(json!({ + "sub": "qzxn1Scrb9lWtGxVedMCky-Ql_ILspZaQA6fyuYktw0", + "email_verified": true, + "role": [ + "CasaAdmin" + ], + "iss": "https://account.gluu.org", + "given_name": "Admin", + "middle_name": "Admin", + "inum": "a6a70301-af49-4901-9687-0bcdcf4e34fa", + "client_id": "d7f71bea-c38d-4caf-a1ba-e43c74a11a62", + "aud": "d7f71bea-c38d-4caf-a1ba-e43c74a11a62", + "updated_at": 1731698135, + "name": "Default Admin User", + "nickname": "Admin", + "family_name": "User", + "jti": "OIn3g1SPSDSKAYDzENVoug", + "email": "admin@jans.test", + "jansAdminUIRole": [ + "api-admin" + ] + }))), + context: json!({ + "current_time": 1735349685, // unix time + "device_health": ["Healthy"], + "fraud_indicators": ["Allowed"], + "geolocation": ["America"], + "network": "127.0.0.1", + "network_type": "Local", + "operating_system": "Linux", + "user_agent": "Linux" + }), + action: "Jans::Action::\"Read\"".to_string(), + resource: ResourceData::deserialize(json!({ + "type": "Jans::Application", + "id": "some_id", + "app_id": "application_id", + "name": "Some Application", + "url": { + "host": "jans.test", + "path": "/protected-endpoint", + "protocol": "http" + } + })) + .expect("ResourceData should be deserialized correctly"), + }; + + let js_request = + serde_wasm_bindgen::to_value(&request).expect("Request should be converted to JsObject"); + + let _result = instance + .authorize(js_request) + .await + .expect("authorize request should be executed"); + + let js_log_ids = instance.get_log_ids(); + let logs_count = js_log_ids.length(); + + for js_log_id in js_log_ids { + let log_id_str = js_log_id.as_string().expect("js_log_id should be string"); + + let log_val = instance + .get_log_by_id(log_id_str.as_str()) + .expect("get_log_by_id should not throw error"); + + assert_ne!(log_val, JsValue::NULL, "log result should be not null") + } + + let pop_logs_result = instance.pop_logs().expect("pop_logs not throw error"); + assert_eq!( + logs_count, + pop_logs_result.length(), + "length of ids and logs should be the same" + ); + + let pop_logs_result2 = instance.pop_logs().expect("pop_logs not throw error"); + assert_eq!( + pop_logs_result2.length(), + 0, + "logs should be removed from storage, storage should be empty" + ); +} From 6d0761b51090f628a326876e37302ffeac072201 Mon Sep 17 00:00:00 2001 From: Oleh Bohzok Date: Thu, 2 Jan 2025 21:40:50 +0200 Subject: [PATCH 12/43] feat(jans-cedarling): implement html page with cedarling usage Signed-off-by: Oleh Bohzok --- .../bindings/cedarling_wasm/index.html | 566 ++++++++++++------ .../bindings/cedarling_wasm/src/lib.rs | 50 +- 2 files changed, 439 insertions(+), 177 deletions(-) diff --git a/jans-cedarling/bindings/cedarling_wasm/index.html b/jans-cedarling/bindings/cedarling_wasm/index.html index 246eeae9de3..812379d21b0 100644 --- a/jans-cedarling/bindings/cedarling_wasm/index.html +++ b/jans-cedarling/bindings/cedarling_wasm/index.html @@ -2,193 +2,411 @@ - - hello-wasm example + + + + Hello world cedarling WASM example + + + + + + + - + + + + + + + + \ No newline at end of file diff --git a/jans-cedarling/bindings/cedarling_wasm/src/lib.rs b/jans-cedarling/bindings/cedarling_wasm/src/lib.rs index 2b3a67820c2..474a97a1bd6 100644 --- a/jans-cedarling/bindings/cedarling_wasm/src/lib.rs +++ b/jans-cedarling/bindings/cedarling_wasm/src/lib.rs @@ -5,11 +5,12 @@ use cedarling::bindings::cedar_policy; use cedarling::{BootstrapConfig, BootstrapConfigRaw, LogStorage, Request}; +use serde::ser::{Serialize, SerializeStruct, Serializer}; +use serde_json::json; use serde_wasm_bindgen::Error; use std::rc::Rc; use wasm_bindgen::prelude::*; use wasm_bindgen_futures::js_sys::{Array, Map, Object}; -use wasm_bindgen_test::console_log; #[cfg(test)] mod tests; @@ -70,9 +71,7 @@ impl Cedarling { request }; - console_log!("request_object: {:?}", request_object); let cedar_request: Request = serde_wasm_bindgen::from_value(request_object)?; - console_log!("cedar_request: {:?}", cedar_request); let result = self .instance @@ -120,6 +119,7 @@ impl Cedarling { /// A WASM wrapper for the Rust `cedarling::AuthorizeResult` struct. /// Represents the result of an authorization request. #[wasm_bindgen] +#[derive(serde::Serialize)] pub struct AuthorizeResult { /// Result of authorization where principal is `Jans::Workload` #[wasm_bindgen(getter_with_clone)] @@ -136,6 +136,14 @@ pub struct AuthorizeResult { pub decision: bool, } +#[wasm_bindgen] +impl AuthorizeResult { + /// Convert `AuthorizeResult` to json string value + pub fn json_string(&self) -> String { + json!(self).to_string() + } +} + impl From for AuthorizeResult { fn from(value: cedarling::AuthorizeResult) -> Self { Self { @@ -176,6 +184,18 @@ impl AuthorizeResultResponse { } } +impl Serialize for AuthorizeResultResponse { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + let mut state = serializer.serialize_struct("Diagnostics", 2)?; + state.serialize_field("decision", &self.decision())?; + state.serialize_field("diagnostics", &self.diagnostics())?; + state.end() + } +} + /// Diagnostics /// =========== /// @@ -212,6 +232,18 @@ impl Diagnostics { } } +impl Serialize for Diagnostics { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + let mut state = serializer.serialize_struct("Diagnostics", 2)?; + state.serialize_field("reason", &self.reason())?; + state.serialize_field("errors", &self.errors())?; + state.end() + } +} + /// PolicyEvaluationError /// ===================== /// @@ -235,3 +267,15 @@ impl PolicyEvaluationError { self.inner.error.clone() } } + +impl Serialize for PolicyEvaluationError { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + let mut state = serializer.serialize_struct("PolicyEvaluationError", 2)?; + state.serialize_field("id", &self.id())?; + state.serialize_field("error", &self.error())?; + state.end() + } +} From d7a04f817e73f9ed84980c300de261435c27eab1 Mon Sep 17 00:00:00 2001 From: Oleh Bohzok Date: Thu, 2 Jan 2025 21:40:51 +0200 Subject: [PATCH 13/43] chore(jans-cedarling): remove `println` Signed-off-by: Oleh Bohzok --- .../cedarling/src/common/cedar_schema/cedar_json/action.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/jans-cedarling/cedarling/src/common/cedar_schema/cedar_json/action.rs b/jans-cedarling/cedarling/src/common/cedar_schema/cedar_json/action.rs index a7442669ff1..efc98a2ad7e 100644 --- a/jans-cedarling/cedarling/src/common/cedar_schema/cedar_json/action.rs +++ b/jans-cedarling/cedarling/src/common/cedar_schema/cedar_json/action.rs @@ -60,7 +60,6 @@ impl Action<'_> { if let Some(ctx_entities) = &self.context_entities { for attr in ctx_entities.iter() { - println!("attr: {:?}", attr); if let CedarType::TypeName(type_name) = &attr.kind { let id = match id_mapping.get(&attr.key) { Some(val) => val, From 8ee1915ac2aada36401ebd8ab7bf7a6e806aab40 Mon Sep 17 00:00:00 2001 From: Oleh Bohzok Date: Thu, 2 Jan 2025 21:40:51 +0200 Subject: [PATCH 14/43] chore(jans-cedarling): fix clippy issue Signed-off-by: Oleh Bohzok --- jans-cedarling/bindings/cedarling_wasm/src/lib.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/jans-cedarling/bindings/cedarling_wasm/src/lib.rs b/jans-cedarling/bindings/cedarling_wasm/src/lib.rs index 474a97a1bd6..cf55df1422d 100644 --- a/jans-cedarling/bindings/cedarling_wasm/src/lib.rs +++ b/jans-cedarling/bindings/cedarling_wasm/src/lib.rs @@ -96,8 +96,7 @@ impl Cedarling { /// Returns `Map` with values or `null`. pub fn get_log_by_id(&self, id: &str) -> Result { let result = if let Some(log_json_value) = self.instance.get_log_by_id(id) { - let js_log = serde_wasm_bindgen::to_value(&log_json_value)?; - js_log + serde_wasm_bindgen::to_value(&log_json_value)? } else { JsValue::NULL }; From 328da64e1d5e772cae7302d59bf82f088b241c07 Mon Sep 17 00:00:00 2001 From: Oleh Bohzok Date: Thu, 2 Jan 2025 21:40:51 +0200 Subject: [PATCH 15/43] chore(jans-cedarling): add to `Cargo.toml` license and description Signed-off-by: Oleh Bohzok --- jans-cedarling/bindings/cedarling_python/Cargo.toml | 2 ++ jans-cedarling/bindings/cedarling_wasm/Cargo.toml | 2 ++ jans-cedarling/cedarling/Cargo.toml | 2 ++ 3 files changed, 6 insertions(+) diff --git a/jans-cedarling/bindings/cedarling_python/Cargo.toml b/jans-cedarling/bindings/cedarling_python/Cargo.toml index 4424b1d2527..b28c005cdba 100644 --- a/jans-cedarling/bindings/cedarling_python/Cargo.toml +++ b/jans-cedarling/bindings/cedarling_python/Cargo.toml @@ -2,6 +2,8 @@ name = "cedarling_python" version = "0.0.0" edition = "2021" +description = "Python binding to Cedarling" +license = "Apache-2.0" [lib] name = "cedarling_python" diff --git a/jans-cedarling/bindings/cedarling_wasm/Cargo.toml b/jans-cedarling/bindings/cedarling_wasm/Cargo.toml index f6f4da727d4..9749bcf08e8 100644 --- a/jans-cedarling/bindings/cedarling_wasm/Cargo.toml +++ b/jans-cedarling/bindings/cedarling_wasm/Cargo.toml @@ -2,6 +2,8 @@ name = "cedarling_wasm" version = "0.0.0" edition = "2021" +description = "The Cedarling is a performant local authorization service that runs the Rust Cedar Engine" +license = "Apache-2.0" [lib] crate-type = ["cdylib"] # Required for WASM output diff --git a/jans-cedarling/cedarling/Cargo.toml b/jans-cedarling/cedarling/Cargo.toml index 6a9b4789186..1217fd888c1 100644 --- a/jans-cedarling/cedarling/Cargo.toml +++ b/jans-cedarling/cedarling/Cargo.toml @@ -2,6 +2,8 @@ name = "cedarling" version = "0.0.0-nightly" edition = "2021" +description = "WASM binding to Cedarling" +license = "Apache-2.0" [features] # blocking with default is used only for local testing, next line should be commented From 03ee0a5e07dfc884fd939b0a01b91b2ac26420f9 Mon Sep 17 00:00:00 2001 From: Oleh Bohzok Date: Thu, 2 Jan 2025 21:59:34 +0200 Subject: [PATCH 16/43] test(jans-cedarling): add run WASM tests on CI Signed-off-by: Oleh Bohzok --- .github/workflows/test-cedarling.yml | 26 +++++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/.github/workflows/test-cedarling.yml b/.github/workflows/test-cedarling.yml index 6647eba00c0..ada540b2174 100644 --- a/.github/workflows/test-cedarling.yml +++ b/.github/workflows/test-cedarling.yml @@ -24,10 +24,34 @@ jobs: run: | cd ./jans-cedarling cargo test --workspace - - name: Run Clippy + - name: Run Clippy on native target run: | cd ./jans-cedarling cargo clippy -- -Dwarnings + wasm_tests: + runs-on: ubuntu-latest + steps: + - name: Harden Runner + uses: step-security/harden-runner@a4aa98b93cab29d9b1101a6143fb8bce00e2eac4 # v2.7.1 + with: + egress-policy: audit + - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + - name: Install Rust + uses: dtolnay/rust-toolchain@stable + - name: Install WASM dependencies + run: curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh + - name: Run WASM tests using chrome + run: | + cd ./jans-cedarling/bindings/cedarling_wasm + wasm-pack test --headless --chrome + - name: Run WASM tests using firefox + run: | + cd ./jans-cedarling/bindings/cedarling_wasm + wasm-pack test --headless --firefox + - name: Run Clippy on WASM target + run: | + cd ./jans-cedarling + cargo clippy --target wasm32-unknown-unknown -- -Dwarnings python_tests: runs-on: ubuntu-latest strategy: From 9e5199e5a421438133f9740e7c36de9c1f0b3788 Mon Sep 17 00:00:00 2001 From: Oleh Bohzok Date: Fri, 3 Jan 2025 01:16:55 +0200 Subject: [PATCH 17/43] chore(jans-cedarling): remove `dynamic = ["version"]` from `pyproject.toml` because it cause fail CI check Signed-off-by: Oleh Bohzok --- jans-cedarling/bindings/cedarling_python/pyproject.toml | 1 - 1 file changed, 1 deletion(-) diff --git a/jans-cedarling/bindings/cedarling_python/pyproject.toml b/jans-cedarling/bindings/cedarling_python/pyproject.toml index 9501c45d37c..d5870170671 100644 --- a/jans-cedarling/bindings/cedarling_python/pyproject.toml +++ b/jans-cedarling/bindings/cedarling_python/pyproject.toml @@ -11,7 +11,6 @@ classifiers = [ "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: PyPy", ] -dynamic = ["version"] [tool.maturin] features = ["pyo3/extension-module"] From f7968fb2c0bdb2bd5b29b2c15aad9d370a6c6988 Mon Sep 17 00:00:00 2001 From: Oleh Bohzok Date: Fri, 3 Jan 2025 01:49:28 +0200 Subject: [PATCH 18/43] chore(jans-cedarling): remove unused WASM code Signed-off-by: Oleh Bohzok --- .../src/log/stdout_logger/wasm_logger.rs | 77 ------------------- 1 file changed, 77 deletions(-) diff --git a/jans-cedarling/cedarling/src/log/stdout_logger/wasm_logger.rs b/jans-cedarling/cedarling/src/log/stdout_logger/wasm_logger.rs index 4b55a690ee4..7d526b08129 100644 --- a/jans-cedarling/cedarling/src/log/stdout_logger/wasm_logger.rs +++ b/jans-cedarling/cedarling/src/log/stdout_logger/wasm_logger.rs @@ -62,80 +62,3 @@ impl LogWriter for StdOutLogger { } } } - -// Test writer created for mocking LogWriter -#[allow(dead_code)] -#[derive(Clone)] -pub(crate) struct TestWriter { - buf: Arc>>, -} - -#[allow(dead_code)] -impl TestWriter { - pub(crate) fn new() -> Self { - Self { - buf: Arc::new(Mutex::new(Vec::new())), - } - } - - pub(crate) fn into_inner_buf(self) -> String { - let buf = self.buf.lock().unwrap(); - String::from_utf8_lossy(buf.as_slice()).into_owned() - } -} - -impl Write for TestWriter { - fn write(&mut self, buf: &[u8]) -> std::io::Result { - self.buf.lock().unwrap().extend_from_slice(buf); - Ok(buf.len()) - } - - fn flush(&mut self) -> std::io::Result<()> { - Ok(()) - } -} - -#[cfg(test)] -mod tests { - use std::io::Write; - - use super::*; - use crate::common::app_types::PdpID; - use crate::log::{LogEntry, LogType}; - - #[test] - fn write_log_ok() { - // Create a log entry - let log_entry = LogEntry { - base: crate::log::BaseLogEntry::new(PdpID::new(), LogType::Decision), - application_id: Some("test_app".to_string().into()), - auth_info: None, - msg: "Test message".to_string(), - error_msg: None, - cedar_lang_version: None, - cedar_sdk_version: None, - }; - - // Serialize the log entry to JSON - let json_str = serde_json::json!(&log_entry).to_string(); - - // Create a test writer - let mut test_writer = TestWriter::new(); - let buffer = Box::new(test_writer.clone()) as Box; - - // Create logger with test writer - let logger = StdOutLogger::new_with(buffer, LogLevel::TRACE); - - // Log the entry - logger.log(log_entry); - - // call flush just to get great coverage - _ = test_writer.flush(); - - // Check logged content - let logged_content = test_writer.into_inner_buf(); - - // Verify that the log entry was logged correctly - assert_eq!(logged_content, json_str + "\n"); - } -} From 09e05820b2548e01e3d69f3495ffd825b8e753de Mon Sep 17 00:00:00 2001 From: Oleh Bohzok Date: Fri, 3 Jan 2025 03:10:05 +0200 Subject: [PATCH 19/43] chore(jans-cedarling): fix clippy issues Signed-off-by: Oleh Bohzok --- jans-cedarling/cedarling/Cargo.toml | 3 +-- jans-cedarling/cedarling/src/log/stdout_logger/wasm_logger.rs | 3 --- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/jans-cedarling/cedarling/Cargo.toml b/jans-cedarling/cedarling/Cargo.toml index 1217fd888c1..9cee2df9376 100644 --- a/jans-cedarling/cedarling/Cargo.toml +++ b/jans-cedarling/cedarling/Cargo.toml @@ -14,7 +14,7 @@ blocking = ["tokio/rt-multi-thread"] [dependencies] serde = { workspace = true } -serde_json = { workspace = true } +serde_json = { workspace = true, features = ["raw_value"] } serde_yml = "0.0.12" thiserror = { workspace = true } sparkv = { workspace = true } @@ -42,7 +42,6 @@ rand = "0.8.5" [target.'cfg(target_arch = "wasm32")'.dependencies] web-sys = { workspace = true, features = ["console"] } -serde-wasm-bindgen = { workspace = true } [dev-dependencies] diff --git a/jans-cedarling/cedarling/src/log/stdout_logger/wasm_logger.rs b/jans-cedarling/cedarling/src/log/stdout_logger/wasm_logger.rs index 7d526b08129..542b51fb036 100644 --- a/jans-cedarling/cedarling/src/log/stdout_logger/wasm_logger.rs +++ b/jans-cedarling/cedarling/src/log/stdout_logger/wasm_logger.rs @@ -3,9 +3,6 @@ // // Copyright (c) 2024, Gluu, Inc. -use std::io::Write; -use std::sync::{Arc, Mutex}; - use crate::log::LogLevel; use crate::log::interface::{LogWriter, Loggable}; From 0b3ef5f8b06a375b7c6e08b0c5a8defcb8368859 Mon Sep 17 00:00:00 2001 From: Oleh Bohzok Date: Fri, 3 Jan 2025 03:16:06 +0200 Subject: [PATCH 20/43] docs(jans-cedarling): remove unnecessary part about optimization wasm. It is already optimized if used `--release` on build Signed-off-by: Oleh Bohzok --- jans-cedarling/bindings/cedarling_wasm/README.md | 8 -------- 1 file changed, 8 deletions(-) diff --git a/jans-cedarling/bindings/cedarling_wasm/README.md b/jans-cedarling/bindings/cedarling_wasm/README.md index d85679de3d8..7a36c132216 100644 --- a/jans-cedarling/bindings/cedarling_wasm/README.md +++ b/jans-cedarling/bindings/cedarling_wasm/README.md @@ -37,11 +37,3 @@ To run example using `index.html` you need execute following steps: 1. Build wasm cedarling. 2. Run webserver using `python3 -m http.server` or any other. 3. Visit [localhost](http://localhost:8000/). - -## Optimization wasm binary - -You can try to use `wasm-opt`, a C++ tool for optimize WebAssembly, you can make it even smaller too! `WASM` file holds in the `pkg` folder. - -```bash -wasm-opt -Os cedarling_wasm_bg.wasm -o cedarling_wasm.wasm -``` From 2d5652dba2ab0b9a03758f15fd6a7af0583419b4 Mon Sep 17 00:00:00 2001 From: Oleh Bohzok Date: Sat, 4 Jan 2025 00:33:11 +0200 Subject: [PATCH 21/43] test(jans-cedarling): fix unit tests Signed-off-by: Oleh Bohzok --- .../bindings/cedarling_wasm/src/tests.rs | 302 +++++++++--------- jans-cedarling/cedarling/src/authz/request.rs | 2 +- 2 files changed, 154 insertions(+), 150 deletions(-) diff --git a/jans-cedarling/bindings/cedarling_wasm/src/tests.rs b/jans-cedarling/bindings/cedarling_wasm/src/tests.rs index 4f21b4546e1..e309cd518a1 100644 --- a/jans-cedarling/bindings/cedarling_wasm/src/tests.rs +++ b/jans-cedarling/bindings/cedarling_wasm/src/tests.rs @@ -5,7 +5,7 @@ use std::sync::LazyLock; use crate::*; -use cedarling::ResourceData; +use cedarling::{ResourceData, Tokens}; use serde::Deserialize; use serde_json::json; use test_utils::token_claims::generate_token_using_claims; @@ -128,80 +128,82 @@ async fn test_run_cedarling() { .expect("init function should be initialized with js map"); let request = Request { - access_token: Some(generate_token_using_claims(json!({ - "sub": "qzxn1Scrb9lWtGxVedMCky-Ql_ILspZaQA6fyuYktw0", - "code": "3e2a2012-099c-464f-890b-448160c2ab25", - "iss": "https://account.gluu.org", - "token_type": "Bearer", - "client_id": "d7f71bea-c38d-4caf-a1ba-e43c74a11a62", - "aud": "d7f71bea-c38d-4caf-a1ba-e43c74a11a62", - "acr": "simple_password_auth", - "x5t#S256": "", - "nbf": 1731953030, - "scope": [ - "role", - "openid", - "profile", - "email" - ], - "auth_time": 1731953027, - "exp": 1732121460, - "iat": 1731953030, - "jti": "uZUh1hDUQo6PFkBPnwpGzg", - "username": "Default Admin User", - "status": { - "status_list": { - "idx": 306, - "uri": "https://jans.test/jans-auth/restv1/status_list" - } - } - }))), - id_token: Some(generate_token_using_claims(json!({ - "at_hash": "bxaCT0ZQXbv4sbzjSDrNiA", - "sub": "qzxn1Scrb9lWtGxVedMCky-Ql_ILspZaQA6fyuYktw0", - "amr": [], - "iss": "https://account.gluu.org", - "nonce": "25b2b16b-32a2-42d6-8a8e-e5fa9ab888c0", - "sid": "6d443734-b7a2-4ed8-9d3a-1606d2f99244", - "jansOpenIDConnectVersion": "openidconnect-1.0", - "aud": "d7f71bea-c38d-4caf-a1ba-e43c74a11a62", - "acr": "simple_password_auth", - "c_hash": "V8h4sO9NzuLKawPO-3DNLA", - "nbf": 1731953030, - "auth_time": 1731953027, - "exp": 1731956630, - "grant": "authorization_code", - "iat": 1731953030, - "jti": "ijLZO1ooRyWrgIn7cIdNyA", - "status": { - "status_list": { - "idx": 307, - "uri": "https://jans.test/jans-auth/restv1/status_list" - } - } - }))), - userinfo_token: Some(generate_token_using_claims(json!({ - "sub": "qzxn1Scrb9lWtGxVedMCky-Ql_ILspZaQA6fyuYktw0", - "email_verified": true, - "role": [ - "CasaAdmin" - ], - "iss": "https://account.gluu.org", - "given_name": "Admin", - "middle_name": "Admin", - "inum": "a6a70301-af49-4901-9687-0bcdcf4e34fa", - "client_id": "d7f71bea-c38d-4caf-a1ba-e43c74a11a62", - "aud": "d7f71bea-c38d-4caf-a1ba-e43c74a11a62", - "updated_at": 1731698135, - "name": "Default Admin User", - "nickname": "Admin", - "family_name": "User", - "jti": "OIn3g1SPSDSKAYDzENVoug", - "email": "admin@jans.test", - "jansAdminUIRole": [ - "api-admin" - ] - }))), + tokens: Tokens { + access_token: Some(generate_token_using_claims(json!({ + "sub": "qzxn1Scrb9lWtGxVedMCky-Ql_ILspZaQA6fyuYktw0", + "code": "3e2a2012-099c-464f-890b-448160c2ab25", + "iss": "https://account.gluu.org", + "token_type": "Bearer", + "client_id": "d7f71bea-c38d-4caf-a1ba-e43c74a11a62", + "aud": "d7f71bea-c38d-4caf-a1ba-e43c74a11a62", + "acr": "simple_password_auth", + "x5t#S256": "", + "nbf": 1731953030, + "scope": [ + "role", + "openid", + "profile", + "email" + ], + "auth_time": 1731953027, + "exp": 1732121460, + "iat": 1731953030, + "jti": "uZUh1hDUQo6PFkBPnwpGzg", + "username": "Default Admin User", + "status": { + "status_list": { + "idx": 306, + "uri": "https://jans.test/jans-auth/restv1/status_list" + } + } + }))), + id_token: Some(generate_token_using_claims(json!({ + "at_hash": "bxaCT0ZQXbv4sbzjSDrNiA", + "sub": "qzxn1Scrb9lWtGxVedMCky-Ql_ILspZaQA6fyuYktw0", + "amr": [], + "iss": "https://account.gluu.org", + "nonce": "25b2b16b-32a2-42d6-8a8e-e5fa9ab888c0", + "sid": "6d443734-b7a2-4ed8-9d3a-1606d2f99244", + "jansOpenIDConnectVersion": "openidconnect-1.0", + "aud": "d7f71bea-c38d-4caf-a1ba-e43c74a11a62", + "acr": "simple_password_auth", + "c_hash": "V8h4sO9NzuLKawPO-3DNLA", + "nbf": 1731953030, + "auth_time": 1731953027, + "exp": 1731956630, + "grant": "authorization_code", + "iat": 1731953030, + "jti": "ijLZO1ooRyWrgIn7cIdNyA", + "status": { + "status_list": { + "idx": 307, + "uri": "https://jans.test/jans-auth/restv1/status_list" + } + } + }))), + userinfo_token: Some(generate_token_using_claims(json!({ + "sub": "qzxn1Scrb9lWtGxVedMCky-Ql_ILspZaQA6fyuYktw0", + "email_verified": true, + "role": [ + "CasaAdmin" + ], + "iss": "https://account.gluu.org", + "given_name": "Admin", + "middle_name": "Admin", + "inum": "a6a70301-af49-4901-9687-0bcdcf4e34fa", + "client_id": "d7f71bea-c38d-4caf-a1ba-e43c74a11a62", + "aud": "d7f71bea-c38d-4caf-a1ba-e43c74a11a62", + "updated_at": 1731698135, + "name": "Default Admin User", + "nickname": "Admin", + "family_name": "User", + "jti": "OIn3g1SPSDSKAYDzENVoug", + "email": "admin@jans.test", + "jansAdminUIRole": [ + "api-admin" + ] + }))), + }, context: json!({ "current_time": 1735349685, // unix time "device_health": ["Healthy"], @@ -266,80 +268,82 @@ async fn test_memory_log_interface() { .expect("init function should be initialized with js map"); let request = Request { - access_token: Some(generate_token_using_claims(json!({ - "sub": "qzxn1Scrb9lWtGxVedMCky-Ql_ILspZaQA6fyuYktw0", - "code": "3e2a2012-099c-464f-890b-448160c2ab25", - "iss": "https://account.gluu.org", - "token_type": "Bearer", - "client_id": "d7f71bea-c38d-4caf-a1ba-e43c74a11a62", - "aud": "d7f71bea-c38d-4caf-a1ba-e43c74a11a62", - "acr": "simple_password_auth", - "x5t#S256": "", - "nbf": 1731953030, - "scope": [ - "role", - "openid", - "profile", - "email" - ], - "auth_time": 1731953027, - "exp": 1732121460, - "iat": 1731953030, - "jti": "uZUh1hDUQo6PFkBPnwpGzg", - "username": "Default Admin User", - "status": { - "status_list": { - "idx": 306, - "uri": "https://jans.test/jans-auth/restv1/status_list" - } - } - }))), - id_token: Some(generate_token_using_claims(json!({ - "at_hash": "bxaCT0ZQXbv4sbzjSDrNiA", - "sub": "qzxn1Scrb9lWtGxVedMCky-Ql_ILspZaQA6fyuYktw0", - "amr": [], - "iss": "https://account.gluu.org", - "nonce": "25b2b16b-32a2-42d6-8a8e-e5fa9ab888c0", - "sid": "6d443734-b7a2-4ed8-9d3a-1606d2f99244", - "jansOpenIDConnectVersion": "openidconnect-1.0", - "aud": "d7f71bea-c38d-4caf-a1ba-e43c74a11a62", - "acr": "simple_password_auth", - "c_hash": "V8h4sO9NzuLKawPO-3DNLA", - "nbf": 1731953030, - "auth_time": 1731953027, - "exp": 1731956630, - "grant": "authorization_code", - "iat": 1731953030, - "jti": "ijLZO1ooRyWrgIn7cIdNyA", - "status": { - "status_list": { - "idx": 307, - "uri": "https://jans.test/jans-auth/restv1/status_list" - } - } - }))), - userinfo_token: Some(generate_token_using_claims(json!({ - "sub": "qzxn1Scrb9lWtGxVedMCky-Ql_ILspZaQA6fyuYktw0", - "email_verified": true, - "role": [ - "CasaAdmin" - ], - "iss": "https://account.gluu.org", - "given_name": "Admin", - "middle_name": "Admin", - "inum": "a6a70301-af49-4901-9687-0bcdcf4e34fa", - "client_id": "d7f71bea-c38d-4caf-a1ba-e43c74a11a62", - "aud": "d7f71bea-c38d-4caf-a1ba-e43c74a11a62", - "updated_at": 1731698135, - "name": "Default Admin User", - "nickname": "Admin", - "family_name": "User", - "jti": "OIn3g1SPSDSKAYDzENVoug", - "email": "admin@jans.test", - "jansAdminUIRole": [ - "api-admin" - ] - }))), + tokens: Tokens { + access_token: Some(generate_token_using_claims(json!({ + "sub": "qzxn1Scrb9lWtGxVedMCky-Ql_ILspZaQA6fyuYktw0", + "code": "3e2a2012-099c-464f-890b-448160c2ab25", + "iss": "https://account.gluu.org", + "token_type": "Bearer", + "client_id": "d7f71bea-c38d-4caf-a1ba-e43c74a11a62", + "aud": "d7f71bea-c38d-4caf-a1ba-e43c74a11a62", + "acr": "simple_password_auth", + "x5t#S256": "", + "nbf": 1731953030, + "scope": [ + "role", + "openid", + "profile", + "email" + ], + "auth_time": 1731953027, + "exp": 1732121460, + "iat": 1731953030, + "jti": "uZUh1hDUQo6PFkBPnwpGzg", + "username": "Default Admin User", + "status": { + "status_list": { + "idx": 306, + "uri": "https://jans.test/jans-auth/restv1/status_list" + } + } + }))), + id_token: Some(generate_token_using_claims(json!({ + "at_hash": "bxaCT0ZQXbv4sbzjSDrNiA", + "sub": "qzxn1Scrb9lWtGxVedMCky-Ql_ILspZaQA6fyuYktw0", + "amr": [], + "iss": "https://account.gluu.org", + "nonce": "25b2b16b-32a2-42d6-8a8e-e5fa9ab888c0", + "sid": "6d443734-b7a2-4ed8-9d3a-1606d2f99244", + "jansOpenIDConnectVersion": "openidconnect-1.0", + "aud": "d7f71bea-c38d-4caf-a1ba-e43c74a11a62", + "acr": "simple_password_auth", + "c_hash": "V8h4sO9NzuLKawPO-3DNLA", + "nbf": 1731953030, + "auth_time": 1731953027, + "exp": 1731956630, + "grant": "authorization_code", + "iat": 1731953030, + "jti": "ijLZO1ooRyWrgIn7cIdNyA", + "status": { + "status_list": { + "idx": 307, + "uri": "https://jans.test/jans-auth/restv1/status_list" + } + } + }))), + userinfo_token: Some(generate_token_using_claims(json!({ + "sub": "qzxn1Scrb9lWtGxVedMCky-Ql_ILspZaQA6fyuYktw0", + "email_verified": true, + "role": [ + "CasaAdmin" + ], + "iss": "https://account.gluu.org", + "given_name": "Admin", + "middle_name": "Admin", + "inum": "a6a70301-af49-4901-9687-0bcdcf4e34fa", + "client_id": "d7f71bea-c38d-4caf-a1ba-e43c74a11a62", + "aud": "d7f71bea-c38d-4caf-a1ba-e43c74a11a62", + "updated_at": 1731698135, + "name": "Default Admin User", + "nickname": "Admin", + "family_name": "User", + "jti": "OIn3g1SPSDSKAYDzENVoug", + "email": "admin@jans.test", + "jansAdminUIRole": [ + "api-admin" + ] + }))), + }, context: json!({ "current_time": 1735349685, // unix time "device_health": ["Healthy"], diff --git a/jans-cedarling/cedarling/src/authz/request.rs b/jans-cedarling/cedarling/src/authz/request.rs index 9fe97bc8537..a5c80de20b9 100644 --- a/jans-cedarling/cedarling/src/authz/request.rs +++ b/jans-cedarling/cedarling/src/authz/request.rs @@ -22,7 +22,7 @@ pub struct Request { } /// Contains the JWTs that will be used for the AuthZ request -#[derive(Debug, Clone, serde::Deserialize)] +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub struct Tokens { /// Access token raw value #[serde(default)] From a06ee3b11001736b0b436ccd3004a25d8a2c285f Mon Sep 17 00:00:00 2001 From: Oleh Bohzok Date: Sat, 4 Jan 2025 00:54:19 +0200 Subject: [PATCH 22/43] chore(jans-cedarling): remove comment that enable blocking `cedarling` by default Signed-off-by: Oleh Bohzok --- jans-cedarling/cedarling/Cargo.toml | 3 --- 1 file changed, 3 deletions(-) diff --git a/jans-cedarling/cedarling/Cargo.toml b/jans-cedarling/cedarling/Cargo.toml index 9cee2df9376..bc22121693b 100644 --- a/jans-cedarling/cedarling/Cargo.toml +++ b/jans-cedarling/cedarling/Cargo.toml @@ -6,9 +6,6 @@ description = "WASM binding to Cedarling" license = "Apache-2.0" [features] -# blocking with default is used only for local testing, next line should be commented -# default = ["blocking"] - # blocking feature allows to use blocking cedarling client blocking = ["tokio/rt-multi-thread"] From 984094c784a2076bcbd58dedff0d9a9f0ceda51f Mon Sep 17 00:00:00 2001 From: Oleh Bohzok Date: Sat, 4 Jan 2025 00:55:40 +0200 Subject: [PATCH 23/43] chore(jans-cedarling): make comment with license more correct Signed-off-by: Oleh Bohzok --- jans-cedarling/cedarling/src/http/mod.rs | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/jans-cedarling/cedarling/src/http/mod.rs b/jans-cedarling/cedarling/src/http/mod.rs index 09ad396d7ef..374260cf54b 100644 --- a/jans-cedarling/cedarling/src/http/mod.rs +++ b/jans-cedarling/cedarling/src/http/mod.rs @@ -1,9 +1,7 @@ -/* -* 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. -cfg*/ +// 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 reqwest::Client; use serde::Deserialize; From 5a83f86cbe124378f1c1351cff17f8526d1f32cc Mon Sep 17 00:00:00 2001 From: Oleh Bohzok Date: Sat, 4 Jan 2025 01:04:31 +0200 Subject: [PATCH 24/43] chore(jans-cedarling): update description for `cedarling` package Signed-off-by: Oleh Bohzok --- jans-cedarling/cedarling/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jans-cedarling/cedarling/Cargo.toml b/jans-cedarling/cedarling/Cargo.toml index bc22121693b..689991b1919 100644 --- a/jans-cedarling/cedarling/Cargo.toml +++ b/jans-cedarling/cedarling/Cargo.toml @@ -2,7 +2,7 @@ name = "cedarling" version = "0.0.0-nightly" edition = "2021" -description = "WASM binding to Cedarling" +description = "The Cedarling: a high-performance local authorization service powered by the Rust Cedar Engine." license = "Apache-2.0" [features] From 939c5c41058840e5ff6862de90da3f72c5a6f23f Mon Sep 17 00:00:00 2001 From: Oleh Bohzok Date: Sat, 4 Jan 2025 01:13:14 +0200 Subject: [PATCH 25/43] chore(jans-cedarling): add `dynamic = ["version"]` to python bindings Signed-off-by: Oleh Bohzok --- jans-cedarling/bindings/cedarling_python/pyproject.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/jans-cedarling/bindings/cedarling_python/pyproject.toml b/jans-cedarling/bindings/cedarling_python/pyproject.toml index 6613ea1796c..e38a1cb33b4 100644 --- a/jans-cedarling/bindings/cedarling_python/pyproject.toml +++ b/jans-cedarling/bindings/cedarling_python/pyproject.toml @@ -10,6 +10,7 @@ classifiers = [ "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: PyPy", ] +dynamic = ["version"] [tool.maturin] features = ["pyo3/extension-module"] From 2c6380b91d5f8ab214ed5ba017a892f36a36043f Mon Sep 17 00:00:00 2001 From: Oleh Bohzok Date: Sat, 4 Jan 2025 01:31:55 +0200 Subject: [PATCH 26/43] chore(jans-cedarling): remove double comments Signed-off-by: Oleh Bohzok --- .../bindings/cedarling_wasm/index.html | 162 +++++++++--------- 1 file changed, 81 insertions(+), 81 deletions(-) diff --git a/jans-cedarling/bindings/cedarling_wasm/index.html b/jans-cedarling/bindings/cedarling_wasm/index.html index 812379d21b0..ecdb56585a2 100644 --- a/jans-cedarling/bindings/cedarling_wasm/index.html +++ b/jans-cedarling/bindings/cedarling_wasm/index.html @@ -181,91 +181,91 @@

Logs

}, null, 2); - // // Payload of access_token: - // // { - // // "sub": "qzxn1Scrb9lWtGxVedMCky-Ql_ILspZaQA6fyuYktw0", - // // "code": "3e2a2012-099c-464f-890b-448160c2ab25", - // // "iss": "https://account.gluu.org", - // // "token_type": "Bearer", - // // "client_id": "d7f71bea-c38d-4caf-a1ba-e43c74a11a62", - // // "aud": "d7f71bea-c38d-4caf-a1ba-e43c74a11a62", - // // "acr": "simple_password_auth", - // // "x5t#S256": "", - // // "nbf": 1731953030, - // // "scope": [ - // // "role", - // // "openid", - // // "profile", - // // "email" - // // ], - // // "auth_time": 1731953027, - // // "exp": 1732121460, - // // "iat": 1731953030, - // // "jti": "uZUh1hDUQo6PFkBPnwpGzg", - // // "username": "Default Admin User", - // // "status": { - // // "status_list": { - // // "idx": 306, - // // "uri": "https://jans.test/jans-auth/restv1/status_list" - // // } - // // } - // // } + // Payload of access_token: + // { + // "sub": "qzxn1Scrb9lWtGxVedMCky-Ql_ILspZaQA6fyuYktw0", + // "code": "3e2a2012-099c-464f-890b-448160c2ab25", + // "iss": "https://account.gluu.org", + // "token_type": "Bearer", + // "client_id": "d7f71bea-c38d-4caf-a1ba-e43c74a11a62", + // "aud": "d7f71bea-c38d-4caf-a1ba-e43c74a11a62", + // "acr": "simple_password_auth", + // "x5t#S256": "", + // "nbf": 1731953030, + // "scope": [ + // "role", + // "openid", + // "profile", + // "email" + // ], + // "auth_time": 1731953027, + // "exp": 1732121460, + // "iat": 1731953030, + // "jti": "uZUh1hDUQo6PFkBPnwpGzg", + // "username": "Default Admin User", + // "status": { + // "status_list": { + // "idx": 306, + // "uri": "https://jans.test/jans-auth/restv1/status_list" + // } + // } + // } let ACCESS_TOKEN = "eyJraWQiOiJjb25uZWN0X2Y5YTAwN2EyLTZkMGItNDkyYS05MGNkLWYwYzliMWMyYjVkYl9zaWdfcnMyNTYiLCJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJzdWIiOiJxenhuMVNjcmI5bFd0R3hWZWRNQ2t5LVFsX0lMc3BaYVFBNmZ5dVlrdHcwIiwiY29kZSI6IjNlMmEyMDEyLTA5OWMtNDY0Zi04OTBiLTQ0ODE2MGMyYWIyNSIsImlzcyI6Imh0dHBzOi8vYWNjb3VudC5nbHV1Lm9yZyIsInRva2VuX3R5cGUiOiJCZWFyZXIiLCJjbGllbnRfaWQiOiJkN2Y3MWJlYS1jMzhkLTRjYWYtYTFiYS1lNDNjNzRhMTFhNjIiLCJhdWQiOiJkN2Y3MWJlYS1jMzhkLTRjYWYtYTFiYS1lNDNjNzRhMTFhNjIiLCJhY3IiOiJzaW1wbGVfcGFzc3dvcmRfYXV0aCIsIng1dCNTMjU2IjoiIiwibmJmIjoxNzMxOTUzMDMwLCJzY29wZSI6WyJyb2xlIiwib3BlbmlkIiwicHJvZmlsZSIsImVtYWlsIl0sImF1dGhfdGltZSI6MTczMTk1MzAyNywiZXhwIjoxNzMyMTIxNDYwLCJpYXQiOjE3MzE5NTMwMzAsImp0aSI6InVaVWgxaERVUW82UEZrQlBud3BHemciLCJ1c2VybmFtZSI6IkRlZmF1bHQgQWRtaW4gVXNlciIsInN0YXR1cyI6eyJzdGF0dXNfbGlzdCI6eyJpZHgiOjMwNiwidXJpIjoiaHR0cHM6Ly9qYW5zLnRlc3QvamFucy1hdXRoL3Jlc3R2MS9zdGF0dXNfbGlzdCJ9fX0.Pt-Y7F-hfde_WP7ZYwyvvSS11rKYQWGZXTzjH_aJKC5VPxzOjAXqI3Igr6gJLsP1aOd9WJvOPchflZYArctopXMWClbX_TxpmADqyCMsz78r4P450TaMKj-WKEa9cL5KtgnFa0fmhZ1ZWolkDTQ_M00Xr4EIvv4zf-92Wu5fOrdjmsIGFot0jt-12WxQlJFfs5qVZ9P-cDjxvQSrO1wbyKfHQ_txkl1GDATXsw5SIpC5wct92vjAVm5CJNuv_PE8dHAY-KfPTxOuDYBuWI5uA2Yjd1WUFyicbJgcmYzUSVt03xZ0kQX9dxKExwU2YnpDorfwebaAPO7G114Bkw208g"; - // // Payload of id_token: - // // { - // // "sub": "qzxn1Scrb9lWtGxVedMCky-Ql_ILspZaQA6fyuYktw0", - // // "code": "3e2a2012-099c-464f-890b-448160c2ab25", - // // "iss": "https://account.gluu.org", - // // "token_type": "Bearer", - // // "client_id": "d7f71bea-c38d-4caf-a1ba-e43c74a11a62", - // // "aud": "d7f71bea-c38d-4caf-a1ba-e43c74a11a62", - // // "acr": "simple_password_auth", - // // "x5t#S256": "", - // // "nbf": 1731953030, - // // "scope": [ - // // "role", - // // "openid", - // // "profile", - // // "email" - // // ], - // // "auth_time": 1731953027, - // // "exp": 1732121460, - // // "iat": 1731953030, - // // "jti": "uZUh1hDUQo6PFkBPnwpGzg", - // // "username": "Default Admin User", - // // "status": { - // // "status_list": { - // // "idx": 306, - // // "uri": "https://jans.test/jans-auth/restv1/status_list" - // // } - // // } - // // } + // Payload of id_token: + // { + // "sub": "qzxn1Scrb9lWtGxVedMCky-Ql_ILspZaQA6fyuYktw0", + // "code": "3e2a2012-099c-464f-890b-448160c2ab25", + // "iss": "https://account.gluu.org", + // "token_type": "Bearer", + // "client_id": "d7f71bea-c38d-4caf-a1ba-e43c74a11a62", + // "aud": "d7f71bea-c38d-4caf-a1ba-e43c74a11a62", + // "acr": "simple_password_auth", + // "x5t#S256": "", + // "nbf": 1731953030, + // "scope": [ + // "role", + // "openid", + // "profile", + // "email" + // ], + // "auth_time": 1731953027, + // "exp": 1732121460, + // "iat": 1731953030, + // "jti": "uZUh1hDUQo6PFkBPnwpGzg", + // "username": "Default Admin User", + // "status": { + // "status_list": { + // "idx": 306, + // "uri": "https://jans.test/jans-auth/restv1/status_list" + // } + // } + // } let ID_TOKEN = "eyJraWQiOiJjb25uZWN0X2Y5YTAwN2EyLTZkMGItNDkyYS05MGNkLWYwYzliMWMyYjVkYl9zaWdfcnMyNTYiLCJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJhdF9oYXNoIjoiYnhhQ1QwWlFYYnY0c2J6alNEck5pQSIsInN1YiI6InF6eG4xU2NyYjlsV3RHeFZlZE1Da3ktUWxfSUxzcFphUUE2Znl1WWt0dzAiLCJhbXIiOltdLCJpc3MiOiJodHRwczovL2FjY291bnQuZ2x1dS5vcmciLCJub25jZSI6IjI1YjJiMTZiLTMyYTItNDJkNi04YThlLWU1ZmE5YWI4ODhjMCIsInNpZCI6IjZkNDQzNzM0LWI3YTItNGVkOC05ZDNhLTE2MDZkMmY5OTI0NCIsImphbnNPcGVuSURDb25uZWN0VmVyc2lvbiI6Im9wZW5pZGNvbm5lY3QtMS4wIiwiYXVkIjoiZDdmNzFiZWEtYzM4ZC00Y2FmLWExYmEtZTQzYzc0YTExYTYyIiwiYWNyIjoic2ltcGxlX3Bhc3N3b3JkX2F1dGgiLCJjX2hhc2giOiJWOGg0c085Tnp1TEthd1BPLTNETkxBIiwibmJmIjoxNzMxOTUzMDMwLCJhdXRoX3RpbWUiOjE3MzE5NTMwMjcsImV4cCI6MTczMTk1NjYzMCwiZ3JhbnQiOiJhdXRob3JpemF0aW9uX2NvZGUiLCJpYXQiOjE3MzE5NTMwMzAsImp0aSI6ImlqTFpPMW9vUnlXcmdJbjdjSWROeUEiLCJzdGF0dXMiOnsic3RhdHVzX2xpc3QiOnsiaWR4IjozMDcsInVyaSI6Imh0dHBzOi8vamFucy50ZXN0L2phbnMtYXV0aC9yZXN0djEvc3RhdHVzX2xpc3QifX19.Nw7MRaJ5LtDak_LdEjrICgVOxDwd1p1I8WxD7IYw0_mKlIJ-J_78rGPski9p3L5ZNCpXiHtVbnhc4lJdmbh-y6mrD3_EY_AmjK50xpuf6YuUuNVtFENCSkj_irPLkIDG65HeZherWsvH0hUn4FVGv8Sw9fjny9Doi-HGHnKg9Qvphqre1U8hCphCVLQlzXAXmBkbPOC8tDwId5yigBKXP50cdqDcT-bjXf9leIdGgq0jxb57kYaFSElprLN9nUygM4RNCn9mtmo1l4IsdTlvvUb3OMAMQkRLfMkiKBjjeSF3819mYRLb3AUBaFH16ZdHFBzTSB6oA22TYpUqOLihMg"; - // // Payload of userinfo_token: - // // { - // // "sub": "qzxn1Scrb9lWtGxVedMCky-Ql_ILspZaQA6fyuYktw0", - // // "email_verified": true, - // // "role": [ - // // "CasaAdmin" - // // ], - // // "iss": "https://account.gluu.org", - // // "given_name": "Admin", - // // "middle_name": "Admin", - // // "inum": "a6a70301-af49-4901-9687-0bcdcf4e34fa", - // // "client_id": "d7f71bea-c38d-4caf-a1ba-e43c74a11a62", - // // "aud": "d7f71bea-c38d-4caf-a1ba-e43c74a11a62", - // // "updated_at": 1731698135, - // // "name": "Default Admin User", - // // "nickname": "Admin", - // // "family_name": "User", - // // "jti": "OIn3g1SPSDSKAYDzENVoug", - // // "email": "admin@jans.test", - // // "jansAdminUIRole": [ - // // "api-admin" - // // ] - // // } + // Payload of userinfo_token: + // { + // "sub": "qzxn1Scrb9lWtGxVedMCky-Ql_ILspZaQA6fyuYktw0", + // "email_verified": true, + // "role": [ + // "CasaAdmin" + // ], + // "iss": "https://account.gluu.org", + // "given_name": "Admin", + // "middle_name": "Admin", + // "inum": "a6a70301-af49-4901-9687-0bcdcf4e34fa", + // "client_id": "d7f71bea-c38d-4caf-a1ba-e43c74a11a62", + // "aud": "d7f71bea-c38d-4caf-a1ba-e43c74a11a62", + // "updated_at": 1731698135, + // "name": "Default Admin User", + // "nickname": "Admin", + // "family_name": "User", + // "jti": "OIn3g1SPSDSKAYDzENVoug", + // "email": "admin@jans.test", + // "jansAdminUIRole": [ + // "api-admin" + // ] + // } let USERINFO_TOKEN = "eyJraWQiOiJjb25uZWN0X2Y5YTAwN2EyLTZkMGItNDkyYS05MGNkLWYwYzliMWMyYjVkYl9zaWdfcnMyNTYiLCJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJzdWIiOiJxenhuMVNjcmI5bFd0R3hWZWRNQ2t5LVFsX0lMc3BaYVFBNmZ5dVlrdHcwIiwiZW1haWxfdmVyaWZpZWQiOnRydWUsInJvbGUiOlsiQ2FzYUFkbWluIl0sImlzcyI6Imh0dHBzOi8vYWNjb3VudC5nbHV1Lm9yZyIsImdpdmVuX25hbWUiOiJBZG1pbiIsIm1pZGRsZV9uYW1lIjoiQWRtaW4iLCJpbnVtIjoiYTZhNzAzMDEtYWY0OS00OTAxLTk2ODctMGJjZGNmNGUzNGZhIiwiY2xpZW50X2lkIjoiZDdmNzFiZWEtYzM4ZC00Y2FmLWExYmEtZTQzYzc0YTExYTYyIiwiYXVkIjoiZDdmNzFiZWEtYzM4ZC00Y2FmLWExYmEtZTQzYzc0YTExYTYyIiwidXBkYXRlZF9hdCI6MTczMTY5ODEzNSwibmFtZSI6IkRlZmF1bHQgQWRtaW4gVXNlciIsIm5pY2tuYW1lIjoiQWRtaW4iLCJmYW1pbHlfbmFtZSI6IlVzZXIiLCJqdGkiOiJPSW4zZzFTUFNEU0tBWUR6RU5Wb3VnIiwiZW1haWwiOiJhZG1pbkBqYW5zLnRlc3QiLCJqYW5zQWRtaW5VSVJvbGUiOlsiYXBpLWFkbWluIl19.CIahQtRpoTkIQx8KttLPIKH7gvGG8OmYCMzz7wch6k792DVYQG1R7q3sS9Ema1rO5Fm_GgjOsR0yTTMKsyhHDLBwkDd3cnMLgsh2AwVFZvxtpafTlUAPfjvMAy9YTtkPcY6rNUhsYLSSOA83kt6pHdIv5nI-G6ybqgg-bLBRpwZDoOV0TulRhmuukdiuugTXHT6Bb-K3ZeYs8CwewztnxoFTSDghSzq7VZIraV8SLTBLx5_xswn9mefamyB2XNN3o6vXuMyf4BEbYSCuJ3pu6YtNgfyWwt9cF8PYe4PVLoXZuJKN-cy4qrtgy43QXPCg96jSQUJqgLb5ZL5_3udm2Q"; let DEFAULT_REQUEST = JSON.stringify({ From f72d234b6230446ee77109060a117639d85d3847 Mon Sep 17 00:00:00 2001 From: Oleh Bohzok Date: Mon, 6 Jan 2025 17:13:46 +0200 Subject: [PATCH 27/43] chore(jans-cedarling): clone cedarling html app to another html app Signed-off-by: Oleh Bohzok --- .../cedarling_wasm/cedarling_app.html | 412 ++++++++++++++++++ 1 file changed, 412 insertions(+) create mode 100644 jans-cedarling/bindings/cedarling_wasm/cedarling_app.html diff --git a/jans-cedarling/bindings/cedarling_wasm/cedarling_app.html b/jans-cedarling/bindings/cedarling_wasm/cedarling_app.html new file mode 100644 index 00000000000..ecdb56585a2 --- /dev/null +++ b/jans-cedarling/bindings/cedarling_wasm/cedarling_app.html @@ -0,0 +1,412 @@ + + + + + + + + Hello world cedarling WASM example + + + + + + + + + + + +
+ +
+
+

Init cedarling

+ input bootstrap config json +
+ + +
+ + +
+ + +
+ + +
+ + + + + + + + + +
+ + + + + +
+ + + + + + + + + + + + + + + + + \ No newline at end of file From cba34876da7c029b17f8a0e2c454bfff387387c8 Mon Sep 17 00:00:00 2001 From: Oleh Bohzok Date: Mon, 6 Jan 2025 18:17:28 +0200 Subject: [PATCH 28/43] chore(jans-cedarling): add simplified example to the index.html Signed-off-by: Oleh Bohzok --- .../cedarling_wasm/cedarling_app.html | 2 +- .../bindings/cedarling_wasm/index.html | 244 ++---------------- 2 files changed, 22 insertions(+), 224 deletions(-) diff --git a/jans-cedarling/bindings/cedarling_wasm/cedarling_app.html b/jans-cedarling/bindings/cedarling_wasm/cedarling_app.html index ecdb56585a2..1f977f27ae1 100644 --- a/jans-cedarling/bindings/cedarling_wasm/cedarling_app.html +++ b/jans-cedarling/bindings/cedarling_wasm/cedarling_app.html @@ -5,7 +5,7 @@ - Hello world cedarling WASM example + Cedarling WASM App diff --git a/jans-cedarling/bindings/cedarling_wasm/index.html b/jans-cedarling/bindings/cedarling_wasm/index.html index ecdb56585a2..dea56718d4f 100644 --- a/jans-cedarling/bindings/cedarling_wasm/index.html +++ b/jans-cedarling/bindings/cedarling_wasm/index.html @@ -15,129 +15,25 @@ width: 100%; } - - - -
-
-
-

Init cedarling

- input bootstrap config json -
- - -
- - -
- - -
- - -
- - - - - - - - - +
+

It is Cedarling example WASM page

+ Result is written to the js console log.
- - - - - - - - - - - - - - + - request_error.hide(); - } catch (error) { - request_error.text(error.message); - request_error.show(); + From 458e6428b0d8ba09da367af09f9ae6f0ea3e10cc Mon Sep 17 00:00:00 2001 From: Oleh Bohzok Date: Mon, 6 Jan 2025 22:54:15 +0200 Subject: [PATCH 29/43] chore(jans-cedarling): refactor examples, move hardcoded data to `example_data.js` Signed-off-by: Oleh Bohzok --- .../cedarling_wasm/cedarling_app.html | 201 ++---------------- .../bindings/cedarling_wasm/example_data.js | 159 ++++++++++++++ .../bindings/cedarling_wasm/index.html | 169 +-------------- 3 files changed, 177 insertions(+), 352 deletions(-) create mode 100644 jans-cedarling/bindings/cedarling_wasm/example_data.js diff --git a/jans-cedarling/bindings/cedarling_wasm/cedarling_app.html b/jans-cedarling/bindings/cedarling_wasm/cedarling_app.html index 1f977f27ae1..ad15330d184 100644 --- a/jans-cedarling/bindings/cedarling_wasm/cedarling_app.html +++ b/jans-cedarling/bindings/cedarling_wasm/cedarling_app.html @@ -102,29 +102,9 @@

Result

Logs


             
- -
- - - - - - - - - - - From 9f771cc83b4f9d4f4b99cf262268abfa4c274936 Mon Sep 17 00:00:00 2001 From: Oleh Bohzok Date: Tue, 7 Jan 2025 00:40:58 +0200 Subject: [PATCH 30/43] chore(jans-cedarling): add scrolling to result when request is executed Signed-off-by: Oleh Bohzok --- .../bindings/cedarling_wasm/cedarling_app.html | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/jans-cedarling/bindings/cedarling_wasm/cedarling_app.html b/jans-cedarling/bindings/cedarling_wasm/cedarling_app.html index ad15330d184..63d0bb2344b 100644 --- a/jans-cedarling/bindings/cedarling_wasm/cedarling_app.html +++ b/jans-cedarling/bindings/cedarling_wasm/cedarling_app.html @@ -210,6 +210,11 @@

Logs

request_result_values.text(auth_result_json_str); request_result_panel.show(); + // scroll to show result + $('html, body').animate({ + scrollTop: request_result_panel.offset().top + }, 1000); + let logs = window.cedar_instance.pop_logs(); if (logs.length != 0) { let pretty_logs = logs.map(log => JSON.stringify(Object.fromEntries(log), null, 2)); @@ -225,6 +230,11 @@

Logs

request_error.text(error.message); request_error.show(); + // scroll to show error + $('html, body').animate({ + scrollTop: request_error.offset().top + }, 1000); + request_result_panel.hide(); request_log_panel.hide(); } finally { From 68702df19f59e3af737c9405c15d9d890e3b3f6c Mon Sep 17 00:00:00 2001 From: Oleh Bohzok Date: Tue, 7 Jan 2025 02:56:42 +0200 Subject: [PATCH 31/43] chore(jans-cedarling): add tokens key for request Signed-off-by: Oleh Bohzok --- jans-cedarling/bindings/cedarling_wasm/example_data.js | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/jans-cedarling/bindings/cedarling_wasm/example_data.js b/jans-cedarling/bindings/cedarling_wasm/example_data.js index 87b5ac6dd3e..78ced6921c8 100644 --- a/jans-cedarling/bindings/cedarling_wasm/example_data.js +++ b/jans-cedarling/bindings/cedarling_wasm/example_data.js @@ -129,9 +129,11 @@ let ID_TOKEN = "eyJraWQiOiJjb25uZWN0X2Y5YTAwN2EyLTZkMGItNDkyYS05MGNkLWYwYzliMWMy let USERINFO_TOKEN = "eyJraWQiOiJjb25uZWN0X2Y5YTAwN2EyLTZkMGItNDkyYS05MGNkLWYwYzliMWMyYjVkYl9zaWdfcnMyNTYiLCJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJzdWIiOiJxenhuMVNjcmI5bFd0R3hWZWRNQ2t5LVFsX0lMc3BaYVFBNmZ5dVlrdHcwIiwiZW1haWxfdmVyaWZpZWQiOnRydWUsInJvbGUiOlsiQ2FzYUFkbWluIl0sImlzcyI6Imh0dHBzOi8vYWNjb3VudC5nbHV1Lm9yZyIsImdpdmVuX25hbWUiOiJBZG1pbiIsIm1pZGRsZV9uYW1lIjoiQWRtaW4iLCJpbnVtIjoiYTZhNzAzMDEtYWY0OS00OTAxLTk2ODctMGJjZGNmNGUzNGZhIiwiY2xpZW50X2lkIjoiZDdmNzFiZWEtYzM4ZC00Y2FmLWExYmEtZTQzYzc0YTExYTYyIiwiYXVkIjoiZDdmNzFiZWEtYzM4ZC00Y2FmLWExYmEtZTQzYzc0YTExYTYyIiwidXBkYXRlZF9hdCI6MTczMTY5ODEzNSwibmFtZSI6IkRlZmF1bHQgQWRtaW4gVXNlciIsIm5pY2tuYW1lIjoiQWRtaW4iLCJmYW1pbHlfbmFtZSI6IlVzZXIiLCJqdGkiOiJPSW4zZzFTUFNEU0tBWUR6RU5Wb3VnIiwiZW1haWwiOiJhZG1pbkBqYW5zLnRlc3QiLCJqYW5zQWRtaW5VSVJvbGUiOlsiYXBpLWFkbWluIl19.CIahQtRpoTkIQx8KttLPIKH7gvGG8OmYCMzz7wch6k792DVYQG1R7q3sS9Ema1rO5Fm_GgjOsR0yTTMKsyhHDLBwkDd3cnMLgsh2AwVFZvxtpafTlUAPfjvMAy9YTtkPcY6rNUhsYLSSOA83kt6pHdIv5nI-G6ybqgg-bLBRpwZDoOV0TulRhmuukdiuugTXHT6Bb-K3ZeYs8CwewztnxoFTSDghSzq7VZIraV8SLTBLx5_xswn9mefamyB2XNN3o6vXuMyf4BEbYSCuJ3pu6YtNgfyWwt9cF8PYe4PVLoXZuJKN-cy4qrtgy43QXPCg96jSQUJqgLb5ZL5_3udm2Q"; let REQUEST = { - "access_token": ACCESS_TOKEN, - "id_token": ID_TOKEN, - "userinfo_token": USERINFO_TOKEN, + "tokens": { + "access_token": ACCESS_TOKEN, + "id_token": ID_TOKEN, + "userinfo_token": USERINFO_TOKEN, + }, "action": 'Jans::Action::"Read"', "resource": { "type": "Jans::Application", From d2b37dbb7189f4d8aab57b677d734baedc6f47da Mon Sep 17 00:00:00 2001 From: Oleh Bohzok Date: Tue, 7 Jan 2025 03:07:49 +0200 Subject: [PATCH 32/43] chore(jans-cedarling): fix serializing json logs Signed-off-by: Oleh Bohzok --- .../cedarling_wasm/cedarling_app.html | 4 +- .../bindings/cedarling_wasm/src/lib.rs | 49 +++++++++++++++++-- 2 files changed, 48 insertions(+), 5 deletions(-) diff --git a/jans-cedarling/bindings/cedarling_wasm/cedarling_app.html b/jans-cedarling/bindings/cedarling_wasm/cedarling_app.html index 63d0bb2344b..b76a90f2469 100644 --- a/jans-cedarling/bindings/cedarling_wasm/cedarling_app.html +++ b/jans-cedarling/bindings/cedarling_wasm/cedarling_app.html @@ -163,7 +163,7 @@

Logs

let logs = cedar_instance.pop_logs(); if (logs.length != 0) { - let pretty_logs = logs.map(log => JSON.stringify(Object.fromEntries(log), null, 2)); + let pretty_logs = logs.map(log => JSON.stringify(log, null, 2)); bootstrap_init_log_panel.show(); bootstrap_init_log_values.text(pretty_logs); @@ -217,7 +217,7 @@

Logs

let logs = window.cedar_instance.pop_logs(); if (logs.length != 0) { - let pretty_logs = logs.map(log => JSON.stringify(Object.fromEntries(log), null, 2)); + let pretty_logs = logs.map(log => JSON.stringify(log, null, 2)); request_log_panel.show(); request_log_values.text(pretty_logs); diff --git a/jans-cedarling/bindings/cedarling_wasm/src/lib.rs b/jans-cedarling/bindings/cedarling_wasm/src/lib.rs index cf55df1422d..43e974467c6 100644 --- a/jans-cedarling/bindings/cedarling_wasm/src/lib.rs +++ b/jans-cedarling/bindings/cedarling_wasm/src/lib.rs @@ -10,7 +10,7 @@ use serde_json::json; use serde_wasm_bindgen::Error; use std::rc::Rc; use wasm_bindgen::prelude::*; -use wasm_bindgen_futures::js_sys::{Array, Map, Object}; +use wasm_bindgen_futures::js_sys::{Array, Map, Object, Reflect}; #[cfg(test)] mod tests; @@ -86,7 +86,7 @@ impl Cedarling { pub fn pop_logs(&self) -> Result { let result = Array::new(); for log in self.instance.pop_logs() { - let js_log = serde_wasm_bindgen::to_value(&log)?; + let js_log = convert_json_to_object(&log)?; result.push(&js_log); } Ok(result) @@ -96,7 +96,7 @@ impl Cedarling { /// Returns `Map` with values or `null`. pub fn get_log_by_id(&self, id: &str) -> Result { let result = if let Some(log_json_value) = self.instance.get_log_by_id(id) { - serde_wasm_bindgen::to_value(&log_json_value)? + convert_json_to_object(&log_json_value)? } else { JsValue::NULL }; @@ -115,6 +115,49 @@ impl Cedarling { } } +/// convert json to js object +fn convert_json_to_object(json_value: &serde_json::Value) -> Result { + let js_map_value = serde_wasm_bindgen::to_value(json_value)?; + to_object_recursive(js_map_value) +} + +/// recurcive convert [`Map`] to object +fn to_object_recursive(value: JsValue) -> Result { + if value.is_instance_of::() { + // Convert the Map into an Object where keys and values are recursively processed + let map = Map::unchecked_from_js(value); + let obj = Object::new(); + for entry in map.entries().into_iter() { + let entry = Array::unchecked_from_js(entry?); + let key = entry.get(0); + let val = to_object_recursive(entry.get(1))?; + Reflect::set(&obj, &key, &val)?; + } + Ok(obj.into()) + } else if value.is_instance_of::() { + // Recursively process arrays + let array = Array::unchecked_from_js(value); + let serialized_array = Array::new(); + for item in array.iter() { + serialized_array.push(&to_object_recursive(item)?); + } + Ok(serialized_array.into()) + } else if value.is_object() { + // Recursively process plain objects + let obj = Object::unchecked_from_js(value); + let keys = Object::keys(&obj); + let serialized_obj = Object::new(); + for key in keys.iter() { + let val = Reflect::get(&obj, &key)?; + Reflect::set(&serialized_obj, &key, &to_object_recursive(val)?)?; + } + Ok(serialized_obj.into()) + } else { + // Return primitive values as-is + Ok(value) + } +} + /// A WASM wrapper for the Rust `cedarling::AuthorizeResult` struct. /// Represents the result of an authorization request. #[wasm_bindgen] From 98233fd005d76ffa74c0e2eb882ad52028bc9daa Mon Sep 17 00:00:00 2001 From: Oleh Bohzok Date: Tue, 7 Jan 2025 03:22:19 +0200 Subject: [PATCH 33/43] chore(jans-cedarling): add licence header Signed-off-by: Oleh Bohzok --- jans-cedarling/bindings/cedarling_wasm/cedarling_app.html | 6 ++++++ jans-cedarling/bindings/cedarling_wasm/index.html | 6 ++++++ jans-cedarling/bindings/cedarling_wasm/src/tests.rs | 5 +++++ 3 files changed, 17 insertions(+) diff --git a/jans-cedarling/bindings/cedarling_wasm/cedarling_app.html b/jans-cedarling/bindings/cedarling_wasm/cedarling_app.html index b76a90f2469..9900abdcf82 100644 --- a/jans-cedarling/bindings/cedarling_wasm/cedarling_app.html +++ b/jans-cedarling/bindings/cedarling_wasm/cedarling_app.html @@ -1,3 +1,9 @@ + diff --git a/jans-cedarling/bindings/cedarling_wasm/index.html b/jans-cedarling/bindings/cedarling_wasm/index.html index 34620b1e016..e5160b6dff8 100644 --- a/jans-cedarling/bindings/cedarling_wasm/index.html +++ b/jans-cedarling/bindings/cedarling_wasm/index.html @@ -1,3 +1,9 @@ + diff --git a/jans-cedarling/bindings/cedarling_wasm/src/tests.rs b/jans-cedarling/bindings/cedarling_wasm/src/tests.rs index e309cd518a1..740e2cb7f72 100644 --- a/jans-cedarling/bindings/cedarling_wasm/src/tests.rs +++ b/jans-cedarling/bindings/cedarling_wasm/src/tests.rs @@ -1,3 +1,8 @@ +// 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. + // allow dead code to avoid highlight test functions (by linter) that is used only using WASM #![allow(dead_code)] From 87356f691daee7363ca93d0c4fb3f2cea88529b8 Mon Sep 17 00:00:00 2001 From: Oleh Bohzok Date: Tue, 7 Jan 2025 04:07:52 +0200 Subject: [PATCH 34/43] chore(jans-cedarling): update bootstrap to latest Signed-off-by: Oleh Bohzok Signed-off-by: Oleh Bohzok <6554798+olehbozhok@users.noreply.github.com> Signed-off-by: Oleh Bohzok --- .../cedarling_wasm/cedarling_app.html | 48 +++++++++++-------- .../bindings/cedarling_wasm/index.html | 4 +- 2 files changed, 29 insertions(+), 23 deletions(-) diff --git a/jans-cedarling/bindings/cedarling_wasm/cedarling_app.html b/jans-cedarling/bindings/cedarling_wasm/cedarling_app.html index 9900abdcf82..84c6a3b6898 100644 --- a/jans-cedarling/bindings/cedarling_wasm/cedarling_app.html +++ b/jans-cedarling/bindings/cedarling_wasm/cedarling_app.html @@ -13,12 +13,16 @@ Cedarling WASM App - + @@ -47,8 +51,11 @@

Init cedarling

- - +
+ + +
@@ -59,7 +66,7 @@

Init cedarling

@@ -85,8 +92,10 @@

Execute authorize request

- - +
+ + +
@@ -99,26 +108,23 @@

Execute authorize request

- - - - + + + +``` + +## Usage + +Before usage make sure that you have completed `Building` steps. +You can find usage examples in the following locations: + +- `jans-cedarling/bindings/cedarling_wasm/index.html`: A simple example demonstrating basic usage. +- `jans-cedarling/bindings/cedarling_wasm/cedarling_app.html`: A fully featured `Cedarling` browser app where you can test and validate your configuration. + +### Defined API + +```ts +/** + * Create a new instance of the Cedarling application. + * This function can take as config parameter the eather `Map` other `Object` + */ +export function init(config: any): Promise; + +/** + * The instance of the Cedarling application. + */ +export class Cedarling { + /** + * Create a new instance of the Cedarling application. + * Assume that config is `Object` + */ + static new(config: object): Promise; + /** + * Create a new instance of the Cedarling application. + * Assume that config is `Map` + */ + static new_from_map(config: Map): Promise; + /** + * Authorize request + * makes authorization decision based on the [`Request`] + */ + authorize(request: any): Promise; + /** + * Get logs and remove them from the storage. + * Returns `Array` of `Map` + */ + pop_logs(): Array; + /** + * Get specific log entry. + * Returns `Map` with values or `null`. + */ + get_log_by_id(id: string): any; + /** + * Returns a list of all log ids. + * Returns `Array` of `String` + */ + get_log_ids(): Array; +} + +/** + * A WASM wrapper for the Rust `cedarling::AuthorizeResult` struct. + * Represents the result of an authorization request. + */ +export class AuthorizeResult { + /** + * Convert `AuthorizeResult` to json string value + */ + json_string(): string; + /** + * Result of authorization where principal is `Jans::Workload` + */ + workload?: AuthorizeResultResponse; + /** + * Result of authorization where principal is `Jans::User` + */ + person?: AuthorizeResultResponse; + /** + * Result of authorization + * true means `ALLOW` + * false means `Deny` + * + * this field is [`bool`] type to be compatible with [authzen Access Evaluation Decision](https://openid.github.io/authzen/#section-6.2.1). + */ + decision: boolean; +} + +/** + * A WASM wrapper for the Rust `cedar_policy::Response` struct. + * Represents the result of an authorization request. + */ +export class AuthorizeResultResponse { + /** + * Authorization decision + */ + readonly decision: boolean; + /** + * Diagnostics providing more information on how this decision was reached + */ + readonly diagnostics: Diagnostics; +} + +/** + * Diagnostics + * =========== + * + * Provides detailed information about how a policy decision was made, including policies that contributed to the decision and any errors encountered during evaluation. + */ +export class Diagnostics { + /** + * `PolicyId`s of the policies that contributed to the decision. + * If no policies applied to the request, this set will be empty. + * + * The ids should be treated as unordered, + */ + readonly reason: (string)[]; + /** + * Errors that occurred during authorization. The errors should be + * treated as unordered, since policies may be evaluated in any order. + */ + readonly errors: (PolicyEvaluationError)[]; +} + +/** + * PolicyEvaluationError + * ===================== + * + * Represents an error that occurred when evaluating a Cedar policy. + */ +export class PolicyEvaluationError { + /** + * Id of the policy with an error + */ + readonly id: string; + /** + * Underlying evaluation error string representation + */ + readonly error: string; +} +``` diff --git a/jans-cedarling/bindings/cedarling_wasm/README.md b/jans-cedarling/bindings/cedarling_wasm/README.md index 7a36c132216..f49af05a1a8 100644 --- a/jans-cedarling/bindings/cedarling_wasm/README.md +++ b/jans-cedarling/bindings/cedarling_wasm/README.md @@ -36,4 +36,153 @@ To run example using `index.html` you need execute following steps: 1. Build wasm cedarling. 2. Run webserver using `python3 -m http.server` or any other. -3. Visit [localhost](http://localhost:8000/). +3. Visit example app [localhost](http://localhost:8000/), on this app you will get log in browser console. + - Also you can try use cedarling with web app using [cedarling_app](http://localhost:8000/cedarling_app.html), using custom bootstrap properties and request. + +## WASM Usage + +After building WASM bindings in folder `pkg` you can find where you can find `cedarling_wasm.js` and `cedarling_wasm.d.ts` where is defined interface for application. + +In `index.html` described simple usage of `cedarling wasm` API: + +```js + import { BOOTSTRAP_CONFIG, REQUEST } from "/example_data.js" // Import js objects: bootstrap config and request + import initWasm, { init } from "/pkg/cedarling_wasm.js"; + + async function main() { + await initWasm(); // Initialize the WebAssembly module + + let instance = await init(BOOTSTRAP_CONFIG); + let result = await instance.authorize(REQUEST); + console.log("result:", result); + } + main().catch(console.error); +``` + +Before using any function from library you need initialize WASM runtime by calling `initWasm` function. + +### Defined API + +```ts +/** + * Create a new instance of the Cedarling application. + * This function can take as config parameter the eather `Map` other `Object` + */ +export function init(config: any): Promise; + +/** + * The instance of the Cedarling application. + */ +export class Cedarling { + /** + * Create a new instance of the Cedarling application. + * Assume that config is `Object` + */ + static new(config: object): Promise; + /** + * Create a new instance of the Cedarling application. + * Assume that config is `Map` + */ + static new_from_map(config: Map): Promise; + /** + * Authorize request + * makes authorization decision based on the [`Request`] + */ + authorize(request: any): Promise; + /** + * Get logs and remove them from the storage. + * Returns `Array` of `Map` + */ + pop_logs(): Array; + /** + * Get specific log entry. + * Returns `Map` with values or `null`. + */ + get_log_by_id(id: string): any; + /** + * Returns a list of all log ids. + * Returns `Array` of `String` + */ + get_log_ids(): Array; +} + +/** + * A WASM wrapper for the Rust `cedarling::AuthorizeResult` struct. + * Represents the result of an authorization request. + */ +export class AuthorizeResult { + /** + * Convert `AuthorizeResult` to json string value + */ + json_string(): string; + /** + * Result of authorization where principal is `Jans::Workload` + */ + workload?: AuthorizeResultResponse; + /** + * Result of authorization where principal is `Jans::User` + */ + person?: AuthorizeResultResponse; + /** + * Result of authorization + * true means `ALLOW` + * false means `Deny` + * + * this field is [`bool`] type to be compatible with [authzen Access Evaluation Decision](https://openid.github.io/authzen/#section-6.2.1). + */ + decision: boolean; +} + +/** + * A WASM wrapper for the Rust `cedar_policy::Response` struct. + * Represents the result of an authorization request. + */ +export class AuthorizeResultResponse { + /** + * Authorization decision + */ + readonly decision: boolean; + /** + * Diagnostics providing more information on how this decision was reached + */ + readonly diagnostics: Diagnostics; +} + +/** + * Diagnostics + * =========== + * + * Provides detailed information about how a policy decision was made, including policies that contributed to the decision and any errors encountered during evaluation. + */ +export class Diagnostics { + /** + * `PolicyId`s of the policies that contributed to the decision. + * If no policies applied to the request, this set will be empty. + * + * The ids should be treated as unordered, + */ + readonly reason: (string)[]; + /** + * Errors that occurred during authorization. The errors should be + * treated as unordered, since policies may be evaluated in any order. + */ + readonly errors: (PolicyEvaluationError)[]; +} + +/** + * PolicyEvaluationError + * ===================== + * + * Represents an error that occurred when evaluating a Cedar policy. + */ +export class PolicyEvaluationError { + /** + * Id of the policy with an error + */ + readonly id: string; + /** + * Underlying evaluation error string representation + */ + readonly error: string; +} +``` diff --git a/jans-cedarling/bindings/cedarling_wasm/index.html b/jans-cedarling/bindings/cedarling_wasm/index.html index 80308f5e145..fb5078e883c 100644 --- a/jans-cedarling/bindings/cedarling_wasm/index.html +++ b/jans-cedarling/bindings/cedarling_wasm/index.html @@ -38,7 +38,7 @@

It is Cedarling example WASM page