4 Commits

Author SHA1 Message Date
f52f1237a2 [RELEASE] - release version: 0.4.0 2025-01-02 07:24:12 +01:00
4130ba681c update addDevice to handle violates of foreign key in DeviceService 2025-01-02 06:53:20 +01:00
df9d2c59a4 replace local jwt with library version
- update DeviceController to handle hlaeja jwt instead of jwtService
- update mapper sign with hlaeja jwt instead of jwtService
- add dependency for hlaeja jwt
- remove dependencies for jjwt
- remove JwtService.kt
- remove PrivateKeyProvider.kt
- remove jwt key property explanation from additional-spring-configuration-metadata.json
2025-01-02 06:53:20 +01:00
7d4ebab8f8 [RELEASE] - bump version 2024-12-28 07:43:32 +01:00
12 changed files with 27 additions and 197 deletions

View File

@@ -9,17 +9,15 @@ plugins {
dependencies { dependencies {
implementation(hlaeja.fasterxml.jackson.module.kotlin) implementation(hlaeja.fasterxml.jackson.module.kotlin)
implementation(hlaeja.jjwt.api)
implementation(hlaeja.kotlin.logging) implementation(hlaeja.kotlin.logging)
implementation(hlaeja.kotlin.reflect) implementation(hlaeja.kotlin.reflect)
implementation(hlaeja.kotlinx.coroutines) implementation(hlaeja.kotlinx.coroutines)
implementation(hlaeja.library.hlaeja.common.messages) implementation(hlaeja.library.hlaeja.common.messages)
implementation(hlaeja.library.hlaeja.jwt)
implementation(hlaeja.springboot.starter.actuator) implementation(hlaeja.springboot.starter.actuator)
implementation(hlaeja.springboot.starter.r2dbc) implementation(hlaeja.springboot.starter.r2dbc)
implementation(hlaeja.springboot.starter.webflux) implementation(hlaeja.springboot.starter.webflux)
runtimeOnly(hlaeja.jjwt.impl)
runtimeOnly(hlaeja.jjwt.jackson)
runtimeOnly(hlaeja.postgresql) runtimeOnly(hlaeja.postgresql)
runtimeOnly(hlaeja.postgresql.r2dbc) runtimeOnly(hlaeja.postgresql.r2dbc)

View File

@@ -1,4 +1,4 @@
kotlin.code.style=official kotlin.code.style=official
version=0.3.0 version=0.4.0
catalog=0.7.0 catalog=0.8.0
container.port.host=9010 container.port.host=9010

View File

@@ -1,9 +1,9 @@
package ltd.hlaeja.controller package ltd.hlaeja.controller
import java.util.UUID import java.util.UUID
import ltd.hlaeja.jwt.service.PrivateJwtService
import ltd.hlaeja.library.deviceRegistry.Device import ltd.hlaeja.library.deviceRegistry.Device
import ltd.hlaeja.service.DeviceService import ltd.hlaeja.service.DeviceService
import ltd.hlaeja.service.JwtService
import ltd.hlaeja.util.toDeviceResponse import ltd.hlaeja.util.toDeviceResponse
import org.springframework.web.bind.annotation.GetMapping import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.PathVariable import org.springframework.web.bind.annotation.PathVariable
@@ -14,18 +14,18 @@ import org.springframework.web.bind.annotation.RestController
@RestController @RestController
class DeviceController( class DeviceController(
private val deviceService: DeviceService, private val deviceService: DeviceService,
private val jwtService: JwtService, private val privateJwtService: PrivateJwtService,
) { ) {
@PostMapping("/device") @PostMapping("/device")
suspend fun addDevice( suspend fun addDevice(
@RequestBody request: Device.Request, @RequestBody request: Device.Request,
): Device.Response = deviceService.addDevice(request.type) ): Device.Response = deviceService.addDevice(request.type)
.toDeviceResponse(jwtService) .toDeviceResponse(privateJwtService)
@GetMapping("/device-{device}") @GetMapping("/device-{device}")
suspend fun getDevice( suspend fun getDevice(
@PathVariable device: UUID, @PathVariable device: UUID,
): Device.Response = deviceService.getDevice(device) ): Device.Response = deviceService.getDevice(device)
.toDeviceResponse(jwtService) .toDeviceResponse(privateJwtService)
} }

View File

@@ -5,6 +5,8 @@ import java.time.ZonedDateTime
import java.util.UUID import java.util.UUID
import ltd.hlaeja.entity.DeviceEntity import ltd.hlaeja.entity.DeviceEntity
import ltd.hlaeja.repository.DeviceRepository import ltd.hlaeja.repository.DeviceRepository
import org.springframework.dao.DataIntegrityViolationException
import org.springframework.http.HttpStatus.BAD_REQUEST
import org.springframework.http.HttpStatus.NOT_FOUND import org.springframework.http.HttpStatus.NOT_FOUND
import org.springframework.stereotype.Service import org.springframework.stereotype.Service
import org.springframework.web.server.ResponseStatusException import org.springframework.web.server.ResponseStatusException
@@ -18,8 +20,13 @@ class DeviceService(
suspend fun addDevice( suspend fun addDevice(
type: UUID, type: UUID,
): DeviceEntity = deviceRepository.save(DeviceEntity(null, ZonedDateTime.now(), type)) ): DeviceEntity = try {
.also { log.debug { "Added device ${it.id}" } } deviceRepository.save(DeviceEntity(null, ZonedDateTime.now(), type))
.also { log.debug { "Added device ${it.id}" } }
} catch (e: DataIntegrityViolationException) {
log.warn { e.localizedMessage }
throw ResponseStatusException(BAD_REQUEST)
}
suspend fun getDevice(device: UUID): DeviceEntity = deviceRepository.findById(device) suspend fun getDevice(device: UUID): DeviceEntity = deviceRepository.findById(device)
?.also { log.debug { "Get device ${it.id}" } } ?.also { log.debug { "Get device ${it.id}" } }

View File

@@ -1,25 +0,0 @@
package ltd.hlaeja.service
import io.jsonwebtoken.Jwts
import java.security.interfaces.RSAPrivateKey
import java.util.UUID
import ltd.hlaeja.property.JwtProperty
import ltd.hlaeja.util.PrivateKeyProvider
import org.springframework.stereotype.Service
@Service
class JwtService(
jwtProperty: JwtProperty,
) {
private var privateKey: RSAPrivateKey = PrivateKeyProvider.load(jwtProperty.privateKey)
suspend fun makeIdentity(device: UUID): String {
return Jwts.builder()
.claims()
.add("device", device)
.and()
.signWith(privateKey)
.compact()
}
}

View File

@@ -4,11 +4,11 @@ import java.time.ZonedDateTime
import ltd.hlaeja.entity.DeviceEntity import ltd.hlaeja.entity.DeviceEntity
import ltd.hlaeja.entity.NodeEntity import ltd.hlaeja.entity.NodeEntity
import ltd.hlaeja.entity.TypeEntity import ltd.hlaeja.entity.TypeEntity
import ltd.hlaeja.jwt.service.PrivateJwtService
import ltd.hlaeja.library.deviceRegistry.Device import ltd.hlaeja.library.deviceRegistry.Device
import ltd.hlaeja.library.deviceRegistry.Identity import ltd.hlaeja.library.deviceRegistry.Identity
import ltd.hlaeja.library.deviceRegistry.Node import ltd.hlaeja.library.deviceRegistry.Node
import ltd.hlaeja.library.deviceRegistry.Type import ltd.hlaeja.library.deviceRegistry.Type
import ltd.hlaeja.service.JwtService
import org.springframework.http.HttpStatus.EXPECTATION_FAILED import org.springframework.http.HttpStatus.EXPECTATION_FAILED
import org.springframework.web.server.ResponseStatusException import org.springframework.web.server.ResponseStatusException
@@ -40,10 +40,10 @@ fun NodeEntity.toIdentityResponse(): Identity.Response = Identity.Response(
device, device,
) )
suspend fun DeviceEntity.toDeviceResponse( fun DeviceEntity.toDeviceResponse(
jwtService: JwtService, jwtService: PrivateJwtService,
): Device.Response = Device.Response( ): Device.Response = Device.Response(
id ?: throw ResponseStatusException(EXPECTATION_FAILED), id ?: throw ResponseStatusException(EXPECTATION_FAILED),
type, type,
jwtService.makeIdentity(id), jwtService.sign("device" to id),
) )

View File

@@ -1,35 +0,0 @@
package ltd.hlaeja.util
import java.security.KeyFactory
import java.security.interfaces.RSAPrivateKey
import java.security.spec.PKCS8EncodedKeySpec
import java.util.Base64.getDecoder
import ltd.hlaeja.exception.KeyProviderException
object PrivateKeyProvider {
fun load(
pemFile: String,
): RSAPrivateKey = readPrivatePemFile(pemFile)
.let(::makePrivateKey)
private fun makePrivateKey(
privateKeyBytes: ByteArray,
): RSAPrivateKey = KeyFactory.getInstance("RSA")
.generatePrivate(PKCS8EncodedKeySpec(privateKeyBytes)) as RSAPrivateKey
private fun readPrivatePemFile(
privateKey: String,
): ByteArray = javaClass.classLoader
.getResource(privateKey)
?.readText()
?.let(::getPrivateKeyByteArray)
?: throw KeyProviderException("Could not load private key")
private fun getPrivateKeyByteArray(
keyText: String,
): ByteArray = keyText.replace(Regex("[\r\n]+"), "")
.removePrefix("-----BEGIN PRIVATE KEY-----")
.removeSuffix("-----END PRIVATE KEY-----")
.let { getDecoder().decode(it) }
}

View File

@@ -19,11 +19,6 @@
"name": "spring.application.build.os.version", "name": "spring.application.build.os.version",
"type": "java.lang.String", "type": "java.lang.String",
"description": "Application build os version." "description": "Application build os version."
},
{
"name": "jwt.private-key",
"type": "java.lang.String",
"description": "Jwt private key file."
} }
] ]
} }

View File

@@ -9,9 +9,9 @@ import java.time.ZonedDateTime
import java.util.UUID import java.util.UUID
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
import ltd.hlaeja.entity.DeviceEntity import ltd.hlaeja.entity.DeviceEntity
import ltd.hlaeja.jwt.service.PrivateJwtService
import ltd.hlaeja.library.deviceRegistry.Device import ltd.hlaeja.library.deviceRegistry.Device
import ltd.hlaeja.service.DeviceService import ltd.hlaeja.service.DeviceService
import ltd.hlaeja.service.JwtService
import org.assertj.core.api.Assertions.assertThat import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Nested import org.junit.jupiter.api.Nested
@@ -28,13 +28,13 @@ class DeviceControllerTest {
} }
val deviceService: DeviceService = mockk() val deviceService: DeviceService = mockk()
val jwtService: JwtService = mockk() val privateJwtService: PrivateJwtService = mockk()
lateinit var controller: DeviceController lateinit var controller: DeviceController
@BeforeEach @BeforeEach
fun setUp() { fun setUp() {
controller = DeviceController(deviceService, jwtService) controller = DeviceController(deviceService, privateJwtService)
} }
@Nested @Nested
@@ -45,14 +45,14 @@ class DeviceControllerTest {
// given // given
val request = Device.Request(uuid) val request = Device.Request(uuid)
coEvery { deviceService.addDevice(any()) } returns DeviceEntity(uuid, timestamp, uuid) coEvery { deviceService.addDevice(any()) } returns DeviceEntity(uuid, timestamp, uuid)
coEvery { jwtService.makeIdentity(any()) } returns PAYLOAD coEvery { privateJwtService.sign(any()) } returns PAYLOAD
// when // when
val response = controller.addDevice(request) val response = controller.addDevice(request)
// then // then
coVerify(exactly = 1) { deviceService.addDevice(any()) } coVerify(exactly = 1) { deviceService.addDevice(any()) }
coVerify(exactly = 1) { jwtService.makeIdentity(any()) } coVerify(exactly = 1) { privateJwtService.sign(any()) }
assertThat(response.identity).isEqualTo(PAYLOAD) assertThat(response.identity).isEqualTo(PAYLOAD)
} }
@@ -80,14 +80,14 @@ class DeviceControllerTest {
fun `get device - success`() = runTest { fun `get device - success`() = runTest {
// given // given
coEvery { deviceService.getDevice(any()) } returns DeviceEntity(uuid, timestamp, uuid) coEvery { deviceService.getDevice(any()) } returns DeviceEntity(uuid, timestamp, uuid)
coEvery { jwtService.makeIdentity(any()) } returns PAYLOAD coEvery { privateJwtService.sign(any()) } returns PAYLOAD
// when // when
val response = controller.getDevice(uuid) val response = controller.getDevice(uuid)
// then // then
coVerify(exactly = 1) { deviceService.getDevice(any()) } coVerify(exactly = 1) { deviceService.getDevice(any()) }
coVerify(exactly = 1) { jwtService.makeIdentity(any()) } coVerify(exactly = 1) { privateJwtService.sign(any()) }
assertThat(response.identity).isEqualTo(PAYLOAD) assertThat(response.identity).isEqualTo(PAYLOAD)
} }

View File

@@ -1,31 +0,0 @@
package ltd.hlaeja.service
import java.util.UUID
import kotlinx.coroutines.test.runTest
import ltd.hlaeja.property.JwtProperty
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
class JwtServiceTest {
val property: JwtProperty = JwtProperty("cert/valid-private-key.pem")
lateinit var service: JwtService
@BeforeEach
fun setUp() {
service = JwtService(property)
}
@Test
fun `should generate a JWT successfully with a valid private key`() = runTest {
// given
val deviceId = UUID.fromString("00000000-0000-0000-0000-000000000000")
// when
val jwt = service.makeIdentity(deviceId)
// then
assertThat(jwt).contains("eyJkZXZpY2UiOiIwMDAwMDAwMC0wMDAwLTAwMDAtMDAwMC0wMDAwMDAwMDAwMDAifQ")
}
}

View File

@@ -1,51 +0,0 @@
package ltd.hlaeja.util
import java.security.interfaces.RSAPrivateKey
import ltd.hlaeja.exception.KeyProviderException
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.assertThrows
class PrivateKeyProviderTest {
@Test
fun `load private key - success`() {
// given
val pemFilePath = "cert/valid-private-key.pem"
// when
val privateKey: RSAPrivateKey = PrivateKeyProvider.load(pemFilePath)
// then
assertThat(privateKey).isNotNull
assertThat(privateKey.algorithm).isEqualTo("RSA")
}
@Test
fun `load private key - file does not exist`() {
// given
val nonExistentPemFilePath = "cert/non-existent.pem"
// when exception
val exception = assertThrows<KeyProviderException> {
PrivateKeyProvider.load(nonExistentPemFilePath)
}
// then
assertThat(exception.message).isEqualTo("Could not load private key")
}
@Test
fun `load private key - file is invalid`() {
// given
val invalidPemFilePath = "cert/invalid-private-key.pem"
// when exception
val exception = assertThrows<IllegalArgumentException> {
PrivateKeyProvider.load(invalidPemFilePath)
}
// then
assertThat(exception.message).contains("Input byte array has wrong 4-byte ending unit")
}
}

View File

@@ -1,28 +0,0 @@
-----BEGIN PRIVATE KEY-----
VEhJUyBJUyBKVU5LIFRISVMgSVMgSlVOSyBUSElTIElTIEpVTksgVEhJUyBJUyBK
VU5LIFRISVMgSVMgSlVOSyBUSElTIElTIEpVTksgVEhJUyBJUyBKVU5LIFRISVMg
SVMgSlVOSyBUSElTIElTIEpVTksgVEhJUyBJUyBKVU5LIFRISVMgSVMgSlVOSyBU
SElTIElTIEpVTksgVEhJUyBJUyBKVU5LIFRISVMgSVMgSlVOSyBUSElTIElTIEpV
TksgVEhJUyBJUyBKVU5LIFRISVMgSVMgSlVOSyBUSElTIElTIEpVTksgVEhJUyBJ
UyBKVU5LIFRISVMgSVMgSlVOSyBUSElTIElTIEpVTksgVEhJUyBJUyBKVU5LIFRI
SVMgSVMgSlVOSyBUSElTIElTIEpVTksgVEhJUyBJUyBKVU5LIFRISVMgSVMgSlVO
SyBUSElTIElTIEpVTksgVEhJUyBJUyBKVU5LIFRISVMgSVMgSlVOSyBUSElTIElT
IEpVTksgVEhJUyBJUyBKVU5LIFRISVMgSVMgSlVOSyBUSElTIElTIEpVTksgVEhJ
UyBJUyBKVU5LIFRISVMgSVMgSlVOSyBUSElTIElTIEpVTksgVEhJUyBJUyBKVU5L
IFRISVMgSVMgSlVOSyBUSElTIElTIEpVTksgVEhJUyBJUyBKVU5LIFRISVMgSVMg
SlVOSyBUSElTIElTIEpVTksgVEhJUyBJUyBKVU5LIFRISVMgSVMgSlVOSyBUSElT
IElTIEpVTksgVEhJUyBJUyBKVU5LIFRISVMgSVMgSlVOSyBUSElTIElTIEpVTksg
VEhJUyBJUyBKVU5LIFRISVMgSVMgSlVOSyBUSElTIElTIEpVTksgVEhJUyBJUyBK
VU5LIFRISVMgSVMgSlVOSyBUSElTIElTIEpVTksgVEhJUyBJUyBKVU5LIFRISVMg
SVMgSlVOSyBUSElTIElTIEpVTksgVEhJUyBJUyBKVU5LIFRISVMgSVMgSlVOSyBU
SElTIElTIEpVTksgVEhJUyBJUyBKVU5LIFRISVMgSVMgSlVOSyBUSElTIElTIEpV
TksgVEhJUyBJUyBKVU5LIFRISVMgSVMgSlVOSyBUSElTIElTIEpVTksgVEhJUyBJ
UyBKVU5LIFRISVMgSVMgSlVOSyBUSElTIElTIEpVTksgVEhJUyBJUyBKVU5LIFRI
SVMgSVMgSlVOSyBUSElTIElTIEpVTksgVEhJUyBJUyBKVU5LIFRISVMgSVMgSlVO
SyBUSElTIElTIEpVTksgVEhJUyBJUyBKVU5LIFRISVMgSVMgSlVOSyBUSElTIElT
IEpVTksgVEhJUyBJUyBKVU5LIFRISVMgSVMgSlVOSyBUSElTIElTIEpVTksgVEhJ
UyBJUyBKVU5LIFRISVMgSVMgSlVOSyBUSElTIElTIEpVTksgVEhJUyBJUyBKVU5L
IFRISVMgSVMgSlVOSyBUSElTIElTIEpVTksgVEhJUyBJUyBKVU5LIFRISVMgSVMg
SlVOSyBUSElTIElTIEpVTksgVEhJUyBJUyBKVU5LIFRISVMgSVMgSlVOSyBUSElT
IElTIEpVTksg==
-----END PRIVATE KEY-----