From 39ee5631cf54c8ccb2c0418209e90717319bc780 Mon Sep 17 00:00:00 2001 From: Yuriy Date: Mon, 4 May 2026 18:42:49 +0300 Subject: [PATCH 1/6] feat(jans-lock): update to conform new binding API Signed-off-by: Yuriy --- jans-lock/lock-server.yaml | 1 - .../cedarling/config/BootstrapConfig.java | 373 ++++++++---------- .../CedarlingAuthorizationService.java | 56 +-- .../service/CedarlingProtectionService.java | 8 +- jans-lock/lock-server/pom.xml | 10 +- .../ws/rs/app/ResteasyInitializer.java | 2 +- 6 files changed, 213 insertions(+), 237 deletions(-) diff --git a/jans-lock/lock-server.yaml b/jans-lock/lock-server.yaml index 397cd902987..fe9e029ddb2 100644 --- a/jans-lock/lock-server.yaml +++ b/jans-lock/lock-server.yaml @@ -17,7 +17,6 @@ tags: - name: Lock - Audit Health - name: Lock - Audit Log - name: Lock - Audit Telemetry -- name: Lock - SSE paths: /api/v1/configuration: get: diff --git a/jans-lock/lock-server/cedarling/src/main/java/io/jans/lock/cedarling/config/BootstrapConfig.java b/jans-lock/lock-server/cedarling/src/main/java/io/jans/lock/cedarling/config/BootstrapConfig.java index b58537c9184..b7f5e35bb6e 100644 --- a/jans-lock/lock-server/cedarling/src/main/java/io/jans/lock/cedarling/config/BootstrapConfig.java +++ b/jans-lock/lock-server/cedarling/src/main/java/io/jans/lock/cedarling/config/BootstrapConfig.java @@ -6,212 +6,187 @@ package io.jans.lock.cedarling.config; +import java.util.Arrays; + +/** + * Cedarling BootstrapConfig builder + * + * @author Yuriy Movchan Date: 12/18/2025 + */ +import org.json.JSONArray; import org.json.JSONObject; import io.jans.lock.model.config.cedarling.LogLevel; import io.jans.lock.model.config.cedarling.LogType; /** - * Cedarling BootstrapConfig builder - * - * @author Yuriy Movchan Date: 12/18/2025 + * Configuration class for Cedarling initialization using Enums for Logging. */ public class BootstrapConfig { - public static final String CEDARLING_APPLICATION_NAME = "CEDARLING_APPLICATION_NAME"; - public static final String CEDARLING_LOG_TYPE = "CEDARLING_LOG_TYPE"; - public static final String CEDARLING_LOG_LEVEL = "CEDARLING_LOG_LEVEL"; - public static final String CEDARLING_LOG_TTL = "CEDARLING_LOG_TTL"; - public static final String CEDARLING_AUDIT_TELEMETRY_INTERVAL = "CEDARLING_AUDIT_TELEMETRY_INTERVAL"; - public static final String CEDARLING_AUDIT_HEALTH_INTERVAL = "CEDARLING_AUDIT_HEALTH_INTERVAL"; - public static final String CEDARLING_POLICY_STORE_LOCAL = "CEDARLING_POLICY_STORE_LOCAL"; - public static final String CEDARLING_POLICY_STORE_LOCAL_ID = "CEDARLING_POLICY_STORE_LOCAL_ID"; - public static final String CEDARLING_USER_AUTHZ = "CEDARLING_USER_AUTHZ"; - public static final String CEDARLING_WORKLOAD_AUTHZ = "CEDARLING_WORKLOAD_AUTHZ"; - - public static final String CEDARLING_DECISION_LOG_WORKLOAD_CLAIMS = "CEDARLING_DECISION_LOG_WORKLOAD_CLAIMS"; - - public static final String CEDARLING_PRINCIPAL_BOOLEAN_OPERATION = "CEDARLING_PRINCIPAL_BOOLEAN_OPERATION"; - - public static final String CEDARLING_JWT_SIGNATURE_ALGORITHMS_SUPPORTED = "CEDARLING_JWT_SIGNATURE_ALGORITHMS_SUPPORTED"; - public static final String CEDARLING_JWT_SIG_VALIDATION = "CEDARLING_JWT_SIG_VALIDATION"; - public static final String CEDARLING_JWT_STATUS_VALIDATION = "CEDARLING_JWT_STATUS_VALIDATION"; - - public static final String CEDARLING_ID_TOKEN_TRUST_MODE = "CEDARLING_ID_TOKEN_TRUST_MODE"; - - public static final String CEDARLING_LOCK = "CEDARLING_LOCK"; - - private String applicationName; - private String policyStoreLocal; - private boolean userAuthz; - private boolean workloadAuthz; - private String idTokenTrustMode; - private LogType logType; - private LogLevel logLevel; - private Integer logTtl; - - private String decisionLogWorkloadClaims; - private String principalBooleanOperation; - - private String jwtSignatureAlgorithmsSupported; - private boolean jwtSigValidation; - private boolean jwtStatusValidation; - - private boolean lockEnabled; - - private BootstrapConfig() { - } - - private BootstrapConfig(Builder builder) { - this.applicationName = builder.applicationName; - this.policyStoreLocal = builder.policyStoreLocal; - this.userAuthz = builder.userAuthz; - this.workloadAuthz = builder.workloadAuthz; - this.idTokenTrustMode = builder.idTokenTrustMode; - this.logType = builder.logType; - this.logLevel = builder.logLevel; - this.logTtl = builder.logTtl; - - this.decisionLogWorkloadClaims = builder.decisionLogWorkloadClaims; - this.principalBooleanOperation = builder.principalBooleanOperation; - - this.jwtSignatureAlgorithmsSupported = builder.jwtSignatureAlgorithmsSupported; - this.jwtSigValidation = builder.jwtSigValidation; - this.jwtStatusValidation = builder.jwtStatusValidation; - - this.lockEnabled = builder.lockEnabled; - } - - public static Builder builder() { - return new Builder(); - } - - public String toJsonConfig() { - JSONObject jo = new JSONObject(); - jo.put(CEDARLING_APPLICATION_NAME, applicationName); - - jo.put(CEDARLING_WORKLOAD_AUTHZ, toEnabled(workloadAuthz)); - jo.put(CEDARLING_USER_AUTHZ, toEnabled(userAuthz)); - - jo.put(CEDARLING_AUDIT_HEALTH_INTERVAL, 0); - jo.put(CEDARLING_AUDIT_TELEMETRY_INTERVAL, 0); - - jo.put(CEDARLING_LOG_TYPE, logType.getType()); - jo.put(CEDARLING_LOG_LEVEL, logLevel); - jo.put(CEDARLING_LOG_TTL, logTtl); - - jo.put(CEDARLING_DECISION_LOG_WORKLOAD_CLAIMS, decisionLogWorkloadClaims); - - jo.put(CEDARLING_PRINCIPAL_BOOLEAN_OPERATION, principalBooleanOperation); - - jo.put(CEDARLING_JWT_SIGNATURE_ALGORITHMS_SUPPORTED, jwtSignatureAlgorithmsSupported); - jo.put(CEDARLING_JWT_SIG_VALIDATION, toEnabled(jwtSigValidation)); - jo.put(CEDARLING_JWT_STATUS_VALIDATION, toEnabled(jwtStatusValidation)); - - jo.put(CEDARLING_ID_TOKEN_TRUST_MODE, idTokenTrustMode); - - jo.put(CEDARLING_LOCK, toEnabled(lockEnabled)); - - jo.put(CEDARLING_POLICY_STORE_LOCAL_ID, "lock-server-policy-id"); - jo.put(CEDARLING_POLICY_STORE_LOCAL, policyStoreLocal); - - return jo.toString(); - } - - private String toEnabled(boolean value) { - return value ? "enabled" : "disabled"; - } - - public static class Builder { - private String applicationName = "Lock Server"; - private String policyStoreLocal = null; - private boolean userAuthz = false; - private boolean workloadAuthz = true; - private String idTokenTrustMode = "never"; - private LogType logType = LogType.MEMORY; - private LogLevel logLevel = LogLevel.DEBUG; - private Integer logTtl; - - private String decisionLogWorkloadClaims = "[\"client_id\", \"rp_id\"]"; - private String principalBooleanOperation = "{\"===\": [{\"var\": \"Jans::Workload\"}, \"ALLOW\"]}"; - - private String jwtSignatureAlgorithmsSupported = "[\"HS256\", \"RS256\"]"; - private boolean jwtSigValidation = false; - private boolean jwtStatusValidation = false; - - private boolean lockEnabled = false; - - protected Builder() { - } - - public Builder applicationName(String applicationName) { - this.applicationName = applicationName; - return this; - } - - public Builder policyStoreLocal(String policyStoreLocal) { - this.policyStoreLocal = policyStoreLocal; - return this; - } - - public Builder userAuthz(boolean userAuthz) { - this.userAuthz = userAuthz; - return this; - } - - public Builder workloadAuthz(boolean workloadAuthz) { - this.workloadAuthz = workloadAuthz; - return this; - } - - public Builder idTokenTrustMode(String idTokenTrustMode) { - this.idTokenTrustMode = idTokenTrustMode; - return this; - } - - public Builder logType(LogType logType) { - this.logType = logType; - return this; - } - - public Builder logLevel(LogLevel logLevel) { - this.logLevel = logLevel; - return this; - } - - public Builder decisionLogWorkloadClaims(String decisionLogWorkloadClaims) { - this.decisionLogWorkloadClaims = decisionLogWorkloadClaims; - return this; - } - - public Builder principalBooleanOperation(String principalBooleanOperation) { - this.principalBooleanOperation = principalBooleanOperation; - return this; - } - - public Builder jwtSignatureAlgorithmsSupported(String jwtSignatureAlgorithmsSupported) { - this.jwtSignatureAlgorithmsSupported = jwtSignatureAlgorithmsSupported; - return this; - } - - public Builder jwtSigValidation(boolean jwtSigValidation) { - this.jwtSigValidation = jwtSigValidation; - return this; - } - - public Builder jwtStatusValidation(boolean jwtStatusValidation) { - this.jwtStatusValidation = jwtStatusValidation; - return this; - } - - public Builder lockEnabled(boolean lockEnabled) { - this.lockEnabled = lockEnabled; - return this; - } - - public BootstrapConfig build() { - return new BootstrapConfig(this); - } - } - + // Configuration Keys + public static final String CEDARLING_APPLICATION_NAME = "CEDARLING_APPLICATION_NAME"; + + public static final String CEDARLING_POLICY_STORE_URI = "CEDARLING_POLICY_STORE_URI"; + + public static final String CEDARLING_LOG_TYPE = "CEDARLING_LOG_TYPE"; + public static final String CEDARLING_LOG_LEVEL = "CEDARLING_LOG_LEVEL"; + public static final String CEDARLING_LOG_TTL = "CEDARLING_LOG_TTL"; + public static final String CEDARLING_LOCAL_JWKS = "CEDARLING_LOCAL_JWKS"; + + public static final String CEDARLING_POLICY_STORE_LOCAL = "CEDARLING_POLICY_STORE_LOCAL"; + public static final String CEDARLING_POLICY_STORE_LOCAL_FN = "CEDARLING_POLICY_STORE_LOCAL_FN"; + public static final String CEDARLING_JWT_SIG_VALIDATION = "CEDARLING_JWT_SIG_VALIDATION"; + public static final String CEDARLING_JWT_STATUS_VALIDATION = "CEDARLING_JWT_STATUS_VALIDATION"; + public static final String CEDARLING_JWT_SIGNATURE_ALGORITHMS_SUPPORTED = "CEDARLING_JWT_SIGNATURE_ALGORITHMS_SUPPORTED"; + + public static final String CEDARLING_LOCK = "CEDARLING_LOCK"; + public static final String CEDARLING_LOCK_SERVER_CONFIGURATION_URI = "CEDARLING_LOCK_SERVER_CONFIGURATION_URI"; + public static final String CEDARLING_LOCK_DYNAMIC_CONFIGURATION = "CEDARLING_LOCK_DYNAMIC_CONFIGURATION"; + public static final String CEDARLING_LOCK_HEALTH_INTERVAL = "CEDARLING_LOCK_HEALTH_INTERVAL"; + public static final String CEDARLING_LOCK_TELEMETRY_INTERVAL = "CEDARLING_LOCK_TELEMETRY_INTERVAL"; + public static final String CEDARLING_LOCK_LISTEN_SSE = "CEDARLING_LOCK_LISTEN_SSE"; + + public static final String CEDARLING_MAX_DEFAULT_ENTITIES = "CEDARLING_MAX_DEFAULT_ENTITIES"; + public static final String CEDARLING_MAX_BASE64_SIZE = "CEDARLING_MAX_BASE64_SIZE"; + + // Variables + private String applicationName; + + private String policyStoreUri; + + private LogType logType; + private LogLevel logLevel; + private int logTtl; + private String localJwks; + + private String policyStoreLocal; + private String policyStoreLocalFn; + private boolean jwtSigValidation; + private boolean jwtStatusValidation; + private String[] jwtSignatureAlgorithmsSupported; + + private boolean lock; + private String lockServerConfigurationUri; + private boolean lockDynamicConfiguration; + private int lockHealthInterval; + private int lockTelemetryInterval; + private boolean lockListenSse; + + private int maxDefaultEntities; + private long maxBase64Size; + + private BootstrapConfig() {} + + private BootstrapConfig(Builder builder) { + this.applicationName = builder.applicationName; + this.policyStoreUri = builder.policyStoreUri; + this.logType = builder.logType; + this.logLevel = builder.logLevel; + this.logTtl = builder.logTtl; + this.localJwks = builder.localJwks; + this.policyStoreLocal = builder.policyStoreLocal; + this.policyStoreLocalFn = builder.policyStoreLocalFn; + this.jwtSigValidation = builder.jwtSigValidation; + this.jwtStatusValidation = builder.jwtStatusValidation; + this.jwtSignatureAlgorithmsSupported = builder.jwtSignatureAlgorithmsSupported; + this.lock = builder.lock; + this.lockServerConfigurationUri = builder.lockServerConfigurationUri; + this.lockDynamicConfiguration = builder.lockDynamicConfiguration; + this.lockHealthInterval = builder.lockHealthInterval; + this.lockTelemetryInterval = builder.lockTelemetryInterval; + this.lockListenSse = builder.lockListenSse; + this.maxDefaultEntities = builder.maxDefaultEntities; + this.maxBase64Size = builder.maxBase64Size; + } + + public static Builder builder() { + return new Builder(); + } + + /** + * Converts configuration to JSON string for Cedarling initialization. + */ + public String toJsonConfig() { + JSONObject jo = new JSONObject(); + jo.put(CEDARLING_APPLICATION_NAME, applicationName); + jo.put(CEDARLING_POLICY_STORE_URI, policyStoreUri); + + // Extract string values from Enums + jo.put(CEDARLING_LOG_TYPE, logType.getType()); + jo.put(CEDARLING_LOG_LEVEL, logLevel.getType()); + + jo.put(CEDARLING_LOG_TTL, logTtl); + jo.put(CEDARLING_LOCAL_JWKS, localJwks); + jo.put(CEDARLING_POLICY_STORE_LOCAL, policyStoreLocal); + jo.put(CEDARLING_POLICY_STORE_LOCAL_FN, policyStoreLocalFn); + jo.put(CEDARLING_JWT_SIG_VALIDATION, toEnabled(jwtSigValidation)); + jo.put(CEDARLING_JWT_STATUS_VALIDATION, toEnabled(jwtStatusValidation)); + jo.put(CEDARLING_JWT_SIGNATURE_ALGORITHMS_SUPPORTED, new JSONArray(Arrays.asList(jwtSignatureAlgorithmsSupported))); + jo.put(CEDARLING_LOCK, toEnabled(lock)); + jo.put(CEDARLING_LOCK_SERVER_CONFIGURATION_URI, lockServerConfigurationUri); + jo.put(CEDARLING_LOCK_DYNAMIC_CONFIGURATION, toEnabled(lockDynamicConfiguration)); + jo.put(CEDARLING_LOCK_HEALTH_INTERVAL, lockHealthInterval); + jo.put(CEDARLING_LOCK_TELEMETRY_INTERVAL, lockTelemetryInterval); + jo.put(CEDARLING_LOCK_LISTEN_SSE, toEnabled(lockListenSse)); + jo.put(CEDARLING_MAX_DEFAULT_ENTITIES, maxDefaultEntities); + jo.put(CEDARLING_MAX_BASE64_SIZE, maxBase64Size); + + return jo.toString(); + } + + private String toEnabled(boolean value) { + return value ? "enabled" : "disabled"; + } + + /** + * Builder class for BootstrapConfig. + */ + public static class Builder { + private String applicationName = "App"; + private String policyStoreUri = ""; + private LogType logType = LogType.MEMORY; + private LogLevel logLevel = LogLevel.DEBUG; + private int logTtl = 60; + private String localJwks = null; + private String policyStoreLocal = null; + private String policyStoreLocalFn = ""; + private boolean jwtSigValidation = true; + private boolean jwtStatusValidation = false; + private String[] jwtSignatureAlgorithmsSupported = {"HS256", "RS256"}; + private boolean lock = false; + private String lockServerConfigurationUri = null; + private boolean lockDynamicConfiguration = false; + private int lockHealthInterval = 0; + private int lockTelemetryInterval = 0; + private boolean lockListenSse = false; + private int maxDefaultEntities = 1000; + private long maxBase64Size = 1048576L; + + protected Builder() {} + + public Builder applicationName(String applicationName) { this.applicationName = applicationName; return this; } + public Builder policyStoreUri(String policyStoreUri) { this.policyStoreUri = policyStoreUri; return this; } + public Builder logType(LogType logType) { this.logType = logType; return this; } + public Builder logLevel(LogLevel logLevel) { this.logLevel = logLevel; return this; } + public Builder logTtl(int logTtl) { this.logTtl = logTtl; return this; } + public Builder localJwks(String localJwks) { this.localJwks = localJwks; return this; } + public Builder policyStoreLocal(String policyStoreLocal) { this.policyStoreLocal = policyStoreLocal; return this; } + public Builder policyStoreLocalFn(String policyStoreLocalFn) { this.policyStoreLocalFn = policyStoreLocalFn; return this; } + public Builder jwtSigValidation(boolean jwtSigValidation) { this.jwtSigValidation = jwtSigValidation; return this; } + public Builder jwtStatusValidation(boolean jwtStatusValidation) { this.jwtStatusValidation = jwtStatusValidation; return this; } + public Builder jwtSignatureAlgorithmsSupported(String[] algorithms) { this.jwtSignatureAlgorithmsSupported = algorithms; return this; } + public Builder lock(boolean lock) { this.lock = lock; return this; } + public Builder lockServerConfigurationUri(String uri) { this.lockServerConfigurationUri = uri; return this; } + public Builder lockDynamicConfiguration(boolean dynamic) { this.lockDynamicConfiguration = dynamic; return this; } + public Builder lockHealthInterval(int interval) { this.lockHealthInterval = interval; return this; } + public Builder lockTelemetryInterval(int interval) { this.lockTelemetryInterval = interval; return this; } + public Builder lockListenSse(boolean listenSse) { this.lockListenSse = listenSse; return this; } + public Builder maxDefaultEntities(int max) { this.maxDefaultEntities = max; return this; } + public Builder maxBase64Size(long size) { this.maxBase64Size = size; return this; } + + public BootstrapConfig build() { + return new BootstrapConfig(this); + } + } } \ No newline at end of file diff --git a/jans-lock/lock-server/cedarling/src/main/java/io/jans/lock/cedarling/service/CedarlingAuthorizationService.java b/jans-lock/lock-server/cedarling/src/main/java/io/jans/lock/cedarling/service/CedarlingAuthorizationService.java index 13a3db6ed78..67efd195087 100644 --- a/jans-lock/lock-server/cedarling/src/main/java/io/jans/lock/cedarling/service/CedarlingAuthorizationService.java +++ b/jans-lock/lock-server/cedarling/src/main/java/io/jans/lock/cedarling/service/CedarlingAuthorizationService.java @@ -18,15 +18,13 @@ import io.jans.lock.model.config.AppConfiguration; import io.jans.lock.model.config.cedarling.CedarlingConfiguration; import io.jans.lock.model.config.cedarling.CedarlingPolicyConfiguration; -import io.jans.service.cdi.event.ApplicationInitialized; -import io.jans.service.cdi.event.ApplicationInitializedEvent; import jakarta.annotation.PostConstruct; import jakarta.annotation.PreDestroy; import jakarta.enterprise.context.ApplicationScoped; -import jakarta.enterprise.event.Observes; import jakarta.inject.Inject; import uniffi.cedarling_uniffi.AuthorizeResult; import uniffi.cedarling_uniffi.CedarlingException; +import uniffi.cedarling_uniffi.MultiIssuerAuthorizeResult; /** * @@ -35,6 +33,8 @@ @ApplicationScoped public class CedarlingAuthorizationService { + public static final String CEDARLING_JANS_ACCESS_TOKEN = "Jans::Access_Token"; + @Inject private Logger log; @@ -75,8 +75,8 @@ private CedarlingAdapter initAdapter(CedarlingConfiguration cedarConf) { BootstrapConfig config = BootstrapConfig.builder() .applicationName("Lock Server") .policyStoreLocal(policyConfiguration.getPolicy()) - .userAuthz(false) - .workloadAuthz(true) + .jwtSigValidation(false) + .jwtSigValidation(false) .logType(cedarConf.getLogType()) .logLevel(cedarConf.getLogLevel()) .build(); @@ -121,29 +121,29 @@ public boolean authorize(Map tokens, String action, Map tokens, String action, JSONObject resource, JSONObject context) { -// try { -// if (log.isDebugEnabled()) { -// log.debug("Before executing authorization request. tokens: {}, action: {}, resource: {}, context: {}", -// tokens, action, resource, context); -// } -// AuthorizeResult res = cedarlingAdapter.authorize(/*tokens/* null, action, resource, context); -// -// if (res == null) { -// log.error("Authorization response is empty for request with tokens: {}, action: {}, resource: {}, context: {}", -// tokens, action, resource, context); -// return false; -// } -// -// String requestId = res.getRequestId(); -// if (log.isDebugEnabled()) { -// log.debug("Authorization workload decision {} for requestId {}, tokens: {}, action: {}, resource: {}, context: {}", -// res.getDecision(), requestId, tokens, action, resource, context); -// } -// -// return res.getDecision(); -// } catch (Exception ex) { -// log.error("Failed to execute Cedarling authorize: tokens: {}, action: {}, resource: {}, context: {}", tokens, action, resource, context, ex); -// } + try { + if (log.isDebugEnabled()) { + log.debug("Before executing authorization request. tokens: {}, action: {}, resource: {}, context: {}", + tokens, action, resource, context); + } + MultiIssuerAuthorizeResult res = cedarlingAdapter.authorizeMultiIssuer(tokens, action, resource, context); + + if (res == null) { + log.error("Authorization response is empty for request with tokens: {}, action: {}, resource: {}, context: {}", + tokens, action, resource, context); + return false; + } + + String requestId = res.getRequestId(); + if (log.isDebugEnabled()) { + log.debug("Authorization workload decision {} for requestId {}, tokens: {}, action: {}, resource: {}, context: {}", + res.getDecision(), requestId, tokens, action, resource, context); + } + + return res.getDecision(); + } catch (Exception ex) { + log.error("Failed to execute Cedarling authorize: tokens: {}, action: {}, resource: {}, context: {}", tokens, action, resource, context, ex); + } return false; } diff --git a/jans-lock/lock-server/cedarling/src/main/java/io/jans/lock/cedarling/service/CedarlingProtectionService.java b/jans-lock/lock-server/cedarling/src/main/java/io/jans/lock/cedarling/service/CedarlingProtectionService.java index fd81b782034..32d36f8ad86 100644 --- a/jans-lock/lock-server/cedarling/src/main/java/io/jans/lock/cedarling/service/CedarlingProtectionService.java +++ b/jans-lock/lock-server/cedarling/src/main/java/io/jans/lock/cedarling/service/CedarlingProtectionService.java @@ -45,7 +45,9 @@ @ApplicationScoped public class CedarlingProtectionService implements CedarlingProtection { - @Inject + private static final String CEDARLING_JANS_ACCESS_TOKEN = "Jans::Access_Token"; + + @Inject private Logger log; @Inject @@ -122,7 +124,7 @@ public Response processAuthorization(String bearerToken, ResourceInfo resourceIn boolean authorized = true; Map tokens = getCedarlingTokens(bearerToken); for (CedarlingPermission requestedPermission : requestedPermissions) { - authorized &= authorizationService.authorize(/*tokens*/null, requestedPermission.getAction(), + authorized &= authorizationService.authorize(tokens, requestedPermission.getAction(), getCedarlingResource(requestedPermission), getCedarlingContext()); if (!authorized) { log.error("Insufficient permissions to access '{}'", requestedPermission); @@ -158,7 +160,7 @@ private Jwt tokenAsJwt(String token) { } private Map getCedarlingTokens(String accessToken) { - return Map.of("access_token", accessToken); + return Map.of(CedarlingAuthorizationService.CEDARLING_JANS_ACCESS_TOKEN, accessToken); } private Map getCedarlingResource(CedarlingPermission requestedPermission) { diff --git a/jans-lock/lock-server/pom.xml b/jans-lock/lock-server/pom.xml index 939532e42d6..69c198595af 100644 --- a/jans-lock/lock-server/pom.xml +++ b/jans-lock/lock-server/pom.xml @@ -59,16 +59,16 @@ maven central https://repo1.maven.org/maven2 - - github - GitHub Packages - https://maven.pkg.github.com/JanssenProject/jans - jans Janssen project repository https://maven.jans.io/maven + + github + GitHub Packages + https://maven.pkg.github.com/JanssenProject/jans + bouncycastle Bouncy Castle diff --git a/jans-lock/lock-server/service/src/main/java/io/jans/lock/service/ws/rs/app/ResteasyInitializer.java b/jans-lock/lock-server/service/src/main/java/io/jans/lock/service/ws/rs/app/ResteasyInitializer.java index 7dcfd99c758..7a1a1e760f7 100644 --- a/jans-lock/lock-server/service/src/main/java/io/jans/lock/service/ws/rs/app/ResteasyInitializer.java +++ b/jans-lock/lock-server/service/src/main/java/io/jans/lock/service/ws/rs/app/ResteasyInitializer.java @@ -56,7 +56,7 @@ version = "OAS Version"), tags = { @Tag(name = "Lock - Server Configuration"), @Tag(name = "Lock - Stat"), @Tag(name = "Lock - Audit Health"), - @Tag(name = "Lock - Audit Log"), @Tag(name = "Lock - Audit Telemetry"), @Tag(name = "Lock - SSE"), + @Tag(name = "Lock - Audit Log"), @Tag(name = "Lock - Audit Telemetry"), }, servers = { @Server(url = "https://jans.local.io", description = "The Jans server") }) From 217ce6a2668e00be79fdab587af9a60533833ab8 Mon Sep 17 00:00:00 2001 From: Yuriy Date: Tue, 12 May 2026 18:28:40 +0300 Subject: [PATCH 2/6] fix(setup): fix ProxyPass in mako template Signed-off-by: Yuriy --- .../jans_setup/templates/apache/https_jans.conf.mako | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jans-linux-setup/jans_setup/templates/apache/https_jans.conf.mako b/jans-linux-setup/jans_setup/templates/apache/https_jans.conf.mako index 9f36146dbd6..83c36ce0b61 100644 --- a/jans-linux-setup/jans_setup/templates/apache/https_jans.conf.mako +++ b/jans-linux-setup/jans_setup/templates/apache/https_jans.conf.mako @@ -164,7 +164,7 @@ ProxyPass /device-code http://localhost:${jans_auth_port}/jans-auth/device_authorization.htm % if context.get('install_jans_lock') in ('true', True): - ProxyPass /.well-known/lock-server-configuration http://localhost:${lock_host_port}/${lock_host_suffix}/api/v1/configuration' + ProxyPass /.well-known/lock-server-configuration http://localhost:${lock_host_port}/${lock_host_suffix}/api/v1/configuration Header edit Set-Cookie ^((?!opbs|session_state).*)$ $1;HttpOnly ProxyPass http://localhost:${lock_host_port}/${lock_host_suffix} retry=5 connectiontimeout=60 timeout=60 From c0a5358b28118c60d1bde741cd5d4a11755c00f7 Mon Sep 17 00:00:00 2001 From: Yuriy Date: Tue, 19 May 2026 17:11:54 +0300 Subject: [PATCH 3/6] feat(jans-lock): update store to conform new Cedarling cjar policy store Signed-off-by: Yuriy --- .../resources/test-policy-store/metadata.json | 10 + .../policies/audit/health_bulk_write.cedar | 12 ++ .../policies/audit/health_write.cedar | 12 ++ .../policies/audit/log_bulk_write.cedar | 12 ++ .../policies/audit/log_write.cedar | 12 ++ .../policies/audit/telemetry_bulk_write.cedar | 12 ++ .../policies/audit/telemetry_write.cedar | 12 ++ .../policies/policy/policy_get.cedar | 12 ++ .../policies/policy/policy_list.cedar | 12 ++ .../policies/stat/stat_query.cedar | 12 ++ .../test-policy-store/schema.cedarschema | 177 ++++++++++++++++++ .../trusted-issuers/jans-issuer.json | 16 ++ 12 files changed, 311 insertions(+) create mode 100644 jans-lock/lock-server/cedarling/src/test/resources/test-policy-store/metadata.json create mode 100644 jans-lock/lock-server/cedarling/src/test/resources/test-policy-store/policies/audit/health_bulk_write.cedar create mode 100644 jans-lock/lock-server/cedarling/src/test/resources/test-policy-store/policies/audit/health_write.cedar create mode 100644 jans-lock/lock-server/cedarling/src/test/resources/test-policy-store/policies/audit/log_bulk_write.cedar create mode 100644 jans-lock/lock-server/cedarling/src/test/resources/test-policy-store/policies/audit/log_write.cedar create mode 100644 jans-lock/lock-server/cedarling/src/test/resources/test-policy-store/policies/audit/telemetry_bulk_write.cedar create mode 100644 jans-lock/lock-server/cedarling/src/test/resources/test-policy-store/policies/audit/telemetry_write.cedar create mode 100644 jans-lock/lock-server/cedarling/src/test/resources/test-policy-store/policies/policy/policy_get.cedar create mode 100644 jans-lock/lock-server/cedarling/src/test/resources/test-policy-store/policies/policy/policy_list.cedar create mode 100644 jans-lock/lock-server/cedarling/src/test/resources/test-policy-store/policies/stat/stat_query.cedar create mode 100644 jans-lock/lock-server/cedarling/src/test/resources/test-policy-store/schema.cedarschema create mode 100644 jans-lock/lock-server/cedarling/src/test/resources/test-policy-store/trusted-issuers/jans-issuer.json diff --git a/jans-lock/lock-server/cedarling/src/test/resources/test-policy-store/metadata.json b/jans-lock/lock-server/cedarling/src/test/resources/test-policy-store/metadata.json new file mode 100644 index 00000000000..09dfbd85c5d --- /dev/null +++ b/jans-lock/lock-server/cedarling/src/test/resources/test-policy-store/metadata.json @@ -0,0 +1,10 @@ +{ + "cedar_version": "4.4.0", + "policy_store": { + "id": "a1bf93115de86de760ee0bea1d529b521489e5a11747", + "name": "JansLock", + "description": "Policy store for Jans Lock endpoints protection", + "version": "1.0.0", + "created_date": "2026-05-18T17:22:39Z" + } +} diff --git a/jans-lock/lock-server/cedarling/src/test/resources/test-policy-store/policies/audit/health_bulk_write.cedar b/jans-lock/lock-server/cedarling/src/test/resources/test-policy-store/policies/audit/health_bulk_write.cedar new file mode 100644 index 00000000000..c268bbe46bf --- /dev/null +++ b/jans-lock/lock-server/cedarling/src/test/resources/test-policy-store/policies/audit/health_bulk_write.cedar @@ -0,0 +1,12 @@ +@id("lock_audit_health_bulk_write") +permit( + principal, + action in [Jans::Action::"POST"], + resource == Jans::HTTP_Request::"lock_audit_health_bulk_write" +) +when { +context has tokens && + context.tokens has jans_access_token && + context.tokens.jans_access_token has scope && + context.tokens.jans_access_token.scope.contains("https://jans.io/oauth/lock/health.write") +}; \ No newline at end of file diff --git a/jans-lock/lock-server/cedarling/src/test/resources/test-policy-store/policies/audit/health_write.cedar b/jans-lock/lock-server/cedarling/src/test/resources/test-policy-store/policies/audit/health_write.cedar new file mode 100644 index 00000000000..4e15f2dee6a --- /dev/null +++ b/jans-lock/lock-server/cedarling/src/test/resources/test-policy-store/policies/audit/health_write.cedar @@ -0,0 +1,12 @@ +@id("lock_audit_health_write") +permit( + principal, + action in [Jans::Action::"POST"], + resource == Jans::HTTP_Request::"lock_audit_health_write" +) +when { +context has tokens && + context.tokens has jans_access_token && + context.tokens.jans_access_token has scope && + context.tokens.jans_access_token.scope.contains("https://jans.io/oauth/lock/health.write") +}; \ No newline at end of file diff --git a/jans-lock/lock-server/cedarling/src/test/resources/test-policy-store/policies/audit/log_bulk_write.cedar b/jans-lock/lock-server/cedarling/src/test/resources/test-policy-store/policies/audit/log_bulk_write.cedar new file mode 100644 index 00000000000..0e1c3e9d9e1 --- /dev/null +++ b/jans-lock/lock-server/cedarling/src/test/resources/test-policy-store/policies/audit/log_bulk_write.cedar @@ -0,0 +1,12 @@ +@id("lock_audit_log_bulk_write") +permit( + principal, + action in [Jans::Action::"POST"], + resource == Jans::HTTP_Request::"lock_audit_log_bulk_write" +) +when { +context has tokens && + context.tokens has jans_access_token && + context.tokens.jans_access_token has scope && + context.tokens.jans_access_token.scope.contains("https://jans.io/oauth/lock/log.write") +}; \ No newline at end of file diff --git a/jans-lock/lock-server/cedarling/src/test/resources/test-policy-store/policies/audit/log_write.cedar b/jans-lock/lock-server/cedarling/src/test/resources/test-policy-store/policies/audit/log_write.cedar new file mode 100644 index 00000000000..c19014bec4d --- /dev/null +++ b/jans-lock/lock-server/cedarling/src/test/resources/test-policy-store/policies/audit/log_write.cedar @@ -0,0 +1,12 @@ +@id("lock_audit_log_write") +permit( + principal, + action in [Jans::Action::"POST"], + resource == Jans::HTTP_Request::"lock_audit_log_write" +) +when { +context has tokens && + context.tokens has jans_access_token && + context.tokens.jans_access_token has scope && + context.tokens.jans_access_token.scope.contains("https://jans.io/oauth/lock/log.write") +}; \ No newline at end of file diff --git a/jans-lock/lock-server/cedarling/src/test/resources/test-policy-store/policies/audit/telemetry_bulk_write.cedar b/jans-lock/lock-server/cedarling/src/test/resources/test-policy-store/policies/audit/telemetry_bulk_write.cedar new file mode 100644 index 00000000000..81788cc8895 --- /dev/null +++ b/jans-lock/lock-server/cedarling/src/test/resources/test-policy-store/policies/audit/telemetry_bulk_write.cedar @@ -0,0 +1,12 @@ +@id("lock_audit_telemetry_bulk_write") +permit( + principal, + action in [Jans::Action::"POST"], + resource == Jans::HTTP_Request::"lock_audit_telemetry_bulk_write" +) +when { +context has tokens && + context.tokens has jans_access_token && + context.tokens.jans_access_token has scope && + context.tokens.jans_access_token.scope.contains("https://jans.io/oauth/lock/telemetry.write") +}; \ No newline at end of file diff --git a/jans-lock/lock-server/cedarling/src/test/resources/test-policy-store/policies/audit/telemetry_write.cedar b/jans-lock/lock-server/cedarling/src/test/resources/test-policy-store/policies/audit/telemetry_write.cedar new file mode 100644 index 00000000000..db5469874de --- /dev/null +++ b/jans-lock/lock-server/cedarling/src/test/resources/test-policy-store/policies/audit/telemetry_write.cedar @@ -0,0 +1,12 @@ +@id("lock_audit_telemetry_write") +permit( + principal, + action in [Jans::Action::"POST"], + resource == Jans::HTTP_Request::"lock_audit_telemetry_write" +) +when { +context has tokens && + context.tokens has jans_access_token && + context.tokens.jans_access_token has scope && + context.tokens.jans_access_token.scope.contains("https://jans.io/oauth/lock/telemetry.write") +}; \ No newline at end of file diff --git a/jans-lock/lock-server/cedarling/src/test/resources/test-policy-store/policies/policy/policy_get.cedar b/jans-lock/lock-server/cedarling/src/test/resources/test-policy-store/policies/policy/policy_get.cedar new file mode 100644 index 00000000000..393ab56d437 --- /dev/null +++ b/jans-lock/lock-server/cedarling/src/test/resources/test-policy-store/policies/policy/policy_get.cedar @@ -0,0 +1,12 @@ +@id("lock_policy_get_by_id") +permit( + principal, + action in [Jans::Action::"GET"], + resource == Jans::HTTP_Request::"lock_policy_get_by_id" +) +when { +context has tokens && + context.tokens has jans_access_token && + context.tokens.jans_access_token has scope && + context.tokens.jans_access_token.scope.contains("https://jans.io/oauth/lock/policy.readonly") +}; \ No newline at end of file diff --git a/jans-lock/lock-server/cedarling/src/test/resources/test-policy-store/policies/policy/policy_list.cedar b/jans-lock/lock-server/cedarling/src/test/resources/test-policy-store/policies/policy/policy_list.cedar new file mode 100644 index 00000000000..29628c5d64e --- /dev/null +++ b/jans-lock/lock-server/cedarling/src/test/resources/test-policy-store/policies/policy/policy_list.cedar @@ -0,0 +1,12 @@ +@id("lock_policy_list") +permit( + principal, + action in [Jans::Action::"GET"], + resource == Jans::HTTP_Request::"lock_policy_list" +) +when { +context has tokens && + context.tokens has jans_access_token && + context.tokens.jans_access_token has scope && + context.tokens.jans_access_token.scope.contains("https://jans.io/oauth/lock/policy.readonly") +}; \ No newline at end of file diff --git a/jans-lock/lock-server/cedarling/src/test/resources/test-policy-store/policies/stat/stat_query.cedar b/jans-lock/lock-server/cedarling/src/test/resources/test-policy-store/policies/stat/stat_query.cedar new file mode 100644 index 00000000000..525b7baaa71 --- /dev/null +++ b/jans-lock/lock-server/cedarling/src/test/resources/test-policy-store/policies/stat/stat_query.cedar @@ -0,0 +1,12 @@ +@id("lock_stat_query") +permit( + principal, + action in [Jans::Action::"Search"], + resource == Jans::HTTP_Request::"lock_stat_query" +) +when { +context has tokens && + context.tokens has jans_access_token && + context.tokens.jans_access_token has scope && + context.tokens.jans_access_token.scope.contains("https://jans.io/oauth/lock/stat.readonly") +}; \ No newline at end of file diff --git a/jans-lock/lock-server/cedarling/src/test/resources/test-policy-store/schema.cedarschema b/jans-lock/lock-server/cedarling/src/test/resources/test-policy-store/schema.cedarschema new file mode 100644 index 00000000000..8942908d688 --- /dev/null +++ b/jans-lock/lock-server/cedarling/src/test/resources/test-policy-store/schema.cedarschema @@ -0,0 +1,177 @@ +namespace Jans { + // ****** TYPES ****** + type Url = { + host: String, + path: String, + protocol: String + }; + type email_address = { + domain: String, + uid: String + }; + type Context = { + network?: String, + network_type?: String, + user_agent?: String, + operating_system?: String, + device_health?: Set, + current_time?: Long, + geolocation?: Set, + fraud_indicators?: Set, + tokens?: TokensContext, + }; + type TokensContext = { + jans_access_token?: Access_token, + total_token_count: Long, + }; + + // ****** Entities ****** + entity Role; + entity User in [Role] = { + email?: email_address, + phone_number?: String, + role: Set, + sub: String, + "username"?: String, + id_token?: id_token, + userinfo_token?: Userinfo_token, + }; + entity Workload = { + client_id: String, + iss?: TrustedIssuer, + name?: String, + rp_id?: String, + spiffe_id?: String, + access_token?: Access_token, + }; + entity Access_token = { + token_type?: String, + validated_at?: Long, + aud?: String, + exp?: Long, + iat?: Long, + iss?: TrustedIssuer, + jti?: String, + nbf?: Long, + scope?: Set + } tags Set; + entity id_token = { + token_type?: String, + validated_at?: Long, + acr?: String, + amr?: Set, + aud?: Set, + azp?: String, + birthdate?: String, + email?: email_address, + exp?: Long, + iat?: Long, + iss?: TrustedIssuer, + jti?: String, + name?: String, + phone_number?: String, + role?: Set, + sub?: String + } tags Set; + entity Userinfo_token = { + token_type?: String, + validated_at?: Long, + aud?: String, + birthdate?: String, + email?: email_address, + exp?: Long, + iat?: Long, + iss?: TrustedIssuer, + jti?: String, + name?: String, + phone_number?: String, + role?: Set, + sub?: String + } tags Set; + entity HTTP_Request = { + "header": { + "Accept"?: String + }, + "url": Url + }; + entity TrustedIssuer = { + issuer_entity_id: Url + }; + entity Application = { + app_id: String, + name: String, + url: Url + }; + + // ****** Actions ****** + action Compare appliesTo { + principal: [User, Workload], + resource: [Application], + context: Context + }; + action Execute appliesTo { + principal: [User, Workload], + resource: [Application], + context: Context + }; + action Monitor appliesTo { + principal: [User, Workload], + resource: [Application], + context: Context + }; + action Read appliesTo { + principal: [User, Workload], + resource: [Application], + context: Context + }; + action Search appliesTo { + principal: [User, Workload], + resource: [Application], + context: Context + }; + action Share appliesTo { + principal: [User, Workload], + resource: [Application], + context: Context + }; + action Tag appliesTo { + principal: [User, Workload], + resource: [Application], + context: Context + }; + action Write appliesTo { + principal: [User, Workload], + resource: [Application], + context: Context + }; + action GET appliesTo { + principal: [Workload], + resource: [HTTP_Request], + context: Context + }; + action POST appliesTo { + principal: [Workload], + resource: [HTTP_Request], + context: Context + }; + action PUT appliesTo { + principal: [Workload], + resource: [HTTP_Request], + context: Context + }; + action DELETE appliesTo { + principal: [Workload], + resource: [HTTP_Request], + context: Context + }; + action HEAD appliesTo { + principal: [Workload], + resource: [HTTP_Request], + context: Context + }; + action PATCH appliesTo { + principal: [Workload], + resource: [HTTP_Request], + context: Context + }; +} diff --git a/jans-lock/lock-server/cedarling/src/test/resources/test-policy-store/trusted-issuers/jans-issuer.json b/jans-lock/lock-server/cedarling/src/test/resources/test-policy-store/trusted-issuers/jans-issuer.json new file mode 100644 index 00000000000..7395bdb9978 --- /dev/null +++ b/jans-lock/lock-server/cedarling/src/test/resources/test-policy-store/trusted-issuers/jans-issuer.json @@ -0,0 +1,16 @@ +{ + "name": "Jans", + "description": "Jans Lock Issuer", + "configuration_endpoint": "https://jans-mysql-lock-server.jans.info/.well-known/openid-configuration", + "token_metadata": { + "access_token": { + "trusted": true, + "entity_type_name": "Jans::Access_token", + "token_id": "jti", + "required_claims": [ + "jti" + ], + "principal_mapping": ["Jans::Workload"] + } + } +} From f4ff847287814c469a3c81aa7ed48539097e2706 Mon Sep 17 00:00:00 2001 From: Yuriy Date: Wed, 20 May 2026 21:04:52 +0300 Subject: [PATCH 4/6] feat(jans-lock): add low level Cedarling integration tests Signed-off-by: Yuriy --- .../lock/cedarling/BaseCedarlingTest.java | 115 ++++++ .../cedarling/CedarlingIntegrationTest.java | 365 ++++++++++++++++++ 2 files changed, 480 insertions(+) create mode 100644 jans-lock/lock-server/cedarling/src/test/java/io/jans/lock/cedarling/BaseCedarlingTest.java create mode 100644 jans-lock/lock-server/cedarling/src/test/java/io/jans/lock/cedarling/CedarlingIntegrationTest.java diff --git a/jans-lock/lock-server/cedarling/src/test/java/io/jans/lock/cedarling/BaseCedarlingTest.java b/jans-lock/lock-server/cedarling/src/test/java/io/jans/lock/cedarling/BaseCedarlingTest.java new file mode 100644 index 00000000000..f51286b9fc5 --- /dev/null +++ b/jans-lock/lock-server/cedarling/src/test/java/io/jans/lock/cedarling/BaseCedarlingTest.java @@ -0,0 +1,115 @@ +/* + * Janssen Project software is available under the Apache License (2004). See http://www.apache.org/licenses/ for full text. + * + * Copyright (c) 2025, Janssen Project + */ + +package io.jans.lock.cedarling; + +import java.io.InputStream; +import java.lang.reflect.Field; +import java.nio.charset.StandardCharsets; +import java.time.Instant; +import java.util.Base64; + +import org.json.JSONObject; + +/** + * Base test class for tests + * + * @author Yuriy Movchan Date: 12/05/2026 + */ +public class BaseCedarlingTest { + + /** + * Patches the {@code exp} claim of a JWT so it expires one hour from now. + * + *

The method only re-encodes the payload; the original header and signature + * parts are preserved unchanged. Because the Cedarling adapter is configured + * with {@code jwtSigValidation(false)} the modified payload will still pass + * validation inside the policy engine. + * + *

Algorithm: + *

    + *
  1. Split the JWT on {@code '.'} to obtain header, payload, signature.
  2. + *
  3. Base64url-decode the payload (without padding).
  4. + *
  5. Parse as JSON and replace {@code exp} with {@code now + 3600} seconds.
  6. + *
  7. Base64url-encode (without padding) and reassemble.
  8. + *
+ * + * @param jwt the original JWT string + * @return the same JWT with a refreshed {@code exp} claim + */ + protected static String withFutureExp(String jwt) { + String[] parts = jwt.split("\\."); + if (parts.length != 3) { + throw new IllegalArgumentException("Not a three-part JWT: " + jwt); + } + + // Decode payload – add padding if the decoder requires it + Base64.Decoder decoder = Base64.getUrlDecoder(); + String payloadJson = new String(decoder.decode(addPadding(parts[1])), StandardCharsets.UTF_8); + + // Update the exp claim to 1 minute in the future + JSONObject payload = new JSONObject(payloadJson); + payload.put("exp", (Instant.now().toEpochMilli() + 1 * 60L * 1000L) / 1000L); + + // Re-encode without padding (standard JWT convention) + String newPayload = Base64.getUrlEncoder() + .withoutPadding() + .encodeToString(payload.toString().getBytes(StandardCharsets.UTF_8)); + + return parts[0] + "." + newPayload + "." + parts[2]; + } + + /** + * Reads the entire content of a classpath resource into a {@link String}. + * + * @param resourceName the resource file name relative to the classpath root + * (e.g. {@code "lock_policy_store.json"}) + * @return the file content, or {@code null} if the resource cannot be found + */ + protected static String loadResourceAsString(String resourceName) throws Exception { + ClassLoader cl = Thread.currentThread().getContextClassLoader(); + try (InputStream is = cl.getResourceAsStream(resourceName)) { + if (is == null) { + return null; + } + return new String(is.readAllBytes(), StandardCharsets.UTF_8); + } + } + + /** + * Injects a value into a private field of an object using reflection. + * Used to wire CDI {@code @Inject} fields without a running container. + * + * @param target the object whose field should be set + * @param fieldName the name of the private field + * @param value the value to assign + */ + protected static void injectField(Object target, String fieldName, Object value) throws Exception { + Field field = target.getClass().getDeclaredField(fieldName); + field.setAccessible(true); + field.set(target, value); + } + + /** + * Appends Base64 padding characters ({@code =}) so that the URL-decoder accepts + * base64url strings whose length is not a multiple of 4. + * + * @param base64url the unpadded base64url string + * @return the same string with trailing {@code =} padding if needed + */ + private static String addPadding(String base64url) { + int mod = base64url.length() % 4; + if (mod == 0) { + return base64url; + } + return base64url + "=".repeat(4 - mod); + } + + public BaseCedarlingTest() { + super(); + } + +} \ No newline at end of file diff --git a/jans-lock/lock-server/cedarling/src/test/java/io/jans/lock/cedarling/CedarlingIntegrationTest.java b/jans-lock/lock-server/cedarling/src/test/java/io/jans/lock/cedarling/CedarlingIntegrationTest.java new file mode 100644 index 00000000000..95c1f9649a6 --- /dev/null +++ b/jans-lock/lock-server/cedarling/src/test/java/io/jans/lock/cedarling/CedarlingIntegrationTest.java @@ -0,0 +1,365 @@ +/* + * Janssen Project software is available under the Apache License (2004). See http://www.apache.org/licenses/ for full text. + * + * Copyright (c) 2025, Janssen Project + */ + +package io.jans.lock.cedarling; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; + +import org.json.JSONObject; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; +import org.junit.jupiter.api.TestInstance.Lifecycle; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import io.jans.cedarling.binding.wrapper.CedarlingAdapter; +import io.jans.lock.cedarling.config.BootstrapConfig; +import io.jans.lock.cedarling.model.CedarlingPermission; +import io.jans.lock.cedarling.service.CedarlingAuthorizationService; +import io.jans.lock.model.config.cedarling.LogLevel; +import io.jans.lock.model.config.cedarling.LogType; +import uniffi.cedarling_uniffi.CedarlingException; +import uniffi.cedarling_uniffi.MultiIssuerAuthorizeResult; + +/** + * Integration tests for the Cedarling authorization engine. + * + *

These tests exercise the Cedarling policy engine end-to-end using real JWT access + * tokens and a real policy store loaded from {@code src/test/resources/test-policy-store}. + * + *

The dependency on {@code CedarlingAuthorizationService} is intentionally removed: + * the test wires the native {@link CedarlingAdapter} directly, so it validates the + * engine behaviour without any service-layer indirection. + * + *

Token matrix

+ *
+ * ┌────────┬──────────────────────────────────────────────────────────────┬──────────────────────────────┐
+ * │ Token  │ OAuth scopes                                                 │ Allowed endpoints            │
+ * ├────────┼──────────────────────────────────────────────────────────────┼──────────────────────────────┤
+ * │ JWT 1  │ lock/health.write, lock/telemetry.write, lock/log.write      │ /log, /health, /telemetry    │
+ * │ JWT 2  │ lock/health.write                                            │ /health only                 │
+ * │ JWT 3  │ lock/log.write                                               │ /log only                    │
+ * └────────┴──────────────────────────────────────────────────────────────┴──────────────────────────────┘
+ * 
+ * + *

JWT expiry: The tokens are real tokens whose {@code exp} claim is in + * the past. Before each test run the payloads are patched (base64-decode → update exp → + * base64-encode) to a value 1 hour in the future. Because the adapter is initialised with + * {@code jwtSigValidation(false)} the modified signature is still accepted by Cedarling. + * + *

Prerequisites: The native Cedarling UniFFI library must be on the + * dynamic-library path ({@code LD_LIBRARY_PATH} / {@code java.library.path}). + * + * @author Yuriy Movchan Date: 12/05/2026 + */ +@TestInstance(Lifecycle.PER_CLASS) +@DisplayName("Cedarling Engine – Authorization Integration Tests") +class CedarlingIntegrationTest extends BaseCedarlingTest { + + private static final Logger log = LoggerFactory.getLogger(CedarlingIntegrationTest.class); + + // ─── Raw JWT strings (exp claims are patched at runtime) ──────────────────── + + /** + * JWT 1 – contains all three scopes: + * {@code health.write}, {@code telemetry.write}, {@code log.write}. + */ + private static final String RAW_JWT_1 = + "eyJraWQiOiJjb25uZWN0XzNjMzJjMTkzLTY5ZjYtNDBkYS1iNmQyLTE3ODY0YmJkYzU1MV9zaWdfcnMyNTYiLCJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9" + + ".eyJhdWQiOiIwZTk5NDAzMC1lZTg4LTRmMTQtYWUwOC04YjBkYTEwNDQwMjkiLCJzdWIiOiIwZTk5NDAzMC1lZTg4LTRmMTQtYWUwOC04YjBkYTEwNDQwMjkiLCJ4NXQjUzI1NiI6IiIsIm5iZiI6MTc3ODYxNDA2Niwic2NvcGUiOlsiaHR0cHM6Ly9qYW5zLmlvL29hdXRoL2xvY2svaGVhbHRoLndyaXRlIiwiaHR0cHM6Ly9qYW5zLmlvL29hdXRoL2xvY2svdGVsZW1ldHJ5LndyaXRlIiwiaHR0cHM6Ly9qYW5zLmlvL29hdXRoL2xvY2svbG9nLndyaXRlIl0sImlzcyI6Imh0dHBzOi8vamFucy1teXNxbC1sb2NrLXNlcnZlci5qYW5zLmluZm8iLCJ0b2tlbl90eXBlIjoiQmVhcmVyIiwiZXhwIjoxNzc4NjE0MzY2LCJpYXQiOjE3Nzg2MTQwNjYsImNsaWVudF9pZCI6IjBlOTk0MDMwLWVlODgtNGYxNC1hZTA4LThiMGRhMTA0NDAyOSIsImp0aSI6IkhNdjluYnBiUkRLRTAzNVRUTkgwT3ciLCJzdGF0dXMiOnsic3RhdHVzX2xpc3QiOnsiaWR4Ijo0MDAsInVyaSI6Imh0dHBzOi8vamFucy1teXNxbC1sb2NrLXNlcnZlci5qYW5zLmluZm8vamFucy1hdXRoL3Jlc3R2MS9zdGF0dXNfbGlzdCJ9fX0" + + ".bNXIL4f1lqvLoS49iMSZJORD2mJ9MWYCM5a8nyAiLqKy_fEqvqb-g1X6SgVeS2dJ9aFV-KRrfcjl0zSSQq6mBn-1pAostlMgV-lkOBi7rCbJUMwmdN7Bv7Op8EyuD44_4hHRYhAXOXYv1CcjkyXtv-A9gDxNjHvhHVvpjaizcIMXVRrPxTTQgZF7r7n0t13La2E0vOxzzsgcWQjJukAY8HYybtoRL4JFswBIWPcgET9Btg9mZghDMlvs0yiLVQfiGUZYcmxCCEQinjtutKgONP0Gv6xVMdsXMUpgXGZi6PCiEaEWButMwBauc9RJWEHbd7C4muKoAQ6_tFNuS_eoRw"; + + /** + * JWT 2 – contains only {@code health.write} scope. + */ + private static final String RAW_JWT_2 = + "eyJraWQiOiJjb25uZWN0XzNjMzJjMTkzLTY5ZjYtNDBkYS1iNmQyLTE3ODY0YmJkYzU1MV9zaWdfcnMyNTYiLCJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9" + + ".eyJhdWQiOiIwZTk5NDAzMC1lZTg4LTRmMTQtYWUwOC04YjBkYTEwNDQwMjkiLCJzdWIiOiIwZTk5NDAzMC1lZTg4LTRmMTQtYWUwOC04YjBkYTEwNDQwMjkiLCJ4NXQjUzI1NiI6IiIsIm5iZiI6MTc3ODYxNDE2MSwic2NvcGUiOlsiaHR0cHM6Ly9qYW5zLmlvL29hdXRoL2xvY2svaGVhbHRoLndyaXRlIl0sImlzcyI6Imh0dHBzOi8vamFucy1teXNxbC1sb2NrLXNlcnZlci5qYW5zLmluZm8iLCJ0b2tlbl90eXBlIjoiQmVhcmVyIiwiZXhwIjoxNzc4NjE0NDYxLCJpYXQiOjE3Nzg2MTQxNjEsImNsaWVudF9pZCI6IjBlOTk0MDMwLWVlODgtNGYxNC1hZTA4LThiMGRhMTA0NDAyOSIsImp0aSI6InRVMGVQb0haU0RPSjJ5Z0EyZFRnc3ciLCJzdGF0dXMiOnsic3RhdHVzX2xpc3QiOnsiaWR4Ijo0MDEsInVyaSI6Imh0dHBzOi8vamFucy1teXNxbC1sb2NrLXNlcnZlci5qYW5zLmluZm8vamFucy1hdXRoL3Jlc3R2MS9zdGF0dXNfbGlzdCJ9fX0" + + ".iIN4rKz4wnGgijhNWJqY_RqYNx_7zT0hdevnU6wqRwiLp3rQG3c4ouv8P6X4CbiaxERzABbrjsS-4JcW2H2oLpAsuJGhJtr-HExe3iLs_OQ2_4NDwo0k2KJ5e_zGP6Wykr6mQ8WhvGIfURk1aLirLCsegKhH1b26tSp6i8z7z-etNLwGjVPDfw6vV01kYJ0_O_tSf0HuLkGTPf34ld86CUNbPf2cE9Q4uqX_3xVTtMW0ffmOhDo8Qs2dL96xs8O6ah-Rvp6UVjcD4A1qbVImN6USE70nEndmtDR_rvfsCBiL-htkgChTDZymceTcOn00NOvWB2I00rvSy7FdWwNAFQ"; + + /** + * JWT 3 – contains only {@code log.write} scope. + */ + private static final String RAW_JWT_3 = + "eyJraWQiOiJjb25uZWN0XzNjMzJjMTkzLTY5ZjYtNDBkYS1iNmQyLTE3ODY0YmJkYzU1MV9zaWdfcnMyNTYiLCJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9" + + ".eyJhdWQiOiIwZTk5NDAzMC1lZTg4LTRmMTQtYWUwOC04YjBkYTEwNDQwMjkiLCJzdWIiOiIwZTk5NDAzMC1lZTg4LTRmMTQtYWUwOC04YjBkYTEwNDQwMjkiLCJ4NXQjUzI1NiI6IiIsIm5iZiI6MTc3ODYxNzc2MSwic2NvcGUiOlsiaHR0cHM6Ly9qYW5zLmlvL29hdXRoL2xvY2svbG9nLndyaXRlIl0sImlzcyI6Imh0dHBzOi8vamFucy1teXNxbC1sb2NrLXNlcnZlci5qYW5zLmluZm8iLCJ0b2tlbl90eXBlIjoiQmVhcmVyIiwiZXhwIjoxNzc4NjE4MDYxLCJpYXQiOjE3Nzg2MTc3NjEsImNsaWVudF9pZCI6IjBlOTk0MDMwLWVlODgtNGYxNC1hZTA4LThiMGRhMTA0NDAyOSIsImp0aSI6IjVxUWRHYWl4VEgybkQ5Z0Y2WDFPaXciLCJzdGF0dXMiOnsic3RhdHVzX2xpc3QiOnsiaWR4Ijo1MDAsInVyaSI6Imh0dHBzOi8vamFucy1teXNxbC1sb2NrLXNlcnZlci5qYW5zLmluZm8vamFucy1hdXRoL3Jlc3R2MS9zdGF0dXNfbGlzdCJ9fX0" + + ".Q7xieptgb5r9eXqjI5BCSDv_ITtzZXbsXoyqcjsYw0PonF6z3c5XjiSPPrXVUU9dY_HQUrd4ib3U7oIQrKtfXcjJ2pMNuTZ0vPRCcZM_XqqbV3IewUbztabDKDNpK0pSaNZy9V1SslHjW_vQoVDnclJL-w2usyXlMVnFub92GV3ldBZ9cB4UYVRovrzG_UxCa8FI-WkikYoET-vIiHbS5yP3EXlRKwP2pWwhHKwhAC7sjbnYW8ApgYVAmvAnWqwPcaY_Bl-UobDHGBr0b0FhLtMIZvGevo1KdQE5dJwiflZOgiUZiYJU9uJ-tklD2gd5Pq-7g1-DW9Fvsmo2WVDcHw"; + + // ─── @ProtectedCedarlingApi annotation parameter constants ────────────────── + + /** Cedar action that maps to HTTP POST requests. */ + private static final String ACTION_POST = "Jans::Action::\"POST\""; + + /** Cedar entity type for HTTP requests. */ + private static final String RESOURCE_TYPE = "Jans::HTTP_Request"; + + // Permission IDs – must match the {@code id} attribute of each annotation + private static final String ID_LOG = "lock_audit_log_write"; + private static final String ID_HEALTH = "lock_audit_health_write"; + private static final String ID_TELEMETRY = "lock_audit_telemetry_write"; + + // Endpoint paths – must match the {@code path} attribute of each annotation + private static final String PATH_LOG = "/audit/log/bulk"; + private static final String PATH_HEALTH = "/audit/health/bulk"; + private static final String PATH_TELEMETRY = "/audit/health/bulk"; + + /** + * The token map key expected by Cedarling's native authorize input. + * Taken verbatim from {@code CedarlingAuthorizationService.CEDARLING_JANS_ACCESS_TOKEN}. + */ + private static final String ACCESS_TOKEN_KEY = CedarlingAuthorizationService.CEDARLING_JANS_ACCESS_TOKEN; + + // ─── Native adapter + runtime-patched tokens ──────────────────────────────── + + /** + * Native Cedarling adapter initialised directly from the policy store. + * Replaces the {@code CedarlingAuthorizationService} wrapper that was + * used in the original test. + */ + private CedarlingAdapter cedarlingAdapter; + + /** + * JWT tokens with their {@code exp} claims patched to one hour in the future. + * Patched once before all tests to keep the value stable across the entire run. + */ + private String jwt1; + private String jwt2; + private String jwt3; + + // ─── Lifecycle ────────────────────────────────────────────────────────────── + + /** + * Performs one-time initialisation before any test in this class runs: + *

    + *
  1. Loads {@code lock_policy_store.json} from the test classpath.
  2. + *
  3. Builds a {@link BootstrapConfig} and initialises the native + * {@link CedarlingAdapter} directly – no CDI, no service wrapper.
  4. + *
  5. Patches the {@code exp} claims of all three JWT tokens.
  6. + *
+ */ + @BeforeAll + void setUpAdapterAndTokens() throws Exception { + CedarlingAdapter ad = new CedarlingAdapter(); + // ── 1. Load policy store from test resources ────────────────────────── + String currentDir = System.getProperty("user.dir"); + String policyStoreFn = currentDir + "/target/test-classes/test-policy-store"; + + // ── 2. Build BootstrapConfig and initialise the native adapter directly + // (mirrors the logic from CedarlingAuthorizationService.init()) + BootstrapConfig bootstrapConfig = BootstrapConfig.builder() + .applicationName("lock-integration-test") + .policyStoreLocalFn(policyStoreFn) + .logType(LogType.STD_OUT) + .logLevel(LogLevel.TRACE) + // Signature validation disabled so exp-patched tokens are accepted + .jwtSigValidation(false) + .jwtStatusValidation(false) + .build(); + + cedarlingAdapter = new CedarlingAdapter(); + + String jsonConfig = bootstrapConfig.toJsonConfig(); + log.info("Cedarling JSON configuration: {}", jsonConfig); + cedarlingAdapter.loadFromJson(jsonConfig); + log.info("Cedarling initialized successfully with trusted issuers count: {}", cedarlingAdapter.loadedTrustedIssuersCount()); + + log.info("CedarlingAdapter initialised successfully"); + + // ── 3. Patch exp claims so the tokens are not expired during the test run + jwt1 = withFutureExp(RAW_JWT_1); + jwt2 = withFutureExp(RAW_JWT_2); + jwt3 = withFutureExp(RAW_JWT_3); + } + + /** Calls the adapter's {@code close()} after the entire test class completes. */ + @AfterAll + void tearDown() { + if (cedarlingAdapter != null) { + cedarlingAdapter.close(); + } + } + + // =========================================================================== + // JWT 1 – scopes: health.write + telemetry.write + log.write + // Expected: all three endpoints are ALLOWED + // =========================================================================== + @Nested + @DisplayName("JWT 1 (health.write + telemetry.write + log.write)") + class Jwt1Tests { + + @Test + @DisplayName("POST /audit/log/bulk → ALLOWED (log.write scope present)") + void jwt1_logEndpoint_isAllowed() { + assertTrue( + authorize(jwt1, ACTION_POST, ID_LOG, PATH_LOG), + "JWT 1 must be authorized for /audit/log/bulk"); + } + + @Test + @DisplayName("POST /audit/health/bulk → ALLOWED (health.write scope present)") + void jwt1_healthEndpoint_isAllowed() { + assertTrue( + authorize(jwt1, ACTION_POST, ID_HEALTH, PATH_HEALTH), + "JWT 1 must be authorized for /audit/health/bulk"); + } + + @Test + @DisplayName("POST /audit/telemetry → ALLOWED (telemetry.write scope present)") + void jwt1_telemetryEndpoint_isAllowed() { + assertTrue( + authorize(jwt1, ACTION_POST, ID_TELEMETRY, PATH_TELEMETRY), + "JWT 1 must be authorized for /audit/telemetry"); + } + } + + // =========================================================================== + // JWT 2 – scopes: health.write only + // Expected: /health endpoint ALLOWED; /log and /telemetry endpoints DENIED + // =========================================================================== + @Nested + @DisplayName("JWT 2 (health.write only)") + class Jwt2Tests { + + @Test + @DisplayName("POST /audit/health/bulk → ALLOWED (health.write scope present)") + void jwt2_healthEndpoint_isAllowed() { + assertTrue( + authorize(jwt2, ACTION_POST, ID_HEALTH, PATH_HEALTH), + "JWT 2 must be authorized for /audit/health/bulk"); + } + + @Test + @DisplayName("POST /audit/log/bulk → DENIED (log.write scope absent)") + void jwt2_logEndpoint_isDenied() { + assertFalse( + authorize(jwt2, ACTION_POST, ID_LOG, PATH_LOG), + "JWT 2 must NOT be authorized for /audit/log/bulk (missing log.write)"); + } + + @Test + @DisplayName("POST /audit/telemetry → DENIED (telemetry.write scope absent)") + void jwt2_telemetryEndpoint_isDenied() { + assertFalse( + authorize(jwt2, ACTION_POST, ID_TELEMETRY, PATH_TELEMETRY), + "JWT 2 must NOT be authorized for /audit/telemetry (missing telemetry.write)"); + } + } + + // =========================================================================== + // JWT 3 – scopes: log.write only + // Expected: /log endpoint ALLOWED; /health and /telemetry endpoints DENIED + // =========================================================================== + @Nested + @DisplayName("JWT 3 (log.write only)") + class Jwt3Tests { + + @Test + @DisplayName("POST /audit/log/bulk → ALLOWED (log.write scope present)") + void jwt3_logEndpoint_isAllowed() { + assertTrue( + authorize(jwt3, ACTION_POST, ID_LOG, PATH_LOG), + "JWT 3 must be authorized for /audit/log/bulk"); + } + + @Test + @DisplayName("POST /audit/health/bulk → DENIED (health.write scope absent)") + void jwt3_healthEndpoint_isDenied() { + assertFalse( + authorize(jwt3, ACTION_POST, ID_HEALTH, PATH_HEALTH), + "JWT 3 must NOT be authorized for /audit/health/bulk (missing health.write)"); + } + + @Test + @DisplayName("POST /audit/telemetry → DENIED (telemetry.write scope absent)") + void jwt3_telemetryEndpoint_isDenied() { + assertFalse( + authorize(jwt3, ACTION_POST, ID_TELEMETRY, PATH_TELEMETRY), + "JWT 3 must NOT be authorized for /audit/telemetry (missing telemetry.write)"); + } + } + + // =========================================================================== + // Helpers + // =========================================================================== + + /** + * Calls the native {@link CedarlingAdapter#authorize} directly, constructing + * the token map and resource exactly the same way as the production code in + * {@code CedarlingProtectionService}. + * + * @param accessToken the JWT access token (after exp patching) + * @param action Cedar action string (e.g. {@code Jans::Action::"POST"}) + * @param permId permission id from {@code @ProtectedCedarlingApi.id()} + * @param path endpoint path from {@code @ProtectedCedarlingApi.path()} + * @return {@code true} when Cedarling grants the request + */ + private boolean authorize(String accessToken, String action, String permId, String path) { + Map tokens = Map.of(ACCESS_TOKEN_KEY, accessToken); + try { + JSONObject resourceObject = new JSONObject(Optional.ofNullable( + buildResource(permId, path)).map(Map.class::cast).orElse(Collections.emptyMap())); + + JSONObject contextObject = new JSONObject(Optional.ofNullable( + buildContext()).map(Map.class::cast).orElse(Collections.emptyMap())); + + MultiIssuerAuthorizeResult res = cedarlingAdapter.authorizeMultiIssuer(tokens, action, resourceObject, contextObject); + + if (res == null) { + log.error("Authorization response is empty for request with tokens: {}, action: {}, resource: {}, context: {}", + tokens, action, resourceObject, contextObject); + return false; + } + + String requestId = res.getRequestId(); + log.info("Authorization workload decision {} for requestId {}, tokens: {}, action: {}, resource: {}, context: {}", + res.getDecision(), requestId, tokens, action, resourceObject, contextObject); + + return res.getDecision(); + } catch (Exception e) { + log.error("Authorization call failed for permId={} path={}: {}", permId, path, e.getMessage(), e); + return false; + } + } + + /** + * Constructs the Cedar resource map exactly as + * {@code CedarlingProtectionService.getCedarlingResource()} does at runtime. + * + *

The numeric {@code id} inside {@code cedar_entity_mapping} is derived from + * {@link CedarlingPermission#hashCode()}, matching production behaviour exactly. + * + * @param permId the permission ID ({@code @ProtectedCedarlingApi.id()}) + * @param path the endpoint path ({@code @ProtectedCedarlingApi.path()}) + * @return resource map ready for the native authorize call + */ + private static Map buildResource(String permId, String path) { + Map resource = new HashMap<>(); + resource.put("cedar_entity_mapping", + Map.of("entity_type", RESOURCE_TYPE, "id", permId)); + resource.put("url", + Map.of("host", "", "path", path, "protocol", "")); + resource.put("header", Collections.emptyMap()); + + return resource; + } + + /** + * Returns an empty context map – mirrors {@code CedarlingProtectionService.getCedarlingContext()}. + */ + private static Map buildContext() { + return Collections.emptyMap(); + } +} \ No newline at end of file From 8b3301971a432955809200b1e483f46b20764e84 Mon Sep 17 00:00:00 2001 From: Yuriy Date: Thu, 28 May 2026 13:56:18 +0300 Subject: [PATCH 5/6] feat(jans-lock): handle Cedarling https requests in integration tests Signed-off-by: Yuriy --- jans-bom/pom.xml | 59 +- jans-lock/lock-server/cedarling/pom.xml | 53 + .../cedarling/config/BootstrapConfig.java | 31 +- .../CedarlingAuthorizationService.java | 46 +- .../service/CedarlingProtectionService.java | 5 +- .../lock/cedarling/BaseCedarlingTest.java | 2 +- .../cedarling/CedarlingIntegrationTest.java | 4 +- ...ngAuthorizationServiceIntegrationTest.java | 332 ++++++ .../CedarlingProtectionServiceTest.java | 694 ++++++++++++ .../telemetry/BaseWireMockHttpTest.java | 87 ++ .../CedarlingTelemetryIntegrationTest.java | 995 ++++++++++++++++++ .../ws/rs/audit/AuditRestWebService.java | 6 +- .../ws/rs/stat/StatRestWebService.java | 4 +- 13 files changed, 2264 insertions(+), 54 deletions(-) create mode 100644 jans-lock/lock-server/cedarling/src/test/java/io/jans/lock/cedarling/service/CedarlingAuthorizationServiceIntegrationTest.java create mode 100644 jans-lock/lock-server/cedarling/src/test/java/io/jans/lock/cedarling/service/CedarlingProtectionServiceTest.java create mode 100644 jans-lock/lock-server/cedarling/src/test/java/io/jans/lock/cedarling/telemetry/BaseWireMockHttpTest.java create mode 100644 jans-lock/lock-server/cedarling/src/test/java/io/jans/lock/cedarling/telemetry/CedarlingTelemetryIntegrationTest.java diff --git a/jans-bom/pom.xml b/jans-bom/pom.xml index 157f827577a..d37d90992dc 100644 --- a/jans-bom/pom.xml +++ b/jans-bom/pom.xml @@ -65,8 +65,9 @@ 2.22.2 1.79 - 5.9.2 - 5.1.1 + 5.11.4 + 5.23.0 + 4.0.0-beta.34 11.0.24 @@ -923,42 +924,56 @@ 4.0.0.Final test - + + + + org.junit + junit-bom + ${junit.version} + pom + import + + + org.mockito + mockito-bom + ${mockito.version} + pom + import + - org.junit.jupiter - junit-jupiter-api - ${junit.jupiter.version} + org.mockito + mockito-core + ${mockito.version} test - org.junit.jupiter - junit-jupiter-engine - ${junit.jupiter.version} + io.rest-assured + rest-assured + 6.0.0 test - org.junit.jupiter - junit-jupiter-params - ${junit.jupiter.version} + org.wiremock + wiremock-junit5 + ${wiremock.version} test - - org.mockito - mockito-core - ${mockito.version} + org.wiremock + wiremock-jetty + ${wiremock.version} test - org.mockito - mockito-inline - ${mockito.version} + org.wiremock + wiremock-httpclient-apache5 + ${wiremock.version} test - org.mockito - mockito-junit-jupiter - ${mockito.version} + org.wiremock + wiremock-grpc-extension + 0.11.0 test diff --git a/jans-lock/lock-server/cedarling/pom.xml b/jans-lock/lock-server/cedarling/pom.xml index 3857b649f9a..97644b3c9f4 100644 --- a/jans-lock/lock-server/cedarling/pom.xml +++ b/jans-lock/lock-server/cedarling/pom.xml @@ -13,6 +13,12 @@ 0.0.0-nightly + + 15 + 15 + 15 + + @@ -83,6 +89,53 @@ swagger-core-jakarta + + + org.junit.jupiter + junit-jupiter-engine + test + + + org.mockito + mockito-core + test + + + org.mockito + mockito-junit-jupiter + test + + + io.rest-assured + rest-assured + test + + + org.wiremock + wiremock-jetty + test + + + org.wiremock + wiremock-junit5 + test + + + org.wiremock + wiremock-jetty + test + + + org.wiremock + wiremock-httpclient-apache5 + test + + + jakarta.servlet + jakarta.servlet-api + 6.0.0 + test + io.jans diff --git a/jans-lock/lock-server/cedarling/src/main/java/io/jans/lock/cedarling/config/BootstrapConfig.java b/jans-lock/lock-server/cedarling/src/main/java/io/jans/lock/cedarling/config/BootstrapConfig.java index b7f5e35bb6e..f5158f6b7cf 100644 --- a/jans-lock/lock-server/cedarling/src/main/java/io/jans/lock/cedarling/config/BootstrapConfig.java +++ b/jans-lock/lock-server/cedarling/src/main/java/io/jans/lock/cedarling/config/BootstrapConfig.java @@ -21,6 +21,8 @@ /** * Configuration class for Cedarling initialization using Enums for Logging. + * + * @author Author Date: 12/05/2026 */ public class BootstrapConfig { @@ -42,11 +44,14 @@ public class BootstrapConfig { public static final String CEDARLING_LOCK = "CEDARLING_LOCK"; public static final String CEDARLING_LOCK_SERVER_CONFIGURATION_URI = "CEDARLING_LOCK_SERVER_CONFIGURATION_URI"; - public static final String CEDARLING_LOCK_DYNAMIC_CONFIGURATION = "CEDARLING_LOCK_DYNAMIC_CONFIGURATION"; + public static final String CEDARLING_LOCK_SSA_JWT = "CEDARLING_LOCK_SSA_JWT"; + public static final String CEDARLING_LOCK_ACCESS_TOKEN_JWT = "CEDARLING_LOCK_ACCESS_TOKEN_JWT"; public static final String CEDARLING_LOCK_HEALTH_INTERVAL = "CEDARLING_LOCK_HEALTH_INTERVAL"; + public static final String CEDARLING_LOCK_DYNAMIC_CONFIGURATION = "CEDARLING_LOCK_DYNAMIC_CONFIGURATION"; public static final String CEDARLING_LOCK_TELEMETRY_INTERVAL = "CEDARLING_LOCK_TELEMETRY_INTERVAL"; public static final String CEDARLING_LOCK_LISTEN_SSE = "CEDARLING_LOCK_LISTEN_SSE"; - + public static final String CEDARLING_LOCK_ACCEPT_INVALID_CERTS = "CEDARLING_LOCK_ACCEPT_INVALID_CERTS"; + public static final String CEDARLING_MAX_DEFAULT_ENTITIES = "CEDARLING_MAX_DEFAULT_ENTITIES"; public static final String CEDARLING_MAX_BASE64_SIZE = "CEDARLING_MAX_BASE64_SIZE"; @@ -67,6 +72,9 @@ public class BootstrapConfig { private String[] jwtSignatureAlgorithmsSupported; private boolean lock; + private boolean lockAcceptInvalidCerts; + private String lockSsaJwt; + private String lockAccessTokenJwt; private String lockServerConfigurationUri; private boolean lockDynamicConfiguration; private int lockHealthInterval; @@ -90,12 +98,17 @@ private BootstrapConfig(Builder builder) { this.jwtSigValidation = builder.jwtSigValidation; this.jwtStatusValidation = builder.jwtStatusValidation; this.jwtSignatureAlgorithmsSupported = builder.jwtSignatureAlgorithmsSupported; + this.lock = builder.lock; + this.lockSsaJwt = builder.lockSsaJwt; + this.lockAccessTokenJwt = builder.lockAccessTokenJwt; + this.lockAcceptInvalidCerts = builder.lockAcceptInvalidCerts; this.lockServerConfigurationUri = builder.lockServerConfigurationUri; this.lockDynamicConfiguration = builder.lockDynamicConfiguration; this.lockHealthInterval = builder.lockHealthInterval; this.lockTelemetryInterval = builder.lockTelemetryInterval; this.lockListenSse = builder.lockListenSse; + this.maxDefaultEntities = builder.maxDefaultEntities; this.maxBase64Size = builder.maxBase64Size; } @@ -118,17 +131,23 @@ public String toJsonConfig() { jo.put(CEDARLING_LOG_TTL, logTtl); jo.put(CEDARLING_LOCAL_JWKS, localJwks); + jo.put(CEDARLING_POLICY_STORE_LOCAL, policyStoreLocal); jo.put(CEDARLING_POLICY_STORE_LOCAL_FN, policyStoreLocalFn); jo.put(CEDARLING_JWT_SIG_VALIDATION, toEnabled(jwtSigValidation)); jo.put(CEDARLING_JWT_STATUS_VALIDATION, toEnabled(jwtStatusValidation)); jo.put(CEDARLING_JWT_SIGNATURE_ALGORITHMS_SUPPORTED, new JSONArray(Arrays.asList(jwtSignatureAlgorithmsSupported))); + jo.put(CEDARLING_LOCK, toEnabled(lock)); + jo.put(CEDARLING_LOCK_SSA_JWT, lockSsaJwt); + jo.put(CEDARLING_LOCK_ACCESS_TOKEN_JWT, lockAccessTokenJwt); + jo.put(CEDARLING_LOCK_ACCEPT_INVALID_CERTS, toEnabled(lockAcceptInvalidCerts)); jo.put(CEDARLING_LOCK_SERVER_CONFIGURATION_URI, lockServerConfigurationUri); jo.put(CEDARLING_LOCK_DYNAMIC_CONFIGURATION, toEnabled(lockDynamicConfiguration)); jo.put(CEDARLING_LOCK_HEALTH_INTERVAL, lockHealthInterval); jo.put(CEDARLING_LOCK_TELEMETRY_INTERVAL, lockTelemetryInterval); jo.put(CEDARLING_LOCK_LISTEN_SSE, toEnabled(lockListenSse)); + jo.put(CEDARLING_MAX_DEFAULT_ENTITIES, maxDefaultEntities); jo.put(CEDARLING_MAX_BASE64_SIZE, maxBase64Size); @@ -143,7 +162,7 @@ private String toEnabled(boolean value) { * Builder class for BootstrapConfig. */ public static class Builder { - private String applicationName = "App"; + private String applicationName = "App"; private String policyStoreUri = ""; private LogType logType = LogType.MEMORY; private LogLevel logLevel = LogLevel.DEBUG; @@ -155,6 +174,9 @@ public static class Builder { private boolean jwtStatusValidation = false; private String[] jwtSignatureAlgorithmsSupported = {"HS256", "RS256"}; private boolean lock = false; + private boolean lockAcceptInvalidCerts = false; + private String lockSsaJwt; + private String lockAccessTokenJwt; private String lockServerConfigurationUri = null; private boolean lockDynamicConfiguration = false; private int lockHealthInterval = 0; @@ -177,6 +199,9 @@ protected Builder() {} public Builder jwtStatusValidation(boolean jwtStatusValidation) { this.jwtStatusValidation = jwtStatusValidation; return this; } public Builder jwtSignatureAlgorithmsSupported(String[] algorithms) { this.jwtSignatureAlgorithmsSupported = algorithms; return this; } public Builder lock(boolean lock) { this.lock = lock; return this; } + public Builder lockAcceptInvalidCerts(boolean lockAcceptInvalidCerts) { this.lockAcceptInvalidCerts = lockAcceptInvalidCerts; return this; } + public Builder lockSsaJwt(String lockSsaJwt) { this.lockSsaJwt = lockSsaJwt; return this; } + public Builder lockAccessTokenJwt(String lockAccessTokenJwt) { this.lockAccessTokenJwt = lockAccessTokenJwt; return this; } public Builder lockServerConfigurationUri(String uri) { this.lockServerConfigurationUri = uri; return this; } public Builder lockDynamicConfiguration(boolean dynamic) { this.lockDynamicConfiguration = dynamic; return this; } public Builder lockHealthInterval(int interval) { this.lockHealthInterval = interval; return this; } diff --git a/jans-lock/lock-server/cedarling/src/main/java/io/jans/lock/cedarling/service/CedarlingAuthorizationService.java b/jans-lock/lock-server/cedarling/src/main/java/io/jans/lock/cedarling/service/CedarlingAuthorizationService.java index 67efd195087..5fe898495d0 100644 --- a/jans-lock/lock-server/cedarling/src/main/java/io/jans/lock/cedarling/service/CedarlingAuthorizationService.java +++ b/jans-lock/lock-server/cedarling/src/main/java/io/jans/lock/cedarling/service/CedarlingAuthorizationService.java @@ -22,18 +22,17 @@ import jakarta.annotation.PreDestroy; import jakarta.enterprise.context.ApplicationScoped; import jakarta.inject.Inject; -import uniffi.cedarling_uniffi.AuthorizeResult; import uniffi.cedarling_uniffi.CedarlingException; import uniffi.cedarling_uniffi.MultiIssuerAuthorizeResult; /** - * * @author Yuriy Movchan Date: 10/08/2022 */ @ApplicationScoped public class CedarlingAuthorizationService { - public static final String CEDARLING_JANS_ACCESS_TOKEN = "Jans::Access_Token"; + public static final String CEDARLING_JANS_ACCESS_TOKEN = "Jans::Access_token"; + public static final String CEDARLING_LOCK_POLICY_STORE_RESOURCE_NAME = "lock-policy-store"; @Inject private Logger log; @@ -44,6 +43,7 @@ public class CedarlingAuthorizationService { @Inject private CedarlingPolicyConfiguration policyConfiguration; + private String policyStoreLocalFn = CEDARLING_LOCK_POLICY_STORE_RESOURCE_NAME; private CedarlingAdapter cedarlingAdapter; private boolean initialized = false; @@ -72,14 +72,7 @@ public void destroy() { } private CedarlingAdapter initAdapter(CedarlingConfiguration cedarConf) { - BootstrapConfig config = BootstrapConfig.builder() - .applicationName("Lock Server") - .policyStoreLocal(policyConfiguration.getPolicy()) - .jwtSigValidation(false) - .jwtSigValidation(false) - .logType(cedarConf.getLogType()) - .logLevel(cedarConf.getLogLevel()) - .build(); + BootstrapConfig config = prepareBootstrapConfig(cedarConf); CedarlingAdapter initCedarlingAdapter = null; try { @@ -89,9 +82,8 @@ private CedarlingAdapter initAdapter(CedarlingConfiguration cedarConf) { if (log.isTraceEnabled()) { log.trace("Cedarling JSON configuration: {}", jsonConfig); } - initCedarlingAdapter.loadFromJson(jsonConfig); - log.info("Cedarling initialized successfully"); + log.info("Cedarling initialized successfully with trusted issuers count: {}", initCedarlingAdapter.loadedTrustedIssuersCount()); return initCedarlingAdapter; } catch (CedarlingException ex) { log.error("Failed to initialize Cedarling!", ex); @@ -110,6 +102,18 @@ private CedarlingAdapter initAdapter(CedarlingConfiguration cedarConf) { return null; } + protected BootstrapConfig prepareBootstrapConfig(CedarlingConfiguration cedarConf) { + BootstrapConfig config = BootstrapConfig.builder() + .applicationName("Lock Server") + .policyStoreLocalFn(policyStoreLocalFn) + .jwtStatusValidation(false) + .jwtSigValidation(false) + .logType(cedarConf.getLogType()) + .logLevel(cedarConf.getLogLevel()) + .build(); + return config; + } + public boolean authorize(Map tokens, String action, Map resource, Map context) { JSONObject resourceObject = new JSONObject(Optional.ofNullable( resource).map(Map.class::cast).orElse(Collections.emptyMap())); @@ -126,8 +130,13 @@ public boolean authorize(Map tokens, String action, JSONObject r log.debug("Before executing authorization request. tokens: {}, action: {}, resource: {}, context: {}", tokens, action, resource, context); } + + if (!tokens.containsKey(CEDARLING_JANS_ACCESS_TOKEN)) { + log.error("Missing token '{}' in tokens map. Failed to execute Cedarling authorize", CEDARLING_JANS_ACCESS_TOKEN); + return false; + } + MultiIssuerAuthorizeResult res = cedarlingAdapter.authorizeMultiIssuer(tokens, action, resource, context); - if (res == null) { log.error("Authorization response is empty for request with tokens: {}, action: {}, resource: {}, context: {}", tokens, action, resource, context); @@ -135,9 +144,12 @@ public boolean authorize(Map tokens, String action, JSONObject r } String requestId = res.getRequestId(); - if (log.isDebugEnabled()) { - log.debug("Authorization workload decision {} for requestId {}, tokens: {}, action: {}, resource: {}, context: {}", - res.getDecision(), requestId, tokens, action, resource, context); + if (res.getDecision()) { + log.info("Authorization decision is PERMIT for requestId {}, tokens: {}, action: {}, resource: {}, context: {}", + requestId, tokens, action, resource, context); + } else { + log.info("Authorization decision is DENY for requestId {}, tokens: {}, action: {}, resource: {}, context: {}", + requestId, tokens, action, resource, context); } return res.getDecision(); diff --git a/jans-lock/lock-server/cedarling/src/main/java/io/jans/lock/cedarling/service/CedarlingProtectionService.java b/jans-lock/lock-server/cedarling/src/main/java/io/jans/lock/cedarling/service/CedarlingProtectionService.java index 32d36f8ad86..8ebd8dde8ff 100644 --- a/jans-lock/lock-server/cedarling/src/main/java/io/jans/lock/cedarling/service/CedarlingProtectionService.java +++ b/jans-lock/lock-server/cedarling/src/main/java/io/jans/lock/cedarling/service/CedarlingProtectionService.java @@ -45,8 +45,6 @@ @ApplicationScoped public class CedarlingProtectionService implements CedarlingProtection { - private static final String CEDARLING_JANS_ACCESS_TOKEN = "Jans::Access_Token"; - @Inject private Logger log; @@ -169,7 +167,8 @@ private Map getCedarlingResource(CedarlingPermission requestedPe id = id > 0 ? id : -id; map.putAll( Map.of("cedar_entity_mapping", - Map.of("entity_type", requestedPermission.getResource(), "id", id) + Map.of("entity_type", requestedPermission.getResource(), + "id", id) ) ); map.putAll( diff --git a/jans-lock/lock-server/cedarling/src/test/java/io/jans/lock/cedarling/BaseCedarlingTest.java b/jans-lock/lock-server/cedarling/src/test/java/io/jans/lock/cedarling/BaseCedarlingTest.java index f51286b9fc5..f10b6118ca0 100644 --- a/jans-lock/lock-server/cedarling/src/test/java/io/jans/lock/cedarling/BaseCedarlingTest.java +++ b/jans-lock/lock-server/cedarling/src/test/java/io/jans/lock/cedarling/BaseCedarlingTest.java @@ -1,7 +1,7 @@ /* * Janssen Project software is available under the Apache License (2004). See http://www.apache.org/licenses/ for full text. * - * Copyright (c) 2025, Janssen Project + * Copyright (c) 2026, Janssen Project */ package io.jans.lock.cedarling; diff --git a/jans-lock/lock-server/cedarling/src/test/java/io/jans/lock/cedarling/CedarlingIntegrationTest.java b/jans-lock/lock-server/cedarling/src/test/java/io/jans/lock/cedarling/CedarlingIntegrationTest.java index 95c1f9649a6..b9aa2b7e6f3 100644 --- a/jans-lock/lock-server/cedarling/src/test/java/io/jans/lock/cedarling/CedarlingIntegrationTest.java +++ b/jans-lock/lock-server/cedarling/src/test/java/io/jans/lock/cedarling/CedarlingIntegrationTest.java @@ -1,7 +1,7 @@ /* * Janssen Project software is available under the Apache License (2004). See http://www.apache.org/licenses/ for full text. * - * Copyright (c) 2025, Janssen Project + * Copyright (c) 2026, Janssen Project */ package io.jans.lock.cedarling; @@ -31,7 +31,6 @@ import io.jans.lock.cedarling.service.CedarlingAuthorizationService; import io.jans.lock.model.config.cedarling.LogLevel; import io.jans.lock.model.config.cedarling.LogType; -import uniffi.cedarling_uniffi.CedarlingException; import uniffi.cedarling_uniffi.MultiIssuerAuthorizeResult; /** @@ -152,7 +151,6 @@ class CedarlingIntegrationTest extends BaseCedarlingTest { */ @BeforeAll void setUpAdapterAndTokens() throws Exception { - CedarlingAdapter ad = new CedarlingAdapter(); // ── 1. Load policy store from test resources ────────────────────────── String currentDir = System.getProperty("user.dir"); String policyStoreFn = currentDir + "/target/test-classes/test-policy-store"; diff --git a/jans-lock/lock-server/cedarling/src/test/java/io/jans/lock/cedarling/service/CedarlingAuthorizationServiceIntegrationTest.java b/jans-lock/lock-server/cedarling/src/test/java/io/jans/lock/cedarling/service/CedarlingAuthorizationServiceIntegrationTest.java new file mode 100644 index 00000000000..cc4de34ab44 --- /dev/null +++ b/jans-lock/lock-server/cedarling/src/test/java/io/jans/lock/cedarling/service/CedarlingAuthorizationServiceIntegrationTest.java @@ -0,0 +1,332 @@ +/* + * Janssen Project software is available under the Apache License (2004). See http://www.apache.org/licenses/ for full text. + * + * Copyright (c) 2026, Janssen Project + */ +package io.jans.lock.cedarling.service; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; +import org.junit.jupiter.api.TestInstance.Lifecycle; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import io.jans.lock.cedarling.BaseCedarlingTest; +import io.jans.lock.cedarling.model.CedarlingPermission; +import io.jans.lock.model.config.AppConfiguration; +import io.jans.lock.model.config.cedarling.CedarlingConfiguration; +import io.jans.lock.model.config.cedarling.CedarlingPolicyConfiguration; +import io.jans.lock.model.config.cedarling.LogLevel; +import io.jans.lock.model.config.cedarling.LogType; + +/** + * Integration tests for {@link CedarlingAuthorizationService}. + * + *

These tests exercise the Cedarling policy engine end-to-end using real JWT access + * tokens and a real policy store loaded from {@code src/test/resources/lock_policy_store.json}. + * + *

Token matrix

+ *
+ * ┌────────┬──────────────────────────────────────────────────────────────┬──────────────────────────────┐
+ * │ Token  │ OAuth scopes                                                 │ Allowed endpoints            │
+ * ├────────┼──────────────────────────────────────────────────────────────┼──────────────────────────────┤
+ * │ JWT 1  │ lock/health.write, lock/telemetry.write, lock/log.write      │ /log, /health, /telemetry    │
+ * │ JWT 2  │ lock/health.write                                            │ /health only                 │
+ * │ JWT 3  │ lock/log.write                                               │ /log only                    │
+ * └────────┴──────────────────────────────────────────────────────────────┴──────────────────────────────┘
+ * 
+ * + *

JWT expiry: The tokens are real tokens whose {@code exp} claim is in + * the past. Before each test run the payloads are patched (base64-decode → update exp → + * base64-encode) to a value 1 hour in the future. Because the adapter is initialised with + * {@code jwtSigValidation(false)} the modified signature is still accepted by Cedarling. + * + *

Prerequisites: The native Cedarling UniFFI library must be on the + * dynamic-library path ({@code LD_LIBRARY_PATH} / {@code java.library.path}). + * + * @author Yuriy Movchan Date: 12/05/2026 + */ +@TestInstance(Lifecycle.PER_CLASS) +@DisplayName("CedarlingAuthorizationService – Integration Tests") +class CedarlingAuthorizationServiceIntegrationTest extends BaseCedarlingTest { + + // ─── Raw JWT strings (exp claims are patched at runtime) ──────────────────── + + /** + * JWT 1 – contains all three scopes: + * {@code health.write}, {@code telemetry.write}, {@code log.write}. + */ + private static final String RAW_JWT_1 = + "eyJraWQiOiJjb25uZWN0XzNjMzJjMTkzLTY5ZjYtNDBkYS1iNmQyLTE3ODY0YmJkYzU1MV9zaWdfcnMyNTYiLCJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9" + + ".eyJhdWQiOiIwZTk5NDAzMC1lZTg4LTRmMTQtYWUwOC04YjBkYTEwNDQwMjkiLCJzdWIiOiIwZTk5NDAzMC1lZTg4LTRmMTQtYWUwOC04YjBkYTEwNDQwMjkiLCJ4NXQjUzI1NiI6IiIsIm5iZiI6MTc3ODYxNDA2Niwic2NvcGUiOlsiaHR0cHM6Ly9qYW5zLmlvL29hdXRoL2xvY2svaGVhbHRoLndyaXRlIiwiaHR0cHM6Ly9qYW5zLmlvL29hdXRoL2xvY2svdGVsZW1ldHJ5LndyaXRlIiwiaHR0cHM6Ly9qYW5zLmlvL29hdXRoL2xvY2svbG9nLndyaXRlIl0sImlzcyI6Imh0dHBzOi8vamFucy1teXNxbC1sb2NrLXNlcnZlci5qYW5zLmluZm8iLCJ0b2tlbl90eXBlIjoiQmVhcmVyIiwiZXhwIjoxNzc4NjE0MzY2LCJpYXQiOjE3Nzg2MTQwNjYsImNsaWVudF9pZCI6IjBlOTk0MDMwLWVlODgtNGYxNC1hZTA4LThiMGRhMTA0NDAyOSIsImp0aSI6IkhNdjluYnBiUkRLRTAzNVRUTkgwT3ciLCJzdGF0dXMiOnsic3RhdHVzX2xpc3QiOnsiaWR4Ijo0MDAsInVyaSI6Imh0dHBzOi8vamFucy1teXNxbC1sb2NrLXNlcnZlci5qYW5zLmluZm8vamFucy1hdXRoL3Jlc3R2MS9zdGF0dXNfbGlzdCJ9fX0" + + ".bNXIL4f1lqvLoS49iMSZJORD2mJ9MWYCM5a8nyAiLqKy_fEqvqb-g1X6SgVeS2dJ9aFV-KRrfcjl0zSSQq6mBn-1pAostlMgV-lkOBi7rCbJUMwmdN7Bv7Op8EyuD44_4hHRYhAXOXYv1CcjkyXtv-A9gDxNjHvhHVvpjaizcIMXVRrPxTTQgZF7r7n0t13La2E0vOxzzsgcWQjJukAY8HYybtoRL4JFswBIWPcgET9Btg9mZghDMlvs0yiLVQfiGUZYcmxCCEQinjtutKgONP0Gv6xVMdsXMUpgXGZi6PCiEaEWButMwBauc9RJWEHbd7C4muKoAQ6_tFNuS_eoRw"; + + /** + * JWT 2 – contains only {@code health.write} scope. + */ + private static final String RAW_JWT_2 = + "eyJraWQiOiJjb25uZWN0XzNjMzJjMTkzLTY5ZjYtNDBkYS1iNmQyLTE3ODY0YmJkYzU1MV9zaWdfcnMyNTYiLCJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9" + + ".eyJhdWQiOiIwZTk5NDAzMC1lZTg4LTRmMTQtYWUwOC04YjBkYTEwNDQwMjkiLCJzdWIiOiIwZTk5NDAzMC1lZTg4LTRmMTQtYWUwOC04YjBkYTEwNDQwMjkiLCJ4NXQjUzI1NiI6IiIsIm5iZiI6MTc3ODYxNDE2MSwic2NvcGUiOlsiaHR0cHM6Ly9qYW5zLmlvL29hdXRoL2xvY2svaGVhbHRoLndyaXRlIl0sImlzcyI6Imh0dHBzOi8vamFucy1teXNxbC1sb2NrLXNlcnZlci5qYW5zLmluZm8iLCJ0b2tlbl90eXBlIjoiQmVhcmVyIiwiZXhwIjoxNzc4NjE0NDYxLCJpYXQiOjE3Nzg2MTQxNjEsImNsaWVudF9pZCI6IjBlOTk0MDMwLWVlODgtNGYxNC1hZTA4LThiMGRhMTA0NDAyOSIsImp0aSI6InRVMGVQb0haU0RPSjJ5Z0EyZFRnc3ciLCJzdGF0dXMiOnsic3RhdHVzX2xpc3QiOnsiaWR4Ijo0MDEsInVyaSI6Imh0dHBzOi8vamFucy1teXNxbC1sb2NrLXNlcnZlci5qYW5zLmluZm8vamFucy1hdXRoL3Jlc3R2MS9zdGF0dXNfbGlzdCJ9fX0" + + ".iIN4rKz4wnGgijhNWJqY_RqYNx_7zT0hdevnU6wqRwiLp3rQG3c4ouv8P6X4CbiaxERzABbrjsS-4JcW2H2oLpAsuJGhJtr-HExe3iLs_OQ2_4NDwo0k2KJ5e_zGP6Wykr6mQ8WhvGIfURk1aLirLCsegKhH1b26tSp6i8z7z-etNLwGjVPDfw6vV01kYJ0_O_tSf0HuLkGTPf34ld86CUNbPf2cE9Q4uqX_3xVTtMW0ffmOhDo8Qs2dL96xs8O6ah-Rvp6UVjcD4A1qbVImN6USE70nEndmtDR_rvfsCBiL-htkgChTDZymceTcOn00NOvWB2I00rvSy7FdWwNAFQ"; + + /** + * JWT 3 – contains only {@code log.write} scope. + */ + private static final String RAW_JWT_3 = + "eyJraWQiOiJjb25uZWN0XzNjMzJjMTkzLTY5ZjYtNDBkYS1iNmQyLTE3ODY0YmJkYzU1MV9zaWdfcnMyNTYiLCJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9" + + ".eyJhdWQiOiIwZTk5NDAzMC1lZTg4LTRmMTQtYWUwOC04YjBkYTEwNDQwMjkiLCJzdWIiOiIwZTk5NDAzMC1lZTg4LTRmMTQtYWUwOC04YjBkYTEwNDQwMjkiLCJ4NXQjUzI1NiI6IiIsIm5iZiI6MTc3ODYxNzc2MSwic2NvcGUiOlsiaHR0cHM6Ly9qYW5zLmlvL29hdXRoL2xvY2svbG9nLndyaXRlIl0sImlzcyI6Imh0dHBzOi8vamFucy1teXNxbC1sb2NrLXNlcnZlci5qYW5zLmluZm8iLCJ0b2tlbl90eXBlIjoiQmVhcmVyIiwiZXhwIjoxNzc4NjE4MDYxLCJpYXQiOjE3Nzg2MTc3NjEsImNsaWVudF9pZCI6IjBlOTk0MDMwLWVlODgtNGYxNC1hZTA4LThiMGRhMTA0NDAyOSIsImp0aSI6IjVxUWRHYWl4VEgybkQ5Z0Y2WDFPaXciLCJzdGF0dXMiOnsic3RhdHVzX2xpc3QiOnsiaWR4Ijo1MDAsInVyaSI6Imh0dHBzOi8vamFucy1teXNxbC1sb2NrLXNlcnZlci5qYW5zLmluZm8vamFucy1hdXRoL3Jlc3R2MS9zdGF0dXNfbGlzdCJ9fX0" + + ".Q7xieptgb5r9eXqjI5BCSDv_ITtzZXbsXoyqcjsYw0PonF6z3c5XjiSPPrXVUU9dY_HQUrd4ib3U7oIQrKtfXcjJ2pMNuTZ0vPRCcZM_XqqbV3IewUbztabDKDNpK0pSaNZy9V1SslHjW_vQoVDnclJL-w2usyXlMVnFub92GV3ldBZ9cB4UYVRovrzG_UxCa8FI-WkikYoET-vIiHbS5yP3EXlRKwP2pWwhHKwhAC7sjbnYW8ApgYVAmvAnWqwPcaY_Bl-UobDHGBr0b0FhLtMIZvGevo1KdQE5dJwiflZOgiUZiYJU9uJ-tklD2gd5Pq-7g1-DW9Fvsmo2WVDcHw"; + + // ─── @ProtectedCedarlingApi annotation parameter constants ────────────────── + + /** Cedar action that maps to HTTP POST requests. */ + private static final String ACTION_POST = "Jans::Action::\"POST\""; + + /** Cedar entity type for HTTP requests (note: trailing quote is intentional, + * it matches the value used in the real @ProtectedCedarlingApi annotations). */ + private static final String RESOURCE_TYPE = "Jans::HTTP_Request"; + + // Permission IDs – must match the {@code id} attribute of each annotation + private static final String ID_LOG = "lock_audit_log_write"; + private static final String ID_HEALTH = "lock_audit_health_write"; + private static final String ID_TELEMETRY = "lock_audit_telemetry_write"; + + // Endpoint paths – must match the {@code path} attribute of each annotation + private static final String PATH_LOG = "/audit/log/bulk"; + private static final String PATH_HEALTH = "/audit/health/bulk"; + private static final String PATH_TELEMETRY = "/audit/health/bulk"; + + /** + * The token map key expected by Cedarling's native authorize input. + * Taken verbatim from {@code CedarlingAuthorizationService.CEDARLING_JANS_ACCESS_TOKEN}. + */ + private static final String ACCESS_TOKEN_KEY = CedarlingAuthorizationService.CEDARLING_JANS_ACCESS_TOKEN; + + // ─── Service under test + runtime-patched tokens ──────────────────────────── + + private CedarlingAuthorizationService authService; + + /** + * JWT tokens with their {@code exp} claims patched to one hour in the future. + * Patched once before all tests to keep the value stable across the entire run. + */ + private String jwt1; + private String jwt2; + private String jwt3; + + // ─── Lifecycle ────────────────────────────────────────────────────────────── + + /** + * Performs one-time initialisation before any test in this class runs: + *

    + *
  1. Loads {@code lock_policy_store.json} from the test classpath.
  2. + *
  3. Creates a fully wired {@link CedarlingAuthorizationService} via field + * injection (bypassing CDI) and calls its {@code @PostConstruct} method.
  4. + *
  5. Patches the {@code exp} claims of all three JWT tokens.
  6. + *
+ */ + @BeforeAll + void setUpServiceAndTokens() throws Exception { + // ── 1. Load policy store from test resources ────────────────────────── + String currentDir = System.getProperty("user.dir"); + String policyStoreFn = currentDir + "/target/test-classes/test-policy-store"; + + // ── 2. Build mocked CDI dependencies ───────────────────────────────── + Logger log = LoggerFactory.getLogger(CedarlingAuthorizationService.class); + AppConfiguration appConfiguration = mock(AppConfiguration.class); + CedarlingPolicyConfiguration policyConfig = mock(CedarlingPolicyConfiguration.class); + CedarlingConfiguration cedarConf = mock(CedarlingConfiguration.class); + + when(cedarConf.isEnabled()).thenReturn(true); + // Use null for log-related settings; BootstrapConfig treats them as "disabled" / default + when(cedarConf.getLogType()).thenReturn(LogType.STD_OUT); + when(cedarConf.getLogLevel()).thenReturn(LogLevel.TRACE); + when(appConfiguration.getCedarlingConfiguration()).thenReturn(cedarConf); + + // ── 3. Wire the service manually (CDI is not available in unit tests) ─ + authService = new CedarlingAuthorizationService(); + injectField(authService, "log", log); + injectField(authService, "appConfiguration", appConfiguration); + injectField(authService, "policyConfiguration", policyConfig); + injectField(authService, "policyStoreLocalFn", policyStoreFn); + + // Trigger @PostConstruct – initialises CedarlingAdapter with the real policy + authService.init(); + + // ── 4. Patch exp claims so the tokens are not expired during the test run + jwt1 = withFutureExp(RAW_JWT_1); + jwt2 = withFutureExp(RAW_JWT_2); + jwt3 = withFutureExp(RAW_JWT_3); + } + + /** Calls the adapter's {@code close()} after the entire test class completes. */ + @AfterAll + void tearDown() { + if (authService != null) { + authService.destroy(); + } + } + + // =========================================================================== + // JWT 1 – scopes: health.write + telemetry.write + log.write + // Expected: all three endpoints are ALLOWED + // =========================================================================== + @Nested + @DisplayName("JWT 1 (health.write + telemetry.write + log.write)") + class Jwt1Tests { + + @Test + @DisplayName("POST /audit/log/bulk → ALLOWED (log.write scope present)") + void jwt1_logEndpoint_isAllowed() { + assertTrue( + authorize(jwt1, ACTION_POST, ID_LOG, PATH_LOG), + "JWT 1 must be authorized for /audit/log/bulk"); + } + + @Test + @DisplayName("POST /audit/health/bulk → ALLOWED (health.write scope present)") + void jwt1_healthEndpoint_isAllowed() { + assertTrue( + authorize(jwt1, ACTION_POST, ID_HEALTH, PATH_HEALTH), + "JWT 1 must be authorized for /audit/health/bulk"); + } + + @Test + @DisplayName("POST /audit/telemetry → ALLOWED (telemetry.write scope present)") + void jwt1_telemetryEndpoint_isAllowed() { + assertTrue( + authorize(jwt1, ACTION_POST, ID_TELEMETRY, PATH_TELEMETRY), + "JWT 1 must be authorized for /audit/telemetry"); + } + } + + // =========================================================================== + // JWT 2 – scopes: health.write only + // Expected: /health endpoint ALLOWED; /log and /telemetry endpoints DENIED + // =========================================================================== + @Nested + @DisplayName("JWT 2 (health.write only)") + class Jwt2Tests { + + @Test + @DisplayName("POST /audit/health/bulk → ALLOWED (health.write scope present)") + void jwt2_healthEndpoint_isAllowed() { + assertTrue( + authorize(jwt2, ACTION_POST, ID_HEALTH, PATH_HEALTH), + "JWT 2 must be authorized for /audit/health/bulk"); + } + + @Test + @DisplayName("POST /audit/log/bulk → DENIED (log.write scope absent)") + void jwt2_logEndpoint_isDenied() { + assertFalse( + authorize(jwt2, ACTION_POST, ID_LOG, PATH_LOG), + "JWT 2 must NOT be authorized for /audit/log/bulk (missing log.write)"); + } + + @Test + @DisplayName("POST /audit/telemetry → DENIED (telemetry.write scope absent)") + void jwt2_telemetryEndpoint_isDenied() { + assertFalse( + authorize(jwt2, ACTION_POST, ID_TELEMETRY, PATH_TELEMETRY), + "JWT 2 must NOT be authorized for /audit/telemetry (missing telemetry.write)"); + } + } + + // =========================================================================== + // JWT 3 – scopes: log.write only + // Expected: /log endpoint ALLOWED; /health and /telemetry endpoints DENIED + // =========================================================================== + @Nested + @DisplayName("JWT 3 (log.write only)") + class Jwt3Tests { + + @Test + @DisplayName("POST /audit/log/bulk → ALLOWED (log.write scope present)") + void jwt3_logEndpoint_isAllowed() { + assertTrue( + authorize(jwt3, ACTION_POST, ID_LOG, PATH_LOG), + "JWT 3 must be authorized for /audit/log/bulk"); + } + + @Test + @DisplayName("POST /audit/health/bulk → DENIED (health.write scope absent)") + void jwt3_healthEndpoint_isDenied() { + assertFalse( + authorize(jwt3, ACTION_POST, ID_HEALTH, PATH_HEALTH), + "JWT 3 must NOT be authorized for /audit/health/bulk (missing health.write)"); + } + + @Test + @DisplayName("POST /audit/telemetry → DENIED (telemetry.write scope absent)") + void jwt3_telemetryEndpoint_isDenied() { + assertFalse( + authorize(jwt3, ACTION_POST, ID_TELEMETRY, PATH_TELEMETRY), + "JWT 3 must NOT be authorized for /audit/telemetry (missing telemetry.write)"); + } + } + + // =========================================================================== + // Helpers + // =========================================================================== + + /** + * Calls {@link CedarlingAuthorizationService#authorize} with a resource map + * built the same way as {@code CedarlingProtectionService.getCedarlingResource()}. + * + * @param accessToken the JWT access token (after exp patching) + * @param action Cedar action string (e.g. {@code Jans::Action::"POST"}) + * @param permId permission id from {@code @ProtectedCedarlingApi.id()} + * @param path endpoint path from {@code @ProtectedCedarlingApi.path()} + * @return {@code true} when Cedarling grants the request + */ + private boolean authorize(String accessToken, String action, String permId, String path) { + Map tokens = Map.of(ACCESS_TOKEN_KEY, accessToken); + + return authService.authorize(tokens, action, buildResource(permId, path), buildContext()); + } + + /** + * Constructs the Cedar resource map that mirrors what + * {@code CedarlingProtectionService.getCedarlingResource()} produces at runtime. + * + *

The numeric {@code id} inside {@code cedar_entity_mapping} is derived from the + * {@link CedarlingPermission#hashCode()} of a permission built with the supplied + * arguments, matching the production code exactly. + * + * @param permId the permission ID ({@code @ProtectedCedarlingApi.id()}) + * @param path the endpoint path ({@code @ProtectedCedarlingApi.path()}) + * @return resource map ready for {@code CedarlingAuthorizationService.authorize()} + */ + private static Map buildResource(String permId, String path) { + Map resource = new HashMap<>(); + resource.put("cedar_entity_mapping", + Map.of("entity_type", RESOURCE_TYPE, "id", permId)); + resource.put("url", + Map.of("host", "", "path", path, "protocol", "")); + resource.put("header", Collections.emptyMap()); + + return resource; + } + + /** + * Returns an empty context map. The production code also sends an empty map, + * so we match that behaviour here. + */ + private static Map buildContext() { + return Collections.emptyMap(); + } +} \ No newline at end of file diff --git a/jans-lock/lock-server/cedarling/src/test/java/io/jans/lock/cedarling/service/CedarlingProtectionServiceTest.java b/jans-lock/lock-server/cedarling/src/test/java/io/jans/lock/cedarling/service/CedarlingProtectionServiceTest.java new file mode 100644 index 00000000000..d1b9ee88c1b --- /dev/null +++ b/jans-lock/lock-server/cedarling/src/test/java/io/jans/lock/cedarling/service/CedarlingProtectionServiceTest.java @@ -0,0 +1,694 @@ +/* + * Janssen Project software is available under the Apache License (2004). See http://www.apache.org/licenses/ for full text. + * + * Copyright (c) 2026, Janssen Project + */ + +package io.jans.lock.cedarling.service; + +import static jakarta.ws.rs.core.Response.Status.FORBIDDEN; +import static jakarta.ws.rs.core.Response.Status.INTERNAL_SERVER_ERROR; +import static jakarta.ws.rs.core.Response.Status.UNAUTHORIZED; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyMap; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.lenient; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.mockConstruction; +import static org.mockito.Mockito.mockStatic; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.lang.reflect.Field; +import java.net.URL; +import java.util.Map; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.MockedConstruction; +import org.mockito.MockedStatic; +import org.mockito.junit.jupiter.MockitoExtension; +import org.slf4j.Logger; + +import com.fasterxml.jackson.databind.ObjectMapper; + +import io.jans.as.client.OpenIdConfigurationResponse; +import io.jans.as.model.crypto.AuthCryptoProvider; +import io.jans.as.model.crypto.signature.SignatureAlgorithm; +import io.jans.as.model.exception.InvalidJwtException; +import io.jans.as.model.jwt.Jwt; +import io.jans.as.model.jwt.JwtClaimName; +import io.jans.as.model.jwt.JwtClaims; +import io.jans.as.model.jwt.JwtHeader; +import io.jans.lock.cedarling.service.security.api.ProtectedCedarlingApi; +import io.jans.lock.model.config.AppConfiguration; +import jakarta.ws.rs.container.ResourceInfo; +import jakarta.ws.rs.core.Response; + +/** + * Unit tests for {@link CedarlingProtectionService#processAuthorization}. + * + *

The method under test implements a multi-step authorization filter: + *

    + *
  1. Presence check – bearer token must be present.
  2. + *
  3. Permission resolution – at least one {@code @ProtectedCedarlingApi} annotation + * must be found on the JAX-RS resource class, method, or its interfaces.
  4. + *
  5. JWT validation – token must be parseable as a JWT.
  6. + *
  7. Issuer check – JWT {@code iss} claim must match the OIDC discovery document.
  8. + *
  9. Expiry check – JWT {@code exp} claim must be in the future.
  10. + *
  11. Algorithm check – HMAC-family algorithms are rejected.
  12. + *
  13. Signature verification – cryptographic signature must be valid.
  14. + *
  15. Cedarling policy evaluation – the Cedarling engine must grant access + * for every declared permission.
  16. + *
+ * + *

A {@code null} return value from {@code processAuthorization} means + * "pass-through" – the JAX-RS filter will allow the request to continue. + * + * @author Author Date: 12/05/2026 + */ +@ExtendWith(MockitoExtension.class) +class CedarlingProtectionServiceTest { + + // ─── Constants ────────────────────────────────────────────────────────────── + + private static final String TEST_ISSUER = "https://idp.example.com"; + private static final String TEST_JWKS_URI = "https://idp.example.com/jwks"; + /** Minimal three-part placeholder used wherever a JWT string is required. */ + private static final String DUMMY_JWT = "header.payload.signature"; + + // ─── Mocks & SUT ──────────────────────────────────────────────────────────── + + @Mock private Logger log; + @Mock private AppConfiguration appConfiguration; + @Mock private CedarlingAuthorizationService authorizationService; + /** Injected as a mock so that JWKS HTTP calls can be stubbed without networking. */ + @Mock private ObjectMapper mapper; + + @InjectMocks + private CedarlingProtectionService service; + + // ─── JAX-RS Resource Fixtures ─────────────────────────────────────────────── + + /** + * A plain resource class that carries no {@code @ProtectedCedarlingApi} + * annotation and implements no interfaces. Used to simulate "empty + * permissions" scenarios. + */ + static class PlainResource { + public void plainMethod() { /* no-op */ } + } + + /** + * An interface-level {@code @ProtectedCedarlingApi} annotation. + * The service resolves permissions by walking the resource class's + * interface hierarchy, so placing the annotation here is the most + * common real-world pattern. + */ + @ProtectedCedarlingApi( + action = "Jans::Action::\"POST\"", + resource = "Jans::HTTP_Request", + id = "lock_audit_log_write", + path = "/audit/log/bulk" + ) + interface SecuredInterface { + void securedMethod(); + } + + /** Resource class that inherits its permission from {@link SecuredInterface}. */ + static class SecuredResource implements SecuredInterface { + @Override + public void securedMethod() { /* no-op */ } + } + + /** + * A resource class that has a {@code @ProtectedCedarlingApi} on the class + * itself AND on the method, producing two permissions. Used to verify + * short-circuit behaviour when the first permission is denied. + */ + @ProtectedCedarlingApi( + action = "Jans::Action::\"POST\"", + resource = "Jans::HTTP_Request", + id = "lock_audit_health_write", + path = "/audit/health/bulk" + ) + static class MultiPermissionResource { + @ProtectedCedarlingApi( + action = "Jans::Action::\"POST\"", + resource = "Jans::HTTP_Request", + id = "lock_audit_log_write", + path = "/audit/log/bulk" + ) + public void multiMethod() { /* no-op */ } + } + + // ─── Setup ────────────────────────────────────────────────────────────────── + + /** + * Injects the fields that are normally initialised in the {@code @PostConstruct} + * lifecycle method. We bypass the real {@code init()} to avoid network calls + * during unit tests. + */ + @BeforeEach + void setUp() throws Exception { + // Inject the mocked ObjectMapper (avoids real HTTP for JWKS fetches) + injectField("mapper", mapper); + + // Build a minimal OpenID configuration stub + OpenIdConfigurationResponse oidcConfig = mock(OpenIdConfigurationResponse.class); + lenient().when(oidcConfig.getIssuer()).thenReturn(TEST_ISSUER); + lenient().when(oidcConfig.getJwksUri()).thenReturn(TEST_JWKS_URI); + injectField("oidcConfig", oidcConfig); + } + + // =========================================================================== + // 1. Missing / blank bearer token → 401 Unauthorized + // =========================================================================== + + @Nested + @DisplayName("Bearer token presence check") + class BearerTokenPresenceTests { + + @Test + @DisplayName("null token returns 401") + void nullToken_returns401() { + Response response = service.processAuthorization(null, mock(ResourceInfo.class)); + + assertStatus(UNAUTHORIZED, response); + } + + @Test + @DisplayName("empty string token returns 401") + void emptyToken_returns401() { + Response response = service.processAuthorization("", mock(ResourceInfo.class)); + + assertStatus(UNAUTHORIZED, response); + } + } + + // =========================================================================== + // 2. No @ProtectedCedarlingApi annotation resolved → 500 Internal Server Error + // =========================================================================== + + @Nested + @DisplayName("Permission annotation resolution") + class PermissionResolutionTests { + + @Test + @DisplayName("resource with no annotation on class, method, or interfaces returns 500") + void noAnnotation_returns500() { + // PlainResource has no @ProtectedCedarlingApi and no interfaces, + // so the permissions list stays empty. + Response response = service.processAuthorization( + "Bearer some-token-value", + mockResourceInfo(PlainResource.class, "plainMethod")); + + assertStatus(INTERNAL_SERVER_ERROR, response); + assertNotNull(response.getEntity(), "Error detail should be present in response body"); + } + } + + // =========================================================================== + // 3. Token is not a valid JWT → 403 Forbidden + // =========================================================================== + + @Nested + @DisplayName("JWT parsing") + class JwtParsingTests { + + @Test + @DisplayName("plain text token (not JWT-encoded) returns 403") + void plainTextToken_returns403() { + // Jwt.parse() will throw InvalidJwtException for a non-JWT string, + // causing tokenAsJwt() to return null. + Response response = service.processAuthorization( + "Bearer i-am-not-a-jwt", + mockResourceInfo(SecuredResource.class, "securedMethod")); + + assertStatus(FORBIDDEN, response); + assertTrue( + response.getEntity().toString().contains("JWT"), + "Response body should mention JWT"); + } + + @Test + @DisplayName("random base64 string that looks like JWT parts returns 403") + void malformedBase64Token_returns403() { + Response response = service.processAuthorization( + "Bearer aaa.bbb.ccc", + mockResourceInfo(SecuredResource.class, "securedMethod")); + + assertStatus(FORBIDDEN, response); + } + } + + // =========================================================================== + // 4. JWT issuer mismatch → 403 Forbidden + // =========================================================================== + + @Nested + @DisplayName("JWT issuer validation") + class IssuerValidationTests { + + @Test + @DisplayName("JWT with wrong issuer returns 403") + void wrongIssuer_returns403() throws Exception { + try (MockedStatic jwtStatic = mockStatic(Jwt.class)) { + Jwt jwt = buildMockJwt("https://untrusted-idp.example.com", futureEpoch(), SignatureAlgorithm.RS256); + jwtStatic.when(() -> Jwt.parse(anyString())).thenReturn(jwt); + + Response response = service.processAuthorization( + "Bearer " + DUMMY_JWT, + mockResourceInfo(SecuredResource.class, "securedMethod")); + + assertStatus(FORBIDDEN, response); + assertTrue( + response.getEntity().toString().contains("issuer"), + "Response body should mention 'issuer'"); + } + } + } + + // =========================================================================== + // 5. Expired JWT → 403 Forbidden + // =========================================================================== + + @Nested + @DisplayName("JWT expiry validation") + class ExpiryValidationTests { + + @Test + @DisplayName("expired JWT returns 403") + void expiredJwt_returns403() throws Exception { + try (MockedStatic jwtStatic = mockStatic(Jwt.class)) { + Jwt jwt = buildMockJwt(TEST_ISSUER, pastEpoch(), SignatureAlgorithm.RS256); + jwtStatic.when(() -> Jwt.parse(anyString())).thenReturn(jwt); + + Response response = service.processAuthorization( + "Bearer " + DUMMY_JWT, + mockResourceInfo(SecuredResource.class, "securedMethod")); + + assertStatus(FORBIDDEN, response); + assertTrue( + response.getEntity().toString().contains("Expired"), + "Response body should mention expiry"); + } + } + + @Test + @DisplayName("JWT with exp claim absent (defaults to epoch 0) returns 403") + void missingExpClaim_returns403() throws Exception { + try (MockedStatic jwtStatic = mockStatic(Jwt.class)) { + // When getClaimAsInteger returns null, the service defaults to 0 + // (epoch 0 = 1970-01-01), so the token is immediately expired. + Jwt jwt = buildMockJwtNullExp(TEST_ISSUER, SignatureAlgorithm.RS256); + jwtStatic.when(() -> Jwt.parse(anyString())).thenReturn(jwt); + + Response response = service.processAuthorization( + "Bearer " + DUMMY_JWT, + mockResourceInfo(SecuredResource.class, "securedMethod")); + + assertStatus(FORBIDDEN, response); + } + } + } + + // =========================================================================== + // 6. HMAC-family algorithm → 500 Internal Server Error + // =========================================================================== + + @Nested + @DisplayName("Signing algorithm validation") + class AlgorithmValidationTests { + + @Test + @DisplayName("HS256 (HMAC) algorithm returns 500 with HMAC message") + void hmacAlgorithmHS256_returns500() throws Exception { + try (MockedStatic jwtStatic = mockStatic(Jwt.class)) { + Jwt jwt = buildMockJwt(TEST_ISSUER, futureEpoch(), SignatureAlgorithm.HS256); + jwtStatic.when(() -> Jwt.parse(anyString())).thenReturn(jwt); + + Response response = service.processAuthorization( + "Bearer " + DUMMY_JWT, + mockResourceInfo(SecuredResource.class, "securedMethod")); + + assertStatus(INTERNAL_SERVER_ERROR, response); + assertTrue( + response.getEntity().toString().contains("HMAC"), + "Response body should explain that HMAC is not allowed"); + } + } + + @Test + @DisplayName("HS384 (HMAC) algorithm also returns 500") + void hmacAlgorithmHS384_returns500() throws Exception { + try (MockedStatic jwtStatic = mockStatic(Jwt.class)) { + Jwt jwt = buildMockJwt(TEST_ISSUER, futureEpoch(), SignatureAlgorithm.HS384); + jwtStatic.when(() -> Jwt.parse(anyString())).thenReturn(jwt); + + Response response = service.processAuthorization( + "Bearer " + DUMMY_JWT, + mockResourceInfo(SecuredResource.class, "securedMethod")); + + assertStatus(INTERNAL_SERVER_ERROR, response); + } + } + } + + // =========================================================================== + // 7. Signature verification fails → 403 Forbidden + // =========================================================================== + + @Nested + @DisplayName("JWT signature verification") + class SignatureVerificationTests { + + @Test + @DisplayName("invalid signature returns 403") + void invalidSignature_returns403() throws Exception { + try (MockedStatic jwtStatic = mockStatic(Jwt.class); + MockedConstruction cryptoMock = mockConstruction( + AuthCryptoProvider.class, + (mock, ctx) -> when( + mock.verifySignature(any(), any(), any(), any(), any(), any())) + .thenReturn(false))) { + + Jwt jwt = buildMockJwt(TEST_ISSUER, futureEpoch(), SignatureAlgorithm.RS256); + jwtStatic.when(() -> Jwt.parse(anyString())).thenReturn(jwt); + when(mapper.readValue(any(URL.class), eq(Map.class))).thenReturn(Map.of()); + + Response response = service.processAuthorization( + "Bearer " + DUMMY_JWT, + mockResourceInfo(SecuredResource.class, "securedMethod")); + + assertStatus(FORBIDDEN, response); + } + } + } + + // =========================================================================== + // 8. Cedarling authorization decisions + // =========================================================================== + + @Nested + @DisplayName("Cedarling policy evaluation") + class CedarlingAuthorizationTests { + + @Test + @DisplayName("valid JWT + Cedarling grants access → null (pass-through)") + void validJwtAndCedarlingGrants_returnsNull() throws Exception { + try (MockedStatic jwtStatic = mockStatic(Jwt.class); + MockedConstruction cryptoMock = mockConstruction( + AuthCryptoProvider.class, + (mock, ctx) -> when( + mock.verifySignature(any(), any(), any(), any(), any(), any())) + .thenReturn(true))) { + + Jwt jwt = buildMockJwt(TEST_ISSUER, futureEpoch(), SignatureAlgorithm.RS256); + jwtStatic.when(() -> Jwt.parse(anyString())).thenReturn(jwt); + when(mapper.readValue(any(URL.class), eq(Map.class))).thenReturn(Map.of()); + when(authorizationService.authorize(anyMap(), anyString(), anyMap(), anyMap())) + .thenReturn(true); + + Response response = service.processAuthorization( + "Bearer " + DUMMY_JWT, + mockResourceInfo(SecuredResource.class, "securedMethod")); + + // null is the "proceed" signal for the JAX-RS filter + assertNull(response, "Authorized request must return null (pass-through)"); + } + } + + @Test + @DisplayName("valid JWT + Cedarling denies access → 403") + void validJwtAndCedarlingDenies_returns403() throws Exception { + try (MockedStatic jwtStatic = mockStatic(Jwt.class); + MockedConstruction cryptoMock = mockConstruction( + AuthCryptoProvider.class, + (mock, ctx) -> when( + mock.verifySignature(any(), any(), any(), any(), any(), any())) + .thenReturn(true))) { + + Jwt jwt = buildMockJwt(TEST_ISSUER, futureEpoch(), SignatureAlgorithm.RS256); + jwtStatic.when(() -> Jwt.parse(anyString())).thenReturn(jwt); + when(mapper.readValue(any(URL.class), eq(Map.class))).thenReturn(Map.of()); + when(authorizationService.authorize(anyMap(), anyString(), anyMap(), anyMap())) + .thenReturn(false); + + Response response = service.processAuthorization( + "Bearer " + DUMMY_JWT, + mockResourceInfo(SecuredResource.class, "securedMethod")); + + assertStatus(FORBIDDEN, response); + } + } + + /** + * When a resource has multiple permissions (class-level + method-level) and the + * first call to {@code authorizationService.authorize()} returns {@code false}, + * the service must short-circuit and not evaluate further permissions. + */ + @Test + @DisplayName("first permission denied short-circuits remaining checks") + void multiplePermissions_firstDenied_doesNotCheckRemainder() throws Exception { + try (MockedStatic jwtStatic = mockStatic(Jwt.class); + MockedConstruction cryptoMock = mockConstruction( + AuthCryptoProvider.class, + (mock, ctx) -> when( + mock.verifySignature(any(), any(), any(), any(), any(), any())) + .thenReturn(true))) { + + Jwt jwt = buildMockJwt(TEST_ISSUER, futureEpoch(), SignatureAlgorithm.RS256); + jwtStatic.when(() -> Jwt.parse(anyString())).thenReturn(jwt); + when(mapper.readValue(any(URL.class), eq(Map.class))).thenReturn(Map.of()); + + // First call is denied; second should never be reached + when(authorizationService.authorize(anyMap(), anyString(), anyMap(), anyMap())) + .thenReturn(false); + + Response response = service.processAuthorization( + "Bearer " + DUMMY_JWT, + mockResourceInfo(MultiPermissionResource.class, "multiMethod")); + + assertStatus(FORBIDDEN, response); + // MultiPermissionResource has 2 permissions; the service must stop after + // the first denied result — so authorize() is called exactly once. + verify(authorizationService, times(1)) + .authorize(anyMap(), anyString(), anyMap(), anyMap()); + } + } + + @Test + @DisplayName("both permissions granted returns null") + void multiplePermissions_allGranted_returnsNull() throws Exception { + try (MockedStatic jwtStatic = mockStatic(Jwt.class); + MockedConstruction cryptoMock = mockConstruction( + AuthCryptoProvider.class, + (mock, ctx) -> when( + mock.verifySignature(any(), any(), any(), any(), any(), any())) + .thenReturn(true))) { + + Jwt jwt = buildMockJwt(TEST_ISSUER, futureEpoch(), SignatureAlgorithm.RS256); + jwtStatic.when(() -> Jwt.parse(anyString())).thenReturn(jwt); + when(mapper.readValue(any(URL.class), eq(Map.class))).thenReturn(Map.of()); + when(authorizationService.authorize(anyMap(), anyString(), anyMap(), anyMap())) + .thenReturn(true); + + Response response = service.processAuthorization( + "Bearer " + DUMMY_JWT, + mockResourceInfo(MultiPermissionResource.class, "multiMethod")); + + assertNull(response, "All permissions granted must return null (pass-through)"); + // Exactly 2 permissions → authorize() called twice + verify(authorizationService, times(2)) + .authorize(anyMap(), anyString(), anyMap(), anyMap()); + } + } + } + + // =========================================================================== + // 9. Unexpected runtime exception → 500 Internal Server Error + // =========================================================================== + + @Nested + @DisplayName("Exception handling") + class ExceptionHandlingTests { + + @Test + @DisplayName("unexpected exception returns 500 with exception message") + void unexpectedException_returns500() throws Exception { + try (MockedStatic jwtStatic = mockStatic(Jwt.class)) { + jwtStatic.when(() -> Jwt.parse(anyString())) + .thenThrow(new RuntimeException("Unexpected adapter failure")); + + Response response = service.processAuthorization( + "Bearer " + DUMMY_JWT, + mockResourceInfo(SecuredResource.class, "securedMethod")); + + assertStatus(INTERNAL_SERVER_ERROR, response); + assertEquals( + "Unexpected adapter failure", + response.getEntity().toString(), + "Exception message must be surfaced in the response body"); + } + } + + @Test + @DisplayName("JWKS fetch failure returns 500") + void jwksFetchFailure_returns500() throws Exception { + try (MockedStatic jwtStatic = mockStatic(Jwt.class)) { + Jwt jwt = buildMockJwt(TEST_ISSUER, futureEpoch(), SignatureAlgorithm.RS256); + jwtStatic.when(() -> Jwt.parse(anyString())).thenReturn(jwt); + + // Simulate an I/O error when fetching the JWKS endpoint + when(mapper.readValue(any(URL.class), eq(Map.class))) + .thenThrow(new java.io.IOException("Connection refused")); + + Response response = service.processAuthorization( + "Bearer " + DUMMY_JWT, + mockResourceInfo(SecuredResource.class, "securedMethod")); + + assertStatus(INTERNAL_SERVER_ERROR, response); + } + } + } + + // =========================================================================== + // Helper methods + // =========================================================================== + + /** + * Injects a value into a private field of the service under test using reflection. + * + * @param fieldName the exact name of the private field + * @param value the value to inject + */ + private void injectField(String fieldName, Object value) throws Exception { + Field field = CedarlingProtectionService.class.getDeclaredField(fieldName); + field.setAccessible(true); + field.set(service, value); + } + + /** + * Creates a {@link ResourceInfo} mock that returns the specified class and method. + * + * @param resourceClass the JAX-RS resource class to return + * @param methodName the public method name to return + * @return a configured {@link ResourceInfo} mock + */ + @SuppressWarnings("unchecked") + private static ResourceInfo mockResourceInfo(Class resourceClass, String methodName) { + ResourceInfo ri = mock(ResourceInfo.class); + try { + when(ri.getResourceClass()).thenReturn((Class) resourceClass); + when(ri.getResourceMethod()).thenReturn(resourceClass.getMethod(methodName)); + } catch (NoSuchMethodException e) { + fail("Test fixture error – method not found: " + resourceClass.getSimpleName() + "#" + methodName); + } + return ri; + } + + /** + * Asserts that the {@link Response} has the expected HTTP status code. + * + * @param expected expected status + * @param actual actual response (must not be null) + */ + private static void assertStatus(Response.Status expected, Response actual) { + assertNotNull(actual, "Expected a response but got null (pass-through)"); + assertEquals( + expected.getStatusCode(), + actual.getStatus(), + String.format("Expected HTTP %d (%s) but got %d", + expected.getStatusCode(), expected, actual.getStatus())); + } + + /** + * Builds a fully mocked {@link Jwt} with the specified issuer, expiry, and + * signing algorithm. All fields needed by the service under test are stubbed. + * + * @param issuer value for the {@code iss} JWT claim + * @param expEpoch value for the {@code exp} JWT claim (epoch seconds) + * @param algorithm signing algorithm to report via the JWT header + * @return a mocked {@link Jwt} + * @throws InvalidJwtException + */ + private static Jwt buildMockJwt(String issuer, int expEpoch, SignatureAlgorithm algorithm) throws InvalidJwtException { + Jwt jwt = mock(Jwt.class); + + // Stub JWT header + JwtHeader header = mock(JwtHeader.class); + lenient().when(header.getSignatureAlgorithm()).thenReturn(algorithm); + lenient().when(header.getKeyId()).thenReturn("test-key-id"); + lenient().when(jwt.getHeader()).thenReturn(header); + + // Stub JWT claims + JwtClaims claims = mock(JwtClaims.class); + lenient().when(claims.getClaimAsString(JwtClaimName.ISSUER)).thenReturn(issuer); + lenient().when(claims.getClaimAsInteger(JwtClaimName.EXPIRATION_TIME)).thenReturn(expEpoch); + lenient().when(jwt.getClaims()).thenReturn(claims); + + // Stub signing material (needed for signature verification) + lenient().when(jwt.getSigningInput()).thenReturn("signing.input"); + lenient().when(jwt.getEncodedSignature()).thenReturn("encoded-signature"); + + return jwt; + } + + /** + * Same as {@link #buildMockJwt} but returns {@code null} for the {@code exp} + * claim, which the service defaults to epoch 0 (i.e. immediately expired). + * + * @param issuer value for the {@code iss} JWT claim + * @param algorithm signing algorithm to report via the JWT header + * @return a mocked {@link Jwt} with a null {@code exp} claim + * @throws InvalidJwtException + */ + private static Jwt buildMockJwtNullExp(String issuer, SignatureAlgorithm algorithm) throws InvalidJwtException { + Jwt jwt = mock(Jwt.class); + + JwtHeader header = mock(JwtHeader.class); + lenient().when(header.getSignatureAlgorithm()).thenReturn(algorithm); + lenient().when(header.getKeyId()).thenReturn("test-key-id"); + lenient().when(jwt.getHeader()).thenReturn(header); + + JwtClaims claims = mock(JwtClaims.class); + lenient().when(claims.getClaimAsString(JwtClaimName.ISSUER)).thenReturn(issuer); + // null triggers Optional.orElse(0) in the service → token is expired + lenient().when(claims.getClaimAsInteger(JwtClaimName.EXPIRATION_TIME)).thenReturn(null); + lenient().when(jwt.getClaims()).thenReturn(claims); + + lenient().when(jwt.getSigningInput()).thenReturn("signing.input"); + lenient().when(jwt.getEncodedSignature()).thenReturn("encoded-signature"); + + return jwt; + } + + /** + * Returns an {@code exp} value (epoch seconds) 1 hour in the future. + * Tokens with this value are not yet expired. + */ + private static int futureEpoch() { + return (int) (System.currentTimeMillis() / 1000) + 3600; + } + + /** + * Returns an {@code exp} value (epoch seconds) 1 hour in the past. + * Tokens with this value are expired. + */ + private static int pastEpoch() { + return (int) (System.currentTimeMillis() / 1000) - 3600; + } +} \ No newline at end of file diff --git a/jans-lock/lock-server/cedarling/src/test/java/io/jans/lock/cedarling/telemetry/BaseWireMockHttpTest.java b/jans-lock/lock-server/cedarling/src/test/java/io/jans/lock/cedarling/telemetry/BaseWireMockHttpTest.java new file mode 100644 index 00000000000..7415a7b76ab --- /dev/null +++ b/jans-lock/lock-server/cedarling/src/test/java/io/jans/lock/cedarling/telemetry/BaseWireMockHttpTest.java @@ -0,0 +1,87 @@ +/* + * Janssen Project software is available under the Apache License (2004). See http://www.apache.org/licenses/ for full text. + * + * Copyright (c) 2026, Janssen Project + */ + +package io.jans.lock.cedarling.telemetry; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.extension.RegisterExtension; + +import com.github.tomakehurst.wiremock.core.WireMockConfiguration; +import com.github.tomakehurst.wiremock.junit5.WireMockExtension; + +import io.restassured.RestAssured; +import io.restassured.config.SSLConfig; + +/** + * Base test class that provides a pre-configured WireMock server running over + * HTTPS for use by telemetry integration tests. + * + *

What this class provides

+ *
    + *
  • A {@link WireMockExtension} registered as a JUnit 5 extension. The + * server starts on a dynamic (randomly assigned) HTTPS port, so + * tests never clash on CI runners that share a port pool.
  • + *
  • A {@link BeforeEach} hook that configures REST Assured to target the + * WireMock server and to bypass strict TLS validation (WireMock uses a + * self-signed certificate).
  • + *
+ * + *

Usage

+ *
{@code
+ * class MyTest extends BaseWireMockHttpTest {
+ *
+ *     @Test
+ *     void example() {
+ *         wireMockServer.stubFor(get("/ping").willReturn(ok()));
+ *         RestAssured.get("/ping").then().statusCode(200);
+ *     }
+ * }
+ * }
+ * + *

Note: the {@link org.junit.jupiter.api.BeforeAll @BeforeAll} + * lifecycle method in this class is non-static. JUnit 5 requires the concrete + * test class to be annotated with + * {@code @TestInstance(Lifecycle.PER_CLASS)} for non-static {@code @BeforeAll} + * methods to be recognised; subclasses that omit that annotation must override + * {@code setUp()} as a {@code @BeforeEach} method instead. + * + * @author Yuriy Movchan Date: 12/05/2026 + * */ +public abstract class BaseWireMockHttpTest { + + /** + * WireMock server started with a dynamically assigned HTTPS port and an + * auto-generated self-signed TLS certificate. + * + *

Declared {@code protected static} so that subclasses annotated with + * {@code @TestInstance(PER_CLASS)} as well as the default per-method + * lifecycle can both access the single server instance. + */ + @RegisterExtension + protected static WireMockExtension wireMockServer = WireMockExtension.newInstance() + .options(WireMockConfiguration.wireMockConfig() + .dynamicHttpsPort()) // auto-generates self-signed TLS cert + .build(); + + /** + * Configures REST Assured before every test: + *

    + *
  1. Sets the base URI to {@code https://localhost}.
  2. + *
  3. Sets the port to the dynamically assigned HTTPS port.
  4. + *
  5. Enables relaxed HTTPS validation so WireMock's self-signed + * certificate is accepted without importing it into a trust store.
  6. + *
+ */ + @BeforeAll + public void setUp() { + RestAssured.baseURI = "https://localhost"; + RestAssured.port = wireMockServer.getHttpsPort(); + + RestAssured.config = RestAssured.config() + .sslConfig(SSLConfig.sslConfig().relaxedHTTPSValidation()); + } +} \ No newline at end of file diff --git a/jans-lock/lock-server/cedarling/src/test/java/io/jans/lock/cedarling/telemetry/CedarlingTelemetryIntegrationTest.java b/jans-lock/lock-server/cedarling/src/test/java/io/jans/lock/cedarling/telemetry/CedarlingTelemetryIntegrationTest.java new file mode 100644 index 00000000000..02a46adafca --- /dev/null +++ b/jans-lock/lock-server/cedarling/src/test/java/io/jans/lock/cedarling/telemetry/CedarlingTelemetryIntegrationTest.java @@ -0,0 +1,995 @@ +/* + * Janssen Project software is available under the Apache License (2004). See http://www.apache.org/licenses/ for full text. + * + * Copyright (c) 2026, Janssen Project + */ +package io.jans.lock.cedarling.telemetry; + +import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; +import static com.github.tomakehurst.wiremock.client.WireMock.anyUrl; +import static com.github.tomakehurst.wiremock.client.WireMock.get; +import static com.github.tomakehurst.wiremock.client.WireMock.post; +import static com.github.tomakehurst.wiremock.client.WireMock.postRequestedFor; +import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.lang.reflect.Field; +import java.net.URI; +import java.nio.charset.StandardCharsets; +import java.time.Duration; +import java.util.AbstractMap; +import java.util.ArrayList; +import java.util.Base64; +import java.util.Collections; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.stream.Collectors; + +import org.apache.logging.log4j.Level; +import org.apache.logging.log4j.core.config.Configurator; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; +import org.junit.jupiter.api.TestInstance.Lifecycle; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.github.tomakehurst.wiremock.verification.LoggedRequest; + +import io.jans.lock.cedarling.config.BootstrapConfig; +import io.jans.lock.cedarling.service.CedarlingAuthorizationService; +import io.jans.lock.model.config.AppConfiguration; +import io.jans.lock.model.config.cedarling.CedarlingConfiguration; +import io.jans.lock.model.config.cedarling.CedarlingPolicyConfiguration; +import io.jans.lock.model.config.cedarling.LogLevel; +import io.jans.lock.model.config.cedarling.LogType; + +/** + * Integration test that verifies Cedarling pushes health, log, and telemetry + * audit data to a Lock server. A WireMock HTTPS server (inherited from + * {@link BaseWireMockHttpTest}) acts as the Lock server, so no real network + * or external process is required. + * + *

Test structure

+ *
    + *
  1. Setup – WireMock stubs the {@code /.well-known} + * discovery document and all audit endpoints. Cedarling is initialised + * with {@code CEDARLING_LOCK=enabled} and a short telemetry interval + * ({@value #TELEMETRY_INTERVAL_SEC} s) so data arrives quickly.
  2. + *
  3. Round 1 – 5 authorization calls (4 ALLOW + 1 DENY). + * The test waits up to {@value #WAIT_TIMEOUT_SEC} seconds for telemetry, + * captures the payloads, and verifies all required fields.
  4. + *
  5. Reset – WireMock's request journal is cleared to + * give Round 2 a clean baseline.
  6. + *
  7. Round 2 – 7 authorization calls (4 ALLOW + 3 DENY). + * Same capture / verification cycle.
  8. + *
  9. Cross-round comparison – health status must remain + * {@code "ok"}; {@code evaluationRequestsCount} must be ≥ the Round 1 + * value (Cedarling accumulates it globally).
  10. + *
+ * + *

Token matrix

+ *
+ * ┌────────┬────────────────────────────────────────────────────┬──────────────────┐
+ * │ Token  │ OAuth scopes                                       │ Allowed endpoints│
+ * ├────────┼────────────────────────────────────────────────────┼──────────────────┤
+ * │ JWT 1  │ health.write + telemetry.write + log.write         │ all three        │
+ * │ JWT 2  │ health.write only                                  │ /health only     │
+ * │ JWT 3  │ log.write only                                     │ /log only        │
+ * └────────┴────────────────────────────────────────────────────┴──────────────────┘
+ * 
+ * + *

Prerequisites: The native Cedarling UniFFI library must be + * available on the dynamic-library path ({@code LD_LIBRARY_PATH} / + * {@code java.library.path}). + * + * @see BaseWireMockHttpTest + * @see CedarlingAuthorizationService + * + * @author Yuriy Movchan Date: 12/05/2026 + */ +@TestInstance(Lifecycle.PER_CLASS) +@DisplayName("Cedarling Telemetry – Integration Tests") +public class CedarlingTelemetryIntegrationTest extends BaseWireMockHttpTest { + + static { + Configurator.setRootLevel(Level.INFO); + } + + private static final Logger log = LoggerFactory.getLogger(CedarlingTelemetryIntegrationTest.class); + + private static final boolean DUMP_CAPTURED_REQUEST = true; + + // ─── WireMock endpoint paths ───────────────────────────────────────────── + + private static final String WELL_KNOWN_PATH = "/.well-known/lock-server-configuration"; + private static final String HEALTH_PATH = "/jans-lock/api/v1/audit/health"; + private static final String LOG_PATH = "/jans-lock/api/v1/audit/log"; + private static final String TELEMETRY_PATH = "/jans-lock/api/v1/audit/telemetry"; + private static final String BULK_HEALTH_PATH = HEALTH_PATH + "/bulk"; + private static final String BULK_LOG_PATH = LOG_PATH + "/bulk"; + private static final String BULK_TELEMETRY_PATH = TELEMETRY_PATH + "/bulk"; + + // ─── Raw JWT strings – exp claims are patched at runtime ──────────────── + + /** + * JWT 1 – scopes: {@code health.write}, {@code telemetry.write}, + * {@code log.write}. + */ + private static final String RAW_JWT_1 = + "eyJraWQiOiJjb25uZWN0XzNjMzJjMTkzLTY5ZjYtNDBkYS1iNmQyLTE3ODY0YmJkYzU1MV9zaWdfcnMyNTYiLCJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9" + + ".eyJhdWQiOiIwZTk5NDAzMC1lZTg4LTRmMTQtYWUwOC04YjBkYTEwNDQwMjkiLCJzdWIiOiIwZTk5NDAzMC1lZTg4LTRmMTQtYWUwOC04YjBkYTEwNDQwMjkiLC" + + "J4NXQjUzI1NiI6IiIsIm5iZiI6MTc3ODYxNDA2Niwic2NvcGUiOlsiaHR0cHM6Ly9qYW5zLmlvL29hdXRoL2xvY2svaGVhbHRoLndyaXRlIiwiaHR0cHM6Ly9qYW" + + "5zLmlvL29hdXRoL2xvY2svdGVsZW1ldHJ5LndyaXRlIiwiaHR0cHM6Ly9qYW5zLmlvL29hdXRoL2xvY2svbG9nLndyaXRlIl0sImlzcyI6Imh0dHBzOi8vamFucy" + + "1teXNxbC1sb2NrLXNlcnZlci5qYW5zLmluZm8iLCJ0b2tlbl90eXBlIjoiQmVhcmVyIiwiZXhwIjoxNzc4NjE0MzY2LCJpYXQiOjE3Nzg2MTQwNjYsImNsaWVudF" + + "9pZCI6IjBlOTk0MDMwLWVlODgtNGYxNC1hZTA4LThiMGRhMTA0NDAyOSIsImp0aSI6IkhNdjluYnBiUkRLRTAzNVRUTkgwT3ciLCJzdGF0dXMiOnsic3RhdHVzX2" + + "xpc3QiOnsiaWR4Ijo0MDAsInVyaSI6Imh0dHBzOi8vamFucy1teXNxbC1sb2NrLXNlcnZlci5qYW5zLmluZm8vamFucy1hdXRoL3Jlc3R2MS9zdGF0dXNfbGlzdC" + + "J9fX0" + + ".bNXIL4f1lqvLoS49iMSZJORD2mJ9MWYCM5a8nyAiLqKy_fEqvqb-g1X6SgVeS2dJ9aFV-KRrfcjl0zSSQq6mBn-1pAostlMgV-lkOBi7rCbJUMwmdN7Bv7Op8E" + + "yuD44_4hHRYhAXOXYv1CcjkyXtv-A9gDxNjHvhHVvpjaizcIMXVRrPxTTQgZF7r7n0t13La2E0vOxzzsgcWQjJukAY8HYybtoRL4JFswBIWPcgET9Btg9mZghDMl" + + "vs0yiLVQfiGUZYcmxCCEQinjtutKgONP0Gv6xVMdsXMUpgXGZi6PCiEaEWButMwBauc9RJWEHbd7C4muKoAQ6_tFNuS_eoRw"; + + /** JWT 2 – scope: {@code health.write} only. */ + private static final String RAW_JWT_2 = + "eyJraWQiOiJjb25uZWN0XzNjMzJjMTkzLTY5ZjYtNDBkYS1iNmQyLTE3ODY0YmJkYzU1MV9zaWdfcnMyNTYiLCJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9" + + ".eyJhdWQiOiIwZTk5NDAzMC1lZTg4LTRmMTQtYWUwOC04YjBkYTEwNDQwMjkiLCJzdWIiOiIwZTk5NDAzMC1lZTg4LTRmMTQtYWUwOC04YjBkYTEwNDQwMjkiLC" + + "J4NXQjUzI1NiI6IiIsIm5iZiI6MTc3ODYxNDE2MSwic2NvcGUiOlsiaHR0cHM6Ly9qYW5zLmlvL29hdXRoL2xvY2svaGVhbHRoLndyaXRlIl0sImlzcyI6Imh0dH" + + "BzOi8vamFucy1teXNxbC1sb2NrLXNlcnZlci5qYW5zLmluZm8iLCJ0b2tlbl90eXBlIjoiQmVhcmVyIiwiZXhwIjoxNzc4NjE0NDYxLCJpYXQiOjE3Nzg2MTQxNj" + + "EsImNsaWVudF9pZCI6IjBlOTk0MDMwLWVlODgtNGYxNC1hZTA4LThiMGRhMTA0NDAyOSIsImp0aSI6InRVMGVQb0haU0RPSjJ5Z0EyZFRnc3ciLCJzdGF0dXMiOn" + + "sic3RhdHVzX2xpc3QiOnsiaWR4Ijo0MDEsInVyaSI6Imh0dHBzOi8vamFucy1teXNxbC1sb2NrLXNlcnZlci5qYW5zLmluZm8vamFucy1hdXRoL3Jlc3R2MS9zdG" + + "F0dXNfbGlzdCJ9fX0" + + ".iIN4rKz4wnGgijhNWJqY_RqYNx_7zT0hdevnU6wqRwiLp3rQG3c4ouv8P6X4CbiaxERzABbrjsS-4JcW2H2oLpAsuJGhJtr-HExe3iLs_OQ2_4NDwo0k2KJ5e_" + + "zGP6Wykr6mQ8WhvGIfURk1aLirLCsegKhH1b26tSp6i8z7z-etNLwGjVPDfw6vV01kYJ0_O_tSf0HuLkGTPf34ld86CUNbPf2cE9Q4uqX_3xVTtMW0ffmOhDo8Qs" + + "2dL96xs8O6ah-Rvp6UVjcD4A1qbVImN6USE70nEndmtDR_rvfsCBiL-htkgChTDZymceTcOn00NOvWB2I00rvSy7FdWwNAFQ"; + + /** JWT 3 – scope: {@code log.write} only. */ + private static final String RAW_JWT_3 = + "eyJraWQiOiJjb25uZWN0XzNjMzJjMTkzLTY5ZjYtNDBkYS1iNmQyLTE3ODY0YmJkYzU1MV9zaWdfcnMyNTYiLCJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9" + + ".eyJhdWQiOiIwZTk5NDAzMC1lZTg4LTRmMTQtYWUwOC04YjBkYTEwNDQwMjkiLCJzdWIiOiIwZTk5NDAzMC1lZTg4LTRmMTQtYWUwOC04YjBkYTEwNDQwMjkiLC" + + "J4NXQjUzI1NiI6IiIsIm5iZiI6MTc3ODYxNzc2MSwic2NvcGUiOlsiaHR0cHM6Ly9qYW5zLmlvL29hdXRoL2xvY2svbG9nLndyaXRlIl0sImlzcyI6Imh0dHBzOi" + + "8vamFucy1teXNxbC1sb2NrLXNlcnZlci5qYW5zLmluZm8iLCJ0b2tlbl90eXBlIjoiQmVhcmVyIiwiZXhwIjoxNzc4NjE4MDYxLCJpYXQiOjE3Nzg2MTc3NjEsIm" + + "NsaWVudF9pZCI6IjBlOTk0MDMwLWVlODgtNGYxNC1hZTA4LThiMGRhMTA0NDAyOSIsImp0aSI6IjVxUWRHYWl4VEgybkQ5Z0Y2WDFPaXciLCJzdGF0dXMiOnsic3" + + "RhdHVzX2xpc3QiOnsiaWR4Ijo1MDAsInVyaSI6Imh0dHBzOi8vamFucy1teXNxbC1sb2NrLXNlcnZlci5qYW5zLmluZm8vamFucy1hdXRoL3Jlc3R2MS9zdGF0dX" + + "NfbGlzdCJ9fX0" + + ".Q7xieptgb5r9eXqjI5BCSDv_ITtzZXbsXoyqcjsYw0PonF6z3c5XjiSPPrXVUU9dY_HQUrd4ib3U7oIQrKtfXcjJ2pMNuTZ0vPRCcZM_XqqbV3IewUbztabDKD" + + "NpK0pSaNZy9V1SslHjW_vQoVDnclJL-w2usyXlMVnFub92GV3ldBZ9cB4UYVRovrzG_UxCa8FI-WkikYoET-vIiHbS5yP3EXlRKwP2pWwhHKwhAC7sjbnYW8ApgY" + + "VAmvAnWqwPcaY_Bl-UobDHGBr0b0FhLtMIZvGevo1KdQE5dJwiflZOgiUZiYJU9uJ-tklD2gd5Pq-7g1-DW9Fvsmo2WVDcHw"; + + // ─── Cedar action / resource constants (mirror CedarlingAuthorizationServiceIntegrationTest) ── + /** + * Lock endpoints access token + */ + private static final String RAW_LOCK_ACCESS_TOKEN_JWT = + "eyJraWQiOiJjb25uZWN0X2RjNzViZWZjLWU4N2QtNDMyZi1hOWExLTczYjE0YzJhNjUyMl9zaWdfcnMyNTYiLCJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9" + + ".eyJhdWQiOiJmNDUwMTQ1Zi1hYjgyLTRiY2UtOTdjZi02MjQ2YjFjNmIxYTYiLCJzdWIiOiJmNDUwMTQ1Zi1hYjgyLTRiY2UtOTdjZi02MjQ2YjFjNmIxYTYiLC" + + "J4NXQjUzI1NiI6IiIsIm5iZiI6MTc3OTk1ODYyNCwic2NvcGUiOlsiaHR0cHM6Ly9qYW5zLmlvL29hdXRoL2xvY2svaGVhbHRoLndyaXRlIiwiaHR0cHM6Ly9qYW" + + "5zLmlvL29hdXRoL2xvY2svdGVsZW1ldHJ5LndyaXRlIiwiaHR0cHM6Ly9qYW5zLmlvL29hdXRoL2xvY2svbG9nLndyaXRlIl0sImlzcyI6Imh0dHBzOi8vamFucy" + + "1teXNxbC1sb2NrLXNlcnZlci5qYW5zLmluZm8iLCJ0b2tlbl90eXBlIjoiQmVhcmVyIiwiZXhwIjoxNzc5OTU4OTI0LCJpYXQiOjE3Nzk5NTg2MjQsImNsaWVudF" + + "9pZCI6ImY0NTAxNDVmLWFiODItNGJjZS05N2NmLTYyNDZiMWM2YjFhNiIsImp0aSI6IjNOU2ZLM1hQU25XZDlNWDFDNlFhT1EiLCJzdGF0dXMiOnsic3RhdHVzX2" + + "xpc3QiOnsiaWR4Ijo0MDEsInVyaSI6Imh0dHBzOi8vamFucy1teXNxbC1sb2NrLXNlcnZlci5qYW5zLmluZm8vamFucy1hdXRoL3Jlc3R2MS9zdGF0dXNfbGlzdC" + + "J9fX0" + + ".YSMkLZIm0JzIIiuShrXbZwLT5Bm58nBpz2fcaGEP4zyyDE0Te1T3WKmJZRsyRNPN3QgIwa9b02C-lGTqUJ9YkylTolLitJHgy7aVhfestGYTZ_r0gvfcGYVF8h" + + "zsk5k11U-hb9SGbZOXOvuis998fCXolG-UUaYj7VGjU8xreGLgmEx7Otmfpi2bjenQ0DGFjo82XAVzgqO7gwGT-5zohBQ8uNQcKKASGj4g2NtVcjXqmxBS9huI7e" + + "dAJxFPlZ5J7gghfJAIARDLm8UrYgNEHqVdQPDOrAdnIOE5n0I4oJad5fl5luyKSNmd6sL4hi82OR7Ldig3XIjxyHQ7VJYZhA"; + + // ─── Cedar action / resource constants (mirror CedarlingAuthorizationServiceIntegrationTest) ── + + private static final String ACTION_POST = "Jans::Action::\"POST\""; + private static final String RESOURCE_TYPE = "Jans::HTTP_Request"; + + private static final String ID_LOG = "lock_audit_log_write"; + private static final String ID_HEALTH = "lock_audit_health_write"; + private static final String ID_TELEMETRY = "lock_audit_telemetry_write"; + + private static final String PATH_LOG = "/audit/log/bulk"; + private static final String PATH_HEALTH = "/audit/health/bulk"; + private static final String PATH_TELEMETRY = "/audit/telemetry/bulk"; + + private static final String ACCESS_TOKEN_KEY = CedarlingAuthorizationService.CEDARLING_JANS_ACCESS_TOKEN; + + // ─── Timing ────────────────────────────────────────────────────────────── + + /** + * Cedarling telemetry / health push interval in seconds. + * Low enough for integration tests to complete in reasonable time. + */ + private static final int TELEMETRY_INTERVAL_SEC = 5; + + /** Maximum time to wait for at least one telemetry payload per round. */ + private static final int WAIT_TIMEOUT_SEC = 30; + private static final Duration WAIT_TIMEOUT = Duration.ofSeconds(WAIT_TIMEOUT_SEC); + + // ─── Test state ────────────────────────────────────────────────────────── + + private final ObjectMapper objectMapper = new ObjectMapper(); + private CedarlingAuthorizationService authService; + private String jwt1, jwt2, jwt3; + + /** + * Guards one-time Cedarling initialisation inside {@link #registerStubs()}. + * WireMock stubs must exist before {@code initCedarlingService()} is called + * (Cedarling fetches the well-known document during startup), so the service + * cannot be initialised in {@code @BeforeAll} – at that point the + * {@link com.github.tomakehurst.wiremock.junit5.WireMockExtension} has not + * yet registered any stubs. The flag ensures initialisation happens exactly + * once, on the first {@code @BeforeEach} invocation. + */ + private boolean serviceInitialized = false; + + // ─── Lifecycle ─────────────────────────────────────────────────────────── + + /** + * Registers WireMock stubs and, on the very first call, initialises the + * Cedarling service. + * + *

Why everything lives here and not in {@code @BeforeAll}

+ *

{@link com.github.tomakehurst.wiremock.junit5.WireMockExtension} resets + * all stub mappings in its own {@code beforeEach} callback, which + * executes after {@code @BeforeAll} but + * before any {@code @BeforeEach} defined in the test class. + * The execution order for each test method is therefore: + *

+     *   @BeforeAll  setUpAll()          – runs once, but stubs aren't registered yet
+     *   WireMockExtension.beforeEach()  – wipes all stub mappings
+     *   @BeforeEach registerStubs()     – stubs registered here, safe to call Cedarling init
+     * 
+ *

Cedarling fetches the well-known discovery document during + * {@code initCedarlingService()}, so the WireMock stubs must be + * present before that call. The {@code serviceInitialized} flag ensures the + * expensive init runs exactly once while the stub registration runs before + * every test. + */ + @BeforeEach + void registerStubs() throws Exception { + configureWellKnownEndpoint(); + configureAuditEndpoints(); + log.info("WireMock stubs registered (well-known + 6 audit endpoints)"); + + if (!serviceInitialized) { + initAuthService(); + serviceInitialized = true; + log.info("WireMock HTTPS port: {} | telemetry interval: {}s | wait timeout: {}s", + wireMockServer.getHttpsPort(), TELEMETRY_INTERVAL_SEC, WAIT_TIMEOUT_SEC); + } + } + + /** Shuts down the Cedarling adapter after the class is done. */ + @AfterAll + void tearDown() { + if (serviceInitialized) { + authService.destroy(); + log.info("Cedarling service destroyed"); + } + } + + // ─── Tests ─────────────────────────────────────────────────────────────── + + @Nested + @DisplayName("Two-round telemetry lifecycle") + class TwoRoundTelemetryLifecycle { + + /** + * Main telemetry lifecycle test. + * + *

Executes two batches of authorization calls separated by a WireMock + * request journal reset, then verifies: + *

    + *
  • All required fields are present in every captured payload.
  • + *
  • Health status is {@code "ok"} in both rounds.
  • + *
  • {@code evaluationRequestsCount} is ≥ the Round 1 value in Round 2 + * (Cedarling accumulates the counter globally).
  • + *
  • Log entries carry correct decision results.
  • + *
+ */ + @Test + @DisplayName("Telemetry accumulates correctly across two authorization rounds") + void telemetryAccumulatesAcrossRounds() throws Exception { + + // ═══════════════════════════════ ROUND 1 ═══════════════════════════ + log.info("═══ ROUND 1 – 5 authorization calls (4 ALLOW + 1 DENY) ═══"); + int round1AuthCalls = executeRound1Authorizations(); + + // Wait for data to arrive at least at the primary telemetry endpoint before waiting the full timeout. + awaitDuration(Duration.ofSeconds(TELEMETRY_INTERVAL_SEC + 2)); + + // Wait for the telemetry push triggered by round-1 evaluations + Map> round1CapturedRequests = awaitAndCapture("R1", WAIT_TIMEOUT); + + List r1Telemetry = findCaptured(round1CapturedRequests, "R1 telemetry", TELEMETRY_PATH); + List r1BulkTelemetry = findCaptured(round1CapturedRequests, "R1 bulk-telemetry", BULK_TELEMETRY_PATH); + List r1Health = findCaptured(round1CapturedRequests, "R1 health", HEALTH_PATH); + List r1BulkHealth = findCaptured(round1CapturedRequests, "R1 bulk-health", BULK_HEALTH_PATH); + List r1Log = findCaptured(round1CapturedRequests, "R1 log", LOG_PATH); + List r1BulkLog = findCaptured(round1CapturedRequests, "R1 bulk-log", BULK_LOG_PATH); + + log.info("Round 1 received – telemetry={}, health={}, log={}, bulkTel={}, bulkHealth={}, bulkLog={}", + r1Telemetry.size(), r1Health.size(), r1Log.size(), + r1BulkTelemetry.size(), r1BulkHealth.size(), r1BulkLog.size()); + + // Structural and value verification + verifyHealthPayloads(r1Health, "Round 1"); + verifyHealthBulkPayloads(r1BulkHealth, "Round 1 (bulk)", -1); + verifyLogPayloads(r1Log, "Round 1"); + verifyLogBulkPayloads(r1BulkLog, "Round 1 (bulk)", round1AuthCalls); + verifyTelemetryPayloads(r1Telemetry, "Round 1"); + verifyTelemetryBulkPayloads(r1BulkTelemetry, "Round 1", -1); + + // Capture round 1's latest evaluationRequestsCount for comparison + long r1EvalCount = latestLong(r1Telemetry, "evaluationRequestsCount"); + log.info("Round 1 – evaluationRequestsCount = {}", r1EvalCount); + + // ─── Wipe request journal – round 2 starts clean ────────────────── + wireMockServer.resetRequests(); + log.info("--- WireMock request journal reset – starting Round 2 ---"); + + // ═══════════════════════════════ ROUND 2 ═══════════════════════════ + log.info("═══ ROUND 2 – 7 authorization calls (4 ALLOW + 3 DENY) ═══"); + int round2AuthCalls = executeRound2Authorizations(); + + // Wait for data to arrive at least at the primary telemetry endpoint before waiting the full timeout. + awaitDuration(Duration.ofSeconds(TELEMETRY_INTERVAL_SEC + 2)); + + // Wait for the telemetry push triggered by round-2 evaluations + Map> round2CapturedRequests = awaitAndCapture("R2", WAIT_TIMEOUT); + + List r2Telemetry = findCaptured(round2CapturedRequests, "R2 telemetry", TELEMETRY_PATH); + List r2BulkTelemetry = findCaptured(round2CapturedRequests, "R2 bulk-telemetry", BULK_TELEMETRY_PATH); + List r2Health = findCaptured(round2CapturedRequests, "R2 health", HEALTH_PATH); + List r2BulkHealth = findCaptured(round2CapturedRequests, "R2 bulk-health", BULK_HEALTH_PATH); + List r2Log = findCaptured(round2CapturedRequests, "R2 log", LOG_PATH); + List r2BulkLog = findCaptured(round2CapturedRequests, "R2 bulk-log", BULK_LOG_PATH); + + log.info("Round 2 received – telemetry={}, health={}, log={}, bulkTel={}, bulkHealth={}, bulkLog={}", + r2Telemetry.size(), r2Health.size(), r2Log.size(), + r2BulkTelemetry.size(), r2BulkHealth.size(), r2BulkLog.size()); + + verifyHealthPayloads(r2Health, "Round 2"); + verifyHealthBulkPayloads(r2BulkHealth, "Round 2 (bulk)", -1); + verifyLogPayloads(r2Log, "Round 2"); + verifyLogBulkPayloads(r2BulkLog, "Round 2 (bulk)", round2AuthCalls); + verifyTelemetryPayloads(r2Telemetry, "Round 2"); + verifyTelemetryBulkPayloads(r2BulkTelemetry, "Round 2", -1); + + // ═══════════════════════════ CROSS-ROUND COMPARISON ════════════════ + compareTelemetryAcrossRounds(r1EvalCount, r1Telemetry, r2Telemetry); + compareHealthAcrossRounds(r1Health, r2Health); + + log.info("✅ All telemetry assertions passed across both rounds."); + } + } + + // ─── Authorization rounds ───────────────────────────────────────────────── + + /** + * Round 1: JWT 1 (all scopes) + JWT 2 (health only) – 5 calls, 4 ALLOW + 1 DENY. + * + * @return total number of authorization calls made + */ + private int executeRound1Authorizations() { + // JWT 1 – all three endpoints allowed + assertTrue(authorize(jwt1, ACTION_POST, ID_LOG, PATH_LOG), + "R1: JWT1 must be allowed for /log"); + assertTrue(authorize(jwt1, ACTION_POST, ID_HEALTH, PATH_HEALTH), + "R1: JWT1 must be allowed for /health"); + assertTrue(authorize(jwt1, ACTION_POST, ID_TELEMETRY, PATH_TELEMETRY), + "R1: JWT1 must be allowed for /telemetry"); + + // JWT 2 – health allowed, log denied + assertTrue(authorize(jwt2, ACTION_POST, ID_HEALTH, PATH_HEALTH), + "R1: JWT2 must be allowed for /health"); + assertFalse(authorize(jwt2, ACTION_POST, ID_LOG, PATH_LOG), + "R1: JWT2 must be denied for /log (missing log.write)"); + + return 5; // 4 ALLOW + 1 DENY + } + + /** + * Round 2: JWT 3 (log only) + JWT 2 (health only) + JWT 1 – 7 calls, 4 ALLOW + 3 DENY. + * + * @return total number of authorization calls made + */ + private int executeRound2Authorizations() { + // JWT 3 – log allowed, health and telemetry denied + assertTrue(authorize(jwt3, ACTION_POST, ID_LOG, PATH_LOG), + "R2: JWT3 must be allowed for /log"); + assertFalse(authorize(jwt3, ACTION_POST, ID_HEALTH, PATH_HEALTH), + "R2: JWT3 must be denied for /health (missing health.write)"); + assertFalse(authorize(jwt3, ACTION_POST, ID_TELEMETRY, PATH_TELEMETRY), + "R2: JWT3 must be denied for /telemetry (missing telemetry.write)"); + + // JWT 2 – health allowed, telemetry denied + assertTrue(authorize(jwt2, ACTION_POST, ID_HEALTH, PATH_HEALTH), + "R2: JWT2 must be allowed for /health"); + assertFalse(authorize(jwt2, ACTION_POST, ID_TELEMETRY, PATH_TELEMETRY), + "R2: JWT2 must be denied for /telemetry (missing telemetry.write)"); + + // JWT 1 – all allowed + assertTrue(authorize(jwt1, ACTION_POST, ID_LOG, PATH_LOG), + "R2: JWT1 must be allowed for /log"); + assertTrue(authorize(jwt1, ACTION_POST, ID_HEALTH, PATH_HEALTH), + "R2: JWT1 must be allowed for /health"); + + return 7; // 4 ALLOW + 3 DENY + } + + // ─── Payload verification ───────────────────────────────────────────────── + + /** + * Verifies structural fields and value assertions for every health payload. + * + *

Required fields: {@code service}, {@code status}, {@code nodeName}.
+ * Value assertions: {@code service == "cedarling"}, {@code status == "ok"}. + */ + private void verifyHealthPayloads(List payloads, String label) { + if (payloads.isEmpty()) { + log.warn("[{}] No health payloads received – skipping field checks", label); + return; + } + for (int i = 0; i < payloads.size(); i++) { + JsonNode node = payloads.get(i); + String ctx = label + " health[" + i + "]"; + + verifyHealthPayload(ctx, node); + } + log.info("[{}] ✅ {} health payload(s) verified", label, payloads.size()); + } + + private void verifyHealthBulkPayloads(List payloads, String label, int minExpectedEntries) { + if (payloads.isEmpty()) { + log.warn("⚠️ [{}] No health payloads received – skipping field checks", label); + return; + } + + int payloadCount = 0; + for (int i = 0; i < payloads.size(); i++) { + JsonNode nodeArray = payloads.get(i); + + assertTrue(nodeArray.isArray(), + "Bulk Health response should be an array of health payloads, but got: " + nodeArray.getNodeType()); + + for (int j = 0; j < nodeArray.size(); j++) { + String ctx = label + " health[" + i + "][" + j + "]"; + JsonNode node = nodeArray.get(j); + + verifyHealthPayload(ctx, node); + payloadCount++; + } + } + if (minExpectedEntries >= 0) { + assertTrue(payloadCount >= minExpectedEntries, + label + ": expected at least " + minExpectedEntries + + " log entries but got " + payloadCount); + } + log.info("[{}] ✅ {} health payload(s) verified", label, payloadCount); + } + + private void verifyHealthPayload(String ctx, JsonNode node) { + // ── Required fields ── + assertTrue(node.hasNonNull("service"), ctx + ": missing 'service'"); + assertTrue(node.hasNonNull("status"), ctx + ": missing 'status'"); + assertTrue(node.hasNonNull("node_name"), ctx + ": missing 'node_name'"); + + // ── Value assertions ── + assertEquals("Lock Server - Test Edition", node.get("service").asText(), ctx + ": service must be 'Lock Server - Test Edition'"); + assertEquals("running", node.get("status").asText(), ctx + ": status must be 'running'"); + + // node_name must be present and non-blank + assertFalse(node.get("node_name").asText("").isBlank(), ctx + ": node_name must not be blank"); + } + + /** + * Verifies structural fields for every log (audit) payload. + * + *

Required fields: {@code clientId}, {@code principalId}, + * {@code decisionResult}, {@code action}.

+ * + * @param payloads parsed request bodies + * @param label test label for assertion messages + */ + private void verifyLogPayloads(List payloads, String label) { + if (payloads.isEmpty()) { + log.warn("[{}] No log payloads received – skipping field checks", label); + return; + } + for (int i = 0; i < payloads.size(); i++) { + JsonNode node = payloads.get(i); + String ctx = label + " log[" + i + "]"; + + verifyLogPayload(ctx, node); + } + log.info("[{}] ✅ {} log payload(s) verified", label, payloads.size()); + } + + private void verifyLogBulkPayloads(List payloads, String label, int minExpectedEntries) { + if (payloads.isEmpty()) { + log.warn("⚠️ [{}] No log payloads received – skipping field checks", label); + return; + } + + int payloadCount = 0; + for (int i = 0; i < payloads.size(); i++) { + JsonNode nodeArray = payloads.get(i); + + assertTrue(nodeArray.isArray(), + "Bulk Log response should be an array of log payloads, but got: " + nodeArray.getNodeType()); + + for (int j = 0; j < nodeArray.size(); j++) { + String ctx = label + " log[" + i + "][" + j + "]"; + JsonNode node = nodeArray.get(j); + + verifyLogPayload(ctx, node); + payloadCount++; + } + } + if (minExpectedEntries >= 0) { + assertTrue(payloadCount >= minExpectedEntries, + label + ": expected at least " + minExpectedEntries + + " log entries but got " + payloadCount); + } + log.info("[{}] ✅ {} log payload(s) verified", label, payloadCount); + } + + private void verifyLogPayload(String ctx, JsonNode node) { + // ── Required fields ── + assertTrue(node.hasNonNull("clientId"), ctx + ": missing 'clientId'"); + assertTrue(node.hasNonNull("principalId"), ctx + ": missing 'principalId'"); + assertTrue(node.hasNonNull("decisionResult"), ctx + ": missing 'decisionResult'"); + assertTrue(node.hasNonNull("action"), ctx + ": missing 'action'"); + + // ── Value assertions ── + String decision = node.get("decisionResult").asText(); + assertTrue("ALLOW".equalsIgnoreCase(decision) || "DENY".equalsIgnoreCase(decision), + ctx + ": decisionResult must be 'ALLOW' or 'DENY', got: " + decision); + + // action must follow the Cedar action URI format: Namespace::Action::"name" + String action = node.get("action").asText(); + assertTrue(action.matches(".+::Action::\"[^\"]+\""), + ctx + ": action must match Cedar format '::Action::\"\"', got: " + action); + + // clientId must be present and non-blank + assertFalse(node.get("clientId").asText("").isBlank(), + ctx + ": clientId must not be blank"); + } + + /** + * Verifies structural fields and value assertions for every telemetry payload. + * + *

Required fields: {@code service}, {@code status}, + * {@code evaluationRequestsCount}, {@code avgPolicyEvaluationTimeNs}. + */ + private void verifyTelemetryPayloads(List payloads, String label) { + if (payloads.isEmpty()) { + log.warn("[{}] No telemetry payloads received – skipping field checks", label); + return; + } + for (int i = 0; i < payloads.size(); i++) { + JsonNode node = payloads.get(i); + String ctx = label + " telemetry[" + i + "]"; + + verifyTelemetryPayload(ctx, node); + } + log.info("[{}] ✅ {} telemetry payload(s) verified", label, payloads.size()); + } + + private void verifyTelemetryBulkPayloads(List payloads, String label, int minExpectedEntries) { + if (payloads.isEmpty()) { + log.warn("⚠️ [{}] No telemetry payloads received – skipping field checks", label); + return; + } + + int payloadCount = 0; + for (int i = 0; i < payloads.size(); i++) { + JsonNode nodeArray = payloads.get(i); + + assertTrue(nodeArray.isArray(), + "Bulk Telemetry response should be an array of telemetry payloads, but got: " + nodeArray.getNodeType()); + + for (int j = 0; j < nodeArray.size(); j++) { + String ctx = label + " telemetry[" + i + "][" + j + "]"; + JsonNode node = nodeArray.get(j); + + verifyTelemetryPayload(ctx, node); + payloadCount++; + } + } + if (minExpectedEntries >= 0) { + assertTrue(payloadCount >= minExpectedEntries, + label + ": expected at least " + minExpectedEntries + + " log entries but got " + payloadCount); + } + log.info("[{}] ✅ {} telemetry payload(s) verified", label, payloadCount); + } + + private void verifyTelemetryPayload(String ctx, JsonNode node) { + // ── Required fields ── + assertTrue(node.hasNonNull("service"), ctx + ": missing 'service'"); + assertTrue(node.hasNonNull("status"), ctx + ": missing 'status'"); + assertTrue(node.hasNonNull("evaluationRequestsCount"), ctx + ": missing 'evaluationRequestsCount'"); + assertTrue(node.hasNonNull("avgPolicyEvaluationTimeNs"), ctx + ": missing 'avgPolicyEvaluationTimeNs'"); + + // ── Value assertions ── + assertEquals("cedarling", node.get("service").asText(), ctx + ": service must be 'cedarling'"); + + long evalCount = node.get("evaluationRequestsCount").asLong(); + assertTrue(evalCount >= 0, ctx + ": evaluationRequestsCount must be >= 0, got " + evalCount); + + // Average evaluation time must be strictly positive once any evaluation has occurred + long avgEvalNs = node.get("avgPolicyEvaluationTimeNs").asLong(-1L); + assertTrue(avgEvalNs > 0, + ctx + ": avgPolicyEvaluationTimeNs must be > 0, got " + avgEvalNs); + } + + // ─── Cross-round comparison ─────────────────────────────────────────────── + + /** + * Compares telemetry counters from Round 1 vs Round 2. + * + *

    + *
  • {@code evaluationRequestsCount} in Round 2 must be ≥ the Round 1 + * value because Cedarling accumulates this counter globally.
  • + *
  • The {@code service} name must be identical across rounds.
  • + *
  • Round 2 must contain at least as many telemetry reports as Round 1.
  • + *
+ * + * @param r1EvalCount already extracted from the last round-1 telemetry entry + * @param round1 Round 1 telemetry payloads + * @param round2 Round 2 telemetry payloads + */ + private void compareTelemetryAcrossRounds(long r1EvalCount, + List round1, List round2) { + + if (round1.isEmpty() || round2.isEmpty()) { + log.warn("⚠️ Cannot compare telemetry rounds – empty data (r1={}, r2={})", + round1.size(), round2.size()); + return; + } + + long r2EvalCount = latestLong(round2, "evaluationRequestsCount"); + log.info("evaluationRequestsCount — Round 1 (last) = {} | Round 2 (last) = {}", + r1EvalCount, r2EvalCount); + + // Cedarling accumulates evaluations globally; Round 2 counter must not decrease + assertTrue(r2EvalCount >= r1EvalCount, + "evaluationRequestsCount must not decrease between rounds: R1=" + r1EvalCount + + ", R2=" + r2EvalCount); + + // Service name is invariant + String r1Service = latestString(round1, "service"); + String r2Service = latestString(round2, "service"); + assertEquals(r1Service, r2Service, "service name must be consistent across rounds"); + + // Round 2 had more authorization calls – the difference should be visible + long delta = r2EvalCount - r1EvalCount; + log.info("evaluationRequestsCount delta (R2 – R1) = {}", delta); + // NOTE: delta could be 0 if Cedarling resets the counter per telemetry interval + // rather than accumulating globally. The ≥ assertion above is the safe guard. + + log.info("✅ Cross-round telemetry comparison passed"); + } + + /** + * Verifies that the Cedarling process remains healthy across both rounds + * regardless of DENY decisions during authorization. + */ + private void compareHealthAcrossRounds(List round1, List round2) { + if (round1.isEmpty() || round2.isEmpty()) { + log.warn("⚠️ Cannot compare health rounds – empty data (r1={}, r2={})", + round1.size(), round2.size()); + return; + } + String r1Status = latestString(round1, "status"); + String r2Status = latestString(round2, "status"); + + assertEquals("ok", r1Status, "Round 1 health status must be 'ok'"); + assertEquals("ok", r2Status, "Round 2 health status must be 'ok'"); + + log.info("✅ Health status is 'ok' in both rounds"); + } + + // ─── WireMock utilities ─────────────────────────────────────────────────── + public List findCaptured( + Map> capturedRequests, + String label, + String endpointPath) { + + List cpatured = capturedRequests.entrySet().stream() + .filter(entry -> entry.getKey() != null + && entry.getKey().endsWith(endpointPath)) + .map(Map.Entry::getValue) + .flatMap(List::stream) + .collect(Collectors.toList()); + log.info("[{}] {} request(s) captured at {}", label, cpatured.size(), endpointPath); + + return cpatured; + } + /** + * Polls WireMock for POST requests at {@code endpointPath} until at least + * one arrives or {@code timeout} elapses, then returns all captured bodies + * as parsed {@link JsonNode}s. + * + * @param label descriptive label for log output + * @param endpointPath WireMock path to watch + * @param timeout maximum time to wait + * @return list of parsed JSON bodies; empty if nothing arrived in time + */ + private Map> awaitAndCapture(String label, Duration timeout) + throws InterruptedException { + + long deadline = System.currentTimeMillis() + timeout.toMillis(); + while (System.currentTimeMillis() < deadline) { + List requests = wireMockServer.findAll( + postRequestedFor(anyUrl())); + if (!requests.isEmpty()) { + Map> parsed = parseRequestBodies(requests); + String logDump = parsed.entrySet().stream() + .map(entry -> entry.getKey() + " = " + entry.getValue().size()) + .collect(Collectors.joining(", ", "{", "}")); + + log.info("Requests captured summary: {}", logDump); + + if (DUMP_CAPTURED_REQUEST) { + parsed.forEach((path, nodes) -> { + log.debug("Captured {} request(s) at {}:", nodes.size(), path); + for (int i = 0; i < nodes.size(); i++) { + log.debug(" [{}][{}]: {}", path, i, nodes.get(i).toPrettyString()); + } + }); + } + return parsed; + } + Thread.sleep(500); + } + log.warn("[{}] Timeout ({}s) – no requests arrived", label, timeout.toSeconds()); + return Collections.emptyMap(); + } + + private void awaitDuration(Duration timeout)throws InterruptedException { + long deadline = System.currentTimeMillis() + timeout.toMillis(); + while (System.currentTimeMillis() < deadline) { + Thread.sleep(500); + } + } + + private Map> parseRequestBodies(List requests) { + return requests.stream() + .map(r -> { + try { + // Extract clean path (ignoring query parameters) + String fullPath = new URI(r.getUrl()).getPath(); + String contextPath = fullPath.startsWith("/") ? "/" + fullPath.substring(1) : fullPath; + // Parse body + JsonNode node = objectMapper.readTree(r.getBodyAsString()); + + return new AbstractMap.SimpleEntry<>(contextPath, node); + } catch (Exception ex) { + log.warn("Could not parse request body or URL: {}", r.getUrl(), ex); + return null; + } + }) + // Remove requests that failed to parse + .filter(Objects::nonNull) + // Group by the path (key) and collect the JsonNodes (value) into a List + .collect(Collectors.groupingBy( + Map.Entry::getKey, + Collectors.mapping(Map.Entry::getValue, Collectors.toList()) + )); + } + + /** Extracts a {@code long} field from the last node in {@code nodes}. */ + private long latestLong(List nodes, String field) { + if (nodes.isEmpty()) return 0L; + return nodes.get(nodes.size() - 1).path(field).asLong(0L); + } + + /** Extracts a {@code String} field from the last node in {@code nodes}. */ + private String latestString(List nodes, String field) { + if (nodes.isEmpty()) return ""; + return nodes.get(nodes.size() - 1).path(field).asText(""); + } + + private List iteratorToList(Iterator it) { + List list = new ArrayList<>(); + it.forEachRemaining(list::add); + return list; + } + + // ─── WireMock stub configuration ───────────────────────────────────────── + + /** + * Stubs the Lock server discovery document with endpoint URIs that all + * point back to the WireMock HTTPS port. + */ + private void configureWellKnownEndpoint() { + int port = wireMockServer.getHttpsPort(); + + String fullUrl = "https://localhost:" + port + WELL_KNOWN_PATH; + log.info("Configuring WireMock well-known endpoint stub at: {}", fullUrl); + + String body = """ + { + "version": "1.0", + "issuer": "https://localhost:%d", + "audit": { + "health_endpoint": "https://localhost:%d/jans-lock/api/v1/audit/health", + "log_endpoint": "https://localhost:%d/jans-lock/api/v1/audit/log", + "telemetry_endpoint": "https://localhost:%d/jans-lock/api/v1/audit/telemetry" + } + } + """.formatted(port, port, port, port); + + wireMockServer.stubFor(get(urlEqualTo(WELL_KNOWN_PATH)) + .willReturn(aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody(body))); + } + + /** Stubs all six audit endpoints (single + bulk) to return HTTP 200 OK. */ + private void configureAuditEndpoints() { + String ok = """ + {"success": true, "message": "Audit data processed successfully"} + """; + List.of(HEALTH_PATH, BULK_HEALTH_PATH, + LOG_PATH, BULK_LOG_PATH, + TELEMETRY_PATH, BULK_TELEMETRY_PATH) + .forEach(path -> + wireMockServer.stubFor(post(urlEqualTo(path)) + .willReturn(aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody(ok)))); + } + + // ─── Cedarling service initialisation ──────────────────────────────────── + + /** + * Builds a fully-wired {@link CedarlingAuthorizationService} (no CDI + * container available in tests) with Lock telemetry enabled and pointing + * at the WireMock HTTPS server. + */ + private void initAuthService() throws Exception { + Logger svcLog = LoggerFactory.getLogger(CedarlingAuthorizationService.class); + AppConfiguration appConfig = mock(AppConfiguration.class); + CedarlingPolicyConfiguration policyConfig = mock(CedarlingPolicyConfiguration.class); + CedarlingConfiguration cedarConf = mock(CedarlingConfiguration.class); + + when(cedarConf.isEnabled()).thenReturn(true); + when(cedarConf.getLogType()).thenReturn(LogType.STD_OUT); + when(cedarConf.getLogLevel()).thenReturn(LogLevel.TRACE); + when(appConfig.getCedarlingConfiguration()).thenReturn(cedarConf); + + String policyStoreFn = System.getProperty("user.dir") + "/target/test-classes/test-policy-store"; + // Point Cedarling at the WireMock discovery document + String lockServerUri = "https://localhost:" + wireMockServer.getHttpsPort() + + WELL_KNOWN_PATH; + String lockAccessTokenJwt = withFutureExp(RAW_LOCK_ACCESS_TOKEN_JWT); + + authService = new CedarlingAuthorizationService() { + @Override + protected BootstrapConfig prepareBootstrapConfig(CedarlingConfiguration cedarConf) { + // Delegate to the standard builder; JWT validation is disabled for tests + return BootstrapConfig.builder() + .applicationName("Lock Server - Test Edition") + .policyStoreLocalFn(System.getProperty("user.dir") + "/target/test-classes/test-policy-store") + .jwtStatusValidation(false) + .jwtSigValidation(false) + .logType(LogType.STD_OUT) + .logLevel(LogLevel.TRACE) + // Lock / telemetry + .lock(true) + .lockAcceptInvalidCerts(true) + .lockServerConfigurationUri(lockServerUri) + .lockDynamicConfiguration(true) + .lockHealthInterval(TELEMETRY_INTERVAL_SEC) + .lockTelemetryInterval(TELEMETRY_INTERVAL_SEC) + .lockListenSse(false) + .lockAccessTokenJwt(lockAccessTokenJwt) + .build(); + } + }; + + when(cedarConf.isEnabled()).thenReturn(true); + + injectField(authService, "log", svcLog); + injectField(authService, "appConfiguration", appConfig); + injectField(authService, "policyConfiguration", policyConfig); + injectField(authService, "policyStoreLocalFn", policyStoreFn); + + authService.init(); + + // Patch exp claims so tokens are valid during the entire test run + jwt1 = withFutureExp(RAW_JWT_1); + jwt2 = withFutureExp(RAW_JWT_2); + jwt3 = withFutureExp(RAW_JWT_3); + + log.info("Cedarling service initialised – lock URI: {}", lockServerUri); + } + + // ─── Authorization helpers ──────────────────────────────────────────────── + + private boolean authorize(String token, String action, String permId, String path) { + Map tokens = Map.of(ACCESS_TOKEN_KEY, token); + return authService.authorize(tokens, action, buildResource(permId, path), buildContext()); + } + + private static Map buildResource(String permId, String path) { + Map resource = new HashMap<>(); + resource.put("cedar_entity_mapping", + Map.of("entity_type", RESOURCE_TYPE, "id", permId)); + resource.put("url", + Map.of("host", "", "path", path, "protocol", "")); + resource.put("header", Collections.emptyMap()); + return resource; + } + + private static Map buildContext() { + return Collections.emptyMap(); + } + + // ─── JWT exp patching ───────────────────────────────────────────────────── + + /** + * Replaces the {@code exp} claim in the JWT payload with {@code now + 1 hour}. + * The original signature is kept but becomes stale; Cedarling accepts it + * because the adapter is initialised with {@code jwtSigValidation(false)}. + * + * @param rawJwt original JWT string (header.payload.signature) + * @return patched JWT with a future {@code exp} + */ + static String withFutureExp(String rawJwt) { + String[] parts = rawJwt.split("\\."); + if (parts.length != 3) { + throw new IllegalArgumentException("Not a 3-part JWT: " + rawJwt); + } + String payloadJson = new String( + Base64.getUrlDecoder().decode(parts[1]), StandardCharsets.UTF_8); + + long futureExp = (System.currentTimeMillis() / 1000L) + 3600L; + String patched = payloadJson.replaceAll("\"exp\"\\s*:\\s*\\d+", "\"exp\":" + futureExp); + + String encodedPayload = Base64.getUrlEncoder() + .withoutPadding() + .encodeToString(patched.getBytes(StandardCharsets.UTF_8)); + + return parts[0] + "." + encodedPayload + "." + parts[2]; + } + + // ─── Reflection utility ─────────────────────────────────────────────────── + + /** + * Sets {@code fieldName} on {@code target} using reflection, walking up + * the class hierarchy until the field is found. + * + * @param target object to mutate + * @param fieldName field name + * @param value value to set + * @throws NoSuchFieldException if the field is not found in any superclass + */ + static void injectField(Object target, String fieldName, Object value) throws Exception { + Class cls = target.getClass(); + while (cls != null) { + try { + Field field = cls.getDeclaredField(fieldName); + field.setAccessible(true); + field.set(target, value); + return; + } catch (NoSuchFieldException ignored) { + cls = cls.getSuperclass(); + } + } + throw new NoSuchFieldException( + "Field '" + fieldName + "' not found in " + target.getClass().getName()); + } +} \ No newline at end of file diff --git a/jans-lock/lock-server/service/src/main/java/io/jans/lock/service/ws/rs/audit/AuditRestWebService.java b/jans-lock/lock-server/service/src/main/java/io/jans/lock/service/ws/rs/audit/AuditRestWebService.java index e901d4c5555..43a5dd04571 100644 --- a/jans-lock/lock-server/service/src/main/java/io/jans/lock/service/ws/rs/audit/AuditRestWebService.java +++ b/jans-lock/lock-server/service/src/main/java/io/jans/lock/service/ws/rs/audit/AuditRestWebService.java @@ -83,7 +83,7 @@ Response processHealthRequest(HealthEntry healthEntry, @Context HttpServletReque @Consumes({ MediaType.APPLICATION_JSON }) @Produces({ MediaType.APPLICATION_JSON }) @ProtectedApi(scopes = { ApiAccessConstants.LOCK_HEALTH_WRITE_ACCESS }, grpcMethodName = "ProcessBulkHealth") - @ProtectedCedarlingApi(action = "Jans::Action::\"POST\"", resource = "Jans::HTTP_Request", id="lock_audit_health_write", path="/audit/health/bulk") + @ProtectedCedarlingApi(action = "Jans::Action::\"POST\"", resource = "Jans::HTTP_Request", id="lock_audit_health_bulk_write", path="/audit/health/bulk") Response processBulkHealthRequest(List healthEntries, @Context HttpServletRequest request, @Context SecurityContext sec); @@ -119,7 +119,7 @@ Response processLogRequest(LogEntry logEntry, @Context HttpServletRequest reques @Consumes({ MediaType.APPLICATION_JSON }) @Produces({ MediaType.APPLICATION_JSON }) @ProtectedApi(scopes = { ApiAccessConstants.LOCK_LOG_WRITE_ACCESS }, grpcMethodName = "ProcessBulkLog") - @ProtectedCedarlingApi(action = "Jans::Action::\"POST\"", resource = "Jans::HTTP_Request", id="lock_audit_log_write", path="/audit/log/bulk") + @ProtectedCedarlingApi(action = "Jans::Action::\"POST\"", resource = "Jans::HTTP_Request", id="lock_audit_log_bulk_write", path="/audit/log/bulk") Response processBulkLogRequest(List logEntries, @Context HttpServletRequest request, @Context SecurityContext sec); @@ -155,7 +155,7 @@ Response processTelemetryRequest(TelemetryEntry telemetryEntry, @Context HttpSer @Consumes({ MediaType.APPLICATION_JSON }) @Produces({ MediaType.APPLICATION_JSON }) @ProtectedApi(scopes = { ApiAccessConstants.LOCK_TELEMETRY_WRITE_ACCESS }, grpcMethodName = "ProcessBulkTelemetry") - @ProtectedCedarlingApi(action = "Jans::Action::\"POST\"", resource = "Jans::HTTP_Request", id="lock_audit_telemetry_write", path="/audit/telemetry/bulk") + @ProtectedCedarlingApi(action = "Jans::Action::\"POST\"", resource = "Jans::HTTP_Request", id="lock_audit_telemetry_bulk_write", path="/audit/telemetry/bulk") Response processBulkTelemetryRequest(List telemetryEntries, @Context HttpServletRequest request, @Context SecurityContext sec); diff --git a/jans-lock/lock-server/service/src/main/java/io/jans/lock/service/ws/rs/stat/StatRestWebService.java b/jans-lock/lock-server/service/src/main/java/io/jans/lock/service/ws/rs/stat/StatRestWebService.java index da5c7734985..f51ad842ed4 100644 --- a/jans-lock/lock-server/service/src/main/java/io/jans/lock/service/ws/rs/stat/StatRestWebService.java +++ b/jans-lock/lock-server/service/src/main/java/io/jans/lock/service/ws/rs/stat/StatRestWebService.java @@ -43,7 +43,7 @@ public interface StatRestWebService { @ApiResponse(responseCode = "500", description = "InternalServerError", content = @Content(mediaType = MediaType.APPLICATION_JSON, schema = @Schema(implementation = LockApiError.class, description = "InternalServerError"))), }) @GET @ProtectedApi(scopes = { ApiAccessConstants.LOCK_STAT_READ_ACCESS }) - @ProtectedCedarlingApi(action = "Jans::Action::\"GET\"", resource = "Jans::HTTP_Request", id="lock_stat_query", path="/internal/stat") + @ProtectedCedarlingApi(action = "Jans::Action::\"Search\"", resource = "Jans::HTTP_Request", id="lock_stat_query", path="/internal/stat") @Produces(MediaType.APPLICATION_JSON) public Response statGet(@Context HttpServletRequest request, @QueryParam("month") String months, @QueryParam("start-month") String startMonth, @QueryParam("end-month") String endMonth, @QueryParam("format") String format); @@ -59,7 +59,7 @@ public Response statGet(@Context HttpServletRequest request, @QueryParam("month" @ApiResponse(responseCode = "500", description = "InternalServerError", content = @Content(mediaType = MediaType.APPLICATION_JSON, schema = @Schema(implementation = LockApiError.class, description = "InternalServerError"))), }) @POST @ProtectedApi(scopes = { ApiAccessConstants.LOCK_STAT_READ_ACCESS }) - @ProtectedCedarlingApi(action = "Jans::Action::\"POST\"", resource = "Jans::HTTP_Request", id="lock_stat_query", path="/internal/stat") + @ProtectedCedarlingApi(action = "Jans::Action::\"Search\"", resource = "Jans::HTTP_Request", id="lock_stat_query", path="/internal/stat") @Produces(MediaType.APPLICATION_JSON) public Response statPost(@Context HttpServletRequest request, @FormParam("month") String months, @FormParam("start-month") String startMonth, @FormParam("end-month") String endMonth, @FormParam("format") String format); From 74b80ede654567d618f3db64e9b3f14563af4114 Mon Sep 17 00:00:00 2001 From: Yuriy Date: Thu, 28 May 2026 14:08:04 +0300 Subject: [PATCH 6/6] feat(jans-lock): dump boostrap properties Signed-off-by: Yuriy --- .../telemetry/CedarlingTelemetryIntegrationTest.java | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/jans-lock/lock-server/cedarling/src/test/java/io/jans/lock/cedarling/telemetry/CedarlingTelemetryIntegrationTest.java b/jans-lock/lock-server/cedarling/src/test/java/io/jans/lock/cedarling/telemetry/CedarlingTelemetryIntegrationTest.java index 02a46adafca..8c5edb0d784 100644 --- a/jans-lock/lock-server/cedarling/src/test/java/io/jans/lock/cedarling/telemetry/CedarlingTelemetryIntegrationTest.java +++ b/jans-lock/lock-server/cedarling/src/test/java/io/jans/lock/cedarling/telemetry/CedarlingTelemetryIntegrationTest.java @@ -880,7 +880,7 @@ private void initAuthService() throws Exception { @Override protected BootstrapConfig prepareBootstrapConfig(CedarlingConfiguration cedarConf) { // Delegate to the standard builder; JWT validation is disabled for tests - return BootstrapConfig.builder() + BootstrapConfig bootstrapConfig = BootstrapConfig.builder() .applicationName("Lock Server - Test Edition") .policyStoreLocalFn(System.getProperty("user.dir") + "/target/test-classes/test-policy-store") .jwtStatusValidation(false) @@ -897,6 +897,9 @@ protected BootstrapConfig prepareBootstrapConfig(CedarlingConfiguration cedarCon .lockListenSse(false) .lockAccessTokenJwt(lockAccessTokenJwt) .build(); + + log.info("Cedarling bootstrap configuration: {}", bootstrapConfig.toJsonConfig()); + return bootstrapConfig; } };