From 6522809dce24fd07d3676a284a5e3a1fabfd398c Mon Sep 17 00:00:00 2001 From: Swordsteel Date: Tue, 24 Dec 2024 05:50:56 +0100 Subject: [PATCH] add Redis cache and some clean up - update and cleanup in README.md - update getIdentity to throw response exception with 401 in JwtService - update http files to use identity from env - add cacheable to getIdentityFromDevice in DeviceRegistryService - add RedisCacheConfiguration - add CacheProperty - set up cache property - set up data redis cache --- README.md | 90 +++++++++---------- build.gradle.kts | 2 + http/configuration.http | 4 +- http/http-client.env.json | 14 +-- http/measurement.http | 8 +- src/main/kotlin/ltd/hlaeja/Application.kt | 4 + .../configuration/RedisCacheConfiguration.kt | 26 ++++++ .../ltd/hlaeja/exception/CacheException.kt | 23 +++++ .../ltd/hlaeja/property/CacheProperty.kt | 8 ++ .../hlaeja/service/DeviceRegistryService.kt | 2 + .../kotlin/ltd/hlaeja/service/JwtService.kt | 12 ++- ...itional-spring-configuration-metadata.json | 5 ++ src/main/resources/application.yml | 15 ++++ src/test/resources/application.yml | 2 + 14 files changed, 153 insertions(+), 62 deletions(-) create mode 100644 src/main/kotlin/ltd/hlaeja/configuration/RedisCacheConfiguration.kt create mode 100644 src/main/kotlin/ltd/hlaeja/exception/CacheException.kt create mode 100644 src/main/kotlin/ltd/hlaeja/property/CacheProperty.kt diff --git a/README.md b/README.md index cf2ed08..6343a42 100644 --- a/README.md +++ b/README.md @@ -5,26 +5,31 @@ Classes and endpoints, to shape and to steer, Devices and sensors, their purpose ## Properties for deployment | name | required | info | -|----------------------------------------------|----------|----------------------------------------------| -| spring.profiles.active | * | Spring Boot environment | -| server.port | * | HTTP port | -| server.ssl.enabled | * | HTTP Enable SSL | -| server.ssl.key-store | * | HTTP Keystore | -| server.ssl.key-store-type | * | HTTP Cert Type | -| server.ssl.key-store-password | ** | HTTP Cert Pass | -| jwt.public-key | * | JWT public key | -| device-registry.url | * | Device Register URL | -| device-data.url | * | Device Data URL | -| device-configuration.url | * | Device Configuration URL | +|----------------------------------------------|:--------:|----------------------------------------------| +| spring.profiles.active | ✓ | Spring Boot environment | +| server.port | ✓ | HTTP port | +| server.ssl.enabled | ✓ | HTTP Enable SSL | +| server.ssl.key-store | ✓ | HTTP Keystore | +| server.ssl.key-store-type | ✓ | HTTP Cert Type | +| server.ssl.key-store-password | ✗ | HTTP Cert Pass | +| spring.cache.type | | Cache type (redis) | +| spring.data.redis.host | ✓ | Redis host | +| spring.data.redis.port | | Redis port | +| spring.data.redis.database | ✓ | Redis database | +| cache.time-to-live | | Cache time to live (minutes) | +| jwt.public-key | ✓ | JWT public key | +| device-registry.url | ✓ | Device Register URL | +| device-data.url | ✓ | Device Data URL | +| device-configuration.url | ✓ | Device Configuration URL | | management.influx.metrics.export.api-version | | InfluxDB API version | | management.influx.metrics.export.enabled | | Enable/Disable exporting metrics to InfluxDB | -| management.influx.metrics.export.bucket | * | InfluxDB bucket name | -| management.influx.metrics.export.org | * | InfluxDB organization | -| management.influx.metrics.export.token | ** | InfluxDB token | -| management.influx.metrics.export.uri | * | InfluxDB URL | -| management.metrics.tags.application | * | Application instance tag for metrics | +| management.influx.metrics.export.bucket | ✓ | InfluxDB bucket name | +| management.influx.metrics.export.org | ✓ | InfluxDB organization | +| management.influx.metrics.export.token | ✗ | InfluxDB token | +| management.influx.metrics.export.uri | ✓ | InfluxDB URL | +| management.metrics.tags.application | ✓ | Application instance tag for metrics | -Required: * can be stored as text, and ** need to be stored as secret. +*Required: ✓ can be stored as text, and ✗ need to be stored as secret.* ## Releasing Service @@ -34,43 +39,32 @@ Run `release.sh` script from `master` branch. ### Developer Keystore -1. Open `hosts` file: - * On Unix-like systems (Linux, macOS), this directory is typically `/etc/hosts`. - * On Windows, this directory is typically `%SystemRoot%\System32\drivers\etc\hosts`. - -2. Add the following lines to the `hosts` file: - ```text - 127.0.0.1 deviceapi # Hlæja Device API - ``` - -3. Generate Keystores - ```shell - keytool -genkeypair -alias device-api -keyalg RSA -keysize 2048 -validity 3650 -dname "CN=deviceapi" -keypass password -keystore ./cert/keystore.p12 -storetype PKCS12 -storepass password - ``` - -4. Export the public certificate - ```shell - keytool -export -alias device-api -keystore ./cert/keystore.p12 -storepass password -file ./cert/device-api.cer -rfc - ``` +We use a keystore to enable HTTPS for our API. To set up your developer environment for local development, please refer to [generate keystore](https://github.com/swordsteel/hlaeja-development/blob/master/doc/keystore.md) documentation. When generating and exporting the certificate for local development, please store it in the `./cert/keystore.p12` folder at the project root. ### Public RSA Key -To validate devices, copy file named `public_key.pem` from `./cert` generated for local development in **Hlæja Device Register** in to `./cert`. +This service uses the public key from **[Hlæja Device Register](https://github.com/swordsteel/hlaeja-device-registry)** to identify devices. To set up device identification for local development, copy the `public_key.pem` file from the `./cert` directory in **Hlæja Device Register** into the `./cert` directory of this project. -### Global gradle properties +*Note: For more information on generating RSA keys, please refer to our [generate RSA key](https://github.com/swordsteel/hlaeja-development/blob/master/doc/rsa_key.md) documentation.* -To authenticate with Gradle to access repositories that require authentication, you can set your user and token in the `gradle.properties` file. +### Global Settings -Here's how you can do it: +This services rely on a set of global settings to configure development environments. These settings, managed through Gradle properties or environment variables. -1. Open or create the `gradle.properties` file in your Gradle user home directory: +*Note: For more information on global properties, please refer to our [global settings](https://github.com/swordsteel/hlaeja-development/blob/master/doc/global_settings.md) documentation.* - - On Unix-like systems (Linux, macOS), this directory is typically `~/.gradle/`. - - On Windows, this directory is typically `C:\Users\\.gradle\`. +#### Gradle Properties -2. Add the following lines to the `gradle.properties` file: - ```properties - repository.user=your_user - repository.token=your_token_value - ``` - or use environment variables `REPOSITORY_USER` and `REPOSITORY_TOKEN` +```properties +repository.user=your_user +repository.token=your_token_value +influxdb.token=your_token_value +``` + +#### Environment Variables + +```properties +REPOSITORY_USER=your_user +REPOSITORY_TOKEN=your_token_value +INFLUXDB_TOKEN=your_token_value +``` diff --git a/build.gradle.kts b/build.gradle.kts index 4ca4b1b..f80cd25 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -18,6 +18,8 @@ dependencies { implementation(hlaeja.micrometer.registry.influx) implementation(hlaeja.library.hlaeja.common.messages) implementation(hlaeja.springboot.starter.actuator) + implementation(hlaeja.springboot.starter.cache) + implementation(hlaeja.springboot.starter.redis) implementation(hlaeja.springboot.starter.webflux) runtimeOnly(hlaeja.jjwt.impl) diff --git a/http/configuration.http b/http/configuration.http index 8ec74ff..869dab4 100644 --- a/http/configuration.http +++ b/http/configuration.http @@ -1,3 +1,3 @@ -### get measurement +### get configuration GET {{hostname}}/configuration -Identity: 0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ.0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ.0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ +Identity: {{identity}} diff --git a/http/http-client.env.json b/http/http-client.env.json index da619a0..8c61560 100644 --- a/http/http-client.env.json +++ b/http/http-client.env.json @@ -1,8 +1,10 @@ { - "development": { - "hostname": "https://localhost:8443" - }, - "docker": { - "hostname": "https://localhost:9000" - } + "development": { + "hostname": "https://localhost:8443", + "identity": "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ.0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ.0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ" + }, + "docker": { + "hostname": "https://localhost:9000", + "identity": "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ.0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ.0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ" + } } diff --git a/http/measurement.http b/http/measurement.http index b8bb0cc..40367f3 100644 --- a/http/measurement.http +++ b/http/measurement.http @@ -1,11 +1,11 @@ ### get measurement GET {{hostname}}/measurement -Identity: 0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ.0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ.0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ +Identity: {{identity}} ### add measurement for all POST {{hostname}}/measurement Content-Type: application/json -Identity: 0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ.0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ.0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ +Identity: {{identity}} { "button0": 0, @@ -16,8 +16,8 @@ Identity: 0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ.0123456789ABCDEFGHIJKLMNOPQRSTUVW ### add measurement for one POST {{hostname}}/measurement Content-Type: application/json -Identity: 0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ.0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ.0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ +Identity: {{identity}} { - "button0": 0 + "button0": 1 } diff --git a/src/main/kotlin/ltd/hlaeja/Application.kt b/src/main/kotlin/ltd/hlaeja/Application.kt index a824e6a..8fc79fc 100644 --- a/src/main/kotlin/ltd/hlaeja/Application.kt +++ b/src/main/kotlin/ltd/hlaeja/Application.kt @@ -1,5 +1,6 @@ package ltd.hlaeja +import ltd.hlaeja.property.CacheProperty import ltd.hlaeja.property.DeviceConfigurationProperty import ltd.hlaeja.property.DeviceDataProperty import ltd.hlaeja.property.DeviceRegistryProperty @@ -7,8 +8,11 @@ import ltd.hlaeja.property.JwtProperty import org.springframework.boot.autoconfigure.SpringBootApplication import org.springframework.boot.context.properties.EnableConfigurationProperties import org.springframework.boot.runApplication +import org.springframework.cache.annotation.EnableCaching +@EnableCaching @EnableConfigurationProperties( + CacheProperty::class, DeviceConfigurationProperty::class, DeviceDataProperty::class, DeviceRegistryProperty::class, diff --git a/src/main/kotlin/ltd/hlaeja/configuration/RedisCacheConfiguration.kt b/src/main/kotlin/ltd/hlaeja/configuration/RedisCacheConfiguration.kt new file mode 100644 index 0000000..764fb91 --- /dev/null +++ b/src/main/kotlin/ltd/hlaeja/configuration/RedisCacheConfiguration.kt @@ -0,0 +1,26 @@ +package ltd.hlaeja.configuration + +import java.time.Duration +import ltd.hlaeja.exception.CacheException +import ltd.hlaeja.property.CacheProperty +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import org.springframework.data.redis.cache.RedisCacheConfiguration +import org.springframework.data.redis.cache.RedisCacheManager +import org.springframework.data.redis.core.RedisTemplate + +@Configuration +class RedisCacheConfiguration( + private val cacheProperty: CacheProperty, +) { + + @Bean + fun cacheManager( + redisTemplate: RedisTemplate, + ): RedisCacheManager = redisTemplate.connectionFactory + ?.let { RedisCacheManager.builder(it).cacheDefaults(getRedisCacheConfiguration()).build() } + ?: throw CacheException("Redis connection factory is not set") + + private fun getRedisCacheConfiguration(): RedisCacheConfiguration = RedisCacheConfiguration.defaultCacheConfig() + .entryTtl(Duration.ofMinutes(cacheProperty.timeToLive)) +} diff --git a/src/main/kotlin/ltd/hlaeja/exception/CacheException.kt b/src/main/kotlin/ltd/hlaeja/exception/CacheException.kt new file mode 100644 index 0000000..a5b3a0f --- /dev/null +++ b/src/main/kotlin/ltd/hlaeja/exception/CacheException.kt @@ -0,0 +1,23 @@ +package ltd.hlaeja.exception + +@Suppress("unused") +class CacheException : Exception { + + constructor() : super() + + constructor(message: String) : super(message) + + constructor(cause: Throwable) : super(cause) + + constructor( + message: String, + cause: Throwable, + ) : super(message, cause) + + constructor( + message: String, + cause: Throwable, + enableSuppression: Boolean, + writableStackTrace: Boolean, + ) : super(message, cause, enableSuppression, writableStackTrace) +} diff --git a/src/main/kotlin/ltd/hlaeja/property/CacheProperty.kt b/src/main/kotlin/ltd/hlaeja/property/CacheProperty.kt new file mode 100644 index 0000000..8ca3658 --- /dev/null +++ b/src/main/kotlin/ltd/hlaeja/property/CacheProperty.kt @@ -0,0 +1,8 @@ +package ltd.hlaeja.property + +import org.springframework.boot.context.properties.ConfigurationProperties + +@ConfigurationProperties(prefix = "cache") +data class CacheProperty( + val timeToLive: Long, +) diff --git a/src/main/kotlin/ltd/hlaeja/service/DeviceRegistryService.kt b/src/main/kotlin/ltd/hlaeja/service/DeviceRegistryService.kt index ed755b2..ef4d035 100644 --- a/src/main/kotlin/ltd/hlaeja/service/DeviceRegistryService.kt +++ b/src/main/kotlin/ltd/hlaeja/service/DeviceRegistryService.kt @@ -7,6 +7,7 @@ import java.util.UUID import ltd.hlaeja.library.deviceRegistry.Identity import ltd.hlaeja.property.DeviceRegistryProperty import ltd.hlaeja.util.deviceRegistryIdentityDevice +import org.springframework.cache.annotation.Cacheable import org.springframework.http.HttpStatus.SERVICE_UNAVAILABLE import org.springframework.stereotype.Service import org.springframework.web.ErrorResponseException @@ -31,6 +32,7 @@ class DeviceRegistryService( .description("Number of failed device identity calls") .register(meterRegistry) + @Cacheable(value = ["identity"], key = "#device") suspend fun getIdentityFromDevice( device: UUID, ): Identity.Response = try { diff --git a/src/main/kotlin/ltd/hlaeja/service/JwtService.kt b/src/main/kotlin/ltd/hlaeja/service/JwtService.kt index 1697a1e..5f575e9 100644 --- a/src/main/kotlin/ltd/hlaeja/service/JwtService.kt +++ b/src/main/kotlin/ltd/hlaeja/service/JwtService.kt @@ -1,13 +1,16 @@ package ltd.hlaeja.service import io.github.oshai.kotlinlogging.KotlinLogging +import io.jsonwebtoken.JwtException import io.jsonwebtoken.JwtParser import io.jsonwebtoken.Jwts import java.util.UUID import ltd.hlaeja.library.deviceRegistry.Identity import ltd.hlaeja.property.JwtProperty import ltd.hlaeja.util.PublicKeyProvider +import org.springframework.http.HttpStatus import org.springframework.stereotype.Service +import org.springframework.web.server.ResponseStatusException private val log = KotlinLogging.logger {} @@ -23,8 +26,13 @@ class JwtService( suspend fun getIdentity( identityToken: String, - ): Identity.Response = readIdentity(identityToken) - .let { deviceRegistry.getIdentityFromDevice(it) } + ): Identity.Response = try { + readIdentity(identityToken) + .let { deviceRegistry.getIdentityFromDevice(it) } + } catch (e: JwtException) { + log.warn { e.localizedMessage } + throw ResponseStatusException(HttpStatus.UNAUTHORIZED) + } private suspend fun readIdentity( identity: String, diff --git a/src/main/resources/META-INF/additional-spring-configuration-metadata.json b/src/main/resources/META-INF/additional-spring-configuration-metadata.json index f1f145b..35b28c5 100644 --- a/src/main/resources/META-INF/additional-spring-configuration-metadata.json +++ b/src/main/resources/META-INF/additional-spring-configuration-metadata.json @@ -39,6 +39,11 @@ "name": "device-configuration.url", "type": "java.lang.String", "description": "Url for device configuration service." + }, + { + "name": "cache.time-to-live", + "type": "java.lang.Long", + "description": "Cache expiration in minutes." } ] } diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 41309d1..f88b098 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -9,6 +9,12 @@ spring: os: name: "%APP_BUILD_OS_NAME%" version: "%APP_BUILD_OS_VERSION%" + cache: + type: redis + data: + redis: + port: 6379 + database: 1 management: endpoints: @@ -29,6 +35,9 @@ management: bucket: hlaeja org: hlaeja_ltd +cache: + time-to-live: 10 + jwt: public-key: cert/public_key.pem @@ -40,6 +49,9 @@ spring: config: activate: on-profile: development + data: + redis: + host: localhost management: metrics: @@ -76,6 +88,9 @@ spring: config: activate: on-profile: docker + data: + redis: + host: Redis management: metrics: diff --git a/src/test/resources/application.yml b/src/test/resources/application.yml index d924ee7..cc03713 100644 --- a/src/test/resources/application.yml +++ b/src/test/resources/application.yml @@ -6,3 +6,5 @@ device-data: url: http://localhost:9020 device-configuration: url: http://localhost:9030 +cache: + time-to-live: 10