Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions common/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ dependencies {
implementation(platform("gg.grounds:grounds-dependencies:0.1.0"))

api("com.google.protobuf:protobuf-java")
implementation("tools.jackson.core:jackson-databind:3.1.0")
implementation("tools.jackson.module:jackson-module-kotlin:3.1.0")

testImplementation("org.junit.jupiter:junit-jupiter")
testRuntimeOnly("org.junit.platform:junit-platform-launcher")
Expand Down
Original file line number Diff line number Diff line change
@@ -1,58 +1,64 @@
package gg.grounds.permissions.velocity
package gg.grounds.permissions.catalog

import java.io.InputStream
import tools.jackson.databind.DeserializationFeature
import tools.jackson.databind.ObjectMapper
import tools.jackson.databind.json.JsonMapper
import tools.jackson.module.kotlin.KotlinModule

data class PermissionManifest(val permissions: List<PermissionManifestEntry>) {
data class PermissionManifest(val source: String, val permissions: List<PermissionManifestEntry>) {
init {
require(source.isNotBlank()) { "Permission manifest source must not be blank." }
require(permissions.isNotEmpty()) {
"Permission manifest must declare at least one permission."
}
require(permissions.map(PermissionManifestEntry::key).toSet().size == permissions.size) {
"Permission manifest must not declare duplicate keys."
}
permissions.forEach { permission ->
require(permission.key.isNotBlank()) { "Permission manifest keys must not be blank." }
require(permission.label.isNotBlank()) {
"Permission manifest labels must not be blank."
}
require(permission.description.isNotBlank()) {
"Permission manifest descriptions must not be blank (key=${permission.key})."
}
require(permission.supportedScopes.isNotEmpty()) {
"Permission manifest scopes must not be empty (key=${permission.key})."
}
}
}

companion object {
private const val RESOURCE_NAME = "META-INF/grounds-permissions.json"
const val RESOURCE_PATH = "META-INF/grounds/permissions.json"

private val mapper: ObjectMapper =
JsonMapper.builder()
.addModule(KotlinModule.Builder().build())
.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)
.build()

fun loadResource(): PermissionManifest {
val input =
PermissionManifest::class.java.classLoader.getResourceAsStream(RESOURCE_NAME)
?: error("Missing required permission manifest resource $RESOURCE_NAME")
return input.use(::load)
}

fun load(input: InputStream): PermissionManifest =
mapper.readValue(input, PermissionManifest::class.java).validated()
}

private fun validated(): PermissionManifest {
permissions.forEach { permission ->
require(permission.key.isNotBlank()) { "Permission manifest keys must not be blank." }
require(permission.label.isNotBlank()) {
"Permission manifest labels must not be blank."
try {
mapper.readValue(input, PermissionManifest::class.java)
} catch (exception: Exception) {
throw IllegalArgumentException("Invalid permission manifest.", exception)
}
require(permission.supportedScopes.isNotEmpty()) {
"Permission manifest scopes must not be empty (key=${permission.key})."

fun loadResource(classLoader: ClassLoader): PermissionManifest? =
classLoader.getResourceAsStream(RESOURCE_PATH)?.use(::load)

fun loadRequiredResource(classLoader: ClassLoader): PermissionManifest =
requireNotNull(loadResource(classLoader)) {
"Missing required permission manifest resource $RESOURCE_PATH"
}
}
return this
}
}

data class PermissionManifestEntry(
val key: String,
val label: String,
val description: String = "",
val description: String,
val supportedScopes: List<PermissionManifestScope>,
)

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package gg.grounds.permissions.catalog

data class ManifestOrigin(val id: String, val version: String, val classLoader: ClassLoader)

data class CollectedPermissionManifest(val origin: ManifestOrigin, val manifest: PermissionManifest)

data class PermissionManifestCollectionFailure(val origin: ManifestOrigin, val reason: String)

data class PermissionManifestCollection(
val manifests: List<CollectedPermissionManifest>,
val failures: List<PermissionManifestCollectionFailure>,
)

class PermissionManifestCollector {
fun collect(origins: Iterable<ManifestOrigin>): PermissionManifestCollection {
val manifests = mutableListOf<CollectedPermissionManifest>()
val failures = mutableListOf<PermissionManifestCollectionFailure>()

origins.forEach { origin ->
try {
val manifest = PermissionManifest.loadResource(origin.classLoader) ?: return@forEach
manifests += CollectedPermissionManifest(origin, manifest)
} catch (exception: IllegalArgumentException) {
failures +=
PermissionManifestCollectionFailure(
origin = origin,
reason = exception.message ?: exception::class.java.simpleName,
)
}
}

return PermissionManifestCollection(manifests = manifests, failures = failures)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
package gg.grounds.permissions.catalog

import java.io.ByteArrayInputStream
import java.io.InputStream
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertThrows
import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.Test

class PermissionManifestCollectorTest {
@Test
fun `loads the established manifest shape`() {
val manifest =
PermissionManifest.load(
"""
{
"source": "plugin-agones",
"permissions": [
{
"key": "grounds.command.agones",
"label": "Use Agones command",
"description": "Allows using the Agones command.",
"supportedScopes": ["GLOBAL", "SERVER_TYPE", "SERVER"]
}
]
}
"""
.trimIndent()
.byteInputStream()
)

assertEquals("plugin-agones", manifest.source)
assertEquals("grounds.command.agones", manifest.permissions.single().key)
assertEquals(
listOf(
PermissionManifestScope.GLOBAL,
PermissionManifestScope.SERVER_TYPE,
PermissionManifestScope.SERVER,
),
manifest.permissions.single().supportedScopes,
)
}

@Test
fun `skips origins without a manifest resource`() {
val collection =
PermissionManifestCollector()
.collect(listOf(ManifestOrigin("missing-plugin", "1.0.0", ResourceClassLoader())))

assertTrue(collection.manifests.isEmpty())
assertTrue(collection.failures.isEmpty())
}

@Test
fun `reports duplicate permission keys without collecting the manifest`() {
val collection =
PermissionManifestCollector()
.collect(
listOf(
ManifestOrigin(
"duplicate-plugin",
"1.0.0",
ResourceClassLoader(
"""
{
"source": "duplicate-plugin",
"permissions": [
{ "key": "grounds.command.duplicate", "label": "Duplicate", "description": "Allows duplicate command.", "supportedScopes": ["GLOBAL"] },
{ "key": "grounds.command.duplicate", "label": "Duplicate again", "description": "Allows duplicate command again.", "supportedScopes": ["GLOBAL"] }
]
}
"""
.trimIndent()
),
)
)
)

assertTrue(collection.manifests.isEmpty())
assertEquals("duplicate-plugin", collection.failures.single().origin.id)
}

@Test
fun `reports manifests with missing or blank sources`() {
val collection =
PermissionManifestCollector()
.collect(
listOf(
ManifestOrigin(
"missing-source",
"1.0.0",
ResourceClassLoader(
"""
{
"permissions": [
{ "key": "grounds.command.missing-source", "label": "Missing source", "description": "Requires a source.", "supportedScopes": ["GLOBAL"] }
]
}
"""
.trimIndent()
),
),
ManifestOrigin(
"blank-source",
"1.0.0",
ResourceClassLoader(
"""
{
"source": " ",
"permissions": [
{ "key": "grounds.command.blank-source", "label": "Blank source", "description": "Requires a source.", "supportedScopes": ["GLOBAL"] }
]
}
"""
.trimIndent()
),
),
)
)

assertTrue(collection.manifests.isEmpty())
assertEquals(
listOf("missing-source", "blank-source"),
collection.failures.map { it.origin.id },
)
}

@Test
fun `reports manifests with blank required permission entry fields`() {
val collection =
PermissionManifestCollector()
.collect(
listOf("key", "label", "description").map { field ->
ManifestOrigin(
"blank-$field",
"1.0.0",
ResourceClassLoader(
"""
{
"source": "blank-$field",
"permissions": [
{
"key": "${if (field == "key") " " else "grounds.command.blank-$field"}",
"label": "${if (field == "label") " " else "Use command"}",
"description": "${if (field == "description") " " else "Allows using the command."}",
"supportedScopes": ["GLOBAL"]
}
]
}
"""
.trimIndent()
),
)
}
)

assertTrue(collection.manifests.isEmpty())
assertEquals(
listOf("blank-key", "blank-label", "blank-description"),
collection.failures.map { it.origin.id },
)
}

@Test
fun `rejects unsupported scopes`() {
assertThrows(IllegalArgumentException::class.java) {
PermissionManifest.load(
"""
{
"source": "invalid-plugin",
"permissions": [
{ "key": "grounds.command.invalid", "label": "Invalid", "description": "Allows invalid command.", "supportedScopes": ["PROJECT"] }
]
}
"""
.trimIndent()
.byteInputStream()
)
}
}

private class ResourceClassLoader(manifest: String? = null) : ClassLoader() {
private val bytes = manifest?.encodeToByteArray()

override fun getResourceAsStream(name: String): InputStream? =
if (name == PermissionManifest.RESOURCE_PATH && bytes != null) {
ByteArrayInputStream(bytes)
} else {
null
}
}
}
2 changes: 1 addition & 1 deletion minestom/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ buildConfig {
dependencies {
implementation(platform("gg.grounds:grounds-dependencies:0.1.0"))

api("gg.grounds:grounds-minestom-runtime-runtime-api:0.1.0")
api("gg.grounds:grounds-minestom-runtime-runtime-api:0.3.0")
implementation(project(":common"))
implementation("io.grpc:grpc-netty-shaded:1.79.0")
implementation("io.grpc:grpc-stub")
Expand Down
Loading