Skip to content
Open
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 build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,13 @@ dependencies {
implementation(enforcedPlatform("io.quarkus.platform:quarkus-bom:3.30.8"))
implementation("io.quarkus:quarkus-arc")
implementation("io.quarkus:quarkus-grpc")
implementation("io.quarkus:quarkus-scheduler")
implementation("io.quarkus:quarkus-jdbc-postgresql")
implementation("io.quarkus:quarkus-flyway")
implementation("io.quarkus:quarkus-kotlin")
implementation("io.quarkus:quarkus-scheduler")
implementation("gg.grounds:library-grpc-contracts-player:0.2.0")
implementation("gg.grounds:library-grpc-contracts-permission:feat-perm-protos-SNAPSHOT")

compileOnly("com.google.protobuf:protobuf-kotlin")

Expand Down
5 changes: 3 additions & 2 deletions devspace.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ deployments:
values:
deployment:
image:
repository: service-player
repository: ghcr.io/groundsgg/service-player
tag: edge
containerPort: 9000
env:
- name: POSTGRES_URL
Expand All @@ -25,7 +26,7 @@ deployments:
dev:
app:
# Search for the container that runs this image
imageSelector: service-player
imageSelector: ghcr.io/groundsgg/service-player
# Replace the container image with this image
devImage: gradle:9.2.1-jdk25
# Sync files between the local filesystem and the development container
Expand Down
16 changes: 16 additions & 0 deletions src/main/kotlin/gg/grounds/api/permissions/ApplyResultMapper.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package gg.grounds.api.permissions

import gg.grounds.domain.permissions.ApplyOutcome
import gg.grounds.grpc.permissions.ApplyResult

object ApplyResultMapper {
fun toProto(outcome: ApplyOutcome): ApplyResult {
return when (outcome) {
ApplyOutcome.NO_CHANGE -> ApplyResult.NO_CHANGE
ApplyOutcome.CREATED -> ApplyResult.CREATED
ApplyOutcome.UPDATED -> ApplyResult.UPDATED
ApplyOutcome.DELETED -> ApplyResult.DELETED
ApplyOutcome.ERROR -> ApplyResult.APPLY_RESULT_UNSPECIFIED
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
package gg.grounds.api.permissions

import gg.grounds.api.permissions.admin.GroupAdminHandler
import gg.grounds.api.permissions.admin.PlayerGroupsAdminHandler
import gg.grounds.api.permissions.admin.PlayerPermissionsAdminHandler
import gg.grounds.grpc.permissions.AddGroupPermissionsReply
import gg.grounds.grpc.permissions.AddGroupPermissionsRequest
import gg.grounds.grpc.permissions.AddPlayerGroupsReply
import gg.grounds.grpc.permissions.AddPlayerGroupsRequest
import gg.grounds.grpc.permissions.AddPlayerPermissionsReply
import gg.grounds.grpc.permissions.AddPlayerPermissionsRequest
import gg.grounds.grpc.permissions.CreateGroupReply
import gg.grounds.grpc.permissions.CreateGroupRequest
import gg.grounds.grpc.permissions.DeleteGroupReply
import gg.grounds.grpc.permissions.DeleteGroupRequest
import gg.grounds.grpc.permissions.GetGroupReply
import gg.grounds.grpc.permissions.GetGroupRequest
import gg.grounds.grpc.permissions.ListGroupsReply
import gg.grounds.grpc.permissions.ListGroupsRequest
import gg.grounds.grpc.permissions.PermissionsAdminService
import gg.grounds.grpc.permissions.RemoveGroupPermissionsReply
import gg.grounds.grpc.permissions.RemoveGroupPermissionsRequest
import gg.grounds.grpc.permissions.RemovePlayerGroupsReply
import gg.grounds.grpc.permissions.RemovePlayerGroupsRequest
import gg.grounds.grpc.permissions.RemovePlayerPermissionsReply
import gg.grounds.grpc.permissions.RemovePlayerPermissionsRequest
import io.quarkus.grpc.GrpcService
import io.smallrye.common.annotation.Blocking
import io.smallrye.mutiny.Uni
import jakarta.inject.Inject

@GrpcService
@Blocking
class PermissionsAdminGrpcService
@Inject
constructor(
private val groupAdminHandler: GroupAdminHandler,
private val playerPermissionsAdminHandler: PlayerPermissionsAdminHandler,
private val playerGroupsAdminHandler: PlayerGroupsAdminHandler,
) : PermissionsAdminService {
override fun createGroup(request: CreateGroupRequest): Uni<CreateGroupReply> {
return Uni.createFrom().item { groupAdminHandler.createGroup(request) }
}

override fun deleteGroup(request: DeleteGroupRequest): Uni<DeleteGroupReply> {
return Uni.createFrom().item { groupAdminHandler.deleteGroup(request) }
}

override fun getGroup(request: GetGroupRequest): Uni<GetGroupReply> {
return Uni.createFrom().item { groupAdminHandler.getGroup(request) }
}

override fun listGroups(request: ListGroupsRequest): Uni<ListGroupsReply> {
return Uni.createFrom().item { groupAdminHandler.listGroups(request) }
}

override fun addGroupPermissions(
request: AddGroupPermissionsRequest
): Uni<AddGroupPermissionsReply> {
return Uni.createFrom().item { groupAdminHandler.addGroupPermissions(request) }
}

override fun removeGroupPermissions(
request: RemoveGroupPermissionsRequest
): Uni<RemoveGroupPermissionsReply> {
return Uni.createFrom().item { groupAdminHandler.removeGroupPermissions(request) }
}

override fun addPlayerPermissions(
request: AddPlayerPermissionsRequest
): Uni<AddPlayerPermissionsReply> {
return Uni.createFrom().item { playerPermissionsAdminHandler.addPlayerPermissions(request) }
}

override fun removePlayerPermissions(
request: RemovePlayerPermissionsRequest
): Uni<RemovePlayerPermissionsReply> {
return Uni.createFrom().item {
playerPermissionsAdminHandler.removePlayerPermissions(request)
}
}

override fun addPlayerGroups(request: AddPlayerGroupsRequest): Uni<AddPlayerGroupsReply> {
return Uni.createFrom().item { playerGroupsAdminHandler.addPlayerGroups(request) }
}

override fun removePlayerGroups(
request: RemovePlayerGroupsRequest
): Uni<RemovePlayerGroupsReply> {
return Uni.createFrom().item { playerGroupsAdminHandler.removePlayerGroups(request) }
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
package gg.grounds.api.permissions

import gg.grounds.grpc.permissions.CheckPlayerPermissionReply
import gg.grounds.grpc.permissions.CheckPlayerPermissionRequest
import gg.grounds.grpc.permissions.GetPlayerPermissionsReply
import gg.grounds.grpc.permissions.GetPlayerPermissionsRequest
import gg.grounds.grpc.permissions.PermissionsService
import gg.grounds.persistence.permissions.PermissionsQueryRepository
import io.grpc.Status
import io.quarkus.grpc.GrpcService
import io.smallrye.common.annotation.Blocking
import io.smallrye.mutiny.Uni
import jakarta.inject.Inject
import org.jboss.logging.Logger

@GrpcService
@Blocking
class PermissionsPluginGrpcService
@Inject
constructor(private val queryRepository: PermissionsQueryRepository) : PermissionsService {
override fun getPlayerPermissions(
request: GetPlayerPermissionsRequest
): Uni<GetPlayerPermissionsReply> {
return Uni.createFrom().item { handleGetPlayerPermissions(request) }
}

override fun checkPlayerPermission(
request: CheckPlayerPermissionRequest
): Uni<CheckPlayerPermissionReply> {
return Uni.createFrom().item { handleCheckPlayerPermission(request) }
}

private fun handleGetPlayerPermissions(
request: GetPlayerPermissionsRequest
): GetPlayerPermissionsReply {
val rawPlayerId = request.playerId?.trim().orEmpty()
val playerId =
PermissionsRequestParser.parsePlayerId(rawPlayerId)
?: run {
LOG.warnf(
"Get player permissions request rejected (playerId=%s, reason=invalid_player_id)",
rawPlayerId,
)
throw Status.INVALID_ARGUMENT.withDescription("player_id must be a UUID")
.asRuntimeException()
}

val data =
queryRepository.getPlayerPermissions(
playerId,
request.includeEffectivePermissions,
request.includeDirectPermissions,
request.includeGroups,
)
?: throw Status.INTERNAL.withDescription("Failed to load player permissions")
.asRuntimeException()

LOG.debugf(
"Fetch player permissions completed (playerId=%s, includeEffectivePermissions=%s, includeDirectPermissions=%s, includeGroups=%s)",
playerId,
request.includeEffectivePermissions,
request.includeDirectPermissions,
request.includeGroups,
)
return GetPlayerPermissionsReply.newBuilder()
.setPlayer(PermissionsProtoMapper.toProto(data))
.build()
}

private fun handleCheckPlayerPermission(
request: CheckPlayerPermissionRequest
): CheckPlayerPermissionReply {
val rawPlayerId = request.playerId?.trim().orEmpty()
val playerId =
PermissionsRequestParser.parsePlayerId(rawPlayerId)
?: run {
LOG.warnf(
"Check player permission request rejected (playerId=%s, reason=invalid_player_id)",
rawPlayerId,
)
throw Status.INVALID_ARGUMENT.withDescription("player_id must be a UUID")
.asRuntimeException()
}
val permission = request.permission?.trim().orEmpty()
if (permission.isEmpty()) {
LOG.warnf(
"Check player permission request rejected (playerId=%s, reason=empty_permission)",
playerId,
)
throw Status.INVALID_ARGUMENT.withDescription("permission must be provided")
.asRuntimeException()
}

val allowed =
queryRepository.checkPlayerPermission(playerId, permission)
?: throw Status.INTERNAL.withDescription("Failed to check player permission")
.asRuntimeException()

LOG.debugf(
"Check player permission completed (playerId=%s, permission=%s, allowed=%s)",
playerId,
permission,
allowed,
)
return CheckPlayerPermissionReply.newBuilder().setAllowed(allowed).build()
}

companion object {
private val LOG = Logger.getLogger(PermissionsPluginGrpcService::class.java)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package gg.grounds.api.permissions

import com.google.protobuf.Timestamp
import gg.grounds.domain.permissions.GroupPermissionGrant
import gg.grounds.domain.permissions.PermissionGroup
import gg.grounds.domain.permissions.PlayerGroupMembership
import gg.grounds.domain.permissions.PlayerPermissionGrant
import gg.grounds.domain.permissions.PlayerPermissionsData
import gg.grounds.grpc.permissions.Group
import gg.grounds.grpc.permissions.PermissionGrant as PermissionGrantProto
import gg.grounds.grpc.permissions.PlayerGroupMembership as PlayerGroupMembershipProto
import gg.grounds.grpc.permissions.PlayerPermissions

object PermissionsProtoMapper {
fun toProto(data: PlayerPermissionsData): PlayerPermissions {
return PlayerPermissions.newBuilder()
.setPlayerId(data.playerId.toString())
.addAllGroupMemberships(data.groupMemberships.map { toProto(it) })
.addAllDirectPermissionGrants(data.directPermissionGrants.map { toProto(it) })
.addAllEffectivePermissions(data.effectivePermissions)
.build()
}

fun toProto(group: PermissionGroup): Group {
return Group.newBuilder()
.setGroupName(group.name)
.addAllPermissionGrants(group.permissionGrants.map { toProto(it) })
.build()
}

private fun toProto(membership: PlayerGroupMembership): PlayerGroupMembershipProto {
val builder = PlayerGroupMembershipProto.newBuilder().setGroupName(membership.groupName)
membership.expiresAt?.let { builder.setExpiresAt(toTimestamp(it)) }
return builder.build()
}

private fun toProto(grant: PlayerPermissionGrant): PermissionGrantProto {
val builder = PermissionGrantProto.newBuilder().setPermission(grant.permission)
grant.expiresAt?.let { builder.setExpiresAt(toTimestamp(it)) }
return builder.build()
}

private fun toProto(grant: GroupPermissionGrant): PermissionGrantProto {
val builder = PermissionGrantProto.newBuilder().setPermission(grant.permission)
grant.expiresAt?.let { builder.setExpiresAt(toTimestamp(it)) }
return builder.build()
}

private fun toTimestamp(instant: java.time.Instant): Timestamp {
return Timestamp.newBuilder().setSeconds(instant.epochSecond).setNanos(instant.nano).build()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
package gg.grounds.api.permissions

import com.google.protobuf.Timestamp
import gg.grounds.domain.permissions.GroupPermissionGrant
import gg.grounds.domain.permissions.PlayerGroupMembership
import gg.grounds.domain.permissions.PlayerPermissionGrant
import gg.grounds.grpc.permissions.PermissionGrant as PermissionGrantProto
import gg.grounds.grpc.permissions.PlayerGroupMembership as PlayerGroupMembershipProto
import java.time.Instant
import java.util.UUID

object PermissionsRequestParser {
fun parsePlayerId(value: String?): UUID? {
return value
?.trim()
?.takeIf { it.isNotEmpty() }
?.let { runCatching { UUID.fromString(it) }.getOrNull() }
}

fun sanitize(values: List<String>): Set<String> {
return values.mapNotNull { it.trim().takeIf { name -> name.isNotEmpty() } }.toSet()
}

fun sanitizeGroupMemberships(
values: List<PlayerGroupMembershipProto>
): Set<PlayerGroupMembership> {
val memberships = linkedMapOf<String, PlayerGroupMembership>()
values.forEach { membership ->
val groupName = membership.groupName?.trim().orEmpty()
if (groupName.isEmpty()) {
return@forEach
}
val expiresAt = if (membership.hasExpiresAt()) toInstant(membership.expiresAt) else null
memberships[groupName] = PlayerGroupMembership(groupName, expiresAt)
}
return memberships.values.toSet()
}

fun sanitizePermissionGrants(values: List<PermissionGrantProto>): Set<PlayerPermissionGrant> {
return parsePermissionGrants(values)
.map { (permission, expiresAt) -> PlayerPermissionGrant(permission, expiresAt) }
.toSet()
}

fun sanitizeGroupPermissionGrants(
values: List<PermissionGrantProto>
): Set<GroupPermissionGrant> {
return parsePermissionGrants(values)
.map { (permission, expiresAt) -> GroupPermissionGrant(permission, expiresAt) }
.toSet()
}

private fun parsePermissionGrants(values: List<PermissionGrantProto>): Map<String, Instant?> {
val grants = linkedMapOf<String, Instant?>()
values.forEach { grant ->
val permission = grant.permission?.trim().orEmpty()
if (permission.isEmpty()) {
return@forEach
}
val expiresAt = if (grant.hasExpiresAt()) toInstant(grant.expiresAt) else null
grants[permission] = expiresAt
}
return grants
}

private fun toInstant(timestamp: Timestamp): Instant {
return Instant.ofEpochSecond(timestamp.seconds, timestamp.nanos.toLong())
}

fun hasPastExpiry(expiresAt: Instant?, now: Instant): Boolean {
return expiresAt != null && !expiresAt.isAfter(now)
}
}
Loading