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
@@ -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)