diff --git a/docker-jans-auth-server/Dockerfile b/docker-jans-auth-server/Dockerfile
index 5d66799b819..bacef4e943a 100644
--- a/docker-jans-auth-server/Dockerfile
+++ b/docker-jans-auth-server/Dockerfile
@@ -290,7 +290,8 @@ RUN chmod -R g=u ${JETTY_BASE}/jans-auth/custom \
&& chown -R 1000:0 /opt/prometheus \
&& chown 1000:0 ${JETTY_BASE}/jans-auth/webapps/jans-auth.xml \
&& chown -R 1000:0 ${JETTY_HOME}/temp \
- && chown -R 1000:0 ${JETTY_BASE}/jans-auth/_libs
+ && chown -R 1000:0 ${JETTY_BASE}/jans-auth/_libs \
+ && chown -R 1000:0 /app/templates
USER 1000
diff --git a/docker-jans-fido2/Dockerfile b/docker-jans-fido2/Dockerfile
index bab86f7852d..39e22e00a86 100644
--- a/docker-jans-fido2/Dockerfile
+++ b/docker-jans-fido2/Dockerfile
@@ -243,7 +243,8 @@ RUN chmod 664 ${JETTY_BASE}/jans-fido2/resources/log4j2.xml \
&& chown -R 1000:0 /usr/share/java \
&& chown -R 1000:0 /opt/prometheus \
&& chown 1000:0 ${JETTY_BASE}/jans-fido2/webapps/jans-fido2.xml \
- && chown -R 1000:0 ${JETTY_HOME}/temp
+ && chown -R 1000:0 ${JETTY_HOME}/temp \
+ && chown -R 1000:0 /app/templates
USER 1000
diff --git a/docker-jans-kc-scheduler/Dockerfile b/docker-jans-kc-scheduler/Dockerfile
index 79e977cd36c..6af6de15d85 100644
--- a/docker-jans-kc-scheduler/Dockerfile
+++ b/docker-jans-kc-scheduler/Dockerfile
@@ -160,7 +160,8 @@ RUN adduser -s /bin/sh -h /home/1000 -D -G root -u 1000 jans
RUN chmod -R g=u /etc/certs \
&& chmod -R g=u /etc/jans \
&& chmod 664 /opt/java/lib/security/cacerts \
- && chown -R 1000:0 /opt/kc-scheduler
+ && chown -R 1000:0 /opt/kc-scheduler \
+ && chown -R 1000:0 /app/templates
USER 1000
diff --git a/docker-jans-keycloak-link/Dockerfile b/docker-jans-keycloak-link/Dockerfile
index bd77b7f94b1..c934ad88811 100644
--- a/docker-jans-keycloak-link/Dockerfile
+++ b/docker-jans-keycloak-link/Dockerfile
@@ -237,7 +237,8 @@ RUN chmod 664 ${JETTY_BASE}/jans-keycloak-link/resources/log4j2.xml \
&& chown -R 1000:0 /opt/prometheus \
&& chown 1000:0 ${JETTY_BASE}/jans-keycloak-link/webapps/jans-keycloak-link.xml \
&& chown -R 1000:0 /var/jans/cr-snapshots \
- && chown -R 1000:0 ${JETTY_HOME}/temp
+ && chown -R 1000:0 ${JETTY_HOME}/temp \
+ && chown -R 1000:0 /app/templates
USER 1000
diff --git a/docker-jans-link/Dockerfile b/docker-jans-link/Dockerfile
index e24f91d95c5..6b62e6cd1a3 100644
--- a/docker-jans-link/Dockerfile
+++ b/docker-jans-link/Dockerfile
@@ -237,7 +237,8 @@ RUN chmod 664 ${JETTY_BASE}/jans-link/resources/log4j2.xml \
&& chown -R 1000:0 /opt/prometheus \
&& chown 1000:0 ${JETTY_BASE}/jans-link/webapps/jans-link.xml \
&& chown -R 1000:0 /var/jans/link-snapshots \
- && chown -R 1000:0 ${JETTY_HOME}/temp
+ && chown -R 1000:0 ${JETTY_HOME}/temp \
+ && chown -R 1000:0 /app/templates
USER 1000
diff --git a/docker-jans-persistence-loader/Dockerfile b/docker-jans-persistence-loader/Dockerfile
index f3359e2bdbc..e62f60b0e32 100644
--- a/docker-jans-persistence-loader/Dockerfile
+++ b/docker-jans-persistence-loader/Dockerfile
@@ -180,7 +180,8 @@ RUN adduser -s /bin/sh -h /home/1000 -D -G root -u 1000 1000
# adjust ownership and permission
RUN chmod -R g=u /app/custom_ldif \
&& chmod -R g=u /etc/certs \
- && chmod -R g=u /etc/jans
+ && chmod -R g=u /etc/jans \
+ && chown -R 1000:0 /app/templates
USER 1000
diff --git a/docker-jans-saml/Dockerfile b/docker-jans-saml/Dockerfile
index 38859e83419..4f9d6805c8a 100644
--- a/docker-jans-saml/Dockerfile
+++ b/docker-jans-saml/Dockerfile
@@ -203,7 +203,8 @@ RUN chmod -R g=u /etc/certs \
&& chown -R 1000:0 /opt/idp \
&& chown -R 1000:0 /usr/share/java \
&& chown -R 1000:0 /opt/keycloak/logs \
- && chown -R 1000:0 /opt/keycloak/conf
+ && chown -R 1000:0 /opt/keycloak/conf \
+ && chown -R 1000:0 /app/templates
USER 1000
diff --git a/docker-jans-scim/Dockerfile b/docker-jans-scim/Dockerfile
index 46b2e549272..d84e7164169 100644
--- a/docker-jans-scim/Dockerfile
+++ b/docker-jans-scim/Dockerfile
@@ -237,7 +237,8 @@ RUN chmod 664 ${JETTY_BASE}/jans-scim/resources/log4j2.xml \
&& chown -R 1000:0 /usr/share/java \
&& chown -R 1000:0 /opt/prometheus \
&& chown 1000:0 ${JETTY_BASE}/jans-scim/webapps/jans-scim.xml \
- && chown -R 1000:0 ${JETTY_HOME}/temp
+ && chown -R 1000:0 ${JETTY_HOME}/temp \
+ && chown -R 1000:0 /app/templates
USER 1000
diff --git a/docs/script-catalog/authorization_challenge/AgamaChallenge.java b/docs/script-catalog/authorization_challenge/AgamaChallenge.java
index 89fdade3277..6c272dff776 100644
--- a/docs/script-catalog/authorization_challenge/AgamaChallenge.java
+++ b/docs/script-catalog/authorization_challenge/AgamaChallenge.java
@@ -20,6 +20,7 @@
import io.jans.agama.engine.client.MiniBrowser;
import io.jans.as.model.configuration.AppConfiguration;
import io.jans.as.model.util.Base64Util;
+import io.jans.as.server.authorize.ws.rs.AuthzRequest;
import io.jans.util.*;
import jakarta.servlet.ServletRequest;
@@ -141,12 +142,14 @@ public boolean authorize(Object scriptContext) {
if (!CdiUtil.bean(FlowUtils.class).serviceEnabled())
return makeUnexpectedError(context, null, "Agama engine is disabled");
+
+ AuthzRequest authRequest = context.getAuthzRequest();
- if (!context.getAuthzRequest().isUseAuthorizationChallengeSession())
+ if (!authRequest.isUseAuthorizationChallengeSession())
return makeMissingParamError(context, "Please set 'use_auth_session=true' in your request");
ServletRequest servletRequest = context.getHttpRequest();
- AuthorizationChallengeSession deviceSessionObject = context.getAuthzRequest().getAuthorizationChallengeSessionObject();
+ AuthorizationChallengeSession deviceSessionObject = authRequest.getAuthorizationChallengeSessionObject();
boolean noSO = deviceSessionObject == null;
scriptLogger.debug("There IS{} device session object", noSO ? " NO" : "");
@@ -313,5 +316,23 @@ public int getApiVersion() {
public Map getAuthenticationMethodClaims(Object context) {
return Map.of();
}
-
+
+ @Override
+ public void prepareAuthzRequest(Object scriptContext) {
+
+ ExternalScriptContext context = (ExternalScriptContext) scriptContext;
+ AuthzRequest authRequest = context.getAuthzRequest();
+
+ AuthorizationChallengeSession sessionObject = authRequest.getAuthorizationChallengeSessionObject();
+ if (sessionObject != null) {
+ Map sessionAttributes = sessionObject.getAttributes().getAttributes();
+
+ // set scope from session into request object
+ String scopeFromSession = sessionAttributes.get("scope");
+ if (StringUtils.isNotBlank(scopeFromSession) && StringUtils.isBlank(authRequest.getScope())) {
+ authRequest.setScope(scopeFromSession);
+ }
+ }
+ }
+
}
diff --git a/docs/script-catalog/consent_gathering/sample-script/ConsentGatheringSample.py b/docs/script-catalog/consent_gathering/sample-script/ConsentGatheringSample.py
index 40576be19b1..f0a48a0a49d 100644
--- a/docs/script-catalog/consent_gathering/sample-script/ConsentGatheringSample.py
+++ b/docs/script-catalog/consent_gathering/sample-script/ConsentGatheringSample.py
@@ -30,7 +30,7 @@ def destroy(self, configurationAttributes):
return True
def getApiVersion(self):
- return 1
+ return 11
# Main consent-gather method. Must return True (if gathering performed successfully) or False (if fail).
# All user entered values can be access via Map context.getPageAttributes()
diff --git a/jans-auth-server/server/src/main/java/io/jans/as/server/authorize/ws/rs/AuthorizeAction.java b/jans-auth-server/server/src/main/java/io/jans/as/server/authorize/ws/rs/AuthorizeAction.java
index a5a96ce2de4..35384a724db 100644
--- a/jans-auth-server/server/src/main/java/io/jans/as/server/authorize/ws/rs/AuthorizeAction.java
+++ b/jans-auth-server/server/src/main/java/io/jans/as/server/authorize/ws/rs/AuthorizeAction.java
@@ -989,6 +989,20 @@ public String getClientDisplayName() {
}
final Client client = clientService.getClient(clientId);
+ return getCheckedClientDisplayName(client);
+ }
+
+ public String getClientDisplayName(final Client client) {
+ log.trace("client {}", client);
+
+ if (client == null) {
+ getClientDisplayName();
+ }
+
+ return getCheckedClientDisplayName(client);
+ }
+
+ private String getCheckedClientDisplayName(final Client client) {
if (StringUtils.isNotBlank(client.getClientName())) {
return client.getClientName();
}
@@ -998,7 +1012,7 @@ public String getClientDisplayName() {
}
return "Unknown";
- }
+ }
public String getAuthReqId() {
return authReqId;
diff --git a/jans-auth-server/server/src/main/java/io/jans/as/server/token/ws/rs/TokenRestWebServiceImpl.java b/jans-auth-server/server/src/main/java/io/jans/as/server/token/ws/rs/TokenRestWebServiceImpl.java
index b32651ec9f2..e51cf97022c 100644
--- a/jans-auth-server/server/src/main/java/io/jans/as/server/token/ws/rs/TokenRestWebServiceImpl.java
+++ b/jans-auth-server/server/src/main/java/io/jans/as/server/token/ws/rs/TokenRestWebServiceImpl.java
@@ -442,13 +442,14 @@ private Response processAuthorizationCode(String code, String scope, String code
executionContext.setGrant(authorizationCodeGrant);
log.trace("AuthorizationCodeGrant : '{}'", authorizationCodeGrant);
+ // if authorization code is not found then code was already used or wrong client provided = remove all grants with this auth code
+ tokenRestWebServiceValidator.validateGrant(authorizationCodeGrant, client, code, executionContext.getAuditLog(), grant -> grantService.removeAllByAuthorizationCode(code));
+
// validate redirectUri only for Authorization Code Flow. For First-Party App redirect uri is blank. It is perfectly valid case.
+ // redirect uri must be validated after grant is validated
if (!authorizationCodeGrant.isAuthorizationChallenge()) {
tokenRestWebServiceValidator.validateRedirectUri(redirectUri, executionContext.getAuditLog());
}
-
- // if authorization code is not found then code was already used or wrong client provided = remove all grants with this auth code
- tokenRestWebServiceValidator.validateGrant(authorizationCodeGrant, client, code, executionContext.getAuditLog(), grant -> grantService.removeAllByAuthorizationCode(code));
tokenRestWebServiceValidator.validatePKCE(authorizationCodeGrant, codeVerifier, executionContext.getAuditLog());
dPoPService.validateDpopThumprint(authorizationCodeGrant.getDpopJkt(), executionContext.getDpop());
diff --git a/jans-auth-server/server/src/main/webapp/WEB-INF/incl/layout/authorize-extended-template.xhtml b/jans-auth-server/server/src/main/webapp/WEB-INF/incl/layout/authorize-extended-template.xhtml
index e8b03eafc30..37ca0a66712 100644
--- a/jans-auth-server/server/src/main/webapp/WEB-INF/incl/layout/authorize-extended-template.xhtml
+++ b/jans-auth-server/server/src/main/webapp/WEB-INF/incl/layout/authorize-extended-template.xhtml
@@ -46,7 +46,7 @@
+ value="#{authorizeAction.getClientDisplayName(client)}" />
diff --git a/jans-cedarling/cedarling/src/log/interface.rs b/jans-cedarling/cedarling/src/log/interface.rs
index eb2a07bcb0e..be322c50285 100644
--- a/jans-cedarling/cedarling/src/log/interface.rs
+++ b/jans-cedarling/cedarling/src/log/interface.rs
@@ -25,6 +25,7 @@ pub(crate) trait LogWriter {
pub(crate) trait Loggable: serde::Serialize {
/// get unique request ID
fn get_request_id(&self) -> Uuid;
+
/// get log level for entity
/// not all log entities have log level, only when `log_kind` == `System`
fn get_log_level(&self) -> Option;
@@ -34,13 +35,8 @@ pub(crate) trait Loggable: serde::Serialize {
// is used to avoid boilerplate code
fn can_log(&self, logger_level: LogLevel) -> bool {
if let Some(entry_log_level) = self.get_log_level() {
- if entry_log_level < logger_level {
- // entry log level lower than logger level
- false
- } else {
- // entry log higher or equal than logger level
- true
- }
+ // higher level is more important, ie closer to fatal
+ logger_level <= entry_log_level
} else {
// if `.get_log_level` return None
// it means that `log_kind` != `System` and we should log it
diff --git a/jans-cedarling/cedarling/src/log/log_entry.rs b/jans-cedarling/cedarling/src/log/log_entry.rs
index b3d4d009a2e..46add8d97fc 100644
--- a/jans-cedarling/cedarling/src/log/log_entry.rs
+++ b/jans-cedarling/cedarling/src/log/log_entry.rs
@@ -356,7 +356,7 @@ impl Loggable for &DecisionLogEntry<'_> {
// TODO: maybe using wasm we can use `js_sys::Date::now()`
// 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 {
+pub fn gen_uuid7() -> Uuid {
use std::sync::{LazyLock, Mutex};
use uuid7::V7Generator;
diff --git a/jans-cedarling/cedarling/src/log/memory_logger.rs b/jans-cedarling/cedarling/src/log/memory_logger.rs
index e744a559ec6..154b1fe187d 100644
--- a/jans-cedarling/cedarling/src/log/memory_logger.rs
+++ b/jans-cedarling/cedarling/src/log/memory_logger.rs
@@ -13,12 +13,10 @@ use super::interface::{LogStorage, LogWriter, Loggable};
use crate::bootstrap_config::log_config::MemoryLogConfig;
const STORAGE_MUTEX_EXPECT_MESSAGE: &str = "MemoryLogger storage mutex should unlock";
-const STORAGE_JSON_PARSE_EXPECT_MESSAGE: &str =
- "In MemoryLogger storage value should be valid LogEntry json string";
/// A logger that store logs in-memory.
pub(crate) struct MemoryLogger {
- storage: Mutex,
+ storage: Mutex>,
log_level: LogLevel,
}
@@ -40,6 +38,44 @@ impl MemoryLogger {
}
}
+/// In case of failure in MemoryLogger, log to stderr where supported.
+/// On WASM, stderr is not supported, so log to whatever the wasm logger uses.
+mod fallback {
+ use crate::LogLevel;
+
+ /// conform to Loggable requirement imposed by LogStrategy
+ #[derive(serde::Serialize)]
+ struct StrWrap<'a>(&'a str);
+
+ impl crate::log::interface::Loggable for StrWrap<'_> {
+ fn get_request_id(&self) -> uuid7::Uuid {
+ crate::log::log_entry::gen_uuid7()
+ }
+
+ fn get_log_level(&self) -> Option {
+ // These must always be logged.
+ Some(LogLevel::TRACE)
+ }
+ }
+
+ /// Fetch the correct logger. That takes some work, and it's done on every
+ /// call. But this is a fallback logger, so it is not intended to be used
+ /// often, and in this case correctness and non-fallibility are far more
+ /// important than performance.
+ pub fn log(msg: &str) {
+ let log_config = crate::bootstrap_config::LogConfig{
+ log_type: crate::bootstrap_config::log_config::LogTypeConfig::StdOut,
+ // level is so that all messages passed here are logged.
+ log_level: LogLevel::TRACE,
+ };
+ // This should always be a LogStrategy::StdOut(StdOutLogger)
+ let log_strategy = crate::log::LogStrategy::new(&log_config);
+ use crate::log::interface::LogWriter;
+ // a string is always serializable
+ log_strategy.log_any(StrWrap(msg))
+ }
+}
+
// Implementation of LogWriter
impl LogWriter for MemoryLogger {
fn log_any(&self, entry: T) {
@@ -48,17 +84,22 @@ impl LogWriter for MemoryLogger {
return;
}
- let json_string = serde_json::json!(entry).to_string();
+ let json = match serde_json::to_value(&entry) {
+ Ok(json) => json,
+ Err(err) => {
+ fallback::log(&format!("could not serialize LogEntry to serde_json::Value: {err:?}"));
+ return;
+ },
+ };
- let result = self
+ let set_result = self
.storage
.lock()
.expect(STORAGE_MUTEX_EXPECT_MESSAGE)
- .set(entry.get_request_id().to_string().as_str(), &json_string);
+ .set(&entry.get_request_id().to_string(), json);
- if let Err(err) = result {
- // log error to stderr
- eprintln!("could not store LogEntry to memory: {err:?}");
+ if let Err(err) = set_result {
+ fallback::log(&format!("could not store LogEntry to memory: {err:?}"));
};
}
}
@@ -66,25 +107,20 @@ impl LogWriter for MemoryLogger {
// Implementation of LogStorage
impl LogStorage for MemoryLogger {
fn pop_logs(&self) -> Vec {
- // TODO: implement more efficient implementation
-
- let mut storage_guard = self.storage.lock().expect(STORAGE_MUTEX_EXPECT_MESSAGE);
-
- let keys = storage_guard.get_keys();
-
- 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())
- .expect(STORAGE_JSON_PARSE_EXPECT_MESSAGE))
+ self.storage
+ .lock()
+ .expect(STORAGE_MUTEX_EXPECT_MESSAGE)
+ .drain()
+ .map(|(_k, value)| value)
.collect()
}
fn get_log_by_id(&self, id: &str) -> Option {
- self.storage.lock().expect(STORAGE_MUTEX_EXPECT_MESSAGE)
+ 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))
+ .cloned()
}
fn get_log_ids(&self) -> Vec {
@@ -211,4 +247,39 @@ mod tests {
"Logs were not fully popped"
);
}
+
+ #[test]
+ fn fallback_logger() {
+ struct FailSerialize;
+
+ impl serde::Serialize for FailSerialize {
+ fn serialize(&self, _serializer: S) -> Result
+ where S: serde::Serializer {
+ Err(serde::ser::Error::custom("this always fails"))
+ }
+ }
+
+ impl crate::log::interface::Loggable for FailSerialize {
+ fn get_request_id(&self) -> uuid7::Uuid {
+ crate::log::log_entry::gen_uuid7()
+ }
+
+ fn get_log_level(&self) -> Option {
+ // These must always be logged.
+ Some(LogLevel::TRACE)
+ }
+ }
+
+ let logger = create_memory_logger();
+ logger.log_any(FailSerialize);
+
+ // There isn't a good way, in unit tests, to verify the output was
+ // actually written to stderr/json console.
+ //
+ // To eyeball-verify it:
+ // cargo test -- --nocapture fall
+ // and look in the output for
+ // "could not serialize LogEntry to serde_json::Value: Error(\"this always fails\", line: 0, column: 0)"
+ assert!(logger.pop_logs().is_empty(), "logger should be empty");
+ }
}
diff --git a/jans-cedarling/sparkv/Cargo.toml b/jans-cedarling/sparkv/Cargo.toml
index ec0f51db975..4347be0c19d 100644
--- a/jans-cedarling/sparkv/Cargo.toml
+++ b/jans-cedarling/sparkv/Cargo.toml
@@ -14,3 +14,6 @@ homepage = "https://crates.io/crates/sparkv"
[dependencies]
thiserror = { workspace = true }
chrono = { workspace = true }
+
+[dev-dependencies]
+serde_json = "*"
diff --git a/jans-cedarling/sparkv/README.md b/jans-cedarling/sparkv/README.md
index b7278655800..ba6c66a180a 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", chrono::Duration::new(60, 0));
+sparkv.set_with_ttl("diff-ttl", "your-value", chrono::Duration::seconds(60));
```
See `config.rs` for more configuration options.
diff --git a/jans-cedarling/sparkv/src/config.rs b/jans-cedarling/sparkv/src/config.rs
index d1f0c966c5c..0e3899a615c 100644
--- a/jans-cedarling/sparkv/src/config.rs
+++ b/jans-cedarling/sparkv/src/config.rs
@@ -21,8 +21,8 @@ impl Config {
Config {
max_items: 10_000,
max_item_size: 500_000,
- max_ttl: Duration::new(60 * 60, 0).expect("a valid duration"),
- default_ttl: Duration::new(5 * 60, 0).expect("a valid duration"), // 5 minutes
+ max_ttl: Duration::seconds(60 * 60),
+ default_ttl: Duration::seconds(5 * 60), // 5 minutes
auto_clear_expired: true,
}
}
@@ -43,14 +43,8 @@ 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,
- Duration::new(60 * 60, 0).expect("a valid duration")
- );
- assert_eq!(
- config.default_ttl,
- Duration::new(5 * 60, 0).expect("a valid duration")
- );
+ assert_eq!(config.max_ttl, Duration::seconds(60 * 60));
+ assert_eq!(config.default_ttl, Duration::seconds(5 * 60));
assert!(config.auto_clear_expired);
}
}
diff --git a/jans-cedarling/sparkv/src/expentry.rs b/jans-cedarling/sparkv/src/expentry.rs
index a702962a93e..a4a1cd2e008 100644
--- a/jans-cedarling/sparkv/src/expentry.rs
+++ b/jans-cedarling/sparkv/src/expentry.rs
@@ -16,15 +16,15 @@ pub struct ExpEntry {
}
impl ExpEntry {
- pub fn new(key: &str, expiration: Duration) -> Self {
+ pub fn new>(key: S, expiration: Duration) -> Self {
let expired_at: DateTime = Utc::now() + expiration;
Self {
- key: String::from(key),
+ key: key.as_ref().into(),
expired_at,
}
}
- pub fn from_kv_entry(kv_entry: &KvEntry) -> Self {
+ pub fn from_kv_entry(kv_entry: &KvEntry) -> Self {
Self {
key: kv_entry.key.clone(),
expired_at: kv_entry.expired_at,
@@ -59,19 +59,15 @@ mod tests {
#[test]
fn test_new() {
- let item = ExpEntry::new("key", Duration::new(10, 0).expect("a valid duration"));
+ let item = ExpEntry::new("key", Duration::seconds(10));
assert_eq!(item.key, "key");
- 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"));
+ assert!(item.expired_at > Utc::now() + Duration::seconds(9));
+ assert!(item.expired_at <= Utc::now() + Duration::seconds(10));
}
#[test]
fn test_from_kventry() {
- let kv_entry = KvEntry::new(
- "keyFromKV",
- "value from KV",
- Duration::new(10, 0).expect("a valid duration"),
- );
+ let kv_entry = KvEntry::new("keyFromKV", "value from KV", Duration::seconds(10));
let exp_item = ExpEntry::from_kv_entry(&kv_entry);
assert_eq!(exp_item.key, "keyFromKV");
assert_eq!(exp_item.expired_at, kv_entry.expired_at);
@@ -79,15 +75,15 @@ mod tests {
#[test]
fn test_cmp() {
- 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"));
+ let item_small = ExpEntry::new("k1", Duration::seconds(10));
+ let item_big = ExpEntry::new("k2", Duration::seconds(8000));
assert!(item_small > item_big); // reverse order
assert!(item_big < item_small); // reverse order
}
#[test]
fn test_is_expired() {
- let item = ExpEntry::new("k1", Duration::new(0, 100).expect("a valid duration"));
+ let item = ExpEntry::new("k1", Duration::seconds(0));
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 8fd8efbe6aa..c8e38c62ee5 100644
--- a/jans-cedarling/sparkv/src/kventry.rs
+++ b/jans-cedarling/sparkv/src/kventry.rs
@@ -8,18 +8,18 @@ use chrono::Duration;
use chrono::prelude::*;
#[derive(Debug, Clone, PartialEq, Eq)]
-pub struct KvEntry {
+pub struct KvEntry {
pub key: String,
- pub value: String,
+ pub value: T,
pub expired_at: DateTime,
}
-impl KvEntry {
- pub fn new(key: &str, value: &str, expiration: Duration) -> Self {
+impl KvEntry {
+ pub fn new>(key: S, value: T, expiration: Duration) -> Self {
let expired_at: DateTime = Utc::now() + expiration;
Self {
- key: String::from(key),
- value: String::from(value),
+ key: key.as_ref().into(),
+ value,
expired_at,
}
}
@@ -31,14 +31,10 @@ mod tests {
#[test]
fn test_new() {
- let item = KvEntry::new(
- "key",
- "value",
- Duration::new(10, 0).expect("a valid duration"),
- );
+ let item = KvEntry::::new("key", "value".into(), Duration::seconds(10));
assert_eq!(item.key, "key");
assert_eq!(item.value, "value");
- 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"));
+ assert!(item.expired_at > Utc::now() + Duration::seconds(9));
+ assert!(item.expired_at <= Utc::now() + Duration::seconds(10));
}
}
diff --git a/jans-cedarling/sparkv/src/lib.rs b/jans-cedarling/sparkv/src/lib.rs
index b5bdd8ca9e9..c44c5750f2e 100644
--- a/jans-cedarling/sparkv/src/lib.rs
+++ b/jans-cedarling/sparkv/src/lib.rs
@@ -18,13 +18,59 @@ pub use kventry::KvEntry;
use chrono::Duration;
use chrono::prelude::*;
-pub struct SparKV {
+pub struct SparKV {
pub config: Config,
- data: std::collections::BTreeMap,
+ data: std::collections::BTreeMap>,
expiries: std::collections::BinaryHeap,
+ /// An optional function that calculates the memory size of a value.
+ ///
+ /// Used by `ensure_item_size`.
+ ///
+ /// If this function is not provided, the container will enforce
+ /// `Config.max_item_size` on the basis of `std::mem::size_of_val` which
+ /// probably won't be what you expect.
+ size_calculator: Option usize>,
}
-impl SparKV {
+/// See the SparKV::iter function
+pub struct Iter<'a, T: 'a> {
+ btree_value_iter: std::collections::btree_map::Values<'a, String, KvEntry>,
+}
+
+impl<'a, T> Iterator for Iter<'a, T> {
+ type Item = (&'a String, &'a T);
+
+ fn next(&mut self) -> Option {
+ self.btree_value_iter
+ .next()
+ .map(|kventry| (&kventry.key, &kventry.value))
+ }
+
+ fn size_hint(&self) -> (usize, Option) {
+ self.btree_value_iter.size_hint()
+ }
+}
+
+/// See the SparKV::drain function
+pub struct DrainIter {
+ value_iter: std::collections::btree_map::IntoValues>,
+}
+
+impl Iterator for DrainIter {
+ type Item = (String, T);
+
+ fn next(&mut self) -> Option {
+ self.value_iter
+ .next()
+ .map(|kventry| (kventry.key, kventry.value))
+ }
+
+ fn size_hint(&self) -> (usize, Option) {
+ self.value_iter.size_hint()
+ }
+}
+
+impl SparKV {
pub fn new() -> Self {
let config = Config::new();
SparKV::with_config(config)
@@ -35,53 +81,76 @@ impl SparKV {
config,
data: std::collections::BTreeMap::new(),
expiries: std::collections::BinaryHeap::new(),
+ // This will underestimate the size of most things.
+ size_calculator: Some(|v| std::mem::size_of_val(v)),
}
}
- pub fn set(&mut self, key: &str, value: &str) -> Result<(), Error> {
+ /// Provide optional size function. See SparKV.size_calculator comments.
+ pub fn with_config_and_sizer(config: Config, sizer: Option usize>) -> Self {
+ SparKV {
+ config,
+ data: std::collections::BTreeMap::new(),
+ expiries: std::collections::BinaryHeap::new(),
+ size_calculator: sizer,
+ }
+ }
+
+ pub fn set(&mut self, key: &str, value: T) -> Result<(), Error> {
self.set_with_ttl(key, value, self.config.default_ttl)
}
- pub fn set_with_ttl(&mut self, key: &str, value: &str, ttl: Duration) -> Result<(), Error> {
+ pub fn set_with_ttl(&mut self, key: &str, value: T, ttl: Duration) -> Result<(), Error> {
self.clear_expired_if_auto();
self.ensure_capacity_ignore_key(key)?;
- self.ensure_item_size(value)?;
+ self.ensure_item_size(&value)?;
self.ensure_max_ttl(ttl)?;
- let item: KvEntry = KvEntry::new(key, value, ttl);
+ let item: KvEntry = KvEntry::new(key, value, ttl);
let exp_item: ExpEntry = ExpEntry::from_kv_entry(&item);
self.expiries.push(exp_item);
- self.data.insert(item.key.clone(), item);
+ self.data.insert(key.into(), item);
Ok(())
}
- pub fn get(&self, key: &str) -> Option {
- let item = self.get_item(key)?;
- Some(item.value.clone())
+ pub fn get(&self, key: &str) -> Option<&T> {
+ Some(&self.get_item(key)?.value)
}
// Only returns if it is not yet expired
- pub fn get_item(&self, key: &str) -> Option<&KvEntry> {
+ pub fn get_item(&self, key: &str) -> Option<&KvEntry> {
let item = self.data.get(key)?;
- if item.expired_at > Utc::now() {
- Some(item)
- } else {
- None
- }
+ (item.expired_at > Utc::now()).then_some(item)
}
pub fn get_keys(&self) -> Vec {
- self.data
- .keys()
- .map(|key| key.to_string())// it clone the string
- .collect()
+ self.data.keys().cloned().collect()
+ }
+
+ /// Return an iterator of (key,value) : (&String,&T).
+ pub fn iter(&self) -> Iter {
+ Iter {
+ btree_value_iter: self.data.values(),
+ }
+ }
+
+ /// Return an iterator of (key,value) : (String,T) which empties the container.
+ /// All entries will be owned by the iterator, and yielded entries will not be checked against expiry.
+ /// All entries and expiries will be cleared.
+ pub fn drain(&mut self) -> DrainIter {
+ // assume that slightly-expired entries should be returned.
+ self.expiries.clear();
+ let data_only = std::mem::take(&mut self.data);
+ DrainIter {
+ value_iter: data_only.into_values(),
+ }
}
- pub fn pop(&mut self, key: &str) -> Option {
+ pub fn pop(&mut self, key: &str) -> Option {
self.clear_expired_if_auto();
let item = self.data.remove(key)?;
- // Does not delete from BinaryHeap as it's expensive.
+ // Does not delete expiry entry from BinaryHeap as it's expensive.
Some(item.value)
}
@@ -90,7 +159,7 @@ impl SparKV {
}
pub fn is_empty(&self) -> bool {
- self.data.len() == 0
+ self.data.is_empty()
}
pub fn contains_key(&self, key: &str) -> bool {
@@ -99,29 +168,27 @@ impl SparKV {
pub fn clear_expired(&mut self) -> usize {
let mut cleared_count: usize = 0;
- loop {
- let peeked = self.expiries.peek().cloned();
- match peeked {
- Some(exp_item) => {
- if exp_item.is_expired() {
- let kv_entry = self.data.get(&exp_item.key).unwrap();
- if kv_entry.key == exp_item.key
- && kv_entry.expired_at == exp_item.expired_at
- {
- cleared_count += 1;
- self.pop(&exp_item.key);
- }
- self.expiries.pop();
- } else {
- break;
- }
- },
- None => break,
+ while let Some(exp_item) = self.expiries.peek().cloned() {
+ if exp_item.is_expired() {
+ let kv_entry = self.data.get(&exp_item.key).unwrap();
+ if kv_entry.key == exp_item.key && kv_entry.expired_at == exp_item.expired_at {
+ cleared_count += 1;
+ self.pop(&exp_item.key);
+ }
+ self.expiries.pop();
+ } else {
+ break;
}
}
cleared_count
}
+ /// Empty the container. That is, remove all key-values and expiries.
+ pub fn clear(&mut self) {
+ self.data.clear();
+ self.expiries.clear();
+ }
+
fn clear_expired_if_auto(&mut self) {
if self.config.auto_clear_expired {
self.clear_expired();
@@ -142,9 +209,11 @@ impl SparKV {
self.ensure_capacity()
}
- fn ensure_item_size(&self, value: &str) -> Result<(), Error> {
- if value.len() > self.config.max_item_size {
- return Err(Error::ItemSizeExceeded);
+ fn ensure_item_size(&self, value: &T) -> Result<(), Error> {
+ if let Some(calc) = self.size_calculator {
+ if calc(value) > self.config.max_item_size {
+ return Err(Error::ItemSizeExceeded);
+ }
}
Ok(())
}
@@ -157,275 +226,14 @@ impl SparKV {
}
}
-impl Default for SparKV {
+impl Default for SparKV {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
-mod tests {
- use super::*;
-
- #[test]
- fn test_sparkv_config() {
- 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,
- Duration::new(60 * 60, 0).expect("a valid duration")
- );
- }
-
- #[test]
- fn test_sparkv_new_with_config() {
- let config: Config = Config::new();
- let sparkv = SparKV::with_config(config);
- assert_eq!(sparkv.config, config);
- }
-
- #[test]
- fn test_len_is_empty() {
- let mut sparkv = SparKV::new();
- assert_eq!(sparkv.len(), 0);
- assert!(sparkv.is_empty());
-
- _ = sparkv.set("keyA", "value");
- assert_eq!(sparkv.len(), 1);
- assert!(!sparkv.is_empty());
- }
-
- #[test]
- fn test_set_get() {
- let mut sparkv = SparKV::new();
- _ = sparkv.set("keyA", "value");
- assert_eq!(sparkv.get("keyA"), Some(String::from("value")));
- assert_eq!(sparkv.expiries.len(), 1);
-
- // Overwrite the value
- _ = sparkv.set("keyA", "value2");
- assert_eq!(sparkv.get("keyA"), Some(String::from("value2")));
- assert_eq!(sparkv.expiries.len(), 2);
-
- assert!(sparkv.get("non-existent").is_none());
- }
-
- #[test]
- fn test_get_item() {
- let mut sparkv = SparKV::new();
- 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();
-
- assert!(get_result.is_some());
- assert_eq!(unwrapped.key, "keyARaw");
- assert_eq!(unwrapped.value, "value99");
-
- assert!(sparkv.get_item("non-existent").is_none());
- }
-
- #[test]
- fn test_get_item_return_none_if_expired() {
- let mut sparkv = SparKV::new();
- _ = sparkv.set_with_ttl(
- "key",
- "value",
- Duration::new(0, 40000).expect("a valid duration"),
- );
- assert_eq!(sparkv.get("key"), Some(String::from("value")));
-
- std::thread::sleep(std::time::Duration::from_nanos(50000));
- assert_eq!(sparkv.get("key"), None);
- }
-
- #[test]
- fn test_set_should_fail_if_capacity_exceeded() {
- let mut config: Config = Config::new();
- config.max_items = 2;
-
- let mut sparkv = SparKV::with_config(config);
- let mut set_result = sparkv.set("keyA", "value");
- assert!(set_result.is_ok());
- assert_eq!(sparkv.get("keyA"), Some(String::from("value")));
-
- set_result = sparkv.set("keyB", "value2");
- assert!(set_result.is_ok());
+mod tests;
- set_result = sparkv.set("keyC", "value3");
- assert!(set_result.is_err());
- assert_eq!(set_result.unwrap_err(), Error::CapacityExceeded);
- assert!(sparkv.get("keyC").is_none());
-
- // Overwrite existing key should not err
- set_result = sparkv.set("keyB", "newValue1234");
- assert!(set_result.is_ok());
- assert_eq!(sparkv.get("keyB"), Some(String::from("newValue1234")));
- }
-
- #[test]
- fn test_set_with_ttl() {
- let mut sparkv = SparKV::new();
- _ = sparkv.set("longest", "value");
- _ = 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")));
- assert!(
- sparkv.get_item("longer").unwrap().expired_at
- > sparkv.get_item("shorter").unwrap().expired_at
- );
- assert!(
- sparkv.get_item("longest").unwrap().expired_at
- > sparkv.get_item("longer").unwrap().expired_at
- );
- }
-
- #[test]
- fn test_ensure_max_ttl() {
- let mut config: Config = Config::new();
- 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",
- 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",
- 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",
- 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);
- }
-
- #[test]
- fn test_delete() {
- let mut sparkv = SparKV::new();
- _ = sparkv.set("keyA", "value");
- assert_eq!(sparkv.get("keyA"), Some(String::from("value")));
- assert_eq!(sparkv.expiries.len(), 1);
-
- let deleted_value = sparkv.pop("keyA");
- assert_eq!(deleted_value, Some(String::from("value")));
- assert!(sparkv.get("keyA").is_none());
- assert_eq!(sparkv.expiries.len(), 1); // it does not delete
- }
-
- #[test]
- fn test_clear_expired() {
- 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",
- 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]
- fn test_clear_expired_with_overwritten_key() {
- 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",
- 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);
-
- let cleared_count = sparkv.clear_expired();
- assert_eq!(cleared_count, 0); // no longer expiring
- assert_eq!(sparkv.expiries.len(), 2); // should have cleared the expiries
- assert_eq!(sparkv.len(), 2); // but not actually deleting
- }
-
- #[test]
- fn test_clear_expired_with_auto_clear_expired_enabled() {
- 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",
- 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 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
- }
-}
+#[cfg(test)]
+mod test_json_value;
diff --git a/jans-cedarling/sparkv/src/test_json_value.rs b/jans-cedarling/sparkv/src/test_json_value.rs
new file mode 100644
index 00000000000..f9e0afc1712
--- /dev/null
+++ b/jans-cedarling/sparkv/src/test_json_value.rs
@@ -0,0 +1,177 @@
+// 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 crate::Config;
+use crate::SparKV;
+use serde_json;
+
+#[cfg(test)]
+fn json_value_size(value: &serde_json::Value) -> usize {
+ std::mem::size_of::()
+ + match value {
+ serde_json::Value::Null => 0,
+ serde_json::Value::Bool(_) => 0,
+ serde_json::Value::Number(_) => 0, // Incorrect if arbitrary_precision is enabled. oh well
+ serde_json::Value::String(s) => s.capacity(),
+ serde_json::Value::Array(a) => {
+ a.iter().map(json_value_size).sum::()
+ + a.capacity() * std::mem::size_of::()
+ },
+ serde_json::Value::Object(o) => o
+ .iter()
+ .map(|(k, v)| {
+ std::mem::size_of::()
+ + k.capacity()
+ + json_value_size(v)
+ + std::mem::size_of::() * 3 // As a crude approximation, I pretend each map entry has 3 words of overhead
+ })
+ .sum(),
+ }
+}
+
+fn first_json() -> serde_json::Value {
+ serde_json::json!({
+ "name" : "first_json",
+ "compile_kind": 0,
+ "config": 3355035640151825893usize,
+ "declared_features": ["bstr", "bytes", "default", "inline", "serde", "text", "unicode", "unicode-segmentation"],
+ "deps": [],
+ "features": ["default", "text"],
+ "local": [
+ {
+ "CheckDepInfo": {
+ "checksum": false,
+ "dep_info": "debug/.fingerprint/similar-056a66f4ad898c88/dep-lib-similar"
+ }
+ }
+ ],
+ "metadata": 943206097653546126i64,
+ "path": 7620609427446831929u64,
+ "profile": 10243973527296709326usize,
+ "rustc": 11594289678289209806usize,
+ "rustflags": [
+ "-C",
+ "link-arg=-fuse-ld=/usr/bin/mold"
+ ],
+ "target": 15605724903113465739u64
+ })
+}
+
+fn second_json() -> serde_json::Value {
+ serde_json::json!({
+ "name" : "second_json",
+ "compile_kind": 0,
+ "config": 5533035641051825893usize,
+ "declared_features": ["bstr", "bytes", "default", "inline", "serde", "text", "unicode", "unicode-segmentation"],
+ "deps": [],
+ "features": ["default", "text"],
+ "local": [
+ {
+ "CheckDepInfo": {
+ "checksum": false,
+ "dep_info": "debug/.fingerprint/utterly-different-0a6664d898c8f8a5/dep-lib-utterly-different"
+ }
+ }
+ ],
+ "metadata": 943206097653546126i64,
+ "path": 7620609427446831929u64,
+ "profile": 10243973527296709326usize,
+ "rustc": 11594289678289209806usize,
+ "rustflags": [
+ "-C",
+ "link-arg=-fuse-ld=/usr/bin/mold"
+ ],
+ "target": 15605724903113465739u64
+ })
+}
+
+#[test]
+fn simple_serde_json() {
+ let config: Config = Config::new();
+ let mut sparkv =
+ SparKV::::with_config_and_sizer(config, Some(json_value_size));
+ let json = first_json();
+ sparkv.set("first", json.clone()).unwrap();
+ let stored_first = sparkv.get("first").unwrap();
+ assert_eq!(&json, stored_first);
+}
+
+#[test]
+fn type_serde_json() {
+ let config: Config = Config::new();
+ let mut sparkv =
+ SparKV::::with_config_and_sizer(config, Some(json_value_size));
+ let json = first_json();
+ sparkv.set("first", json.clone()).unwrap();
+
+ // now make sure it's actually stored as the value, not as a String
+ let kv = sparkv.get_item("first").unwrap();
+ use std::any::{Any, TypeId};
+ assert_eq!(kv.value.type_id(), TypeId::of::());
+}
+
+#[test]
+fn fails_size_calculator() {
+ // create this first, so we know what item size is too large
+ let json = first_json();
+
+ let mut config: Config = Config::new();
+ // set item size to something smaller than item
+ config.max_item_size = json_value_size(&json) / 2;
+ let mut sparkv =
+ SparKV::::with_config_and_sizer(config, Some(json_value_size));
+
+ let should_be_error = sparkv.set("first", json.clone());
+ assert_eq!(should_be_error, Err(crate::Error::ItemSizeExceeded));
+}
+
+#[test]
+fn two_json_items() {
+ let mut sparkv = SparKV::::new();
+ sparkv.set("first", first_json()).unwrap();
+ sparkv.set("second", second_json()).unwrap();
+
+ let fj = sparkv.get("first").unwrap();
+ assert_eq!(
+ fj.pointer("/name").unwrap(),
+ &serde_json::Value::String("first_json".into())
+ );
+
+ let sj = sparkv.get("second").unwrap();
+ assert_eq!(
+ sj.pointer("/name").unwrap(),
+ &serde_json::Value::String("second_json".into())
+ );
+}
+
+#[test]
+fn drain_all_json_items() {
+ let mut sparkv = SparKV::::new();
+ sparkv.set("first", first_json()).unwrap();
+ sparkv.set("second", second_json()).unwrap();
+
+ let all_items = sparkv.drain();
+ let all_values = all_items.map(|(_, v)| v).collect::>();
+ assert_eq!(all_values, vec![first_json(), second_json()]);
+
+ assert!(sparkv.is_empty(), "sparkv not empty");
+}
+
+#[test]
+fn rc_json_items() {
+ use std::rc::Rc;
+ let mut sparkv = SparKV::>::new();
+ sparkv.set("first", Rc::new(first_json())).unwrap();
+ sparkv.set("second", Rc::new(second_json())).unwrap();
+
+ let all_items = sparkv.drain();
+ let all_values = all_items.map(|(_, v)| v).collect::>();
+ assert_eq!(all_values, vec![
+ Rc::new(first_json()),
+ Rc::new(second_json())
+ ]);
+
+ assert!(sparkv.is_empty(), "sparkv not empty");
+}
diff --git a/jans-cedarling/sparkv/src/tests.rs b/jans-cedarling/sparkv/src/tests.rs
new file mode 100644
index 00000000000..848fdd9db98
--- /dev/null
+++ b/jans-cedarling/sparkv/src/tests.rs
@@ -0,0 +1,266 @@
+// 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 crate::*;
+
+#[test]
+fn test_sparkv_config() {
+ 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, Duration::seconds(60 * 60));
+}
+
+#[test]
+fn test_sparkv_new_with_config() {
+ let config: Config = Config::new();
+ let sparkv = SparKV::::with_config(config);
+ assert_eq!(sparkv.config, config);
+}
+
+#[test]
+fn test_len_is_empty() {
+ let mut sparkv = SparKV::::new();
+ assert_eq!(sparkv.len(), 0);
+ assert!(sparkv.is_empty());
+
+ _ = sparkv.set("keyA", "value".to_string());
+ assert_eq!(sparkv.len(), 1);
+ assert!(!sparkv.is_empty());
+}
+
+#[test]
+fn test_set_get() {
+ let mut sparkv = SparKV::::new();
+ _ = sparkv.set("keyA", "value".into());
+ assert_eq!(sparkv.get("keyA"), Some(&String::from("value")));
+ assert_eq!(sparkv.expiries.len(), 1);
+
+ // Overwrite the value
+ _ = sparkv.set("keyA", "value2".into());
+ assert_eq!(sparkv.get("keyA"), Some(&String::from("value2")));
+ assert_eq!(sparkv.expiries.len(), 2);
+
+ assert!(sparkv.get("non-existent").is_none());
+}
+
+#[test]
+fn test_get_item() {
+ let mut sparkv = SparKV::new();
+ let item = KvEntry::new("keyARaw", "value99", Duration::seconds(1));
+ sparkv.data.insert(item.key.clone(), item);
+ let get_result = sparkv.get_item("keyARaw");
+ let unwrapped = get_result.unwrap();
+
+ assert!(get_result.is_some());
+ assert_eq!(unwrapped.key, "keyARaw");
+ assert_eq!(unwrapped.value, "value99");
+
+ assert!(sparkv.get_item("non-existent").is_none());
+}
+
+#[test]
+fn test_get_item_return_none_if_expired() {
+ let mut sparkv = SparKV::new();
+ _ = sparkv.set_with_ttl("key", "value", Duration::microseconds(40));
+ assert_eq!(sparkv.get("key"), Some(&"value"));
+
+ std::thread::sleep(std::time::Duration::from_micros(80));
+ assert_eq!(sparkv.get("key"), None);
+}
+
+#[test]
+fn test_set_should_fail_if_capacity_exceeded() {
+ let mut config: Config = Config::new();
+ config.max_items = 2;
+
+ let mut sparkv = SparKV::::with_config(config);
+ let mut set_result = sparkv.set("keyA", "value".to_string());
+ assert!(set_result.is_ok());
+ assert_eq!(sparkv.get("keyA"), Some(&String::from("value")));
+
+ set_result = sparkv.set("keyB", "value2".to_string());
+ assert!(set_result.is_ok());
+
+ set_result = sparkv.set("keyC", "value3".to_string());
+ assert!(set_result.is_err());
+ assert_eq!(set_result.unwrap_err(), Error::CapacityExceeded);
+ assert!(sparkv.get("keyC").is_none());
+
+ // Overwrite existing key should not err
+ set_result = sparkv.set("keyB", "newValue1234".to_string());
+ assert!(set_result.is_ok());
+ assert_eq!(sparkv.get("keyB"), Some(&String::from("newValue1234")));
+}
+
+#[test]
+fn memsize_item_capacity_exceeded() {
+ let value: String = "jay".into();
+
+ let mut config: Config = Config::new();
+ config.max_item_size = std::mem::size_of_val(&value) / 2;
+ let mut sparkv = SparKV::::with_config(config);
+
+ let error = sparkv.set("blue", value);
+ assert_eq!(error, Err(crate::Error::ItemSizeExceeded));
+}
+
+#[test]
+fn custom_item_capacity_exceeded() {
+ let mut config: Config = Config::new();
+ config.max_item_size = 20;
+ let mut sparkv = SparKV::<&str>::with_config_and_sizer(config, Some(|s| s.len()));
+
+ assert_eq!(Ok(()), sparkv.set("short", "value"));
+ assert_eq!(
+ Err(crate::Error::ItemSizeExceeded),
+ sparkv.set("long", "This is a value that exceeds 20 characters")
+ );
+}
+
+#[test]
+fn test_set_with_ttl() {
+ let mut sparkv = SparKV::::new();
+ _ = sparkv.set("longest", "value".into());
+ _ = sparkv.set_with_ttl("longer", "value".into(), Duration::seconds(2));
+ _ = sparkv.set_with_ttl("shorter", "value".into(), Duration::seconds(1));
+
+ assert_eq!(sparkv.get("longer"), Some(&String::from("value")));
+ assert_eq!(sparkv.get("shorter"), Some(&String::from("value")));
+ assert!(
+ sparkv.get_item("longer").unwrap().expired_at
+ > sparkv.get_item("shorter").unwrap().expired_at
+ );
+ assert!(
+ sparkv.get_item("longest").unwrap().expired_at
+ > sparkv.get_item("longer").unwrap().expired_at
+ );
+}
+
+#[test]
+fn test_ensure_max_ttl() {
+ let mut config: Config = Config::new();
+ config.max_ttl = Duration::seconds(3600);
+ config.default_ttl = Duration::seconds(5000);
+ let mut sparkv = SparKV::::with_config(config);
+
+ let set_result_long_def = sparkv.set("default is longer than max", "should fail".to_string());
+ 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".into(), Duration::seconds(3599));
+ assert!(set_result_ok.is_ok());
+
+ let set_result_ok_2 = sparkv.set_with_ttl("exact", "ok".into(), Duration::seconds(3600));
+ assert!(set_result_ok_2.is_ok());
+
+ let set_result_not_ok = sparkv.set_with_ttl("not", "not ok".into(), Duration::seconds(33601));
+ assert!(set_result_not_ok.is_err());
+ assert_eq!(set_result_not_ok.unwrap_err(), Error::TTLTooLong);
+}
+
+#[test]
+fn test_delete() {
+ let mut sparkv = SparKV::::new();
+ _ = sparkv.set("keyA", "value".to_string());
+ assert_eq!(sparkv.get("keyA"), Some(&String::from("value")));
+ assert_eq!(sparkv.expiries.len(), 1);
+
+ let deleted_value = sparkv.pop("keyA");
+ assert_eq!(deleted_value, Some(String::from("value")));
+ assert!(sparkv.get("keyA").is_none());
+ assert_eq!(sparkv.expiries.len(), 1); // it does not delete
+}
+
+#[test]
+fn test_clear_expired() {
+ 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", Duration::seconds(90));
+ _ = sparkv.set_with_ttl("expiring", "value", Duration::milliseconds(1));
+ _ = sparkv.set_with_ttl("not-expired", "value", Duration::seconds(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);
+}
+
+#[test]
+fn test_clear_expired_with_overwritten_key() {
+ 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", Duration::milliseconds(1));
+ _ = sparkv.set_with_ttl("no-longer", "v", Duration::seconds(90));
+ _ = sparkv.set_with_ttl("not-expired", "value", Duration::seconds(60));
+ std::thread::sleep(std::time::Duration::from_millis(2));
+ assert_eq!(sparkv.expiries.len(), 3); // overwriting key does not update expiries
+ assert_eq!(sparkv.len(), 2);
+
+ let cleared_count = sparkv.clear_expired();
+ assert_eq!(cleared_count, 0); // no longer expiring
+ assert_eq!(sparkv.expiries.len(), 2); // should have cleared the expiries
+ assert_eq!(sparkv.len(), 2); // but not actually deleting
+}
+
+#[test]
+fn test_clear_expired_with_auto_clear_expired_enabled() {
+ 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".into(), Duration::milliseconds(1));
+ _ = sparkv.set_with_ttl("no-longer", "v".into(), Duration::seconds(90));
+ std::thread::sleep(std::time::Duration::from_millis(2));
+ _ = sparkv.set_with_ttl("not-expired", "value".into(), Duration::seconds(60));
+ assert_eq!(sparkv.expiries.len(), 2); // diff from above, because of auto clear
+ assert_eq!(sparkv.len(), 2);
+
+ // auto clear 2
+ _ = sparkv.set_with_ttl("new-", "value".into(), Duration::seconds(60));
+ assert_eq!(sparkv.expiries.len(), 3); // should have cleared the expiries
+ assert_eq!(sparkv.len(), 3); // but not actually deleting
+}
+
+#[test]
+fn iterator() {
+ let mut sparkv = SparKV::::new();
+ sparkv.set("this", "town".into()).unwrap();
+ sparkv.set("woo", "oooo".into()).unwrap();
+ sparkv.set("is", "coming".into()).unwrap();
+ sparkv.set("like", "a".into()).unwrap();
+ sparkv.set("ghost", "town".into()).unwrap();
+ sparkv.set("oh", "yeah".into()).unwrap();
+
+ let iter = sparkv.iter();
+ assert!(!sparkv.is_empty(), "sparkv should be not empty");
+ assert_eq!(sparkv.get("ghost").unwrap(), "town");
+
+ let (keys, values): (Vec<_>, Vec<_>) = iter.unzip();
+ assert_eq!(keys, vec!["ghost", "is", "like", "oh", "this", "woo"]);
+ assert_eq!(values, vec!["town", "coming", "a", "yeah", "town", "oooo"]);
+}
+
+#[test]
+fn drain() {
+ let mut sparkv = SparKV::::new();
+ sparkv.set("this", "town".into()).unwrap();
+ sparkv.set("woo", "oooo".into()).unwrap();
+ sparkv.set("is", "coming".into()).unwrap();
+ sparkv.set("like", "a".into()).unwrap();
+ sparkv.set("ghost", "town".into()).unwrap();
+ sparkv.set("oh", "yeah".into()).unwrap();
+
+ let iter = sparkv.drain();
+ assert!(sparkv.is_empty(), "sparkv should be empty");
+
+ let (keys, values): (Vec<_>, Vec<_>) = iter.unzip();
+ assert_eq!(keys, vec!["ghost", "is", "like", "oh", "this", "woo"]);
+ assert_eq!(values, vec!["town", "coming", "a", "yeah", "town", "oooo"]);
+}
diff --git a/jans-linux-setup/jans_setup/static/extension/consent_gathering/ConsentGatheringSample.py b/jans-linux-setup/jans_setup/static/extension/consent_gathering/ConsentGatheringSample.py
index 40576be19b1..f0a48a0a49d 100644
--- a/jans-linux-setup/jans_setup/static/extension/consent_gathering/ConsentGatheringSample.py
+++ b/jans-linux-setup/jans_setup/static/extension/consent_gathering/ConsentGatheringSample.py
@@ -30,7 +30,7 @@ def destroy(self, configurationAttributes):
return True
def getApiVersion(self):
- return 1
+ return 11
# Main consent-gather method. Must return True (if gathering performed successfully) or False (if fail).
# All user entered values can be access via Map context.getPageAttributes()