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; + } }