feat(security): add internal secret handling and Redis integration for bot events

This commit is contained in:
2026-04-28 09:29:05 +02:00
parent c10a4d7e64
commit 1ab6532b0a
50 changed files with 951 additions and 214 deletions
+1
View File
@@ -52,6 +52,7 @@ dependencies {
implementation(project(":modules:rule"))
implementation(project(":modules:io"))
implementation(project(":modules:official-bots"))
implementation(project(":modules:security"))
implementation(enforcedPlatform("${quarkusPlatformGroupId}:${quarkusPlatformArtifactId}:${quarkusPlatformVersion}"))
@@ -23,6 +23,9 @@ nowchess:
port: 6379
prefix: nowchess
internal:
secret: ${INTERNAL_SECRET}
coordinator:
enabled: ${NOWCHESS_COORDINATOR_ENABLED:false}
host: localhost
@@ -2,14 +2,17 @@ package de.nowchess.chess.client
import de.nowchess.api.dto.{ImportFenRequest, ImportPgnRequest}
import de.nowchess.api.game.GameContext
import de.nowchess.security.InternalSecretClientFilter
import jakarta.ws.rs.*
import jakarta.ws.rs.core.MediaType
import org.eclipse.microprofile.rest.client.annotation.RegisterProvider
import org.eclipse.microprofile.rest.client.inject.RegisterRestClient
case class CombinedExportResponse(fen: String, pgn: String)
@Path("/io")
@RegisterRestClient(configKey = "io-service")
@RegisterProvider(classOf[InternalSecretClientFilter])
trait IoServiceClient:
@POST
@@ -3,8 +3,10 @@ package de.nowchess.chess.client
import de.nowchess.api.game.GameContext
import de.nowchess.api.move.Move
import de.nowchess.api.rules.PostMoveStatus
import de.nowchess.security.InternalSecretClientFilter
import jakarta.ws.rs.*
import jakarta.ws.rs.core.MediaType
import org.eclipse.microprofile.rest.client.annotation.RegisterProvider
import org.eclipse.microprofile.rest.client.inject.RegisterRestClient
case class RuleSquareRequest(context: GameContext, square: String)
@@ -12,6 +14,7 @@ case class RuleMoveRequest(context: GameContext, move: Move)
@Path("/api/rules")
@RegisterRestClient(configKey = "rule-service")
@RegisterProvider(classOf[InternalSecretClientFilter])
trait RuleServiceClient:
@POST
@@ -1,10 +1,13 @@
package de.nowchess.chess.client
import de.nowchess.security.InternalSecretClientFilter
import jakarta.ws.rs.*
import jakarta.ws.rs.core.MediaType
import org.eclipse.microprofile.rest.client.annotation.RegisterProvider
import org.eclipse.microprofile.rest.client.inject.RegisterRestClient
@RegisterRestClient(configKey = "store-service")
@RegisterProvider(classOf[InternalSecretClientFilter])
@Path("/game")
trait StoreServiceClient:
@GET
@@ -4,6 +4,8 @@ import com.fasterxml.jackson.databind.ObjectMapper
import de.nowchess.api.dto.GameStateEventDto
import de.nowchess.api.game.{CorrespondenceClockState, LiveClockState}
import de.nowchess.chess.grpc.IoGrpcClientWrapper
import de.nowchess.api.game.{DrawReason, GameResult, WinReason}
import de.nowchess.api.board.Color
import de.nowchess.chess.observer.{GameEvent, Observer}
import de.nowchess.chess.registry.GameRegistry
import de.nowchess.chess.resource.GameDtoMapper
@@ -54,6 +56,21 @@ class GameRedisPublisher(
clockMoveDeadline = clock.collect { case c: CorrespondenceClockState => c.moveDeadline.toEpochMilli },
clockActiveColor = clock.map(_.activeColor.label.toLowerCase),
pendingDrawOffer = entry.engine.pendingDrawOfferBy.map(_.label.toLowerCase),
result = entry.engine.context.result.map {
case GameResult.Win(Color.White, _) => "white"
case GameResult.Win(Color.Black, _) => "black"
case GameResult.Draw(_) => "draw"
},
terminationReason = entry.engine.context.result.map {
case GameResult.Win(_, WinReason.Checkmate) => "checkmate"
case GameResult.Win(_, WinReason.Resignation) => "resignation"
case GameResult.Win(_, WinReason.TimeControl) => "timeout"
case GameResult.Draw(DrawReason.Stalemate) => "stalemate"
case GameResult.Draw(DrawReason.InsufficientMaterial) => "insufficient_material"
case GameResult.Draw(DrawReason.FiftyMoveRule) => "fifty_move"
case GameResult.Draw(DrawReason.ThreefoldRepetition) => "repetition"
case GameResult.Draw(DrawReason.Agreement) => "agreement"
},
redoStack = entry.engine.redoStackMoves.map(GameDtoMapper.moveToUci),
pendingTakebackRequest = entry.engine.pendingTakebackRequestBy.map(_.label.toLowerCase),
)
@@ -21,6 +21,8 @@ case class GameWritebackEventDto(
clockMoveDeadline: Option[Long],
clockActiveColor: Option[String],
pendingDrawOffer: Option[String],
result: Option[String] = None,
terminationReason: Option[String] = None,
redoStack: List[String] = Nil,
pendingTakebackRequest: Option[String] = None,
)
@@ -38,7 +38,7 @@ object GameDtoMapper:
case _ => base
def toPlayerDto(info: PlayerInfo): PlayerInfoDto =
PlayerInfoDto(info.id.value, info.displayName)
PlayerInfoDto(info.id.value, info.displayName, info.playerType)
def toClockDto(entry: GameEntry): Option[ClockDto] =
val now = Instant.now()
@@ -24,6 +24,7 @@ import de.nowchess.chess.grpc.{IoGrpcClientWrapper, RuleSetGrpcAdapter}
import de.nowchess.chess.observer.*
import de.nowchess.chess.redis.GameRedisSubscriberManager
import de.nowchess.chess.registry.{GameEntry, GameRegistry}
import de.nowchess.security.InternalOnly
import jakarta.enterprise.context.ApplicationScoped
import jakarta.inject.Inject
import jakarta.ws.rs.*
@@ -79,9 +80,9 @@ class GameResource:
val color = colorOf(entry)
if color != entry.engine.context.turn then throw ForbiddenException("Not your turn")
private def assertIsBot(): Unit =
private def assertIsNotBot(): Unit =
val botType = Option(jwt.getClaim[AnyRef]("type")).map(_.toString).getOrElse("")
if !Set("bot", "official-bot").contains(botType) then
if Set("bot", "official-bot").contains(botType) then
throw ForbiddenException("Only bots can make moves")
// scalafix:on DisableSyntax.throw
@@ -153,6 +154,7 @@ class GameResource:
// scalafix:off DisableSyntax.throw
@POST
@InternalOnly
@Consumes(Array(MediaType.APPLICATION_JSON))
@Produces(Array(MediaType.APPLICATION_JSON))
def createGame(body: CreateGameRequestDto): Response =
@@ -189,7 +191,7 @@ class GameResource:
@Path("/{gameId}/move/{uci}")
@Produces(Array(MediaType.APPLICATION_JSON))
def makeMove(@PathParam("gameId") gameId: String, @PathParam("uci") uci: String): Response =
assertIsBot()
assertIsNotBot()
val entry = registry.get(gameId).getOrElse(throw GameNotFoundException(gameId))
assertGameNotOver(entry)
assertIsCurrentPlayer(entry)