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
+65
View File
@@ -0,0 +1,65 @@
plugins {
id("scala")
id("org.scoverage") version "8.1"
}
group = "de.nowchess"
version = "1.0-SNAPSHOT"
@Suppress("UNCHECKED_CAST")
val versions = rootProject.extra["VERSIONS"] as Map<String, String>
repositories {
mavenCentral()
}
scala {
scalaVersion = versions["SCALA3"]!!
}
scoverage {
scoverageVersion.set(versions["SCOVERAGE"]!!)
}
tasks.withType<ScalaCompile> {
scalaCompileOptions.additionalParameters = listOf("-encoding", "UTF-8")
}
val quarkusPlatformGroupId: String by project
val quarkusPlatformArtifactId: String by project
val quarkusPlatformVersion: String by project
dependencies {
compileOnly("org.scala-lang:scala3-compiler_3") {
version { strictly(versions["SCALA3"]!!) }
}
implementation("org.scala-lang:scala3-library_3") {
version { strictly(versions["SCALA3"]!!) }
}
compileOnly(platform("${quarkusPlatformGroupId}:${quarkusPlatformArtifactId}:${quarkusPlatformVersion}"))
compileOnly("io.quarkus:quarkus-rest")
compileOnly("io.quarkus:quarkus-rest-client")
compileOnly("io.quarkus:quarkus-grpc")
compileOnly("io.quarkus:quarkus-arc")
}
configurations.matching { !it.name.startsWith("scoverage") }.configureEach {
resolutionStrategy.force("org.scala-lang:scala-library:${versions["SCALA_LIBRARY"]!!}")
}
configurations.scoverage {
resolutionStrategy.eachDependency {
if (requested.group == "org.scoverage" && requested.name.startsWith("scalac-scoverage-plugin_")) {
useTarget("${requested.group}:scalac-scoverage-plugin_2.13.16:2.3.0")
}
}
}
tasks.withType<JavaCompile> {
options.encoding = "UTF-8"
options.compilerArgs.add("-parameters")
}
tasks.withType<Jar>().configureEach {
duplicatesStrategy = DuplicatesStrategy.EXCLUDE
}
@@ -0,0 +1,12 @@
package de.nowchess.security;
import jakarta.ws.rs.NameBinding;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@NameBinding
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD})
public @interface InternalOnly {}
@@ -0,0 +1,23 @@
package de.nowchess.security
import jakarta.enterprise.context.ApplicationScoped
import jakarta.ws.rs.container.{ContainerRequestContext, ContainerRequestFilter}
import jakarta.ws.rs.core.Response
import jakarta.ws.rs.ext.Provider
import org.eclipse.microprofile.config.inject.ConfigProperty
import scala.compiletime.uninitialized
@Provider
@InternalOnly
@ApplicationScoped
class InternalAuthFilter extends ContainerRequestFilter:
@ConfigProperty(name = "nowchess.internal.secret")
// scalafix:off DisableSyntax.var
var secret: String = uninitialized
// scalafix:on DisableSyntax.var
override def filter(ctx: ContainerRequestContext): Unit =
val header = ctx.getHeaderString("X-Internal-Secret")
if header == null || header != secret then
ctx.abortWith(Response.status(Response.Status.UNAUTHORIZED).build())
@@ -0,0 +1,30 @@
package de.nowchess.security
import io.grpc.{Metadata, ServerCall, ServerCallHandler, ServerInterceptor, Status}
import io.quarkus.grpc.GlobalInterceptor
import jakarta.enterprise.context.ApplicationScoped
import org.eclipse.microprofile.config.inject.ConfigProperty
import scala.compiletime.uninitialized
@GlobalInterceptor
@ApplicationScoped
class InternalGrpcAuthInterceptor extends ServerInterceptor:
private val secretKey = Metadata.Key.of("x-internal-secret", Metadata.ASCII_STRING_MARSHALLER)
@ConfigProperty(name = "nowchess.internal.secret")
// scalafix:off DisableSyntax.var
var secret: String = uninitialized
// scalafix:on DisableSyntax.var
override def interceptCall[Req, Resp](
call: ServerCall[Req, Resp],
headers: Metadata,
next: ServerCallHandler[Req, Resp],
): ServerCall.Listener[Req] =
val token = Option(headers.get(secretKey)).getOrElse("")
if token != secret then
call.close(Status.UNAUTHENTICATED.withDescription("Missing or invalid internal secret"), new Metadata())
new ServerCall.Listener[Req] {}
else
next.startCall(call, headers)
@@ -0,0 +1,28 @@
package de.nowchess.security
import io.grpc.{CallOptions, Channel, ClientCall, ClientInterceptor, ForwardingClientCall, Metadata, MethodDescriptor}
import io.quarkus.grpc.GlobalInterceptor
import jakarta.enterprise.context.ApplicationScoped
import org.eclipse.microprofile.config.inject.ConfigProperty
import scala.compiletime.uninitialized
@GlobalInterceptor
@ApplicationScoped
class InternalGrpcSecretClientInterceptor extends ClientInterceptor:
private val secretKey = Metadata.Key.of("x-internal-secret", Metadata.ASCII_STRING_MARSHALLER)
@ConfigProperty(name = "nowchess.internal.secret")
// scalafix:off DisableSyntax.var
var secret: String = uninitialized
// scalafix:on DisableSyntax.var
override def interceptCall[Req, Resp](
method: MethodDescriptor[Req, Resp],
callOptions: CallOptions,
next: Channel,
): ClientCall[Req, Resp] =
new ForwardingClientCall.SimpleForwardingClientCall[Req, Resp](next.newCall(method, callOptions)):
override def start(responseListener: ClientCall.Listener[Resp], headers: Metadata): Unit =
headers.put(secretKey, secret)
super.start(responseListener, headers)
@@ -0,0 +1,17 @@
package de.nowchess.security
import jakarta.enterprise.context.ApplicationScoped
import jakarta.ws.rs.client.{ClientRequestContext, ClientRequestFilter}
import org.eclipse.microprofile.config.inject.ConfigProperty
import scala.compiletime.uninitialized
@ApplicationScoped
class InternalSecretClientFilter extends ClientRequestFilter:
@ConfigProperty(name = "nowchess.internal.secret")
// scalafix:off DisableSyntax.var
var secret: String = uninitialized
// scalafix:on DisableSyntax.var
override def filter(ctx: ClientRequestContext): Unit =
ctx.getHeaders.putSingle("X-Internal-Secret", secret)