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
Original file line number Diff line number Diff line change
Expand Up @@ -37,12 +37,27 @@ enum class PermissionGrantSource {
PLAYER,
}

enum class PermissionGrantOriginKind {
DEFAULT_ROLE,
DIRECT_ROLE,
GROUP_MAPPING,
DIRECT_PERMISSION,
}

data class PermissionGrantOrigin(
val kind: PermissionGrantOriginKind,
val roleKey: String? = null,
val mappingId: String? = null,
val inheritedPath: List<String> = emptyList(),
)

data class PermissionGrant(
val effect: PermissionEffect,
val pattern: String,
val scope: PermissionScope,
val source: PermissionGrantSource,
val expiresAt: Instant? = null,
val origin: PermissionGrantOrigin? = null,
)

data class RoleMetadata(
Expand Down
19 changes: 18 additions & 1 deletion common/src/main/proto/permissions.proto
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@ service PermissionCatalogService {

message GetPlayerSnapshotRequest {
string player_id = 1;
repeated string keycloak_groups = 2;
reserved 2;
reserved "keycloak_groups";
string server_type = 3;
string server_id = 4;
}
Expand Down Expand Up @@ -51,6 +52,14 @@ message PermissionGrant {
PermissionScope scope = 3;
PermissionGrantSource source = 4;
google.protobuf.Timestamp expires_at = 5;
PermissionGrantOrigin origin = 6;
}

message PermissionGrantOrigin {
PermissionGrantOriginKind kind = 1;
string role_key = 2;
string mapping_id = 3;
repeated string inherited_path = 4;
}

message PermissionScope {
Expand Down Expand Up @@ -104,3 +113,11 @@ enum PermissionGrantSource {
PERMISSION_GRANT_SOURCE_ROLE = 1;
PERMISSION_GRANT_SOURCE_PLAYER = 2;
}

enum PermissionGrantOriginKind {
PERMISSION_GRANT_ORIGIN_KIND_UNSPECIFIED = 0;
PERMISSION_GRANT_ORIGIN_KIND_DEFAULT_ROLE = 1;
PERMISSION_GRANT_ORIGIN_KIND_DIRECT_ROLE = 2;
PERMISSION_GRANT_ORIGIN_KIND_GROUP_MAPPING = 3;
PERMISSION_GRANT_ORIGIN_KIND_DIRECT_PERMISSION = 4;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package gg.grounds.permissions

import gg.grounds.grpc.permissions.GetPlayerSnapshotRequest
import gg.grounds.grpc.permissions.PermissionGrant
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertFalse
import org.junit.jupiter.api.Assertions.assertNotNull
import org.junit.jupiter.api.Test

class PermissionProtoContractTest {
@Test
fun `snapshot request reserves removed keycloak groups field`() {
val descriptor = GetPlayerSnapshotRequest.getDescriptor().toProto()

assertFalse(
GetPlayerSnapshotRequest.getDescriptor().fields.any { it.name == "keycloak_groups" }
)
assertEquals(listOf("keycloak_groups"), descriptor.reservedNameList)
assertEquals(listOf(2 to 3), descriptor.reservedRangeList.map { it.start to it.end })
assertEquals(
3,
GetPlayerSnapshotRequest.getDescriptor().findFieldByName("server_type").number,
)
assertEquals(
4,
GetPlayerSnapshotRequest.getDescriptor().findFieldByName("server_id").number,
)
}

@Test
fun `permission grants expose structured origin metadata`() {
val descriptor = PermissionGrant.getDescriptor()

assertNotNull(descriptor.findFieldByName("origin"))
assertEquals(6, descriptor.findFieldByName("origin").number)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import com.google.protobuf.Timestamp
import gg.grounds.grpc.permissions.GetPlayerSnapshotRequest
import gg.grounds.grpc.permissions.PermissionEffect as GrpcPermissionEffect
import gg.grounds.grpc.permissions.PermissionGrant as GrpcPermissionGrant
import gg.grounds.grpc.permissions.PermissionGrantOriginKind as GrpcPermissionGrantOriginKind
import gg.grounds.grpc.permissions.PermissionGrantSource as GrpcPermissionGrantSource
import gg.grounds.grpc.permissions.PermissionScope as GrpcPermissionScope
import gg.grounds.grpc.permissions.PermissionScopeKind as GrpcPermissionScopeKind
Expand All @@ -12,6 +13,8 @@ import gg.grounds.grpc.permissions.PlayerPermissionSnapshot
import gg.grounds.grpc.permissions.RoleMetadata as GrpcRoleMetadata
import gg.grounds.permissions.PermissionEffect
import gg.grounds.permissions.PermissionGrant
import gg.grounds.permissions.PermissionGrantOrigin
import gg.grounds.permissions.PermissionGrantOriginKind
import gg.grounds.permissions.PermissionGrantSource
import gg.grounds.permissions.PermissionScope
import gg.grounds.permissions.PermissionSnapshot
Expand Down Expand Up @@ -123,8 +126,33 @@ private fun GrpcPermissionGrant.toDomain(): PermissionGrant =
else -> PermissionGrantSource.ROLE
},
expiresAt = if (hasExpiresAt()) expiresAt.toInstant() else null,
origin = toDomainOrigin(),
)

private fun GrpcPermissionGrant.toDomainOrigin(): PermissionGrantOrigin? {
if (!hasOrigin()) return null

val kind =
when (origin.kind) {
GrpcPermissionGrantOriginKind.PERMISSION_GRANT_ORIGIN_KIND_DEFAULT_ROLE ->
PermissionGrantOriginKind.DEFAULT_ROLE
GrpcPermissionGrantOriginKind.PERMISSION_GRANT_ORIGIN_KIND_DIRECT_ROLE ->
PermissionGrantOriginKind.DIRECT_ROLE
GrpcPermissionGrantOriginKind.PERMISSION_GRANT_ORIGIN_KIND_GROUP_MAPPING ->
PermissionGrantOriginKind.GROUP_MAPPING
GrpcPermissionGrantOriginKind.PERMISSION_GRANT_ORIGIN_KIND_DIRECT_PERMISSION ->
PermissionGrantOriginKind.DIRECT_PERMISSION
else -> return null
}

return PermissionGrantOrigin(
kind = kind,
roleKey = origin.roleKey.takeIf { it.isNotEmpty() },
mappingId = origin.mappingId.takeIf { it.isNotEmpty() },
inheritedPath = origin.inheritedPathList.toList(),
)
}

private fun GrpcPermissionScope.toDomain(): PermissionScope =
when (kind) {
GrpcPermissionScopeKind.PERMISSION_SCOPE_KIND_SERVER -> PermissionScope.server(value)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
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 io.grpc.Server
import io.grpc.netty.shaded.io.grpc.netty.NettyServerBuilder
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
import org.junit.jupiter.api.Test

class PermissionSnapshotClientTest {
private var server: Server? = null

@AfterEach
fun stopServer() {
server?.shutdownNow()
}

@Test
fun `sends only player and server context and maps grant origin`() {
val playerId = UUID.randomUUID()
var capturedRequest: GetPlayerSnapshotRequest? = null
server =
NettyServerBuilder.forPort(0)
.addService(
object : PermissionSnapshotServiceGrpc.PermissionSnapshotServiceImplBase() {
override fun getPlayerSnapshot(
request: GetPlayerSnapshotRequest,
responseObserver: StreamObserver<PlayerPermissionSnapshot>,
) {
capturedRequest = request
responseObserver.onNext(snapshotResponse(playerId))
responseObserver.onCompleted()
}
}
)
.build()
.start()

GrpcPermissionSnapshotClient.create("localhost:${server!!.port}").use { client ->
val result =
client.fetchSnapshot(
playerId,
PermissionSnapshotContext(serverType = "paper", serverId = "lobby-1"),
)

val success =
assertInstanceOf(PermissionSnapshotFetchResult.Success::class.java, result)
val request = requireNotNull(capturedRequest)
assertEquals(setOf(1, 3, 4), request.allFields.keys.map { it.number }.toSet())
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,
)
}
}

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()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import com.google.protobuf.Timestamp
import gg.grounds.grpc.permissions.GetPlayerSnapshotRequest
import gg.grounds.grpc.permissions.PermissionEffect as GrpcPermissionEffect
import gg.grounds.grpc.permissions.PermissionGrant as GrpcPermissionGrant
import gg.grounds.grpc.permissions.PermissionGrantOriginKind as GrpcPermissionGrantOriginKind
import gg.grounds.grpc.permissions.PermissionGrantSource as GrpcPermissionGrantSource
import gg.grounds.grpc.permissions.PermissionScope as GrpcPermissionScope
import gg.grounds.grpc.permissions.PermissionScopeKind as GrpcPermissionScopeKind
Expand All @@ -12,6 +13,8 @@ import gg.grounds.grpc.permissions.PlayerPermissionSnapshot
import gg.grounds.grpc.permissions.RoleMetadata as GrpcRoleMetadata
import gg.grounds.permissions.PermissionEffect
import gg.grounds.permissions.PermissionGrant
import gg.grounds.permissions.PermissionGrantOrigin
import gg.grounds.permissions.PermissionGrantOriginKind
import gg.grounds.permissions.PermissionGrantSource
import gg.grounds.permissions.PermissionScope
import gg.grounds.permissions.PermissionSnapshot
Expand Down Expand Up @@ -124,8 +127,33 @@ private fun GrpcPermissionGrant.toDomain(): PermissionGrant =
else -> PermissionGrantSource.ROLE
},
expiresAt = if (hasExpiresAt()) expiresAt.toInstant() else null,
origin = toDomainOrigin(),
)

private fun GrpcPermissionGrant.toDomainOrigin(): PermissionGrantOrigin? {
if (!hasOrigin()) return null

val kind =
when (origin.kind) {
GrpcPermissionGrantOriginKind.PERMISSION_GRANT_ORIGIN_KIND_DEFAULT_ROLE ->
PermissionGrantOriginKind.DEFAULT_ROLE
GrpcPermissionGrantOriginKind.PERMISSION_GRANT_ORIGIN_KIND_DIRECT_ROLE ->
PermissionGrantOriginKind.DIRECT_ROLE
GrpcPermissionGrantOriginKind.PERMISSION_GRANT_ORIGIN_KIND_GROUP_MAPPING ->
PermissionGrantOriginKind.GROUP_MAPPING
GrpcPermissionGrantOriginKind.PERMISSION_GRANT_ORIGIN_KIND_DIRECT_PERMISSION ->
PermissionGrantOriginKind.DIRECT_PERMISSION
else -> return null
}

return PermissionGrantOrigin(
kind = kind,
roleKey = origin.roleKey.takeIf { it.isNotEmpty() },
mappingId = origin.mappingId.takeIf { it.isNotEmpty() },
inheritedPath = origin.inheritedPathList.toList(),
)
}

private fun GrpcPermissionScope.toDomain(): PermissionScope =
when (kind) {
GrpcPermissionScopeKind.PERMISSION_SCOPE_KIND_SERVER -> PermissionScope.server(value)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package gg.grounds.permissions.velocity

import gg.grounds.permissions.PermissionEffect
import gg.grounds.permissions.PermissionGrant
import gg.grounds.permissions.PermissionGrantOrigin
import gg.grounds.permissions.PermissionGrantSource
import gg.grounds.permissions.PermissionScope
import gg.grounds.permissions.PermissionScopeKind
Expand Down Expand Up @@ -112,6 +113,7 @@ private data class CachedPermissionGrant(
val scope: CachedPermissionScope,
val source: PermissionGrantSource,
val expiresAt: String? = null,
val origin: PermissionGrantOrigin? = null,
) {
fun toDomain(): PermissionGrant =
PermissionGrant(
Expand All @@ -120,6 +122,7 @@ private data class CachedPermissionGrant(
scope = scope.toDomain(),
source = source,
expiresAt = expiresAt?.let(Instant::parse),
origin = origin,
)
}

Expand Down Expand Up @@ -158,6 +161,7 @@ private fun PermissionGrant.toCache(): CachedPermissionGrant =
scope = scope.toCache(),
source = source,
expiresAt = expiresAt?.toString(),
origin = origin,
)

private fun PermissionScope.toCache(): CachedPermissionScope = CachedPermissionScope(kind, value)
Expand Down
Loading