feat(security): add internal secret handling and Redis integration for bot events
This commit is contained in:
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user