diff --git a/jans-lock/cedarling/.gitignore b/jans-lock/cedarling/.gitignore index 2a354980784..660b01c1314 100644 --- a/jans-lock/cedarling/.gitignore +++ b/jans-lock/cedarling/.gitignore @@ -1,3 +1,4 @@ /target /meta /.vscode +/samples diff --git a/jans-lock/cedarling/Cargo.lock b/jans-lock/cedarling/Cargo.lock index dc0f79bec5f..720c92d9b58 100644 --- a/jans-lock/cedarling/Cargo.lock +++ b/jans-lock/cedarling/Cargo.lock @@ -2,6 +2,12 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "adler" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" + [[package]] name = "aho-corasick" version = "1.1.3" @@ -11,21 +17,6 @@ dependencies = [ "memchr", ] -[[package]] -name = "alloc-no-stdlib" -version = "2.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc7bb162ec39d46ab1ca8c77bf72e890535becd1751bb45f64c597edb4c8c6b3" - -[[package]] -name = "alloc-stdlib" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94fb8275041c72129eb51b7d0322c29b8387a0386127718b096429201a5d6ece" -dependencies = [ - "alloc-no-stdlib", -] - [[package]] name = "android-tzdata" version = "0.1.1" @@ -83,27 +74,6 @@ version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf4b9d6a944f767f8e5e0db018570623c85f3d925ac718db4e06d0187adb21c1" -[[package]] -name = "brotli" -version = "6.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74f7971dbd9326d58187408ab83117d8ac1bb9c17b085fdacd1cf2f598719b6b" -dependencies = [ - "alloc-no-stdlib", - "alloc-stdlib", - "brotli-decompressor", -] - -[[package]] -name = "brotli-decompressor" -version = "4.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a45bd2e4095a8b518033b128020dd4a55aab1c0a381ba4404a472630f4bc362" -dependencies = [ - "alloc-no-stdlib", - "alloc-stdlib", -] - [[package]] name = "bumpalo" version = "3.16.0" @@ -188,11 +158,12 @@ dependencies = [ name = "cedarling" version = "0.1.0" dependencies = [ - "brotli", "cedar-policy", "console_error_panic_hook", + "miniz_oxide", "serde", "serde-wasm-bindgen", + "serde_json", "static-toml", "wasm-bindgen", "wasm-bindgen-futures", @@ -574,6 +545,15 @@ dependencies = [ "syn 2.0.66", ] +[[package]] +name = "miniz_oxide" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8a240ddb74feaf34a79a7add65a741f3167852fba007066dcac1ca548d89c08" +dependencies = [ + "adler", +] + [[package]] name = "new_debug_unreachable" version = "1.0.6" diff --git a/jans-lock/cedarling/Cargo.toml b/jans-lock/cedarling/Cargo.toml index b01408ff986..ed1f187824c 100644 --- a/jans-lock/cedarling/Cargo.toml +++ b/jans-lock/cedarling/Cargo.toml @@ -6,12 +6,19 @@ version = "0.1.0" edition = "2021" [dependencies] -brotli = "6.0.0" cedar-policy = { version = "3.2.0", features = ["wasm"] } -console_error_panic_hook = "0.1.7" +static-toml = { version = "1.2.0", default-features = false } +miniz_oxide = { version = "0.7.4", default-features = false, features = [ + "with-alloc", +] } + +# serde serde = { version = "*", features = ["derive"] } +serde_json = "1.0.117" + +# Environment Dependencies +console_error_panic_hook = "0.1.7" serde-wasm-bindgen = "0.6.5" -static-toml = "1.2.0" wasm-bindgen = "0.2.92" wasm-bindgen-futures = "0.4.42" @@ -19,7 +26,6 @@ wasm-bindgen-futures = "0.4.42" version = "0.3.69" features = [ 'console', - 'Window', 'Response', 'Request', 'Headers', @@ -27,6 +33,7 @@ features = [ 'RequestInit', 'EventSource', 'MessageEvent', + 'UrlSearchParams', ] [dev-dependencies] diff --git a/jans-lock/cedarling/README.md b/jans-lock/cedarling/README.md new file mode 100644 index 00000000000..afd8d34bda5 --- /dev/null +++ b/jans-lock/cedarling/README.md @@ -0,0 +1,5 @@ +## cedarling + +The `cedarling` is an embeddable Webassembly Component that runs a local Cedar Engine, enabling fine grained and responsive Policy Management on the Web. The `cedarling` allows for dynamic updates to it's internal Policy Store via Server Sent events, enabling sub-second Access Management. + +### How it works diff --git a/jans-lock/cedarling/config.toml b/jans-lock/cedarling/config.toml index 336419fcfbe..8e8c2c72eaa 100644 --- a/jans-lock/cedarling/config.toml +++ b/jans-lock/cedarling/config.toml @@ -1,7 +1,20 @@ -schema_url = "https://api.github.com/repos/nynymike/cedar-playground/contents/gluu_lock.cedarschema?ref=main" +# https://github.com/JanssenProject/jans/wiki/Cedarling-Nativity-Plan#bootstrap-properties -# Policy Store Settings -[policy_store] -use_static_store = false -remote_uri = "https://api.github.com/repos/nynymike/cedar-playground/contents/gluu_lock.cedarschema?ref=main" -use_brotli_decompression = false # Whether the policy store is to be decopmressed using Brotli +# Whether policy store should be decompressed using deflate +decompress-policy-store = false + +# Self explanatory +openid-config-url = "https://account.gluu.org/.well-known/openid-configuration" + +# whether Cedarling should listen for SSE config updates +dynamic-configuration = false + +[policy-store] +# How to get policy store, can be local, remote or lock-master +strategy = "local" +uri = "https://api.github.com/repos/nynymike/cedar-playground/contents/gluu_lock.cedarschema?ref=main" + +[lock-master] +url = "*" +ssa_jwt = "eyJhbG" +policy_store_id = "test#5" diff --git a/jans-lock/cedarling/policies.store b/jans-lock/cedarling/policies.store deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/jans-lock/cedarling/policy-store/default.json b/jans-lock/cedarling/policy-store/default.json new file mode 100644 index 00000000000..66183ececb6 --- /dev/null +++ b/jans-lock/cedarling/policy-store/default.json @@ -0,0 +1,408 @@ +{ + "Schema": { + "Jans": { + "commonTypes": { + "Url": { + "type": "Record", + "attributes": { + "host": { + "type": "String" + }, + "path": { + "type": "String" + }, + "protocol": { + "type": "String" + } + } + }, + "Context": { + "type": "Record", + "attributes": { + "browser": { + "type": "String" + }, + "current_time": { + "type": "Long" + }, + "device_health": { + "type": "Set", + "element": { + "type": "String" + } + }, + "fraud_indicators": { + "type": "Set", + "element": { + "type": "String" + } + }, + "geolocation": { + "type": "Set", + "element": { + "type": "String" + } + }, + "network": { + "type": "Extension", + "name": "ipaddr" + }, + "network_type": { + "type": "String" + }, + "operating_system": { + "type": "String" + } + } + }, + "email_address": { + "type": "Record", + "attributes": { + "domain": { + "type": "String" + }, + "id": { + "type": "String" + } + } + } + }, + "entityTypes": { + "Userinfo_token": { + "shape": { + "type": "Record", + "attributes": { + "aud": { + "type": "String" + }, + "birthdate": { + "type": "String" + }, + "email": { + "type": "email_address" + }, + "exp": { + "type": "Long" + }, + "iat": { + "type": "Long" + }, + "iss": { + "type": "Entity", + "name": "TrustedIssuer" + }, + "jti": { + "type": "String" + }, + "name": { + "type": "String" + }, + "phone_number": { + "type": "String" + }, + "role": { + "type": "Set", + "element": { + "type": "String" + } + }, + "sub": { + "type": "String" + } + } + } + }, + "Access_token": { + "shape": { + "type": "Record", + "attributes": { + "aud": { + "type": "String" + }, + "exp": { + "type": "Long" + }, + "iat": { + "type": "Long" + }, + "iss": { + "type": "Entity", + "name": "TrustedIssuer" + }, + "jti": { + "type": "String" + }, + "nbf": { + "type": "Long" + }, + "scope": { + "type": "String" + } + } + } + }, + "Application": { + "shape": { + "type": "Record", + "attributes": { + "client": { + "type": "Entity", + "name": "Client" + }, + "name": { + "type": "String" + } + } + } + }, + "Role": {}, + "TrustedIssuer": { + "shape": { + "type": "Record", + "attributes": { + "issuer_entity_id": { + "type": "Url" + } + } + } + }, + "Client": { + "shape": { + "type": "Record", + "attributes": { + "client_id": { + "type": "String" + }, + "iss": { + "type": "Entity", + "name": "TrustedIssuer" + } + } + } + }, + "id_token": { + "shape": { + "type": "Record", + "attributes": { + "acr": { + "type": "Set", + "element": { + "type": "String" + } + }, + "amr": { + "type": "String" + }, + "aud": { + "type": "String" + }, + "azp": { + "type": "String" + }, + "birthdate": { + "type": "String" + }, + "email": { + "type": "email_address" + }, + "exp": { + "type": "Long" + }, + "iat": { + "type": "Long" + }, + "iss": { + "type": "Entity", + "name": "TrustedIssuer" + }, + "jti": { + "type": "String" + }, + "name": { + "type": "String" + }, + "phone_number": { + "type": "String" + }, + "role": { + "type": "Set", + "element": { + "type": "String" + } + }, + "sub": { + "type": "String" + } + } + } + }, + "User": { + "memberOfTypes": [ + "Role" + ], + "shape": { + "type": "Record", + "attributes": { + "email": { + "type": "email_address" + }, + "phone_number": { + "type": "String" + }, + "role": { + "type": "Set", + "element": { + "type": "String" + } + }, + "sub": { + "type": "String" + }, + "username": { + "type": "String" + } + } + } + }, + "HTTP_Request": { + "shape": { + "type": "Record", + "attributes": { + "accept": { + "type": "Set", + "element": { + "type": "String" + } + }, + "header": { + "type": "Set", + "element": { + "type": "String" + } + }, + "url": { + "type": "Url" + } + } + } + } + }, + "actions": { + "GET": { + "appliesTo": { + "resourceTypes": [ + "HTTP_Request" + ], + "principalTypes": [ + "Client" + ], + "context": { + "type": "Context" + } + } + }, + "DELETE": { + "appliesTo": { + "resourceTypes": [ + "HTTP_Request" + ], + "principalTypes": [ + "Client" + ], + "context": { + "type": "Context" + } + } + }, + "POST": { + "appliesTo": { + "resourceTypes": [ + "HTTP_Request" + ], + "principalTypes": [ + "Client" + ], + "context": { + "type": "Context" + } + } + }, + "HEAD": { + "appliesTo": { + "resourceTypes": [ + "HTTP_Request" + ], + "principalTypes": [ + "Client" + ], + "context": { + "type": "Context" + } + } + }, + "PUT": { + "appliesTo": { + "resourceTypes": [ + "HTTP_Request" + ], + "principalTypes": [ + "Client" + ], + "context": { + "type": "Context" + } + } + }, + "PATCH": { + "appliesTo": { + "resourceTypes": [ + "HTTP_Request" + ], + "principalTypes": [ + "Client" + ], + "context": { + "type": "Context" + } + } + }, + "Access": { + "appliesTo": { + "resourceTypes": [ + "Application" + ], + "principalTypes": [ + "User", + "Role" + ], + "context": { + "type": "Context" + } + } + } + } + } + }, + "PolicySet": [], + "TrustedIssuers": [ + { + "name": "Gluu", + "Description": "Consumer IDP", + "openid_configuration_endpoint": "https://account.gluu.org/.well-known/openid-configuration", + "access_tokens": { + "trusted": true + }, + "id_tokens": { + "trusted": true, + "principal_identifier": "email" + }, + "userinfo_tokens": { + "trusted": true, + "role_mapping": "role" + }, + "tx_tokens": { + "trusted": true + } + } + ] +} \ No newline at end of file diff --git a/jans-lock/cedarling/src/authz/mod.rs b/jans-lock/cedarling/src/authz/mod.rs new file mode 100644 index 00000000000..35180a2f3e5 --- /dev/null +++ b/jans-lock/cedarling/src/authz/mod.rs @@ -0,0 +1,25 @@ +use serde_wasm_bindgen::from_value; +use wasm_bindgen::prelude::*; + +#[derive(serde::Deserialize, serde::Serialize, Debug, Default)] +pub struct Tokens { + // generates entities + #[serde(rename = "access_token")] + pub(crate) access: String, + #[serde(rename = "id_token")] + pub(crate) id: String, + + #[serde(rename = "userinfo_token")] + pub(crate) user_info: String, + #[serde(rename = "tx_token")] + pub(crate) tx: String, + #[serde(rename = "sse")] + pub(crate) sse_url: String, +} + +#[wasm_bindgen] +pub async fn authz(req: JsValue) -> JsValue { + // TODO: Stub + let _request = from_value::(req).unwrap(); + JsValue::NULL +} diff --git a/jans-lock/cedarling/src/cedar.rs b/jans-lock/cedarling/src/cedar.rs deleted file mode 100644 index beff952ed77..00000000000 --- a/jans-lock/cedarling/src/cedar.rs +++ /dev/null @@ -1,35 +0,0 @@ -use cedar_policy::*; -use wasm_bindgen::UnwrapThrowExt; - -// SAFETY: Webassembly is single-threaded, so this is safe. The statics are also scoped -pub(crate) fn entities(swap: Option) -> &'static Entities { - static mut ENTITIES: Option = None; - - unsafe { - swap.map(|s| ENTITIES = Some(s)); - ENTITIES.as_ref().unwrap_throw() - } -} - -pub(crate) fn policies(swap: Option) -> &'static PolicySet { - static mut ENTITIES: Option = None; - - unsafe { - swap.map(|s| ENTITIES = Some(s)); - ENTITIES.as_ref().unwrap_throw() - } -} - -pub(crate) fn schema(swap: Option) -> Option<&'static Schema> { - static mut ENTITIES: Option = None; - - unsafe { - swap.map(|s| ENTITIES = Some(s)); - ENTITIES.as_ref() - } -} - -pub(crate) fn authorizer() -> &'static Authorizer { - static mut AUTHORIZER: Option = None; - unsafe { AUTHORIZER.get_or_insert_with(|| Authorizer::new()) } -} diff --git a/jans-lock/cedarling/src/http.rs b/jans-lock/cedarling/src/http.rs new file mode 100644 index 00000000000..b9a20a2369b --- /dev/null +++ b/jans-lock/cedarling/src/http.rs @@ -0,0 +1,129 @@ +use std::{borrow::Cow, future::Future}; + +use wasm_bindgen::prelude::*; +use wasm_bindgen_futures::*; +use web_sys::*; + +#[wasm_bindgen] +extern "C" { + #[wasm_bindgen(js_name = fetch)] + pub fn fetch_with_request_and_init(input: &Request, init: &RequestInit) -> js_sys::Promise; +} + +pub(crate) async fn get(url: &str, headers: &[(&str, &str)]) -> Option { + let mut opts = RequestInit::new(); + opts.method("GET"); + opts.mode(RequestMode::NoCors); + + // insert headers + let h = Headers::new().unwrap(); + for (key, value) in headers { + h.set(key, value).unwrap(); + } + + opts.headers(&h); + + let req = Request::new_with_str(url).unwrap(); + let res = JsFuture::from(fetch_with_request_and_init(&req, &opts)).await.unwrap(); + + // check if return value is response + res.dyn_into::().ok() +} + +#[allow(dead_code)] +pub(crate) enum PostBody<'a, T: serde::Serialize = ()> { + None, + Json(T), + Form(T), + String(Cow<'a, str>), + Bytes(Vec), +} + +pub(crate) async fn post<'a, T: serde::Serialize>(url: &str, body: PostBody<'a, T>, headers: &[(&'a str, &'a str)]) -> Option { + let mut opts = RequestInit::new(); + let h = Headers::new().unwrap(); + + opts.method("POST"); + opts.mode(RequestMode::NoCors); + + // Set the body + match body { + PostBody::None => {} + PostBody::Form(form) => { + let init = serde_wasm_bindgen::to_value(&form).unwrap(); + let params = UrlSearchParams::new_with_str_sequence_sequence(&init).unwrap(); + + opts.body(Some(&JsValue::from(params))); + h.set("Content-Type", "application/x-www-form-urlencoded").unwrap(); + } + PostBody::Json(json) => { + let json = serde_json::to_string(&json).unwrap(); + let json = JsValue::from_str(&json); + + opts.body(Some(&json)); + h.set("Content-Type", "application/json").unwrap(); + } + PostBody::String(string) => { + opts.body(Some(&JsValue::from_str(&string))); + h.set("Content-Type", "text/plain").unwrap(); + } + PostBody::Bytes(bytes) => { + let array = js_sys::Uint8Array::new_with_length(bytes.len() as _); + array.copy_from(&bytes); + opts.body(Some(&array)); + h.set("Content-Type", "application/octet-stream").unwrap(); + } + } + + // set headers + for (key, value) in headers { + h.set(key, value).unwrap(); + } + opts.headers(&h); + + let request = Request::new_with_str(url).unwrap(); + let res = JsFuture::from(fetch_with_request_and_init(&request, &opts)).await.unwrap(); + + // check if return value is response + res.dyn_into::().ok() +} + +pub trait ResponseEx { + fn into_string(self) -> impl Future>; + fn into_bytes(self) -> impl Future>>; + fn into_json(self) -> impl Future>; +} + +impl ResponseEx for Response { + async fn into_json(self) -> Option { + let text = self.into_string().await?; + serde_json::from_str(&text).ok() + } + + async fn into_string(self) -> Option { + if self.ok() { + let text = JsFuture::from(self.text().ok()?).await.ok()?; + text.as_string() + } else { + let text = JsFuture::from(self.text().ok()?).await.ok()?; + let message = JsValue::from_str(format!("{}: {}", self.status_text(), text.as_string().unwrap()).as_str()); + + console::error_1(&message); + None + } + } + + async fn into_bytes(self) -> Option> { + if self.ok() { + let array = JsFuture::from(self.array_buffer().ok()?).await.ok()?; + let array = js_sys::Uint8Array::new(&array); + Some(array.to_vec()) + } else { + let text = JsFuture::from(self.text().ok()?).await.ok()?; + let message = JsValue::from_str(format!("{}: {}", self.status_text(), text.as_string().unwrap()).as_str()); + + console::error_1(&message); + None + } + } +} diff --git a/jans-lock/cedarling/src/lib.rs b/jans-lock/cedarling/src/lib.rs index 0c9e4766494..f4b60fb78e0 100644 --- a/jans-lock/cedarling/src/lib.rs +++ b/jans-lock/cedarling/src/lib.rs @@ -1,81 +1,27 @@ -use std::{borrow::Cow, str}; +use wasm_bindgen::prelude::*; -use cedar_policy::*; -use serde_wasm_bindgen::{from_value, to_value}; -use wasm_bindgen::{prelude::*, throw_val}; -use web_sys::*; +pub(crate) mod http; -pub mod cedar; -pub mod types; -pub mod utils; - -#[wasm_bindgen(start)] -pub fn start() { - // setup panic hook - console_error_panic_hook::set_once(); -} +mod authz; +mod sse; +mod startup; static_toml::static_toml! { pub(crate) static CONFIG = include_toml!("config.toml"); } -#[wasm_bindgen] -pub async fn init(tokens: JsValue, entities: Option) { - let tokens = from_value::(tokens).unwrap_throw(); - let schema = utils::fetch_schema().await; - cedar::schema(Some(schema)); - - if let Some(entities) = entities { - let entities = utils::get(&entities).await.expect_throw("Can't fetch entities from URL"); - let entities = Entities::from_json_str(str::from_utf8(&entities).unwrap_throw(), cedar::schema(None)).unwrap_throw(); - cedar::entities(Some(entities)); - } - - // Load Policy Store - let vector; - let mut raw = if CONFIG.policy_store.use_static_store { - include_bytes!("../policies.store") - } else { - vector = utils::get(&CONFIG.policy_store.remote_uri).await.expect_throw("Can't fetch policies from remote location"); - vector.as_slice() - }; - - let decompressed = if CONFIG.policy_store.use_brotli_decompression { - let mut buffer = Vec::with_capacity(raw.len()); - brotli::BrotliDecompress(&mut raw, &mut buffer).expect_throw("Unable to Decompress Policy Store!"); - Cow::Owned(buffer) - } else { - Cow::Borrowed(raw) - }; - - let policies: PolicySet = str::from_utf8(&decompressed).unwrap_throw().parse().unwrap_throw(); - cedar::policies(Some(policies)); - - // register sse - let sse = EventSource::new(&tokens.sse_url).unwrap_throw(); - let sse2 = sse.clone(); - - // Setup event listeners - let onopen = Closure::once_into_js(move || { - let onmessage = Closure::::new(move |ev: MessageEvent| { - console::log_2(&JsValue::from_str("Received message: "), &ev); - }) - .into_js_value(); - sse2.set_onmessage(Some(onmessage.as_ref().unchecked_ref())); - - let onerror = Closure::::new(move |ev: JsValue| throw_val(ev)).into_js_value(); - sse2.set_onerror(Some(onerror.as_ref().unchecked_ref())); - }); - - sse.set_onopen(Some(onopen.as_ref().unchecked_ref())); -} +#[wasm_bindgen(start)] +pub(crate) async fn start() { + // TODO: setup panic hook, on production we should use wasm-bindgen::UnwrapThrowExt + console_error_panic_hook::set_once(); -#[wasm_bindgen] -pub async fn authz(req: JsValue) -> JsValue { - let request = from_value::(req).unwrap_throw(); + // load policy store + let config = startup::open_id_config(CONFIG.openid_config_url).await; + let policy_store = startup::get_policy_store(config).await; - let request = cedar_policy::Request::from(request); - let response = cedar::authorizer().is_authorized(&request, cedar::policies(None), cedar::entities(None)); + // Persist Policy Store Data + startup::persist_policy_store_data(policy_store); - to_value(&response.decision()).unwrap_throw() + // Enable Dynamic Updates via Server Sent Events + sse::install(); } diff --git a/jans-lock/cedarling/src/sse.rs b/jans-lock/cedarling/src/sse.rs new file mode 100644 index 00000000000..3a8a05ace42 --- /dev/null +++ b/jans-lock/cedarling/src/sse.rs @@ -0,0 +1,27 @@ +use wasm_bindgen::{prelude::*, throw_val}; +use web_sys::*; + +use super::CONFIG; + +pub(crate) fn install() { + if CONFIG.dynamic_configuration { + let url = format!("{}/sse", CONFIG.lock_master.url); + let sse = EventSource::new(&url).unwrap(); + let sse2 = sse.clone(); + + // Setup SSE event listeners + let onopen = Closure::once_into_js(move || { + let onmessage = Closure::::new(move |ev: MessageEvent| { + console::log_2(&JsValue::from_str("Cedarling Received message: "), &ev); + unimplemented!("Dynamic Configuration Updates") + }) + .into_js_value(); + sse2.set_onmessage(Some(onmessage.as_ref().unchecked_ref())); + + let onerror = Closure::::new(move |ev: JsValue| throw_val(ev)).into_js_value(); + sse2.set_onerror(Some(onerror.as_ref().unchecked_ref())); + }); + + sse.set_onopen(Some(onopen.as_ref().unchecked_ref())); + } +} diff --git a/jans-lock/cedarling/src/startup/mod.rs b/jans-lock/cedarling/src/startup/mod.rs new file mode 100644 index 00000000000..d3affdbd315 --- /dev/null +++ b/jans-lock/cedarling/src/startup/mod.rs @@ -0,0 +1,148 @@ +use std::{borrow::Cow, collections::BTreeMap}; + +use cedar_policy::*; +use wasm_bindgen::{prelude::*, throw_str}; +use web_sys::*; + +use super::{http, CONFIG}; +use http::ResponseEx; + +mod statics; +mod types; + +pub async fn open_id_config(issuer_url: &'static str) -> &'static types::OpenIdConfiguration { + static mut OPENID_CONFIGURATION_STORE: BTreeMap<&'static str, types::OpenIdConfiguration> = BTreeMap::new(); + + unsafe { + match OPENID_CONFIGURATION_STORE.get(issuer_url) { + Some(config) => config, + None => { + // fetch OpenID Configuration + let res = http::get(issuer_url, &[]).await.expect_throw("Can't fetch OpenID Configuration"); + let config = res.into_json().await.expect_throw("Can't parse OpenID Configuration"); + + OPENID_CONFIGURATION_STORE.insert(issuer_url, config); + OPENID_CONFIGURATION_STORE.get(issuer_url).unwrap_unchecked() + } + } + } +} + +pub async fn get_policy_store(config: &types::OpenIdConfiguration) -> serde_json::Map { + // Get PolicyStore JSON + let source = match CONFIG.policy_store.strategy { + "local" => Cow::Borrowed(include_bytes!(concat!(env!("CARGO_MANIFEST_DIR"), "/policy-store/default.json")).as_slice()), + "remote" => { + let res = http::get(CONFIG.policy_store.uri, &[]).await.expect_throw("Unable fetch Policy Store"); + let bytes = res.into_bytes().await.expect_throw("Can't convert Policy Store response to String"); + Cow::Owned(bytes) + } + "lock-master" => { + let client: types::OpenIdDynamicClient = { + // OpenID dynamic client registration + let client_req = types::OpenIdDynamicClientRequest { + client_name: "Jans Cedarling", + application_type: "web", + redirect_uris: &[], + token_endpoint_auth_method: "client_secret_basic", + software_statement: CONFIG.lock_master.ssa_jwt, + contacts: &["newton@gluu.org"], + }; + + // send + let res = http::post(&config.registration_endpoint, http::PostBody::Json(client_req), &[]) + .await + .expect_throw("Unable to register client"); + res.into_json().await.expect_throw("Unable to parse client registration response") + }; + + let grant: types::OpenIdGrantResponse = { + // https://gluu.org/docs/gluu-server/4.0/admin-guide/oauth2/ + let token = if let Some(client_secret) = client.client_secret { + format!("{}:{}", client.client_id, client_secret) + } else { + console::warn_1(&JsValue::from_str("Client Secret is not provided")); + client.client_id + }; + + #[wasm_bindgen] + extern "C" { + #[wasm_bindgen(js_name = btoa)] + pub fn js_btoa(input: &str) -> String; + } + + let auth = format!("Basic {}", js_btoa(&token)); + + let grant = types::OpenIdGrantRequest { + scope: "cedarling", + grant_type: "client_credentials", + }; + + // send + let url = format!("{}/token", config.token_endpoint); + let headers = [("Authorization", auth.as_str())]; + let res = http::post(&url, http::PostBody::Form(&grant), &headers).await.expect_throw("Unable to get Access Token"); + + res.into_json().await.expect_throw("Unable to parse Access Token Response") + }; + + let buffer = { + let url = format!("{}/config?id={}", CONFIG.lock_master.url, CONFIG.lock_master.policy_store_id); + let auth = format!("Bearer {}", grant.access_token); + let res = http::get(&url, &[("Authorization", &auth)]).await.expect_throw("Unable to fetch policies from remote location"); + Cow::Owned(res.into_bytes().await.expect_throw("Unable to convert response to bytes")) + }; + + buffer + } + strategy => { + let msg = format!("Unknown Policy Store Strategy: {}", strategy); + throw_str(&msg) + } + }; + + // Decompress if necessary + let source = match CONFIG.decompress_policy_store { + true => Cow::Owned(miniz_oxide::inflate::decompress_to_vec_zlib(&source).unwrap_throw()), + false => source, + }; + + // Parse JSON + let policy_store = serde_json::from_slice::(&source).unwrap_throw(); + match policy_store { + serde_json::Value::Object(map) => map, + p => { + let error = format!("Expected top-level policy store to be an Object. Found: {:?}", p); + throw_str(&error) + } + } +} + +pub fn persist_policy_store_data(mut policy_store: serde_json::Map) { + // Load Schema + let schema = { + let schema = policy_store.remove("Schema").expect_throw("Can't find Schema in policy store"); + Schema::from_json_value(schema).unwrap_throw() + }; + + // Load PolicySet + let policies = { + let mut policies = policy_store.remove("PolicySet").expect_throw("Can't find PolicySet in policy store"); + let policies = policies.as_array_mut().expect_throw("expect_throw PolicySet to be an array"); + + let iter = policies.drain(..).map(|policy| Policy::from_json(None, policy).unwrap_throw()); + PolicySet::from_policies(iter).unwrap_throw() + }; + + // Load trusted issuers + let trusted_issuers = { + let mut issuers = policy_store.remove("TrustedIssuers").expect_throw("Can't find TrustedIssuers in policy store"); + let issuers = issuers.as_array_mut().expect_throw("expect_throw TrustedIssuers to be an array"); + issuers.drain(..).map(|issuer| serde_json::from_value(issuer).unwrap_throw()).collect::>() + }; + + // Persist PolicyStore data + statics::schema(Some(schema)); + statics::policies(Some(policies)); + statics::trusted_issuers(Some(trusted_issuers)); +} diff --git a/jans-lock/cedarling/src/startup/statics.rs b/jans-lock/cedarling/src/startup/statics.rs new file mode 100644 index 00000000000..dfe763fe7fc --- /dev/null +++ b/jans-lock/cedarling/src/startup/statics.rs @@ -0,0 +1,33 @@ +//! SAFETY: Webassembly is single-threaded, so this is safe. The statics are also scoped and the references are read only + +#![allow(clippy::option_map_unit_fn)] + +use super::types; +use cedar_policy::*; + +pub(crate) fn policies(swap: Option) -> &'static PolicySet { + static mut POLICIES: Option = None; + + unsafe { + swap.map(|s| POLICIES = Some(s)); + POLICIES.as_ref().unwrap() + } +} + +pub(crate) fn trusted_issuers(swap: Option>) -> &'static [types::TrustedIssuer] { + static mut ISSUERS: Vec = Vec::new(); + + unsafe { + swap.map(|s| ISSUERS = s); + ISSUERS.as_slice() + } +} + +pub(crate) fn schema(swap: Option) -> Option<&'static Schema> { + static mut SCHEMA: Option = None; + + unsafe { + swap.map(|s| SCHEMA = Some(s)); + SCHEMA.as_ref() + } +} diff --git a/jans-lock/cedarling/src/startup/types.rs b/jans-lock/cedarling/src/startup/types.rs new file mode 100644 index 00000000000..70a01eb4136 --- /dev/null +++ b/jans-lock/cedarling/src/startup/types.rs @@ -0,0 +1,42 @@ +#[derive(serde::Serialize, Debug)] +pub(crate) struct OpenIdDynamicClientRequest<'a> { + pub(crate) client_name: &'static str, + pub(crate) application_type: &'static str, + pub(crate) redirect_uris: &'a [&'static str], // basically a list of callback urls + pub(crate) token_endpoint_auth_method: &'static str, + pub(crate) software_statement: &'a str, + pub(crate) contacts: &'a [&'static str], +} + +#[derive(serde::Deserialize, Debug)] +pub(crate) struct TrustedIssuer { + pub(crate) name: String, + pub(crate) description: String, + pub(crate) openid_configuration_endpoint: String, +} + +#[derive(serde::Deserialize, Debug)] +pub struct OpenIdConfiguration { + pub(crate) issuer: String, + pub(crate) authorization_endpoint: String, + pub(crate) registration_endpoint: String, + pub(crate) token_endpoint: String, + pub(crate) jwks_uri: String, +} + +#[derive(serde::Deserialize, Debug)] +pub(crate) struct OpenIdDynamicClient { + pub(crate) client_id: String, + pub(crate) client_secret: Option, +} + +#[derive(serde::Serialize, Debug)] +pub(crate) struct OpenIdGrantRequest<'a> { + pub(crate) scope: &'a str, + pub(crate) grant_type: &'a str, +} + +#[derive(serde::Deserialize, Debug)] +pub(crate) struct OpenIdGrantResponse { + pub(crate) access_token: String, +} diff --git a/jans-lock/cedarling/src/types.rs b/jans-lock/cedarling/src/types.rs deleted file mode 100644 index fe201e3c492..00000000000 --- a/jans-lock/cedarling/src/types.rs +++ /dev/null @@ -1,32 +0,0 @@ -use wasm_bindgen::UnwrapThrowExt; - -#[derive(serde::Deserialize, serde::Serialize, Debug, Default)] -pub struct Tokens { - #[serde(rename = "access_token")] - pub(crate) access: String, - #[serde(rename = "id_token")] - pub(crate) id: String, - #[serde(rename = "userinfo_token")] - pub(crate) user_info: String, - #[serde(rename = "tx_token")] - pub(crate) tx: String, - #[serde(rename = "sse")] - pub(crate) sse_url: String, -} - -#[derive(serde::Deserialize, serde::Serialize)] -pub struct Request { - pub principal: String, - pub action: String, - pub resource: String, -} - -impl From for cedar_policy::Request { - fn from(value: Request) -> cedar_policy::Request { - let principal = value.principal.parse().unwrap_throw(); - let action = value.action.parse().unwrap_throw(); - let resource = value.resource.parse().unwrap_throw(); - - cedar_policy::Request::new(Some(principal), Some(action), Some(resource), cedar_policy::Context::empty(), None).unwrap_throw() - } -} diff --git a/jans-lock/cedarling/src/utils.rs b/jans-lock/cedarling/src/utils.rs deleted file mode 100644 index 20c7cffa795..00000000000 --- a/jans-lock/cedarling/src/utils.rs +++ /dev/null @@ -1,63 +0,0 @@ -use cedar_policy::Schema; -use wasm_bindgen::{prelude::*, throw_str}; -use wasm_bindgen_futures::*; -use web_sys::*; - -#[wasm_bindgen] -extern "C" { - #[wasm_bindgen(js_name = fetch)] - fn fetch_with_request_and_init(input: &Request, init: &RequestInit) -> js_sys::Promise; -} - -pub async fn get(url: &str) -> Option> { - let mut opts = RequestInit::new(); - opts.method("GET"); - opts.mode(RequestMode::NoCors); - - let request = Request::new_with_str(url).unwrap_throw(); - let resp_value = JsFuture::from(fetch_with_request_and_init(&request, &opts)).await.unwrap_throw(); - - let resp: Response = resp_value.dyn_into().unwrap_throw(); - if !resp.ok() { - return None; - } - - // extract text - let blob = resp.array_buffer().unwrap_throw(); - let blob = JsFuture::from(blob).await.unwrap_throw(); - - // Send the text response back to JS. - let array = js_sys::Uint8Array::new(&blob); - Some(array.to_vec()) -} - -pub async fn fetch_schema() -> cedar_policy::Schema { - let request = Request::new_with_str(crate::CONFIG.schema_url).unwrap_throw(); - - let mut opts = RequestInit::new(); - opts.method("GET"); - opts.mode(RequestMode::NoCors); - - request.headers().set("Accept", "application/vnd.github.v3.raw").unwrap_throw(); - let response_value = JsFuture::from(fetch_with_request_and_init(&request, &opts)).await.unwrap_throw(); - - let response: Response = response_value.dyn_into().unwrap_throw(); - if response.ok() { - let buffer = JsFuture::from(response.array_buffer().unwrap_throw()).await.unwrap_throw(); - let buffer = js_sys::Uint8Array::new(&buffer); - let buffer = buffer.to_vec(); - - let (schema, warnings) = Schema::from_file_natural(buffer.as_slice()).unwrap_throw(); - for warning in warnings { - let msg = format!("Schema Parser generated Warning: {}", warning); - let msg = JsValue::from_str(&msg); - console::warn_1(&msg) - } - - schema - } else { - let status_text = response.status_text(); - let error_message = format!("Failed to fetch Schema: {}", status_text); - throw_str(&error_message) - } -} diff --git a/jans-lock/cedarling/tests/fetch-open-id-config.rs b/jans-lock/cedarling/tests/fetch-open-id-config.rs new file mode 100644 index 00000000000..c1627282970 --- /dev/null +++ b/jans-lock/cedarling/tests/fetch-open-id-config.rs @@ -0,0 +1,8 @@ +use cedarling::open_id_config; +use wasm_bindgen_test::*; + +#[wasm_bindgen_test] +async fn test() { + let config = open_id_config("https://account.gluu.org/.well-known/openid-configuration").await; + console_log!("Fetched config: {:?}", &config); +} diff --git a/jans-lock/cedarling/tests/fetch-schema.rs b/jans-lock/cedarling/tests/fetch-schema.rs index 875c3334acd..9022ef9e6a9 100644 --- a/jans-lock/cedarling/tests/fetch-schema.rs +++ b/jans-lock/cedarling/tests/fetch-schema.rs @@ -1,8 +1,15 @@ -use cedarling::utils; +use cedar_policy::*; use wasm_bindgen_test::*; #[wasm_bindgen_test] async fn test() { - let schema = utils::fetch_schema().await; - console_log!("Fetched Schema: {:?}", &schema) + let mut policy_store = serde_json::from_str::(include_str!("../policy-store/default.json")).unwrap(); + let policy_store = policy_store.as_object_mut().expect("Expect top level policy store to be an object"); + + let _schema = { + let schema = policy_store.remove("Schema").expect("Can't find Schema in policy store"); + Schema::from_json_value(schema).unwrap() + }; + + console_log!("Loaded Schema: {:?}", _schema); } diff --git a/jans-lock/cedarling/tests/get-str.rs b/jans-lock/cedarling/tests/get-str.rs index 9bc743f9b8c..8e780a500be 100644 --- a/jans-lock/cedarling/tests/get-str.rs +++ b/jans-lock/cedarling/tests/get-str.rs @@ -1,10 +1,9 @@ -use cedarling::utils; -use wasm_bindgen::UnwrapThrowExt; +use cedarling::http::{self, ResponseEx}; use wasm_bindgen_test::*; #[wasm_bindgen_test] async fn test() { - let data = utils::get("https://site-scraper.sokorototo.workers.dev/").await.unwrap_throw(); - let string = std::str::from_utf8(&data).unwrap_throw(); + let data = http::get("https://site-scraper.sokorototo.workers.dev/", &[]).await.unwrap(); + let string = data.into_string().await.unwrap(); assert_eq!(string, "site-scraper v0.2.0"); } diff --git a/jans-lock/cedarling/tests/test-btoa.rs b/jans-lock/cedarling/tests/test-btoa.rs new file mode 100644 index 00000000000..394005dd557 --- /dev/null +++ b/jans-lock/cedarling/tests/test-btoa.rs @@ -0,0 +1,16 @@ +use wasm_bindgen::prelude::*; + +#[wasm_bindgen] +extern "C" { + #[wasm_bindgen(js_name = btoa)] + pub fn js_btoa(input: &str) -> String; +} + +use wasm_bindgen_test::*; + +#[wasm_bindgen_test] +async fn test() { + let input = "Hello, World!"; + let output = js_btoa(input); + assert_eq!(output, "SGVsbG8sIFdvcmxkIQ=="); +}