diff --git a/jans-auth-server/pom.xml b/jans-auth-server/pom.xml
index 80483d7a52c..05999ad6882 100644
--- a/jans-auth-server/pom.xml
+++ b/jans-auth-server/pom.xml
@@ -134,6 +134,7 @@
1.6.0
+
org.jboss.weld
diff --git a/jans-auth-server/server/pom.xml b/jans-auth-server/server/pom.xml
index 8f4fcb0d6e7..6c08b7e9434 100644
--- a/jans-auth-server/server/pom.xml
+++ b/jans-auth-server/server/pom.xml
@@ -273,7 +273,6 @@
io.prometheus
simpleclient_common
- 0.9.0
net.agkn
diff --git a/jans-bom/pom.xml b/jans-bom/pom.xml
index 2bd364bc104..2015b451200 100644
--- a/jans-bom/pom.xml
+++ b/jans-bom/pom.xml
@@ -499,16 +499,21 @@
commons-text
1.12.0
-
- commons-beanutils
- commons-beanutils
- 1.9.4
-
commons-collections
commons-collections
3.2.2
+
+ io.prometheus
+ simpleclient_common
+ 0.9.0
+
+
+ net.agkn
+ hll
+ 1.6.0
+
@@ -605,7 +610,7 @@
jackson-dataformat-cbor
${jackson.version}
-
org.quartz-scheduler
@@ -832,7 +837,7 @@
velocity-engine-core
2.3
-
+
joda-time
@@ -897,8 +902,8 @@
${mockito.version}
test
-
-
+
+
net.openhft
compiler
diff --git a/jans-linux-setup/jans_setup/schema/jans_schema.json b/jans-linux-setup/jans_setup/schema/jans_schema.json
index a9d5c3eeba3..865de347097 100644
--- a/jans-linux-setup/jans_setup/schema/jans_schema.json
+++ b/jans-linux-setup/jans_setup/schema/jans_schema.json
@@ -2454,6 +2454,17 @@
"syntax": "1.3.6.1.4.1.1466.115.121.1.15",
"x_origin": "Jans created attribute"
},
+ {
+ "desc": "Jans client data",
+ "equality": "caseIgnoreMatch",
+ "names": [
+ "clntDat"
+ ],
+ "oid": "jansAttr",
+ "substr": "caseIgnoreSubstringsMatch",
+ "syntax": "1.3.6.1.4.1.1466.115.121.1.15",
+ "x_origin": "Jans created attribute"
+ },
{
"desc": "OX PKCE code challenge",
"equality": "caseIgnoreMatch",
@@ -4083,6 +4094,7 @@
"requestedResource"
],
"oid": "jansAttr",
+ "rdbm_json_column": true,
"substr": "caseIgnoreSubstringsMatch",
"syntax": "1.3.6.1.4.1.1466.115.121.1.15",
"x_origin": "Jans created attribute"
@@ -5209,6 +5221,27 @@
],
"x_origin": "Jans created objectclass"
},
+ {
+ "kind": "STRUCTURAL",
+ "may": [
+ "jansId",
+ "dat",
+ "clntDat",
+ "jansData",
+ "attr"
+ ],
+ "must": [
+ "objectclass"
+ ],
+ "names": [
+ "jansLockStatEntry"
+ ],
+ "oid": "jansObjClass",
+ "sup": [
+ "top"
+ ],
+ "x_origin": "Jans Lock created objectclass"
+ },
{
"kind": "STRUCTURAL",
"may": [
diff --git a/jans-linux-setup/jans_setup/setup_app/installers/base.py b/jans-linux-setup/jans_setup/setup_app/installers/base.py
index 7d32a96e211..b848834f7be 100644
--- a/jans-linux-setup/jans_setup/setup_app/installers/base.py
+++ b/jans-linux-setup/jans_setup/setup_app/installers/base.py
@@ -1,10 +1,13 @@
import os
import uuid
import inspect
+import json
from setup_app import paths
from setup_app.utils import base
from setup_app.config import Config
+from setup_app.pylib.ldif4.ldif import LDIFWriter
+
from setup_app.utils.db_utils import dbUtils
from setup_app.utils.progress import jansProgress
from setup_app.utils.printVersion import get_war_info
@@ -12,11 +15,19 @@
class BaseInstaller:
needdb = True
dbUtils = dbUtils
+ service_scopes_created = False
def register_progess(self):
+ if not hasattr(self, 'output_folder'):
+ self.output_folder = os.path.join(Config.output_dir, self.service_name)
+
+ if not hasattr(self, 'templates_dir'):
+ self.templates_dir = os.path.join(Config.templateFolder, self.service_name)
+
jansProgress.register(self)
def start_installation(self):
+
if not hasattr(self, 'pbar_text'):
pbar_text = "Installing " + self.service_name.title()
else:
@@ -44,6 +55,9 @@ def start_installation(self):
self.render_unit_file()
self.render_import_templates()
+ if not self.service_scopes_created:
+ self.create_scopes()
+
self.update_backend()
self.service_post_setup()
@@ -244,3 +258,41 @@ def service_post_setup(self):
def service_post_install_tasks(self):
pass
+
+ def create_scopes(self):
+ scopes_json_fn = os.path.join(self.templates_dir, 'scopes.json')
+
+ if not os.path.exists(scopes_json_fn):
+ return
+
+ self.logIt(f"Creating {self.service_name} scopes from {scopes_json_fn}")
+ scopes = base.readJsonFile(scopes_json_fn)
+ scopes_ldif_fn = os.path.join(self.output_folder, 'scopes.ldif')
+ self.createDirs(self.output_folder)
+
+ scopes_list = []
+
+ with open(scopes_ldif_fn, 'wb') as scope_ldif_fd:
+ ldif_scopes_writer = LDIFWriter(scope_ldif_fd, cols=1000)
+ for scope in scopes:
+ scope_dn = 'inum={},ou=scopes,o=jans'.format(scope['inum'])
+ scopes_list.append(scope_dn)
+ ldif_dict = {
+ 'objectClass': ['top', 'jansScope'],
+ 'description': [scope['description']],
+ 'displayName': [scope['displayName']],
+ 'inum': [scope['inum']],
+ 'jansDefScope': [str(scope['jansDefScope'])],
+ 'jansId': [scope['jansId']],
+ 'jansScopeTyp': [scope['jansScopeTyp']],
+ 'jansAttrs': [json.dumps({
+ "spontaneousClientId":None,
+ "spontaneousClientScopes":[],
+ "showInConfigurationEndpoint": False
+ })],
+ }
+ ldif_scopes_writer.unparse(scope_dn, ldif_dict)
+
+ self.dbUtils.import_ldif([scopes_ldif_fn])
+ self.service_scopes_created = True
+ return scopes_list
diff --git a/jans-linux-setup/jans_setup/setup_app/installers/jans_casa.py b/jans-linux-setup/jans_setup/setup_app/installers/jans_casa.py
index 7c47effb813..3d00ac032ae 100644
--- a/jans-linux-setup/jans_setup/setup_app/installers/jans_casa.py
+++ b/jans-linux-setup/jans_setup/setup_app/installers/jans_casa.py
@@ -78,7 +78,8 @@ def add_plugins(self):
def generate_configuration(self):
- self.casa_scopes = self.create_scopes()
+ if not hasattr(self, 'casa_scopes'):
+ self.casa_scopes = self.create_scopes()
self.check_clients([('casa_client_id', self.client_id_prefix)])
@@ -117,38 +118,6 @@ def create_folders(self):
self.createDirs(os.path.join(self.jetty_service_dir, cdir))
- def create_scopes(self):
- self.logIt("Creating Casa client scopes")
- scopes = base.readJsonFile(self.scopes_fn)
- casa_scopes_ldif_fn = os.path.join(self.output_folder, 'scopes.ldif')
- self.createDirs(self.output_folder)
- scope_ldif_fd = open(casa_scopes_ldif_fn, 'wb')
- scopes_list = []
-
- ldif_scopes_writer = LDIFWriter(scope_ldif_fd, cols=1000)
-
- for scope in scopes:
- scope_dn = 'inum={},ou=scopes,o=jans'.format(scope['inum'])
- scopes_list.append(scope_dn)
- ldif_dict = {
- 'objectClass': ['top', 'jansScope'],
- 'description': [scope['description']],
- 'displayName': [scope['displayName']],
- 'inum': [scope['inum']],
- 'jansDefScope': [str(scope['jansDefScope'])],
- 'jansId': [scope['jansId']],
- 'jansScopeTyp': [scope['jansScopeTyp']],
- 'jansAttrs': [json.dumps({"spontaneousClientId":None, "spontaneousClientScopes":[], "showInConfigurationEndpoint": False})],
- }
- ldif_scopes_writer.unparse(scope_dn, ldif_dict)
-
- scope_ldif_fd.close()
-
- self.dbUtils.import_ldif([casa_scopes_ldif_fn])
-
- return scopes_list
-
-
def service_post_setup(self):
self.writeFile(os.path.join(self.jetty_service_dir, '.administrable'), '', backup=False)
self.chown(self.jetty_service_dir, Config.jetty_user, Config.jetty_group, recursive=True)
diff --git a/jans-linux-setup/jans_setup/static/rdbm/sql_data_types.json b/jans-linux-setup/jans_setup/static/rdbm/sql_data_types.json
index 88f52e7e44d..be4af219580 100644
--- a/jans-linux-setup/jans_setup/static/rdbm/sql_data_types.json
+++ b/jans-linux-setup/jans_setup/static/rdbm/sql_data_types.json
@@ -4,6 +4,11 @@
"type": "TEXT"
}
},
+ "clntDat": {
+ "mysql": {
+ "type": "TEXT"
+ }
+ },
"description": {
"mysql": {
"size": 768,
diff --git a/jans-linux-setup/jans_setup/templates/jans-lock/dynamic-conf.json b/jans-linux-setup/jans_setup/templates/jans-lock/dynamic-conf.json
index 28b61e923ff..81f47b5119c 100644
--- a/jans-linux-setup/jans_setup/templates/jans-lock/dynamic-conf.json
+++ b/jans-linux-setup/jans_setup/templates/jans-lock/dynamic-conf.json
@@ -51,6 +51,7 @@
"metricReporterInterval": 300,
"metricReporterKeepDataDays": 15,
"metricReporterEnabled": true,
+ "statEnabled": true,
"errorReasonEnabled": false,
"opaConfiguration": {
"baseUrl": "http://%(jans_opa_host)s:%(jans_opa_port)s/v1/",
diff --git a/jans-linux-setup/jans_setup/templates/jans-lock/errors.json b/jans-linux-setup/jans_setup/templates/jans-lock/errors.json
index c6cf6d78f14..442fcfcb538 100644
--- a/jans-linux-setup/jans_setup/templates/jans-lock/errors.json
+++ b/jans-linux-setup/jans_setup/templates/jans-lock/errors.json
@@ -1,9 +1,26 @@
{
- "common": [
- {
- "id": "unknown_error",
- "description": "Unknown or not found error",
- "uri": null
- }
- ]
-}
\ No newline at end of file
+ "common": [
+ {
+ "id": "invalid_request",
+ "description": "The request is missing a required parameter, includes an unsupported parameter or parameter value, or is otherwise malformed",
+ "uri": null
+ },
+ {
+ "id": "unknown_error",
+ "description": "Unknown or not found error",
+ "uri": null
+ }
+ ],
+ "stat":[
+ {
+ "id":"invalid_request",
+ "description":"The request is missing a required parameter, includes an unsupported parameter or parameter value, repeats a parameter, includes multiple credentials, utilizes more than one mechanism for authenticating the client, or is otherwise malformed.",
+ "uri":null
+ },
+ {
+ "id":"access_denied",
+ "description":"The resource owner or authorization server denied the request.",
+ "uri":null
+ }
+ ]
+ }
diff --git a/jans-linux-setup/jans_setup/templates/jans-lock/scopes.json b/jans-linux-setup/jans_setup/templates/jans-lock/scopes.json
new file mode 100644
index 00000000000..104baf2a1ac
--- /dev/null
+++ b/jans-linux-setup/jans_setup/templates/jans-lock/scopes.json
@@ -0,0 +1,10 @@
+[
+ {
+ "inum": "4000.01.1",
+ "jansId": "https://jans.io/oauth/lock/sse.read",
+ "displayName": "Lock API scope",
+ "description": "Permission to access SSE endpoint",
+ "jansDefScope": false,
+ "jansScopeTyp": "oauth"
+ }
+]
diff --git a/jans-linux-setup/jans_setup/templates/jans-lock/static-conf.json b/jans-linux-setup/jans_setup/templates/jans-lock/static-conf.json
index da3b6aee9a7..4647113b47d 100644
--- a/jans-linux-setup/jans_setup/templates/jans-lock/static-conf.json
+++ b/jans-linux-setup/jans_setup/templates/jans-lock/static-conf.json
@@ -6,6 +6,7 @@
"attributes":"ou=attributes,o=jans",
"tokens":"ou=tokens,o=jans",
"sessions":"ou=sessions,o=jans",
- "metric":"ou=statistic,o=metric"
+ "metric":"ou=statistic,o=metric",
+ "stat": "ou=lock,ou=stat,o=jans"
}
}
diff --git a/jans-lock/lock-server/model/src/main/java/io/jans/lock/model/config/AppConfiguration.java b/jans-lock/lock-server/model/src/main/java/io/jans/lock/model/config/AppConfiguration.java
index 45e59344cf3..042ee8849b1 100644
--- a/jans-lock/lock-server/model/src/main/java/io/jans/lock/model/config/AppConfiguration.java
+++ b/jans-lock/lock-server/model/src/main/java/io/jans/lock/model/config/AppConfiguration.java
@@ -46,6 +46,14 @@ public class AppConfiguration implements Configuration {
@DocProperty(description = "OpenID issuer URL")
@Schema(description = "OpenID issuer URL")
private String openIdIssuer;
+
+ @DocProperty(description = "Active stat enabled")
+ @Schema(description = "Active stat enabled")
+ private boolean statEnabled;
+
+ @DocProperty(description = "Statistical data capture time interval")
+ @Schema(description = "Statistical data capture time interval")
+ private int statTimerIntervalInSeconds;
@DocProperty(description = "List of token channel names", defaultValue = "jans_token")
@Schema(description = "List of token channel names")
@@ -135,6 +143,9 @@ public class AppConfiguration implements Configuration {
@Schema(description = "List of Zip Uris with policies")
private List policiesZipUris;
+ @DocProperty(description = "Boolean value specifying whether to return detailed reason of the error from AS. Default value is false", defaultValue = "false")
+ private Boolean errorReasonEnabled = false;
+
public String getBaseDN() {
return baseDN;
}
@@ -159,7 +170,23 @@ public void setOpenIdIssuer(String openIdIssuer) {
this.openIdIssuer = openIdIssuer;
}
- public List getTokenChannels() {
+ public boolean isStatEnabled() {
+ return statEnabled;
+ }
+
+ public void setStatEnabled(boolean statEnabled) {
+ this.statEnabled = statEnabled;
+ }
+
+ public int getStatTimerIntervalInSeconds() {
+ return statTimerIntervalInSeconds;
+ }
+
+ public void setStatTimerIntervalInSeconds(int statTimerIntervalInSeconds) {
+ this.statTimerIntervalInSeconds = statTimerIntervalInSeconds;
+ }
+
+ public List getTokenChannels() {
return tokenChannels;
}
@@ -335,19 +362,31 @@ public void setPoliciesZipUris(List policiesZipUris) {
this.policiesZipUris = policiesZipUris;
}
- @Override
- public String toString() {
- return "AppConfiguration [baseDN=" + baseDN + ", baseEndpoint=" + baseEndpoint + ", openIdIssuer="
- + openIdIssuer + ", tokenChannels=" + tokenChannels + ", clientId=" + clientId + ", tokenUrl="
- + tokenUrl + ", groupScopeEnabled=" + groupScopeEnabled+ ", endpointGroups=" + endpointGroups + ", endpointDetails=" + endpointDetails
- + ", disableJdkLogger=" + disableJdkLogger + ", loggingLevel=" + loggingLevel + ", loggingLayout="
- + loggingLayout + ", externalLoggerConfiguration=" + externalLoggerConfiguration + ", metricChannel="
- + metricChannel + ", metricReporterInterval=" + metricReporterInterval + ", metricReporterKeepDataDays="
- + metricReporterKeepDataDays + ", metricReporterEnabled=" + metricReporterEnabled
- + ", cleanServiceInterval=" + cleanServiceInterval + ", opaConfiguration=" + opaConfiguration
- + ", pdpType=" + pdpType + ", policiesJsonUrisAuthorizationToken=" + policiesJsonUrisAuthorizationToken
- + ", policiesJsonUris=" + policiesJsonUris + ", policiesZipUrisAuthorizationToken="
- + policiesZipUrisAuthorizationToken + ", policiesZipUris=" + policiesZipUris + "]";
+ public Boolean getErrorReasonEnabled() {
+ if (errorReasonEnabled == null) errorReasonEnabled = false;
+ return errorReasonEnabled;
+ }
+
+ public void setErrorReasonEnabled(Boolean errorReasonEnabled) {
+ this.errorReasonEnabled = errorReasonEnabled;
}
+ @Override
+ public String toString() {
+ return "AppConfiguration [baseDN=" + baseDN + ", baseEndpoint=" + baseEndpoint + ", openIdIssuer="
+ + openIdIssuer + ", statEnabled=" + statEnabled + ", statTimerIntervalInSeconds="
+ + statTimerIntervalInSeconds + ", tokenChannels=" + tokenChannels + ", clientId=" + clientId
+ + ", clientPassword=" + clientPassword + ", tokenUrl=" + tokenUrl + ", groupScopeEnabled="
+ + groupScopeEnabled + ", endpointGroups=" + endpointGroups + ", endpointDetails=" + endpointDetails
+ + ", disableJdkLogger=" + disableJdkLogger + ", loggingLevel=" + loggingLevel + ", loggingLayout="
+ + loggingLayout + ", externalLoggerConfiguration=" + externalLoggerConfiguration + ", metricChannel="
+ + metricChannel + ", metricReporterInterval=" + metricReporterInterval + ", metricReporterKeepDataDays="
+ + metricReporterKeepDataDays + ", metricReporterEnabled=" + metricReporterEnabled
+ + ", cleanServiceInterval=" + cleanServiceInterval + ", opaConfiguration=" + opaConfiguration
+ + ", pdpType=" + pdpType + ", policiesJsonUrisAuthorizationToken=" + policiesJsonUrisAuthorizationToken
+ + ", policiesJsonUris=" + policiesJsonUris + ", policiesZipUrisAuthorizationToken="
+ + policiesZipUrisAuthorizationToken + ", policiesZipUris=" + policiesZipUris + ", errorReasonEnabled="
+ + errorReasonEnabled + "]";
+ }
+
}
diff --git a/jans-lock/lock-server/model/src/main/java/io/jans/lock/model/config/BaseDnConfiguration.java b/jans-lock/lock-server/model/src/main/java/io/jans/lock/model/config/BaseDnConfiguration.java
index 4e911975ee3..265f7bcf74f 100644
--- a/jans-lock/lock-server/model/src/main/java/io/jans/lock/model/config/BaseDnConfiguration.java
+++ b/jans-lock/lock-server/model/src/main/java/io/jans/lock/model/config/BaseDnConfiguration.java
@@ -36,6 +36,7 @@ public class BaseDnConfiguration {
private String tokens;
private String scripts;
private String metric;
+ private String stat;
public String getConfiguration() {
return configuration;
@@ -93,4 +94,12 @@ public void setMetric(String metric) {
this.metric = metric;
}
+ public String getStat() {
+ return stat;
+ }
+
+ public void setStat(String stat) {
+ this.stat = stat;
+ }
+
}
diff --git a/jans-lock/lock-server/model/src/main/java/io/jans/lock/model/config/ErrorMessages.java b/jans-lock/lock-server/model/src/main/java/io/jans/lock/model/config/ErrorMessages.java
index d0b15cb02e0..bb318aabf66 100644
--- a/jans-lock/lock-server/model/src/main/java/io/jans/lock/model/config/ErrorMessages.java
+++ b/jans-lock/lock-server/model/src/main/java/io/jans/lock/model/config/ErrorMessages.java
@@ -22,6 +22,11 @@
import io.jans.model.error.ErrorMessage;
import jakarta.enterprise.inject.Vetoed;
+import jakarta.xml.bind.annotation.XmlAccessType;
+import jakarta.xml.bind.annotation.XmlAccessorType;
+import jakarta.xml.bind.annotation.XmlElement;
+import jakarta.xml.bind.annotation.XmlElementWrapper;
+import jakarta.xml.bind.annotation.XmlRootElement;
/**
* Base interface for all Jans Auth configurations
@@ -29,11 +34,19 @@
* @author Yuriy Movchan Date: 12/18/2023
*/
@Vetoed
+@XmlRootElement(name = "errors")
+@XmlAccessorType(XmlAccessType.FIELD)
@JsonIgnoreProperties(ignoreUnknown = true)
public class ErrorMessages implements Configuration {
+ @XmlElementWrapper(name = "common")
+ @XmlElement(name = "error")
private List common;
+ @XmlElementWrapper(name = "stat")
+ @XmlElement(name = "error")
+ private List stat;
+
public List getCommon() {
return common;
}
@@ -42,4 +55,12 @@ public void setCommon(List common) {
this.common = common;
}
+ public List getStat() {
+ return stat;
+ }
+
+ public void setStat(List stat) {
+ this.stat = stat;
+ }
+
}
diff --git a/jans-lock/lock-server/server/src/main/java/io/jans/lock/server/service/AppInitializer.java b/jans-lock/lock-server/server/src/main/java/io/jans/lock/server/service/AppInitializer.java
index 6274f2ae623..b454ca1981f 100644
--- a/jans-lock/lock-server/server/src/main/java/io/jans/lock/server/service/AppInitializer.java
+++ b/jans-lock/lock-server/server/src/main/java/io/jans/lock/server/service/AppInitializer.java
@@ -28,6 +28,8 @@
import io.jans.exception.ConfigurationException;
import io.jans.lock.service.config.ApplicationFactory;
import io.jans.lock.service.config.ConfigurationFactory;
+import io.jans.lock.service.stat.StatService;
+import io.jans.lock.service.stat.StatTimer;
import io.jans.lock.service.status.StatusCheckerTimer;
import io.jans.model.custom.script.CustomScriptType;
import io.jans.orm.PersistenceEntryManager;
@@ -123,6 +125,11 @@ public class AppInitializer {
@Inject
private DocumentStoreManager documentStoreManager;
+
+ @Inject
+ private StatService statService;
+
+ @Inject StatTimer statTimer;
@PostConstruct
public void createApplicationComponents() {
@@ -154,6 +161,9 @@ public void applicationInitialized(@Observes @Initialized(ApplicationScoped.clas
// Initialize script manager
List supportedCustomScriptTypes = Lists.newArrayList(CustomScriptType.LOCK_EXTENSION);
+
+ // Initialize stat service
+ statService.init();
// Start timer
initSchedulerService();
@@ -162,10 +172,11 @@ public void applicationInitialized(@Observes @Initialized(ApplicationScoped.clas
loggerService.initTimer();
statusCheckerTimer.initTimer();
customScriptManager.initTimer(supportedCustomScriptTypes);
+ statTimer.initTimer();
// Initialize Document Store Manager
documentStoreManager.initTimer(Arrays.asList(DOCUMENT_STORE_MANAGER_JANS_LOCK_TYPE));
-
+
// Notify plugins about finish application initialization
eventApplicationInitialized.select(ApplicationInitialized.Literal.APPLICATION)
.fire(new ApplicationInitializedEvent());
diff --git a/jans-lock/lock-server/service/pom.xml b/jans-lock/lock-server/service/pom.xml
index 02d91f09c5e..5a35cea34df 100644
--- a/jans-lock/lock-server/service/pom.xml
+++ b/jans-lock/lock-server/service/pom.xml
@@ -67,7 +67,6 @@
io.jans
jans-auth-model
- ${project.version}
@@ -109,6 +108,10 @@
commons-codec
commons-codec
+
+ commons-beanutils
+ commons-beanutils
+
commons-collections
commons-collections
@@ -121,6 +124,14 @@
commons-io
commons-io
+
+ io.prometheus
+ simpleclient_common
+
+
+ net.agkn
+ hll
+
diff --git a/jans-lock/lock-server/service/src/main/java/io/jans/lock/model/Stat.java b/jans-lock/lock-server/service/src/main/java/io/jans/lock/model/Stat.java
new file mode 100644
index 00000000000..7cfffeaf620
--- /dev/null
+++ b/jans-lock/lock-server/service/src/main/java/io/jans/lock/model/Stat.java
@@ -0,0 +1,61 @@
+package io.jans.lock.model;
+
+import java.io.Serializable;
+import java.util.HashMap;
+import java.util.Map;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+/**
+ * @author Yuriy Movchan Date: 12/02/2024
+ */
+@JsonIgnoreProperties(ignoreUnknown = true)
+public class Stat implements Serializable {
+
+ private static final long serialVersionUID = -1659698750177377994L;
+
+ @JsonProperty("countOpByType")
+ private Map> operationsByType;
+
+ @JsonProperty("lastUpdatedAt")
+ private long lastUpdatedAt;
+
+ @JsonProperty("month")
+ private String month;
+
+ public Map> getOperationsByType() {
+ if (operationsByType == null) operationsByType = new HashMap<>();
+ return operationsByType;
+ }
+
+ public void setOperationsByType(Map> operationsByType) {
+ this.operationsByType = operationsByType;
+ }
+
+ public long getLastUpdatedAt() {
+ return lastUpdatedAt;
+ }
+
+ public void setLastUpdatedAt(long lastUpdatedAt) {
+ this.lastUpdatedAt = lastUpdatedAt;
+ }
+
+ public String getMonth() {
+ return month;
+ }
+
+ public void setMonth(String month) {
+ this.month = month;
+ }
+
+ @Override
+ public String toString() {
+ return "Stat{" +
+ "operationsByType=" + operationsByType +
+ ", lastUpdatedAt=" + lastUpdatedAt +
+ ", month='" + month + '\'' +
+ '}';
+ }
+}
+
diff --git a/jans-lock/lock-server/service/src/main/java/io/jans/lock/model/StatEntry.java b/jans-lock/lock-server/service/src/main/java/io/jans/lock/model/StatEntry.java
new file mode 100644
index 00000000000..40922bddd6f
--- /dev/null
+++ b/jans-lock/lock-server/service/src/main/java/io/jans/lock/model/StatEntry.java
@@ -0,0 +1,76 @@
+package io.jans.lock.model;
+
+import io.jans.orm.annotation.AttributeName;
+import io.jans.orm.annotation.DataEntry;
+import io.jans.orm.annotation.JsonObject;
+import io.jans.orm.annotation.ObjectClass;
+import io.jans.orm.model.base.BaseEntry;
+
+/**
+ * @author Yuriy Movchan Date: 12/02/2024
+ */
+@DataEntry
+@ObjectClass(value = "jansLockStatEntry")
+public class StatEntry extends BaseEntry {
+
+ private static final long serialVersionUID = 7349181838267756343L;
+
+ @AttributeName(name = "jansId")
+ private String id;
+
+ @AttributeName(name = "jansData")
+ private String month;
+
+ @AttributeName(name = "dat")
+ private String userHllData;
+
+ @AttributeName(name = "clntDat")
+ private String clientHllData;
+
+ @AttributeName(name = "attr")
+ @JsonObject
+ private Stat stat;
+
+ public String getMonth() {
+ return month;
+ }
+
+ public void setMonth(String month) {
+ this.month = month;
+ }
+
+ public String getId() {
+ return id;
+ }
+
+ public void setId(String id) {
+ this.id = id;
+ }
+
+ public String getUserHllData() {
+ return userHllData;
+ }
+
+ public void setUserHllData(String userHllData) {
+ this.userHllData = userHllData;
+ }
+
+ public String getClientHllData() {
+ return clientHllData;
+ }
+
+ public void setClientHllData(String clientHllData) {
+ this.clientHllData = clientHllData;
+ }
+
+ public Stat getStat() {
+ if (stat == null) {
+ stat = new Stat();
+ }
+ return stat;
+ }
+
+ public void setStat(Stat stat) {
+ this.stat = stat;
+ }
+}
diff --git a/jans-lock/lock-server/service/src/main/java/io/jans/lock/model/error/CommonErrorResponseType.java b/jans-lock/lock-server/service/src/main/java/io/jans/lock/model/error/CommonErrorResponseType.java
new file mode 100644
index 00000000000..fe8d5547478
--- /dev/null
+++ b/jans-lock/lock-server/service/src/main/java/io/jans/lock/model/error/CommonErrorResponseType.java
@@ -0,0 +1,38 @@
+package io.jans.lock.model.error;
+
+import io.jans.as.model.error.IErrorType;
+
+/**
+ * @author Yuriy Movchan Date: 23/02/2024
+ */
+public enum CommonErrorResponseType implements IErrorType {
+
+ /**
+ * The request is missing a required parameter, includes an
+ * invalid parameter value, includes a parameter more than
+ * once, or is otherwise malformed.
+ */
+ INVALID_REQUEST("invalid_request"),
+
+ /**
+ * Unknown or not found error.
+ */
+ UNKNOWN_ERROR("unknown_error"),
+ ;
+
+ private final String paramName;
+
+ CommonErrorResponseType(String paramName) {
+ this.paramName = paramName;
+ }
+
+ @Override
+ public String getParameter() {
+ return paramName;
+ }
+
+ @Override
+ public String toString() {
+ return paramName;
+ }
+}
diff --git a/jans-lock/lock-server/service/src/main/java/io/jans/lock/model/error/ErrorResponseFactory.java b/jans-lock/lock-server/service/src/main/java/io/jans/lock/model/error/ErrorResponseFactory.java
new file mode 100644
index 00000000000..25d0d4d55e9
--- /dev/null
+++ b/jans-lock/lock-server/service/src/main/java/io/jans/lock/model/error/ErrorResponseFactory.java
@@ -0,0 +1,149 @@
+package io.jans.lock.model.error;
+
+import io.jans.as.model.config.Constants;
+import io.jans.as.model.configuration.Configuration;
+import io.jans.as.model.error.DefaultErrorResponse;
+import io.jans.as.model.error.IErrorType;
+import io.jans.lock.model.config.AppConfiguration;
+import io.jans.lock.model.config.ErrorMessages;
+import io.jans.model.error.ErrorMessage;
+import jakarta.ws.rs.WebApplicationException;
+import jakarta.ws.rs.core.MediaType;
+import jakarta.ws.rs.core.Response;
+import org.apache.commons.lang3.BooleanUtils;
+import org.apache.logging.log4j.ThreadContext;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.List;
+import java.util.Optional;
+import java.util.function.Predicate;
+
+import static jakarta.ws.rs.core.Response.Status.*;
+
+/**
+ * @author Yuriy Movchan Date: 23/02/2024
+ */
+public class ErrorResponseFactory implements Configuration {
+
+ private static final Logger log = LoggerFactory.getLogger(ErrorResponseFactory.class);
+
+ private ErrorMessages messages;
+
+ private AppConfiguration appConfiguration;
+
+ public ErrorResponseFactory() {
+ }
+
+ public ErrorResponseFactory(ErrorMessages messages, AppConfiguration appConfiguration) {
+ this.messages = messages;
+ this.appConfiguration = appConfiguration;
+ }
+
+ public WebApplicationException createWebApplicationException(Response.Status status, IErrorType type, String reason) {
+ return createWebApplicationException(status, type, reason, null);
+ }
+
+ private WebApplicationException createWebApplicationException(Response.Status status, IErrorType type, String reason, Throwable e) {
+ WebApplicationException error = new WebApplicationException(Response
+ .status(status)
+ .entity(errorAsJson(type, reason))
+ .type(MediaType.APPLICATION_JSON_TYPE)
+ .build());
+ if (log.isErrorEnabled()) {
+ log.error("Exception Handle, status: {}, body: {}", formatStatus(error.getResponse().getStatusInfo()), error.getResponse().getEntity(), e);
+ }
+ return error;
+ }
+
+ public WebApplicationException badRequestException(IErrorType type, String reason) {
+ return createWebApplicationException(BAD_REQUEST, type, reason);
+ }
+
+ public WebApplicationException badRequestException(IErrorType type, String reason, Throwable e) {
+ return createWebApplicationException(BAD_REQUEST, type, reason, e);
+ }
+
+ public WebApplicationException notFoundException(IErrorType type, String reason) {
+ return createWebApplicationException(NOT_FOUND, type, reason);
+ }
+
+ public WebApplicationException forbiddenException() {
+ WebApplicationException error = new WebApplicationException(Response
+ .status(FORBIDDEN)
+ .entity("")
+ .build());
+ if (log.isErrorEnabled()) {
+ log.error("Exception Handle, status: {}", formatStatus(error.getResponse().getStatusInfo()));
+ }
+ return error;
+ }
+
+ public WebApplicationException invalidRequest(String reason) {
+ return createWebApplicationException(BAD_REQUEST, CommonErrorResponseType.INVALID_REQUEST, reason);
+ }
+
+ public WebApplicationException invalidRequest(String reason, Throwable e) {
+ return createWebApplicationException(BAD_REQUEST, CommonErrorResponseType.INVALID_REQUEST, reason, e);
+ }
+
+ public WebApplicationException unknownError(String reason) {
+ throw createWebApplicationException(INTERNAL_SERVER_ERROR, CommonErrorResponseType.UNKNOWN_ERROR, reason);
+ }
+
+ private String errorAsJson(IErrorType type, String reason) {
+ final DefaultErrorResponse error = getErrorResponse(type);
+ error.setReason(BooleanUtils.isTrue(appConfiguration.getErrorReasonEnabled()) ? reason : "");
+ return error.toJSonString();
+ }
+
+ private DefaultErrorResponse getErrorResponse(IErrorType type) {
+ final DefaultErrorResponse response = new DefaultErrorResponse();
+ response.setType(type);
+ if (type != null && messages != null) {
+ List list = null;
+ if (type instanceof CommonErrorResponseType) {
+ list = messages.getCommon();
+ } else if (type instanceof StatErrorResponseType) {
+ list = messages.getStat();
+ }
+ if (list != null) {
+ final ErrorMessage m = getError(list, type);
+ String description = Optional.ofNullable(ThreadContext.get(Constants.CORRELATION_ID_HEADER))
+ .map(id -> m.getDescription().concat(" CorrelationId: " + id))
+ .orElse(m.getDescription());
+ response.setErrorDescription(description);
+ response.setErrorUri(m.getUri());
+ }
+ }
+
+ return response;
+ }
+
+ /**
+ * Looks for an error message.
+ *
+ * @param list error list
+ * @param type The type of the error.
+ * @return Error message or null
if not found.
+ */
+ private ErrorMessage getError(List list, IErrorType type) {
+ log.debug("Looking for the error with id: {}", type);
+
+ if (list != null) {
+ Predicate equalsErrorMessageId = s -> s.getId().equals(type.getParameter());
+ Optional errorMessage = list.stream().filter(equalsErrorMessageId).findFirst();
+ if (errorMessage.isPresent()) {
+ log.debug("Found error, id: {}", type);
+ return errorMessage.get();
+ }
+ }
+
+ log.error("Error not found, id: {}", type);
+ return new ErrorMessage(type.getParameter(), type.getParameter(), null);
+ }
+
+ private String formatStatus(Response.StatusType status) {
+ return String.format("%s %s", status.getStatusCode(), status);
+ }
+}
diff --git a/jans-lock/lock-server/service/src/main/java/io/jans/lock/model/error/StatErrorResponseType.java b/jans-lock/lock-server/service/src/main/java/io/jans/lock/model/error/StatErrorResponseType.java
new file mode 100644
index 00000000000..4db9a85bd07
--- /dev/null
+++ b/jans-lock/lock-server/service/src/main/java/io/jans/lock/model/error/StatErrorResponseType.java
@@ -0,0 +1,70 @@
+/*
+ * Janssen Project software is available under the Apache License (2004). See http://www.apache.org/licenses/ for full text.
+ *
+ * Copyright (c) 2020, Janssen Project
+ */
+
+package io.jans.lock.model.error;
+
+import io.jans.as.model.error.IErrorType;
+
+/**
+ * @author Yuriy Movchan Date: 23/02/2024
+ */
+public enum StatErrorResponseType implements IErrorType {
+ /**
+ * The request is missing a required parameter, includes an unsupported
+ * parameter or parameter value, repeats a parameter, includes multiple
+ * credentials, utilizes more than one mechanism for authenticating the
+ * client, or is otherwise malformed.
+ */
+ INVALID_REQUEST("invalid_request"),
+
+ /**
+ * The end-user denied the authorization request.
+ */
+ ACCESS_DENIED("access_denied");
+
+ private final String paramName;
+
+ StatErrorResponseType(String paramName) {
+ this.paramName = paramName;
+ }
+
+ /**
+ * Returns the corresponding {@link StatErrorResponseType} from a given string.
+ *
+ * @param param The string value to convert.
+ * @return The corresponding {@link StatErrorResponseType}, otherwise null
.
+ */
+ public static StatErrorResponseType fromString(String param) {
+ if (param != null) {
+ for (StatErrorResponseType err : StatErrorResponseType.values()) {
+ if (param.equals(err.paramName)) {
+ return err;
+ }
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Returns a string representation of the object. In this case the parameter name.
+ *
+ * @return The string representation of the object.
+ */
+ @Override
+ public String toString() {
+ return paramName;
+ }
+
+ /**
+ * Gets error parameter.
+ *
+ * @return error parameter
+ */
+ @Override
+ public String getParameter() {
+ return paramName;
+ }
+}
diff --git a/jans-lock/lock-server/service/src/main/java/io/jans/lock/service/audit/AuditService.java b/jans-lock/lock-server/service/src/main/java/io/jans/lock/service/audit/AuditService.java
index 9e6e8b7f3ab..dccfb3300ce 100644
--- a/jans-lock/lock-server/service/src/main/java/io/jans/lock/service/audit/AuditService.java
+++ b/jans-lock/lock-server/service/src/main/java/io/jans/lock/service/audit/AuditService.java
@@ -22,6 +22,15 @@
@ApplicationScoped
public class AuditService {
+ public static final String AUDIT_TELEMETRY = "telemetry";
+ public static final String AUDIT_TELEMETRY_BULK = "telemetry/bulk";
+
+ public static final String AUDIT_LOG = "log";
+ public static final String AUDIT_LOG_BULK = "log/bulk";
+
+ public static final String AUDIT_HEALTH = "health";
+ public static final String AUDIT_HEALTH_BULK = "health/bulk";
+
@Inject
private Logger log;
diff --git a/jans-lock/lock-server/service/src/main/java/io/jans/lock/service/config/ConfigurationFactory.java b/jans-lock/lock-server/service/src/main/java/io/jans/lock/service/config/ConfigurationFactory.java
index a3a1052764d..240abfcab4a 100644
--- a/jans-lock/lock-server/service/src/main/java/io/jans/lock/service/config/ConfigurationFactory.java
+++ b/jans-lock/lock-server/service/src/main/java/io/jans/lock/service/config/ConfigurationFactory.java
@@ -28,6 +28,7 @@
import io.jans.lock.model.config.Conf;
import io.jans.lock.model.config.Configuration;
import io.jans.lock.model.config.StaticConfiguration;
+import io.jans.lock.model.error.ErrorResponseFactory;
import io.jans.orm.PersistenceEntryManager;
import io.jans.orm.exception.BasePersistenceException;
import io.jans.orm.model.PersistenceConfiguration;
@@ -80,6 +81,8 @@ public class ConfigurationFactory extends ApplicationConfigurationFactory {
@Inject
private Instance configurationInstance;
+ private ErrorResponseFactory errorResponseFactory;
+
public final static String PERSISTENCE_CONFIGUARION_RELOAD_EVENT_TYPE = "persistenceConfigurationReloadEvent";
public final static String BASE_CONFIGUARION_RELOAD_EVENT_TYPE = "baseConfigurationReloadEvent";
@@ -250,6 +253,12 @@ public StaticConfiguration getStaticConfiguration() {
return staticConf;
}
+ @Produces
+ @ApplicationScoped
+ public ErrorResponseFactory getFido2ErrorResponseFactory() {
+ return errorResponseFactory;
+ }
+
public BaseDnConfiguration getBaseDn() {
return getStaticConfiguration().getBaseDn();
}
@@ -327,6 +336,9 @@ private void initConfigurationConf(Conf conf) {
if (conf.getStatics() != null) {
staticConf = conf.getStatics();
}
+ if (conf.getErrors() != null) {
+ errorResponseFactory = new ErrorResponseFactory(conf.getErrors(), conf.getDynamic());
+ }
}
private void loadBaseConfiguration() {
diff --git a/jans-lock/lock-server/service/src/main/java/io/jans/lock/service/event/StatEvent.java b/jans-lock/lock-server/service/src/main/java/io/jans/lock/service/event/StatEvent.java
new file mode 100644
index 00000000000..c05cdcd010d
--- /dev/null
+++ b/jans-lock/lock-server/service/src/main/java/io/jans/lock/service/event/StatEvent.java
@@ -0,0 +1,7 @@
+package io.jans.lock.service.event;
+
+/**
+ * @author Yuriy Movchan Date: 12/02/2024
+ */
+public class StatEvent {
+}
diff --git a/jans-lock/lock-server/service/src/main/java/io/jans/lock/service/filter/openid/OpenIdProtectionService.java b/jans-lock/lock-server/service/src/main/java/io/jans/lock/service/filter/openid/OpenIdProtectionService.java
index 7dcd87831f7..39cbc12c1f4 100644
--- a/jans-lock/lock-server/service/src/main/java/io/jans/lock/service/filter/openid/OpenIdProtectionService.java
+++ b/jans-lock/lock-server/service/src/main/java/io/jans/lock/service/filter/openid/OpenIdProtectionService.java
@@ -6,6 +6,7 @@
import java.lang.annotation.Annotation;
import java.lang.reflect.AnnotatedElement;
+import java.lang.reflect.Method;
import java.net.URL;
import java.util.ArrayList;
import java.util.Arrays;
@@ -160,6 +161,22 @@ private List getRequestedScopes(ResourceInfo resourceInfo) {
scopes.addAll(getScopesFromAnnotation(resourceInfo.getResourceClass()));
scopes.addAll(getScopesFromAnnotation(resourceInfo.getResourceMethod()));
+ Method baseMethod = resourceInfo.getResourceMethod();
+ for (Class> interfaces : resourceInfo.getResourceClass().getInterfaces()) {
+ scopes.addAll(getScopesFromAnnotation(interfaces));
+
+ Method method = null;
+ try {
+ method = interfaces.getDeclaredMethod(baseMethod.getName(), baseMethod.getParameterTypes());
+ } catch (NoSuchMethodException | SecurityException e) {
+ // It's expected behavior
+ }
+ if (method != null) {
+ scopes.addAll(getScopesFromAnnotation(method));
+ }
+
+ }
+
return scopes;
}
diff --git a/jans-lock/lock-server/service/src/main/java/io/jans/lock/service/stat/StatResponseService.java b/jans-lock/lock-server/service/src/main/java/io/jans/lock/service/stat/StatResponseService.java
new file mode 100644
index 00000000000..120b25516bf
--- /dev/null
+++ b/jans-lock/lock-server/service/src/main/java/io/jans/lock/service/stat/StatResponseService.java
@@ -0,0 +1,179 @@
+package io.jans.lock.service.stat;
+
+import static io.jans.as.model.util.Util.escapeLog;
+
+import java.util.Base64;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+import java.util.concurrent.TimeUnit;
+
+import org.slf4j.Logger;
+
+import com.google.common.cache.Cache;
+import com.google.common.cache.CacheBuilder;
+import com.google.common.collect.Lists;
+
+import io.jans.lock.model.StatEntry;
+import io.jans.lock.service.ws.rs.stat.StatResponse;
+import io.jans.lock.service.ws.rs.stat.StatResponseItem;
+import io.jans.orm.PersistenceEntryManager;
+import io.jans.orm.search.filter.Filter;
+import jakarta.enterprise.context.ApplicationScoped;
+import jakarta.inject.Inject;
+import net.agkn.hll.HLL;
+
+/**
+ * @author Yuriy Movchan Date: 12/02/2024
+ */
+@ApplicationScoped
+public class StatResponseService {
+
+ @Inject
+ private Logger log;
+
+ @Inject
+ private PersistenceEntryManager entryManager;
+
+ @Inject
+ private StatService statService;
+
+ private final Cache responseCache = CacheBuilder
+ .newBuilder()
+ .expireAfterWrite(1, TimeUnit.HOURS)
+ .build();
+
+ public StatResponse buildResponse(Set months) {
+ final String cacheKey = months.toString();
+ final StatResponse cachedResponse = responseCache.getIfPresent(cacheKey);
+ if (cachedResponse != null) {
+ if (log.isTraceEnabled()) {
+ log.trace("Get stat response from cache for: {}", escapeLog(cacheKey));
+ }
+ return cachedResponse;
+ }
+
+ StatResponse response = new StatResponse();
+ for (String month : months) {
+ final StatResponseItem responseItem = buildItem(month);
+ if (responseItem != null) {
+ response.getResponse().put(month, responseItem);
+ }
+ }
+
+ responseCache.put(cacheKey, response);
+ return response;
+ }
+
+ private StatResponseItem buildItem(String month) {
+ try {
+ final String escapedMonth = escapeLog(month);
+ log.trace("Trying to fetch stat for month: {}", escapedMonth);
+
+ final List entries = entryManager.findEntries(statService.getBaseDn(), StatEntry.class, Filter.createEqualityFilter("jansData", month));
+ if (entries == null || entries.isEmpty()) {
+ log.trace("Can't find stat entries for month: {}", escapedMonth);
+ return null;
+ }
+ log.trace("Fetched stat entries for month {} successfully", escapedMonth);
+
+ checkNotMatchedEntries(month, entries);
+ if (entries.isEmpty()) {
+ log.trace("No stat entries for month: {}", escapedMonth);
+ return null;
+ }
+
+ final StatResponseItem responseItem = new StatResponseItem();
+ responseItem.setMonthlyActiveUsers(userCardinality(entries));
+ responseItem.setMonthlyActiveClients(clientCardinality(entries));
+ responseItem.setMonth(month);
+
+ unionOpearationsMapIntoResponseItem(entries, responseItem);
+
+ return responseItem;
+ } catch (Exception e) {
+ log.error(e.getMessage(), e);
+ return null;
+ }
+ }
+
+ // This should not occur for newly created StatEntry (only outdated db)
+ private void checkNotMatchedEntries(String month, List entries) {
+ final List notMatched = Lists.newArrayList();
+ for (StatEntry entry : entries) {
+ if (!Objects.equals(month, entry.getMonth())) {
+ log.error("Not matched entry: {}", entry.getDn());
+ notMatched.add(entry);
+ }
+ }
+
+ entries.removeAll(notMatched);
+ }
+
+
+ private long userCardinality(List entries) {
+ HLL hll = decodeUserHll(entries.get(0));
+
+ // Union hll
+ if (entries.size() > 1) {
+ for (int i = 1; i < entries.size(); i++) {
+ hll.union(decodeUserHll(entries.get(i)));
+ }
+ }
+ return hll.cardinality();
+ }
+
+ private long clientCardinality(List entries) {
+ HLL hll = decodeClientHll(entries.get(0));
+
+ // Union hll
+ if (entries.size() > 1) {
+ for (int i = 1; i < entries.size(); i++) {
+ hll.union(decodeClientHll(entries.get(i)));
+ }
+ }
+ return hll.cardinality();
+ }
+
+ private HLL decodeUserHll(StatEntry entry) {
+ try {
+ return HLL.fromBytes(Base64.getDecoder().decode(entry.getUserHllData()));
+ } catch (Exception e) {
+ log.error("Failed to decode user HLL data, entry dn: {}, data: {}", entry.getDn(), entry.getUserHllData());
+ return statService.newUserHll();
+ }
+ }
+
+
+ private HLL decodeClientHll(StatEntry entry) {
+ try {
+ return HLL.fromBytes(Base64.getDecoder().decode(entry.getClientHllData()));
+ } catch (Exception e) {
+ log.error("Failed to decode client HLL data, entry dn: {}, data: {}", entry.getDn(), entry.getUserHllData());
+ return statService.newClientHll();
+ }
+ }
+
+ private static void unionOpearationsMapIntoResponseItem(List entries, StatResponseItem responseItem) {
+ for (StatEntry entry : entries) {
+ entry.getStat().getOperationsByType().entrySet().stream().filter(en -> en.getValue() != null).forEach(en -> {
+ final Map operationMap = responseItem.getOperationsByType().get(en.getKey());
+ if (operationMap == null) {
+ responseItem.getOperationsByType().put(en.getKey(), en.getValue());
+ return;
+ }
+ for (Map.Entry operationEntry : en.getValue().entrySet()) {
+ final Long counter = operationMap.get(operationEntry.getKey());
+ if (counter == null) {
+ operationMap.put(operationEntry.getKey(), operationEntry.getValue());
+ continue;
+ }
+
+ operationMap.put(operationEntry.getKey(), counter + operationEntry.getValue());
+ }
+ });
+ }
+ }
+
+}
diff --git a/jans-lock/lock-server/service/src/main/java/io/jans/lock/service/stat/StatService.java b/jans-lock/lock-server/service/src/main/java/io/jans/lock/service/stat/StatService.java
new file mode 100644
index 00000000000..3756017dc21
--- /dev/null
+++ b/jans-lock/lock-server/service/src/main/java/io/jans/lock/service/stat/StatService.java
@@ -0,0 +1,334 @@
+package io.jans.lock.service.stat;
+
+import java.text.SimpleDateFormat;
+import java.util.Base64;
+import java.util.Date;
+import java.util.Map;
+import java.util.UUID;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
+import java.util.concurrent.locks.ReentrantLock;
+
+import org.apache.commons.lang3.StringUtils;
+import org.slf4j.Logger;
+
+import io.jans.lock.model.Stat;
+import io.jans.lock.model.StatEntry;
+import io.jans.lock.model.config.AppConfiguration;
+import io.jans.lock.model.config.StaticConfiguration;
+import io.jans.net.InetAddressUtility;
+import io.jans.orm.PersistenceEntryManager;
+import io.jans.orm.exception.EntryPersistenceException;
+import io.jans.orm.model.base.SimpleBranch;
+import jakarta.annotation.PostConstruct;
+import jakarta.enterprise.context.ApplicationScoped;
+import jakarta.inject.Inject;
+import net.agkn.hll.HLL;
+
+/**
+ * @author Yuriy Movchan Date: 12/02/2024
+ */
+@ApplicationScoped
+public class StatService {
+
+ private static final String RESULT_ALLOW = "allow";
+ private static final String RESULT_DENY = "deny";
+
+ // January - 202001, December - 202012
+ private static final int REGWIDTH = 5;
+ private static final int LOG_2_M = 15;
+
+ @Inject
+ private Logger log;
+
+ @Inject
+ private PersistenceEntryManager entryManager;
+
+ @Inject
+ private StaticConfiguration staticConfiguration;
+
+ @Inject
+ private AppConfiguration appConfiguration;
+
+ private String nodeId;
+ private String monthlyDn;
+ private StatEntry currentEntry;
+ private HLL userHll, clientHll;
+ private ConcurrentMap> opearationCounters;
+ private final SimpleDateFormat periodDateFormat = new SimpleDateFormat("yyyyMM");
+
+ private boolean initialized = false;
+ private final ReentrantLock setupCurrentEntryLock = new ReentrantLock();
+
+ @PostConstruct
+ public void create() {
+ initialized = false;
+ }
+
+ public boolean init() {
+ try {
+ if (!appConfiguration.isStatEnabled()) {
+ log.trace("Stat service is not enabled");
+ return false;
+ }
+ log.info("Initializing Stat Service");
+
+ final Date now = new Date();
+ initNodeId(now);
+ if (StringUtils.isBlank(nodeId)) {
+ log.error("Failed to initialize stat service. statNodeId is not set in configuration");
+ return false;
+ }
+ if (StringUtils.isBlank(getBaseDn())) {
+ log.error("Failed to initialize stat service. 'stat' base dn is not set in configuration");
+ return false;
+ }
+
+ prepareMonthlyBranch(now);
+ log.trace("Monthly branch created: {}", monthlyDn);
+
+ setupCurrentEntry(now);
+ log.info("Initialized Stat Service");
+
+ initialized = true;
+ return true;
+ } catch (Exception ex) {
+ log.error("Failed to initialize Stat Service", ex);
+ return false;
+ }
+ }
+
+ public void updateStat() {
+ if (!initialized) {
+ return;
+ }
+
+ log.trace("Started updateStat ...");
+
+ Date now = new Date();
+ prepareMonthlyBranch(now);
+
+ setupCurrentEntry(now);
+
+ final Stat stat = currentEntry.getStat();
+ stat.setOperationsByType(opearationCounters);
+ stat.setLastUpdatedAt(now.getTime());
+
+ synchronized (userHll) {
+ currentEntry.setUserHllData(Base64.getEncoder().encodeToString(userHll.toBytes()));
+ }
+ synchronized (clientHll) {
+ currentEntry.setClientHllData(Base64.getEncoder().encodeToString(clientHll.toBytes()));
+ }
+ entryManager.merge(currentEntry);
+
+ log.trace("Finished updateStat");
+ }
+
+ private void setupCurrentEntry() {
+ setupCurrentEntry(new Date());
+ }
+
+ private void setupCurrentEntry(Date now) {
+ String dn = String.format("jansId=%s,%s", nodeId, monthlyDn); // jansId=,ou=yyyyMM,ou=lock,ou=stat,o=jans
+
+ final String month = monthString(now);
+ initNodeId(now);
+
+ if (currentEntry != null && month.equals(currentEntry.getStat().getMonth())) {
+ return;
+ }
+
+ setupCurrentEntryLock.lock();
+ try {
+ // After getting lock check if another thread did initialization already
+ if (currentEntry != null && month.equals(currentEntry.getStat().getMonth())) {
+ return;
+ }
+
+ StatEntry entryFromPersistence = entryManager.find(StatEntry.class, dn);
+ if ((entryFromPersistence != null) && month.equals(entryFromPersistence.getStat().getMonth())) {
+ userHll = HLL.fromBytes(Base64.getDecoder().decode(entryFromPersistence.getUserHllData()));
+ clientHll = HLL.fromBytes(Base64.getDecoder().decode(entryFromPersistence.getClientHllData()));
+ opearationCounters = new ConcurrentHashMap<>(entryFromPersistence.getStat().getOperationsByType());
+ currentEntry = entryFromPersistence;
+ log.trace("Stat entry loaded");
+
+ if (StringUtils.isBlank(currentEntry.getMonth()) && currentEntry.getStat() != null) {
+ currentEntry.setMonth(currentEntry.getStat().getMonth());
+ }
+ return;
+ }
+ } catch (EntryPersistenceException e) {
+ log.trace("Stat entry is not found in persistence");
+ } finally {
+ setupCurrentEntryLock.unlock();
+ }
+
+ log.trace("Creating stat entry ...");
+ userHll = newUserHll();
+ clientHll = newClientHll();
+ opearationCounters = new ConcurrentHashMap<>();
+ final String monthString = periodDateFormat.format(new Date());
+
+ currentEntry = new StatEntry();
+ currentEntry.setId(nodeId);
+ currentEntry.setDn(dn);
+ currentEntry.setUserHllData(Base64.getEncoder().encodeToString(userHll.toBytes()));
+ currentEntry.setClientHllData(Base64.getEncoder().encodeToString(clientHll.toBytes()));
+
+ currentEntry.getStat().setMonth(monthString);
+ currentEntry.setMonth(monthString);
+ entryManager.persist(currentEntry);
+
+ log.trace("Created stat entry");
+ }
+
+ protected HLL newUserHll() {
+ return new HLL(LOG_2_M, REGWIDTH);
+ }
+
+ protected HLL newClientHll() {
+ return new HLL(LOG_2_M, REGWIDTH);
+ }
+
+ private void initNodeId(Date now) {
+ if (StringUtils.isNotBlank(nodeId)) {
+ return;
+ }
+
+ try {
+ nodeId = InetAddressUtility.getMACAddressOrNull() + "_" + monthString(now);
+ if (StringUtils.isNotBlank(nodeId)) {
+ return;
+ }
+
+ nodeId = UUID.randomUUID().toString() + "_" + monthString(now);
+ } catch (Exception e) {
+ log.error("Failed to identify nodeId.", e);
+ nodeId = UUID.randomUUID().toString() + "_" + monthString(now);
+ }
+ }
+
+ public String getNodeId() {
+ return nodeId;
+ }
+
+ public String monthString(Date now) {
+ return periodDateFormat.format(now); // yyyyMM
+ }
+
+ private void prepareMonthlyBranch(Date now) {
+ final String baseDn = getBaseDn();
+ final String month = monthString(now); // yyyyMM
+ monthlyDn = String.format("ou=%s,%s", month, baseDn); // ou=yyyyMM,ou=lock,ou=stat,o=jans
+
+ if (!entryManager.hasBranchesSupport(baseDn)) {
+ return;
+ }
+
+ try {
+ if (!entryManager.contains(monthlyDn, SimpleBranch.class)) { // Create ou=yyyyMM branch if needed
+ createBranch(monthlyDn, month);
+ }
+ } catch (Exception e) {
+ if (log.isErrorEnabled())
+ log.error("Failed to prepare monthly branch: " + monthlyDn, e);
+ throw e;
+ }
+ }
+
+ public String getBaseDn() {
+ return staticConfiguration.getBaseDn().getStat();
+ }
+
+ public void createBranch(String branchDn, String ou) {
+ try {
+ SimpleBranch branch = new SimpleBranch();
+ branch.setOrganizationalUnitName(ou);
+ branch.setDn(branchDn);
+
+ entryManager.persist(branch);
+ } catch (EntryPersistenceException ex) {
+ // Check if another process added this branch already
+ if (!entryManager.contains(branchDn, SimpleBranch.class)) {
+ throw ex;
+ }
+ }
+ }
+
+ public void reportActiveUser(String id) {
+ if (!initialized) {
+ return;
+ }
+
+ if (StringUtils.isBlank(id)) {
+ return;
+ }
+
+ final int hashCode = id.hashCode();
+ try {
+ setupCurrentEntry();
+ synchronized (userHll) {
+ userHll.addRaw(hashCode);
+ }
+ } catch (Exception e) {
+ log.error("Failed to report active user, id: " + id + ", hash: " + hashCode, e);
+ }
+ }
+
+ public void reportActiveClient(String id) {
+ if (!initialized) {
+ return;
+ }
+
+ if (StringUtils.isBlank(id)) {
+ return;
+ }
+
+ final int hashCode = id.hashCode();
+ try {
+ setupCurrentEntry();
+ synchronized (clientHll) {
+ clientHll.addRaw(hashCode);
+ }
+ } catch (Exception e) {
+ log.error("Failed to report active client, id: " + id + ", hash: " + hashCode, e);
+ }
+ }
+
+ public void reportAllow(String operationGroup) {
+ reportOpearation(operationGroup, RESULT_ALLOW);
+ }
+
+ public void reportDeny(String operationGroup) {
+ reportOpearation(operationGroup, RESULT_DENY);
+ }
+
+ public void reportOpearation(String operationGroup, String operationType) {
+ if (!initialized) {
+ return;
+ }
+
+ if (operationGroup == null || operationType == null) {
+ return;
+ }
+ if (opearationCounters == null) {
+ log.error("Stat service is not initialized");
+ return;
+ }
+
+ Map operationMap = opearationCounters.computeIfAbsent(operationGroup, v -> new ConcurrentHashMap<>());
+
+ Long counter = operationMap.get(operationType);
+
+ if (counter == null) {
+ counter = 1L;
+ } else {
+ counter++;
+ }
+
+ operationMap.put(operationType, counter);
+ }
+
+}
diff --git a/jans-lock/lock-server/service/src/main/java/io/jans/lock/service/stat/StatTimer.java b/jans-lock/lock-server/service/src/main/java/io/jans/lock/service/stat/StatTimer.java
new file mode 100644
index 00000000000..ba6c6165045
--- /dev/null
+++ b/jans-lock/lock-server/service/src/main/java/io/jans/lock/service/stat/StatTimer.java
@@ -0,0 +1,97 @@
+package io.jans.lock.service.stat;
+
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import org.slf4j.Logger;
+
+import io.jans.lock.model.config.AppConfiguration;
+import io.jans.lock.service.event.StatEvent;
+import io.jans.service.cdi.async.Asynchronous;
+import io.jans.service.cdi.event.Scheduled;
+import io.jans.service.timer.event.TimerEvent;
+import io.jans.service.timer.schedule.TimerSchedule;
+import jakarta.enterprise.context.ApplicationScoped;
+import jakarta.enterprise.event.Event;
+import jakarta.enterprise.event.Observes;
+import jakarta.inject.Inject;
+
+/**
+ * @author Yuriy Movchan Date: 12/02/2024
+ */
+@ApplicationScoped
+public class StatTimer {
+
+ private static final int TIMER_TICK_INTERVAL_IN_SECONDS = 60; // 1 min
+ private static final int TIMER_INTERVAL_IN_SECONDS = 15 * 60; // 15 min
+
+ @Inject
+ private Logger log;
+
+ @Inject
+ private Event timerEvent;
+
+ @Inject
+ private AppConfiguration appConfiguration;
+
+ @Inject
+ private StatService statService;
+
+ private AtomicBoolean isActive;
+ private long lastFinishedTime;
+
+ @Asynchronous
+ public void initTimer() {
+ log.info("Initializing Stat Service Timer");
+
+ this.isActive = new AtomicBoolean(false);
+
+ timerEvent.fire(new TimerEvent(new TimerSchedule(TIMER_TICK_INTERVAL_IN_SECONDS, TIMER_TICK_INTERVAL_IN_SECONDS), new StatEvent(), Scheduled.Literal.INSTANCE));
+
+ this.lastFinishedTime = System.currentTimeMillis();
+ log.info("Initialized Stat Service Timer");
+ }
+
+ @Asynchronous
+ public void process(@Observes @Scheduled StatEvent event) {
+ if (!appConfiguration.isStatEnabled()) {
+ return;
+ }
+
+ if (this.isActive.get()) {
+ return;
+ }
+
+ if (!this.isActive.compareAndSet(false, true)) {
+ return;
+ }
+
+ try {
+ if (!allowToRun()) {
+ return;
+ }
+ statService.updateStat();
+ this.lastFinishedTime = System.currentTimeMillis();
+ } catch (Exception ex) {
+ log.error("Exception happened while updating stat", ex);
+ } finally {
+ this.isActive.set(false);
+ }
+ }
+
+ private boolean allowToRun() {
+ int interval = appConfiguration.getStatTimerIntervalInSeconds();
+ if (interval < 0) {
+ log.info("Stat Timer is disabled.");
+ log.warn("Stat Timer Interval (statTimerIntervalInSeconds in server configuration) is negative which turns OFF statistic on the server. Please set it to positive value if you wish it to run.");
+ return false;
+ }
+ if (interval == 0)
+ interval = TIMER_INTERVAL_IN_SECONDS;
+
+ long timerInterval = interval * 1000L;
+
+ long timeDiff = System.currentTimeMillis() - this.lastFinishedTime;
+
+ return timeDiff >= timerInterval;
+ }
+}
diff --git a/jans-lock/lock-server/service/src/main/java/io/jans/lock/service/util/ResteasyInitializer.java b/jans-lock/lock-server/service/src/main/java/io/jans/lock/service/util/ResteasyInitializer.java
index 4755e28efc2..45b5fdf7737 100644
--- a/jans-lock/lock-server/service/src/main/java/io/jans/lock/service/util/ResteasyInitializer.java
+++ b/jans-lock/lock-server/service/src/main/java/io/jans/lock/service/util/ResteasyInitializer.java
@@ -24,31 +24,32 @@
import io.jans.lock.service.ws.rs.audit.AuditRestWebServiceImpl;
import io.jans.lock.service.ws.rs.config.ConfigRestWebServiceImpl;
import io.jans.lock.service.ws.rs.sse.SseRestWebServiceImpl;
+import io.jans.lock.service.ws.rs.stat.StatRestWebServiceImpl;
import jakarta.ws.rs.ApplicationPath;
import jakarta.ws.rs.core.Application;
-
/**
* Integration with Resteasy
*
* @author Yuriy Movchan Date: 06/06/2024
*/
@ApplicationPath("/v1")
-public class ResteasyInitializer extends Application {
+public class ResteasyInitializer extends Application {
@Override
- public Set> getClasses() {
- HashSet> classes = new HashSet>();
- classes.add(ConfigurationRestWebService.class);
+ public Set> getClasses() {
+ HashSet> classes = new HashSet>();
+ classes.add(ConfigurationRestWebService.class);
+
+ classes.add(AuditRestWebServiceImpl.class);
+ classes.add(ConfigRestWebServiceImpl.class);
+ classes.add(StatRestWebServiceImpl.class);
- classes.add(AuditRestWebServiceImpl.class);
- classes.add(ConfigRestWebServiceImpl.class);
+ classes.add(SseRestWebServiceImpl.class);
- classes.add(SseRestWebServiceImpl.class);
-
- classes.add(AuthorizationProcessingFilter.class);
+ classes.add(AuthorizationProcessingFilter.class);
- return classes;
- }
+ return classes;
+ }
}
diff --git a/jans-lock/lock-server/service/src/main/java/io/jans/lock/service/ws/rs/ConfigurationRestWebService.java b/jans-lock/lock-server/service/src/main/java/io/jans/lock/service/ws/rs/ConfigurationRestWebService.java
index 4a8b4bb4ccc..aa4bbe60348 100644
--- a/jans-lock/lock-server/service/src/main/java/io/jans/lock/service/ws/rs/ConfigurationRestWebService.java
+++ b/jans-lock/lock-server/service/src/main/java/io/jans/lock/service/ws/rs/ConfigurationRestWebService.java
@@ -19,7 +19,7 @@
import com.fasterxml.jackson.databind.node.ObjectNode;
import io.jans.lock.service.config.ConfigurationService;
-import jakarta.enterprise.context.ApplicationScoped;
+import jakarta.enterprise.context.Dependent;
import jakarta.inject.Inject;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
@@ -32,7 +32,7 @@
*
* @author Yuriy Movchan Date: 12/19/2018
*/
-@ApplicationScoped
+@Dependent
@Path("/configuration")
public class ConfigurationRestWebService {
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 dac5464b53d..21cb2db3a77 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
@@ -38,37 +38,37 @@ public interface AuditRestWebService {
@POST
@Path("/health")
@Produces({ MediaType.APPLICATION_JSON })
- @ProtectedApi(scopes = {"https://jans.io/lock-server/audit.write"})
+ @ProtectedApi(scopes = {"https://jans.io/oauth/lock/health.write"})
Response processHealthRequest(@Context HttpServletRequest request, @Context HttpServletResponse response, @Context SecurityContext sec);
@POST
@Path("/health/bulk")
@Produces({ MediaType.APPLICATION_JSON })
- @ProtectedApi(scopes = {"https://jans.io/lock-server/audit.write"})
+ @ProtectedApi(scopes = {"https://jans.io/oauth/lock/health.write"})
Response processBulkHealthRequest(@Context HttpServletRequest request, @Context HttpServletResponse response, @Context SecurityContext sec);
@POST
@Path("/log")
@Produces({ MediaType.APPLICATION_JSON })
- @ProtectedApi(scopes = {"https://jans.io/lock-server/audit.write"})
+ @ProtectedApi(scopes = {"https://jans.io/oauth/lock/log.write"})
Response processLogRequest(@Context HttpServletRequest request, @Context HttpServletResponse response, @Context SecurityContext sec);
@POST
@Path("/log/bulk")
@Produces({ MediaType.APPLICATION_JSON })
- @ProtectedApi(scopes = {"https://jans.io/lock-server/audit.write"})
+ @ProtectedApi(scopes = {"https://jans.io/oauth/lock/log.write"})
Response processBulkLogRequest(@Context HttpServletRequest request, @Context HttpServletResponse response, @Context SecurityContext sec);
@POST
@Path("/telemetry")
@Produces({ MediaType.APPLICATION_JSON })
- @ProtectedApi(scopes = {"https://jans.io/lock-server/audit.write"})
+ @ProtectedApi(scopes = {"https://jans.io/oauth/lock/telemetry.write"})
Response processTelemetryRequest(@Context HttpServletRequest request, @Context HttpServletResponse response, @Context SecurityContext sec);
@POST
@Path("/telemetry/bulk")
@Produces({ MediaType.APPLICATION_JSON })
- @ProtectedApi(scopes = {"https://jans.io/lock-server/audit.write"})
+ @ProtectedApi(scopes = {"https://jans.io/oauth/lock/telemetry.write"})
Response processBulkTelemetryRequest(@Context HttpServletRequest request, @Context HttpServletResponse response, @Context SecurityContext sec);
}
\ No newline at end of file
diff --git a/jans-lock/lock-server/service/src/main/java/io/jans/lock/service/ws/rs/audit/AuditRestWebServiceImpl.java b/jans-lock/lock-server/service/src/main/java/io/jans/lock/service/ws/rs/audit/AuditRestWebServiceImpl.java
index adf121597ef..a3fdfc3e4ba 100644
--- a/jans-lock/lock-server/service/src/main/java/io/jans/lock/service/ws/rs/audit/AuditRestWebServiceImpl.java
+++ b/jans-lock/lock-server/service/src/main/java/io/jans/lock/service/ws/rs/audit/AuditRestWebServiceImpl.java
@@ -22,6 +22,8 @@
import com.fasterxml.jackson.databind.JsonNode;
import io.jans.lock.service.audit.AuditService;
+import io.jans.lock.service.stat.StatResponseService;
+import io.jans.lock.service.stat.StatService;
import io.jans.lock.util.ServerUtil;
import jakarta.enterprise.context.Dependent;
import jakarta.inject.Inject;
@@ -32,6 +34,8 @@
import jakarta.ws.rs.core.Response.Status;
import jakarta.ws.rs.core.SecurityContext;
+import static io.jans.lock.service.audit.AuditService.*;
+
/**
* Provides interface for audit REST web services
*
@@ -41,75 +45,98 @@
@Path("/audit")
public class AuditRestWebServiceImpl implements AuditRestWebService {
- @Inject
+ private static final String LOG_PRINCIPAL_ID = "principalId";
+ private static final String LOG_CLIENT_ID = "clientId";
+ private static final String LOG_DECISION_RESULT = "decisionResult";
+ private static final String LOG_ACTION = "action";
+
+ private static final String LOG_DECISION_RESULT_ALLOW = "allow";
+ private static final String LOG_DECISION_RESULT_DENY = "deny";
+
+ @Inject
private Logger log;
@Inject
- AuditService auditService;
+ private AuditService auditService;
+
+ @Inject
+ private StatService statService;
@Override
public Response processHealthRequest(HttpServletRequest request, HttpServletResponse response,
SecurityContext sec) {
- log.info("Processing Health request - request:{}", request);
- return processAuditRequest(request, "health");
+ log.info("Processing Health request - request: {}", request);
+ return processAuditRequest(request, AUDIT_HEALTH);
}
@Override
public Response processBulkHealthRequest(HttpServletRequest request, HttpServletResponse response,
SecurityContext sec) {
- log.info("Processing Bulk Health request - request:{}", request);
- return processAuditRequest(request, "health/bulk");
+ log.info("Processing Bulk Health request - request: {}", request);
+ return processAuditRequest(request, AUDIT_HEALTH_BULK);
}
@Override
public Response processLogRequest(HttpServletRequest request, HttpServletResponse response, SecurityContext sec) {
- log.info("Processing Log request - request:{}", request);
- return processAuditRequest(request, "log");
-
+ log.info("Processing Log request - request: {}", request);
+ return processAuditRequest(request, AUDIT_LOG, true, false);
}
@Override
public Response processBulkLogRequest(HttpServletRequest request, HttpServletResponse response, SecurityContext sec) {
- log.info("Processing Bulk Log request - request:{}", request);
- return processAuditRequest(request, "log/bulk");
+ log.info("Processing Bulk Log request - request: {}", request);
+ return processAuditRequest(request, AUDIT_LOG_BULK, true, true);
}
@Override
public Response processTelemetryRequest(HttpServletRequest request, HttpServletResponse response, SecurityContext sec) {
- log.info("Processing Telemetry request - request:{}", request);
- return processAuditRequest(request, "telemetry");
+ log.info("Processing Telemetry request - request: {}", request);
+ return processAuditRequest(request, AUDIT_TELEMETRY);
}
@Override
public Response processBulkTelemetryRequest(HttpServletRequest request, HttpServletResponse response, SecurityContext sec) {
- log.info("Processing Bulk Telemetry request - request:{}", request);
- return processAuditRequest(request, "telemetry/bulk");
+ log.info("Processing Bulk Telemetry request - request: {}", request);
+ return processAuditRequest(request, AUDIT_TELEMETRY_BULK);
}
- private Response processAuditRequest(HttpServletRequest request, String requestType) {
- log.info("Processing request - request:{}, requestType:{}", request, requestType);
+ private Response processAuditRequest(HttpServletRequest request, String requestType) {
+ return processAuditRequest(request, requestType, false, false);
+ }
+
+ private Response processAuditRequest(HttpServletRequest request, String requestType, boolean reportStat, boolean bulkData) {
+ log.info("Processing request - request: {}, requestType: {}", request, requestType);
Response.ResponseBuilder builder = Response.ok();
builder.cacheControl(ServerUtil.cacheControlWithNoStoreTransformAndPrivate());
builder.header(ServerUtil.PRAGMA, ServerUtil.NO_CACHE);
JsonNode json = this.auditService.getJsonNode(request);
+
+ if (reportStat) {
+ if (bulkData) {
+ reportBulkStat(json);
+ } else {
+ reportStat(json);
+ }
+ }
+
Response response = this.auditService.post(requestType, json.toString(), ContentType.APPLICATION_JSON);
- log.debug("response:{}", response);
+ log.debug("response: {}", response);
if (response != null) {
log.debug(
- "Response for Access Token - response.getStatus():{}, response.getStatusInfo():{}, response.getEntity().getClass():{}",
+ "Response for Access Token - response.getStatus(): {}, response.getStatusInfo(): {}, response.getEntity().getClass(): {}",
response.getStatus(), response.getStatusInfo(), response.getEntity().getClass());
String entity = response.readEntity(String.class);
- log.debug(" entity:{}", entity);
+ log.debug(" entity: {}", entity);
builder.entity(entity);
if (response.getStatusInfo().equals(Status.OK)) {
- log.debug(" Status.CREATED:{}, entity:{}", Status.OK, entity);
+ log.debug(" Status.CREATED: {}, entity: {}", Status.OK, entity);
} else {
- log.error("Error while saving audit data - response.getStatusInfo():{}, entity:{}",
+ log.error("Error while saving audit data - response.getStatusInfo(): {}, entity: {}",
response.getStatusInfo(), entity);
builder.status(response.getStatusInfo());
}
@@ -118,4 +145,41 @@ private Response processAuditRequest(HttpServletRequest request, String requestT
return builder.build();
}
+ private void reportStat(JsonNode json) {
+ boolean hasClientId = json.hasNonNull(LOG_CLIENT_ID);
+ if (hasClientId) {
+ statService.reportActiveClient(json.get(LOG_CLIENT_ID).asText());
+ }
+
+ boolean hasPrincipalId = json.hasNonNull(LOG_PRINCIPAL_ID);
+ if (hasPrincipalId) {
+ statService.reportActiveUser(json.get(LOG_PRINCIPAL_ID).asText());
+ }
+
+ boolean hasВecisionResult = json.hasNonNull(LOG_DECISION_RESULT);
+ if (hasВecisionResult) {
+ String decisionResult = json.get(LOG_DECISION_RESULT).asText();
+ if (LOG_DECISION_RESULT_ALLOW.equals(decisionResult)) {
+ statService.reportAllow(LOG_DECISION_RESULT);
+ } else if (LOG_DECISION_RESULT_DENY.equals(decisionResult)) {
+ statService.reportDeny(LOG_DECISION_RESULT);
+ }
+ }
+
+ boolean hasAction = json.hasNonNull(LOG_ACTION);
+ if (hasAction) {
+ statService.reportOpearation(LOG_ACTION, json.get(LOG_ACTION).asText());
+ }
+ }
+
+ private void reportBulkStat(JsonNode json) {
+ if (!json.isArray()) {
+ log.error("Failed to calculate stat for bulk log entry: {}", json);
+ }
+
+ for (JsonNode jsonItem : json) {
+ reportStat(jsonItem);
+ }
+
+ }
}
diff --git a/jans-lock/lock-server/service/src/main/java/io/jans/lock/service/ws/rs/config/ConfigRestWebService.java b/jans-lock/lock-server/service/src/main/java/io/jans/lock/service/ws/rs/config/ConfigRestWebService.java
index c377b12fb2c..8ac8e46ffe5 100644
--- a/jans-lock/lock-server/service/src/main/java/io/jans/lock/service/ws/rs/config/ConfigRestWebService.java
+++ b/jans-lock/lock-server/service/src/main/java/io/jans/lock/service/ws/rs/config/ConfigRestWebService.java
@@ -16,6 +16,7 @@
package io.jans.lock.service.ws.rs.config;
+import io.jans.service.security.api.ProtectedApi;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.ws.rs.GET;
@@ -36,22 +37,26 @@ public interface ConfigRestWebService {
@GET
@Path("/config")
@Produces({ MediaType.APPLICATION_JSON })
+ @ProtectedApi(scopes = {"https://jans.io/lock-server/config.read"})
Response processConfigRequest(@Context HttpServletRequest request, @Context HttpServletResponse response, @Context SecurityContext sec);
@GET
@Path("/config/issuers")
@Produces({ MediaType.APPLICATION_JSON })
+ @ProtectedApi(scopes = {"https://jans.io/lock-server/issuers.read"})
Response processIssuersRequest(@Context HttpServletRequest request, @Context HttpServletResponse response,
@Context SecurityContext sec);
@GET
@Path("/config/schema")
@Produces({ MediaType.APPLICATION_JSON })
+ @ProtectedApi(scopes = {"https://jans.io/lock-server/schema.read"})
Response processSchemaRequest(@Context HttpServletRequest request, @Context HttpServletResponse response, @Context SecurityContext sec);
@GET
@Path("/config/policy")
@Produces({ MediaType.APPLICATION_JSON })
+ @ProtectedApi(scopes = {"https://jans.io/lock-server/policy.read"})
Response processPolicyRequest(@Context HttpServletRequest request, @Context HttpServletResponse response, @Context SecurityContext sec);
}
\ No newline at end of file
diff --git a/jans-lock/lock-server/service/src/main/java/io/jans/lock/service/ws/rs/sse/SseRestWebService.java b/jans-lock/lock-server/service/src/main/java/io/jans/lock/service/ws/rs/sse/SseRestWebService.java
index 01594c92d2f..b518d19af49 100644
--- a/jans-lock/lock-server/service/src/main/java/io/jans/lock/service/ws/rs/sse/SseRestWebService.java
+++ b/jans-lock/lock-server/service/src/main/java/io/jans/lock/service/ws/rs/sse/SseRestWebService.java
@@ -16,6 +16,7 @@
package io.jans.lock.service.ws.rs.sse;
+import io.jans.service.security.api.ProtectedApi;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
@@ -32,6 +33,7 @@ public interface SseRestWebService {
@GET
@Path("/sse")
@Produces(MediaType.SERVER_SENT_EVENTS)
+ @ProtectedApi(scopes = {"https://jans.io/oauth/lock/sse.read"})
public void subscribe(@Context Sse sse, @Context SseEventSink sseEventSink);
}
\ No newline at end of file
diff --git a/jans-lock/lock-server/service/src/main/java/io/jans/lock/service/ws/rs/sse/SseRestWebServiceImpl.java b/jans-lock/lock-server/service/src/main/java/io/jans/lock/service/ws/rs/sse/SseRestWebServiceImpl.java
index 14dc0ca6219..c45f6eefebe 100644
--- a/jans-lock/lock-server/service/src/main/java/io/jans/lock/service/ws/rs/sse/SseRestWebServiceImpl.java
+++ b/jans-lock/lock-server/service/src/main/java/io/jans/lock/service/ws/rs/sse/SseRestWebServiceImpl.java
@@ -40,7 +40,7 @@ public class SseRestWebServiceImpl implements SseRestWebService {
@Override
public void subscribe(@Context Sse sse, @Context SseEventSink sseEventSink) {
- log.info("Sibscribe broadcaster");
+ log.info("Subscribe broadcaster");
if (lockSseBroadcater.getSseBroadcaster() == null) {
log.info("Init broadcaster");
lockSseBroadcater.setSse(sse);
diff --git a/jans-lock/lock-server/service/src/main/java/io/jans/lock/service/ws/rs/stat/FlatStatResponse.java b/jans-lock/lock-server/service/src/main/java/io/jans/lock/service/ws/rs/stat/FlatStatResponse.java
new file mode 100644
index 00000000000..40892da5862
--- /dev/null
+++ b/jans-lock/lock-server/service/src/main/java/io/jans/lock/service/ws/rs/stat/FlatStatResponse.java
@@ -0,0 +1,37 @@
+package io.jans.lock.service.ws.rs.stat;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import org.jboss.resteasy.annotations.providers.jaxb.IgnoreMediaTypes;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * @author Yuriy Movchan Date: 12/02/2024
+ */
+@IgnoreMediaTypes("application/*+json")
+public class FlatStatResponse {
+ @JsonProperty(value = "response") // month to stat item
+ private List response = new ArrayList<>();
+
+ public FlatStatResponse() {
+ }
+
+ public FlatStatResponse(List response) {
+ this.response = response;
+ }
+
+ public List getResponse() {
+ if (response == null) response = new ArrayList<>();
+ return response;
+ }
+
+ public void setResponse(List response) {
+ this.response = response;
+ }
+
+ @Override
+ public String toString() {
+ return "FlatStatResponse [response=" + response + "]";
+ }
+}
diff --git a/jans-lock/lock-server/service/src/main/java/io/jans/lock/service/ws/rs/stat/Months.java b/jans-lock/lock-server/service/src/main/java/io/jans/lock/service/ws/rs/stat/Months.java
new file mode 100644
index 00000000000..3f1b497b376
--- /dev/null
+++ b/jans-lock/lock-server/service/src/main/java/io/jans/lock/service/ws/rs/stat/Months.java
@@ -0,0 +1,99 @@
+package io.jans.lock.service.ws.rs.stat;
+
+import org.apache.commons.lang3.StringUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.time.LocalDate;
+import java.time.format.DateTimeFormatter;
+import java.util.LinkedHashSet;
+import java.util.Set;
+
+import static java.time.temporal.TemporalAdjusters.firstDayOfMonth;
+
+/**
+ * @author Yuriy Movchan Date: 12/02/2024
+ */
+public class Months {
+
+ private static final Logger log = LoggerFactory.getLogger(Months.class);
+
+ public static final DateTimeFormatter YYYYMMDD = DateTimeFormatter.ofPattern("yyyyMMdd");
+ public static final DateTimeFormatter YYYYMM = DateTimeFormatter.ofPattern("yyyyMM");
+
+ private Months() {
+ }
+
+ public static boolean isValid(String months, String startMonth, String endMonth) {
+ boolean hasMonths = StringUtils.isNotBlank(months);
+ boolean hasRange = StringUtils.isNotBlank(startMonth) && StringUtils.isNotBlank(endMonth);
+ if (hasMonths && hasRange) { // if both are present then invalid
+ return false;
+ }
+ return hasMonths || hasRange;
+ }
+
+ public static Set getMonths(String months, String startMonth, String endMonth) {
+ if (!isValid(months, startMonth, endMonth)) {
+ return new LinkedHashSet<>();
+ }
+
+ boolean hasMonths = StringUtils.isNotBlank(months);
+ if (hasMonths) {
+ return getMonths(months);
+ }
+ return getMonths(startMonth, endMonth);
+ }
+
+ public static LocalDate parse(String month) {
+ // append first day of month -> "01"
+ return LocalDate.parse(month + "01", YYYYMMDD).with(firstDayOfMonth());
+ }
+
+ public static Set getMonths(String startMonth, String endMonth) {
+ Set monthList = new LinkedHashSet<>();
+ if (!checkMonthFormat(startMonth) || !checkMonthFormat(endMonth)) {
+ return monthList;
+ }
+
+ LocalDate start = parse(startMonth);
+ LocalDate end = parse(endMonth);
+
+ LocalDate date = start;
+
+ while (date.isBefore(end)) {
+ monthList.add(date.format(YYYYMM));
+
+ date = date.plusMonths(1).with(firstDayOfMonth());
+ }
+
+ if (!monthList.isEmpty()) { // add last month
+ monthList.add(date.format(YYYYMM));
+ }
+ return monthList;
+ }
+
+ public static boolean checkMonthFormat(String month) {
+ if (month.length() == 6) {
+ return true;
+ }
+
+ log.error("Invalid month `{}`, month must be 6 chars length in format yyyyMM, e.g. 202212", month);
+ return false;
+ }
+
+ public static Set getMonths(String months) {
+ Set monthList = new LinkedHashSet<>();
+ if (StringUtils.isBlank(months)) {
+ return monthList;
+ }
+
+ for (String m : months.split(" ")) {
+ m = m.trim();
+ if (checkMonthFormat(m)) {
+ monthList.add(m);
+ }
+ }
+ return monthList;
+ }
+}
diff --git a/jans-lock/lock-server/service/src/main/java/io/jans/lock/service/ws/rs/stat/StatResponse.java b/jans-lock/lock-server/service/src/main/java/io/jans/lock/service/ws/rs/stat/StatResponse.java
new file mode 100644
index 00000000000..df7a2b233f5
--- /dev/null
+++ b/jans-lock/lock-server/service/src/main/java/io/jans/lock/service/ws/rs/stat/StatResponse.java
@@ -0,0 +1,33 @@
+package io.jans.lock.service.ws.rs.stat;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import org.jboss.resteasy.annotations.providers.jaxb.IgnoreMediaTypes;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * @author Yuriy Movchan Date: 12/02/2024
+ */
+@IgnoreMediaTypes("application/*+json")
+@JsonIgnoreProperties(ignoreUnknown = true)
+public class StatResponse {
+
+ @JsonProperty(value = "response") // month to stat item
+ private Map response = new HashMap<>();
+
+ public Map getResponse() {
+ if (response == null) response = new HashMap<>();
+ return response;
+ }
+
+ public void setResponse(Map response) {
+ this.response = response;
+ }
+
+ @Override
+ public String toString() {
+ return "StatResponse [response=" + response + "]";
+ }
+}
\ No newline at end of file
diff --git a/jans-lock/lock-server/service/src/main/java/io/jans/lock/service/ws/rs/stat/StatResponseItem.java b/jans-lock/lock-server/service/src/main/java/io/jans/lock/service/ws/rs/stat/StatResponseItem.java
new file mode 100644
index 00000000000..d734dbf7b7a
--- /dev/null
+++ b/jans-lock/lock-server/service/src/main/java/io/jans/lock/service/ws/rs/stat/StatResponseItem.java
@@ -0,0 +1,64 @@
+package io.jans.lock.service.ws.rs.stat;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * @author Yuriy Movchan Date: 12/02/2024
+ */
+public class StatResponseItem {
+
+ @JsonProperty
+ private String month;
+
+ @JsonProperty(value = "monthly_active_users")
+ private long monthlyActiveUsers;
+
+ @JsonProperty(value = "monthly_active_clients")
+ private long monthlyActiveClients;
+
+ @JsonProperty("operations_by_type")
+ private Map> operationsByType;
+
+ public long getMonthlyActiveUsers() {
+ return monthlyActiveUsers;
+ }
+
+ public void setMonthlyActiveUsers(long monthlyActiveUsers) {
+ this.monthlyActiveUsers = monthlyActiveUsers;
+ }
+
+ public long getMonthlyActiveClients() {
+ return monthlyActiveClients;
+ }
+
+ public void setMonthlyActiveClients(long monthlyActiveClients) {
+ this.monthlyActiveClients = monthlyActiveClients;
+ }
+
+ public Map> getOperationsByType() {
+ if (operationsByType == null) operationsByType = new HashMap<>();
+ return operationsByType;
+ }
+
+ public void setOperationsByType(Map> operationsByType) {
+ this.operationsByType = operationsByType;
+ }
+
+ public String getMonth() {
+ return month;
+ }
+
+ public void setMonth(String month) {
+ this.month = month;
+ }
+
+ @Override
+ public String toString() {
+ return "StatResponseItem [month=" + month + ", monthlyActiveUsers=" + monthlyActiveUsers
+ + ", monthlyActiveClients=" + monthlyActiveClients + ", operationsByType=" + operationsByType + "]";
+ }
+}
+
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
new file mode 100644
index 00000000000..b3864974174
--- /dev/null
+++ b/jans-lock/lock-server/service/src/main/java/io/jans/lock/service/ws/rs/stat/StatRestWebService.java
@@ -0,0 +1,41 @@
+package io.jans.lock.service.ws.rs.stat;
+
+import io.jans.service.security.api.ProtectedApi;
+import jakarta.enterprise.context.Dependent;
+import jakarta.ws.rs.FormParam;
+import jakarta.ws.rs.GET;
+import jakarta.ws.rs.HeaderParam;
+import jakarta.ws.rs.POST;
+import jakarta.ws.rs.Path;
+import jakarta.ws.rs.Produces;
+import jakarta.ws.rs.QueryParam;
+import jakarta.ws.rs.core.MediaType;
+import jakarta.ws.rs.core.Response;
+
+/**
+ * Provides server with basic statistic
+ *
+ * @author Yuriy Movchan Date: 12/02/2024
+ */
+@Dependent
+@Path("/internal/stat")
+public interface StatRestWebService {
+
+ @GET
+ @ProtectedApi(scopes = {"jans_stat"})
+ @Produces(MediaType.APPLICATION_JSON)
+ public Response statGet(@HeaderParam("Authorization") String authorization,
+ @QueryParam("month") String months,
+ @QueryParam("start-month") String startMonth,
+ @QueryParam("end-month") String endMonth,
+ @QueryParam("format") String format);
+
+ @POST
+ @ProtectedApi(scopes = {"jans_stat"})
+ @Produces(MediaType.APPLICATION_JSON)
+ public Response statPost(@HeaderParam("Authorization") String authorization,
+ @FormParam("month") String months,
+ @FormParam("start-month") String startMonth,
+ @FormParam("end-month") String endMonth,
+ @FormParam("format") String format);
+}
diff --git a/jans-lock/lock-server/service/src/main/java/io/jans/lock/service/ws/rs/stat/StatRestWebServiceImpl.java b/jans-lock/lock-server/service/src/main/java/io/jans/lock/service/ws/rs/stat/StatRestWebServiceImpl.java
new file mode 100644
index 00000000000..b581f34968a
--- /dev/null
+++ b/jans-lock/lock-server/service/src/main/java/io/jans/lock/service/ws/rs/stat/StatRestWebServiceImpl.java
@@ -0,0 +1,183 @@
+package io.jans.lock.service.ws.rs.stat;
+
+import static io.jans.as.model.util.Util.escapeLog;
+
+import java.io.IOException;
+import java.io.StringWriter;
+import java.io.Writer;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Set;
+
+import org.slf4j.Logger;
+
+import io.jans.lock.model.config.AppConfiguration;
+import io.jans.lock.model.error.ErrorResponseFactory;
+import io.jans.lock.model.error.StatErrorResponseType;
+import io.jans.lock.service.stat.StatResponseService;
+import io.jans.lock.util.Constants;
+import io.jans.lock.util.ServerUtil;
+import io.prometheus.client.CollectorRegistry;
+import io.prometheus.client.Counter;
+import io.prometheus.client.exporter.common.TextFormat;
+import jakarta.enterprise.context.Dependent;
+import jakarta.inject.Inject;
+import jakarta.ws.rs.FormParam;
+import jakarta.ws.rs.HeaderParam;
+import jakarta.ws.rs.Path;
+import jakarta.ws.rs.QueryParam;
+import jakarta.ws.rs.WebApplicationException;
+import jakarta.ws.rs.core.MediaType;
+import jakarta.ws.rs.core.Response;
+
+/**
+ * Provides server with basic statistic
+ *
+ * @author Yuriy Movchan Date: 12/02/2024
+ */
+@Dependent
+@Path("/internal/stat")
+public class StatRestWebServiceImpl implements StatRestWebService {
+
+ @Inject
+ private Logger log;
+
+ @Inject
+ private StatResponseService statResponseService;
+
+ @Inject
+ private ErrorResponseFactory errorResponseFactory;
+
+ @Inject
+ private AppConfiguration appConfiguration;
+
+ @Override
+ public Response statGet(@HeaderParam("Authorization") String authorization,
+ @QueryParam("month") String months,
+ @QueryParam("start-month") String startMonth,
+ @QueryParam("end-month") String endMonth,
+ @QueryParam("format") String format) {
+ return stat(authorization, months, startMonth, endMonth, format);
+ }
+
+ @Override
+ public Response statPost(@HeaderParam("Authorization") String authorization,
+ @FormParam("month") String months,
+ @FormParam("start-month") String startMonth,
+ @FormParam("end-month") String endMonth,
+ @FormParam("format") String format) {
+ return stat(authorization, months, startMonth, endMonth, format);
+ }
+
+ public static String createOpenMetricsResponse(StatResponse statResponse) throws IOException {
+ Writer writer = new StringWriter();
+ CollectorRegistry registry = new CollectorRegistry();
+
+ final Counter usersCounter = Counter.build().name("monthly_active_users").labelNames(Constants.MONTH)
+ .help("Monthly active users").register(registry);
+
+ final Counter clientsCounter = Counter.build().name("monthly_active_clients").labelNames(Constants.MONTH)
+ .help("Monthly active clients").register(registry);
+
+ Map counterMap = new HashMap();
+ for (Map.Entry entry : statResponse.getResponse().entrySet()) {
+ final String month = entry.getKey();
+ final StatResponseItem item = entry.getValue();
+
+ usersCounter.labels(month).inc(item.getMonthlyActiveUsers());
+
+ clientsCounter.labels(month).inc(item.getMonthlyActiveClients());
+
+ for (Map.Entry> operationTypeEntry : item.getOperationsByType().entrySet()) {
+ final String operationType = operationTypeEntry.getKey();
+ final Map operationTypeMap = operationTypeEntry.getValue();
+
+ for (Map.Entry operationEntry : operationTypeMap.entrySet()) {
+ final String operation = operationEntry.getKey();
+
+ Counter operationCounter;
+ if (counterMap.containsKey(operationType)) {
+ operationCounter = counterMap.get(operationType);
+ } else {
+ operationCounter = Counter.build()
+ .name(operationType)
+ .labelNames(Constants.MONTH, "decision")
+ .help(operationType).register(registry);
+ counterMap.put(operationType, operationCounter);
+ }
+
+ operationCounter.labels(month, operation).inc(getOperationCount(operationTypeMap, operation));
+ }
+ }
+ }
+
+ TextFormat.write004(writer, registry.metricFamilySamples());
+
+ return writer.toString();
+ }
+
+ private static long getOperationCount(Map map, String key) {
+ Long v = map.get(key);
+ return v != null ? v : 0;
+ }
+
+ public Response stat(String authorization, String monthsParam, String startMonth, String endMonth, String format) {
+ if (log.isDebugEnabled()) {
+ log.debug("Attempting to request stat, month: {}, startMonth: {}, endMonth: {}, format: {}",
+ escapeLog(monthsParam), escapeLog(startMonth), escapeLog(endMonth), escapeLog(format));
+ }
+
+ if (!appConfiguration.isStatEnabled()) {
+ throw errorResponseFactory.createWebApplicationException(Response.Status.FORBIDDEN, StatErrorResponseType.ACCESS_DENIED, "Future stat is disabled on server.");
+ }
+
+ final Set months = validateMonths(monthsParam, startMonth, endMonth);
+
+ try {
+ if (log.isTraceEnabled()) {
+ log.trace("Recognized months: {}", escapeLog(months));
+ }
+ final StatResponse statResponse = statResponseService.buildResponse(months);
+
+ final String responseAsStr;
+ if ("openmetrics".equalsIgnoreCase(format)) {
+ responseAsStr = createOpenMetricsResponse(statResponse);
+ } else if ("jsonmonth".equalsIgnoreCase(format)) {
+ responseAsStr = ServerUtil.asJson(statResponse);
+ } else {
+ responseAsStr = ServerUtil.asJson(new FlatStatResponse(new ArrayList<>(statResponse.getResponse().values())));
+ }
+
+ if (log.isTraceEnabled()) {
+ log.trace("Stat: {}", responseAsStr);
+ }
+ return Response.ok().entity(responseAsStr).build();
+ } catch (WebApplicationException e) {
+ if (log.isTraceEnabled()) {
+ log.trace(e.getMessage(), e);
+ }
+ throw e;
+ } catch (Exception e) {
+ log.error(e.getMessage(), e);
+ return Response.status(Response.Status.INTERNAL_SERVER_ERROR).type(MediaType.APPLICATION_JSON_TYPE).build();
+ }
+ }
+
+ private Set validateMonths(String months, String startMonth, String endMonth) {
+ if (!Months.isValid(months, startMonth, endMonth)) {
+ throw errorResponseFactory.createWebApplicationException(Response.Status.BAD_REQUEST, StatErrorResponseType.INVALID_REQUEST, "`month` or `start-month`/`end-month` parameter(s) can't be blank and should be in format yyyyMM (e.g. 202012)");
+ }
+
+ months = ServerUtil.urlDecode(months);
+
+ Set monthList = Months.getMonths(months, startMonth, endMonth);
+
+ if (monthList.isEmpty()) {
+ throw errorResponseFactory.createWebApplicationException(Response.Status.BAD_REQUEST, StatErrorResponseType.INVALID_REQUEST, "Unable to identify months. Check `month` or `start-month`/`end-month` parameter(s). It can't be blank and should be in format yyyyMM (e.g. 202012). start-month must be before end-month");
+ }
+
+ return monthList;
+ }
+
+}
diff --git a/jans-lock/lock-server/service/src/main/java/io/jans/lock/util/Constants.java b/jans-lock/lock-server/service/src/main/java/io/jans/lock/util/Constants.java
new file mode 100644
index 00000000000..d4eac1f29cc
--- /dev/null
+++ b/jans-lock/lock-server/service/src/main/java/io/jans/lock/util/Constants.java
@@ -0,0 +1,20 @@
+/*
+ * Janssen Project software is available under the Apache License (2004). See http://www.apache.org/licenses/ for full text.
+ *
+ * Copyright (c) 2020, Janssen Project
+ */
+package io.jans.lock.util;
+
+/**
+ * Provides server with basic statistic
+ *
+ * @author Yuriy Movchan Date: 12/24/2024
+ */
+public class Constants {
+
+ private Constants() {
+ }
+
+
+ public static final String MONTH = "month";
+}
diff --git a/jans-lock/lock-server/service/src/main/java/io/jans/lock/util/ServerUtil.java b/jans-lock/lock-server/service/src/main/java/io/jans/lock/util/ServerUtil.java
index 5395e76df23..55ca7062b7b 100644
--- a/jans-lock/lock-server/service/src/main/java/io/jans/lock/util/ServerUtil.java
+++ b/jans-lock/lock-server/service/src/main/java/io/jans/lock/util/ServerUtil.java
@@ -17,7 +17,10 @@
package io.jans.lock.util;
import java.io.IOException;
+import java.io.UnsupportedEncodingException;
+import java.net.URLDecoder;
+import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -29,6 +32,7 @@
import com.fasterxml.jackson.databind.introspect.JacksonAnnotationIntrospector;
import com.fasterxml.jackson.databind.node.ObjectNode;
+import io.jans.util.Util;
import jakarta.ws.rs.core.CacheControl;
/**
@@ -93,4 +97,16 @@ public static String toPrettyJson(ObjectNode jsonObject) throws JsonProcessingEx
ObjectMapper mapper = new ObjectMapper();
return mapper.writerWithDefaultPrettyPrinter().writeValueAsString(jsonObject);
}
+
+
+ public static String urlDecode(String str) {
+ if (StringUtils.isNotBlank(str)) {
+ try {
+ return URLDecoder.decode(str, Util.UTF8);
+ } catch (UnsupportedEncodingException e) {
+ log.trace(e.getMessage(), e);
+ }
+ }
+ return str;
+ }
}