Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f52f1237a2 | |||
| 4130ba681c | |||
| df9d2c59a4 | |||
| 7d4ebab8f8 |
@@ -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)
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
): 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}" } }
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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),
|
||||
)
|
||||
|
||||
@@ -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) }
|
||||
}
|
||||
@@ -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."
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
@@ -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-----
|
||||
Reference in New Issue
Block a user