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 {
implementation(hlaeja.fasterxml.jackson.module.kotlin)
implementation(hlaeja.jjwt.api)
implementation(hlaeja.kotlin.logging)
implementation(hlaeja.kotlin.reflect)
implementation(hlaeja.kotlinx.coroutines)
implementation(hlaeja.library.hlaeja.common.messages)
implementation(hlaeja.library.hlaeja.jwt)
implementation(hlaeja.springboot.starter.actuator)
implementation(hlaeja.springboot.starter.r2dbc)
implementation(hlaeja.springboot.starter.webflux)
runtimeOnly(hlaeja.jjwt.impl)
runtimeOnly(hlaeja.jjwt.jackson)
runtimeOnly(hlaeja.postgresql)
runtimeOnly(hlaeja.postgresql.r2dbc)

View File

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

View File

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

View File

@@ -5,6 +5,8 @@ import java.time.ZonedDateTime
import java.util.UUID
import ltd.hlaeja.entity.DeviceEntity
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.stereotype.Service
import org.springframework.web.server.ResponseStatusException
@@ -18,8 +20,13 @@ class DeviceService(
suspend fun addDevice(
type: UUID,
): DeviceEntity = deviceRepository.save(DeviceEntity(null, ZonedDateTime.now(), type))
.also { log.debug { "Added device ${it.id}" } }
): DeviceEntity = try {
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)
?.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.NodeEntity
import ltd.hlaeja.entity.TypeEntity
import ltd.hlaeja.jwt.service.PrivateJwtService
import ltd.hlaeja.library.deviceRegistry.Device
import ltd.hlaeja.library.deviceRegistry.Identity
import ltd.hlaeja.library.deviceRegistry.Node
import ltd.hlaeja.library.deviceRegistry.Type
import ltd.hlaeja.service.JwtService
import org.springframework.http.HttpStatus.EXPECTATION_FAILED
import org.springframework.web.server.ResponseStatusException
@@ -40,10 +40,10 @@ fun NodeEntity.toIdentityResponse(): Identity.Response = Identity.Response(
device,
)
suspend fun DeviceEntity.toDeviceResponse(
jwtService: JwtService,
fun DeviceEntity.toDeviceResponse(
jwtService: PrivateJwtService,
): Device.Response = Device.Response(
id ?: throw ResponseStatusException(EXPECTATION_FAILED),
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",
"type": "java.lang.String",
"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 kotlinx.coroutines.test.runTest
import ltd.hlaeja.entity.DeviceEntity
import ltd.hlaeja.jwt.service.PrivateJwtService
import ltd.hlaeja.library.deviceRegistry.Device
import ltd.hlaeja.service.DeviceService
import ltd.hlaeja.service.JwtService
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Nested
@@ -28,13 +28,13 @@ class DeviceControllerTest {
}
val deviceService: DeviceService = mockk()
val jwtService: JwtService = mockk()
val privateJwtService: PrivateJwtService = mockk()
lateinit var controller: DeviceController
@BeforeEach
fun setUp() {
controller = DeviceController(deviceService, jwtService)
controller = DeviceController(deviceService, privateJwtService)
}
@Nested
@@ -45,14 +45,14 @@ class DeviceControllerTest {
// given
val request = Device.Request(uuid)
coEvery { deviceService.addDevice(any()) } returns DeviceEntity(uuid, timestamp, uuid)
coEvery { jwtService.makeIdentity(any()) } returns PAYLOAD
coEvery { privateJwtService.sign(any()) } returns PAYLOAD
// when
val response = controller.addDevice(request)
// then
coVerify(exactly = 1) { deviceService.addDevice(any()) }
coVerify(exactly = 1) { jwtService.makeIdentity(any()) }
coVerify(exactly = 1) { privateJwtService.sign(any()) }
assertThat(response.identity).isEqualTo(PAYLOAD)
}
@@ -80,14 +80,14 @@ class DeviceControllerTest {
fun `get device - success`() = runTest {
// given
coEvery { deviceService.getDevice(any()) } returns DeviceEntity(uuid, timestamp, uuid)
coEvery { jwtService.makeIdentity(any()) } returns PAYLOAD
coEvery { privateJwtService.sign(any()) } returns PAYLOAD
// when
val response = controller.getDevice(uuid)
// then
coVerify(exactly = 1) { deviceService.getDevice(any()) }
coVerify(exactly = 1) { jwtService.makeIdentity(any()) }
coVerify(exactly = 1) { privateJwtService.sign(any()) }
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-----