15 Commits

Author SHA1 Message Date
audapi
c7a824702e [RELEASE] - Release version: 0.4.0 2025-08-18 11:07:11 +00:00
adefbb465e update registry device to accept admin as well, as registry role 2025-08-18 13:06:28 +02:00
46a4852257 move jwt authentication converter 2025-08-18 13:06:28 +02:00
7a910c8428 move jwt authentication manager 2025-08-18 13:06:28 +02:00
b070a22b0e move jwt user and jwt authentication 2025-08-18 13:06:28 +02:00
e19e0e59bc update public path 2025-08-18 13:06:28 +02:00
audapi
c69a9cd07c [RELEASE] - Bump version 2025-07-29 18:46:56 +00:00
audapi
1aeed3a457 [RELEASE] - Release version: 0.3.0 2025-07-29 18:46:54 +00:00
8b6cc3b96e update gradlew 2025-07-29 20:46:25 +02:00
5dabd53c2c update project 2025-07-29 20:46:25 +02:00
b22dac2d25 add actuator.http 2025-07-29 20:46:25 +02:00
d9db974741 add GitHub actions for release and checks 2025-04-16 12:38:37 +02:00
82a473a613 upgrade gradle and spring boot 2025-04-16 12:38:37 +02:00
503e307c69 remove influx monitoring 2025-04-16 12:38:37 +02:00
7cc40e7fc6 [RELEASE] - bump version 2025-01-02 07:21:03 +01:00
22 changed files with 75 additions and 210 deletions

12
.github/workflows/release.yml vendored Normal file
View File

@@ -0,0 +1,12 @@
name: Release
on:
workflow_dispatch:
jobs:
release:
uses: swordsteel/hlaeja-common-workflows/.github/workflows/release.yml@master
secrets:
CI_BOT_PAT: ${{ secrets.CI_BOT_PAT }}
with:
TYPE: service

12
.github/workflows/run-checks.yml vendored Normal file
View File

@@ -0,0 +1,12 @@
name: Pull request validation
on:
pull_request:
paths-ignore:
- '.github/**'
jobs:
validate:
uses: swordsteel/hlaeja-common-workflows/.github/workflows/run-checks.yml@master
secrets:
CI_BOT_PAT: ${{ secrets.CI_BOT_PAT }}

View File

@@ -15,19 +15,12 @@ Classes and endpoints, to shape and to steer, Devices and sensors, their purpose
| jwt.public-key | ✓ | JWT public key file | | jwt.public-key | ✓ | JWT public key file |
| account-registry.url | ✓ | Account Register URL | | account-registry.url | ✓ | Account Register URL |
| device-registry.url | ✓ | Device Register URL | | device-registry.url | ✓ | Device Register 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 |
*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 ## Releasing Service
Run `release.sh` script from `master` branch. Run release pipeline from `master` branch.
## Development Configuration ## Development Configuration

View File

@@ -1,12 +1,10 @@
import com.bmuschko.gradle.docker.tasks.container.DockerCreateContainer
plugins { plugins {
alias(hlaeja.plugins.kotlin.jvm) alias(hlaeja.plugins.kotlin.jvm)
alias(hlaeja.plugins.kotlin.spring) alias(hlaeja.plugins.kotlin.spring)
alias(hlaeja.plugins.ltd.hlaeja.plugin.certificate) alias(hlaeja.plugins.spring.boot)
alias(hlaeja.plugins.ltd.hlaeja.plugin.service)
alias(hlaeja.plugins.spring.dependency.management) alias(hlaeja.plugins.spring.dependency.management)
alias(hlaeja.plugins.springframework.boot) alias(hlaeja.plugins.certificate)
alias(hlaeja.plugins.service)
} }
dependencies { dependencies {
@@ -15,9 +13,8 @@ dependencies {
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.common.messages)
implementation(hlaeja.library.hlaeja.jwt) implementation(hlaeja.library.jwt)
implementation(hlaeja.micrometer.registry.influx)
implementation(hlaeja.springboot.starter.actuator) implementation(hlaeja.springboot.starter.actuator)
implementation(hlaeja.springboot.starter.security) implementation(hlaeja.springboot.starter.security)
implementation(hlaeja.springboot.starter.webflux) implementation(hlaeja.springboot.starter.webflux)
@@ -33,17 +30,6 @@ dependencies {
group = "ltd.hlaeja" group = "ltd.hlaeja"
fun influxDbToken(): String = config.findOrDefault("influxdb.token", "INFLUXDB_TOKEN", "") tasks.named("processResources") {
dependsOn("copyCertificates")
tasks {
named("containerCreate", DockerCreateContainer::class) {
withEnvVar("MANAGEMENT_INFLUX_METRICS_EXPORT_TOKEN", influxDbToken())
}
withType<ProcessResources> {
filesMatching("**/application.yml") { filter { it.replace("%INFLUXDB_TOKEN%", influxDbToken()) } }
onlyIf { file("src/main/resources/application.yml").exists() }
}
named("processResources") {
dependsOn("copyCertificates")
}
} }

View File

@@ -1,6 +1,6 @@
kotlin.code.style=official kotlin.code.style=official
version=0.2.0 version=0.4.0
catalog=0.8.0 catalog=0.11.0
docker.port.expose=8443 docker.port.expose=8443
container.port.expose=8443 container.port.expose=8443
container.port.host=9040 container.port.host=9040

Binary file not shown.

View File

@@ -1,6 +1,6 @@
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-bin.zip distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.3-bin.zip
networkTimeout=10000 networkTimeout=10000
validateDistributionUrl=true validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME

9
gradlew vendored
View File

@@ -86,8 +86,7 @@ done
# shellcheck disable=SC2034 # shellcheck disable=SC2034
APP_BASE_NAME=${0##*/} APP_BASE_NAME=${0##*/}
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
' "$PWD" ) || exit
# Use the maximum available, or set MAX_FD != -1 to use that value. # Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum MAX_FD=maximum
@@ -115,7 +114,7 @@ case "$( uname )" in #(
NONSTOP* ) nonstop=true ;; NONSTOP* ) nonstop=true ;;
esac esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar CLASSPATH="\\\"\\\""
# Determine the Java command to use to start the JVM. # Determine the Java command to use to start the JVM.
@@ -206,7 +205,7 @@ fi
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Collect all arguments for the java command: # Collect all arguments for the java command:
# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, # * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
# and any embedded shellness will be escaped. # and any embedded shellness will be escaped.
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be # * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
# treated as '${Hostname}' itself on the command line. # treated as '${Hostname}' itself on the command line.
@@ -214,7 +213,7 @@ DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
set -- \ set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \ "-Dorg.gradle.appname=$APP_BASE_NAME" \
-classpath "$CLASSPATH" \ -classpath "$CLASSPATH" \
org.gradle.wrapper.GradleWrapperMain \ -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \
"$@" "$@"
# Stop when "xargs" is not available. # Stop when "xargs" is not available.

4
gradlew.bat vendored
View File

@@ -70,11 +70,11 @@ goto fail
:execute :execute
@rem Setup the command line @rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar set CLASSPATH=
@rem Execute Gradle @rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %*
:end :end
@rem End local scope for the variables with windows NT shell @rem End local scope for the variables with windows NT shell

5
http/actuator.http Normal file
View File

@@ -0,0 +1,5 @@
### get actuator
GET {{hostname}}/actuator
### get actuator health
GET {{hostname}}/actuator/health

View File

@@ -1,89 +0,0 @@
#!/bin/sh
### This should be a pipeline, but for this example let use this ###
check_active_branch() {
if [ "$(git rev-parse --abbrev-ref HEAD)" != "$1" ]; then
echo "Error: The current branch is not $1."
exit 1
fi
}
check_uncommitted_changes() {
if [ -n "$(git status --porcelain)" ]; then
echo "Error: There are uncommitted changes in the repository."
exit 1
fi
}
prepare_environment() {
git fetch origin
}
check_last_commit() {
last_commit_message=$(git log -1 --pretty=format:%s)
if [ "$last_commit_message" = "[RELEASE] - bump version" ]; then
echo "Warning: Nothing to release!!!"
exit 1
fi
}
check_differences() {
if ! git diff --quiet origin/"$1" "$1"; then
echo "Error: The branches origin/$1 and $1 have differences."
exit 1
fi
}
un_snapshot_version() {
sed -i "s/\($1\s*=\s*[0-9]\+\.[0-9]\+\.[0-9]\+\).*/\1/" gradle.properties
}
current_version() {
awk -F '=' '/version\s*=\s*[0-9.]*/ {gsub(/^ +| +$/,"",$2); print $2}' gradle.properties
}
stage_files() {
for file in "$@"; do
if git diff --exit-code --quiet -- "$file"; then
echo "No changes in $file"
else
git add "$file"
echo "Changes in $file staged for commit"
fi
done
}
commit_change() {
stage_files gradle.properties
git commit -m "[RELEASE] - $1"
git push --porcelain origin master
}
add_release_tag() {
gitTag="v$(current_version)"
git tag -a "$gitTag" -m "Release version $gitTag"
git push --porcelain origin "$gitTag"
}
snapshot_version() {
new_version="$(current_version | awk -F '.' '{print $1 "." $2+1 ".0"}')"
sed -i "s/\(version\s*=\s*\)[0-9.]*/\1$new_version-SNAPSHOT/" gradle.properties
}
# check and prepare for release
check_active_branch master
check_uncommitted_changes
prepare_environment
check_last_commit
check_differences master
# un-snapshot version for release
un_snapshot_version version
un_snapshot_version catalog
# release changes and prepare for next release
commit_change "release version: $(current_version)"
add_release_tag
snapshot_version
commit_change 'bump version'

View File

@@ -1,7 +1,8 @@
package ltd.hlaeja.configuration package ltd.hlaeja.configuration
import ltd.hlaeja.security.JwtAuthenticationConverter import ltd.hlaeja.security.authorize.publicPaths
import ltd.hlaeja.security.JwtAuthenticationManager import ltd.hlaeja.security.converter.JwtAuthenticationConverter
import ltd.hlaeja.security.manager.JwtAuthenticationManager
import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration import org.springframework.context.annotation.Configuration
import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity
@@ -55,6 +56,6 @@ class SecurityConfiguration {
private fun authorizeExchange( private fun authorizeExchange(
authorizeExchange: AuthorizeExchangeSpec, authorizeExchange: AuthorizeExchangeSpec,
) = authorizeExchange ) = authorizeExchange
.pathMatchers("/login").permitAll() .publicPaths().permitAll()
.anyExchange().hasRole("REGISTRY") .anyExchange().hasAnyRole("REGISTRY", "ADMIN")
} }

View File

@@ -0,0 +1,8 @@
package ltd.hlaeja.security.authorize
import org.springframework.security.config.web.server.ServerHttpSecurity.AuthorizeExchangeSpec
fun AuthorizeExchangeSpec.publicPaths(): AuthorizeExchangeSpec.Access = pathMatchers(
"/actuator/**",
"/login",
)

View File

@@ -1,9 +1,11 @@
package ltd.hlaeja.security package ltd.hlaeja.security.converter
import io.github.oshai.kotlinlogging.KotlinLogging import io.github.oshai.kotlinlogging.KotlinLogging
import io.jsonwebtoken.JwtException import io.jsonwebtoken.JwtException
import java.util.UUID import java.util.UUID
import ltd.hlaeja.jwt.service.PublicJwtService import ltd.hlaeja.jwt.service.PublicJwtService
import ltd.hlaeja.security.user.JwtAuthentication
import ltd.hlaeja.security.user.JwtUserDetails
import org.springframework.http.HttpStatus.UNAUTHORIZED import org.springframework.http.HttpStatus.UNAUTHORIZED
import org.springframework.security.core.Authentication import org.springframework.security.core.Authentication
import org.springframework.security.core.authority.SimpleGrantedAuthority import org.springframework.security.core.authority.SimpleGrantedAuthority
@@ -40,14 +42,14 @@ class JwtAuthenticationConverter(
} }
private fun jwtAuthenticationToken(token: String) = publicJwtService.verify(token) { claims -> private fun jwtAuthenticationToken(token: String) = publicJwtService.verify(token) { claims ->
JwtAuthenticationToken( JwtAuthentication(
JwtUserDetails( JwtUserDetails(
UUID.fromString(claims.payload["id"] as String), UUID.fromString(claims.payload["id"] as String),
claims.payload["username"] as String, claims.payload["username"] as String,
), ),
token, token,
(claims.payload["role"] as String).split(",") (claims.payload["role"] as String).split(",")
.map { SimpleGrantedAuthority(it) } .map { SimpleGrantedAuthority("ROLE_$it") }
.toMutableList(), .toMutableList(),
true, true,
) )

View File

@@ -1,5 +1,6 @@
package ltd.hlaeja.security package ltd.hlaeja.security.manager
import ltd.hlaeja.security.user.JwtAuthentication
import org.springframework.security.authentication.ReactiveAuthenticationManager import org.springframework.security.authentication.ReactiveAuthenticationManager
import org.springframework.security.core.Authentication import org.springframework.security.core.Authentication
import org.springframework.security.core.AuthenticationException import org.springframework.security.core.AuthenticationException
@@ -11,14 +12,14 @@ class JwtAuthenticationManager : ReactiveAuthenticationManager {
override fun authenticate( override fun authenticate(
authentication: Authentication, authentication: Authentication,
): Mono<Authentication> = if (authentication is JwtAuthenticationToken) { ): Mono<Authentication> = if (authentication is JwtAuthentication) {
handleJwtToken(authentication) handleJwtToken(authentication)
} else { } else {
Mono.error(object : AuthenticationException("Unsupported authentication type") {}) Mono.error(object : AuthenticationException("Unsupported authentication type") {})
} }
private fun handleJwtToken( private fun handleJwtToken(
token: JwtAuthenticationToken, token: JwtAuthentication,
): Mono<Authentication> = if (token.isAuthenticated) { ): Mono<Authentication> = if (token.isAuthenticated) {
Mono.just(token) Mono.just(token)
} else { } else {

View File

@@ -1,9 +1,9 @@
package ltd.hlaeja.security package ltd.hlaeja.security.user
import org.springframework.security.core.Authentication import org.springframework.security.core.Authentication
import org.springframework.security.core.GrantedAuthority import org.springframework.security.core.GrantedAuthority
data class JwtAuthenticationToken( data class JwtAuthentication(
private val jwtUserDetails: JwtUserDetails, private val jwtUserDetails: JwtUserDetails,
private val token: String, private val token: String,
private var authorities: MutableCollection<out GrantedAuthority>, private var authorities: MutableCollection<out GrantedAuthority>,

View File

@@ -1,4 +1,4 @@
package ltd.hlaeja.security package ltd.hlaeja.security.user
import java.util.UUID import java.util.UUID

View File

@@ -1,8 +1,6 @@
package ltd.hlaeja.service package ltd.hlaeja.service
import io.github.oshai.kotlinlogging.KotlinLogging import io.github.oshai.kotlinlogging.KotlinLogging
import io.micrometer.core.instrument.Counter
import io.micrometer.core.instrument.MeterRegistry
import ltd.hlaeja.library.accountRegistry.Authentication import ltd.hlaeja.library.accountRegistry.Authentication
import ltd.hlaeja.property.AccountRegistryProperty import ltd.hlaeja.property.AccountRegistryProperty
import ltd.hlaeja.util.accountRegistryAuthenticate import ltd.hlaeja.util.accountRegistryAuthenticate
@@ -19,33 +17,20 @@ private val log = KotlinLogging.logger {}
@Service @Service
class AuthenticationService( class AuthenticationService(
meterRegistry: MeterRegistry,
private val webClient: WebClient, private val webClient: WebClient,
private val property: AccountRegistryProperty, private val property: AccountRegistryProperty,
) { ) {
private val accountRegistrySuccess = Counter.builder("account.registry.success")
.description("Number of successful account registry calls")
.register(meterRegistry)
private val accountRegistryFailure = Counter.builder("account.registry.failure")
.description("Number of failed account registry calls")
.register(meterRegistry)
suspend fun authenticate( suspend fun authenticate(
request: Authentication.Request, request: Authentication.Request,
): Authentication.Response = try { ): Authentication.Response = try {
webClient.accountRegistryAuthenticate(request, property) webClient.accountRegistryAuthenticate(request, property)
.also { accountRegistrySuccess.increment() }
} catch (e: ErrorResponseException) { } catch (e: ErrorResponseException) {
accountRegistryFailure.increment()
throw e throw e
} catch (e: WebClientRequestException) { } catch (e: WebClientRequestException) {
accountRegistryFailure.increment()
log.error(e) { "Error device registry" } log.error(e) { "Error device registry" }
throw ResponseStatusException(SERVICE_UNAVAILABLE) throw ResponseStatusException(SERVICE_UNAVAILABLE)
} catch (e: WebClientResponseException) { } catch (e: WebClientResponseException) {
accountRegistryFailure.increment()
log.error(e) { "Error device registry" } log.error(e) { "Error device registry" }
throw ResponseStatusException(INTERNAL_SERVER_ERROR) throw ResponseStatusException(INTERNAL_SERVER_ERROR)
} }

View File

@@ -1,8 +1,6 @@
package ltd.hlaeja.service package ltd.hlaeja.service
import io.github.oshai.kotlinlogging.KotlinLogging import io.github.oshai.kotlinlogging.KotlinLogging
import io.micrometer.core.instrument.Counter
import io.micrometer.core.instrument.MeterRegistry
import ltd.hlaeja.library.deviceRegistry.Device import ltd.hlaeja.library.deviceRegistry.Device
import ltd.hlaeja.property.DeviceRegistryProperty import ltd.hlaeja.property.DeviceRegistryProperty
import ltd.hlaeja.util.deviceRegistryCreateDevice import ltd.hlaeja.util.deviceRegistryCreateDevice
@@ -19,33 +17,20 @@ private val log = KotlinLogging.logger {}
@Service @Service
class DeviceRegistryService( class DeviceRegistryService(
meterRegistry: MeterRegistry,
private val webClient: WebClient, private val webClient: WebClient,
private val deviceRegistryProperty: DeviceRegistryProperty, private val deviceRegistryProperty: DeviceRegistryProperty,
) { ) {
private val registerDeviceSuccess = Counter.builder("device.registry.success")
.description("Number of successful device registrations")
.register(meterRegistry)
private val registerDeviceFailure = Counter.builder("device.registry.failure")
.description("Number of failed device registrations")
.register(meterRegistry)
suspend fun registerDevice( suspend fun registerDevice(
request: Device.Request, request: Device.Request,
): Device.Response = try { ): Device.Response = try {
webClient.deviceRegistryCreateDevice(request, deviceRegistryProperty) webClient.deviceRegistryCreateDevice(request, deviceRegistryProperty)
.also { registerDeviceSuccess.increment() }
} catch (e: ErrorResponseException) { } catch (e: ErrorResponseException) {
registerDeviceFailure.increment()
throw e throw e
} catch (e: WebClientRequestException) { } catch (e: WebClientRequestException) {
registerDeviceFailure.increment()
log.error(e) { "Error device registry" } log.error(e) { "Error device registry" }
throw ResponseStatusException(SERVICE_UNAVAILABLE) throw ResponseStatusException(SERVICE_UNAVAILABLE)
} catch (e: WebClientResponseException) { } catch (e: WebClientResponseException) {
registerDeviceFailure.increment()
log.error(e) { "Error device registry" } log.error(e) { "Error device registry" }
throw ResponseStatusException(INTERNAL_SERVER_ERROR) throw ResponseStatusException(INTERNAL_SERVER_ERROR)
} }

View File

@@ -12,22 +12,17 @@ spring:
management: management:
endpoints: endpoints:
enabled-by-default: false access:
default: none
web: web:
exposure: exposure:
include: "health,info" include: "health,info"
endpoint: endpoint:
health: health:
enabled: true
show-details: always show-details: always
access: read_only
info: info:
enabled: true access: read_only
influx:
metrics:
export:
api-version: v2
bucket: hlaeja
org: hlaeja_ltd
jwt: jwt:
public-key: cert/public_key.pem public-key: cert/public_key.pem
@@ -55,16 +50,6 @@ account-registry:
device-registry: device-registry:
url: http://localhost:9010 url: http://localhost:9010
management:
metrics:
tags:
application: register-api
influx:
metrics:
export:
enabled: false
token: %INFLUXDB_TOKEN%
--- ---
########################## ##########################
### Docker environment ### ### Docker environment ###
@@ -74,15 +59,6 @@ spring:
activate: activate:
on-profile: docker on-profile: docker
management:
metrics:
tags:
application: register-api
influx:
metrics:
export:
uri: http://InfluxDB:8086
server: server:
port: 8443 port: 8443
ssl: ssl:

View File

@@ -1,11 +0,0 @@
<configuration>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<root level="INFO">
<appender-ref ref="STDOUT"/>
</root>
<logger level="DEBUG" name="ltd.hlaeja"/>
</configuration>