7 Commits

Author SHA1 Message Date
7184854ed4 [RELEASE] - release version: 0.3.0 2024-12-28 07:43:29 +01:00
df8e1a7b48 update for getDevice
- add get device in DeviceController
- add get device in DeviceService
- update logging
- update and cleanup in README.md
2024-12-27 22:56:27 +01:00
e7dbbc7a78 [RELEASE] - bump version 2024-12-10 23:25:29 +01:00
2a69d06d7f [RELEASE] - release version: 0.2.0 2024-12-10 23:25:26 +01:00
d5746aa22d update for common messages v0.2.0
- change DeviceController addDevice to use toDeviceResponse
- add DeviceEntity.toDeviceResponse to Mapping.kt
2024-12-10 21:28:58 +01:00
5d36099738 update for common plugin v0.2.0
- rename keys folder to cert
- update build.gradle.kts to use certificate plugin
- update catalog version
2024-12-10 19:20:14 +01:00
5c2e2e617e [RELEASE] - bump version 2024-11-26 11:42:12 +01:00
18 changed files with 170 additions and 96 deletions

2
.gitignore vendored
View File

@@ -40,4 +40,4 @@ out/
.kotlin
### Cert ###
/keys/
/cert/

View File

@@ -5,14 +5,14 @@ Classes crafted, identities bestowed, Each device recorded, their functions unfo
## Properties for deployment
| name | required | info |
|------------------------|----------|-------------------------|
| spring.profiles.active | * | Spring Boot environment |
| spring.r2dbc.url | * | Postgres host url |
| spring.r2dbc.username | * | Postgres username |
| spring.r2dbc.password | ** | Postgres password |
| jwt.private-key | | JWT private cert |
|------------------------|:--------:|-------------------------|
| spring.profiles.active | ✓ | Spring Boot environment |
| spring.r2dbc.url | ✓ | Postgres host url |
| spring.r2dbc.username | ✓ | Postgres username |
| spring.r2dbc.password | ✗ | Postgres password |
| jwt.private-key | ✓ | JWT private cert |
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
@@ -20,34 +20,28 @@ Run `release.sh` script from `master` branch.
## Development Information
### Generate Private and Public RSA Key
### Private RSA Key
OpenSSL Project is dedicated to providing a simple installation of OpenSSL for Microsoft Windows. [Download](https://slproweb.com/products/Win32OpenSSL.html)
This service uses RAS keys to create identities for devices. The private key is used here to generate identities, while the public key is used by **[Hlæja Device API](https://github.com/swordsteel/hlaeja-device-api)** to identify a device and accept data.
Generate an RSA private key, of size 2048, and output it to a file named `private_key.pem` in to `./keys`
*For instructions on how to set these up, please refer to our [generate RSA key](https://github.com/swordsteel/hlaeja-development/blob/master/doc/rsa_key.md) documentation.*
```shell
openssl genrsa -out private_key.pem 2048
### Global Setting
The following global settings are used in Hlaeja Device Registry. You can configure these settings using either Gradle properties or alternatively environment variables.
*For instructions on how to set these up, please refer to our [set global settings](https://github.com/swordsteel/hlaeja-development/blob/master/doc/global_settings.md) documentation.*
#### Gradle Properties
```properties
repository.user=your_user
repository.token=your_token_value
```
Extract the public key from `private_key.pem` from `./keys`, and output it to a file named `public_key.pem` in to `./keys`
#### Environment Variables
```shell
openssl rsa -in private_key.pem -pubout -out public_key.pem
```properties
REPOSITORY_USER=your_user
REPOSITORY_TOKEN=your_token_value
```
### Global gradle properties
To authenticate with Gradle to access repositories that require authentication, you can set your user and token in the `gradle.properties` file.
Here's how you can do it:
1. Open or create the `gradle.properties` file in your Gradle user home directory:
- On Unix-like systems (Linux, macOS), this directory is typically `~/.gradle/`.
- On Windows, this directory is typically `C:\Users\<YourUsername>\.gradle\`.
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`

View File

@@ -1,47 +1,40 @@
plugins {
alias(hlaeja.plugins.kotlin.jvm)
alias(hlaeja.plugins.kotlin.spring)
alias(hlaeja.plugins.ltd.hlaeja.plugin.certificate)
alias(hlaeja.plugins.ltd.hlaeja.plugin.service)
alias(hlaeja.plugins.spring.dependency.management)
alias(hlaeja.plugins.springframework.boot)
}
dependencies {
implementation(hlaeja.com.fasterxml.jackson.module.kotlin)
implementation(hlaeja.fasterxml.jackson.module.kotlin)
implementation(hlaeja.jjwt.api)
implementation(hlaeja.kotlin.logging)
implementation(hlaeja.kotlin.reflect)
implementation(hlaeja.kotlinx.coroutines)
implementation(hlaeja.ltd.hlaeja.library.common.messages)
implementation(hlaeja.org.springframework.springboot.actuator.starter)
implementation(hlaeja.org.springframework.springboot.r2dbc.starter)
implementation(hlaeja.org.springframework.springboot.webflux.starter)
implementation(hlaeja.library.hlaeja.common.messages)
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.org.postgresql)
runtimeOnly(hlaeja.org.postgresql.r2dbc)
runtimeOnly(hlaeja.postgresql)
runtimeOnly(hlaeja.postgresql.r2dbc)
testImplementation(hlaeja.assertj.core)
testImplementation(hlaeja.io.mockk)
testImplementation(hlaeja.io.projectreactor.reactor.test)
testImplementation(hlaeja.mockk)
testImplementation(hlaeja.projectreactor.reactor.test)
testImplementation(hlaeja.kotlin.test.junit5)
testImplementation(hlaeja.kotlinx.coroutines.test)
testImplementation(hlaeja.org.springframework.springboot.test.starter)
testImplementation(hlaeja.springboot.starter.test)
testRuntimeOnly(hlaeja.org.junit.platform.launcher)
testRuntimeOnly(hlaeja.junit.platform.launcher)
}
group = "ltd.hlaeja"
tasks {
named("processResources") {
dependsOn("copyPrivateKey")
}
register<Copy>("copyPrivateKey") {
group = "hlaeja"
from("keys/private_key.pem")
into("${layout.buildDirectory.get()}/resources/main/keys")
onlyIf { file("keys/private_key.pem").exists() }
}
tasks.named("processResources") {
dependsOn("copyCertificates")
}

View File

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

View File

@@ -5,3 +5,6 @@ Content-Type: application/json
{
"type": "00000000-0000-0000-0000-000000000000"
}
### register device for a type
GET {{hostname}}/device-00000000-0000-0000-0000-000000000000

View File

@@ -1,13 +1,15 @@
package ltd.hlaeja.controller
import java.util.UUID
import ltd.hlaeja.library.deviceRegistry.Device
import ltd.hlaeja.service.DeviceService
import ltd.hlaeja.service.JwtService
import org.springframework.http.HttpStatus.EXPECTATION_FAILED
import ltd.hlaeja.util.toDeviceResponse
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.PathVariable
import org.springframework.web.bind.annotation.PostMapping
import org.springframework.web.bind.annotation.RequestBody
import org.springframework.web.bind.annotation.RestController
import org.springframework.web.server.ResponseStatusException
@RestController
class DeviceController(
@@ -18,7 +20,12 @@ class DeviceController(
@PostMapping("/device")
suspend fun addDevice(
@RequestBody request: Device.Request,
): Device.Identity = deviceService.addDevice(request.type)
.let { jwtService.makeIdentity(it.id ?: throw ResponseStatusException(EXPECTATION_FAILED)) }
.let { Device.Identity(it) }
): Device.Response = deviceService.addDevice(request.type)
.toDeviceResponse(jwtService)
@GetMapping("/device-{device}")
suspend fun getDevice(
@PathVariable device: UUID,
): Device.Response = deviceService.getDevice(device)
.toDeviceResponse(jwtService)
}

View File

@@ -1,11 +1,13 @@
package ltd.hlaeja.service
import io.github.oshai.kotlinlogging.KotlinLogging
import java.time.ZonedDateTime
import java.util.UUID
import ltd.hlaeja.entity.DeviceEntity
import ltd.hlaeja.repository.DeviceRepository
import mu.KotlinLogging
import org.springframework.http.HttpStatus.NOT_FOUND
import org.springframework.stereotype.Service
import org.springframework.web.server.ResponseStatusException
private val log = KotlinLogging.logger {}
@@ -18,4 +20,8 @@ class DeviceService(
type: UUID,
): DeviceEntity = deviceRepository.save(DeviceEntity(null, ZonedDateTime.now(), type))
.also { log.debug { "Added device ${it.id}" } }
suspend fun getDevice(device: UUID): DeviceEntity = deviceRepository.findById(device)
?.also { log.debug { "Get device ${it.id}" } }
?: throw ResponseStatusException(NOT_FOUND)
}

View File

@@ -1,9 +1,9 @@
package ltd.hlaeja.service
import io.github.oshai.kotlinlogging.KotlinLogging
import java.util.UUID
import ltd.hlaeja.entity.NodeEntity
import ltd.hlaeja.repository.NodeRepository
import mu.KotlinLogging
import org.springframework.http.HttpStatus.NOT_FOUND
import org.springframework.stereotype.Service
import org.springframework.web.server.ResponseStatusException

View File

@@ -1,9 +1,9 @@
package ltd.hlaeja.service
import io.github.oshai.kotlinlogging.KotlinLogging
import kotlinx.coroutines.flow.Flow
import ltd.hlaeja.entity.TypeEntity
import ltd.hlaeja.repository.TypeRepository
import mu.KotlinLogging
import org.springframework.dao.DuplicateKeyException
import org.springframework.http.HttpStatus
import org.springframework.stereotype.Service
@@ -22,9 +22,9 @@ class TypeService(
entity: TypeEntity,
): TypeEntity = try {
typeRepository.save(entity)
.also { log.debug("Added new type: {}", it.id) }
.also { log.debug { "Added new type: $it.id" } }
} catch (e: DuplicateKeyException) {
log.warn(e.localizedMessage)
log.warn { e.localizedMessage }
throw ResponseStatusException(HttpStatus.CONFLICT)
}
}

View File

@@ -1,11 +1,14 @@
package ltd.hlaeja.util
import java.time.ZonedDateTime
import ltd.hlaeja.entity.DeviceEntity
import ltd.hlaeja.entity.NodeEntity
import ltd.hlaeja.entity.TypeEntity
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
@@ -36,3 +39,11 @@ fun NodeEntity.toIdentityResponse(): Identity.Response = Identity.Response(
id ?: throw ResponseStatusException(EXPECTATION_FAILED),
device,
)
suspend fun DeviceEntity.toDeviceResponse(
jwtService: JwtService,
): Device.Response = Device.Response(
id ?: throw ResponseStatusException(EXPECTATION_FAILED),
type,
jwtService.makeIdentity(id),
)

View File

@@ -11,7 +11,7 @@ spring:
version: "%APP_BUILD_OS_VERSION%"
jwt:
private-key: keys/private_key.pem
private-key: cert/private_key.pem
---
###############################

View File

@@ -14,6 +14,7 @@ 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
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.assertThrows
import org.springframework.web.server.ResponseStatusException
@@ -36,35 +37,59 @@ class DeviceControllerTest {
controller = DeviceController(deviceService, jwtService)
}
@Test
fun `add device - success`() = runTest {
// given
val request = Device.Request(uuid)
coEvery { deviceService.addDevice(any()) } returns DeviceEntity(uuid, timestamp, uuid)
coEvery { jwtService.makeIdentity(any()) } returns PAYLOAD
@Nested
inner class AddDeviceTest {
// when
val response = controller.addDevice(request)
@Test
fun `add device - success`() = runTest {
// given
val request = Device.Request(uuid)
coEvery { deviceService.addDevice(any()) } returns DeviceEntity(uuid, timestamp, uuid)
coEvery { jwtService.makeIdentity(any()) } returns PAYLOAD
// then
coVerify(exactly = 1) { deviceService.addDevice(any()) }
coVerify(exactly = 1) { jwtService.makeIdentity(any()) }
// when
val response = controller.addDevice(request)
assertThat(response.identity).isEqualTo(PAYLOAD)
}
// then
coVerify(exactly = 1) { deviceService.addDevice(any()) }
coVerify(exactly = 1) { jwtService.makeIdentity(any()) }
@Test
fun `add device - device service fail`() = runTest {
// given
val request = Device.Request(uuid)
coEvery { deviceService.addDevice(any()) } returns DeviceEntity(null, timestamp, uuid)
// when exception
val exception = assertThrows<ResponseStatusException> {
controller.addDevice(request)
assertThat(response.identity).isEqualTo(PAYLOAD)
}
// then
assertThat(exception.message).isEqualTo("417 EXPECTATION_FAILED")
@Test
fun `add device - device service fail`() = runTest {
// given
val request = Device.Request(uuid)
coEvery { deviceService.addDevice(any()) } returns DeviceEntity(null, timestamp, uuid)
// when exception
val exception = assertThrows<ResponseStatusException> {
controller.addDevice(request)
}
// then
assertThat(exception.message).isEqualTo("417 EXPECTATION_FAILED")
}
}
@Nested
inner class GetDeviceTest {
@Test
fun `get device - success`() = runTest {
// given
coEvery { deviceService.getDevice(any()) } returns DeviceEntity(uuid, timestamp, uuid)
coEvery { jwtService.makeIdentity(any()) } returns PAYLOAD
// when
val response = controller.getDevice(uuid)
// then
coVerify(exactly = 1) { deviceService.getDevice(any()) }
coVerify(exactly = 1) { jwtService.makeIdentity(any()) }
assertThat(response.identity).isEqualTo(PAYLOAD)
}
}
}

View File

@@ -17,6 +17,8 @@ import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.AfterEach
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.assertThrows
import org.springframework.web.server.ResponseStatusException
class DeviceServiceTest {
companion object {
@@ -42,7 +44,7 @@ class DeviceServiceTest {
}
@Test
fun `add new type success`() = runTest {
fun `add new device success`() = runTest {
// given
coEvery { repository.save(any()) } answers { call ->
(call.invocation.args[0] as DeviceEntity).copy(id = device)
@@ -58,4 +60,37 @@ class DeviceServiceTest {
assertThat(result.timestamp.toString()).isEqualTo("2000-01-01T00:00:00.001Z[UTC]")
assertThat(result.type).isEqualTo(type)
}
@Test
fun `get device - success`() = runTest {
// given
val device = UUID.fromString("00000000-0000-0000-0000-000000000000")
val entity: DeviceEntity = mockk()
coEvery { repository.findById(any()) } returns entity
coEvery { entity.id } returns device
// when
val result = service.getDevice(type)
// then
coVerify(exactly = 1) { repository.findById(any()) }
assertThat(result.id).isEqualTo(device)
}
@Test
fun `get device - fail not found`() = runTest {
// given
val device = UUID.fromString("00000000-0000-0000-0000-000000000000")
coEvery { repository.findById(any()) } returns null
// when
val exception = assertThrows<ResponseStatusException> {
service.getDevice(device)
}
// then
assertThat(exception.message).isEqualTo("404 NOT_FOUND")
}
}

View File

@@ -9,7 +9,7 @@ import org.junit.jupiter.api.Test
class JwtServiceTest {
val property: JwtProperty = JwtProperty("keys/valid-private-key.pem")
val property: JwtProperty = JwtProperty("cert/valid-private-key.pem")
lateinit var service: JwtService
@BeforeEach

View File

@@ -11,7 +11,7 @@ class PrivateKeyProviderTest {
@Test
fun `load private key - success`() {
// given
val pemFilePath = "keys/valid-private-key.pem"
val pemFilePath = "cert/valid-private-key.pem"
// when
val privateKey: RSAPrivateKey = PrivateKeyProvider.load(pemFilePath)
@@ -24,7 +24,7 @@ class PrivateKeyProviderTest {
@Test
fun `load private key - file does not exist`() {
// given
val nonExistentPemFilePath = "keys/non-existent.pem"
val nonExistentPemFilePath = "cert/non-existent.pem"
// when exception
val exception = assertThrows<KeyProviderException> {
@@ -38,7 +38,7 @@ class PrivateKeyProviderTest {
@Test
fun `load private key - file is invalid`() {
// given
val invalidPemFilePath = "keys/invalid-private-key.pem"
val invalidPemFilePath = "cert/invalid-private-key.pem"
// when exception
val exception = assertThrows<IllegalArgumentException> {

View File

@@ -1,5 +1,5 @@
jwt:
private-key: keys/valid-private-key.pem
private-key: cert/valid-private-key.pem
spring:
r2dbc: