diff --git a/README.md b/README.md index 2247681f5..84e331a96 100644 --- a/README.md +++ b/README.md @@ -103,9 +103,9 @@ java -jar # Example 1 : Start service with test config against default Database URL (useful for local env) java -jar build/libs/naksha-2.0.6-all.jar test-config -# Example 2 : Start service with given custom config and custom database URL (useful for cloud env) +# Example 2 : Start service with given custom config (using NAKSHA_CONFIG_PATH) and custom database URL (useful for cloud env) java -jar build/libs/naksha-2.0.6-all.jar cloud-config 'jdbc:postgresql://localhost:5432/postgres?user=postgres&password=pswd&schema=naksha&app=naksha_local&id=naksha_admin_db' -# Example 3 : Start service with given custom config and default (local) database URL +# Example 3 : Start service with given custom config (using NAKSHA_CONFIG_PATH) and default (local) database URL java -jar build/libs/naksha-2.0.6-all.jar custom-config ``` @@ -126,17 +126,16 @@ Once application is UP, the OpenAPI specification is accessible at `http(s)://{h The service persists out of modules with a bootstrap code to start the service. Service provides default configuration in [default-config.json](here-naksha-lib-hub/src/main/resources/config/default-config.json). The custom (external) configuration file can be supplied by modifying environment variable or by creating the `default-config.json` file in the corresponding configuration folder. -The exact configuration folder is platform dependent, but generally follows the [XGD user configuration directory](https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html), standard, so on Linux being by default `~/.config/naksha/v{x.x.x}/`. For Windows the files will reside in the [CSIDL_PROFILE](https://learn.microsoft.com/en-us/windows/win32/shell/csidl?redirectedfrom=MSDN) folder, by default `C:\Users\{username}\.config\naksha\v{x.x.x}\`. -Here `{x.x.x}` is the Naksha application version (for example, if version is `2.0.7`, then path will be `...\.config\naksha\v2.0.7`) +The exact configuration folder is platform dependent, but generally follows the [XGD user configuration directory](https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html), standard, so on Linux being by default `~/.config/naksha/`. For Windows the files will reside in the [CSIDL_PROFILE](https://learn.microsoft.com/en-us/windows/win32/shell/csidl?redirectedfrom=MSDN) folder, by default `C:\Users\{username}\.config\naksha\`. -Next to this, an explicit location can be specified via the environment variable `NAKSHA_CONFIG_PATH`, this path will not be extended by the `naksha/v{x.x.x}` folder, so you can directly specify where to keep the config files. This is important when you want to start multiple versions of the service: `NAKSHA_CONFIG_PATH=~/.config/naksha/ java -jar naksha.jar {arguments}`. +Next to this, an explicit location can be specified via the environment variable `NAKSHA_CONFIG_PATH`, this path will not be extended by the `naksha/` folder, so you can directly specify where to keep the config files. This is important when you want to start multiple versions of the service: `NAKSHA_CONFIG_PATH=~/.config/naksha/ java -jar naksha.jar {arguments}`. In the custom config file, the name of the individual properties can be set as per source code here [NakshaHubConfig](here-naksha-lib-hub/src/main/java/com/here/naksha/lib/hub/NakshaHubConfig.java). All properties annotated with `@JsonProperty` can be set in custom config file. Config file is loaded using `{config-id}` supplied as CLI argument, as per following precedence on file location (first match wins): 1. using env variable `NAKSHA_CONFIG_PATH` (full path will be `$NAKSHA_CONFIG_PATH/{config-id}.json`) -2. as per user's home directory `user.home` (full path will be `{user-home}/.config/naksha/v{x.x.x}/{config-id}.json` ) +2. as per user's home directory `user.home` (full path will be `{user-home}/.config/naksha/{config-id}.json` ) 3. as per config previously loaded in Naksha Admin Storage (PostgreSQL database) 4. default config loaded from jar (`here-naksha-lib-hub/src/main/resources/config/default-config.json`) @@ -154,6 +153,21 @@ vi $NAKSHA_CONFIG_PATH/default-config.json java -jar naksha.jar default-config ``` +The config also accepts custom RSA256 Private key and multiple Public key files (in PEM format) to support JWT signing/verification operations. + +* If custom Private key not provided, default will be loaded from Jar bundled resource [here-naksha-app-service/src/main/resources/auth/jwt.key](here-naksha-app-service/src/main/resources/auth/jwt.key). +* If custom Public key not provided, default will be loaded from Jar bundled resource [here-naksha-app-service/src/main/resources/auth/jwt.pub](here-naksha-app-service/src/main/resources/auth/jwt.pub). + +Sample commands to generate the custom key files: + +```bash +# Generate private key +openssl genrsa -out ./custom_rsa256.key 2048 + +# Generate public key (using above private key) +openssl rsa -in ./custom_rsa256.key -pubout -outform PEM -out ./custom_rsa256.pub +``` + # Usage Start using the service by creating a _space_: diff --git a/build.gradle.kts b/build.gradle.kts index 4534d59da..0895a31b4 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -158,7 +158,9 @@ subprojects { // excluding tests where Builder pattern gets broken by palantir targetExclude("src/test/**") encoding("UTF-8") - val YEAR = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy")) + // TODO - Disabling auto-correction to 2025 for now. Will handle via separate change. + //val YEAR = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy")) + val YEAR = 2024 licenseHeader(""" /* * Copyright (C) 2017-$YEAR HERE Europe B.V. diff --git a/docker/Dockerfile b/docker/Dockerfile index 2c5b9a58a..9b5fdef0d 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -24,6 +24,7 @@ ENV NAKSHA_ADMIN_DB_URL 'jdbc:postgresql://host.docker.internal:5432/postgres?us ENV NAKSHA_EXTENSION_S3_BUCKET 'naksha-pvt-releases' ENV NAKSHA_JWT_PVT_KEY '' ENV NAKSHA_JWT_PUB_KEY '' +ENV NAKSHA_JWT_PUB_KEY_2 '' ENV JAVA_OPTS '' # Execute Shell Script diff --git a/docker/README.md b/docker/README.md index 7d9ed86d2..0e92394bd 100644 --- a/docker/README.md +++ b/docker/README.md @@ -30,8 +30,9 @@ To get Naksha container running, one must do the following: use, `jdbc:postgresql://host.docker.internal:5432/postgres?user=postgres&password=password&schema=naksha&app=naksha_local&id=naksha_admin_db` by default - `NAKSHA_EXTENSION_S3_BUCKET`: S3 bucket name or S3 bucket access point.The default value is `naksha-pvt-releases`. - - `NAKSHA_JWT_PVT_KEY`: Naksha JWT private key. If not provided then it will load it from `here-naksha-app-service/src/main/resources/auth/jwt.key`. - - `NAKSHA_JWT_PUB_KEY`: Naksha JWT public key. If not provided then it will load it from `here-naksha-app-service/src/main/resources/auth/jwt.pub`. + - `NAKSHA_JWT_PVT_KEY`: Naksha JWT private key. + - `NAKSHA_JWT_PUB_KEY`: Naksha JWT public key. + - `NAKSHA_JWT_PUB_KEY_2`: Additional Naksha JWT public key, if needed to validate the JWT signed by some other application's PVT key. - `JAVA_OPTS`: Any custom java options like `-Xms1024m -Xmx2048m` When connecting Naksha app to database, one has to consider container networking - if your @@ -61,6 +62,7 @@ To get Naksha container running, one must do the following: --env NAKSHA_EXTENSION_S3_BUCKET= \ --env NAKSHA_JWT_PVT_KEY= \ --env NAKSHA_JWT_PUB_KEY= \ + --env NAKSHA_JWT_PUB_KEY_2= \ --env JAVA_OPTS="-Xms1024m -Xmx2048m" \ -p 8080:8080 \ local-naksha-app diff --git a/docker/cloud-config.json b/docker/cloud-config.json index db60ca862..bb7db8f3a 100644 --- a/docker/cloud-config.json +++ b/docker/cloud-config.json @@ -1,11 +1,13 @@ { "id": "cloud-config", - "type": "Config", + "type": "Feature", "httpPort": 7080, "requestBodyLimit": 25, + "authMode": "JWT", + "jwtPvtKeyPath": "SOME_PVT_KEY_PATH", + "jwtPubKeyPaths": "SOME_PUB_KEY_PATHS", "maxParallelRequestsPerCPU": 100, "maxPctParallelRequestsPerActor": 25, - "authMode": "JWT", "extensionConfigParams": { "whitelistClasses": [ "java.*", "javax.*", "com.here.*", "jdk.internal.reflect.*", "com.sun.*", "org.w3c.dom.*", "sun.misc.*","org.locationtech.jts.*"], "intervalms": 30000, diff --git a/docker/run-app.sh b/docker/run-app.sh index 1b342a029..51b9e3c14 100644 --- a/docker/run-app.sh +++ b/docker/run-app.sh @@ -3,13 +3,15 @@ # Set the NAKSHA_CONFIG_PATH export NAKSHA_CONFIG_PATH=/home/naksha/app/config/ -# Replace placeholder in cloud-config.json -sed -i "s+SOME_S3_BUCKET+${NAKSHA_EXTENSION_S3_BUCKET}+g" /home/naksha/app/config/cloud-config.json +NAKSHA_PVT_KEY_PATH="" +NAKSHA_PUB_KEY_PATHS="" # Check if NAKSHA_JWT_PVT_KEY is set and create jwt.key file if it is if [ -n "$NAKSHA_JWT_PVT_KEY" ]; then mkdir -p ${NAKSHA_CONFIG_PATH}auth/ - echo "$NAKSHA_JWT_PVT_KEY" | sed 's/\\n/\n/g' > ${NAKSHA_CONFIG_PATH}auth/jwt.key + KEY_FILE_PATH=auth/jwt.key + echo "$NAKSHA_JWT_PVT_KEY" | sed 's/\\n/\n/g' > ${NAKSHA_CONFIG_PATH}${KEY_FILE_PATH} + NAKSHA_PVT_KEY_PATH=${KEY_FILE_PATH} echo "Using custom JWT private key" else echo "No custom JWT private key supplied" @@ -18,11 +20,29 @@ fi # Check if NAKSHA_JWT_PUB_KEY is set and create jwt.pub file if it is if [ -n "$NAKSHA_JWT_PUB_KEY" ]; then mkdir -p ${NAKSHA_CONFIG_PATH}auth/ - echo "$NAKSHA_JWT_PUB_KEY" | sed 's/\\n/\n/g' > ${NAKSHA_CONFIG_PATH}auth/jwt.pub + KEY_FILE_PATH=auth/jwt.pub + echo "$NAKSHA_JWT_PUB_KEY" | sed 's/\\n/\n/g' > ${NAKSHA_CONFIG_PATH}${KEY_FILE_PATH} + NAKSHA_PUB_KEY_PATHS=${KEY_FILE_PATH} echo "Using custom JWT public key" else echo "No custom JWT public key supplied" fi +# Check if NAKSHA_JWT_PUB_KEY_2 is set and create jwt_2.pub file if it is +if [ -n "$NAKSHA_JWT_PUB_KEY_2" ]; then + mkdir -p ${NAKSHA_CONFIG_PATH}auth/ + KEY_FILE_PATH=auth/jwt_2.pub + echo "$NAKSHA_JWT_PUB_KEY_2" | sed 's/\\n/\n/g' > ${NAKSHA_CONFIG_PATH}${KEY_FILE_PATH} + NAKSHA_PUB_KEY_PATHS=${NAKSHA_PUB_KEY_PATHS},${KEY_FILE_PATH} + echo "Using custom JWT public key 2" +else + echo "No custom JWT public key 2 supplied" +fi + +# Replace all placeholders in cloud-config.json +sed -i "s+SOME_S3_BUCKET+${NAKSHA_EXTENSION_S3_BUCKET}+g" /home/naksha/app/config/cloud-config.json +sed -i "s+SOME_PVT_KEY_PATH+${NAKSHA_PVT_KEY_PATH}+g" /home/naksha/app/config/cloud-config.json +sed -i "s+SOME_PUB_KEY_PATHS+${NAKSHA_PUB_KEY_PATHS}+g" /home/naksha/app/config/cloud-config.json + # Start the application java $JAVA_OPTS -jar /home/naksha/app/naksha-*-all.jar $NAKSHA_CONFIG_ID $NAKSHA_ADMIN_DB_URL \ No newline at end of file diff --git a/gradle.properties b/gradle.properties index fee820e8e..cb06ea2e9 100644 --- a/gradle.properties +++ b/gradle.properties @@ -8,8 +8,7 @@ mavenUser=YourUserName mavenPassword=YourPassword # When updating the version, please as well consider: -# - here-naksha-lib-core/NakshaVersion (static property: latest) -# - here-naksha-lib-psql/resources/naksha_plpgsql.sql (method: naksha_version) +# - here-naksha-lib-core/src/main/com/here/naksha/lib/core/NakshaVersion (static property: latest) # - here-naksha-app-service/src/main/resources/swagger/openapi.yaml (info.version property) -version=2.2.0 +version=2.2.1 diff --git a/here-naksha-app-service/src/main/java/com/here/naksha/app/service/NakshaApp.java b/here-naksha-app-service/src/main/java/com/here/naksha/app/service/NakshaApp.java index d34a72217..e9bc98429 100644 --- a/here-naksha-app-service/src/main/java/com/here/naksha/app/service/NakshaApp.java +++ b/here-naksha-app-service/src/main/java/com/here/naksha/app/service/NakshaApp.java @@ -102,13 +102,13 @@ private static void printUsage() { err.println("Examples:"); err.println(" "); err.println(" Example 1 : Start service with given config and default (local) database URL"); - err.println(" java -jar naksha.jar default-config"); + err.println(" java -jar naksha.jar test-config"); err.println(" "); err.println(" Example 2 : Start service with given config and custom database URL"); - err.println(" java -jar naksha.jar default-config '" + DEFAULT_URL + "'"); + err.println(" java -jar naksha.jar test-config '" + DEFAULT_URL + "'"); err.println(" "); - err.println(" Example 3 : Start service with mock config (with in-memory hub)"); - err.println(" java -jar naksha.jar mock-config"); + err.println(" Example 3 : Start service with custom config (using custom NAKSHA_CONFIG_PATH)"); + err.println(" java -jar naksha.jar custom-config"); err.println(" "); err.flush(); } @@ -220,20 +220,20 @@ public NakshaApp( } this.vertx = Vertx.vertx(this.vertxOptions); - final String jwtKey; - final String jwtPub; + final List keyOptions = new ArrayList<>(); + // read JWT pvt key { - final String path = "auth/" + config.jwtName + ".key"; - jwtKey = readAuthKeyFile(path, NakshaHubConfig.APP_NAME); + final String keyContent = readAuthKeyFile(config.jwtPvtKeyPath, NakshaHubConfig.APP_NAME); + keyOptions.add(new PubSecKeyOptions().setAlgorithm("RS256").setBuffer(keyContent)); } - { - final String path = "auth/" + config.jwtName + ".pub"; - jwtPub = readAuthKeyFile(path, NakshaHubConfig.APP_NAME); + // read JWT pub keys + for (final String keyPath : config.jwtPubKeyPaths.split(",")) { + final String keyContent = readAuthKeyFile(keyPath, NakshaHubConfig.APP_NAME); + keyOptions.add(new PubSecKeyOptions().setAlgorithm("RS256").setBuffer(keyContent)); } this.authOptions = new JWTAuthOptions() .setJWTOptions(new JWTOptions().setAlgorithm("RS256")) - .addPubSecKey(new PubSecKeyOptions().setAlgorithm("RS256").setBuffer(jwtKey)) - .addPubSecKey(new PubSecKeyOptions().setAlgorithm("RS256").setBuffer(jwtPub)); + .setPubSecKeys(keyOptions); this.authProvider = new NakshaAuthProvider(this.vertx, this.authOptions); final WebClientOptions webClientOptions = new WebClientOptions(); diff --git a/here-naksha-app-service/src/main/java/com/here/naksha/app/service/http/auth/NakshaJwtAuthHandler.java b/here-naksha-app-service/src/main/java/com/here/naksha/app/service/http/auth/NakshaJwtAuthHandler.java index cb730f77a..1d2a558bb 100644 --- a/here-naksha-app-service/src/main/java/com/here/naksha/app/service/http/auth/NakshaJwtAuthHandler.java +++ b/here-naksha-app-service/src/main/java/com/here/naksha/app/service/http/auth/NakshaJwtAuthHandler.java @@ -44,7 +44,7 @@ public class NakshaJwtAuthHandler extends JWTAuthHandlerImpl { /** * The master JWT used for testing. */ - private final String MASTER_JWT = authProvider.generateToken(MASTER_JWT_PAYLOAD); + private static String MASTER_JWT = null; public NakshaJwtAuthHandler( @NotNull JWTAuth authProvider, @NotNull NakshaHubConfig hubConfig, @Nullable String realm) { @@ -57,6 +57,9 @@ public void authenticate(@NotNull RoutingContext context, @NotNull Handler<@NotN if (hubConfig.authMode == AuthorizationMode.DUMMY && !context.request().headers().contains(HttpHeaders.AUTHORIZATION)) { // Use the master JWT for testing in DUMMY auth mode with no JWT provided in request + if (MASTER_JWT == null) { + MASTER_JWT = authProvider.generateToken(MASTER_JWT_PAYLOAD); + } context.request().headers().set(HttpHeaders.AUTHORIZATION, "Bearer " + MASTER_JWT); } // TODO: If compressed JWTs are supported diff --git a/here-naksha-app-service/src/main/resources/mock-config.json b/here-naksha-app-service/src/main/resources/mock-config.json deleted file mode 100644 index a6679c724..000000000 --- a/here-naksha-app-service/src/main/resources/mock-config.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "id": "mock-config", - "type": "Config", - - "httpPort": 8080, - "env": "local", - "jwtName": "jwt", - "hubClassName": "com.here.naksha.lib.hub.mock.NakshaHubMock", - "maintenanceInitialDelayInMins": 60, - "maintenanceIntervalInMins": 720, - "maintenancePoolCoreSize": 5, - "maintenancePoolMaxSize": 20 -} \ No newline at end of file diff --git a/here-naksha-app-service/src/main/resources/swagger/openapi.yaml b/here-naksha-app-service/src/main/resources/swagger/openapi.yaml index 061878e6d..bb3e48c6a 100644 --- a/here-naksha-app-service/src/main/resources/swagger/openapi.yaml +++ b/here-naksha-app-service/src/main/resources/swagger/openapi.yaml @@ -12,7 +12,7 @@ servers: info: title: "Naksha Hub-API" description: "Naksha Hub-API is a REST API to provide simple access to geo data." - version: "2.2.0" + version: "2.2.1" security: - AccessToken: [ ] diff --git a/here-naksha-app-service/src/main/resources/test-config-with-extensions.json b/here-naksha-app-service/src/main/resources/test-config-with-extensions.json index 5be333f36..1ffeb2100 100644 --- a/here-naksha-app-service/src/main/resources/test-config-with-extensions.json +++ b/here-naksha-app-service/src/main/resources/test-config-with-extensions.json @@ -1,16 +1,13 @@ { "id": "test-config", - "type": "Config", + "type": "Feature", "httpPort": 8080, "env": "local", "requestBodyLimit": 25, "authMode": "DUMMY", - "jwtName": "jwt", - "maintenanceInitialDelayInMins": 60, - "maintenanceIntervalInMins": 720, - "maintenancePoolCoreSize": 5, - "maintenancePoolMaxSize": 20, + "jwtPvtKeyPath": "auth/jwt.key", + "jwtPubKeyPaths": "auth/jwt.pub", "maxParallelRequestsPerCPU": 30, "maxPctParallelRequestsPerActor": 100, "storageParams": { diff --git a/here-naksha-app-service/src/main/resources/test-config.json b/here-naksha-app-service/src/main/resources/test-config.json index c5d31cad8..b0e9d2f6d 100644 --- a/here-naksha-app-service/src/main/resources/test-config.json +++ b/here-naksha-app-service/src/main/resources/test-config.json @@ -1,16 +1,13 @@ { "id": "test-config", - "type": "Config", + "type": "Feature", "httpPort": 8080, "env": "local", "requestBodyLimit": 25, "authMode": "DUMMY", - "jwtName": "jwt", - "maintenanceInitialDelayInMins": 60, - "maintenanceIntervalInMins": 720, - "maintenancePoolCoreSize": 5, - "maintenancePoolMaxSize": 20, + "jwtPvtKeyPath": "auth/jwt.key", + "jwtPubKeyPaths": "auth/jwt.pub", "maxParallelRequestsPerCPU": 30, "maxPctParallelRequestsPerActor": 100, "storageParams": { diff --git a/here-naksha-lib-core/src/main/java/com/here/naksha/lib/core/IExtensionInit.java b/here-naksha-lib-core/src/main/java/com/here/naksha/lib/core/IExtensionInit.java new file mode 100644 index 000000000..33d21a305 --- /dev/null +++ b/here-naksha-lib-core/src/main/java/com/here/naksha/lib/core/IExtensionInit.java @@ -0,0 +1,42 @@ +/* + * Copyright (C) 2017-2024 HERE Europe B.V. + * + * 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. + * + * SPDX-License-Identifier: Apache-2.0 + * License-Filename: LICENSE + */ +package com.here.naksha.lib.core; + +import com.here.naksha.lib.core.models.features.Extension; + +/** + * Naksha Extension Interface for all extensions providing initClassName. + */ +public interface IExtensionInit { + + /** + * Initializes the extension with the specified hub and extension parameters. + * This method should be called to set up any necessary resources or configurations + * required by the extension to operate correctly. + * @param hub The hub instance to be used by the extension. + * @param extension Extension configuration supplied as part of deployment pipeline for respective Extension and sub-env. + */ + void init(INaksha hub, Extension extension); + + /** + * Closes the extension. This method should be called to ensure proper + * cleanup when the extension is no longer needed. + */ + void close(); +} diff --git a/here-naksha-lib-core/src/main/java/com/here/naksha/lib/core/NakshaVersion.java b/here-naksha-lib-core/src/main/java/com/here/naksha/lib/core/NakshaVersion.java index fa997276c..0cfa490c6 100644 --- a/here-naksha-lib-core/src/main/java/com/here/naksha/lib/core/NakshaVersion.java +++ b/here-naksha-lib-core/src/main/java/com/here/naksha/lib/core/NakshaVersion.java @@ -57,12 +57,14 @@ public class NakshaVersion implements Comparable { public static final String v2_0_20 = "2.0.20"; public static final String v2_1_0 = "2.1.0"; public static final String v2_1_1 = "2.1.1"; + public static final String v2_2_0 = "2.2.0"; + public static final String v2_2_1 = "2.2.1"; /** * The latest version of the naksha-extension stored in the resources. */ @AvailableSince(v2_0_5) - public static final NakshaVersion latest = of(v2_1_1); + public static final NakshaVersion latest = of(v2_2_1); private final int major; private final int minor; diff --git a/here-naksha-lib-ext-manager/src/main/java/com/here/naksha/lib/extmanager/ExtensionCache.java b/here-naksha-lib-ext-manager/src/main/java/com/here/naksha/lib/extmanager/ExtensionCache.java index e45229fb1..847331982 100644 --- a/here-naksha-lib-ext-manager/src/main/java/com/here/naksha/lib/extmanager/ExtensionCache.java +++ b/here-naksha-lib-ext-manager/src/main/java/com/here/naksha/lib/extmanager/ExtensionCache.java @@ -18,6 +18,7 @@ */ package com.here.naksha.lib.extmanager; +import com.here.naksha.lib.core.IExtensionInit; import com.here.naksha.lib.core.INaksha; import com.here.naksha.lib.core.SimpleTask; import com.here.naksha.lib.core.models.ExtensionConfig; @@ -29,7 +30,6 @@ import com.here.naksha.lib.extmanager.models.KVPair; import java.io.File; import java.io.IOException; -import java.lang.reflect.InvocationTargetException; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -46,10 +46,10 @@ */ public class ExtensionCache { private static final @NotNull Logger logger = LoggerFactory.getLogger(ExtensionCache.class); - private static final ConcurrentHashMap> loaderCache = - new ConcurrentHashMap<>(); + private static final ConcurrentHashMap loaderCache = new ConcurrentHashMap<>(); private static final Map jarClientMap = new HashMap<>(); private final @NotNull INaksha naksha; + private static final String WHITE_LIST_CLASSES = "whitelistClasses"; static { jarClientMap.put(JarClientType.S3.getType(), new AmazonS3Helper()); @@ -90,10 +90,8 @@ protected void buildExtensionCache(ExtensionConfig extensionConfig) { for (String key : loaderCache.keySet()) { if (!extIds.contains(key)) { - loaderCache.remove(key); - PluginCache.removeExtensionCache(key); - logger.info("Extension {} removed from cache.", key); - } + removeExtensionFromCache(key); + } } logger.info("Extension cache size " + loaderCache.size()); } @@ -103,23 +101,35 @@ private void publishIntoCache(KVPair result, ExtensionConfig ex final Extension extension = result.getKey(); final String extensionIdWthEnv = extension.getEnv() + ":" + extension.getId(); final File jarFile = result.getValue(); + IExtensionInit initObj = null; ClassLoader loader; try { - loader = ClassLoaderHelper.getClassLoader(jarFile, extensionConfig.getWhilelistDelegateClass()); + @SuppressWarnings("unchecked") + List whitelistClasses = (List) extension + .getProperties() + .getOrDefault(WHITE_LIST_CLASSES, extensionConfig.getWhilelistDelegateClass()); + loader = ClassLoaderHelper.getClassLoader(jarFile, whitelistClasses); } catch (Exception e) { - logger.error("Failed to load extension jar " + extension.getId(), e); + logger.error("Failed to load extension jar " + extensionIdWthEnv, e); return; } if (!isNullOrEmpty(extension.getInitClassName())) { try { Class clz = loader.loadClass(extension.getInitClassName()); - clz.getConstructor(INaksha.class, Extension.class).newInstance(naksha, extension); - } catch (ClassNotFoundException - | InvocationTargetException - | InstantiationException - | NoSuchMethodException - | IllegalAccessException e) { + Object obj = clz.getConstructor().newInstance(); + if (obj instanceof IExtensionInit initInstance) { + initInstance.init(naksha, extension); + initObj = initInstance; + logger.info( + "Extension {} initialization using initClassName {} done successfully.", + extensionIdWthEnv, + extension.getInitClassName()); + } else { + logger.error("Extension does not implement IExtensionInit for extension {}", extensionIdWthEnv); + return; + } + } catch (Exception e) { logger.error( "Failed to instantiate class {} for extension {} ", extension.getInitClassName(), @@ -128,28 +138,48 @@ private void publishIntoCache(KVPair result, ExtensionConfig ex return; } } - if (!isNullOrEmpty(extension.getInitClassName())) - logger.info( - "Extension {} initialization using initClassName {} done successfully.", - extensionIdWthEnv, - extension.getInitClassName()); - loaderCache.put(extensionIdWthEnv, new KVPair(extension, loader)); - PluginCache.removeExtensionCache(extensionIdWthEnv); + + ValueTuple previousValue = loaderCache.put(extensionIdWthEnv, new ValueTuple(extension, loader, initObj)); + if (previousValue != null) { + IExtensionInit previousInitObj = previousValue.getInstance(); + closeExtensionInstance(extensionIdWthEnv, previousInitObj); + } + logger.info( - "Extension id={}, version={} is successfully loaded into the cache, using Jar at {} for env={}.", + "Extension id={}, version={} is successfully loaded into the cache, using Jar at {}.", extensionIdWthEnv, extension.getVersion(), - extension.getUrl().substring(extension.getUrl().lastIndexOf("/") + 1), - extension.getEnv()); + extension.getUrl().substring(extension.getUrl().lastIndexOf("/") + 1)); + } + } + + private void removeExtensionFromCache(String extensionId) { + ValueTuple valueTuple = loaderCache.remove(extensionId); + PluginCache.removeExtensionCache(extensionId); + logger.info("Extension {} removed from cache.", extensionId); + if (valueTuple != null) { + IExtensionInit initObj = valueTuple.getInstance(); + closeExtensionInstance(extensionId, initObj); + } + } + + private void closeExtensionInstance(String extensionId, IExtensionInit initObj) { + if (initObj != null) { + try { + initObj.close(); + logger.info("Extension {} closed successfully.", extensionId); + } catch (Exception e) { + logger.error("Failed to close extension {}", extensionId, e); + } } } private boolean isLoaderMappingExist(Extension extension) { final String extensionIdWthEnv = extension.getEnv() + ":" + extension.getId(); - KVPair existingMapping = loaderCache.get(extensionIdWthEnv); + ValueTuple existingMapping = loaderCache.get(extensionIdWthEnv); if (existingMapping == null) return false; - final Extension exExtension = existingMapping.getKey(); + final Extension exExtension = existingMapping.getExtension(); final String exInitClassName = isNullOrEmpty(exExtension.getInitClassName()) ? "" : exExtension.getInitClassName(); final String initClassName = isNullOrEmpty(extension.getInitClassName()) ? "" : extension.getInitClassName(); @@ -160,7 +190,7 @@ private boolean isLoaderMappingExist(Extension extension) { } /** - * Lamda function which will initiate the downloading for extension jar + * Lambda function which will initiate the downloading for extension jar */ private KVPair downloadJar(Extension extension) { logger.info("Downloading jar {} with version {} ", extension.getId(), extension.getVersion()); @@ -184,8 +214,8 @@ protected FileClient getJarClient(String url) { } protected ClassLoader getClassLoaderById(@NotNull String extensionId) { - KVPair mappedLoader = loaderCache.get(extensionId); - return mappedLoader == null ? null : mappedLoader.getValue(); + ValueTuple mappedLoader = loaderCache.get(extensionId); + return mappedLoader == null ? null : mappedLoader.getClassLoader(); } public int getCacheLength() { @@ -193,7 +223,7 @@ public int getCacheLength() { } public List getCachedExtensions() { - return loaderCache.values().stream().map(KVPair::getKey).toList(); + return loaderCache.values().stream().map(ValueTuple::getExtension).toList(); } private boolean isNullOrEmpty(String value) { diff --git a/here-naksha-lib-ext-manager/src/main/java/com/here/naksha/lib/extmanager/ValueTuple.java b/here-naksha-lib-ext-manager/src/main/java/com/here/naksha/lib/extmanager/ValueTuple.java new file mode 100644 index 000000000..6eca51d76 --- /dev/null +++ b/here-naksha-lib-ext-manager/src/main/java/com/here/naksha/lib/extmanager/ValueTuple.java @@ -0,0 +1,46 @@ +/* + * Copyright (C) 2017-2024 HERE Europe B.V. + * + * 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. + * + * SPDX-License-Identifier: Apache-2.0 + * License-Filename: LICENSE + */ +package com.here.naksha.lib.extmanager; + +import com.here.naksha.lib.core.IExtensionInit; +import com.here.naksha.lib.core.models.features.Extension; + +public class ValueTuple { + private final Extension extension; + private final ClassLoader classLoader; + private final IExtensionInit instance; + + public ValueTuple(Extension extension, ClassLoader classLoader, IExtensionInit instance) { + this.extension = extension; + this.classLoader = classLoader; + this.instance = instance; + } + + public Extension getExtension() { + return extension; + } + + public ClassLoader getClassLoader() { + return classLoader; + } + + public IExtensionInit getInstance() { + return instance; + } +} diff --git a/here-naksha-lib-hub/src/main/java/com/here/naksha/lib/hub/NakshaHub.java b/here-naksha-lib-hub/src/main/java/com/here/naksha/lib/hub/NakshaHub.java index a6f453bf2..0e30d9539 100644 --- a/here-naksha-lib-hub/src/main/java/com/here/naksha/lib/hub/NakshaHub.java +++ b/here-naksha-lib-hub/src/main/java/com/here/naksha/lib/hub/NakshaHub.java @@ -47,7 +47,6 @@ import com.here.naksha.lib.core.util.IoHelp; import com.here.naksha.lib.core.util.json.Json; import com.here.naksha.lib.core.util.storage.RequestHelper; -import com.here.naksha.lib.core.util.storage.ResultHelper; import com.here.naksha.lib.core.view.ViewDeserialize; import com.here.naksha.lib.extmanager.ExtensionManager; import com.here.naksha.lib.extmanager.IExtensionManager; @@ -55,9 +54,7 @@ import com.here.naksha.lib.hub.storages.NHAdminStorage; import com.here.naksha.lib.hub.storages.NHSpaceStorage; import com.here.naksha.lib.psql.PsqlStorage; - import java.util.*; - import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -242,8 +239,7 @@ private static WriteXyzCollections createAdminCollectionsRequest() { new Exception("Unable to read custom/default config from Admin DB. " + er, er.exception)); } else { try { - List nakshaHubConfigs = - readFeaturesFromResult(rdResult, NakshaHubConfig.class); + List nakshaHubConfigs = readFeaturesFromResult(rdResult, NakshaHubConfig.class); for (final NakshaHubConfig cfg : nakshaHubConfigs) { if (cfg.getId().equals(configId)) { customDbCfg = cfg; @@ -307,15 +303,21 @@ private static WriteXyzCollections createAdminCollectionsRequest() { try (IReadSession readSession = getAdminStorage().newReadSession(nakshaContext, false)) { rdResult = readSession.execute(request); } catch (Exception e) { - logger.error("Failed during reading extension handler configurations from collections {}. ", request.getCollections(), e); + logger.error( + "Failed during reading extension handler configurations from collections {}. ", + request.getCollections(), + e); throw new RuntimeException("Failed reading extension handler configurations", e); } final List eventHandlers; try { eventHandlers = readFeaturesFromResult(rdResult, EventHandler.class); - } catch (NoCursor e) { - logger.error("NoCursor exception encountered", e); - throw new RuntimeException("Failed to open cursor", e); + } catch (NoCursor | NoSuchElementException e) { + logger.info("No relevant handlers found for Extension loading", e); + return new ExtensionConfig( + System.currentTimeMillis() + nakshaHubConfig.extensionConfigParams.getIntervalMs(), + Collections.emptyList(), + null); } Set extensionIds = new HashSet<>(); @@ -356,7 +358,7 @@ private List loadExtensionConfigFromS3(String extensionRootPath, Set< } filePath = extensionRootPath + extensionIdWotEnv + "/" + extensionIdWotEnv + "-" + version + "." - + env.toLowerCase() + ".json"; + + env.toLowerCase() + ".json"; String exJson; try { exJson = s3Helper.getFileContent(filePath); diff --git a/here-naksha-lib-hub/src/main/java/com/here/naksha/lib/hub/NakshaHubConfig.java b/here-naksha-lib-hub/src/main/java/com/here/naksha/lib/hub/NakshaHubConfig.java index aa8a8f320..eab00743d 100644 --- a/here-naksha-lib-hub/src/main/java/com/here/naksha/lib/hub/NakshaHubConfig.java +++ b/here-naksha-lib-hub/src/main/java/com/here/naksha/lib/hub/NakshaHubConfig.java @@ -22,7 +22,6 @@ import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonInclude.Include; import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.annotation.JsonTypeName; import com.here.naksha.lib.core.NakshaVersion; import com.here.naksha.lib.core.models.geojson.implementation.XyzFeature; import com.here.naksha.lib.core.util.json.JsonSerializable; @@ -39,7 +38,6 @@ /** * The Naksha-Hub service configuration. */ -@JsonTypeName(value = "Config") public final class NakshaHubConfig extends XyzFeature implements JsonSerializable { private static final Logger logger = LoggerFactory.getLogger(NakshaHubConfig.class); @@ -81,21 +79,40 @@ public final class NakshaHubConfig extends XyzFeature implements JsonSerializabl return NakshaHub.class.getName(); } + /** + * Returns a default relative path to Private Key useful for JWT signing + * + * @return The default private key path + */ + public static @NotNull String defaultJwtPvtKeyPath() { + return "auth/jwt.key"; + } + + /** + * Returns default relative paths to Public Keys useful for JWT signature verification + * + * @return The default public key paths + */ + public static @NotNull String defaultJwtPubKeyPaths() { + return "auth/jwt.pub"; + } + @JsonCreator NakshaHubConfig( @JsonProperty("id") @NotNull String id, - @JsonProperty("hubClassName") @Nullable String hubClassName, - @JsonProperty("userAgent") @Nullable String userAgent, - @JsonProperty("appId") @Nullable String appId, - @JsonProperty("author") @Nullable String author, - @JsonProperty("httpPort") @Nullable Integer httpPort, - @JsonProperty("hostname") @Nullable String hostname, - @JsonProperty("endpoint") @Nullable String endpoint, - @JsonProperty("env") @Nullable String env, - @JsonProperty("webRoot") @Nullable String webRoot, + @JsonProperty(HUB_CLASS_NAME) @Nullable String hubClassName, + @JsonProperty(USER_AGENT) @Nullable String userAgent, + @JsonProperty(APP_ID) @Nullable String appId, + @JsonProperty(AUTHOR) @Nullable String author, + @JsonProperty(HTTP_PORT) @Nullable Integer httpPort, + @JsonProperty(HOSTNAME) @Nullable String hostname, + @JsonProperty(ENDPOINT) @Nullable String endpoint, + @JsonProperty(ENV) @Nullable String env, + @JsonProperty(WEB_ROOT) @Nullable String webRoot, @JsonProperty(NAKSHA_AUTH) @Nullable AuthorizationMode authMode, - @JsonProperty("jwtName") @Nullable String jwtName, - @JsonProperty("debug") @Nullable Boolean debug, + @JsonProperty(JWT_PVT_KEY_PATH) @Nullable String jwtPvtKeyPath, + @JsonProperty(JWT_PUB_KEY_PATHS) @Nullable String jwtPubKeyPaths, + @JsonProperty(DEBUG) @Nullable Boolean debug, @JsonProperty("maintenanceIntervalInMins") @Nullable Integer maintenanceIntervalInMins, @JsonProperty("maintenanceInitialDelayInMins") @Nullable Integer maintenanceInitialDelayInMins, @JsonProperty("maintenancePoolCoreSize") @Nullable Integer maintenancePoolCoreSize, @@ -107,8 +124,8 @@ public final class NakshaHubConfig extends XyzFeature implements JsonSerializabl @JsonProperty("maxPctParallelRequestsPerActor") @Nullable Integer maxPctParallelRequestsPerActor) { super(id); if (httpPort != null && (httpPort < 0 || httpPort > 65535)) { - logger.atError() - .setMessage("Invalid port in Naksha configuration: {}") + logger.atWarn() + .setMessage("Invalid port in Naksha configuration: {}, falling back to default \"8080\"") .addArgument(httpPort) .log(); httpPort = 8080; @@ -119,8 +136,8 @@ public final class NakshaHubConfig extends XyzFeature implements JsonSerializabl try { hostname = InetAddress.getLocalHost().getHostAddress(); } catch (UnknownHostException e) { - logger.atError() - .setMessage("Unable to resolve the hostname using Java's API.") + logger.atWarn() + .setMessage("Unable to resolve the hostname using Java's API, using default \"localhost\".") .setCause(e) .log(); hostname = "localhost"; @@ -167,7 +184,10 @@ public final class NakshaHubConfig extends XyzFeature implements JsonSerializabl this.env = env; this.webRoot = webRoot; this.authMode = (authMode == null) ? AuthorizationMode.JWT : authMode; - this.jwtName = jwtName != null && !jwtName.isEmpty() ? jwtName : "jwt"; + this.jwtPvtKeyPath = + (jwtPvtKeyPath != null && !jwtPvtKeyPath.isEmpty()) ? jwtPvtKeyPath : defaultJwtPvtKeyPath(); + this.jwtPubKeyPaths = + (jwtPubKeyPaths != null && !jwtPubKeyPaths.isEmpty()) ? jwtPubKeyPaths : defaultJwtPubKeyPaths(); this.userAgent = userAgent != null && !userAgent.isEmpty() ? userAgent : defaultAppName(); this.debug = Boolean.TRUE.equals(debug); this.maintenanceIntervalInMins = @@ -263,13 +283,25 @@ private String getEnv(String env) { @JsonProperty(WEB_ROOT) public final @Nullable String webRoot; - public static final String JWT_NAME = "jwtName"; + public static final String JWT_PVT_KEY_PATH = "jwtPvtKeyPath"; + /** + * The relative path to Private key file to support JWT signing (e.g. {@code "auth/jwt.key"}). + * The path should be relative to the directory where config file is supplied. + * For example - if config file is {@code "/home/config/cloud-config.json"} then the key path {@code "auth/jwt.key"} + * will be considered relative to {@code "/home/config"} folder, resulting into absolute path as {@code "/home/config/auth/jwt.key}" + */ + @JsonProperty(JWT_PVT_KEY_PATH) + public final @NotNull String jwtPvtKeyPath; + public static final String JWT_PUB_KEY_PATHS = "jwtPubKeyPaths"; /** - * The JWT key files to be read from the disk ({@code "~/.config/naksha/auth/$.(key|pub)"}). + * The comma separated relative paths to Public key files to support JWT signature verification (e.g. {@code "auth/jwt.pub,auth/jwt_2.pub"}). + * The path should be relative to the directory where config file is supplied. + * For example - if config file is {@code "/home/config/cloud-config.json"} then the key path {@code "auth/jwt.pub"} + * will be considered relative to {@code "/home/config"} folder, resulting into absolute path as {@code "/home/config/auth/jwt.pub}" */ - @JsonProperty(JWT_NAME) - public final @NotNull String jwtName; + @JsonProperty(JWT_PUB_KEY_PATHS) + public final @NotNull String jwtPubKeyPaths; public static final String USER_AGENT = "userAgent"; diff --git a/here-naksha-lib-hub/src/test/resources/unit_test_data/mock_config.json b/here-naksha-lib-hub/src/test/resources/unit_test_data/mock_config.json index 076b651fc..fe31543a6 100644 --- a/here-naksha-lib-hub/src/test/resources/unit_test_data/mock_config.json +++ b/here-naksha-lib-hub/src/test/resources/unit_test_data/mock_config.json @@ -1,6 +1,6 @@ { "id": "mock-config", - "type": "Config", + "type": "Feature", "httpPort": 8080, "//hostname": "some-instance.aws.com",