diff --git a/common/build.gradle.kts b/common/build.gradle.kts index 6d85acb..b38ee3b 100644 --- a/common/build.gradle.kts +++ b/common/build.gradle.kts @@ -1,4 +1,7 @@ -plugins { id("gg.grounds.grpc-conventions") } +plugins { + id("gg.grounds.grpc-conventions") + id("java-test-fixtures") +} dependencies { implementation(platform("gg.grounds:grounds-dependencies:0.1.0")) @@ -8,5 +11,6 @@ dependencies { implementation("tools.jackson.module:jackson-module-kotlin:3.1.0") testImplementation("org.junit.jupiter:junit-jupiter") + testFixturesImplementation(platform("gg.grounds:grounds-dependencies:0.1.0")) testRuntimeOnly("org.junit.platform:junit-platform-launcher") } diff --git a/common/src/testFixtures/kotlin/gg/grounds/permissions/ReleasedPermissionSnapshotFixture.kt b/common/src/testFixtures/kotlin/gg/grounds/permissions/ReleasedPermissionSnapshotFixture.kt new file mode 100644 index 0000000..487ec34 --- /dev/null +++ b/common/src/testFixtures/kotlin/gg/grounds/permissions/ReleasedPermissionSnapshotFixture.kt @@ -0,0 +1,98 @@ +package gg.grounds.permissions + +import com.google.protobuf.TextFormat +import gg.grounds.grpc.permissions.PlayerPermissionSnapshot +import java.time.Instant +import java.util.UUID + +object ReleasedPermissionSnapshotFixture { + val playerId: UUID = UUID.fromString("00000000-0000-0000-0000-000000000321") + + fun proto(): PlayerPermissionSnapshot { + val fixture = + requireNotNull(javaClass.getResourceAsStream(FIXTURE_PATH)) { + "Released permission snapshot fixture is missing (path=$FIXTURE_PATH)" + } + return fixture.bufferedReader().use { reader -> + PlayerPermissionSnapshot.newBuilder() + .also { TextFormat.getParser().merge(reader, it) } + .build() + } + } + + fun expected(): PermissionSnapshot = + PermissionSnapshot( + playerId = playerId, + policyVersion = 73, + issuedAt = Instant.parse("2030-01-01T00:00:00Z"), + refreshAfter = Instant.parse("2030-01-01T00:05:00Z"), + expiresAt = Instant.parse("2030-01-01T01:00:00Z"), + allowPatterns = + listOf( + roleGrant( + pattern = "spawn.use", + origin = PermissionGrantOriginKind.DEFAULT_ROLE, + roleKey = "default", + ), + roleGrant( + pattern = "build.use", + origin = PermissionGrantOriginKind.DIRECT_ROLE, + roleKey = "builder", + ), + roleGrant( + pattern = "staff.chat", + origin = PermissionGrantOriginKind.GROUP_MAPPING, + roleKey = "staff", + mappingId = MAPPING_ID, + ), + roleGrant( + pattern = "home.use", + origin = PermissionGrantOriginKind.GROUP_MAPPING, + roleKey = "member", + mappingId = MAPPING_ID, + inheritedPath = listOf("staff", "member"), + ), + ), + denyPatterns = + listOf( + PermissionGrant( + effect = PermissionEffect.DENY, + pattern = "operator.use", + scope = PermissionScope.server("survival-1"), + source = PermissionGrantSource.PLAYER, + expiresAt = Instant.parse("2030-01-01T01:00:00Z"), + origin = + PermissionGrantOrigin( + kind = PermissionGrantOriginKind.DIRECT_PERMISSION + ), + ) + ), + roleKeys = setOf("default", "builder", "staff", "member"), + roleMetadata = + listOf( + RoleMetadata("default", "Default", "[D]", "gray", 100), + RoleMetadata("builder", "Builder", "[B]", "green", 75), + RoleMetadata("staff", "Staff", "[S]", "red", 50), + RoleMetadata("member", "Member", null, null, 25), + ), + ) + + private fun roleGrant( + pattern: String, + origin: PermissionGrantOriginKind, + roleKey: String, + mappingId: String? = null, + inheritedPath: List = emptyList(), + ): PermissionGrant = + PermissionGrant( + effect = PermissionEffect.ALLOW, + pattern = pattern, + scope = PermissionScope.global(), + source = PermissionGrantSource.ROLE, + origin = PermissionGrantOrigin(origin, roleKey, mappingId, inheritedPath), + ) + + private const val FIXTURE_PATH = + "/fixtures/service-permissions-v0.7.0/player-permission-snapshot.textproto" + private const val MAPPING_ID = "00000000-0000-0000-0000-000000000322" +} diff --git a/common/src/testFixtures/resources/fixtures/service-permissions-v0.7.0/player-permission-snapshot.textproto b/common/src/testFixtures/resources/fixtures/service-permissions-v0.7.0/player-permission-snapshot.textproto new file mode 100644 index 0000000..b59bdc0 --- /dev/null +++ b/common/src/testFixtures/resources/fixtures/service-permissions-v0.7.0/player-permission-snapshot.textproto @@ -0,0 +1,90 @@ +player_id: "00000000-0000-0000-0000-000000000321" +policy_version: 73 +issued_at { seconds: 1893456000 } +refresh_after { seconds: 1893456300 } +expires_at { seconds: 1893459600 } +allow_patterns { + effect: PERMISSION_EFFECT_ALLOW + pattern: "spawn.use" + scope { kind: PERMISSION_SCOPE_KIND_GLOBAL } + source: PERMISSION_GRANT_SOURCE_ROLE + origin { + kind: PERMISSION_GRANT_ORIGIN_KIND_DEFAULT_ROLE + role_key: "default" + } +} +allow_patterns { + effect: PERMISSION_EFFECT_ALLOW + pattern: "build.use" + scope { kind: PERMISSION_SCOPE_KIND_GLOBAL } + source: PERMISSION_GRANT_SOURCE_ROLE + origin { + kind: PERMISSION_GRANT_ORIGIN_KIND_DIRECT_ROLE + role_key: "builder" + } +} +allow_patterns { + effect: PERMISSION_EFFECT_ALLOW + pattern: "staff.chat" + scope { kind: PERMISSION_SCOPE_KIND_GLOBAL } + source: PERMISSION_GRANT_SOURCE_ROLE + origin { + kind: PERMISSION_GRANT_ORIGIN_KIND_GROUP_MAPPING + role_key: "staff" + mapping_id: "00000000-0000-0000-0000-000000000322" + } +} +allow_patterns { + effect: PERMISSION_EFFECT_ALLOW + pattern: "home.use" + scope { kind: PERMISSION_SCOPE_KIND_GLOBAL } + source: PERMISSION_GRANT_SOURCE_ROLE + origin { + kind: PERMISSION_GRANT_ORIGIN_KIND_GROUP_MAPPING + role_key: "member" + mapping_id: "00000000-0000-0000-0000-000000000322" + inherited_path: "staff" + inherited_path: "member" + } +} +deny_patterns { + effect: PERMISSION_EFFECT_DENY + pattern: "operator.use" + scope { + kind: PERMISSION_SCOPE_KIND_SERVER + value: "survival-1" + } + source: PERMISSION_GRANT_SOURCE_PLAYER + expires_at { seconds: 1893459600 } + origin { kind: PERMISSION_GRANT_ORIGIN_KIND_DIRECT_PERMISSION } +} +role_keys: "default" +role_keys: "builder" +role_keys: "staff" +role_keys: "member" +role_metadata { + key: "default" + name: "Default" + prefix: "[D]" + color: "gray" + sort_order: 100 +} +role_metadata { + key: "builder" + name: "Builder" + prefix: "[B]" + color: "green" + sort_order: 75 +} +role_metadata { + key: "staff" + name: "Staff" + prefix: "[S]" + color: "red" + sort_order: 50 +} +role_metadata { + key: "member" + name: "Member" + sort_order: 25 +} diff --git a/minestom/build.gradle.kts b/minestom/build.gradle.kts index 23773f8..0eebf71 100644 --- a/minestom/build.gradle.kts +++ b/minestom/build.gradle.kts @@ -20,5 +20,7 @@ dependencies { implementation("org.slf4j:slf4j-api") testImplementation("org.junit.jupiter:junit-jupiter") + testImplementation(testFixtures(project(":common"))) + testImplementation("io.grpc:grpc-inprocess:1.82.0") testRuntimeOnly("org.junit.platform:junit-platform-launcher") } diff --git a/minestom/src/main/kotlin/gg/grounds/permissions/minestom/PermissionSnapshotClient.kt b/minestom/src/main/kotlin/gg/grounds/permissions/minestom/PermissionSnapshotClient.kt index d9f667c..11fa1db 100644 --- a/minestom/src/main/kotlin/gg/grounds/permissions/minestom/PermissionSnapshotClient.kt +++ b/minestom/src/main/kotlin/gg/grounds/permissions/minestom/PermissionSnapshotClient.kt @@ -91,6 +91,9 @@ class GrpcPermissionSnapshotClient private constructor(private val channel: Mana return GrpcPermissionSnapshotClient(channel) } + internal fun create(channel: ManagedChannel): GrpcPermissionSnapshotClient = + GrpcPermissionSnapshotClient(channel) + private const val DEFAULT_TIMEOUT_MS = 2000L } } diff --git a/minestom/src/test/kotlin/gg/grounds/permissions/minestom/PermissionSnapshotClientTest.kt b/minestom/src/test/kotlin/gg/grounds/permissions/minestom/PermissionSnapshotClientTest.kt index edcb21a..817720e 100644 --- a/minestom/src/test/kotlin/gg/grounds/permissions/minestom/PermissionSnapshotClientTest.kt +++ b/minestom/src/test/kotlin/gg/grounds/permissions/minestom/PermissionSnapshotClientTest.kt @@ -1,20 +1,13 @@ package gg.grounds.permissions.minestom -import com.google.protobuf.Timestamp import gg.grounds.grpc.permissions.GetPlayerSnapshotRequest -import gg.grounds.grpc.permissions.PermissionEffect.PERMISSION_EFFECT_ALLOW -import gg.grounds.grpc.permissions.PermissionGrant -import gg.grounds.grpc.permissions.PermissionGrantOrigin -import gg.grounds.grpc.permissions.PermissionGrantOriginKind.PERMISSION_GRANT_ORIGIN_KIND_GROUP_MAPPING -import gg.grounds.grpc.permissions.PermissionGrantSource.PERMISSION_GRANT_SOURCE_ROLE -import gg.grounds.grpc.permissions.PermissionScope -import gg.grounds.grpc.permissions.PermissionScopeKind.PERMISSION_SCOPE_KIND_GLOBAL import gg.grounds.grpc.permissions.PermissionSnapshotServiceGrpc import gg.grounds.grpc.permissions.PlayerPermissionSnapshot +import gg.grounds.permissions.ReleasedPermissionSnapshotFixture import io.grpc.Server -import io.grpc.netty.shaded.io.grpc.netty.NettyServerBuilder +import io.grpc.inprocess.InProcessChannelBuilder +import io.grpc.inprocess.InProcessServerBuilder import io.grpc.stub.StreamObserver -import java.util.UUID import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Assertions.assertInstanceOf @@ -29,11 +22,13 @@ class PermissionSnapshotClientTest { } @Test - fun `sends only player and server context and maps grant origin`() { - val playerId = UUID.randomUUID() + fun `consumes authoritative backend snapshot without caller groups`() { + val playerId = ReleasedPermissionSnapshotFixture.playerId var capturedRequest: GetPlayerSnapshotRequest? = null + val serverName = InProcessServerBuilder.generateName() server = - NettyServerBuilder.forPort(0) + InProcessServerBuilder.forName(serverName) + .directExecutor() .addService( object : PermissionSnapshotServiceGrpc.PermissionSnapshotServiceImplBase() { override fun getPlayerSnapshot( @@ -41,15 +36,16 @@ class PermissionSnapshotClientTest { responseObserver: StreamObserver, ) { capturedRequest = request - responseObserver.onNext(snapshotResponse(playerId)) + responseObserver.onNext(ReleasedPermissionSnapshotFixture.proto()) responseObserver.onCompleted() } } ) .build() .start() + val channel = InProcessChannelBuilder.forName(serverName).directExecutor().build() - GrpcPermissionSnapshotClient.create("localhost:${server!!.port}").use { client -> + GrpcPermissionSnapshotClient.create(channel).use { client -> val result = client.fetchSnapshot( playerId, @@ -63,40 +59,7 @@ class PermissionSnapshotClientTest { assertEquals(playerId.toString(), request.playerId) assertEquals("paper", request.serverType) assertEquals("lobby-1", request.serverId) - assertEquals( - gg.grounds.permissions.PermissionGrantOrigin( - kind = gg.grounds.permissions.PermissionGrantOriginKind.GROUP_MAPPING, - roleKey = "moderator", - mappingId = "mapping-1", - inheritedPath = listOf("member", "moderator"), - ), - success.snapshot.allowPatterns.single().origin, - ) + assertEquals(ReleasedPermissionSnapshotFixture.expected(), success.snapshot) } } - - private fun snapshotResponse(playerId: UUID): PlayerPermissionSnapshot { - val timestamp = Timestamp.newBuilder().setSeconds(1_700_000_000).build() - return PlayerPermissionSnapshot.newBuilder() - .setPlayerId(playerId.toString()) - .setPolicyVersion(42) - .setIssuedAt(timestamp) - .setRefreshAfter(timestamp) - .setExpiresAt(timestamp) - .addAllowPatterns( - PermissionGrant.newBuilder() - .setEffect(PERMISSION_EFFECT_ALLOW) - .setPattern("grounds.chat") - .setScope(PermissionScope.newBuilder().setKind(PERMISSION_SCOPE_KIND_GLOBAL)) - .setSource(PERMISSION_GRANT_SOURCE_ROLE) - .setOrigin( - PermissionGrantOrigin.newBuilder() - .setKind(PERMISSION_GRANT_ORIGIN_KIND_GROUP_MAPPING) - .setRoleKey("moderator") - .setMappingId("mapping-1") - .addAllInheritedPath(listOf("member", "moderator")) - ) - ) - .build() - } } diff --git a/velocity/build.gradle.kts b/velocity/build.gradle.kts index 3ffd14f..650e7c4 100644 --- a/velocity/build.gradle.kts +++ b/velocity/build.gradle.kts @@ -10,6 +10,8 @@ dependencies { implementation("tools.jackson.module:jackson-module-kotlin:3.1.0") testImplementation("org.junit.jupiter:junit-jupiter") + testImplementation(testFixtures(project(":common"))) + testImplementation("io.grpc:grpc-inprocess:1.82.0") testImplementation("org.slf4j:slf4j-api") testImplementation("com.velocitypowered:velocity-api") testRuntimeOnly("org.junit.platform:junit-platform-launcher") diff --git a/velocity/src/main/kotlin/gg/grounds/permissions/velocity/PermissionSnapshotClient.kt b/velocity/src/main/kotlin/gg/grounds/permissions/velocity/PermissionSnapshotClient.kt index dc5e920..3c4fe71 100644 --- a/velocity/src/main/kotlin/gg/grounds/permissions/velocity/PermissionSnapshotClient.kt +++ b/velocity/src/main/kotlin/gg/grounds/permissions/velocity/PermissionSnapshotClient.kt @@ -92,6 +92,9 @@ class GrpcPermissionSnapshotClient private constructor(private val channel: Mana return GrpcPermissionSnapshotClient(channel) } + internal fun create(channel: ManagedChannel): GrpcPermissionSnapshotClient = + GrpcPermissionSnapshotClient(channel) + private const val DEFAULT_TIMEOUT_MS = 2000L } } diff --git a/velocity/src/test/kotlin/gg/grounds/permissions/velocity/PermissionSnapshotClientTest.kt b/velocity/src/test/kotlin/gg/grounds/permissions/velocity/PermissionSnapshotClientTest.kt index 2f67c5c..bf9f755 100644 --- a/velocity/src/test/kotlin/gg/grounds/permissions/velocity/PermissionSnapshotClientTest.kt +++ b/velocity/src/test/kotlin/gg/grounds/permissions/velocity/PermissionSnapshotClientTest.kt @@ -1,20 +1,13 @@ package gg.grounds.permissions.velocity -import com.google.protobuf.Timestamp import gg.grounds.grpc.permissions.GetPlayerSnapshotRequest -import gg.grounds.grpc.permissions.PermissionEffect.PERMISSION_EFFECT_ALLOW -import gg.grounds.grpc.permissions.PermissionGrant -import gg.grounds.grpc.permissions.PermissionGrantOrigin -import gg.grounds.grpc.permissions.PermissionGrantOriginKind.PERMISSION_GRANT_ORIGIN_KIND_GROUP_MAPPING -import gg.grounds.grpc.permissions.PermissionGrantSource.PERMISSION_GRANT_SOURCE_ROLE -import gg.grounds.grpc.permissions.PermissionScope -import gg.grounds.grpc.permissions.PermissionScopeKind.PERMISSION_SCOPE_KIND_GLOBAL import gg.grounds.grpc.permissions.PermissionSnapshotServiceGrpc import gg.grounds.grpc.permissions.PlayerPermissionSnapshot +import gg.grounds.permissions.ReleasedPermissionSnapshotFixture import io.grpc.Server -import io.grpc.netty.shaded.io.grpc.netty.NettyServerBuilder +import io.grpc.inprocess.InProcessChannelBuilder +import io.grpc.inprocess.InProcessServerBuilder import io.grpc.stub.StreamObserver -import java.util.UUID import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Assertions.assertInstanceOf @@ -29,11 +22,13 @@ class PermissionSnapshotClientTest { } @Test - fun `sends only player and server context and maps grant origin`() { - val playerId = UUID.randomUUID() + fun `consumes authoritative backend snapshot without caller groups`() { + val playerId = ReleasedPermissionSnapshotFixture.playerId var capturedRequest: GetPlayerSnapshotRequest? = null + val serverName = InProcessServerBuilder.generateName() server = - NettyServerBuilder.forPort(0) + InProcessServerBuilder.forName(serverName) + .directExecutor() .addService( object : PermissionSnapshotServiceGrpc.PermissionSnapshotServiceImplBase() { override fun getPlayerSnapshot( @@ -41,15 +36,16 @@ class PermissionSnapshotClientTest { responseObserver: StreamObserver, ) { capturedRequest = request - responseObserver.onNext(snapshotResponse(playerId)) + responseObserver.onNext(ReleasedPermissionSnapshotFixture.proto()) responseObserver.onCompleted() } } ) .build() .start() + val channel = InProcessChannelBuilder.forName(serverName).directExecutor().build() - GrpcPermissionSnapshotClient.create("localhost:${server!!.port}").use { client -> + GrpcPermissionSnapshotClient.create(channel).use { client -> val result = client.fetchSnapshot( playerId, @@ -63,40 +59,7 @@ class PermissionSnapshotClientTest { assertEquals(playerId.toString(), request.playerId) assertEquals("velocity", request.serverType) assertEquals("proxy-1", request.serverId) - assertEquals( - gg.grounds.permissions.PermissionGrantOrigin( - kind = gg.grounds.permissions.PermissionGrantOriginKind.GROUP_MAPPING, - roleKey = "moderator", - mappingId = "mapping-1", - inheritedPath = listOf("member", "moderator"), - ), - success.snapshot.allowPatterns.single().origin, - ) + assertEquals(ReleasedPermissionSnapshotFixture.expected(), success.snapshot) } } - - private fun snapshotResponse(playerId: UUID): PlayerPermissionSnapshot { - val timestamp = Timestamp.newBuilder().setSeconds(1_700_000_000).build() - return PlayerPermissionSnapshot.newBuilder() - .setPlayerId(playerId.toString()) - .setPolicyVersion(42) - .setIssuedAt(timestamp) - .setRefreshAfter(timestamp) - .setExpiresAt(timestamp) - .addAllowPatterns( - PermissionGrant.newBuilder() - .setEffect(PERMISSION_EFFECT_ALLOW) - .setPattern("grounds.chat") - .setScope(PermissionScope.newBuilder().setKind(PERMISSION_SCOPE_KIND_GLOBAL)) - .setSource(PERMISSION_GRANT_SOURCE_ROLE) - .setOrigin( - PermissionGrantOrigin.newBuilder() - .setKind(PERMISSION_GRANT_ORIGIN_KIND_GROUP_MAPPING) - .setRoleKey("moderator") - .setMappingId("mapping-1") - .addAllInheritedPath(listOf("member", "moderator")) - ) - ) - .build() - } }