From 16128ee3c305ee3406d8770e4bb3eee314a9725b Mon Sep 17 00:00:00 2001 From: Lukas Jost Date: Sun, 28 Jun 2026 23:25:18 +0200 Subject: [PATCH] feat: gate agones command with permission --- velocity/build.gradle.kts | 1 + .../META-INF/grounds/permissions.json | 11 +++ .../gg/grounds/command/AgonesCommandTest.kt | 72 +++++++++++++++++++ .../command/AgonesPermissionManifestTest.kt | 36 ++++++++++ 4 files changed, 120 insertions(+) create mode 100644 velocity/src/main/resources/META-INF/grounds/permissions.json create mode 100644 velocity/src/test/kotlin/gg/grounds/command/AgonesCommandTest.kt create mode 100644 velocity/src/test/kotlin/gg/grounds/command/AgonesPermissionManifestTest.kt diff --git a/velocity/build.gradle.kts b/velocity/build.gradle.kts index c4619f1..c7d7a05 100644 --- a/velocity/build.gradle.kts +++ b/velocity/build.gradle.kts @@ -6,6 +6,7 @@ dependencies { implementation(project(":common")) implementation("io.kubernetes:client-java") + testImplementation("com.velocitypowered:velocity-api") testImplementation("org.junit.jupiter:junit-jupiter") testRuntimeOnly("org.junit.platform:junit-platform-launcher") } diff --git a/velocity/src/main/resources/META-INF/grounds/permissions.json b/velocity/src/main/resources/META-INF/grounds/permissions.json new file mode 100644 index 0000000..3061811 --- /dev/null +++ b/velocity/src/main/resources/META-INF/grounds/permissions.json @@ -0,0 +1,11 @@ +{ + "source": "plugin-agones", + "permissions": [ + { + "key": "grounds.command.agones", + "label": "Use Agones command", + "description": "Allows using the Velocity /agones command to inspect registered Agones servers.", + "supportedScopes": ["GLOBAL", "SERVER_TYPE", "SERVER"] + } + ] +} diff --git a/velocity/src/test/kotlin/gg/grounds/command/AgonesCommandTest.kt b/velocity/src/test/kotlin/gg/grounds/command/AgonesCommandTest.kt new file mode 100644 index 0000000..7aaab84 --- /dev/null +++ b/velocity/src/test/kotlin/gg/grounds/command/AgonesCommandTest.kt @@ -0,0 +1,72 @@ +package gg.grounds.command + +import com.velocitypowered.api.command.CommandSource +import com.velocitypowered.api.command.SimpleCommand +import com.velocitypowered.api.permission.Tristate +import com.velocitypowered.api.proxy.ConsoleCommandSource +import com.velocitypowered.api.proxy.ProxyServer +import java.lang.reflect.Proxy +import org.junit.jupiter.api.Assertions.assertFalse +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.Test + +class AgonesCommandTest { + + @Test + fun `allows console to run agones command`() { + val command = AgonesCommand(proxyServer(), getServerRole = { null }) + + assertTrue(command.hasPermission(invocation(consoleSource()))) + } + + @Test + fun `allows command source with grounds command namespace permission`() { + val command = AgonesCommand(proxyServer(), getServerRole = { null }) + + assertTrue(command.hasPermission(invocation(commandSource("grounds.command.agones")))) + } + + @Test + fun `rejects command source without agones command permission`() { + val command = AgonesCommand(proxyServer(), getServerRole = { null }) + + assertFalse(command.hasPermission(invocation(commandSource()))) + } + + private fun proxyServer(): ProxyServer = proxy() + + private fun invocation(source: CommandSource): SimpleCommand.Invocation = + proxy(mapOf("source" to source, "arguments" to emptyArray(), "alias" to "agones")) + + private fun commandSource(vararg permissions: String): CommandSource = + proxy( + mapOf( + "hasPermission" to { args: Array -> args.single() in permissions }, + "getPermissionValue" to + { args: Array -> + Tristate.fromBoolean(args.single() in permissions) + }, + ) + ) + + private fun consoleSource(): ConsoleCommandSource = proxy() + + @Suppress("UNCHECKED_CAST") + private inline fun proxy(responses: Map = emptyMap()): T = + Proxy.newProxyInstance(T::class.java.classLoader, arrayOf(T::class.java)) { _, method, args + -> + val response = responses[method.name] + when { + response is Function1<*, *> -> + (response as (Array) -> Any?)(args.orEmpty()) + response != null -> response + method.returnType == Boolean::class.javaPrimitiveType -> false + method.returnType == Int::class.javaPrimitiveType -> 0 + method.returnType == Long::class.javaPrimitiveType -> 0L + method.returnType == Double::class.javaPrimitiveType -> 0.0 + method.returnType == Float::class.javaPrimitiveType -> 0.0f + method.returnType == Void.TYPE -> null + else -> null + } + } as T +} diff --git a/velocity/src/test/kotlin/gg/grounds/command/AgonesPermissionManifestTest.kt b/velocity/src/test/kotlin/gg/grounds/command/AgonesPermissionManifestTest.kt new file mode 100644 index 0000000..d76cd8c --- /dev/null +++ b/velocity/src/test/kotlin/gg/grounds/command/AgonesPermissionManifestTest.kt @@ -0,0 +1,36 @@ +package gg.grounds.command + +import com.google.gson.JsonParser +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertNotNull +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.Test + +class AgonesPermissionManifestTest { + + @Test + fun `declares agones command permission in manifest resource`() { + val resource = + checkNotNull( + javaClass.classLoader.getResourceAsStream("META-INF/grounds/permissions.json") + ) { + "permissions manifest resource is missing" + } + + val manifest = JsonParser.parseReader(resource.reader()).asJsonObject + val permissions = manifest.getAsJsonArray("permissions") + val agonesCommand = + permissions + .map { it.asJsonObject } + .singleOrNull { it.get("key").asString == "grounds.command.agones" } + + assertNotNull(agonesCommand) + assertEquals("plugin-agones", manifest.get("source").asString) + assertEquals("Use Agones command", agonesCommand!!.get("label").asString) + assertTrue(agonesCommand.get("description").asString.isNotBlank()) + assertEquals( + listOf("GLOBAL", "SERVER_TYPE", "SERVER"), + agonesCommand.getAsJsonArray("supportedScopes").map { it.asString }, + ) + } +}