chore(ping-service, security): integrate centralized security module and enhance Ping-Service
- Replaced local `SecurityConfig` in `ping-service` with the shared `infrastructure:security` module. - Added `GlobalSecurityConfig` to standardize OAuth2, JWT validation, and CORS for all services. - Introduced new endpoints (`/ping/public`, `/ping/secure`) with role-based access control. - Updated database schema with Flyway migration (`V1__init_ping.sql`) and refactored persistence layer to align with the standardized approach (`createdAt` field). - Enhanced application configuration (`application.yaml`) to use shared security and Flyway settings.
This commit is contained in:
@@ -25,7 +25,12 @@ spring:
|
|||||||
response-timeout: 5s
|
response-timeout: 5s
|
||||||
routes:
|
routes:
|
||||||
- id: ping-service
|
- id: ping-service
|
||||||
uri: http://ping-service:8080
|
# Nutze lb:// wenn Service Discovery aktiv ist, sonst http://hostname:port
|
||||||
|
# Da wir Consul nutzen, ist lb://ping-service besser, aber für Tracer Bullet
|
||||||
|
# und direkte Docker-Kommunikation ist http://ping-service:8082 sicherer,
|
||||||
|
# falls Consul noch nicht 100% stabil ist.
|
||||||
|
# Wir nutzen hier den Docker Alias und den konfigurierten Port.
|
||||||
|
uri: http://ping-service:8082
|
||||||
predicates:
|
predicates:
|
||||||
- Path=/api/ping/**
|
- Path=/api/ping/**
|
||||||
filters:
|
filters:
|
||||||
@@ -42,13 +47,12 @@ management:
|
|||||||
include: health,info,prometheus
|
include: health,info,prometheus
|
||||||
tracing:
|
tracing:
|
||||||
sampling:
|
sampling:
|
||||||
probability: 1.0 # 100% der Requests tracen (für Dev/Test sinnvoll, in Prod reduzieren)
|
probability: 1.0
|
||||||
propagation:
|
propagation:
|
||||||
type: w3c # Standard W3C Trace Context (kompatibel mit OpenTelemetry)
|
type: w3c
|
||||||
|
|
||||||
# Gateway-spezifische Einstellungen
|
|
||||||
gateway:
|
gateway:
|
||||||
ratelimit:
|
ratelimit:
|
||||||
enabled: false # Start: ausgeschaltet; zum Aktivieren default-filters plus RequestRateLimiter in YAML hinzufügen
|
enabled: false
|
||||||
replenish-rate: 10
|
replenish-rate: 10
|
||||||
burst-capacity: 20
|
burst-capacity: 20
|
||||||
|
|||||||
+64
@@ -0,0 +1,64 @@
|
|||||||
|
package at.mocode.infrastructure.security
|
||||||
|
|
||||||
|
import org.springframework.context.annotation.Bean
|
||||||
|
import org.springframework.context.annotation.Configuration
|
||||||
|
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity
|
||||||
|
import org.springframework.security.config.annotation.web.builders.HttpSecurity
|
||||||
|
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity
|
||||||
|
import org.springframework.security.config.http.SessionCreationPolicy
|
||||||
|
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationConverter
|
||||||
|
import org.springframework.security.web.SecurityFilterChain
|
||||||
|
import org.springframework.web.cors.CorsConfiguration
|
||||||
|
import org.springframework.web.cors.CorsConfigurationSource
|
||||||
|
import org.springframework.web.cors.UrlBasedCorsConfigurationSource
|
||||||
|
|
||||||
|
@Configuration
|
||||||
|
@EnableWebSecurity
|
||||||
|
@EnableMethodSecurity(prePostEnabled = true) // Erlaubt @PreAuthorize in Services/Controllern
|
||||||
|
class GlobalSecurityConfig {
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
fun filterChain(http: HttpSecurity): SecurityFilterChain {
|
||||||
|
http
|
||||||
|
.csrf { it.disable() } // CSRF nicht nötig für Stateless REST APIs
|
||||||
|
.cors { it.configurationSource(corsConfigurationSource()) }
|
||||||
|
.sessionManagement { it.sessionCreationPolicy(SessionCreationPolicy.STATELESS) }
|
||||||
|
.authorizeHttpRequests { auth ->
|
||||||
|
// Explizite Freigaben (Health, Info, Public Endpoints)
|
||||||
|
auth.requestMatchers("/actuator/**").permitAll()
|
||||||
|
auth.requestMatchers("/ping/public").permitAll()
|
||||||
|
auth.requestMatchers("/error").permitAll()
|
||||||
|
|
||||||
|
// Alles andere muss authentifiziert sein
|
||||||
|
auth.anyRequest().authenticated()
|
||||||
|
}
|
||||||
|
.oauth2ResourceServer { oauth2 ->
|
||||||
|
oauth2.jwt { jwt ->
|
||||||
|
jwt.jwtAuthenticationConverter(jwtAuthenticationConverter())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return http.build()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
fun jwtAuthenticationConverter(): JwtAuthenticationConverter {
|
||||||
|
val converter = JwtAuthenticationConverter()
|
||||||
|
converter.setJwtGrantedAuthoritiesConverter(KeycloakRoleConverter())
|
||||||
|
return converter
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
fun corsConfigurationSource(): CorsConfigurationSource {
|
||||||
|
val configuration = CorsConfiguration()
|
||||||
|
// Erlaube Frontend (localhost, docker host)
|
||||||
|
configuration.allowedOriginPatterns = listOf("*") // Für Dev; in Prod einschränken!
|
||||||
|
configuration.allowedMethods = listOf("GET", "POST", "PUT", "DELETE", "OPTIONS")
|
||||||
|
configuration.allowedHeaders = listOf("*")
|
||||||
|
configuration.allowCredentials = true
|
||||||
|
|
||||||
|
val source = UrlBasedCorsConfigurationSource()
|
||||||
|
source.registerCorsConfiguration("/**", configuration)
|
||||||
|
return source
|
||||||
|
}
|
||||||
|
}
|
||||||
+40
@@ -0,0 +1,40 @@
|
|||||||
|
package at.mocode.infrastructure.security
|
||||||
|
|
||||||
|
import org.springframework.core.convert.converter.Converter
|
||||||
|
import org.springframework.security.core.GrantedAuthority
|
||||||
|
import org.springframework.security.core.authority.SimpleGrantedAuthority
|
||||||
|
import org.springframework.security.oauth2.jwt.Jwt
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Konvertiert Keycloak-Rollen aus dem JWT (Realm Access & Resource Access)
|
||||||
|
* in Spring Security GrantedAuthorities.
|
||||||
|
*
|
||||||
|
* Erwartetes Format im Token:
|
||||||
|
* "realm_access": { "roles": ["admin", "user"] }
|
||||||
|
* "resource_access": { "my-client": { "roles": ["client-role"] } }
|
||||||
|
*/
|
||||||
|
class KeycloakRoleConverter : Converter<Jwt, Collection<GrantedAuthority>> {
|
||||||
|
|
||||||
|
override fun convert(jwt: Jwt): Collection<GrantedAuthority> {
|
||||||
|
val roles = mutableSetOf<String>()
|
||||||
|
|
||||||
|
// 1. Realm Roles extrahieren
|
||||||
|
val realmAccess = jwt.claims["realm_access"] as? Map<*, *>
|
||||||
|
if (realmAccess != null) {
|
||||||
|
(realmAccess["roles"] as? List<*>)?.forEach { role ->
|
||||||
|
if (role is String) {
|
||||||
|
roles.add(role)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Resource (Client) Roles extrahieren
|
||||||
|
// Optional: Falls wir Client-spezifische Rollen brauchen.
|
||||||
|
// Hier mappen wir vorerst nur Realm-Rollen global.
|
||||||
|
|
||||||
|
// 3. Mapping zu GrantedAuthority (Prefix "ROLE_" ist Standard in Spring Security)
|
||||||
|
return roles.map { role ->
|
||||||
|
SimpleGrantedAuthority("ROLE_${role.uppercase()}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+3
-1
@@ -2,11 +2,13 @@ package at.mocode.ping
|
|||||||
|
|
||||||
import org.springframework.boot.autoconfigure.SpringBootApplication
|
import org.springframework.boot.autoconfigure.SpringBootApplication
|
||||||
import org.springframework.boot.runApplication
|
import org.springframework.boot.runApplication
|
||||||
|
import org.springframework.context.annotation.ComponentScan
|
||||||
import org.springframework.context.annotation.EnableAspectJAutoProxy
|
import org.springframework.context.annotation.EnableAspectJAutoProxy
|
||||||
|
|
||||||
@SpringBootApplication
|
@SpringBootApplication
|
||||||
// Scannt explizit alle Sub-Packages (infrastructure, application, domain)
|
|
||||||
@EnableAspectJAutoProxy
|
@EnableAspectJAutoProxy
|
||||||
|
// Scannt das eigene Service-Package UND das Security-Infrastruktur-Package
|
||||||
|
@ComponentScan(basePackages = ["at.mocode.ping", "at.mocode.infrastructure.security"])
|
||||||
class PingServiceApplication
|
class PingServiceApplication
|
||||||
|
|
||||||
fun main(args: Array<String>) {
|
fun main(args: Array<String>) {
|
||||||
|
|||||||
-32
@@ -1,32 +0,0 @@
|
|||||||
package at.mocode.ping.infrastructure
|
|
||||||
|
|
||||||
import org.springframework.context.annotation.Bean
|
|
||||||
import org.springframework.context.annotation.Configuration
|
|
||||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity
|
|
||||||
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity
|
|
||||||
import org.springframework.security.web.SecurityFilterChain
|
|
||||||
|
|
||||||
@Configuration
|
|
||||||
@EnableWebSecurity
|
|
||||||
class SecurityConfig {
|
|
||||||
|
|
||||||
@Bean
|
|
||||||
fun filterChain(http: HttpSecurity): SecurityFilterChain {
|
|
||||||
http
|
|
||||||
.authorizeHttpRequests { auth ->
|
|
||||||
auth
|
|
||||||
// Public endpoints
|
|
||||||
.requestMatchers("/actuator/**").permitAll()
|
|
||||||
.requestMatchers("/ping/simple", "/ping/enhanced", "/ping/health", "/ping/history").permitAll()
|
|
||||||
// Secure endpoints
|
|
||||||
.requestMatchers("/ping/secure").authenticated()
|
|
||||||
// Default deny
|
|
||||||
.anyRequest().authenticated()
|
|
||||||
}
|
|
||||||
.oauth2ResourceServer { oauth2 ->
|
|
||||||
oauth2.jwt { }
|
|
||||||
}
|
|
||||||
|
|
||||||
return http.build()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+2
-7
@@ -6,18 +6,13 @@ import jakarta.persistence.Table
|
|||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
|
|
||||||
/**
|
|
||||||
* JPA Entity (Infrastructure Detail).
|
|
||||||
* Spiegelt die Datenbank-Tabelle wider.
|
|
||||||
* Nutzt java.util.UUID für JPA-Kompatibilität (bis Hibernate kotlin.uuid nativ unterstützt).
|
|
||||||
*/
|
|
||||||
@Entity
|
@Entity
|
||||||
@Table(name = "pings")
|
@Table(name = "ping")
|
||||||
class PingJpaEntity(
|
class PingJpaEntity(
|
||||||
@Id
|
@Id
|
||||||
val id: UUID,
|
val id: UUID,
|
||||||
val message: String,
|
val message: String,
|
||||||
val timestamp: Instant
|
val createdAt: Instant
|
||||||
) {
|
) {
|
||||||
// Default constructor for JPA
|
// Default constructor for JPA
|
||||||
protected constructor() : this(UUID.randomUUID(), "", Instant.now())
|
protected constructor() : this(UUID.randomUUID(), "", Instant.now())
|
||||||
|
|||||||
+18
-25
@@ -2,48 +2,41 @@ package at.mocode.ping.infrastructure.persistence
|
|||||||
|
|
||||||
import at.mocode.ping.domain.Ping
|
import at.mocode.ping.domain.Ping
|
||||||
import at.mocode.ping.domain.PingRepository
|
import at.mocode.ping.domain.PingRepository
|
||||||
import org.springframework.stereotype.Component
|
import org.springframework.stereotype.Repository
|
||||||
import kotlin.uuid.ExperimentalUuidApi
|
import kotlin.uuid.ExperimentalUuidApi
|
||||||
import kotlin.uuid.Uuid
|
import kotlin.uuid.Uuid
|
||||||
import kotlin.uuid.toJavaUuid
|
import kotlin.uuid.toJavaUuid
|
||||||
import kotlin.uuid.toKotlinUuid
|
import kotlin.uuid.toKotlinUuid
|
||||||
|
|
||||||
/**
|
|
||||||
* Driven Adapter.
|
|
||||||
* Implementiert den Domain-Port `PingRepository` mithilfe von Spring Data JPA.
|
|
||||||
* Mappt zwischen Domain-Entity und JPA-Entity.
|
|
||||||
*/
|
|
||||||
@Component
|
|
||||||
@OptIn(ExperimentalUuidApi::class)
|
@OptIn(ExperimentalUuidApi::class)
|
||||||
|
@Repository
|
||||||
class PingRepositoryAdapter(
|
class PingRepositoryAdapter(
|
||||||
private val jpaRepository: SpringDataPingRepository
|
private val jpaRepository: SpringDataPingRepository
|
||||||
) : PingRepository {
|
) : PingRepository {
|
||||||
|
|
||||||
override fun save(ping: Ping): Ping {
|
override fun save(ping: Ping): Ping {
|
||||||
val jpaEntity = PingJpaEntity(
|
val entity = ping.toEntity()
|
||||||
id = ping.id.toJavaUuid(),
|
val savedEntity = jpaRepository.save(entity)
|
||||||
message = ping.message,
|
return savedEntity.toDomain()
|
||||||
timestamp = ping.timestamp
|
|
||||||
)
|
|
||||||
val saved = jpaRepository.save(jpaEntity)
|
|
||||||
return mapToDomain(saved)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun findAll(): List<Ping> {
|
override fun findAll(): List<Ping> {
|
||||||
return jpaRepository.findAll().map { mapToDomain(it) }
|
return jpaRepository.findAll().map { it.toDomain() }
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun findById(id: Uuid): Ping? {
|
override fun findById(id: Uuid): Ping? {
|
||||||
return jpaRepository.findById(id.toJavaUuid())
|
return jpaRepository.findById(id.toJavaUuid()).map { it.toDomain() }.orElse(null)
|
||||||
.map { mapToDomain(it) }
|
|
||||||
.orElse(null)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun mapToDomain(entity: PingJpaEntity): Ping {
|
private fun Ping.toEntity() = PingJpaEntity(
|
||||||
return Ping(
|
id = this.id.toJavaUuid(),
|
||||||
id = entity.id.toKotlinUuid(),
|
message = this.message,
|
||||||
message = entity.message,
|
createdAt = this.timestamp
|
||||||
timestamp = entity.timestamp
|
)
|
||||||
)
|
|
||||||
}
|
private fun PingJpaEntity.toDomain() = Ping(
|
||||||
|
id = this.id.toKotlinUuid(),
|
||||||
|
message = this.message,
|
||||||
|
timestamp = this.createdAt
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
-2
@@ -1,8 +1,6 @@
|
|||||||
package at.mocode.ping.infrastructure.persistence
|
package at.mocode.ping.infrastructure.persistence
|
||||||
|
|
||||||
import org.springframework.data.jpa.repository.JpaRepository
|
import org.springframework.data.jpa.repository.JpaRepository
|
||||||
import org.springframework.stereotype.Repository
|
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
|
|
||||||
@Repository
|
|
||||||
interface SpringDataPingRepository : JpaRepository<PingJpaEntity, UUID>
|
interface SpringDataPingRepository : JpaRepository<PingJpaEntity, UUID>
|
||||||
|
|||||||
+25
-11
@@ -7,6 +7,7 @@ import at.mocode.ping.api.PingResponse
|
|||||||
import at.mocode.ping.application.PingUseCase
|
import at.mocode.ping.application.PingUseCase
|
||||||
import io.github.resilience4j.circuitbreaker.annotation.CircuitBreaker
|
import io.github.resilience4j.circuitbreaker.annotation.CircuitBreaker
|
||||||
import org.slf4j.LoggerFactory
|
import org.slf4j.LoggerFactory
|
||||||
|
import org.springframework.security.access.prepost.PreAuthorize
|
||||||
import org.springframework.web.bind.annotation.*
|
import org.springframework.web.bind.annotation.*
|
||||||
import java.time.ZoneOffset
|
import java.time.ZoneOffset
|
||||||
import java.time.format.DateTimeFormatter
|
import java.time.format.DateTimeFormatter
|
||||||
@@ -32,14 +33,8 @@ class PingController(
|
|||||||
|
|
||||||
@GetMapping("/ping/simple")
|
@GetMapping("/ping/simple")
|
||||||
override suspend fun simplePing(): PingResponse {
|
override suspend fun simplePing(): PingResponse {
|
||||||
// Ruft Use Case auf -> Speichert in DB
|
|
||||||
val domainPing = pingUseCase.executePing("Simple Ping")
|
val domainPing = pingUseCase.executePing("Simple Ping")
|
||||||
|
return createResponse(domainPing, "pong")
|
||||||
return PingResponse(
|
|
||||||
status = "pong",
|
|
||||||
timestamp = domainPing.timestamp.atOffset(ZoneOffset.UTC).format(formatter),
|
|
||||||
service = "ping-service"
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/ping/enhanced")
|
@GetMapping("/ping/enhanced")
|
||||||
@@ -53,9 +48,7 @@ class PingController(
|
|||||||
throw RuntimeException("Simulated service failure")
|
throw RuntimeException("Simulated service failure")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Use Case Aufruf
|
|
||||||
val domainPing = pingUseCase.executePing("Enhanced Ping")
|
val domainPing = pingUseCase.executePing("Enhanced Ping")
|
||||||
|
|
||||||
val elapsedMs = (System.nanoTime() - start) / 1_000_000
|
val elapsedMs = (System.nanoTime() - start) / 1_000_000
|
||||||
|
|
||||||
return EnhancedPingResponse(
|
return EnhancedPingResponse(
|
||||||
@@ -67,7 +60,29 @@ class PingController(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fallback muss public sein für Resilience4j Proxy
|
// Neue Endpunkte
|
||||||
|
|
||||||
|
@GetMapping("/ping/public")
|
||||||
|
override suspend fun publicPing(): PingResponse {
|
||||||
|
val domainPing = pingUseCase.executePing("Public Ping")
|
||||||
|
return createResponse(domainPing, "public-pong")
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/ping/secure")
|
||||||
|
@PreAuthorize("hasRole('MELD_USER') or hasRole('MELD_ADMIN')") // Beispiel-Rollen
|
||||||
|
override suspend fun securePing(): PingResponse {
|
||||||
|
val domainPing = pingUseCase.executePing("Secure Ping")
|
||||||
|
return createResponse(domainPing, "secure-pong")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper
|
||||||
|
private fun createResponse(domainPing: at.mocode.ping.domain.Ping, status: String) = PingResponse(
|
||||||
|
status = status,
|
||||||
|
timestamp = domainPing.timestamp.atOffset(ZoneOffset.UTC).format(formatter),
|
||||||
|
service = "ping-service"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Fallback
|
||||||
fun fallbackPing(simulate: Boolean, ex: Exception): EnhancedPingResponse {
|
fun fallbackPing(simulate: Boolean, ex: Exception): EnhancedPingResponse {
|
||||||
logger.warn("Circuit breaker fallback triggered: {}", ex.message)
|
logger.warn("Circuit breaker fallback triggered: {}", ex.message)
|
||||||
return EnhancedPingResponse(
|
return EnhancedPingResponse(
|
||||||
@@ -89,7 +104,6 @@ class PingController(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Zusätzlicher Endpunkt um die DB zu prüfen (History)
|
|
||||||
@GetMapping("/ping/history")
|
@GetMapping("/ping/history")
|
||||||
fun getHistory() = pingUseCase.getPingHistory().map {
|
fun getHistory() = pingUseCase.getPingHistory().map {
|
||||||
mapOf("id" to it.id.toString(), "message" to it.message, "time" to it.timestamp.toString())
|
mapOf("id" to it.id.toString(), "message" to it.message, "time" to it.timestamp.toString())
|
||||||
|
|||||||
@@ -8,27 +8,29 @@ spring:
|
|||||||
profiles:
|
profiles:
|
||||||
active: ${SPRING_PROFILES_ACTIVE:dev}
|
active: ${SPRING_PROFILES_ACTIVE:dev}
|
||||||
|
|
||||||
# datasource:
|
datasource:
|
||||||
# url: ${SPRING_DATASOURCE_URL:jdbc:postgresql://localhost:5432/pg-meldestelle-db}
|
url: ${SPRING_DATASOURCE_URL:jdbc:postgresql://localhost:5432/meldestelle}
|
||||||
# username: ${SPRING_DATASOURCE_USERNAME:pg-user}
|
username: ${SPRING_DATASOURCE_USERNAME:postgres}
|
||||||
# password: ${SPRING_DATASOURCE_PASSWORD:pg-password}
|
password: ${SPRING_DATASOURCE_PASSWORD:postgres}
|
||||||
# driver-class-name: org.postgresql.Driver
|
driver-class-name: org.postgresql.Driver
|
||||||
|
|
||||||
# # --- REDIS KONFIGURATION (NEU) ---
|
jpa:
|
||||||
# data:
|
database-platform: org.hibernate.dialect.PostgreSQLDialect
|
||||||
# redis:
|
hibernate:
|
||||||
# host: ${SPRING_DATA_REDIS_HOST:localhost}
|
ddl-auto: validate # Flyway kümmert sich um Schema!
|
||||||
# port: ${SPRING_DATA_REDIS_PORT:6379}
|
open-in-view: false
|
||||||
# password: ${SPRING_DATA_REDIS_PASSWORD:redis-password} # Leer lassen als Default
|
|
||||||
# # Optional: Timeouts für Stabilität
|
|
||||||
# connect-timeout: 5s
|
|
||||||
# timeout: 2s
|
|
||||||
|
|
||||||
# jpa:
|
flyway:
|
||||||
# database-platform: org.hibernate.dialect.PostgreSQLDialect
|
enabled: true
|
||||||
# hibernate:
|
baseline-on-migrate: true
|
||||||
# ddl-auto: update
|
|
||||||
# open-in-view: false
|
security:
|
||||||
|
oauth2:
|
||||||
|
resourceserver:
|
||||||
|
jwt:
|
||||||
|
# Keycloak URL (innerhalb Docker Netzwerk oder Localhost)
|
||||||
|
issuer-uri: ${KEYCLOAK_ISSUER_URI:http://localhost:9090/realms/meldestelle}
|
||||||
|
jwk-set-uri: ${KEYCLOAK_JWK_SET_URI:http://localhost:9090/realms/meldestelle/protocol/openid-connect/certs}
|
||||||
|
|
||||||
cloud:
|
cloud:
|
||||||
consul:
|
consul:
|
||||||
@@ -54,49 +56,29 @@ management:
|
|||||||
probes:
|
probes:
|
||||||
enabled: true
|
enabled: true
|
||||||
tracing:
|
tracing:
|
||||||
# Disable tracing by default to avoid Zipkin connection errors
|
|
||||||
enabled: ${TRACING_ENABLED:false}
|
enabled: ${TRACING_ENABLED:false}
|
||||||
sampling:
|
sampling:
|
||||||
probability: ${TRACING_SAMPLING_PROBABILITY:0.1}
|
probability: ${TRACING_SAMPLING_PROBABILITY:0.1}
|
||||||
zipkin:
|
zipkin:
|
||||||
tracing:
|
tracing:
|
||||||
# Only configure endpoint if tracing is explicitly enabled
|
|
||||||
endpoint: ${ZIPKIN_TRACING_ENDPOINT:http://localhost:9411/api/v2/spans}
|
endpoint: ${ZIPKIN_TRACING_ENDPOINT:http://localhost:9411/api/v2/spans}
|
||||||
# Configure timeout and connection settings to handle missing Zipkin gracefully
|
|
||||||
connect-timeout: 1s
|
connect-timeout: 1s
|
||||||
read-timeout: 5s
|
read-timeout: 5s
|
||||||
|
|
||||||
# Resilience4j Circuit Breaker Configuration
|
|
||||||
resilience4j:
|
resilience4j:
|
||||||
circuitbreaker:
|
circuitbreaker:
|
||||||
configs:
|
configs:
|
||||||
default:
|
default:
|
||||||
# Circuit breaker opens when the failure rate exceeds 50%
|
|
||||||
failure-rate-threshold: 50
|
failure-rate-threshold: 50
|
||||||
# Minimum number of calls to calculate the failure rate
|
|
||||||
minimum-number-of-calls: 5
|
minimum-number-of-calls: 5
|
||||||
# Time to wait before transitioning from OPEN to HALF_OPEN
|
|
||||||
wait-duration-in-open-state: 10s
|
wait-duration-in-open-state: 10s
|
||||||
# Number of calls in HALF_OPEN state before deciding to close/open
|
|
||||||
permitted-number-of-calls-in-half-open-state: 3
|
permitted-number-of-calls-in-half-open-state: 3
|
||||||
# Sliding window size for calculating failure rate
|
|
||||||
sliding-window-size: 10
|
sliding-window-size: 10
|
||||||
# Type of sliding window (COUNT_BASED or TIME_BASED)
|
|
||||||
sliding-window-type: COUNT_BASED
|
sliding-window-type: COUNT_BASED
|
||||||
# Record exceptions that should be considered as failures
|
|
||||||
record-exceptions:
|
record-exceptions:
|
||||||
- java.lang.Exception
|
- java.lang.Exception
|
||||||
# Ignore certain exceptions (don't count as failures)
|
|
||||||
ignore-exceptions:
|
ignore-exceptions:
|
||||||
- java.lang.IllegalArgumentException
|
- java.lang.IllegalArgumentException
|
||||||
instances:
|
instances:
|
||||||
pingCircuitBreaker:
|
pingCircuitBreaker:
|
||||||
# Use default configuration
|
|
||||||
base-config: default
|
base-config: default
|
||||||
# Override specific settings for this instance if needed
|
|
||||||
failure-rate-threshold: 60
|
|
||||||
minimum-number-of-calls: 4
|
|
||||||
wait-duration-in-open-state: 5s
|
|
||||||
|
|
||||||
# Metrics configuration removed to avoid property resolution warnings
|
|
||||||
# Use micrometer and actuator endpoints for metrics instead
|
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
CREATE TABLE ping (
|
||||||
|
id UUID PRIMARY KEY,
|
||||||
|
message VARCHAR(255) NOT NULL,
|
||||||
|
created_at TIMESTAMP WITH TIME ZONE NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Index für schnelle Sortierung nach Zeit (wichtig für Sync später)
|
||||||
|
CREATE INDEX idx_ping_created_at ON ping(created_at);
|
||||||
@@ -4,4 +4,8 @@ interface PingApi {
|
|||||||
suspend fun simplePing(): PingResponse
|
suspend fun simplePing(): PingResponse
|
||||||
suspend fun enhancedPing(simulate: Boolean = false): EnhancedPingResponse
|
suspend fun enhancedPing(simulate: Boolean = false): EnhancedPingResponse
|
||||||
suspend fun healthCheck(): HealthResponse
|
suspend fun healthCheck(): HealthResponse
|
||||||
|
|
||||||
|
// Neue Endpunkte für Security Hardening
|
||||||
|
suspend fun publicPing(): PingResponse
|
||||||
|
suspend fun securePing(): PingResponse
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,38 @@
|
|||||||
|
# Ping Service
|
||||||
|
|
||||||
|
Der `ping-service` ist der "Tracer Bullet" Service für die Meldestelle-Architektur. Er dient als Blueprint für alle weiteren Microservices.
|
||||||
|
|
||||||
|
## Verantwortlichkeit
|
||||||
|
* Technischer Durchstich (Frontend -> Gateway -> Service -> DB).
|
||||||
|
* Validierung der Infrastruktur (Security, Resilience, Observability).
|
||||||
|
* Referenzimplementierung für DDD, Hexagonal Architecture und KMP-Integration.
|
||||||
|
|
||||||
|
## API Endpunkte
|
||||||
|
|
||||||
|
| Methode | Pfad | Beschreibung | Auth |
|
||||||
|
| :--- | :--- | :--- | :--- |
|
||||||
|
| GET | `/ping/simple` | Einfacher Ping, speichert in DB | Public |
|
||||||
|
| GET | `/ping/enhanced` | Ping mit Circuit Breaker Simulation | Public |
|
||||||
|
| GET | `/ping/public` | Expliziter Public Endpoint | Public |
|
||||||
|
| GET | `/ping/secure` | Geschützter Endpoint (benötigt Rolle) | **Secure** (MELD_USER) |
|
||||||
|
| GET | `/ping/health` | Health Check | Public |
|
||||||
|
| GET | `/ping/history` | Historie aller Pings | Public (Debug) |
|
||||||
|
|
||||||
|
## Architektur
|
||||||
|
Der Service folgt der Hexagonalen Architektur (Ports & Adapters):
|
||||||
|
* **Domain:** `at.mocode.ping.domain` (Pure Kotlin, keine Frameworks).
|
||||||
|
* **Application:** `at.mocode.ping.application` (Use Cases, Spring Service).
|
||||||
|
* **Infrastructure:** `at.mocode.ping.infrastructure` (Web, Persistence, Security).
|
||||||
|
|
||||||
|
## Security
|
||||||
|
* Nutzt das zentrale Modul `backend:infrastructure:security`.
|
||||||
|
* OAuth2 Resource Server (JWT Validation via Keycloak).
|
||||||
|
* Rollen-Mapping: Keycloak Realm Roles -> Spring Security Authorities (`ROLE_...`).
|
||||||
|
|
||||||
|
## Persistence
|
||||||
|
* Datenbank: PostgreSQL.
|
||||||
|
* Migration: Flyway (`V1__init_ping.sql`).
|
||||||
|
* ORM: Spring Data JPA (für Write Model).
|
||||||
|
|
||||||
|
## Resilience
|
||||||
|
* Circuit Breaker: Resilience4j (für DB-Zugriffe und simulierte Fehler).
|
||||||
@@ -34,7 +34,7 @@ Dieses Modul wurde neu angelegt. Fülle es mit Leben.
|
|||||||
* Implementiere OAuth2 Resource Server Support (JWT Validierung).
|
* Implementiere OAuth2 Resource Server Support (JWT Validierung).
|
||||||
* Definiere globale CORS-Regeln (Frontend darf zugreifen).
|
* Definiere globale CORS-Regeln (Frontend darf zugreifen).
|
||||||
* [ ] **Role Converter:**
|
* [ ] **Role Converter:**
|
||||||
* Implementiere einen `KeycloakRoleConverter`, der die Rollen aus dem JWT (Realm/Resource Access) in Spring Security `GrantedAuthority` mappt.
|
* Implementiere einen `KeycloakRoleConverter`, der die Rollen aus dem JWT (Realm/Resource Access) in Spring Security `GrantedAuthority` mapping.
|
||||||
* **Wichtig:** Achte auf Kompatibilität. Das Gateway nutzt WebFlux (Reactive), die Services nutzen WebMVC (Servlet). Falls nötig, trenne die Konfigurationen oder nutze `ConditionalOnWebApplication`.
|
* **Wichtig:** Achte auf Kompatibilität. Das Gateway nutzt WebFlux (Reactive), die Services nutzen WebMVC (Servlet). Falls nötig, trenne die Konfigurationen oder nutze `ConditionalOnWebApplication`.
|
||||||
|
|
||||||
### B. Persistence Layer (`backend/infrastructure/persistence`)
|
### B. Persistence Layer (`backend/infrastructure/persistence`)
|
||||||
@@ -46,7 +46,7 @@ Das Modul ist bereits konfiguriert.
|
|||||||
* Nutze `JpaRepository` für Standard-CRUD-Operationen.
|
* Nutze `JpaRepository` für Standard-CRUD-Operationen.
|
||||||
|
|
||||||
### C. Ping Service Hardening (`backend/services/ping/ping-service`)
|
### C. Ping Service Hardening (`backend/services/ping/ping-service`)
|
||||||
Mache den Service "Production Ready".
|
Mache den Service "Production Ready."
|
||||||
|
|
||||||
* [ ] **Flyway:**
|
* [ ] **Flyway:**
|
||||||
* Erstelle `src/main/resources/db/migration/V1__init_ping.sql`.
|
* Erstelle `src/main/resources/db/migration/V1__init_ping.sql`.
|
||||||
|
|||||||
Reference in New Issue
Block a user