diff --git a/jans-bom/pom.xml b/jans-bom/pom.xml index 3cbdde941eb..8770c66020a 100644 --- a/jans-bom/pom.xml +++ b/jans-bom/pom.xml @@ -25,6 +25,8 @@ 4.5.14 6.2.12.Final + 1.78.0 + 1.0.0.Final 4.5.19-gluu.Final 4.0.3.Final @@ -685,8 +687,26 @@ org.jboss.resteasy resteasy-bom ${resteasy.version} + pom import + + + + + io.grpc + grpc-bom + ${grpc.version} + pom + import + + + + + dev.resteasy.grpc + resteasy-grpc-bom + ${grpc-bridge.version} pom + import @@ -1099,6 +1119,16 @@ maven-antrun-plugin 3.1.0 + + org.xolstice.maven.plugins + protobuf-maven-plugin + 0.6.1 + + + kr.motd.maven + os-maven-plugin + 1.7.1 + diff --git a/jans-config-api/plugins/docs/fido2-plugin-swagger.yaml b/jans-config-api/plugins/docs/fido2-plugin-swagger.yaml index fec122c8ac9..950bb8d25f2 100644 --- a/jans-config-api/plugins/docs/fido2-plugin-swagger.yaml +++ b/jans-config-api/plugins/docs/fido2-plugin-swagger.yaml @@ -574,6 +574,8 @@ components: type: boolean disableJdkLogger: type: boolean + disableExternalLoggerConfiguration: + type: boolean loggingLevel: type: string loggingLayout: diff --git a/jans-config-api/plugins/docs/lock-plugin-swagger.yaml b/jans-config-api/plugins/docs/lock-plugin-swagger.yaml index a2da80e1e37..f1695d2b831 100644 --- a/jans-config-api/plugins/docs/lock-plugin-swagger.yaml +++ b/jans-config-api/plugins/docs/lock-plugin-swagger.yaml @@ -615,106 +615,193 @@ components: properties: dn: type: string + engineStatus: + type: object + additionalProperties: + type: string + baseDn: + type: string + inum: + type: string creationDate: type: string + description: Creation date of the entry format: date-time + example: 2024-04-21T17:25:43-05:00 eventTime: type: string + description: Time when the event occurred format: date-time + example: 2024-04-21T18:25:43-05:00 service: type: string + description: Service name + example: jans-auth nodeName: type: string + description: Node name or identifier + example: "1" status: type: string - engineStatus: - type: string - baseDn: - type: string - inum: - type: string + description: Health status + example: ok + enum: + - ok + - warning + - error + description: Health audit entry LogEntry: type: object properties: dn: type: string + baseDn: + type: string + inum: + type: string creationDate: type: string + description: Creation date of the entry format: date-time + example: 2024-04-21T18:25:43-05:00 eventTime: type: string + description: Time when the event occurred format: date-time + example: 2024-04-21T18:25:43-05:00 service: type: string + description: Service name + example: jans-auth nodeName: type: string + description: Node name or identifier + example: "1" eventType: type: string - severetyLevel: + description: Type of event + example: registration + severityLevel: type: string + description: Severity level + example: warning + enum: + - info + - warning + - error + - critical action: type: string + description: Action performed + example: ACTION_NAME_3 decisionResult: type: string + description: Decision result + example: allow + enum: + - allow + - deny requestedResource: type: string - princiaplId: + description: Requested resource as JSON string + example: "{\"t1\":\"value1\",\"t2\":\"value2\"}" + principalId: type: string + description: Principal (user) identifier + example: ACC0001 clientId: type: string + description: Client identifier + example: CLI001 + jti: + type: string + description: JWT ID - unique identifier for the token + example: 550e8400-e29b-41d4-a716-446655440000 contextInformation: type: object additionalProperties: type: string - baseDn: - type: string - inum: - type: string + description: Additional context information as key-value pairs + description: Additional context information as key-value pairs + description: Log audit entry TelemetryEntry: type: object properties: dn: type: string + baseDn: + type: string + inum: + type: string creationDate: type: string + description: Creation date of the entry format: date-time + example: 2024-04-21T18:25:43-05:00 eventTime: type: string + description: Time when the event occurred format: date-time + example: 2024-04-21T18:25:43-05:00 service: type: string + description: Service name + example: jans-auth nodeName: type: string + description: Node name or identifier + example: "1" status: type: string + description: Service status + example: ok + enum: + - ok + - warning + - error lastPolicyLoadSize: type: integer - format: int32 + description: Size of the last policy load in bytes + format: int64 + example: 1024 policySuccessLoadCounter: type: integer + description: Number of successful policy loads format: int64 + example: 100 policyFailedLoadCounter: type: integer + description: Number of failed policy loads format: int64 + example: 3 lastPolicyEvaluationTimeNs: type: integer - format: int32 + description: Last policy evaluation time in nanoseconds + format: int64 + example: 100 avgPolicyEvaluationTimeNs: type: integer - format: int32 + description: Average policy evaluation time in nanoseconds + format: int64 + example: 75 + memoryUsage: + type: integer + description: Memory usage in bytes + format: int64 + example: 2097152 evaluationRequestsCount: type: integer + description: Total number of evaluation requests format: int64 + example: 100 policyStats: type: object additionalProperties: - type: string - baseDn: - type: string - inum: - type: string - memoryUsage: - type: string + type: integer + description: Additional policy statistics as key-value pairs + format: int64 + description: Additional policy statistics as key-value pairs + description: Telemetry audit entry ApiError: type: object properties: @@ -751,6 +838,8 @@ components: - config-api cedarlingConfiguration: $ref: "#/components/schemas/CedarlingConfiguration" + grpcConfiguration: + $ref: "#/components/schemas/GrpcConfiguration" statEnabled: type: boolean description: Active stat enabled @@ -773,6 +862,8 @@ components: disableJdkLogger: type: boolean description: Choose whether to disable JDK loggers + disableExternalLoggerConfiguration: + type: boolean loggingLevel: type: string description: Specify the logging level of loggers @@ -839,6 +930,31 @@ components: type: string description: External policy store URI description: Cedarling configuration + GrpcConfiguration: + type: object + properties: + serverMode: + type: string + description: gRPC server mode + enum: + - disabled + - bridge + - plain_server + - tls_server + grpcPort: + type: integer + description: Specify grpc port + format: int32 + useTls: + type: boolean + description: Use TLS for gRPC communication + tlsCertChainFilePath: + type: string + description: TLS Cert Chain File Path + tlsPrivateKeyFilePath: + type: string + description: TLS Private Key File Path + description: gRPC server configuration PolicySource: type: object properties: diff --git a/jans-core/service/src/main/java/io/jans/service/security/api/ProtectedApi.java b/jans-core/service/src/main/java/io/jans/service/security/api/ProtectedApi.java index a8f45a484fa..dcc2bbd1410 100644 --- a/jans-core/service/src/main/java/io/jans/service/security/api/ProtectedApi.java +++ b/jans-core/service/src/main/java/io/jans/service/security/api/ProtectedApi.java @@ -17,4 +17,9 @@ */ String[] scopes() default {}; + /** + * `@return` gRPC method name mapped to this endpoint (empty if not applicable) + */ + String grpcMethodName() default ""; + } diff --git a/jans-core/service/src/main/java/io/jans/service/security/protect/BaseAuthorizationProtection.java b/jans-core/service/src/main/java/io/jans/service/security/protect/BaseAuthorizationProtection.java new file mode 100644 index 00000000000..7f69fd73c03 --- /dev/null +++ b/jans-core/service/src/main/java/io/jans/service/security/protect/BaseAuthorizationProtection.java @@ -0,0 +1,14 @@ +/* + * Janssen Project software is available under the Apache License (2004). See http://www.apache.org/licenses/ for full text. + * + * Copyright (c) 2025, Janssen Project + */ + +package io.jans.service.security.protect; + +import jakarta.ws.rs.container.ResourceInfo; +import jakarta.ws.rs.core.Response; + +public interface BaseAuthorizationProtection { + Response processAuthorization(String bearerToken, ResourceInfo resourceInfo); +} diff --git a/jans-linux-setup/jans_setup/schema/jans_schema.json b/jans-linux-setup/jans_setup/schema/jans_schema.json index d52a8c7cf05..8cf308a9b2e 100644 --- a/jans-linux-setup/jans_setup/schema/jans_schema.json +++ b/jans-linux-setup/jans_setup/schema/jans_schema.json @@ -4078,10 +4078,10 @@ "x_origin": "Jans created attribute" }, { - "desc": "severetyLevel", + "desc": "severityLevel", "equality": "caseIgnoreMatch", "names": [ - "severetyLevel" + "severityLevel" ], "oid": "jansAttr", "substr": "caseIgnoreSubstringsMatch", @@ -6094,12 +6094,13 @@ "jansService", "jansNodeName", "eventType", - "severetyLevel", + "severityLevel", "actionName", "decisionResult", "requestedResource", "principalId", "clientId", + "jti", "contextInformation" ], "must": [ diff --git a/jans-linux-setup/jans_setup/setup_app/data/jetty_app_configuration.json b/jans-linux-setup/jans_setup/setup_app/data/jetty_app_configuration.json index 22d2fe3920f..02cd12b87c7 100644 --- a/jans-linux-setup/jans_setup/setup_app/data/jetty_app_configuration.json +++ b/jans-linux-setup/jans_setup/setup_app/data/jetty_app_configuration.json @@ -80,7 +80,7 @@ "ratio": 0.10 }, "jetty": { - "modules": "server,resources,http,http-forwarded,console-capture,ee9-jsp,ee9-deploy,ee9-websocket-jakarta,ee9-cdi-decorate" + "modules": "server,resources,http,http-forwarded,console-capture,ee9-jsp,ee9-deploy,ee9-websocket-jakarta,ee9-cdi-decorate,http2c" }, "installed": false, "name": "jans-lock" diff --git a/jans-linux-setup/jans_setup/setup_app/installers/jans_auth.py b/jans-linux-setup/jans_setup/setup_app/installers/jans_auth.py index 3243daff3ea..20135d89580 100644 --- a/jans-linux-setup/jans_setup/setup_app/installers/jans_auth.py +++ b/jans-linux-setup/jans_setup/setup_app/installers/jans_auth.py @@ -56,6 +56,8 @@ def __init__(self): def install(self): self.make_pairwise_calculation_salt() + if Config.install_jans_lock: + self.jetty_app_configuration[self.service_name]['jetty']['modules'] += ',http2c' self.install_jettyService(self.jetty_app_configuration[self.service_name], True) self.set_class_path([os.path.join(self.custom_lib_dir, '*')]) self.external_libs() diff --git a/jans-linux-setup/jans_setup/setup_app/installers/jans_lock.py b/jans-linux-setup/jans_setup/setup_app/installers/jans_lock.py index a50b1ea9268..2b3e51b7770 100644 --- a/jans-linux-setup/jans_setup/setup_app/installers/jans_lock.py +++ b/jans-linux-setup/jans_setup/setup_app/installers/jans_lock.py @@ -57,6 +57,7 @@ def __init__(self): def install(self): + if Config.get('install_jans_lock_as_server'): self.install_as_server() self.systemd_units.append('jans-lock') @@ -68,8 +69,6 @@ def install(self): if Config.persistence_type == 'sql' and Config.rdbm_type == 'pgsql': Config.lock_message_provider_type = 'POSTGRES' - self.apache_lock_config() - def create_client(self): _, jans_auth_config = self.dbUtils.get_jans_auth_conf_dynamic() @@ -170,31 +169,9 @@ def configure_message_conf(self): message_conf_json = self.readFile(self.message_conf_json) self.dbUtils.set_configuration('jansMessageConf', message_conf_json) - def apache_lock_config(self): - apache_config = self.readFile(base.current_app.HttpdInstaller.https_jans_fn).splitlines() - if Config.get('install_jans_lock_as_server'): - proxy_context = 'jans-lock' - proxy_port = Config.jans_lock_port - else: - proxy_port = Config.jans_auth_port - proxy_context = 'jans-auth' - - jans_lock_well_known_proxy_pass = f' ProxyPass /.well-known/lock-server-configuration http://localhost:{proxy_port}/{proxy_context}/api/v1/configuration' - jans_lock_well_known_proxy_pass += f'\n\n \n Header edit Set-Cookie ^((?!opbs|session_state).*)$ $1;HttpOnly\n ProxyPass http://localhost:{proxy_port}/{proxy_context} retry=5 connectiontimeout=60 timeout=60\n Order deny,allow\n Allow from all\n \n' - - - proyx_pass_n = 0 - for i, l in enumerate(apache_config): - if l.strip().startswith('ProxyErrorOverride') and l.strip().endswith('On'): - proyx_pass_n = i - - apache_config.insert(proyx_pass_n-1, jans_lock_well_known_proxy_pass) - self.writeFile(base.current_app.HttpdInstaller.https_jans_fn, '\n'.join(apache_config), backup=False) - def installed(self): return os.path.exists(self.jetty_service_webapps) or os.path.exists(os.path.join(base.current_app.JansAuthInstaller.custom_lib_dir, os.path.basename(self.source_files[1][0]))) - def service_post_install_tasks(self): base.current_app.ConfigApiInstaller.install_plugin('lock') diff --git a/jans-linux-setup/jans_setup/templates/apache/https_jans.conf.mako b/jans-linux-setup/jans_setup/templates/apache/https_jans.conf.mako index fc32a0402af..9f36146dbd6 100644 --- a/jans-linux-setup/jans_setup/templates/apache/https_jans.conf.mako +++ b/jans-linux-setup/jans_setup/templates/apache/https_jans.conf.mako @@ -9,6 +9,10 @@ ServerName ${hostname}:443 LogLevel warn + % if context.get('install_jans_lock') in ('true', True): + Protocols h2 http/1.1 + % endif + SSLEngine on # If this is using options file from letsencrypt, the below changes should be applied there as well # Example is: Include /etc/letsencrypt/options-ssl-apache.conf @@ -111,6 +115,44 @@ Allow from all + % if context.get('install_jans_lock') in ('true', True): + + % if context.get('install_jans_lock_as_server') in ('true', True): + <% + lock_host_port = jans_lock_port + lock_host_suffix = 'jans-lock' + %> + % else: + <% + lock_host_port = jans_auth_port + lock_host_suffix = 'jans-auth' + %> + % endif + + + # Main proxy configuration + ProxyPass http://localhost:${lock_host_port}/${lock_host_suffix}/io.jans.lock.audit.AuditService/ upgrade=h2c + ProxyPassReverse http://localhost:${lock_host_port}/${lock_host_suffix}/io.jans.lock.audit.AuditService/ + + # Required headers for gRPC + RequestHeader set Content-Type "application/grpc" + + # Disable buffering and keep-alive for streaming + SetEnv proxy-nokeepalive 1 + SetEnv proxy-initial-not-pooled 1 + + # Disable Request/Response body processing + SetEnv proxy-sendcl 1 + + # Access control + Require all granted + + # Allow HTTP/2 methods + Require method GET POST + + + % endif + ProxyPass /.well-known/openid-configuration http://localhost:${jans_auth_port}/jans-auth/.well-known/openid-configuration ProxyPass /.well-known/webfinger http://localhost:${jans_auth_port}/jans-auth/.well-known/webfinger ProxyPass /.well-known/uma2-configuration http://localhost:${jans_auth_port}/jans-auth/restv1/uma2-configuration @@ -121,6 +163,16 @@ ProxyPass /firebase-messaging-sw.js http://localhost:${jans_auth_port}/jans-auth/firebase-messaging-sw.js ProxyPass /device-code http://localhost:${jans_auth_port}/jans-auth/device_authorization.htm + % if context.get('install_jans_lock') in ('true', True): + ProxyPass /.well-known/lock-server-configuration http://localhost:${lock_host_port}/${lock_host_suffix}/api/v1/configuration' + + Header edit Set-Cookie ^((?!opbs|session_state).*)$ $1;HttpOnly + ProxyPass http://localhost:${lock_host_port}/${lock_host_suffix} retry=5 connectiontimeout=60 timeout=60 + Order deny,allow + Allow from all + + % endif + ProxyErrorOverride On diff --git a/jans-lock/lock-server.yaml b/jans-lock/lock-server.yaml index 9089c193712..397cd902987 100644 --- a/jans-lock/lock-server.yaml +++ b/jans-lock/lock-server.yaml @@ -512,106 +512,193 @@ components: properties: dn: type: string + engineStatus: + type: object + additionalProperties: + type: string + baseDn: + type: string + inum: + type: string creationDate: type: string + description: Creation date of the entry format: date-time + example: 2024-04-21T17:25:43-05:00 eventTime: type: string + description: Time when the event occurred format: date-time + example: 2024-04-21T18:25:43-05:00 service: type: string + description: Service name + example: jans-auth nodeName: type: string + description: Node name or identifier + example: "1" status: type: string - engineStatus: - type: string - baseDn: - type: string - inum: - type: string + description: Health status + example: ok + enum: + - ok + - warning + - error + description: Health audit entry LogEntry: type: object properties: dn: type: string + baseDn: + type: string + inum: + type: string creationDate: type: string + description: Creation date of the entry format: date-time + example: 2024-04-21T18:25:43-05:00 eventTime: type: string + description: Time when the event occurred format: date-time + example: 2024-04-21T18:25:43-05:00 service: type: string + description: Service name + example: jans-auth nodeName: type: string + description: Node name or identifier + example: "1" eventType: type: string - severetyLevel: - type: string + description: Type of event + example: registration + severityLevel: + type: string + description: Severity level + example: warning + enum: + - info + - warning + - error + - critical action: type: string + description: Action performed + example: ACTION_NAME_3 decisionResult: type: string + description: Decision result + example: allow + enum: + - allow + - deny requestedResource: type: string - princiaplId: + description: Requested resource as JSON string + example: "{\"t1\":\"value1\",\"t2\":\"value2\"}" + principalId: type: string + description: Principal (user) identifier + example: ACC0001 clientId: type: string + description: Client identifier + example: CLI001 + jti: + type: string + description: JWT ID - unique identifier for the token + example: 550e8400-e29b-41d4-a716-446655440000 contextInformation: type: object additionalProperties: type: string - baseDn: - type: string - inum: - type: string + description: Additional context information as key-value pairs + description: Additional context information as key-value pairs + description: Log audit entry TelemetryEntry: type: object properties: dn: type: string + baseDn: + type: string + inum: + type: string creationDate: type: string + description: Creation date of the entry format: date-time + example: 2024-04-21T18:25:43-05:00 eventTime: type: string + description: Time when the event occurred format: date-time + example: 2024-04-21T18:25:43-05:00 service: type: string + description: Service name + example: jans-auth nodeName: type: string + description: Node name or identifier + example: "1" status: type: string + description: Service status + example: ok + enum: + - ok + - warning + - error lastPolicyLoadSize: type: integer - format: int32 + description: Size of the last policy load in bytes + format: int64 + example: 1024 policySuccessLoadCounter: type: integer + description: Number of successful policy loads format: int64 + example: 100 policyFailedLoadCounter: type: integer + description: Number of failed policy loads format: int64 + example: 3 lastPolicyEvaluationTimeNs: type: integer - format: int32 + description: Last policy evaluation time in nanoseconds + format: int64 + example: 100 avgPolicyEvaluationTimeNs: type: integer - format: int32 + description: Average policy evaluation time in nanoseconds + format: int64 + example: 75 + memoryUsage: + type: integer + description: Memory usage in bytes + format: int64 + example: 2097152 evaluationRequestsCount: type: integer + description: Total number of evaluation requests format: int64 + example: 100 policyStats: type: object additionalProperties: - type: string - baseDn: - type: string - inum: - type: string - memoryUsage: - type: string + type: integer + description: Additional policy statistics as key-value pairs + format: int64 + description: Additional policy statistics as key-value pairs + description: Telemetry audit entry FlatStatResponse: type: object properties: diff --git a/jans-lock/lock-server/cedarling/src/main/java/io/jans/lock/cedarling/config/BootstrapConfig.java b/jans-lock/lock-server/cedarling/src/main/java/io/jans/lock/cedarling/config/BootstrapConfig.java index 67cdb85a02f..b58537c9184 100644 --- a/jans-lock/lock-server/cedarling/src/main/java/io/jans/lock/cedarling/config/BootstrapConfig.java +++ b/jans-lock/lock-server/cedarling/src/main/java/io/jans/lock/cedarling/config/BootstrapConfig.java @@ -1,17 +1,7 @@ /* - * Copyright [2025] [Janssen Project] + * Janssen Project software is available under the Apache License (2004). See http://www.apache.org/licenses/ for full text. * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * Copyright (c) 2025, Janssen Project */ package io.jans.lock.cedarling.config; diff --git a/jans-lock/lock-server/cedarling/src/main/java/io/jans/lock/cedarling/service/CedarlingAuthorizationService.java b/jans-lock/lock-server/cedarling/src/main/java/io/jans/lock/cedarling/service/CedarlingAuthorizationService.java index 3a94d32871e..3ef70477131 100644 --- a/jans-lock/lock-server/cedarling/src/main/java/io/jans/lock/cedarling/service/CedarlingAuthorizationService.java +++ b/jans-lock/lock-server/cedarling/src/main/java/io/jans/lock/cedarling/service/CedarlingAuthorizationService.java @@ -1,18 +1,7 @@ - /* - * Copyright [2025] [Janssen Project] - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 + * Janssen Project software is available under the Apache License (2004). See http://www.apache.org/licenses/ for full text. * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * Copyright (c) 2025, Janssen Project */ package io.jans.lock.cedarling.service; diff --git a/jans-lock/lock-server/cedarling/src/main/java/io/jans/lock/cedarling/service/CedarlingProtection.java b/jans-lock/lock-server/cedarling/src/main/java/io/jans/lock/cedarling/service/CedarlingProtection.java new file mode 100644 index 00000000000..13e0d9eb29f --- /dev/null +++ b/jans-lock/lock-server/cedarling/src/main/java/io/jans/lock/cedarling/service/CedarlingProtection.java @@ -0,0 +1,21 @@ +/* + * Janssen Project software is available under the Apache License (2004). See http://www.apache.org/licenses/ for full text. + * + * Copyright (c) 2025, Janssen Project + */ + +package io.jans.lock.cedarling.service; + +import io.jans.service.security.protect.BaseAuthorizationProtection; +import jakarta.ws.rs.container.ResourceInfo; +import jakarta.ws.rs.core.Response; + +public interface CedarlingProtection extends BaseAuthorizationProtection { + + Response processAuthorization(String bearerToken, ResourceInfo resourceInfo); + + public static Response simpleResponse(Response.Status status, String detail) { + return Response.status(status).entity(detail).build(); + } + +} \ No newline at end of file diff --git a/jans-lock/lock-server/cedarling/src/main/java/io/jans/lock/cedarling/service/filter/CedarlingProtectionService.java b/jans-lock/lock-server/cedarling/src/main/java/io/jans/lock/cedarling/service/CedarlingProtectionService.java similarity index 92% rename from jans-lock/lock-server/cedarling/src/main/java/io/jans/lock/cedarling/service/filter/CedarlingProtectionService.java rename to jans-lock/lock-server/cedarling/src/main/java/io/jans/lock/cedarling/service/CedarlingProtectionService.java index 9453bea3473..ad4372a11e7 100644 --- a/jans-lock/lock-server/cedarling/src/main/java/io/jans/lock/cedarling/service/filter/CedarlingProtectionService.java +++ b/jans-lock/lock-server/cedarling/src/main/java/io/jans/lock/cedarling/service/CedarlingProtectionService.java @@ -1,4 +1,4 @@ -package io.jans.lock.cedarling.service.filter; +package io.jans.lock.cedarling.service; import static jakarta.ws.rs.core.Response.Status.FORBIDDEN; import static jakarta.ws.rs.core.Response.Status.INTERNAL_SERVER_ERROR; @@ -31,7 +31,6 @@ import io.jans.as.model.jwt.JwtClaimName; import io.jans.as.model.jwt.JwtClaims; import io.jans.lock.cedarling.model.CedarlingPermission; -import io.jans.lock.cedarling.service.CedarlingAuthorizationService; import io.jans.lock.cedarling.service.security.api.ProtectedCedarlingApi; import io.jans.lock.model.config.AppConfiguration; import io.jans.util.StringHelper; @@ -40,9 +39,7 @@ import jakarta.annotation.PostConstruct; import jakarta.enterprise.context.ApplicationScoped; import jakarta.inject.Inject; -import jakarta.ws.rs.container.ContainerRequestContext; import jakarta.ws.rs.container.ResourceInfo; -import jakarta.ws.rs.core.HttpHeaders; import jakarta.ws.rs.core.Response; @ApplicationScoped @@ -71,10 +68,9 @@ private void init() { } } - public Response processAuthorization(ContainerRequestContext requestContext, HttpHeaders headers, ResourceInfo resourceInfo) { + public Response processAuthorization(String bearerToken, ResourceInfo resourceInfo) { try { - String token = headers.getHeaderString(HttpHeaders.AUTHORIZATION); - boolean authFound = StringUtils.isNotEmpty(token); + boolean authFound = StringUtils.isNotEmpty(bearerToken); log.info("Authorization header {} found", authFound ? "" : "not"); if (!authFound) { @@ -83,8 +79,8 @@ public Response processAuthorization(ContainerRequestContext requestContext, Htt return simpleResponse(UNAUTHORIZED, "No authorization header found"); } - token = token.replaceFirst("Bearer\\s+",""); - log.debug("Validating token {}", token); + bearerToken = bearerToken.replaceFirst("Bearer\\s+",""); + log.debug("Validating token {}", bearerToken); List requestedPermissions = getRequestedOperations(resourceInfo); log.info("Check access to requested opearations: {}", requestedPermissions); @@ -92,7 +88,7 @@ public Response processAuthorization(ContainerRequestContext requestContext, Htt return simpleResponse(INTERNAL_SERVER_ERROR, "Access to operation is not correct"); } - Jwt jwt = tokenAsJwt(token); + Jwt jwt = tokenAsJwt(bearerToken); if (jwt == null) { return simpleResponse(FORBIDDEN, "Provided token isn't JWT encoded"); } @@ -124,7 +120,7 @@ public Response processAuthorization(ContainerRequestContext requestContext, Htt if (valid) { boolean authorized = true; - Map tokens = getCedarlingTokens(token); + Map tokens = getCedarlingTokens(bearerToken); for (CedarlingPermission requestedPermission : requestedPermissions) { authorized &= authorizationService.authorize(tokens, requestedPermission.getAction(), getCedarlingResource(requestedPermission), getCedarlingContext()); @@ -171,7 +167,7 @@ private Map getCedarlingResource(CedarlingPermission requestedPe id = id > 0 ? id : -id; map.putAll( Map.of("cedar_entity_mapping", - Map.of("entity_type", requestedPermission.getResource(), "id", requestedPermission.getId()) + Map.of("entity_type", requestedPermission.getResource(), "id", id) ) ); map.putAll( diff --git a/jans-lock/lock-server/cedarling/src/main/java/io/jans/lock/cedarling/service/filter/CedarlingAuthorizationProcessingFilter.java b/jans-lock/lock-server/cedarling/src/main/java/io/jans/lock/cedarling/service/filter/CedarlingAuthorizationProcessingFilter.java index 044e08d7a62..5a10d19ba0f 100644 --- a/jans-lock/lock-server/cedarling/src/main/java/io/jans/lock/cedarling/service/filter/CedarlingAuthorizationProcessingFilter.java +++ b/jans-lock/lock-server/cedarling/src/main/java/io/jans/lock/cedarling/service/filter/CedarlingAuthorizationProcessingFilter.java @@ -2,8 +2,10 @@ import java.io.IOException; +import org.apache.commons.lang3.StringUtils; import org.slf4j.Logger; +import io.jans.lock.cedarling.service.CedarlingProtection; import io.jans.lock.cedarling.service.app.audit.ApplicationCedarlingAuditLogger; import io.jans.lock.cedarling.service.security.api.ProtectedCedarlingApi; import io.jans.lock.model.app.audit.AuditActionType; @@ -70,7 +72,7 @@ public void filter(ContainerRequestContext requestContext) throws IOException { log.debug("REST call to '{}' intercepted", path); if (LockProtectionMode.CEDARLING.equals(appConfiguration.getProtectionMode())) { - Response authorizationResponse = protectionService.processAuthorization(requestContext, httpHeaders, resourceInfo); + Response authorizationResponse = protectionService.processAuthorization(extractBearerToken(), resourceInfo); boolean success = authorizationResponse == null; AuditLogEntry auditLogEntry = new AuditLogEntry(InetAddressUtility.getIpAddress(httpRequest), AuditActionType.CEDARLING_AUTHZ_FILTER); @@ -85,6 +87,16 @@ public void filter(ContainerRequestContext requestContext) throws IOException { } } + private String extractBearerToken() { + String authHeader = httpHeaders.getHeaderString(HttpHeaders.AUTHORIZATION); + + if (StringUtils.isEmpty(authHeader)) { + return null; + } + + return authHeader.replaceFirst("(?i)Bearer\\s+", ""); + } + private Response unprotectedApiResponse(String name) { return Response.status(Response.Status.UNAUTHORIZED).entity(name + " API not protected") .build(); diff --git a/jans-lock/lock-server/cedarling/src/main/java/io/jans/lock/cedarling/service/filter/CedarlingProtection.java b/jans-lock/lock-server/cedarling/src/main/java/io/jans/lock/cedarling/service/filter/CedarlingProtection.java deleted file mode 100644 index d48d2f84bc2..00000000000 --- a/jans-lock/lock-server/cedarling/src/main/java/io/jans/lock/cedarling/service/filter/CedarlingProtection.java +++ /dev/null @@ -1,16 +0,0 @@ -package io.jans.lock.cedarling.service.filter; - -import jakarta.ws.rs.container.ContainerRequestContext; -import jakarta.ws.rs.container.ResourceInfo; -import jakarta.ws.rs.core.HttpHeaders; -import jakarta.ws.rs.core.Response; - -public interface CedarlingProtection { - - Response processAuthorization(ContainerRequestContext requestContext, HttpHeaders httpHeaders, ResourceInfo resourceInfo); - - public static Response simpleResponse(Response.Status status, String detail) { - return Response.status(status).entity(detail).build(); - } - -} \ No newline at end of file diff --git a/jans-lock/lock-server/cedarling/src/main/java/io/jans/lock/cedarling/service/inject/CedarlingPolicy.java b/jans-lock/lock-server/cedarling/src/main/java/io/jans/lock/cedarling/service/inject/CedarlingPolicy.java index 7ca9af8516a..eabf45aee67 100644 --- a/jans-lock/lock-server/cedarling/src/main/java/io/jans/lock/cedarling/service/inject/CedarlingPolicy.java +++ b/jans-lock/lock-server/cedarling/src/main/java/io/jans/lock/cedarling/service/inject/CedarlingPolicy.java @@ -1,17 +1,7 @@ /* - * Copyright [2025] [Janssen Project] + * Janssen Project software is available under the Apache License (2004). See http://www.apache.org/licenses/ for full text. * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * Copyright (c) 2025, Janssen Project */ package io.jans.lock.cedarling.service.inject; @@ -27,7 +17,9 @@ import jakarta.enterprise.util.AnnotationLiteral; /** - * + * Qualifier annotation for CDI injection of Cedarling policy components. + * Apply to injection points that require policy-specific bean selection. + * * @author Yuriy Movchan Date: 10/08/2022 */ @Target({ METHOD, FIELD }) diff --git a/jans-lock/lock-server/model/src/main/java/io/jans/lock/model/app/audit/AuditActionType.java b/jans-lock/lock-server/model/src/main/java/io/jans/lock/model/app/audit/AuditActionType.java index da582f0704b..dc896b17d20 100644 --- a/jans-lock/lock-server/model/src/main/java/io/jans/lock/model/app/audit/AuditActionType.java +++ b/jans-lock/lock-server/model/src/main/java/io/jans/lock/model/app/audit/AuditActionType.java @@ -10,20 +10,21 @@ * @version November 03, 2025 */ public enum AuditActionType { - CEDARLING_AUTHZ_FILTER("CEDARLING_AUTHZ_FILTER"), - OPENID_AUTHZ_FILTER("OPENID_AUTHZ_FILTER"), + CEDARLING_AUTHZ_FILTER("CEDARLING_AUTHZ_FILTER"), + OPENID_AUTHZ_FILTER("OPENID_AUTHZ_FILTER"), + GRPC_AUTHZ_FILTER("GRPC_AUTHZ_FILTER"), - AUDIT_HEALTH_WRITE("AUDIT_HEALTH_WRITE"), - AUDIT_HEALTH_BULK_WRITE("AUDIT_HEALTH_BULK_WRITE"), - AUDIT_LOG_WRITE("AUDIT_LOG_WRITE"), - AUDIT_LOG_BULK_WRITE("AUDIT_LOG_BULK_WRITE"), - AUDIT_TELEMETRY_WRITE("AUDIT_TELEMETRY_WRITE"), - AUDIT_TELEMETRY_BULK_WRITE("AUDIT_TELEMETRY_BULK_WRITE"), + AUDIT_HEALTH_WRITE("AUDIT_HEALTH_WRITE"), + AUDIT_HEALTH_BULK_WRITE("AUDIT_HEALTH_BULK_WRITE"), + AUDIT_LOG_WRITE("AUDIT_LOG_WRITE"), + AUDIT_LOG_BULK_WRITE("AUDIT_LOG_BULK_WRITE"), + AUDIT_TELEMETRY_WRITE("AUDIT_TELEMETRY_WRITE"), + AUDIT_TELEMETRY_BULK_WRITE("AUDIT_TELEMETRY_BULK_WRITE"), - POLICIES_URI_LIST_READ("POLICIES_URI_LIST_READ"), - POLICY_BY_URI_READ("POLICY_BY_URI_READ"), + POLICIES_URI_LIST_READ("POLICIES_URI_LIST_READ"), + POLICY_BY_URI_READ("POLICY_BY_URI_READ"), - CONFIGURATION_READ("CONFIGURATION_READ"), + CONFIGURATION_READ("CONFIGURATION_READ"), SSA_READ("SSA_READ") ; diff --git a/jans-lock/lock-server/model/src/main/java/io/jans/lock/model/audit/HealthEntry.java b/jans-lock/lock-server/model/src/main/java/io/jans/lock/model/audit/HealthEntry.java index 14760579ec8..39e9b0bbf4c 100644 --- a/jans-lock/lock-server/model/src/main/java/io/jans/lock/model/audit/HealthEntry.java +++ b/jans-lock/lock-server/model/src/main/java/io/jans/lock/model/audit/HealthEntry.java @@ -1,8 +1,18 @@ +/* + * Janssen Project software is available under the Apache License (2004). See http://www.apache.org/licenses/ for full text. + * + * Copyright (c) 2025, Janssen Project + */ + package io.jans.lock.model.audit; import java.io.Serializable; import java.util.Date; +import java.util.Map; +import java.util.Objects; +import com.fasterxml.jackson.annotation.JsonFormat; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; import io.jans.orm.annotation.AttributeName; @@ -10,7 +20,16 @@ import io.jans.orm.annotation.JsonObject; import io.jans.orm.annotation.ObjectClass; import io.jans.orm.model.base.BaseEntry; - +import io.swagger.v3.oas.annotations.media.Schema; + +/** + * Health Entry for audit logging. Represents health status information of a + * service node. + * + * @author Yuriy Movchan + */ +@Schema(description = "Health audit entry") +@JsonIgnoreProperties(ignoreUnknown = true) @DataEntry(sortByName = "eventTime") @ObjectClass(value = "jansHealthEntry") public class HealthEntry extends BaseEntry implements Serializable { @@ -21,25 +40,37 @@ public class HealthEntry extends BaseEntry implements Serializable { @AttributeName(name = "inum", ignoreDuringUpdate = true) private String inum; + @JsonProperty("creationDate") + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd'T'HH:mm:ssXXX") + @Schema(description = "Creation date of the entry", example = "2024-04-21T17:25:43-05:00") @AttributeName(name = "creationDate") private Date creationDate; - @AttributeName(name = "eventTime") - private Date eventTime; + @JsonProperty("eventTime") + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd'T'HH:mm:ssXXX") + @Schema(description = "Time when the event occurred", example = "2024-04-21T18:25:43-05:00") + @AttributeName(name = "eventTime") + private Date eventTime; + @JsonProperty("service") + @Schema(description = "Service name", example = "jans-auth") @AttributeName(name = "jansService") private String service; + @JsonProperty("nodeName") + @Schema(description = "Node name or identifier", example = "1") @AttributeName(name = "jansNodeName") private String nodeName; + @JsonProperty("status") + @Schema(description = "Health status", example = "ok", allowableValues = { "ok", "warning", "error" }) @AttributeName(name = "jansStatus") private String status; // Details: cedarEngineStatus, cedarPolicyStatus, tokenDataStatus. etc.. @JsonObject @AttributeName(name = "engineStatus") - private String engineStatus; + private Map engineStatus; public String getInum() { return inum; @@ -89,19 +120,37 @@ public void setStatus(String status) { this.status = status; } - public String getEngineStatus() { + public Map getEngineStatus() { return engineStatus; } - public void setEngineStatus(String engineStatus) { + public void setEngineStatus(Map engineStatus) { this.engineStatus = engineStatus; } + @Override + public boolean equals(Object o) { + if (this == o) + return true; + if (o == null || getClass() != o.getClass()) + return false; + HealthEntry that = (HealthEntry) o; + return Objects.equals(creationDate, that.creationDate) && Objects.equals(eventTime, that.eventTime) + && Objects.equals(service, that.service) && Objects.equals(nodeName, that.nodeName) + && Objects.equals(status, that.status) && Objects.equals(engineStatus, that.engineStatus); + + } + + @Override + public int hashCode() { + return Objects.hash(creationDate, eventTime, service, nodeName, status, engineStatus); + } + @Override public String toString() { - return "HealthEntry [inum=" + inum + ", creationDate=" + creationDate + ", eventTime=" + eventTime - + ", service=" + service + ", nodeName=" + nodeName + ", status=" + status + ", engineStatus=" - + engineStatus + ", toString()=" + super.toString() + "]"; + return "HealthEntry{" + "creationDate='" + creationDate + '\'' + ", eventTime='" + eventTime + '\'' + + ", service='" + service + '\'' + ", nodeName='" + nodeName + '\'' + ", status='" + status + '\'' + + ", engineStatus='" + engineStatus + '\'' + '}'; } } diff --git a/jans-lock/lock-server/model/src/main/java/io/jans/lock/model/audit/LogEntry.java b/jans-lock/lock-server/model/src/main/java/io/jans/lock/model/audit/LogEntry.java index b4d459469b5..f8608de7bd0 100644 --- a/jans-lock/lock-server/model/src/main/java/io/jans/lock/model/audit/LogEntry.java +++ b/jans-lock/lock-server/model/src/main/java/io/jans/lock/model/audit/LogEntry.java @@ -1,9 +1,18 @@ +/* + * Janssen Project software is available under the Apache License (2004). See http://www.apache.org/licenses/ for full text. + * + * Copyright (c) 2025, Janssen Project + */ + package io.jans.lock.model.audit; import java.io.Serializable; import java.util.Date; import java.util.Map; +import java.util.Objects; +import com.fasterxml.jackson.annotation.JsonFormat; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; import io.jans.orm.annotation.AttributeName; @@ -11,7 +20,10 @@ import io.jans.orm.annotation.JsonObject; import io.jans.orm.annotation.ObjectClass; import io.jans.orm.model.base.BaseEntry; +import io.swagger.v3.oas.annotations.media.Schema; +@Schema(description = "Log audit entry") +@JsonIgnoreProperties(ignoreUnknown = true) @DataEntry(sortByName = "eventTime") @ObjectClass(value = "jansLogEntry") public class LogEntry extends BaseEntry implements Serializable { @@ -22,39 +34,70 @@ public class LogEntry extends BaseEntry implements Serializable { @AttributeName(name = "inum", ignoreDuringUpdate = true) private String inum; + @JsonProperty("creationDate") + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd'T'HH:mm:ssXXX") + @Schema(description = "Creation date of the entry", example = "2024-04-21T18:25:43-05:00") @AttributeName(name = "creationDate") private Date creationDate; + @JsonProperty("eventTime") + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd'T'HH:mm:ssXXX") + @Schema(description = "Time when the event occurred", example = "2024-04-21T18:25:43-05:00") @AttributeName(name = "eventTime") private Date eventTime; + @JsonProperty("service") + @Schema(description = "Service name", example = "jans-auth") @AttributeName(name = "jansService") private String service; + @JsonProperty("nodeName") + @Schema(description = "Node name or identifier", example = "1") @AttributeName(name = "jansNodeName") private String nodeName; + @JsonProperty("eventType") + @Schema(description = "Type of event", example = "registration") @AttributeName(name = "eventType") private String eventType; - @AttributeName(name = "severetyLevel") - private String severetyLevel; + @JsonProperty("severityLevel") + @Schema(description = "Severity level", example = "warning", allowableValues = {"info", "warning", "error", "critical"}) + @AttributeName(name = "severityLevel") + private String severityLevel; + @JsonProperty("action") + @Schema(description = "Action performed", example = "ACTION_NAME_3") @AttributeName(name = "actionName") private String action; + @JsonProperty("decisionResult") + @Schema(description = "Decision result", example = "allow", allowableValues = {"allow", "deny"}) @AttributeName(name = "decisionResult") private String decisionResult; + @JsonProperty("requestedResource") + @Schema(description = "Requested resource as JSON string", example = "{\"t1\":\"value1\",\"t2\":\"value2\"}") @AttributeName(name = "requestedResource") private String requestedResource; + @JsonProperty("principalId") + @Schema(description = "Principal (user) identifier", example = "ACC0001") @AttributeName(name = "principalId") - private String princiaplId; + private String principalId; + @JsonProperty("clientId") + @Schema(description = "Client identifier", example = "CLI001") @AttributeName(name = "clientId") private String clientId; + @JsonProperty("jti") + @Schema(description = "JWT ID - unique identifier for the token", example = "550e8400-e29b-41d4-a716-446655440000") + @AttributeName(name = "jti") + private String jti; + + @JsonProperty("contextInformation") + @Schema(description = "Additional context information as key-value pairs") @JsonObject @AttributeName(name = "contextInformation") private Map contextInformation; @@ -107,12 +150,12 @@ public void setEventType(String eventType) { this.eventType = eventType; } - public String getSeveretyLevel() { - return severetyLevel; + public String getSeverityLevel() { + return severityLevel; } - public void setSeveretyLevel(String severetyLevel) { - this.severetyLevel = severetyLevel; + public void setSeverityLevel(String severityLevel) { + this.severityLevel = severityLevel; } public String getAction() { @@ -139,12 +182,12 @@ public void setRequestedResource(String requestedResource) { this.requestedResource = requestedResource; } - public String getPrinciaplId() { - return princiaplId; + public String getPrincipalId() { + return principalId; } - public void setPrinciaplId(String princiaplId) { - this.princiaplId = princiaplId; + public void setPrincipalId(String principalId) { + this.principalId = principalId; } public String getClientId() { @@ -155,6 +198,14 @@ public void setClientId(String clientId) { this.clientId = clientId; } + public String getJti() { + return jti; + } + + public void setJti(String jti) { + this.jti = jti; + } + public Map getContextInformation() { return contextInformation; } @@ -163,13 +214,49 @@ public void setContextInformation(Map contextInformation) { this.contextInformation = contextInformation; } - @Override - public String toString() { - return "LogEntry [inum=" + inum + ", creationDate=" + creationDate + ", eventTime=" + eventTime + ", service=" - + service + ", nodeName=" + nodeName + ", eventType=" + eventType + ", severetyLevel=" + severetyLevel - + ", action=" + action + ", decisionResult=" + decisionResult + ", requestedResource=" - + requestedResource + ", princiaplId=" + princiaplId + ", clientId=" + clientId - + ", contextInformation=" + contextInformation + ", toString()=" + super.toString() + "]"; - } - + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + LogEntry logEntry = (LogEntry) o; + return Objects.equals(creationDate, logEntry.creationDate) && + Objects.equals(eventTime, logEntry.eventTime) && + Objects.equals(service, logEntry.service) && + Objects.equals(nodeName, logEntry.nodeName) && + Objects.equals(eventType, logEntry.eventType) && + Objects.equals(severityLevel, logEntry.severityLevel) && + Objects.equals(action, logEntry.action) && + Objects.equals(decisionResult, logEntry.decisionResult) && + Objects.equals(requestedResource, logEntry.requestedResource) && + Objects.equals(principalId, logEntry.principalId) && + Objects.equals(clientId, logEntry.clientId) && + Objects.equals(jti, logEntry.jti) && + Objects.equals(contextInformation, logEntry.contextInformation); + } + + @Override + public int hashCode() { + return Objects.hash(creationDate, eventTime, service, nodeName, eventType, + severityLevel, action, decisionResult, requestedResource, + principalId, clientId, jti, contextInformation); + } + + @Override + public String toString() { + return "LogEntry{" + + "creationDate='" + creationDate + '\'' + + ", eventTime='" + eventTime + '\'' + + ", service='" + service + '\'' + + ", nodeName='" + nodeName + '\'' + + ", eventType='" + eventType + '\'' + + ", severityLevel='" + severityLevel + '\'' + + ", action='" + action + '\'' + + ", decisionResult='" + decisionResult + '\'' + + ", requestedResource='" + requestedResource + '\'' + + ", principalId='" + principalId + '\'' + + ", clientId='" + clientId + '\'' + + ", jti='" + jti + '\'' + + ", contextInformation=" + contextInformation + + '}'; + } } diff --git a/jans-lock/lock-server/model/src/main/java/io/jans/lock/model/audit/TelemetryEntry.java b/jans-lock/lock-server/model/src/main/java/io/jans/lock/model/audit/TelemetryEntry.java index 4c2f2d643df..7cb03da76e3 100644 --- a/jans-lock/lock-server/model/src/main/java/io/jans/lock/model/audit/TelemetryEntry.java +++ b/jans-lock/lock-server/model/src/main/java/io/jans/lock/model/audit/TelemetryEntry.java @@ -1,9 +1,18 @@ +/* + * Janssen Project software is available under the Apache License (2004). See http://www.apache.org/licenses/ for full text. + * + * Copyright (c) 2025, Janssen Project + */ + package io.jans.lock.model.audit; import java.io.Serializable; import java.util.Date; import java.util.Map; +import java.util.Objects; +import com.fasterxml.jackson.annotation.JsonFormat; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; import io.jans.orm.annotation.AttributeName; @@ -11,58 +20,98 @@ import io.jans.orm.annotation.JsonObject; import io.jans.orm.annotation.ObjectClass; import io.jans.orm.model.base.BaseEntry; - +import io.swagger.v3.oas.annotations.media.Schema; + +/** + * Telemetry Entry for audit logging. Represents telemetry and performance + * metrics of a service. + * + * @author Yuriy Movchan + */ +@Schema(description = "Telemetry audit entry") +@JsonIgnoreProperties(ignoreUnknown = true) @DataEntry(sortByName = "eventTime") @ObjectClass(value = "jansTelemetryEntry") public class TelemetryEntry extends BaseEntry implements Serializable { private static final long serialVersionUID = 3237727784024903177L; - @JsonProperty("inum") - @AttributeName(name = "inum", ignoreDuringUpdate = true) - private String inum; - - @AttributeName(name = "creationDate") - private Date creationDate; - - @AttributeName(name = "eventTime") - private Date eventTime; - - @AttributeName(name = "jansService") - private String service; - - @AttributeName(name = "jansNodeName") - private String nodeName; - - @AttributeName(name = "jansStatus") - private String status; - - @AttributeName(name = "jansDownloadSize") - private int lastPolicyLoadSize; - - @AttributeName(name = "jansSuccessLoadCounter") - private long policySuccessLoadCounter; - - @AttributeName(name = "jansFailedLoadCounter") - private long policyFailedLoadCounter; - - @AttributeName(name = "evaluationTimeNs") - private int lastPolicyEvaluationTimeNs; - - @AttributeName(name = "averageTimeNs") - private int avgPolicyEvaluationTimeNs; - - @JsonProperty("memoryUsage") - private String memoryUsage; - - @AttributeName(name = "requestCounter") - private long evaluationRequestsCount; - - @JsonObject - @AttributeName(name = "policyStats") - private Map policyStats; + @JsonProperty("inum") + @AttributeName(name = "inum", ignoreDuringUpdate = true) + private String inum; + + @JsonProperty("creationDate") + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd'T'HH:mm:ssXXX") + @Schema(description = "Creation date of the entry", example = "2024-04-21T18:25:43-05:00") + @AttributeName(name = "creationDate") + private Date creationDate; + + @JsonProperty("eventTime") + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd'T'HH:mm:ssXXX") + @Schema(description = "Time when the event occurred", example = "2024-04-21T18:25:43-05:00") + @AttributeName(name = "eventTime") + private Date eventTime; + + @JsonProperty("service") + @Schema(description = "Service name", example = "jans-auth") + @AttributeName(name = "jansService") + private String service; + + @JsonProperty("nodeName") + @Schema(description = "Node name or identifier", example = "1") + @AttributeName(name = "jansNodeName") + private String nodeName; + + @JsonProperty("status") + @Schema(description = "Service status", example = "ok", allowableValues = { "ok", "warning", "error" }) + @AttributeName(name = "jansStatus") + private String status; + + @JsonProperty("lastPolicyLoadSize") + @Schema(description = "Size of the last policy load in bytes", example = "1024") + @AttributeName(name = "jansDownloadSize") + private Long lastPolicyLoadSize; + + @JsonProperty("policySuccessLoadCounter") + @Schema(description = "Number of successful policy loads", example = "100") + @AttributeName(name = "jansSuccessLoadCounter") + private Long policySuccessLoadCounter; + + @JsonProperty("policyFailedLoadCounter") + @Schema(description = "Number of failed policy loads", example = "3") + @AttributeName(name = "jansFailedLoadCounter") + private Long policyFailedLoadCounter; + + @JsonProperty("lastPolicyEvaluationTimeNs") + @Schema(description = "Last policy evaluation time in nanoseconds", example = "100") + @AttributeName(name = "evaluationTimeNs") + private Long lastPolicyEvaluationTimeNs; + + @JsonProperty("avgPolicyEvaluationTimeNs") + @Schema(description = "Average policy evaluation time in nanoseconds", example = "75") + @AttributeName(name = "averageTimeNs") + private Long avgPolicyEvaluationTimeNs; + + @JsonProperty("memoryUsage") + @Schema(description = "Memory usage in bytes", example = "2097152") + @AttributeName(name = "memoryUsage") + private Long memoryUsage; + + @JsonProperty("evaluationRequestsCount") + @Schema(description = "Total number of evaluation requests", example = "100") + @AttributeName(name = "requestCounter") + private Long evaluationRequestsCount; + + @JsonProperty("policyStats") + @Schema(description = "Additional policy statistics as key-value pairs") + @JsonObject + @AttributeName(name = "policyStats") + private Map policyStats; + + public TelemetryEntry() { + } - public String getInum() { + public String getInum() { return inum; } @@ -110,79 +159,104 @@ public void setStatus(String status) { this.status = status; } - public int getLastPolicyLoadSize() { + public Long getLastPolicyLoadSize() { return lastPolicyLoadSize; } - public void setLastPolicyLoadSize(int lastPolicyLoadSize) { + public void setLastPolicyLoadSize(Long lastPolicyLoadSize) { this.lastPolicyLoadSize = lastPolicyLoadSize; } - public long getPolicySuccessLoadCounter() { + public Long getPolicySuccessLoadCounter() { return policySuccessLoadCounter; } - public void setPolicySuccessLoadCounter(long policySuccessLoadCounter) { + public void setPolicySuccessLoadCounter(Long policySuccessLoadCounter) { this.policySuccessLoadCounter = policySuccessLoadCounter; } - public long getPolicyFailedLoadCounter() { + public Long getPolicyFailedLoadCounter() { return policyFailedLoadCounter; } - public void setPolicyFailedLoadCounter(long policyFailedLoadCounter) { + public void setPolicyFailedLoadCounter(Long policyFailedLoadCounter) { this.policyFailedLoadCounter = policyFailedLoadCounter; } - public int getLastPolicyEvaluationTimeNs() { + public Long getLastPolicyEvaluationTimeNs() { return lastPolicyEvaluationTimeNs; } - public void setLastPolicyEvaluationTimeNs(int lastPolicyEvaluationTimeNs) { + public void setLastPolicyEvaluationTimeNs(Long lastPolicyEvaluationTimeNs) { this.lastPolicyEvaluationTimeNs = lastPolicyEvaluationTimeNs; } - public int getAvgPolicyEvaluationTimeNs() { + public Long getAvgPolicyEvaluationTimeNs() { return avgPolicyEvaluationTimeNs; } - public void setAvgPolicyEvaluationTimeNs(int avgPolicyEvaluationTimeNs) { + public void setAvgPolicyEvaluationTimeNs(Long avgPolicyEvaluationTimeNs) { this.avgPolicyEvaluationTimeNs = avgPolicyEvaluationTimeNs; } - public String getMemoryUsage() { + public Long getMemoryUsage() { return memoryUsage; } - public void setMemoryUsage(String memoryUsage) { + public void setMemoryUsage(Long memoryUsage) { this.memoryUsage = memoryUsage; } - public long getEvaluationRequestsCount() { + public Long getEvaluationRequestsCount() { return evaluationRequestsCount; } - public void setEvaluationRequestsCount(long evaluationRequestsCount) { + public void setEvaluationRequestsCount(Long evaluationRequestsCount) { this.evaluationRequestsCount = evaluationRequestsCount; } - public Map getPolicyStats() { + public Map getPolicyStats() { return policyStats; } - public void setPolicyStats(Map policyStats) { + public void setPolicyStats(Map policyStats) { this.policyStats = policyStats; } + @Override + public boolean equals(Object o) { + if (this == o) + return true; + if (o == null || getClass() != o.getClass()) + return false; + TelemetryEntry that = (TelemetryEntry) o; + return Objects.equals(creationDate, that.creationDate) && Objects.equals(eventTime, that.eventTime) + && Objects.equals(service, that.service) && Objects.equals(nodeName, that.nodeName) + && Objects.equals(status, that.status) && Objects.equals(lastPolicyLoadSize, that.lastPolicyLoadSize) + && Objects.equals(policySuccessLoadCounter, that.policySuccessLoadCounter) + && Objects.equals(policyFailedLoadCounter, that.policyFailedLoadCounter) + && Objects.equals(lastPolicyEvaluationTimeNs, that.lastPolicyEvaluationTimeNs) + && Objects.equals(avgPolicyEvaluationTimeNs, that.avgPolicyEvaluationTimeNs) + && Objects.equals(memoryUsage, that.memoryUsage) + && Objects.equals(evaluationRequestsCount, that.evaluationRequestsCount) + && Objects.equals(policyStats, that.policyStats); + } + + @Override + public int hashCode() { + return Objects.hash(creationDate, eventTime, service, nodeName, status, lastPolicyLoadSize, + policySuccessLoadCounter, policyFailedLoadCounter, lastPolicyEvaluationTimeNs, + avgPolicyEvaluationTimeNs, memoryUsage, evaluationRequestsCount, policyStats); + } + @Override public String toString() { - return "TelemetryEntry [inum=" + inum + ", creationDate=" + creationDate + ", eventTime=" + eventTime - + ", service=" + service + ", nodeName=" + nodeName + ", status=" + status + ", lastPolicyLoadSize=" - + lastPolicyLoadSize + ", policySuccessLoadCounter=" + policySuccessLoadCounter - + ", policyFailedLoadCounter=" + policyFailedLoadCounter + ", lastPolicyEvaluationTimeNs=" - + lastPolicyEvaluationTimeNs + ", avgPolicyEvaluationTimeNs=" + avgPolicyEvaluationTimeNs - + ", memoryUsage=" + memoryUsage + ", evaluationRequestsCount=" + evaluationRequestsCount - + ", policyStats=" + policyStats + ", toString()=" + super.toString() + "]"; - } - + return "TelemetryEntry{" + "creationDate='" + creationDate + '\'' + ", eventTime='" + eventTime + '\'' + + ", service='" + service + '\'' + ", nodeName='" + nodeName + '\'' + ", status='" + status + '\'' + + ", lastPolicyLoadSize=" + lastPolicyLoadSize + ", policySuccessLoadCounter=" + + policySuccessLoadCounter + ", policyFailedLoadCounter=" + policyFailedLoadCounter + + ", lastPolicyEvaluationTimeNs=" + lastPolicyEvaluationTimeNs + ", avgPolicyEvaluationTimeNs=" + + avgPolicyEvaluationTimeNs + ", memoryUsage=" + memoryUsage + ", evaluationRequestsCount=" + + evaluationRequestsCount + ", policyStats=" + policyStats + '}'; + } } 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 b08f5d70c9e..9044828b5e4 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 @@ -1,17 +1,7 @@ /* - * Copyright [2024] [Janssen Project] + * Janssen Project software is available under the Apache License (2004). See http://www.apache.org/licenses/ for full text. * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * Copyright (c) 2024, Janssen Project */ package io.jans.lock.model.config; @@ -22,6 +12,7 @@ import io.jans.doc.annotation.DocProperty; import io.jans.lock.model.config.cedarling.CedarlingConfiguration; +import io.jans.lock.model.config.grpc.GrpcConfiguration; import io.swagger.v3.oas.annotations.media.Schema; import jakarta.enterprise.inject.Vetoed; @@ -58,6 +49,10 @@ public class AppConfiguration implements Configuration { @Schema(description = "Cedarling configuration") private CedarlingConfiguration cedarlingConfiguration; + @DocProperty(description = "gRPC server configuration") + @Schema(description = "gRPC server configuration") + private GrpcConfiguration grpcConfiguration; + @DocProperty(description = "Active stat enabled") @Schema(description = "Active stat enabled") private boolean statEnabled; @@ -193,6 +188,14 @@ public void setCedarlingConfiguration(CedarlingConfiguration cedarlingConfigurat this.cedarlingConfiguration = cedarlingConfiguration; } + public GrpcConfiguration getGrpcConfiguration() { + return grpcConfiguration; + } + + public void setGrpcConfiguration(GrpcConfiguration grpcConfiguration) { + this.grpcConfiguration = grpcConfiguration; + } + public boolean isStatEnabled() { return statEnabled; } diff --git a/jans-lock/lock-server/model/src/main/java/io/jans/lock/model/config/AuditPersistenceMode.java b/jans-lock/lock-server/model/src/main/java/io/jans/lock/model/config/AuditPersistenceMode.java index db2a0bde6f8..21e65895e66 100644 --- a/jans-lock/lock-server/model/src/main/java/io/jans/lock/model/config/AuditPersistenceMode.java +++ b/jans-lock/lock-server/model/src/main/java/io/jans/lock/model/config/AuditPersistenceMode.java @@ -1,3 +1,9 @@ +/* + * Janssen Project software is available under the Apache License (2004). See http://www.apache.org/licenses/ for full text. + * + * Copyright (c) 2025, Janssen Project + */ + package io.jans.lock.model.config; import com.fasterxml.jackson.annotation.JsonValue; 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 a7972db4c14..ddb0ca725a8 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 @@ -1,19 +1,10 @@ /* - * Copyright [2024] [Janssen Project] + * Janssen Project software is available under the Apache License (2004). See http://www.apache.org/licenses/ for full text. * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * Copyright (c) 2024, Janssen Project */ + package io.jans.lock.model.config; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; diff --git a/jans-lock/lock-server/model/src/main/java/io/jans/lock/model/config/Conf.java b/jans-lock/lock-server/model/src/main/java/io/jans/lock/model/config/Conf.java index 09c76ee3f0e..83a131ca753 100644 --- a/jans-lock/lock-server/model/src/main/java/io/jans/lock/model/config/Conf.java +++ b/jans-lock/lock-server/model/src/main/java/io/jans/lock/model/config/Conf.java @@ -1,19 +1,10 @@ -/* - * Copyright [2024] [Janssen Project] +/*/* + * Janssen Project software is available under the Apache License (2004). See http://www.apache.org/licenses/ for full text. * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * Copyright (c) 2024, Janssen Project */ + package io.jans.lock.model.config; import io.jans.lock.model.config.cedarling.CedarlingPolicyConfiguration; diff --git a/jans-lock/lock-server/model/src/main/java/io/jans/lock/model/config/Configuration.java b/jans-lock/lock-server/model/src/main/java/io/jans/lock/model/config/Configuration.java index ef026df64c9..43c49039b0f 100644 --- a/jans-lock/lock-server/model/src/main/java/io/jans/lock/model/config/Configuration.java +++ b/jans-lock/lock-server/model/src/main/java/io/jans/lock/model/config/Configuration.java @@ -1,19 +1,10 @@ /* - * Copyright [2024] [Janssen Project] + * Janssen Project software is available under the Apache License (2004). See http://www.apache.org/licenses/ for full text. * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * Copyright (c) 2024, Janssen Project */ + package io.jans.lock.model.config; /** diff --git a/jans-lock/lock-server/model/src/main/java/io/jans/lock/model/config/GrpcServerMode.java b/jans-lock/lock-server/model/src/main/java/io/jans/lock/model/config/GrpcServerMode.java new file mode 100644 index 00000000000..f0fe887c612 --- /dev/null +++ b/jans-lock/lock-server/model/src/main/java/io/jans/lock/model/config/GrpcServerMode.java @@ -0,0 +1,38 @@ +/* + * Janssen Project software is available under the Apache License (2004). See http://www.apache.org/licenses/ for full text. + * + * Copyright (c) 2025, Janssen Project + */ + +package io.jans.lock.model.config; + +import com.fasterxml.jackson.annotation.JsonValue; + +/** + * @author Yuriy Movchan Date: 15/1/2022 + */ +public enum GrpcServerMode { + + DISABLED("disabled"), BRIDGE("bridge"), PLAIN_SERVER("plain_server"), TLS_SERVER("tls_server"); + + private final String mode; + + /** + * Create an enum constant with the specified string representation used for JSON serialization. + * + * @param mode the string value to use as this enum constant's JSON representation + */ + private GrpcServerMode(String mode) { + this.mode = mode; + } + + /** + * Mode string used for JSON serialization of the enum constant. + * + * @return the enum's mode string + */ + @JsonValue + public String getMode() { + return mode; + } +} \ No newline at end of file diff --git a/jans-lock/lock-server/model/src/main/java/io/jans/lock/model/config/LockProtectionMode.java b/jans-lock/lock-server/model/src/main/java/io/jans/lock/model/config/LockProtectionMode.java index b1bef535a8e..4ca746202ae 100644 --- a/jans-lock/lock-server/model/src/main/java/io/jans/lock/model/config/LockProtectionMode.java +++ b/jans-lock/lock-server/model/src/main/java/io/jans/lock/model/config/LockProtectionMode.java @@ -1,17 +1,7 @@ /* - * Copyright [2025] [Janssen Project] + * Janssen Project software is available under the Apache License (2004). See http://www.apache.org/licenses/ for full text. * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * Copyright (c) 2025, Janssen Project */ package io.jans.lock.model.config; diff --git a/jans-lock/lock-server/model/src/main/java/io/jans/lock/model/config/StaticConfiguration.java b/jans-lock/lock-server/model/src/main/java/io/jans/lock/model/config/StaticConfiguration.java index 52f373d3de4..7fafbeadc8a 100644 --- a/jans-lock/lock-server/model/src/main/java/io/jans/lock/model/config/StaticConfiguration.java +++ b/jans-lock/lock-server/model/src/main/java/io/jans/lock/model/config/StaticConfiguration.java @@ -1,17 +1,7 @@ /* - * Copyright [2024] [Janssen Project] + * Janssen Project software is available under the Apache License (2004). See http://www.apache.org/licenses/ for full text. * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * Copyright (c) 2024, Janssen Project */ package io.jans.lock.model.config; diff --git a/jans-lock/lock-server/model/src/main/java/io/jans/lock/model/config/cedarling/CedarlingConfiguration.java b/jans-lock/lock-server/model/src/main/java/io/jans/lock/model/config/cedarling/CedarlingConfiguration.java index f166724eb9e..c0c1440d0a5 100644 --- a/jans-lock/lock-server/model/src/main/java/io/jans/lock/model/config/cedarling/CedarlingConfiguration.java +++ b/jans-lock/lock-server/model/src/main/java/io/jans/lock/model/config/cedarling/CedarlingConfiguration.java @@ -1,17 +1,7 @@ /* - * Copyright [2025] [Janssen Project] + * Janssen Project software is available under the Apache License (2004). See http://www.apache.org/licenses/ for full text. * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * Copyright (c) 2025, Janssen Project */ package io.jans.lock.model.config.cedarling; diff --git a/jans-lock/lock-server/model/src/main/java/io/jans/lock/model/config/cedarling/LogLevel.java b/jans-lock/lock-server/model/src/main/java/io/jans/lock/model/config/cedarling/LogLevel.java index bc4f50f707b..8dab413278d 100644 --- a/jans-lock/lock-server/model/src/main/java/io/jans/lock/model/config/cedarling/LogLevel.java +++ b/jans-lock/lock-server/model/src/main/java/io/jans/lock/model/config/cedarling/LogLevel.java @@ -1,17 +1,7 @@ /* - * Copyright [2025] [Janssen Project] + * Janssen Project software is available under the Apache License (2004). See http://www.apache.org/licenses/ for full text. * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * Copyright (c) 2025, Janssen Project */ package io.jans.lock.model.config.cedarling; diff --git a/jans-lock/lock-server/model/src/main/java/io/jans/lock/model/config/cedarling/LogType.java b/jans-lock/lock-server/model/src/main/java/io/jans/lock/model/config/cedarling/LogType.java index 1a6885eb25d..434ba345979 100644 --- a/jans-lock/lock-server/model/src/main/java/io/jans/lock/model/config/cedarling/LogType.java +++ b/jans-lock/lock-server/model/src/main/java/io/jans/lock/model/config/cedarling/LogType.java @@ -1,17 +1,7 @@ /* - * Copyright [2025] [Janssen Project] + * Janssen Project software is available under the Apache License (2004). See http://www.apache.org/licenses/ for full text. * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * Copyright (c) 2025, Janssen Project */ package io.jans.lock.model.config.cedarling; diff --git a/jans-lock/lock-server/model/src/main/java/io/jans/lock/model/config/cedarling/PolicySource.java b/jans-lock/lock-server/model/src/main/java/io/jans/lock/model/config/cedarling/PolicySource.java index bdb8668498c..f01b179432c 100644 --- a/jans-lock/lock-server/model/src/main/java/io/jans/lock/model/config/cedarling/PolicySource.java +++ b/jans-lock/lock-server/model/src/main/java/io/jans/lock/model/config/cedarling/PolicySource.java @@ -1,17 +1,7 @@ /* - * Copyright [2025] [Janssen Project] + * Janssen Project software is available under the Apache License (2004). See http://www.apache.org/licenses/ for full text. * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * Copyright (c) 2025, Janssen Project */ package io.jans.lock.model.config.cedarling; diff --git a/jans-lock/lock-server/model/src/main/java/io/jans/lock/model/config/grpc/GrpcConfiguration.java b/jans-lock/lock-server/model/src/main/java/io/jans/lock/model/config/grpc/GrpcConfiguration.java new file mode 100644 index 00000000000..e5986418667 --- /dev/null +++ b/jans-lock/lock-server/model/src/main/java/io/jans/lock/model/config/grpc/GrpcConfiguration.java @@ -0,0 +1,91 @@ +/* + * Janssen Project software is available under the Apache License (2004). See http://www.apache.org/licenses/ for full text. + * + * Copyright (c) 2025, Janssen Project + */ + +package io.jans.lock.model.config.grpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; + +import io.jans.doc.annotation.DocProperty; +import io.jans.lock.model.config.GrpcServerMode; +import io.swagger.v3.oas.annotations.media.Schema; + +/** + * gRPC server configuration + * + * @author Yuriy Movchan Date: 10/08/2022 + */ +@JsonIgnoreProperties(ignoreUnknown = true) +public class GrpcConfiguration { + + @DocProperty(description = "gRPC server mode") + @Schema(description = "gRPC server mode") + private GrpcServerMode serverMode = GrpcServerMode.BRIDGE; + + @DocProperty(description = "Specify grpc port", defaultValue = "50051") + @Schema(description = "Specify grpc port") + private int grpcPort = 50051; // Default gRPC port + + // TLS/ALPN support (Netty-based) + @DocProperty(description = "Use TLS for gRPC communication", defaultValue = "false") + @Schema(description = "Use TLS for gRPC communication") + private boolean useTls = false; + + @DocProperty(description = "TLS Cert Chain File Path", defaultValue = "") + @Schema(description = "TLS Cert Chain File Path") + private String tlsCertChainFilePath; // PEM cert chain file + + @DocProperty(description = "TLS Private Key File Path", defaultValue = "") + @Schema(description = "TLS Private Key File Path") + private String tlsPrivateKeyFilePath; // PEM private key file + + public GrpcServerMode getServerMode() { + return serverMode; + } + + public void setServerMode(GrpcServerMode serverMode) { + this.serverMode = serverMode; + } + + public int getGrpcPort() { + return grpcPort; + } + + public void setGrpcPort(int grpcPort) { + this.grpcPort = grpcPort; + } + + public boolean isUseTls() { + return useTls; + } + + public void setUseTls(boolean useTls) { + this.useTls = useTls; + } + + public String getTlsCertChainFilePath() { + return tlsCertChainFilePath; + } + + public void setTlsCertChainFilePath(String tlsCertChainFilePath) { + this.tlsCertChainFilePath = tlsCertChainFilePath; + } + + public String getTlsPrivateKeyFilePath() { + return tlsPrivateKeyFilePath; + } + + public void setTlsPrivateKeyFilePath(String tlsPrivateKeyFilePath) { + this.tlsPrivateKeyFilePath = tlsPrivateKeyFilePath; + } + + @Override + public String toString() { + return "GrpcConfiguration [serverMode=" + serverMode + ", grpcPort=" + grpcPort + ", useTls=" + useTls + + ", tlsCertChainFilePath=" + tlsCertChainFilePath + ", tlsPrivateKeyFilePath=" + tlsPrivateKeyFilePath + + "]"; + } + +} diff --git a/jans-lock/lock-server/model/src/main/java/io/jans/lock/model/core/LockApiError.java b/jans-lock/lock-server/model/src/main/java/io/jans/lock/model/core/LockApiError.java index 0b3183b8836..10f68fd9f29 100644 --- a/jans-lock/lock-server/model/src/main/java/io/jans/lock/model/core/LockApiError.java +++ b/jans-lock/lock-server/model/src/main/java/io/jans/lock/model/core/LockApiError.java @@ -1,7 +1,7 @@ /* - * Janssen Project software is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * Janssen Project software is available under the Apache License (2004). See http://www.apache.org/licenses/ for full text. * - * Copyright (c) 2020, Janssen Project + * Copyright (c) 2022, Janssen Project */ package io.jans.lock.model.core; diff --git a/jans-lock/lock-server/server/src/test/manual/automatic_audit_enpoints_tests.sh b/jans-lock/lock-server/server/src/test/manual/automatic_audit_enpoints_tests.sh new file mode 100755 index 00000000000..e1a50c2c280 --- /dev/null +++ b/jans-lock/lock-server/server/src/test/manual/automatic_audit_enpoints_tests.sh @@ -0,0 +1,352 @@ +#!/bin/bash + +# ============================================================================= +# Test script for checking REST ↔ gRPC bridge + Audit API authorization +# Tests: log, health, telemetry (single + bulk) with write & read scopes +# Requirements: curl, jq, grpcurl +# ============================================================================= + +set -euo pipefail + +HOST="https://server.jans.info" +GRPC_ADDR="server.jans.info:443" +CLIENT_ID="2200...." +CLIENT_SECRET="..." +INVALID_TOKEN="this-is-definitely-not-a-valid-token" + +# ───────────────────────────────────────────────────────────────────────────── +# Test data — REST format +# ───────────────────────────────────────────────────────────────────────────── + +# Log +SINGLE_LOG_JSON_REST='{ + "creation_date": "2024-04-21T18:25:43-05:00", + "event_time": "2024-04-21T18:25:43-05:00", + "service": "jans-auth", + "node_name": "node-1", + "event_type": "registration", + "severity_level": "warning", + "action": "LOGIN_ATTEMPT", + "decision_result": "allow", + "requested_resource": "{\"res\":\"/api/user\",\"method\":\"POST\"}", + "principal_id": "ACC-000123", + "client_id": "CLI-001", + "jti": "test-jti-123456", + "context_information": { + "ip": "192.168.1.77", + "user_agent": "Mozilla/5.0 (test)" + } +}' + +BULK_LOG_JSON_REST='[ + { + "creation_date": "2024-04-21T18:25:43-05:00", + "event_time": "2024-04-21T18:25:43-05:00", + "service": "jans-auth", + "node_name": "node-1", + "event_type": "registration", + "severity_level": "warning", + "action": "LOGIN_ATTEMPT", + "decision_result": "deny", + "requested_resource": "{\"res\":\"/api/admin\"}", + "principal_id": "ACC-000123", + "client_id": "CLI-001", + "context_information": {"ip": "10.0.0.5"} + }, + { + "creation_date": "2024-06-15T14:10:22Z", + "event_time": "2024-06-15T14:10:22Z", + "service": "jans-auth", + "node_name": "node-2", + "event_type": "transaction", + "severity_level": "info", + "action": "PAYMENT", + "decision_result": "allow", + "requested_resource": "{\"amount\":99.99}", + "principal_id": "ACC-000123", + "client_id": "CLI-001" + } +]' + +# Health +SINGLE_HEALTH_JSON_REST='{ + "creationDate": "2024-04-21T17:25:43-05:00", + "eventTime": "2024-04-21T18:25:43-05:00", + "service": "jans-auth", + "nodeName": "node-1", + "status": "ok" +}' + +BULK_HEALTH_JSON_REST='[ + { + "creationDate": "2024-04-21T17:25:43-05:00", + "eventTime": "2024-04-21T18:25:43-05:00", + "service": "jans-auth", + "nodeName": "node-1", + "status": "ok" + }, + { + "creationDate": "2024-04-21T17:30:00-05:00", + "eventTime": "2024-04-21T18:30:00-05:00", + "service": "jans-lock", + "nodeName": "node-2", + "status": "degraded" + } +]' + +# Telemetry +SINGLE_TELEMETRY_JSON_REST='{ + "creationDate": "2024-04-21T18:00:00-05:00", + "eventTime": "2024-04-21T18:25:43-05:00", + "service": "jans-auth", + "nodeName": "node-1", + "status": "ok", + "lastPolicyLoadSize": 1024, + "policySuccessLoadCounter": 100, + "policyFailedLoadCounter": 3, + "lastPolicyEvaluationTimeNs": 100000, + "avgPolicyEvaluationTimeNs": 75000, + "memoryUsage": 2097152, + "evaluationRequestsCount": 100, + "policyStats": {"stat_1":100,"stat_2":3} +}' + +BULK_TELEMETRY_JSON_REST='[ + { + "creationDate": "2024-04-21T18:00:00-05:00", + "eventTime": "2024-04-21T18:25:43-05:00", + "service": "jans-auth", + "nodeName": "node-1", + "status": "ok", + "lastPolicyLoadSize": 1024, + "policySuccessLoadCounter": 100, + "policyFailedLoadCounter": 3, + "lastPolicyEvaluationTimeNs": 100000, + "avgPolicyEvaluationTimeNs": 75000, + "memoryUsage": 2097152, + "evaluationRequestsCount": 100, + "policyStats": {"stat_1":100,"stat_2":3} + }, + { + "creationDate": "2024-04-21T19:00:00Z", + "eventTime": "2024-04-21T19:10:22Z", + "service": "jans-lock", + "nodeName": "node-2", + "status": "ok", + "lastPolicyLoadSize": 2048, + "policySuccessLoadCounter": 250, + "policyFailedLoadCounter": 1, + "lastPolicyEvaluationTimeNs": 85000, + "avgPolicyEvaluationTimeNs": 92000, + "memoryUsage": 4194304, + "evaluationRequestsCount": 420, + "policyStats": {"p1":300,"p2":120} + } +]' + +# gRPC wrappers +SINGLE_LOG_GRPC='{ "entry": '"${SINGLE_LOG_JSON_REST}"' }' +BULK_LOG_GRPC='{ "entries": '"${BULK_LOG_JSON_REST}"' }' + +SINGLE_HEALTH_GRPC='{ "entry": '"${SINGLE_HEALTH_JSON_REST}"' }' +BULK_HEALTH_GRPC='{ "entries": '"${BULK_HEALTH_JSON_REST}"' }' + +SINGLE_TELEMETRY_GRPC='{ "entry": '"${SINGLE_TELEMETRY_JSON_REST}"' }' +BULK_TELEMETRY_GRPC='{ "entries": '"${BULK_TELEMETRY_JSON_REST}"' }' + +# ───────────────────────────────────────────────────────────────────────────── +# Helper functions +# ───────────────────────────────────────────────────────────────────────────── + +get_token() { + local scope="$1" + curl -s -k \ + -u "${CLIENT_ID}:${CLIENT_SECRET}" \ + "${HOST}/jans-auth/restv1/token" \ + -d "grant_type=client_credentials" \ + -d "scope=${scope}" \ + | jq -r '.access_token // empty' +} + +check_http_success() { + local cmd="$1" + local desc="$2" + local status + status=$(eval "$cmd" -s -o /dev/null -w "%{http_code}") + if [[ $status -ge 200 && $status -lt 300 ]]; then + echo "✅ $desc (HTTP $status)" + else + echo "❌ $desc → HTTP $status" + echo "Command: $cmd" + return 1 + fi +} + +check_http_should_fail() { + local cmd="$1" + local desc="$2" + local status + status=$(eval "$cmd" -s -o /dev/null -w "%{http_code}") + if [[ $status -ge 401 && $status -le 403 ]]; then + echo "✅ (expected failure) $desc (HTTP $status)" + else + echo "❌ UNEXPECTED success → $desc (HTTP $status)" + echo "Command: $cmd" + return 1 + fi +} + +check_grpc_success() { + local cmd="$1" + local desc="$2" + > res.txt + if eval "$cmd" >res.txt 2>&1; then + echo "✅ $desc" + else + echo "❌ $desc" + echo "Command: $cmd" + echo "Output:" + cat res.txt + return 1 + fi +} + +check_grpc_should_fail() { + local cmd="$1" + local desc="$2" + > res.txt + if eval "$cmd" >res.txt 2>&1; then + echo "❌ UNEXPECTED success → $desc" + echo "Command: $cmd" + echo "Output:" + cat res.txt + return 1 + else + local output + output=$(cat res.txt) + if echo "$output" | grep -q -i "PermissionDenied" || echo "$output" | grep -q -i "Invalid token" || echo "$output" | grep -q -i "unauthenticated"; then + local code + code=$(echo "$output" | grep -i "Code:" | head -n 1 | sed -E 's/.*Code:[[:space:]]*([^[:space:],().]+).*/\1/i') + echo "✅ (expected failure) $desc with status: $code" + else + echo "❌ UNEXPECTED gRPC error → $desc" + echo "Command: $cmd" + echo "Output:" + echo "$output" + return 1 + fi + fi +} + +# ───────────────────────────────────────────────────────────────────────────── +# Main test flow +# ───────────────────────────────────────────────────────────────────────────── + +echo "┌────────────────────────────────────────────────────────────────────┐" +echo "│ AUDIT API AUTO-TEST (log / health / telemetry) │" +echo "└────────────────────────────────────────────────────────────────────┘" + +echo -e "\n1. Obtaining tokens\n" + +WRITE_LOG_TOKEN=$(get_token "https://jans.io/oauth/lock/log.write") +[[ -z "$WRITE_LOG_TOKEN" ]] && { echo "Failed to obtain log.write token"; exit 1; } +echo "Log write token obtained" + +WRITE_HEALTH_TOKEN=$(get_token "https://jans.io/oauth/lock/health.write") +[[ -z "$WRITE_HEALTH_TOKEN" ]] && { echo "Failed to obtain health.write token"; exit 1; } +echo "Health write token obtained" + +WRITE_TELEMETRY_TOKEN=$(get_token "https://jans.io/oauth/lock/telemetry.write") +[[ -z "$WRITE_TELEMETRY_TOKEN" ]] && { echo "Failed to obtain telemetry.write token"; exit 1; } +echo "Telemetry write token obtained" + +READ_LOG_TOKEN=$(get_token "https://jans.io/oauth/lock/log.read") +[[ -z "$READ_LOG_TOKEN" ]] && { echo "Failed to obtain log.read token"; exit 1; } +echo "Log read token obtained" + +READ_HEALTH_TOKEN=$(get_token "https://jans.io/oauth/lock/health.read") +[[ -z "$READ_HEALTH_TOKEN" ]] && { echo "Failed to obtain health.read token"; exit 1; } +echo "Health read token obtained" + +READ_TELEMETRY_TOKEN=$(get_token "https://jans.io/oauth/lock/telemetry.read") +[[ -z "$READ_TELEMETRY_TOKEN" ]] && { echo "Failed to obtain telemetry.read token"; exit 1; } +echo "Telemetry read token obtained" + +echo -e "\n2. Tests with valid write tokens (should succeed)\n" + +echo -e "\n── Log ───────────────────────────────────────────────" +check_http_success "curl -k -H 'Authorization: Bearer $WRITE_LOG_TOKEN' -H 'Content-Type: application/json' -d @- '${HOST}/jans-lock/api/v1/audit/log' <<< '$SINGLE_LOG_JSON_REST'" "REST → single log entry" +check_http_success "curl -k -H 'Authorization: Bearer $WRITE_LOG_TOKEN' -H 'Content-Type: application/json' -d @- '${HOST}/jans-lock/api/v1/audit/log/bulk' <<< '$BULK_LOG_JSON_REST'" "REST → bulk log entries" +check_grpc_success "grpcurl -insecure --proto audit.proto -H 'authorization: bearer $WRITE_LOG_TOKEN' -d '$SINGLE_LOG_GRPC' $GRPC_ADDR io.jans.lock.audit.AuditService/ProcessLog" "gRPC → ProcessLog" +check_grpc_success "grpcurl -insecure --proto audit.proto -H 'authorization: bearer $WRITE_LOG_TOKEN' -H 'GRPC_APP: jans-lock' -d '$BULK_LOG_GRPC' $GRPC_ADDR io.jans.lock.audit.AuditService/ProcessBulkLog" "gRPC → ProcessBulkLog" + +echo -e "\n── Health ───────────────────────────────────────────" +check_http_success "curl -k -H 'Authorization: Bearer $WRITE_HEALTH_TOKEN' -H 'Content-Type: application/json' -d @- '${HOST}/jans-lock/api/v1/audit/health' <<< '$SINGLE_HEALTH_JSON_REST'" "REST → single health entry" +check_http_success "curl -k -H 'Authorization: Bearer $WRITE_HEALTH_TOKEN' -H 'Content-Type: application/json' -d @- '${HOST}/jans-lock/api/v1/audit/health/bulk' <<< '$BULK_HEALTH_JSON_REST'" "REST → bulk health entries" +check_grpc_success "grpcurl -insecure --proto audit.proto -H 'authorization: bearer $WRITE_HEALTH_TOKEN' -d '$SINGLE_HEALTH_GRPC' $GRPC_ADDR io.jans.lock.audit.AuditService/ProcessHealth" "gRPC → ProcessHealth" +check_grpc_success "grpcurl -insecure --proto audit.proto -H 'authorization: bearer $WRITE_HEALTH_TOKEN' -d '$BULK_HEALTH_GRPC' $GRPC_ADDR io.jans.lock.audit.AuditService/ProcessBulkHealth" "gRPC → ProcessBulkHealth" + +echo -e "\n── Telemetry ─────────────────────────────────────────" +check_http_success "curl -k -H 'Authorization: Bearer $WRITE_TELEMETRY_TOKEN' -H 'Content-Type: application/json' -d @- '${HOST}/jans-lock/api/v1/audit/telemetry' <<< '$SINGLE_TELEMETRY_JSON_REST'" "REST → single telemetry entry" +check_http_success "curl -k -H 'Authorization: Bearer $WRITE_TELEMETRY_TOKEN' -H 'Content-Type: application/json' -d @- '${HOST}/jans-lock/api/v1/audit/telemetry/bulk' <<< '$BULK_TELEMETRY_JSON_REST'" "REST → bulk telemetry entries" +check_grpc_success "grpcurl -insecure --proto audit.proto -H 'authorization: bearer $WRITE_TELEMETRY_TOKEN' -d '$SINGLE_TELEMETRY_GRPC' $GRPC_ADDR io.jans.lock.audit.AuditService/ProcessTelemetry" "gRPC → ProcessTelemetry" +check_grpc_success "grpcurl -insecure --proto audit.proto -H 'authorization: bearer $WRITE_TELEMETRY_TOKEN' -d '$BULK_TELEMETRY_GRPC' $GRPC_ADDR io.jans.lock.audit.AuditService/ProcessBulkTelemetry" "gRPC → ProcessBulkTelemetry" + +echo -e "\n3. Tests with invalid write tokens → write operations should fail\n" + +echo -e "\n── Log with invalid scope ────────────────────────────" +check_http_should_fail "curl -k -H 'Authorization: Bearer $WRITE_HEALTH_TOKEN' -H 'Content-Type: application/json' -d @- '${HOST}/jans-lock/api/v1/audit/log' <<< '$SINGLE_LOG_JSON_REST'" "REST → single log entry" +check_grpc_should_fail "grpcurl -insecure --proto audit.proto -H 'authorization: bearer $WRITE_HEALTH_TOKEN' -d '$SINGLE_LOG_GRPC' $GRPC_ADDR io.jans.lock.audit.AuditService/ProcessLog" "gRPC → ProcessLog" + +echo -e "\n── Health with invalid scope ─────────────────────────" +check_http_should_fail "curl -k -H 'Authorization: Bearer $WRITE_LOG_TOKEN' -H 'Content-Type: application/json' -d @- '${HOST}/jans-lock/api/v1/audit/health' <<< '$SINGLE_HEALTH_JSON_REST'" "REST → single health entry" +check_grpc_should_fail "grpcurl -insecure --proto audit.proto -H 'authorization: bearer $WRITE_LOG_TOKEN' -d '$SINGLE_HEALTH_GRPC' $GRPC_ADDR io.jans.lock.audit.AuditService/ProcessHealth" "gRPC → ProcessHealth" + +echo -e "\n── Telemetry with invalid scope ──────────────────────" +check_http_should_fail "curl -k -H 'Authorization: Bearer $WRITE_LOG_TOKEN' -H 'Content-Type: application/json' -d @- '${HOST}/jans-lock/api/v1/audit/telemetry' <<< '$SINGLE_TELEMETRY_JSON_REST'" "REST → single telemetry entry" +check_grpc_should_fail "grpcurl -insecure --proto audit.proto -H 'authorization: bearer $WRITE_LOG_TOKEN' -d '$SINGLE_TELEMETRY_GRPC' $GRPC_ADDR io.jans.lock.audit.AuditService/ProcessTelemetry" "gRPC → ProcessTelemetry" + +echo -e "\n4. Tests with read-only tokens → write operations should fail\n" + +echo -e "\n── Log read token ────────────────────────────────────" +check_http_should_fail "curl -k -H 'Authorization: Bearer $READ_LOG_TOKEN' -H 'Content-Type: application/json' -d @- '${HOST}/jans-lock/api/v1/audit/log' <<< '$SINGLE_LOG_JSON_REST'" "REST → single log entry with read-only token" +check_http_should_fail "curl -k -H 'Authorization: Bearer $READ_LOG_TOKEN' -H 'Content-Type: application/json' -d @- '${HOST}/jans-lock/api/v1/audit/log/bulk' <<< '$BULK_LOG_JSON_REST'" "REST → bulk log entries with read-only token" +check_grpc_should_fail "grpcurl -insecure --proto audit.proto -H 'authorization: bearer $READ_LOG_TOKEN' -d '$SINGLE_LOG_GRPC' $GRPC_ADDR io.jans.lock.audit.AuditService/ProcessLog" "gRPC → ProcessLog with read-only token" +check_grpc_should_fail "grpcurl -insecure --proto audit.proto -H 'authorization: bearer $READ_LOG_TOKEN' -d '$BULK_LOG_GRPC' $GRPC_ADDR io.jans.lock.audit.AuditService/ProcessBulkLog" "gRPC → ProcessBulkLog with read-only token" + +echo -e "\n── Health read token ─────────────────────────────────" +check_http_should_fail "curl -k -H 'Authorization: Bearer $READ_HEALTH_TOKEN' -H 'Content-Type: application/json' -d @- '${HOST}/jans-lock/api/v1/audit/health' <<< '$SINGLE_HEALTH_JSON_REST'" "REST → single health entry with read-only token" +check_http_should_fail "curl -k -H 'Authorization: Bearer $READ_HEALTH_TOKEN' -H 'Content-Type: application/json' -d @- '${HOST}/jans-lock/api/v1/audit/health/bulk' <<< '$BULK_HEALTH_JSON_REST'" "REST → bulk health entries with read-only token" +check_grpc_should_fail "grpcurl -insecure --proto audit.proto -H 'authorization: bearer $READ_HEALTH_TOKEN' -d '$SINGLE_HEALTH_GRPC' $GRPC_ADDR io.jans.lock.audit.AuditService/ProcessHealth" "gRPC → ProcessHealth with read-only token" +check_grpc_should_fail "grpcurl -insecure --proto audit.proto -H 'authorization: bearer $READ_HEALTH_TOKEN' -d '$BULK_HEALTH_GRPC' $GRPC_ADDR io.jans.lock.audit.AuditService/ProcessBulkHealth" "gRPC → ProcessBulkHealth with read-only token" + +echo -e "\n── Telemetry read token ──────────────────────────────" +check_http_should_fail "curl -k -H 'Authorization: Bearer $READ_TELEMETRY_TOKEN' -H 'Content-Type: application/json' -d @- '${HOST}/jans-lock/api/v1/audit/telemetry' <<< '$SINGLE_TELEMETRY_JSON_REST'" "REST → single telemetry entry with read-only token" +check_http_should_fail "curl -k -H 'Authorization: Bearer $READ_TELEMETRY_TOKEN' -H 'Content-Type: application/json' -d @- '${HOST}/jans-lock/api/v1/audit/telemetry/bulk' <<< '$BULK_TELEMETRY_JSON_REST'" "REST → bulk telemetry entries with read-only token" +check_grpc_should_fail "grpcurl -insecure --proto audit.proto -H 'authorization: bearer $READ_TELEMETRY_TOKEN' -d '$SINGLE_TELEMETRY_GRPC' $GRPC_ADDR io.jans.lock.audit.AuditService/ProcessTelemetry" "gRPC → ProcessTelemetry with read-only token" +check_grpc_should_fail "grpcurl -insecure --proto audit.proto -H 'authorization: bearer $READ_TELEMETRY_TOKEN' -d '$BULK_TELEMETRY_GRPC' $GRPC_ADDR io.jans.lock.audit.AuditService/ProcessBulkTelemetry" "gRPC → ProcessBulkTelemetry with read-only token" + +echo -e "\n5. Tests with invalid token (should fail)\n" + +echo -e "\n── Log invalid token ────────────────────────────────────" +check_http_should_fail "curl -k -H 'Authorization: Bearer $INVALID_TOKEN' -H 'Content-Type: application/json' -d @- '${HOST}/jans-lock/api/v1/audit/log' <<< '$SINGLE_LOG_JSON_REST'" "REST → single log entry with invalid token" +check_http_should_fail "curl -k -H 'Authorization: Bearer $INVALID_TOKEN' -H 'Content-Type: application/json' -d @- '${HOST}/jans-lock/api/v1/audit/log/bulk' <<< '$BULK_LOG_JSON_REST'" "REST → bulk log entries with invalid token" +check_grpc_should_fail "grpcurl -insecure --proto audit.proto -H 'authorization: bearer $INVALID_TOKEN' -d '$SINGLE_LOG_GRPC' $GRPC_ADDR io.jans.lock.audit.AuditService/ProcessLog" "gRPC → ProcessLog with invalid token" +check_grpc_should_fail "grpcurl -insecure --proto audit.proto -H 'authorization: bearer $INVALID_TOKEN' -d '$BULK_LOG_GRPC' $GRPC_ADDR io.jans.lock.audit.AuditService/ProcessBulkLog" "gRPC → ProcessBulkLog with invalid token" + +echo -e "\n── Health invalid token ─────────────────────────────────" +check_http_should_fail "curl -k -H 'Authorization: Bearer $INVALID_TOKEN' -H 'Content-Type: application/json' -d @- '${HOST}/jans-lock/api/v1/audit/health' <<< '$SINGLE_HEALTH_JSON_REST'" "REST → single health entry with invalid token" +check_http_should_fail "curl -k -H 'Authorization: Bearer $INVALID_TOKEN' -H 'Content-Type: application/json' -d @- '${HOST}/jans-lock/api/v1/audit/health/bulk' <<< '$BULK_HEALTH_JSON_REST'" "REST → bulk health entries with invalid token" +check_grpc_should_fail "grpcurl -insecure --proto audit.proto -H 'authorization: bearer $INVALID_TOKEN' -d '$SINGLE_HEALTH_GRPC' $GRPC_ADDR io.jans.lock.audit.AuditService/ProcessHealth" "gRPC → ProcessHealth with invalid token" +check_grpc_should_fail "grpcurl -insecure --proto audit.proto -H 'authorization: bearer $INVALID_TOKEN' -d '$BULK_HEALTH_GRPC' $GRPC_ADDR io.jans.lock.audit.AuditService/ProcessBulkHealth" "gRPC → ProcessBulkHealth with invalid token" + +echo -e "\n── Telemetry invalid token ──────────────────────────────" +check_http_should_fail "curl -k -H 'Authorization: Bearer $INVALID_TOKEN' -H 'Content-Type: application/json' -d @- '${HOST}/jans-lock/api/v1/audit/telemetry' <<< '$SINGLE_TELEMETRY_JSON_REST'" "REST → single telemetry entry with invalid token" +check_http_should_fail "curl -k -H 'Authorization: Bearer $INVALID_TOKEN' -H 'Content-Type: application/json' -d @- '${HOST}/jans-lock/api/v1/audit/telemetry/bulk' <<< '$BULK_TELEMETRY_JSON_REST'" "REST → bulk telemetry entries with invalid token" +check_grpc_should_fail "grpcurl -insecure --proto audit.proto -H 'authorization: bearer $INVALID_TOKEN' -H 'GRPC_APP: jans-lock' -d '$SINGLE_TELEMETRY_GRPC' $GRPC_ADDR io.jans.lock.audit.AuditService/ProcessTelemetry" "gRPC → ProcessTelemetry with invalid token" +check_grpc_should_fail "grpcurl -insecure --proto audit.proto -H 'authorization: bearer $INVALID_TOKEN' -d '$BULK_TELEMETRY_GRPC' $GRPC_ADDR io.jans.lock.audit.AuditService/ProcessBulkTelemetry" "gRPC → ProcessBulkTelemetry with invalid token" + +echo -e "\n┌──────────────────────────────┐" +echo "│ TESTS PASSED │" +echo "└──────────────────────────────┘" + diff --git a/jans-lock/lock-server/service/pom.xml b/jans-lock/lock-server/service/pom.xml index 8fce5ad7e95..a615e803401 100644 --- a/jans-lock/lock-server/service/pom.xml +++ b/jans-lock/lock-server/service/pom.xml @@ -62,6 +62,41 @@ + + + + org.xolstice.maven.plugins + protobuf-maven-plugin + + com.google.protobuf:protoc:4.33.2:exe:${os.detected.classifier} + grpc-java + io.grpc:protoc-gen-grpc-java:1.78.0:exe:${os.detected.classifier} + + + + compile_proto + process-sources + + compile + compile-custom + + + + + + + + kr.motd.maven + os-maven-plugin + + + initialize + + detect + + + + @@ -126,9 +161,14 @@ + + + - jakarta.servlet - jakarta.servlet-api + javax.servlet + javax.servlet-api + 4.0.1 + provided @@ -183,6 +223,30 @@ resteasy-jackson2-provider + + + io.grpc + grpc-netty-shaded + + + io.grpc + grpc-protobuf + + + io.grpc + grpc-stub + + + io.grpc + grpc-servlet-jakarta + + + + + dev.resteasy.grpc + grpc-bridge + + io.swagger.core.v3 @@ -210,4 +274,4 @@ test - + \ No newline at end of file diff --git a/jans-lock/lock-server/service/src/main/java/io/jans/lock/service/DataMapperService.java b/jans-lock/lock-server/service/src/main/java/io/jans/lock/service/DataMapperService.java index d901199dd31..e4f15afaebb 100644 --- a/jans-lock/lock-server/service/src/main/java/io/jans/lock/service/DataMapperService.java +++ b/jans-lock/lock-server/service/src/main/java/io/jans/lock/service/DataMapperService.java @@ -1,11 +1,11 @@ -package io.jans.lock.service; - /* - * Janssen Project software is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * 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.service; + import java.io.BufferedReader; import java.io.IOException; import java.io.InputStream; diff --git a/jans-lock/lock-server/service/src/main/java/io/jans/lock/service/OpenIdService.java b/jans-lock/lock-server/service/src/main/java/io/jans/lock/service/OpenIdService.java index 87aa2eea194..d4f1e2ea273 100644 --- a/jans-lock/lock-server/service/src/main/java/io/jans/lock/service/OpenIdService.java +++ b/jans-lock/lock-server/service/src/main/java/io/jans/lock/service/OpenIdService.java @@ -1,7 +1,7 @@ /* - * Janssen Project software is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * Janssen Project software is available under the Apache License (2004). See http://www.apache.org/licenses/ for full text. * - * Copyright (c) 2020, Janssen Project + * Copyright (c) 2022, Janssen Project */ package io.jans.lock.service; diff --git a/jans-lock/lock-server/service/src/main/java/io/jans/lock/service/ServiceInitializer.java b/jans-lock/lock-server/service/src/main/java/io/jans/lock/service/ServiceInitializer.java index 8c7b2ee18b0..f02b91b348d 100644 --- a/jans-lock/lock-server/service/src/main/java/io/jans/lock/service/ServiceInitializer.java +++ b/jans-lock/lock-server/service/src/main/java/io/jans/lock/service/ServiceInitializer.java @@ -19,6 +19,7 @@ import org.slf4j.Logger; import io.jans.lock.service.config.ConfigurationFactory; +import io.jans.lock.service.grpc.server.GrpcServerStarter; import io.jans.service.cdi.event.ApplicationInitializedEvent; import jakarta.enterprise.context.ApplicationScoped; import jakarta.enterprise.event.Observes; @@ -39,10 +40,14 @@ public class ServiceInitializer { @Inject private ConfigurationFactory configurationFactory; + @Inject + private GrpcServerStarter grpcServerStarter; + public void applicationInitialized(@Observes ApplicationInitializedEvent applicationInitializedEvent) { log.info("Initializing Lock service module services"); configurationFactory.initTimer(); + grpcServerStarter.initGrpcServer(); log.debug("Initializing Lock service module services complete"); } diff --git a/jans-lock/lock-server/service/src/main/java/io/jans/lock/service/filter/AuthorizationProcessingFilter.java b/jans-lock/lock-server/service/src/main/java/io/jans/lock/service/filter/AuthorizationProcessingFilter.java index c875d226e04..a3197b6bcc7 100644 --- a/jans-lock/lock-server/service/src/main/java/io/jans/lock/service/filter/AuthorizationProcessingFilter.java +++ b/jans-lock/lock-server/service/src/main/java/io/jans/lock/service/filter/AuthorizationProcessingFilter.java @@ -2,6 +2,7 @@ import java.io.IOException; +import org.apache.commons.lang3.StringUtils; import org.slf4j.Logger; import io.jans.lock.model.app.audit.AuditActionType; @@ -9,6 +10,7 @@ import io.jans.lock.model.config.AppConfiguration; import io.jans.lock.model.config.LockProtectionMode; import io.jans.lock.service.app.audit.ApplicationAuditLogger; +import io.jans.lock.service.openid.OpenIdProtection; import io.jans.net.InetAddressUtility; import io.jans.service.security.api.ProtectedApi; import jakarta.annotation.Priority; @@ -70,7 +72,7 @@ public void filter(ContainerRequestContext requestContext) throws IOException { log.debug("REST call to '{}' intercepted", path); if (LockProtectionMode.OAUTH.equals(appConfiguration.getProtectionMode()) || (appConfiguration.getProtectionMode() == null)) { - Response authorizationResponse = protectionService.processAuthorization(httpHeaders, resourceInfo); + Response authorizationResponse = protectionService.processAuthorization(extractBearerToken(), resourceInfo); boolean success = authorizationResponse == null; AuditLogEntry auditLogEntry = new AuditLogEntry(InetAddressUtility.getIpAddress(httpRequest), AuditActionType.OPENID_AUTHZ_FILTER); @@ -85,6 +87,15 @@ public void filter(ContainerRequestContext requestContext) throws IOException { } } + private String extractBearerToken() { + String authHeader = httpHeaders.getHeaderString(HttpHeaders.AUTHORIZATION); + + if (StringUtils.isEmpty(authHeader)) { + return null; + } + + return authHeader.replaceFirst("(?i)Bearer\\s+", ""); + } private Response unprotectedApiResponse(String name) { return Response.status(Response.Status.UNAUTHORIZED).entity(name + " API not protected") .build(); diff --git a/jans-lock/lock-server/service/src/main/java/io/jans/lock/service/filter/OpenIdProtection.java b/jans-lock/lock-server/service/src/main/java/io/jans/lock/service/filter/OpenIdProtection.java deleted file mode 100644 index ae63cbd7525..00000000000 --- a/jans-lock/lock-server/service/src/main/java/io/jans/lock/service/filter/OpenIdProtection.java +++ /dev/null @@ -1,15 +0,0 @@ -package io.jans.lock.service.filter; - -import jakarta.ws.rs.container.ResourceInfo; -import jakarta.ws.rs.core.HttpHeaders; -import jakarta.ws.rs.core.Response; - -public interface OpenIdProtection { - - Response processAuthorization(HttpHeaders httpHeaders, ResourceInfo resourceInfo); - - public static Response simpleResponse(Response.Status status, String detail) { - return Response.status(status).entity(detail).build(); - } - -} \ No newline at end of file diff --git a/jans-lock/lock-server/service/src/main/java/io/jans/lock/service/grpc/audit/GrpcAuditServiceImpl.java b/jans-lock/lock-server/service/src/main/java/io/jans/lock/service/grpc/audit/GrpcAuditServiceImpl.java new file mode 100644 index 00000000000..062c08c1913 --- /dev/null +++ b/jans-lock/lock-server/service/src/main/java/io/jans/lock/service/grpc/audit/GrpcAuditServiceImpl.java @@ -0,0 +1,188 @@ +/* + * Janssen Project software is available under the Apache License (2004). See http://www.apache.org/licenses/ for full text. + * + * Copyright (c) 2025, Janssen Project + */ + +package io.jans.lock.service.grpc.audit; + +import io.grpc.stub.StreamObserver; +import io.jans.lock.model.audit.HealthEntry; +import io.jans.lock.model.audit.LogEntry; +import io.jans.lock.model.audit.TelemetryEntry; +import io.jans.lock.model.audit.grpc.*; +import io.jans.lock.service.ws.rs.audit.AuditRestWebService; +import jakarta.ws.rs.core.Response; +import org.slf4j.Logger; + +import java.util.ArrayList; +import java.util.List; + +/** + * gRPC service implementation for Audit operations. + * This service acts as a bridge between gRPC and REST endpoints. + * + * Note: This class is NOT a CDI bean because the base class AuditServiceImplBase + * contains final methods which are not proxyable by Weld CDI. + * Instead, it's created by GrpcAuditServiceProvider. + * + * @author Yuriy Movchan + */ +public class GrpcAuditServiceImpl extends AuditServiceGrpc.AuditServiceImplBase { + + private final Logger log; + private final AuditRestWebService auditRestWebService; + private final GrpcToJavaMapper mapper; + + /** + * Constructor for manual instantiation (not CDI). + * + * @param auditRestWebService REST service to delegate to + * @param mapper mapper for proto to Java conversion + * @param log logger instance + */ + public GrpcAuditServiceImpl( + AuditRestWebService auditRestWebService, + GrpcToJavaMapper mapper, + Logger log) { + this.auditRestWebService = auditRestWebService; + this.mapper = mapper; + this.log = log; + } + + @Override + public void processHealth(io.jans.lock.model.audit.grpc.HealthRequest request, + StreamObserver responseObserver) { + log.info("gRPC processHealth called"); + + try { + HealthEntry healthEntry = mapper.toHealthEntry(request.getEntry()); + Response restResponse = auditRestWebService.processHealthRequest(healthEntry, null, null); + + AuditResponse grpcResponse = buildAuditResponse(restResponse); + responseObserver.onNext(grpcResponse); + responseObserver.onCompleted(); + } catch (Exception e) { + log.error("Error processing health request", e); + responseObserver.onError(e); + } + } + + @Override + public void processBulkHealth(BulkHealthRequest request, + StreamObserver responseObserver) { + log.info("gRPC processBulkHealth called with {} entries", request.getEntriesCount()); + + try { + List healthEntries = new ArrayList<>(); + for (io.jans.lock.model.audit.grpc.HealthEntry grpcEntry : request.getEntriesList()) { + healthEntries.add(mapper.toHealthEntry(grpcEntry)); + } + + Response restResponse = auditRestWebService.processBulkHealthRequest(healthEntries, null, null); + + AuditResponse grpcResponse = buildAuditResponse(restResponse); + responseObserver.onNext(grpcResponse); + responseObserver.onCompleted(); + } catch (Exception e) { + log.error("Error processing bulk health request", e); + responseObserver.onError(e); + } + } + + @Override + public void processLog(io.jans.lock.model.audit.grpc.LogRequest request, + StreamObserver responseObserver) { + log.info("gRPC processLog called"); + + try { + LogEntry logEntry = mapper.toLogEntry(request.getEntry()); + Response restResponse = auditRestWebService.processLogRequest(logEntry, null, null); + + AuditResponse grpcResponse = buildAuditResponse(restResponse); + responseObserver.onNext(grpcResponse); + responseObserver.onCompleted(); + } catch (Exception e) { + log.error("Error processing log request", e); + responseObserver.onError(e); + } + } + + @Override + public void processBulkLog(BulkLogRequest request, + StreamObserver responseObserver) { + log.info("gRPC processBulkLog called with {} entries", request.getEntriesCount()); + + try { + List logEntries = new ArrayList<>(); + for (io.jans.lock.model.audit.grpc.LogEntry grpcEntry : request.getEntriesList()) { + logEntries.add(mapper.toLogEntry(grpcEntry)); + } + + Response restResponse = auditRestWebService.processBulkLogRequest(logEntries, null, null); + + AuditResponse grpcResponse = buildAuditResponse(restResponse); + responseObserver.onNext(grpcResponse); + responseObserver.onCompleted(); + } catch (Exception e) { + log.error("Error processing bulk log request", e); + responseObserver.onError(e); + } + } + + @Override + public void processTelemetry(io.jans.lock.model.audit.grpc.TelemetryRequest request, + StreamObserver responseObserver) { + log.info("gRPC processTelemetry called"); + + try { + TelemetryEntry telemetryEntry = mapper.toTelemetryEntry(request.getEntry()); + Response restResponse = auditRestWebService.processTelemetryRequest(telemetryEntry, null, null); + + AuditResponse grpcResponse = buildAuditResponse(restResponse); + responseObserver.onNext(grpcResponse); + responseObserver.onCompleted(); + } catch (Exception e) { + log.error("Error processing telemetry request", e); + responseObserver.onError(e); + } + } + + @Override + public void processBulkTelemetry(BulkTelemetryRequest request, + StreamObserver responseObserver) { + log.info("gRPC processBulkTelemetry called with {} entries", request.getEntriesCount()); + + try { + List telemetryEntries = new ArrayList<>(); + for (io.jans.lock.model.audit.grpc.TelemetryEntry grpcEntry : request.getEntriesList()) { + telemetryEntries.add(mapper.toTelemetryEntry(grpcEntry)); + } + + Response restResponse = auditRestWebService.processBulkTelemetryRequest(telemetryEntries, null, null); + + AuditResponse grpcResponse = buildAuditResponse(restResponse); + responseObserver.onNext(grpcResponse); + responseObserver.onCompleted(); + } catch (Exception e) { + log.error("Error processing bulk telemetry request", e); + responseObserver.onError(e); + } + } + + /** + * Build gRPC AuditResponse from JAX-RS Response. + * + * @param restResponse the JAX-RS response + * @return the gRPC AuditResponse + */ + private AuditResponse buildAuditResponse(Response restResponse) { + boolean success = restResponse.getStatus() >= 200 && restResponse.getStatus() < 300; + String message = restResponse.getEntity() != null ? restResponse.getEntity().toString() : ""; + + return AuditResponse.newBuilder() + .setSuccess(success) + .setMessage(message) + .build(); + } +} \ No newline at end of file diff --git a/jans-lock/lock-server/service/src/main/java/io/jans/lock/service/grpc/audit/GrpcAuditServiceProvider.java b/jans-lock/lock-server/service/src/main/java/io/jans/lock/service/grpc/audit/GrpcAuditServiceProvider.java new file mode 100644 index 00000000000..85a0cfe0572 --- /dev/null +++ b/jans-lock/lock-server/service/src/main/java/io/jans/lock/service/grpc/audit/GrpcAuditServiceProvider.java @@ -0,0 +1,58 @@ +/* + * Janssen Project software is available under the Apache License (2004). See http://www.apache.org/licenses/ for full text. + * + * Copyright (c) 2025, Janssen Project + */ + +package io.jans.lock.service.grpc.audit; + +import org.slf4j.Logger; + +import io.jans.lock.service.ws.rs.audit.AuditRestWebService; +import jakarta.annotation.PostConstruct; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; + +/** + * Provider/Factory bean for GrpcAuditServiceImpl. + * + * This is a workaround for Weld CDI proxy issue with gRPC generated classes + * that contain final methods. Instead of making GrpcAuditServiceImpl a CDI bean, + * we create it in this factory and expose it as ApplicationScoped singleton. + * + * @author Yuriy Movchan + */ +@ApplicationScoped +public class GrpcAuditServiceProvider { + + @Inject + private Logger log; + + @Inject + private AuditRestWebService auditRestWebService; + + @Inject + private GrpcToJavaMapper mapper; + + private GrpcAuditServiceImpl grpcAuditService; + + @PostConstruct + public void init() { + log.debug("Initializing GrpcAuditServiceProvider"); + + // Create the gRPC service implementation + // This avoids CDI trying to proxy the class with final methods + grpcAuditService = new GrpcAuditServiceImpl(auditRestWebService, mapper, log); + + log.info("GrpcAuditServiceImpl created successfully"); + } + + /** + * Get the gRPC audit service instance. + * + * @return GrpcAuditServiceImpl instance + */ + public GrpcAuditServiceImpl getService() { + return grpcAuditService; + } +} diff --git a/jans-lock/lock-server/service/src/main/java/io/jans/lock/service/grpc/audit/GrpcToJavaMapper.java b/jans-lock/lock-server/service/src/main/java/io/jans/lock/service/grpc/audit/GrpcToJavaMapper.java new file mode 100644 index 00000000000..f7e0c6dd30e --- /dev/null +++ b/jans-lock/lock-server/service/src/main/java/io/jans/lock/service/grpc/audit/GrpcToJavaMapper.java @@ -0,0 +1,151 @@ +/* + * Janssen Project software is available under the Apache License (2004). See http://www.apache.org/licenses/ for full text. + * + * Copyright (c) 2025, Janssen Project + */ + +package io.jans.lock.service.grpc.audit; + +import java.time.Instant; +import java.time.format.DateTimeFormatter; +import java.util.Date; +import java.util.HashMap; +import java.util.Map; + +import org.slf4j.Logger; + +import com.google.protobuf.Timestamp; + +import io.jans.lock.model.audit.HealthEntry; +import io.jans.lock.model.audit.LogEntry; +import io.jans.lock.model.audit.TelemetryEntry; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; + +/** + * Mapper class to convert between gRPC proto messages and Java beans. + * + * @author Yuriy Movchan + */ +@ApplicationScoped +public class GrpcToJavaMapper { + + @Inject + private Logger log; + + private static final DateTimeFormatter ISO_FORMATTER = DateTimeFormatter.ISO_OFFSET_DATE_TIME; + + /** + * Convert gRPC HealthEntry to Java HealthEntry bean. + * + * @param grpcEntry the gRPC health entry + * @return the Java health entry bean + */ + public HealthEntry toHealthEntry(io.jans.lock.model.audit.grpc.HealthEntry grpcEntry) { + if (grpcEntry == null) { + return null; + } + + HealthEntry healthEntry = new HealthEntry(); + healthEntry.setCreationDate(toZonedDateTime(grpcEntry.getCreationDate())); + healthEntry.setEventTime(toZonedDateTime(grpcEntry.getEventTime())); + healthEntry.setService(grpcEntry.getService()); + healthEntry.setNodeName(grpcEntry.getNodeName()); + healthEntry.setStatus(grpcEntry.getStatus()); + + // Convert engine status map + if (grpcEntry.getEngineStatusCount() > 0) { + Map engineStatus = new HashMap<>(); + grpcEntry.getEngineStatusMap().forEach(engineStatus::put); + healthEntry.setEngineStatus(engineStatus); + } + + return healthEntry; + } + + /** + * Convert gRPC LogEntry to Java LogEntry bean. + * + * @param grpcEntry the gRPC log entry + * @return the Java log entry bean + */ + public LogEntry toLogEntry(io.jans.lock.model.audit.grpc.LogEntry grpcEntry) { + if (grpcEntry == null) { + return null; + } + + LogEntry logEntry = new LogEntry(); + logEntry.setCreationDate(toZonedDateTime(grpcEntry.getCreationDate())); + logEntry.setEventTime(toZonedDateTime(grpcEntry.getEventTime())); + logEntry.setService(grpcEntry.getService()); + logEntry.setNodeName(grpcEntry.getNodeName()); + logEntry.setEventType(grpcEntry.getEventType()); + logEntry.setSeverityLevel(grpcEntry.getSeverityLevel()); + logEntry.setAction(grpcEntry.getAction()); + logEntry.setDecisionResult(grpcEntry.getDecisionResult()); + logEntry.setRequestedResource(grpcEntry.getRequestedResource()); + logEntry.setPrincipalId(grpcEntry.getPrincipalId()); + logEntry.setClientId(grpcEntry.getClientId()); + logEntry.setJti(grpcEntry.getJti()); + + // Convert context information map + if (grpcEntry.getContextInformationCount() > 0) { + Map contextInfo = new HashMap<>(); + grpcEntry.getContextInformationMap().forEach(contextInfo::put); + logEntry.setContextInformation(contextInfo); + } + + return logEntry; + } + + /** + * Convert gRPC TelemetryEntry to Java TelemetryEntry bean. + * + * @param grpcEntry the gRPC telemetry entry + * @return the Java telemetry entry bean + */ + public TelemetryEntry toTelemetryEntry(io.jans.lock.model.audit.grpc.TelemetryEntry grpcEntry) { + if (grpcEntry == null) { + return null; + } + + TelemetryEntry telemetryEntry = new TelemetryEntry(); + telemetryEntry.setCreationDate(toZonedDateTime(grpcEntry.getCreationDate())); + telemetryEntry.setEventTime(toZonedDateTime(grpcEntry.getEventTime())); + telemetryEntry.setService(grpcEntry.getService()); + telemetryEntry.setNodeName(grpcEntry.getNodeName()); + telemetryEntry.setStatus(grpcEntry.getStatus()); + telemetryEntry.setLastPolicyLoadSize(grpcEntry.getLastPolicyLoadSize()); + telemetryEntry.setPolicySuccessLoadCounter(grpcEntry.getPolicySuccessLoadCounter()); + telemetryEntry.setPolicyFailedLoadCounter(grpcEntry.getPolicyFailedLoadCounter()); + telemetryEntry.setLastPolicyEvaluationTimeNs(grpcEntry.getLastPolicyEvaluationTimeNs()); + telemetryEntry.setAvgPolicyEvaluationTimeNs(grpcEntry.getAvgPolicyEvaluationTimeNs()); + telemetryEntry.setMemoryUsage(grpcEntry.getMemoryUsage()); + telemetryEntry.setEvaluationRequestsCount(grpcEntry.getEvaluationRequestsCount()); + + // Convert policy stats map + if (grpcEntry.getPolicyStatsCount() > 0) { + Map policyStats = new HashMap<>(); + grpcEntry.getPolicyStatsMap().forEach(policyStats::put); + telemetryEntry.setPolicyStats(policyStats); + } + + return telemetryEntry; + } + + /** + * Convert Protobuf Timestamp to Date. + * + * @param timestamp the Protobuf timestamp + * @return the date + */ + private Date toZonedDateTime(Timestamp timestamp) { + if (timestamp == null || (timestamp.getSeconds() == 0 && timestamp.getNanos() == 0)) { + return null; + } + + Instant instant = Instant.ofEpochSecond(timestamp.getSeconds(), timestamp.getNanos()); + + return Date.from(instant); + } +} \ No newline at end of file diff --git a/jans-lock/lock-server/service/src/main/java/io/jans/lock/service/grpc/security/GrpcAuthorizationInterceptor.java b/jans-lock/lock-server/service/src/main/java/io/jans/lock/service/grpc/security/GrpcAuthorizationInterceptor.java new file mode 100644 index 00000000000..e69ca227f9c --- /dev/null +++ b/jans-lock/lock-server/service/src/main/java/io/jans/lock/service/grpc/security/GrpcAuthorizationInterceptor.java @@ -0,0 +1,234 @@ +/* + * Janssen Project software is available under the Apache License (2004). See http://www.apache.org/licenses/ for full text. + * + * Copyright (c) 2025, Janssen Project + */ + +package io.jans.lock.service.grpc.security; + +import java.lang.annotation.Annotation; +import java.lang.reflect.AnnotatedElement; +import java.lang.reflect.Method; +import java.util.Optional; + +import org.apache.commons.lang3.StringUtils; +import org.slf4j.Logger; + +import io.grpc.Context; +import io.grpc.Contexts; +import io.grpc.Metadata; +import io.grpc.ServerCall; +import io.grpc.ServerCallHandler; +import io.grpc.ServerInterceptor; +import io.grpc.Status; +import io.jans.lock.cedarling.service.CedarlingProtection; +import io.jans.lock.model.app.audit.AuditActionType; +import io.jans.lock.model.app.audit.AuditLogEntry; +import io.jans.lock.model.config.AppConfiguration; +import io.jans.lock.model.config.LockProtectionMode; +import io.jans.lock.service.app.audit.ApplicationAuditLogger; +import io.jans.lock.service.openid.OpenIdProtection; +import io.jans.lock.service.ws.rs.audit.AuditRestWebService; +import io.jans.lock.util.HeaderUtils; +import io.jans.lock.util.ServerUtil; +import io.jans.service.security.api.ProtectedApi; +import io.jans.service.security.protect.BaseAuthorizationProtection; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; +import jakarta.ws.rs.container.ResourceInfo; +import jakarta.ws.rs.core.Response; + +/** + * gRPC Server Interceptor for authorization. + * This is the gRPC equivalent of AuthorizationProcessingFilter for REST. + * + * @author Yuriy Movchan + */ +@ApplicationScoped +public class GrpcAuthorizationInterceptor implements ServerInterceptor { + + @Inject + private Logger log; + + @Inject + private AppConfiguration appConfiguration; + + @Inject + private OpenIdProtection openIdProtectionService; + + @Inject + private CedarlingProtection cedarlingProtectionService; + + @Inject + private ApplicationAuditLogger applicationAuditLogger; + + @Override + public ServerCall.Listener interceptCall( + ServerCall call, + Metadata headers, + ServerCallHandler next) { + + String methodName = call.getMethodDescriptor().getFullMethodName(); + log.debug("gRPC call to '{}' intercepted", methodName); + + BaseAuthorizationProtection authorizationProtection = null; + if (LockProtectionMode.OAUTH.equals(appConfiguration.getProtectionMode())) { + log.debug("OAuth protection is enabled"); + + authorizationProtection = openIdProtectionService; + } else if (LockProtectionMode.CEDARLING.equals(appConfiguration.getProtectionMode())) { + log.debug("Cedarling protection is enabled"); + + authorizationProtection = cedarlingProtectionService; + } else { + // Send error if protection mode is not selected + call.close(Status.PERMISSION_DENIED + .withDescription("Authorization error"), new Metadata()); + + return new ServerCall.Listener() {}; + } + + try { + // Extract method-specific ResourceInfo + ResourceInfo resourceInfo = extractResourceInfo(methodName); + log.debug("gRPC call requires access to: {}", resourceInfo); + + String clientIp = ServerUtil.getGrpcClientIpAddress(call, headers); + Context context = ServerUtil.setClientContextIpAddress(clientIp); + + // Process authorization + Response authorizationResponse = authorizationProtection.processAuthorization(HeaderUtils.findAndExtractBearerToken(headers), resourceInfo); + boolean success = authorizationResponse == null; + + // Audit logging + AuditLogEntry auditLogEntry = new AuditLogEntry(clientIp, AuditActionType.GRPC_AUTHZ_FILTER); + applicationAuditLogger.log(auditLogEntry, success); + + if (!success) { + log.warn("Authorization failed for gRPC call '{}': {}", methodName, authorizationResponse.getEntity()); + + // Map HTTP status to gRPC status + Status grpcStatus = mapHttpStatusToGrpcStatus(authorizationResponse.getStatusInfo().toEnum()); + call.close(grpcStatus.withDescription(String.valueOf(authorizationResponse.getEntity())), new Metadata()); + + return new ServerCall.Listener() {}; + } + + log.debug("Authorization passed for gRPC call '{}'", methodName); + + return Contexts.interceptCall(context, call, headers, next); + } catch (Exception e) { + log.error("Error during gRPC authorization for '{}'", methodName, e); + + call.close(Status.INTERNAL + .withDescription("Authorization error: " + e.getMessage()) + .withCause(e), new Metadata()); + + return new ServerCall.Listener() {}; + } + } + + /** + * Map HTTP status codes to gRPC status codes. + * + * @param httpStatus HTTP response status + * @return corresponding gRPC Status + */ + private Status mapHttpStatusToGrpcStatus(Response.Status httpStatus) { + if (httpStatus == null) { + return Status.INTERNAL; + } + + switch (httpStatus) { + case UNAUTHORIZED: + return Status.UNAUTHENTICATED; + case FORBIDDEN: + return Status.PERMISSION_DENIED; + case BAD_REQUEST: + return Status.INVALID_ARGUMENT; + case NOT_FOUND: + return Status.NOT_FOUND; + case INTERNAL_SERVER_ERROR: + return Status.INTERNAL; + case SERVICE_UNAVAILABLE: + return Status.UNAVAILABLE; + default: + return Status.UNKNOWN; + } + } + + /** + * Extract target resource for the gRPC method. + * + * @param methodName full gRPC method name (e.g., "io.jans.lock.audit.AuditService/ProcessHealth") + * @return requested resource info + */ + private ResourceInfo extractResourceInfo(String methodName) { + // Parse method name: "package.Service/Method" + String[] parts = methodName.split("/"); + if (parts.length != 2) { + log.warn("Invalid gRPC method name format: {}", methodName); + return null; + } + + String method = parts[1]; + + // Map gRPC methods to ResourceInfo (same as REST API) + Optional resourceInfo = getProtectionApiMethod(AuditRestWebService.class, method); + if (resourceInfo.isPresent()) { + return resourceInfo.get(); + } + + log.warn("No ResourceInfo found for gRPC method: {}", methodName); + return null; + } + + private Optional getProtectionApiMethod(Class clazz, String grpcMethodName) { + for (Method method : clazz.getMethods()) { + Optional protectedApi = getProtectedApiAnnotation(method); + if (protectedApi.isPresent()) { + if (grpcMethodName.equals(protectedApi.get().grpcMethodName())) { + GrpcResourceInfo grpcResourceInfo = new GrpcResourceInfo(clazz, method); + return Optional.ofNullable(grpcResourceInfo); + } + } + } + + return Optional.empty(); + } + + private Optional getProtectedApiAnnotation(AnnotatedElement elem) { + Optional protectedApi = optAnnnotation(elem, ProtectedApi.class); + return protectedApi; + } + + private static Optional optAnnnotation(AnnotatedElement elem, Class cls) { + return Optional.ofNullable(elem.getAnnotation(cls)); + } + + static class GrpcResourceInfo implements ResourceInfo { + + private Class clazz; + private Method method; + + public GrpcResourceInfo( Class clazz, Method method) { + this.clazz = clazz; + this.method = method; + } + + @Override + public Method getResourceMethod() { + return method; + } + + @Override + public Class getResourceClass() { + return clazz; + } + + @Override + public String toString() { + return "GrpcResourceInfo [clazz=" + clazz + ", method=" + method + "]"; + } + } +} \ No newline at end of file diff --git a/jans-lock/lock-server/service/src/main/java/io/jans/lock/service/grpc/server/GrpcServerStarter.java b/jans-lock/lock-server/service/src/main/java/io/jans/lock/service/grpc/server/GrpcServerStarter.java new file mode 100644 index 00000000000..24c022a6aba --- /dev/null +++ b/jans-lock/lock-server/service/src/main/java/io/jans/lock/service/grpc/server/GrpcServerStarter.java @@ -0,0 +1,178 @@ +/* + * Janssen Project software is available under the Apache License (2004). See http://www.apache.org/licenses/ for full text. + * + * Copyright (c) 2025, Janssen Project + */ +package io.jans.lock.service.grpc.server; + +import java.io.File; +import java.io.IOException; +import java.util.concurrent.TimeUnit; + +import org.slf4j.Logger; + +import io.grpc.Server; +import io.grpc.ServerBuilder; +import io.jans.lock.model.config.AppConfiguration; +import io.jans.lock.model.config.GrpcServerMode; +import io.jans.lock.model.config.grpc.GrpcConfiguration; +import io.jans.lock.service.grpc.audit.GrpcAuditServiceProvider; +import io.jans.lock.service.grpc.security.GrpcAuthorizationInterceptor; +import jakarta.annotation.PreDestroy; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; + +/** + * Configuration and lifecycle management for gRPC server. + * + * Added: Netty-based TLS/ALPN support when enabled. + * + * @author Yuriy Movchan + */ +@ApplicationScoped +public class GrpcServerStarter { + + @Inject + private Logger log; + + @Inject + private AppConfiguration appConfiguration; + + @Inject + private GrpcAuditServiceProvider grpcAuditServiceProvider; + + @Inject + private GrpcAuthorizationInterceptor authorizationInterceptor; + + private Server grpcServer; + + public void initGrpcServer() { + try { + if (grpcServer == null) { + startGrpcServer(); + } else { + log.warn("gRPC server is already started"); + } + } catch (IOException e) { + log.error("Failed to start gRPC server", e); + } + } + + /** + * Start the gRPC server with authorization interceptor. + * Uses NettyServerBuilder with SslContext when TLS is enabled, otherwise falls back to plain ServerBuilder. + * + * @throws IOException if server fails to start + */ + private void startGrpcServer() throws IOException { + GrpcConfiguration grpcConfiguration = appConfiguration.getGrpcConfiguration(); + if (grpcConfiguration == null || grpcConfiguration.getServerMode() == null || + !(GrpcServerMode.PLAIN_SERVER == grpcConfiguration.getServerMode() || GrpcServerMode.TLS_SERVER == grpcConfiguration.getServerMode())) { + log.info("gRPC inproc server was disabled in configuration"); + return; + } + + if (GrpcServerMode.TLS_SERVER == grpcConfiguration.getServerMode()) { + // Use Netty-based server with TLS/ALPN + try { + // Use shaded Netty classes bundled with gRPC + io.grpc.netty.shaded.io.netty.handler.ssl.SslContext sslContext = + buildSslContext(grpcConfiguration.getTlsCertChainFilePath(), grpcConfiguration.getTlsPrivateKeyFilePath()); + + io.grpc.netty.shaded.io.grpc.netty.NettyServerBuilder nettyBuilder = + io.grpc.netty.shaded.io.grpc.netty.NettyServerBuilder.forPort(grpcConfiguration.getGrpcPort()) + .sslContext(sslContext) + .addService(grpcAuditServiceProvider.getService()) // Get service from provider + .intercept(authorizationInterceptor); // Add authorization interceptor + + grpcServer = nettyBuilder.build().start(); + + log.info("gRPC (Netty) server started on port {} with TLS/ALPN and authorization enabled", grpcConfiguration.getGrpcPort()); + } catch (Exception e) { + log.error("Failed to start Netty-based gRPC server with TLS", e); + throw new IOException("Failed to start Netty-based gRPC server with TLS", e); + } + } else { + grpcServer = ServerBuilder.forPort(grpcConfiguration.getGrpcPort()) + .addService(grpcAuditServiceProvider.getService()) // Get service from provider + .intercept(authorizationInterceptor) // Add authorization interceptor + .build() + .start(); + + log.info("gRPC server started on port {} with authorization enabled", grpcConfiguration.getGrpcPort()); + } + + // Add shutdown hook + Runtime.getRuntime().addShutdownHook(new Thread(() -> { + log.info("Shutting down gRPC server due to JVM shutdown"); + stopGrpcServer(); + })); + } + + /** + * Build SslContext for server from PEM cert chain and private key files. + * Uses gRPC-shaded Netty's SslContextBuilder so ALPN is properly configured for HTTP/2. + */ + private io.grpc.netty.shaded.io.netty.handler.ssl.SslContext buildSslContext(String certChainPath, String privateKeyPath) throws Exception { + if (certChainPath == null || privateKeyPath == null) { + throw new IllegalArgumentException("TLS is enabled but cert chain or private key path is not set"); + } + + File certChainFile = new File(certChainPath); + File privateKeyFile = new File(privateKeyPath); + + if (!certChainFile.exists() || !privateKeyFile.exists()) { + throw new IllegalArgumentException("TLS certificate chain or private key file not found"); + } + + io.grpc.netty.shaded.io.netty.handler.ssl.SslContextBuilder sslCtxBuilder = + io.grpc.netty.shaded.io.netty.handler.ssl.SslContextBuilder.forServer(certChainFile, privateKeyFile); + + // Let Netty choose optimal SslProvider (OpenSSL/JDK) and enable ALPN for HTTP/2 automatically. + return sslCtxBuilder.build(); + } + + /** + * Stop the gRPC server. + * + * @throws InterruptedException if shutdown is interrupted + */ + @PreDestroy + public void stopGrpcServer() { + if (grpcServer != null) { + log.info("Stopping gRPC server..."); + grpcServer.shutdown(); + + try { + if (!grpcServer.awaitTermination(30, TimeUnit.SECONDS)) { + log.warn("gRPC server did not terminate gracefully, forcing shutdown"); + grpcServer.shutdownNow(); + + if (!grpcServer.awaitTermination(10, TimeUnit.SECONDS)) { + log.error("gRPC server did not terminate"); + } + } + + log.info("gRPC server stopped"); + } catch (InterruptedException e) { + log.error("gRPC server shutdown was interrupted", e); + // Restore interrupt status + Thread.currentThread().interrupt(); + // Force shutdown + grpcServer.shutdownNow(); + } + } + } + + /** + * Block until the server shuts down. + * + * @throws InterruptedException if waiting is interrupted + */ + public void blockUntilShutdown() throws InterruptedException { + if (grpcServer != null) { + grpcServer.awaitTermination(); + } + } + +} diff --git a/jans-lock/lock-server/service/src/main/java/io/jans/lock/service/grpc/servlet/GrpcAuditServlet.java b/jans-lock/lock-server/service/src/main/java/io/jans/lock/service/grpc/servlet/GrpcAuditServlet.java new file mode 100644 index 00000000000..ee97520863e --- /dev/null +++ b/jans-lock/lock-server/service/src/main/java/io/jans/lock/service/grpc/servlet/GrpcAuditServlet.java @@ -0,0 +1,263 @@ +/* + * Janssen Project software is available under the Apache License (2004). See http://www.apache.org/licenses/ for full text. + * + * Copyright (c) 2026, Janssen Project + */ +package io.jans.lock.service.grpc.servlet; + +import java.io.IOException; +import java.util.Collections; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.TreeMap; + +import org.slf4j.Logger; + +import io.grpc.BindableService; +import io.grpc.ServerInterceptors; +import io.grpc.ServerServiceDefinition; +import io.grpc.servlet.jakarta.ServletAdapter; +import io.grpc.servlet.jakarta.ServletServerBuilder; +import io.jans.lock.model.config.AppConfiguration; +import io.jans.lock.model.config.GrpcServerMode; +import io.jans.lock.model.config.grpc.GrpcConfiguration; +import io.jans.lock.service.grpc.audit.GrpcAuditServiceProvider; +import io.jans.lock.service.grpc.security.GrpcAuthorizationInterceptor; +import jakarta.annotation.PostConstruct; +import jakarta.inject.Inject; +import jakarta.servlet.ServletException; +import jakarta.servlet.annotation.WebServlet; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletRequestWrapper; +import jakarta.servlet.http.HttpServletResponse; + +/** + * gRPC servlet for gRPC bridge + * + * Added: Netty-based TLS/ALPN support when enabled. + * + * @author Yuriy Movchan + */ +@WebServlet( + name = "GrpcAuditServlet", + urlPatterns = {"/"}, // if you want gRPC at the root — ok, but make sure REST endpoints use more specific paths + asyncSupported = true, + loadOnStartup = 10 +) +public class GrpcAuditServlet extends HttpServlet { + + private static final long serialVersionUID = -5675524890589330190L; + + @Inject + private Logger log; + + @Inject + private AppConfiguration appConfiguration; + + @Inject + private GrpcAuditServiceProvider grpcAuditServiceProvider; + + @Inject + private GrpcAuthorizationInterceptor authorizationInterceptor; + + private volatile ServletAdapter adapter = null; + + public GrpcAuditServlet() { + } + + @PostConstruct + public void initializeGrpc() { + log.info("gRPC adapter initializion"); + GrpcConfiguration grpcConfiguration = appConfiguration.getGrpcConfiguration(); + if (grpcConfiguration == null || grpcConfiguration.getServerMode() == null || GrpcServerMode.BRIDGE != grpcConfiguration.getServerMode()) { + log.info("gRPC server bridge was disabled in configuration"); + return; + } + + try { + BindableService rawService = grpcAuditServiceProvider.getService(); + if (rawService == null) { + throw new ServletException("GrpcAuditServiceProvider returned null service"); + } + + // Wrap the service with the authorization interceptor + ServerServiceDefinition wrapped = ServerInterceptors.intercept(rawService, authorizationInterceptor); + + ServletServerBuilder builder = new ServletServerBuilder(); + builder.addService(wrapped); + + builder.maxInboundMessageSize(10 * 1024 * 1024); + // builder.maxInboundMetadataSize(8192); // if you have a lot of metadata + // builder.executor(Executors.newCachedThreadPool()); // custom executor if needed (default is ForkJoinPool) + + this.adapter = builder.buildServletAdapter(); + + log.info("gRPC adapter initialized successfully with authorization enabled for service: " + + rawService.getClass().getSimpleName()); + } catch (Exception e) { + log.error("Failed to initialize gRPC servlet", e); + } + } + + @Override + protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException { + ServletAdapter localAdapter = this.adapter; + if (localAdapter != null) { + // Wrap request for header normalization and context path manipulation + HttpServletRequest wrappedRequest = new GrpcRequestWrapper(req); + localAdapter.doPost(wrappedRequest, resp); + } else { + resp.sendError(HttpServletResponse.SC_SERVICE_UNAVAILABLE, "gRPC not initialized yet"); + } + } + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException { + ServletAdapter localAdapter = this.adapter; + if (localAdapter != null) { + // Wrap request for header normalization and context path manipulation + HttpServletRequest wrappedRequest = new GrpcRequestWrapper(req); + localAdapter.doGet(wrappedRequest, resp); + } else { + resp.sendError(HttpServletResponse.SC_SERVICE_UNAVAILABLE, "gRPC not initialized yet"); + } + } + + @Override + public void destroy() { + ServletAdapter localAdapter = this.adapter; + if (localAdapter != null) { + try { + localAdapter.destroy(); + log.info("gRPC adapter destroyed"); + } catch (Exception e) { + log.warn("Error during gRPC adapter destroy", e); + } + } + super.destroy(); + } + + /** + * /** Wrapper for gRPC requests: 1. Normalizes headers to lowercase 2. Removes + * context path from URI for gRPC bridge + */ + private static class GrpcRequestWrapper extends HttpServletRequestWrapper { + + private final Map> headers; + private final String requestURI; + private final String servletPath; + + public GrpcRequestWrapper(HttpServletRequest request) { + super(request); + + // 1. Normalize headers + this.headers = new TreeMap<>(String.CASE_INSENSITIVE_ORDER); + Enumeration headerNames = request.getHeaderNames(); + while (headerNames.hasMoreElements()) { + String name = headerNames.nextElement(); + List values = Collections.list(request.getHeaders(name)); + headers.put(name.toLowerCase(), values); + } + + // 2. Remove context path from URI + // Was: /jans-lock/io.jans.lock.audit.AuditService/ProcessLog + // Becomes: /io.jans.lock.audit.AuditService/ProcessLog + String originalURI = request.getRequestURI(); + String contextPath = request.getContextPath(); + + if (contextPath != null && !contextPath.isEmpty() && originalURI.startsWith(contextPath)) { + this.requestURI = originalURI.substring(contextPath.length()); + this.servletPath = this.requestURI; + } else { + this.requestURI = originalURI; + this.servletPath = request.getServletPath(); + } + } + + // === Override methods for paths === + + @Override + public String getRequestURI() { + return requestURI; + } + + @Override + public String getServletPath() { + return servletPath; + } + + @Override + public String getPathInfo() { + // gRPC usually uses requestURI, but just in case + return null; + } + + @Override + public String getContextPath() { + // Return empty context path for gRPC + return ""; + } + + @Override + public StringBuffer getRequestURL() { + StringBuffer url = new StringBuffer(); + url.append(getScheme()).append("://").append(getServerName()); + + int port = getServerPort(); + if ((getScheme().equals("http") && port != 80) || (getScheme().equals("https") && port != 443)) { + url.append(':').append(port); + } + + url.append(getRequestURI()); + return url; + } + + // === Override methods for headers === + + @Override + public String getHeader(String name) { + List values = headers.get(name.toLowerCase()); + return values != null && !values.isEmpty() ? values.get(0) : null; + } + + @Override + public Enumeration getHeaders(String name) { + List values = headers.get(name.toLowerCase()); + return values != null ? Collections.enumeration(values) : Collections.enumeration(Collections.emptyList()); + } + + @Override + public Enumeration getHeaderNames() { + return Collections.enumeration(headers.keySet()); + } + + @Override + public long getDateHeader(String name) { + String value = getHeader(name); + if (value == null) { + return -1L; + } + try { + return super.getDateHeader(name); + } catch (IllegalArgumentException e) { + return -1L; + } + } + + @Override + public int getIntHeader(String name) { + String value = getHeader(name); + if (value == null) { + return -1; + } + try { + return Integer.parseInt(value); + } catch (NumberFormatException e) { + return -1; + } + } + } +} \ No newline at end of file diff --git a/jans-lock/lock-server/service/src/main/java/io/jans/lock/service/openid/OpenIdProtection.java b/jans-lock/lock-server/service/src/main/java/io/jans/lock/service/openid/OpenIdProtection.java new file mode 100644 index 00000000000..475248039b8 --- /dev/null +++ b/jans-lock/lock-server/service/src/main/java/io/jans/lock/service/openid/OpenIdProtection.java @@ -0,0 +1,21 @@ +/* + * Janssen Project software is available under the Apache License (2004). See http://www.apache.org/licenses/ for full text. + * + * Copyright (c) 2022, Janssen Project + */ + +package io.jans.lock.service.openid; + +import io.jans.service.security.protect.BaseAuthorizationProtection; +import jakarta.ws.rs.container.ResourceInfo; +import jakarta.ws.rs.core.Response; + +public interface OpenIdProtection extends BaseAuthorizationProtection { + + Response processAuthorization(String bearerToken, ResourceInfo resourceInfo); + + public static Response simpleResponse(Response.Status status, String detail) { + return Response.status(status).entity(detail).build(); + } + +} \ No newline at end of file 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/openid/OpenIdProtectionService.java similarity index 93% rename from jans-lock/lock-server/service/src/main/java/io/jans/lock/service/filter/openid/OpenIdProtectionService.java rename to jans-lock/lock-server/service/src/main/java/io/jans/lock/service/openid/OpenIdProtectionService.java index 4e9e4724a97..c1d38d444d5 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/openid/OpenIdProtectionService.java @@ -1,4 +1,4 @@ -package io.jans.lock.service.filter.openid; +package io.jans.lock.service.openid; import static jakarta.ws.rs.core.Response.Status.FORBIDDEN; import static jakarta.ws.rs.core.Response.Status.INTERNAL_SERVER_ERROR; @@ -33,13 +33,11 @@ import io.jans.as.model.jwt.JwtClaimName; import io.jans.as.model.jwt.JwtClaims; import io.jans.lock.service.OpenIdService; -import io.jans.lock.service.filter.OpenIdProtection; import io.jans.service.security.api.ProtectedApi; import jakarta.annotation.PostConstruct; import jakarta.enterprise.context.ApplicationScoped; import jakarta.inject.Inject; import jakarta.ws.rs.container.ResourceInfo; -import jakarta.ws.rs.core.HttpHeaders; import jakarta.ws.rs.core.Response; @ApplicationScoped @@ -76,14 +74,13 @@ private void init() { *

Accepts either a JWT or an opaque token. For opaque tokens, performs token introspection. For JWTs, validates issuer, expiration, * cryptographic signature (HMAC-signed tokens are rejected), and required scopes for the target resource.

* - * @param headers HTTP headers containing the Authorization header + * @param bearerToken Authorization bearer token * @param resourceInfo information about the target resource used to determine required scopes * @return a Response describing the authorization failure (UNAUTHORIZED, FORBIDDEN, or INTERNAL_SERVER_ERROR) or `null` if authorization succeeds */ - public Response processAuthorization(HttpHeaders headers, ResourceInfo resourceInfo) { + public Response processAuthorization(String bearerToken, ResourceInfo resourceInfo) { try { - String token = headers.getHeaderString(HttpHeaders.AUTHORIZATION); - boolean authFound = StringUtils.isNotEmpty(token); + boolean authFound = StringUtils.isNotEmpty(bearerToken); log.debug("Authorization header{} found", authFound ? "" : " not"); if (!authFound) { @@ -92,18 +89,18 @@ public Response processAuthorization(HttpHeaders headers, ResourceInfo resourceI return simpleResponse(UNAUTHORIZED, "No authorization header found"); } - token = token.replaceFirst("Bearer\\s+",""); + bearerToken = bearerToken.replaceFirst("Bearer\\s+",""); log.debug("Validating bearer token"); List scopes = getRequestedScopes(resourceInfo); log.debug("Call requires scopes: {}", scopes); - Jwt jwt = tokenAsJwt(token); + Jwt jwt = tokenAsJwt(bearerToken); if (jwt == null) { // Do standard token validation IntrospectionResponse iresp = null; try { - iresp = introspectionService.introspectToken("Bearer " + token, token); + iresp = introspectionService.introspectToken("Bearer " + bearerToken, bearerToken); } catch (Exception e) { log.error(e.getMessage()); } 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 d503cecb0c5..e901d4c5555 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 @@ -32,7 +32,7 @@ import io.swagger.v3.oas.annotations.responses.ApiResponses; import io.swagger.v3.oas.annotations.security.SecurityRequirement; import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; +import jakarta.ws.rs.Consumes; import jakarta.ws.rs.POST; import jakarta.ws.rs.Path; import jakarta.ws.rs.Produces; @@ -41,6 +41,8 @@ import jakarta.ws.rs.core.Response; import jakarta.ws.rs.core.SecurityContext; +import java.util.List; + /** * Provides interface for audit REST web services * @@ -60,10 +62,11 @@ public interface AuditRestWebService { @ApiResponse(responseCode = "500", description = "InternalServerError", content = @Content(mediaType = MediaType.APPLICATION_JSON, schema = @Schema(implementation = LockApiError.class, description = "InternalServerError"))), }) @POST @Path("/health") + @Consumes({ MediaType.APPLICATION_JSON }) @Produces({ MediaType.APPLICATION_JSON }) - @ProtectedApi(scopes = { ApiAccessConstants.LOCK_HEALTH_WRITE_ACCESS }) + @ProtectedApi(scopes = { ApiAccessConstants.LOCK_HEALTH_WRITE_ACCESS }, grpcMethodName = "ProcessHealth") @ProtectedCedarlingApi(action = "Jans::Action::\"POST\"", resource = "Jans::HTTP_Request", id="lock_audit_health_write", path="/audit/health") - Response processHealthRequest(@Context HttpServletRequest request, + Response processHealthRequest(HealthEntry healthEntry, @Context HttpServletRequest request, @Context SecurityContext sec); @Operation(summary = "Bulk save health data", description = "Bulk save health data", tags = { @@ -77,10 +80,11 @@ Response processHealthRequest(@Context HttpServletRequest request, @ApiResponse(responseCode = "500", description = "InternalServerError", content = @Content(mediaType = MediaType.APPLICATION_JSON, schema = @Schema(implementation = LockApiError.class, description = "InternalServerError"))), }) @POST @Path("/health/bulk") + @Consumes({ MediaType.APPLICATION_JSON }) @Produces({ MediaType.APPLICATION_JSON }) - @ProtectedApi(scopes = { ApiAccessConstants.LOCK_HEALTH_WRITE_ACCESS }) - @ProtectedCedarlingApi(action = "Jans::Action::\"POST\"", resource = "Jans::HTTP_Request", id="lock_audit_health_write", path="/audit/health") - Response processBulkHealthRequest(@Context HttpServletRequest request, + @ProtectedApi(scopes = { ApiAccessConstants.LOCK_HEALTH_WRITE_ACCESS }, grpcMethodName = "ProcessBulkHealth") + @ProtectedCedarlingApi(action = "Jans::Action::\"POST\"", resource = "Jans::HTTP_Request", id="lock_audit_health_write", path="/audit/health/bulk") + Response processBulkHealthRequest(List healthEntries, @Context HttpServletRequest request, @Context SecurityContext sec); @Operation(summary = "Save log data", description = "Save log data", tags = { @@ -94,10 +98,11 @@ Response processBulkHealthRequest(@Context HttpServletRequest request, @ApiResponse(responseCode = "500", description = "InternalServerError", content = @Content(mediaType = MediaType.APPLICATION_JSON, schema = @Schema(implementation = LockApiError.class, description = "InternalServerError"))), }) @POST @Path("/log") + @Consumes({ MediaType.APPLICATION_JSON }) @Produces({ MediaType.APPLICATION_JSON }) - @ProtectedApi(scopes = { ApiAccessConstants.LOCK_LOG_WRITE_ACCESS }) + @ProtectedApi(scopes = { ApiAccessConstants.LOCK_LOG_WRITE_ACCESS }, grpcMethodName = "ProcessLog") @ProtectedCedarlingApi(action = "Jans::Action::\"POST\"", resource = "Jans::HTTP_Request", id="lock_audit_log_write", path="/audit/log") - Response processLogRequest(@Context HttpServletRequest request, + Response processLogRequest(LogEntry logEntry, @Context HttpServletRequest request, @Context SecurityContext sec); @Operation(summary = "Bulk save log data", description = "Bulk save log data", tags = { @@ -111,10 +116,11 @@ Response processLogRequest(@Context HttpServletRequest request, @ApiResponse(responseCode = "500", description = "InternalServerError", content = @Content(mediaType = MediaType.APPLICATION_JSON, schema = @Schema(implementation = LockApiError.class, description = "InternalServerError"))), }) @POST @Path("/log/bulk") + @Consumes({ MediaType.APPLICATION_JSON }) @Produces({ MediaType.APPLICATION_JSON }) - @ProtectedApi(scopes = { ApiAccessConstants.LOCK_LOG_WRITE_ACCESS }) - @ProtectedCedarlingApi(action = "Jans::Action::\"POST\"", resource = "Jans::HTTP_Request", id="lock_audit_log_write", path="/audit/log") - Response processBulkLogRequest(@Context HttpServletRequest request, + @ProtectedApi(scopes = { ApiAccessConstants.LOCK_LOG_WRITE_ACCESS }, grpcMethodName = "ProcessBulkLog") + @ProtectedCedarlingApi(action = "Jans::Action::\"POST\"", resource = "Jans::HTTP_Request", id="lock_audit_log_write", path="/audit/log/bulk") + Response processBulkLogRequest(List logEntries, @Context HttpServletRequest request, @Context SecurityContext sec); @Operation(summary = "Save telemetry data", description = "Save telemetry data", tags = { @@ -128,10 +134,11 @@ Response processBulkLogRequest(@Context HttpServletRequest request, @ApiResponse(responseCode = "500", description = "InternalServerError", content = @Content(mediaType = MediaType.APPLICATION_JSON, schema = @Schema(implementation = LockApiError.class, description = "InternalServerError"))), }) @POST @Path("/telemetry") + @Consumes({ MediaType.APPLICATION_JSON }) @Produces({ MediaType.APPLICATION_JSON }) - @ProtectedApi(scopes = { ApiAccessConstants.LOCK_TELEMETRY_WRITE_ACCESS }) + @ProtectedApi(scopes = { ApiAccessConstants.LOCK_TELEMETRY_WRITE_ACCESS }, grpcMethodName = "ProcessTelemetry") @ProtectedCedarlingApi(action = "Jans::Action::\"POST\"", resource = "Jans::HTTP_Request", id="lock_audit_telemetry_write", path="/audit/telemetry") - Response processTelemetryRequest(@Context HttpServletRequest request, + Response processTelemetryRequest(TelemetryEntry telemetryEntry, @Context HttpServletRequest request, @Context SecurityContext sec); @Operation(summary = "Bulk save telemetry data", description = "Bulk save telemetry data", tags = { @@ -145,10 +152,11 @@ Response processTelemetryRequest(@Context HttpServletRequest request, @ApiResponse(responseCode = "500", description = "InternalServerError", content = @Content(mediaType = MediaType.APPLICATION_JSON, schema = @Schema(implementation = LockApiError.class, description = "InternalServerError"))), }) @POST @Path("/telemetry/bulk") + @Consumes({ MediaType.APPLICATION_JSON }) @Produces({ MediaType.APPLICATION_JSON }) - @ProtectedApi(scopes = { ApiAccessConstants.LOCK_TELEMETRY_WRITE_ACCESS }) - @ProtectedCedarlingApi(action = "Jans::Action::\"POST\"", resource = "Jans::HTTP_Request", id="lock_audit_telemetry_write", path="/audit/telemetry") - Response processBulkTelemetryRequest(@Context HttpServletRequest request, + @ProtectedApi(scopes = { ApiAccessConstants.LOCK_TELEMETRY_WRITE_ACCESS }, grpcMethodName = "ProcessBulkTelemetry") + @ProtectedCedarlingApi(action = "Jans::Action::\"POST\"", resource = "Jans::HTTP_Request", id="lock_audit_telemetry_write", path="/audit/telemetry/bulk") + Response processBulkTelemetryRequest(List telemetryEntries, @Context HttpServletRequest request, @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 c7f057d4db5..4061c60b89b 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 @@ -16,14 +16,11 @@ package io.jans.lock.service.ws.rs.audit; -import java.io.IOException; import java.util.List; import org.apache.http.entity.ContentType; import org.slf4j.Logger; -import com.fasterxml.jackson.databind.JsonNode; - import io.jans.lock.model.AuditEndpointType; import io.jans.lock.model.app.audit.AuditActionType; import io.jans.lock.model.app.audit.AuditLogEntry; @@ -33,7 +30,6 @@ import io.jans.lock.model.config.AppConfiguration; import io.jans.lock.model.config.AuditPersistenceMode; import io.jans.lock.service.AuditService; -import io.jans.lock.service.DataMapperService; import io.jans.lock.service.app.audit.ApplicationAuditLogger; import io.jans.lock.service.audit.AuditForwarderService; import io.jans.lock.service.stat.StatService; @@ -57,8 +53,6 @@ @Dependent public class AuditRestWebServiceImpl extends BaseResource implements AuditRestWebService { - 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"; @@ -71,9 +65,6 @@ public class AuditRestWebServiceImpl extends BaseResource implements AuditRestWe @Inject private AppConfiguration appConfiguration; - @Inject - private DataMapperService dataMapperService; - @Inject private JsonService jsonService; @@ -90,22 +81,27 @@ public class AuditRestWebServiceImpl extends BaseResource implements AuditRestWe private ApplicationAuditLogger applicationAuditLogger; /** - * Processes an incoming health audit request and delegates handling to the - * audit processor. + * Processes an incoming health audit request with typed HealthEntry bean. * - * @return a Response containing the HTTP response to return for the health - * audit request + * @param healthEntry the health entry bean + * @param request the HTTP servlet request + * @param sec the security context + * @return a Response containing the HTTP response to return for the health audit request */ @Override - public Response processHealthRequest(HttpServletRequest request, SecurityContext sec) { - log.info("Processing Health request - request: {}", request); + public Response processHealthRequest(HealthEntry healthEntry, HttpServletRequest request, SecurityContext sec) { + log.info("Processing Health request - healthEntry: {}", healthEntry); - AuditLogEntry auditLogEntry = new AuditLogEntry(InetAddressUtility.getIpAddress(request), + if (healthEntry == null) { + throwBadRequestException("Health entry is required"); + } + + AuditLogEntry auditLogEntry = new AuditLogEntry(getClientIpAddress(request), AuditActionType.AUDIT_HEALTH_WRITE); Response response = null; try { - response = processAuditRequest(request, AuditEndpointType.HEALTH); + response = processAuditRequest(healthEntry, AuditEndpointType.HEALTH); } finally { applicationAuditLogger.log(auditLogEntry, getResponseResult(response)); } @@ -114,24 +110,27 @@ public Response processHealthRequest(HttpServletRequest request, SecurityContext } /** - * Handles incoming bulk health audit requests. - * - * Produces a Response representing the outcome of processing the bulk health - * audit payload. + * Handles incoming bulk health audit requests with typed list of HealthEntry beans. * - * @return the Response representing the outcome of processing the bulk health - * audit request + * @param healthEntries list of health entry beans + * @param request the HTTP servlet request + * @param sec the security context + * @return the Response representing the outcome of processing the bulk health audit request */ @Override - public Response processBulkHealthRequest(HttpServletRequest request, SecurityContext sec) { - log.info("Processing Bulk Health request - request: {}", request); + public Response processBulkHealthRequest(List healthEntries, HttpServletRequest request, SecurityContext sec) { + log.info("Processing Bulk Health request - entries count: {}", healthEntries != null ? healthEntries.size() : 0); - AuditLogEntry auditLogEntry = new AuditLogEntry(InetAddressUtility.getIpAddress(request), + if (healthEntries == null || healthEntries.isEmpty()) { + throwBadRequestException("Health entries list is required and cannot be empty"); + } + + AuditLogEntry auditLogEntry = new AuditLogEntry(getClientIpAddress(request), AuditActionType.AUDIT_HEALTH_BULK_WRITE); Response response = null; try { - response = processAuditRequest(request, AuditEndpointType.HEALTH_BULK); + response = processBulkAuditRequest(healthEntries, AuditEndpointType.HEALTH_BULK); } finally { applicationAuditLogger.log(auditLogEntry, getResponseResult(response)); } @@ -139,27 +138,39 @@ public Response processBulkHealthRequest(HttpServletRequest request, SecurityCon return response; } + private String getClientIpAddress(HttpServletRequest request) { + if (request != null) { + return InetAddressUtility.getIpAddress(request); + } else { + // gRPC request + return ServerUtil.getClientContextIpAddress(); + } + } + /** - * Handle an incoming audit log request for a single log event and process it - * while reporting statistics. + * Handle an incoming audit log request with typed LogEntry bean. * - * @param request the HTTP servlet request containing the log JSON payload - * @param response the HTTP servlet response (unused by this method but provided - * by the servlet layer) - * @param sec the security context for the request - * @return a JAX-RS Response containing the processing result; `400 BAD_REQUEST` - * on parse failure, `200 OK` on success + * @param logEntry the log entry bean + * @param request the HTTP servlet request + * @param sec the security context + * @return a JAX-RS Response containing the processing result */ @Override - public Response processLogRequest(HttpServletRequest request, SecurityContext sec) { - log.info("Processing Log request - request: {}", request); + public Response processLogRequest(LogEntry logEntry, HttpServletRequest request, SecurityContext sec) { + log.info("Processing Log request - logEntry: {}", logEntry); + + if (logEntry == null) { + throwBadRequestException("Log entry is required"); + } - AuditLogEntry auditLogEntry = new AuditLogEntry(InetAddressUtility.getIpAddress(request), + AuditLogEntry auditLogEntry = new AuditLogEntry(getClientIpAddress(request), AuditActionType.AUDIT_LOG_WRITE); Response response = null; try { - response = processAuditRequest(request, AuditEndpointType.LOG, true, false); + // Report statistics for single log entry + reportLogStat(logEntry); + response = processAuditRequest(logEntry, AuditEndpointType.LOG); } finally { applicationAuditLogger.log(auditLogEntry, getResponseResult(response)); } @@ -168,25 +179,31 @@ public Response processLogRequest(HttpServletRequest request, SecurityContext se } /** - * Handles an incoming bulk log audit request, reports relevant statistics, and - * delegates processing. + * Handles an incoming bulk log audit request with typed list of LogEntry beans. * - * @param request the HTTP request containing the bulk log payload - * @param response the HTTP response - * @param sec the security context for the request - * @return the HTTP response representing the processing result; status - * indicates success or failure + * @param logEntries list of log entry beans + * @param request the HTTP request + * @param sec the security context for the request + * @return the HTTP response representing the processing result */ @Override - public Response processBulkLogRequest(HttpServletRequest request, SecurityContext sec) { - log.info("Processing Bulk Log request - request: {}", request); + public Response processBulkLogRequest(List logEntries, HttpServletRequest request, SecurityContext sec) { + log.info("Processing Bulk Log request - entries count: {}", logEntries != null ? logEntries.size() : 0); + + if (logEntries == null || logEntries.isEmpty()) { + throwBadRequestException("Log entries list is required and cannot be empty"); + } - AuditLogEntry auditLogEntry = new AuditLogEntry(InetAddressUtility.getIpAddress(request), + AuditLogEntry auditLogEntry = new AuditLogEntry(getClientIpAddress(request), AuditActionType.AUDIT_LOG_BULK_WRITE); Response response = null; try { - response = processAuditRequest(request, AuditEndpointType.LOG_BULK, true, true); + // Report statistics for bulk log entries + for (LogEntry entry : logEntries) { + reportLogStat(entry); + } + response = processBulkAuditRequest(logEntries, AuditEndpointType.LOG_BULK); } finally { applicationAuditLogger.log(auditLogEntry, getResponseResult(response)); } @@ -195,24 +212,27 @@ public Response processBulkLogRequest(HttpServletRequest request, SecurityContex } /** - * Handle an incoming telemetry audit request. + * Handle an incoming telemetry audit request with typed TelemetryEntry bean. * - * @param request the HTTP servlet request containing the telemetry payload - * @param response the HTTP servlet response - * @param sec the security context for the request - * @return a Response representing the result of processing the telemetry audit - * request + * @param telemetryEntry the telemetry entry bean + * @param request the HTTP servlet request + * @param sec the security context for the request + * @return a Response representing the result of processing the telemetry audit request */ @Override - public Response processTelemetryRequest(HttpServletRequest request, SecurityContext sec) { - log.info("Processing Telemetry request - request: {}", request); + public Response processTelemetryRequest(TelemetryEntry telemetryEntry, HttpServletRequest request, SecurityContext sec) { + log.info("Processing Telemetry request - telemetryEntry: {}", telemetryEntry); + + if (telemetryEntry == null) { + throwBadRequestException("Telemetry entry is required"); + } - AuditLogEntry auditLogEntry = new AuditLogEntry(InetAddressUtility.getIpAddress(request), + AuditLogEntry auditLogEntry = new AuditLogEntry(getClientIpAddress(request), AuditActionType.AUDIT_TELEMETRY_WRITE); Response response = null; try { - response = processAuditRequest(request, AuditEndpointType.TELEMETRY); + response = processAuditRequest(telemetryEntry, AuditEndpointType.TELEMETRY); } finally { applicationAuditLogger.log(auditLogEntry, getResponseResult(response)); } @@ -221,21 +241,27 @@ public Response processTelemetryRequest(HttpServletRequest request, SecurityCont } /** - * Handles an incoming bulk telemetry audit request. + * Handles an incoming bulk telemetry audit request with typed list of TelemetryEntry beans. * - * @return a Response representing the result of processing the bulk telemetry - * audit request + * @param telemetryEntries list of telemetry entry beans + * @param request the HTTP servlet request + * @param sec the security context + * @return a Response representing the result of processing the bulk telemetry audit request */ @Override - public Response processBulkTelemetryRequest(HttpServletRequest request, SecurityContext sec) { - log.info("Processing Bulk Telemetry request - request: {}", request); + public Response processBulkTelemetryRequest(List telemetryEntries, HttpServletRequest request, SecurityContext sec) { + log.info("Processing Bulk Telemetry request - entries count: {}", telemetryEntries != null ? telemetryEntries.size() : 0); + + if (telemetryEntries == null || telemetryEntries.isEmpty()) { + throwBadRequestException("Telemetry entries list is required and cannot be empty"); + } - AuditLogEntry auditLogEntry = new AuditLogEntry(InetAddressUtility.getIpAddress(request), + AuditLogEntry auditLogEntry = new AuditLogEntry(getClientIpAddress(request), AuditActionType.AUDIT_TELEMETRY_BULK_WRITE); Response response = null; try { - response = processAuditRequest(request, AuditEndpointType.TELEMETRY_BULK); + response = processBulkAuditRequest(telemetryEntries, AuditEndpointType.TELEMETRY_BULK); } finally { applicationAuditLogger.log(auditLogEntry, getResponseResult(response)); } @@ -244,101 +270,92 @@ public Response processBulkTelemetryRequest(HttpServletRequest request, Security } /** - * Delegates processing of an audit HTTP request to the main processor using - * default flags (do not report statistics, not bulk data). + * Process a single audit request with typed entry object. * - * @param request the incoming HTTP servlet request containing the audit - * JSON payload - * @param requestType the audit endpoint type indicating which audit path to - * process - * @return the JAX-RS response produced by processing the audit request + * @param entry the audit entry object (HealthEntry, LogEntry, or TelemetryEntry) + * @param requestType the audit endpoint type + * @return a JAX-RS Response */ - private Response processAuditRequest(HttpServletRequest request, AuditEndpointType requestType) { - return processAuditRequest(request, requestType, false, false); - } - - /** - * Process an incoming audit HTTP request: parse its JSON payload, optionally - * report statistics, then either forward the payload to the configured audit - * API or persist it locally, and return an HTTP response containing the - * operation result. - * - * @param request the HTTP request containing the audit JSON payload - * @param requestType the audit endpoint type (HEALTH, LOG, TELEMETRY or their - * bulk variants) - * @param reportStat when true, report usage/operation statistics extracted - * from the payload - * @param bulkData when true, treat the payload as an array of entries for - * bulk reporting - * @return a JAX-RS Response whose entity is the result message from forwarding - * or persistence; the response is marked private and no-store with a - * Pragma: no-cache header - */ - private Response processAuditRequest(HttpServletRequest request, AuditEndpointType requestType, boolean reportStat, - boolean bulkData) { - log.info("Processing request - request: {}, requestType: {}", request, requestType); + private Response processAuditRequest(Object entry, AuditEndpointType requestType) { + log.info("Processing single audit request - requestType: {}", requestType); - JsonNode json = getJsonNode(request); - if (json == null) { - throwBadRequestException("Failed to parse request"); - } + Response.ResponseBuilder builder = Response.ok(); - if (reportStat) { - if (bulkData) { - reportBulkStat(json); + try { + String response; + if (AuditPersistenceMode.CONFIG_API.equals(appConfiguration.getAuditPersistenceMode())) { + String json = jsonService.objectToJson(entry); + response = auditForwarderService.post(builder, requestType, json, ContentType.APPLICATION_JSON); } else { - reportStat(json); + response = persistAuditData(builder, requestType, entry); } - } - - Response.ResponseBuilder builder = Response.ok(); - String response; - if (AuditPersistenceMode.CONFIG_API.equals(appConfiguration.getAuditPersistenceMode())) { - response = auditForwarderService.post(builder, requestType, json.toString(), ContentType.APPLICATION_JSON); - } else { - response = persistetAuditData(builder, requestType, json.toString()); + builder.cacheControl(ServerUtil.cacheControlWithNoStoreTransformAndPrivate()); + builder.header(ServerUtil.PRAGMA, ServerUtil.NO_CACHE); + builder.entity(response); + + log.debug("Response entity: {}", response); + } catch (Exception ex) { + builder.status(Status.INTERNAL_SERVER_ERROR); + log.error("Failed to process audit request", ex); + builder.entity("Failed to process audit request"); } - builder.cacheControl(ServerUtil.cacheControlWithNoStoreTransformAndPrivate()); - builder.header(ServerUtil.PRAGMA, ServerUtil.NO_CACHE); - - builder.entity(response); - log.debug("Response entity: {}", response); - return builder.build(); } - private JsonNode getJsonNode(HttpServletRequest request) { - if (request == null) { - return null; - } + /** + * Process bulk audit requests with typed list of entry objects. + * + * @param entries list of audit entry objects + * @param requestType the audit endpoint type + * @return a JAX-RS Response + */ + private Response processBulkAuditRequest(List entries, AuditEndpointType requestType) { + log.info("Processing bulk audit request - requestType: {}, count: {}", requestType, entries.size()); + + Response.ResponseBuilder builder = Response.ok(); - JsonNode jsonBody = null; try { - jsonBody = dataMapperService.readTree(request.getInputStream()); - log.debug("Parsed request body data: {}", jsonBody); + String response; + + if (AuditPersistenceMode.CONFIG_API.equals(appConfiguration.getAuditPersistenceMode())) { + String json = jsonService.objectToJson(entries); + response = auditForwarderService.post(builder, requestType, json, ContentType.APPLICATION_JSON); + } else { + response = persistBulkAuditData(builder, requestType, entries); + } + + builder.cacheControl(ServerUtil.cacheControlWithNoStoreTransformAndPrivate()); + builder.header(ServerUtil.PRAGMA, ServerUtil.NO_CACHE); + builder.entity(response); + + log.debug("Response entity: {}", response); } catch (Exception ex) { - log.error("Failed to parse request", ex); + builder.status(Status.INTERNAL_SERVER_ERROR); + log.error("Failed to process bulk audit request", ex); + builder.entity("Failed to process bulk audit request"); } - return jsonBody; + 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()); + /** + * Report statistics for a single log entry. + * + * @param logEntry the log entry to report statistics for + */ + private void reportLogStat(LogEntry logEntry) { + if (logEntry.getClientId() != null) { + statService.reportActiveClient(logEntry.getClientId()); } - boolean hasPrincipalId = json.hasNonNull(LOG_PRINCIPAL_ID); - if (hasPrincipalId) { - statService.reportActiveUser(json.get(LOG_PRINCIPAL_ID).asText()); + if (logEntry.getPrincipalId() != null) { + statService.reportActiveUser(logEntry.getPrincipalId()); } - boolean hasВecisionResult = json.hasNonNull(LOG_DECISION_RESULT); - if (hasВecisionResult) { - String decisionResult = json.get(LOG_DECISION_RESULT).asText(); + if (logEntry.getDecisionResult() != null) { + String decisionResult = logEntry.getDecisionResult(); if (LOG_DECISION_RESULT_ALLOW.equals(decisionResult)) { statService.reportAllow(LOG_DECISION_RESULT); } else if (LOG_DECISION_RESULT_DENY.equals(decisionResult)) { @@ -346,96 +363,81 @@ private void reportStat(JsonNode json) { } } - boolean hasAction = json.hasNonNull(LOG_ACTION); - if (hasAction) { - statService.reportOpearation(LOG_ACTION, json.get(LOG_ACTION).asText()); + if (logEntry.getAction() != null) { + statService.reportOpearation(LOG_ACTION, logEntry.getAction()); } } /** - * Reports statistics for each element of a JSON array representing bulk audit - * entries. - * - * If the provided node is not a JSON array, an error is logged and the method - * still attempts to process its elements. + * Persist a single audit entry. * - * @param json JSON array of audit entries whose elements will be processed to - * report statistics + * @param builder response builder + * @param requestType the audit endpoint type + * @param entry the audit entry object + * @return empty string on success, error message on failure */ - 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); + private String persistAuditData(ResponseBuilder builder, AuditEndpointType requestType, Object entry) { + try { + switch (requestType) { + case LOG: + auditService.addLogEntry((LogEntry) entry); + break; + case HEALTH: + auditService.addHealthEntry((HealthEntry) entry); + break; + case TELEMETRY: + auditService.addTelemetryEntry((TelemetryEntry) entry); + break; + default: + builder.status(Status.BAD_REQUEST); + return "Invalid request type for single entry"; + } + } catch (Exception ex) { + builder.status(Status.INTERNAL_SERVER_ERROR); + log.error("Failed to persist audit data", ex); + return "Failed to persist data"; } - + return ""; } /** - * Persist audit data for the given request type. - * - *

- * Parses the provided JSON payload into the appropriate audit entry or entries - * based on {@code requestType} and persists them via the audit service. - *

+ * Persist bulk audit entries. * - * @param builder response builder that will be updated to BAD_REQUEST on - * parse failure - * @param requestType the type of audit endpoint (log, health, telemetry, or - * their bulk variants) - * @param json the JSON payload to parse and persist - * @return an empty string on success, or the message "Failed to parse data" if - * parsing failed + * @param builder response builder + * @param requestType the audit endpoint type + * @param entries list of audit entry objects + * @return empty string on success, error message on failure */ - private String persistetAuditData(ResponseBuilder builder, AuditEndpointType requestType, String json) { + @SuppressWarnings("unchecked") + private String persistBulkAuditData(ResponseBuilder builder, AuditEndpointType requestType, List entries) { try { switch (requestType) { - case LOG: - LogEntry logEntry = jsonService.jsonToObject(json, LogEntry.class); - auditService.addLogEntry(logEntry); - break; case LOG_BULK: - List logEntries = jsonService.jsonToObject(json, - jsonService.getTypeFactory().constructCollectionType(List.class, LogEntry.class)); + List logEntries = (List) entries; for (LogEntry entry : logEntries) { auditService.addLogEntry(entry); } break; - case HEALTH: - HealthEntry healthEntry = jsonService.jsonToObject(json, HealthEntry.class); - auditService.addHealthEntry(healthEntry); - break; case HEALTH_BULK: - List healthEntries = jsonService.jsonToObject(json, - jsonService.getTypeFactory().constructCollectionType(List.class, HealthEntry.class)); + List healthEntries = (List) entries; for (HealthEntry entry : healthEntries) { auditService.addHealthEntry(entry); } break; - case TELEMETRY: - TelemetryEntry telemetryEntry = jsonService.jsonToObject(json, TelemetryEntry.class); - auditService.addTelemetryEntry(telemetryEntry); - break; case TELEMETRY_BULK: - List telemetryEntries = jsonService.jsonToObject(json, - jsonService.getTypeFactory().constructCollectionType(List.class, TelemetryEntry.class)); + List telemetryEntries = (List) entries; for (TelemetryEntry entry : telemetryEntries) { auditService.addTelemetryEntry(entry); } break; + default: + builder.status(Status.BAD_REQUEST); + return "Invalid request type for bulk entries"; } - } catch (IOException ex) { - builder.status(Status.BAD_REQUEST); - log.warn("Failed to parse data", ex); - - return "Failed to parse data"; } catch (Exception ex) { builder.status(Status.INTERNAL_SERVER_ERROR); - log.error("Failed to persist audit data", ex); - - return "Failed to persist data"; + log.error("Failed to persist bulk audit data", ex); + return "Failed to persist bulk data"; } return ""; } diff --git a/jans-lock/lock-server/service/src/main/java/io/jans/lock/util/HeaderUtils.java b/jans-lock/lock-server/service/src/main/java/io/jans/lock/util/HeaderUtils.java new file mode 100644 index 00000000000..7b0cef9e522 --- /dev/null +++ b/jans-lock/lock-server/service/src/main/java/io/jans/lock/util/HeaderUtils.java @@ -0,0 +1,87 @@ +/* + * Janssen Project software is available under the Apache License (2004). See http://www.apache.org/licenses/ for full text. + * + * Copyright (c) 2026, Janssen Project + */ + +package io.jans.lock.util; + +import io.grpc.Metadata; + +/** + * Utility for handling gRPC authorization headers + * + * @author Yuriy Movchan Date: 01/20/2026 + */ +public class HeaderUtils { + + /** + * Finds the authorization header in gRPC metadata, handling various + * case variations and naming conventions. + * + * @param headers The gRPC metadata headers + * @return The authorization header value, or null if not found + */ + public static String findAuthorizationHeader(Metadata headers) { + if (headers == null) { + return null; + } + + // Common variations of authorization header keys + String[] possibleKeys = { + "authorization", + "grpc-metadata-authorization", + "x-authorization", + "x-grpc-authorization" + }; + + for (String key : possibleKeys) { + String value = headers.get(Metadata.Key.of(key, Metadata.ASCII_STRING_MARSHALLER)); + if (value != null && !value.trim().isEmpty()) { + return value; + } + } + + return null; + } + + /** + * Extracts the bearer token from an authorization header. + * + * @param authHeader The full authorization header value + * @return The token without the "bearer " prefix, or null if invalid + */ + public static String extractBearerToken(String authHeader) { + if (authHeader == null || authHeader.trim().isEmpty()) { + return null; + } + + // Remove any extra whitespace + String trimmed = authHeader.trim(); + + // Check if it's a bearer token + if (trimmed.toLowerCase().startsWith("bearer ")) { + return trimmed.substring(7).trim(); // Remove "bearer " prefix + } + + // Also accept "Bearer " with capital B + if (trimmed.startsWith("Bearer ")) { + return trimmed.substring(7).trim(); + } + + // If it doesn't start with bearer, return as-is (might be basic auth or other) + return trimmed; + } + + /** + * Combines both methods: finds and extracts the bearer token. + * + * @param headers The gRPC metadata headers + * @return The bearer token, or null if not found/invalid + */ + public static String findAndExtractBearerToken(Metadata headers) { + String authHeader = findAuthorizationHeader(headers); + return extractBearerToken(authHeader); + } + +} \ No newline at end of file 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 5abe5fff520..6ff64d32d04 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 @@ -32,11 +32,14 @@ import com.fasterxml.jackson.databind.introspect.JacksonAnnotationIntrospector; import com.fasterxml.jackson.databind.node.ObjectNode; +import io.grpc.Context; +import io.grpc.Metadata; import io.jans.util.Util; import jakarta.ws.rs.core.CacheControl; /** * @author Yuriy Zabrovarnyy + * @author Yuriy Movchan * @version 0.1, 10/07/2024 */ @@ -44,10 +47,15 @@ public class ServerUtil { private static final Logger log = LoggerFactory.getLogger(ServerUtil.class); - public static final String PRAGMA = "Pragma"; public static final String NO_CACHE = "no-cache"; + private static final String UNKNOWN = "unknown"; + + public static final Context.Key CLIENT_IP_CONTEXT_KEY = Context.key("client-ip"); + private static final Metadata.Key X_FORWARDED_FOR = Metadata.Key.of("x-forwarded-for", Metadata.ASCII_STRING_MARSHALLER); + private static final Metadata.Key X_REAL_IP = Metadata.Key.of("x-real-ip", Metadata.ASCII_STRING_MARSHALLER); + public static CacheControl cacheControl(boolean noStore) { final CacheControl cacheControl = new CacheControl(); cacheControl.setNoStore(noStore); @@ -110,4 +118,87 @@ public static String urlDecode(String str) { return str; } -} + /** + * Get client IP address from current context + * @return client IP address or "unknown" if not determined + */ + public static String getClientContextIpAddress() { + String clientIp = CLIENT_IP_CONTEXT_KEY.get(Context.current()); + + if (clientIp == null) { + return UNKNOWN; + } + + return clientIp; + } + + /** + * Set client IP address from current context + * @return + */ + public static Context setClientContextIpAddress(String clientIp) { + return Context.current().withValue(CLIENT_IP_CONTEXT_KEY, clientIp); + } + + /** + * Get client IP address when ServerCall is available (e.g., in interceptor) + */ + public static String getGrpcClientIpAddress(io.grpc.ServerCall call, Metadata headers) { + try { + // Method 1: Try to get from metadata headers + String ipFromHeaders = getIpFromHeaders(headers); + if (ipFromHeaders != null) { + log.debug("Client IP from headers: {}", ipFromHeaders); + return ipFromHeaders; + } + + // Method 2: Get from ServerCall attributes + if (call != null) { + io.grpc.Attributes attributes = call.getAttributes(); + java.net.SocketAddress remoteAddr = attributes.get(io.grpc.Grpc.TRANSPORT_ATTR_REMOTE_ADDR); + if (remoteAddr instanceof java.net.InetSocketAddress) { + java.net.InetSocketAddress inetAddr = (java.net.InetSocketAddress) remoteAddr; + String ip = inetAddr.getAddress().getHostAddress(); + log.debug("Client IP from ServerCall attributes: {}", ip); + return ip; + } + } + + log.warn("Could not determine client IP address"); + return UNKNOWN; + } catch (Exception e) { + log.error("Error getting client IP address", e); + return UNKNOWN; + } + } + + /** + * Extract IP directly from headers (for use in interceptor) + */ + public static String getIpFromHeaders(Metadata headers) { + if (headers == null) { + return null; + } + + try { + // 1. Try X-Forwarded-For + String xForwardedFor = headers.get(X_FORWARDED_FOR); + if (xForwardedFor != null && !xForwardedFor.trim().isEmpty()) { + String[] ips = xForwardedFor.split(","); + return ips[0].trim(); + } + + // 2. Try X-Real-IP + String xRealIp = headers.get(X_REAL_IP); + if (xRealIp != null && !xRealIp.trim().isEmpty()) { + return xRealIp.trim(); + } + + return null; + } catch (Exception e) { + log.debug("Error extracting IP from headers", e); + return null; + } + } + +} \ No newline at end of file diff --git a/jans-lock/lock-server/service/src/main/policy/audit/health_bulk_write.json b/jans-lock/lock-server/service/src/main/policy/audit/health_bulk_write.json new file mode 100644 index 00000000000..5c7f5fb549d --- /dev/null +++ b/jans-lock/lock-server/service/src/main/policy/audit/health_bulk_write.json @@ -0,0 +1,11 @@ +@id("lock_audit_health_write") +permit( + principal is Jans::Workload, + action in Jans::Action::"POST", + resource == Jans::HTTP_Request::"lock_audit_health_write" +) +when { + principal has access_token.scope && + principal.access_token.scope.contains("https://jans.io/oauth/lock/health.write") && + resource has url && resource.url has path && resource.url.path == "/audit/health/bulk" +}; diff --git a/jans-lock/lock-server/service/src/main/policy/audit/log_bulk_write.json b/jans-lock/lock-server/service/src/main/policy/audit/log_bulk_write.json new file mode 100644 index 00000000000..89017c2c707 --- /dev/null +++ b/jans-lock/lock-server/service/src/main/policy/audit/log_bulk_write.json @@ -0,0 +1,11 @@ +@id("lock_audit_log_write") +permit( + principal is Jans::Workload, + action in Jans::Action::"POST", + resource == Jans::HTTP_Request::"lock_audit_log_write" +) +when { + principal has access_token.scope && + principal.access_token.scope.contains("https://jans.io/oauth/lock/log.write") && + resource has url && resource.url has path && resource.url.path == "/audit/log/bulk" +}; diff --git a/jans-lock/lock-server/service/src/main/policy/audit/telemetry_bulk_write.json b/jans-lock/lock-server/service/src/main/policy/audit/telemetry_bulk_write.json new file mode 100644 index 00000000000..52c5a6d8940 --- /dev/null +++ b/jans-lock/lock-server/service/src/main/policy/audit/telemetry_bulk_write.json @@ -0,0 +1,11 @@ +@id("lock_audit_telemetry_write") +permit( + principal is Jans::Workload, + action in Jans::Action::"POST", + resource == Jans::HTTP_Request::"lock_audit_telemetry_write" +) +when { + principal has access_token.scope && + principal.access_token.scope.contains("https://jans.io/oauth/lock/telemetry.write") && + resource has url && resource.url has path && resource.url.path == "/audit/telemetry/bulk" +}; diff --git a/jans-lock/lock-server/service/src/main/proto/grpc/audit.proto b/jans-lock/lock-server/service/src/main/proto/grpc/audit.proto new file mode 100644 index 00000000000..2bee038ef11 --- /dev/null +++ b/jans-lock/lock-server/service/src/main/proto/grpc/audit.proto @@ -0,0 +1,93 @@ +syntax = "proto3"; + +package io.jans.lock.audit; + +option java_multiple_files = true; +option java_package = "io.jans.lock.model.audit.grpc"; +option java_outer_classname = "AuditProto"; + +import "google/protobuf/timestamp.proto"; + +// Health Entry Message +message HealthEntry { + google.protobuf.Timestamp creation_date = 1; + google.protobuf.Timestamp event_time = 2; + string service = 3; + string node_name = 4; + string status = 5; + map engine_status = 6; +} + +message HealthRequest { + HealthEntry entry = 1; +} + +message BulkHealthRequest { + repeated HealthEntry entries = 1; +} + +// Log Entry Message +message LogEntry { + google.protobuf.Timestamp creation_date = 1; + google.protobuf.Timestamp event_time = 2; + string service = 3; + string node_name = 4; + string event_type = 5; + string severity_level = 6; + string action = 7; + string decision_result = 8; + string requested_resource = 9; + string principal_id = 10; + string client_id = 11; + string jti = 12; + map context_information = 13; +} + +message LogRequest { + LogEntry entry = 1; +} + +message BulkLogRequest { + repeated LogEntry entries = 1; +} + +// Telemetry Entry Message +message TelemetryEntry { + google.protobuf.Timestamp creation_date = 1; + google.protobuf.Timestamp event_time = 2; + string service = 3; + string node_name = 4; + string status = 5; + int64 last_policy_load_size = 6; + int64 policy_success_load_counter = 7; + int64 policy_failed_load_counter = 8; + int64 last_policy_evaluation_time_ns = 9; + int64 avg_policy_evaluation_time_ns = 10; + int64 memory_usage = 11; + int64 evaluation_requests_count = 12; + map policy_stats = 13; +} + +message TelemetryRequest { + TelemetryEntry entry = 1; +} + +message BulkTelemetryRequest { + repeated TelemetryEntry entries = 1; +} + +// Common Response +message AuditResponse { + bool success = 1; + string message = 2; +} + +// Audit Service +service AuditService { + rpc ProcessHealth(HealthRequest) returns (AuditResponse); + rpc ProcessBulkHealth(BulkHealthRequest) returns (AuditResponse); + rpc ProcessLog(LogRequest) returns (AuditResponse); + rpc ProcessBulkLog(BulkLogRequest) returns (AuditResponse); + rpc ProcessTelemetry(TelemetryRequest) returns (AuditResponse); + rpc ProcessBulkTelemetry(BulkTelemetryRequest) returns (AuditResponse); +} \ No newline at end of file