feat: 프로필별 플레이 감사 조회와 권한 검사 연결

This commit is contained in:
2026-09-16 02:52:52 +00:00
parent 35155e6b59
commit ed2597aef3
12 changed files with 718 additions and 1 deletions
+14
View File
@@ -10,6 +10,20 @@ export interface AdminCapabilityDefinition {
}
export const ADMIN_CAPABILITIES: readonly AdminCapabilityDefinition[] = [
{
permission: 'admin.playAudit.read',
label: '플레이 감사 조회',
description: '지정 profile의 장수·도시·재정과 플레이 이력을 조회합니다.',
risk: 'HIGH',
scope: 'PROFILE',
},
{
permission: 'admin.playAudit.accounts',
label: '플레이 감사 계정 조사',
description: '플레이 감사 권한을 가진 profile에서 계정 시도·접속지 연관 자료를 조사합니다.',
risk: 'HIGH',
scope: 'GLOBAL',
},
{
permission: 'admin.notice.manage',
label: 'Gateway 공지 관리',
@@ -1,3 +1,5 @@
import { z } from 'zod';
import { decryptGameSessionToken } from '@sammo-ts/common/auth/gameToken';
import fastify, { type FastifyRequest } from 'fastify';
import { fastifyTRPCPlugin } from '@trpc/server/adapters/fastify';
import { afterEach, describe, expect, it } from 'vitest';
@@ -248,6 +250,46 @@ describe('admin security over HTTP transport', () => {
expect(harness.flushes).toEqual([]);
});
it('grants scoped play audit roles, flushes old tokens and transports the exact scope', async () => {
const grant = 'admin.playAudit.read:che:default';
const harness = await createHarness(['admin.users.manage', grant, 'admin.playAudit.accounts']);
const granted = await postTrpc(
harness.baseUrl,
'admin.users.updateRoles',
{
userId: harness.target.id,
roles: [grant, 'admin.playAudit.accounts'],
mode: 'grant',
reason: '감사 조회 권한 부여',
},
harness.adminSessionToken
);
expect(granted.response.status).toBe(200);
expect(harness.flushes).toEqual([{ userId: harness.target.id, reason: 'admin-roles-updated' }]);
const issued = await postTrpc(harness.baseUrl, 'auth.issueGameSession', {
sessionToken: harness.targetSessionToken,
profile: 'che:default',
});
expect(issued.response.status).toBe(200);
const body = z.object({ result: z.object({ data: z.object({ gameToken: z.string() }) }) }).parse(issued.body);
const payload = decryptGameSessionToken(body.result.data.gameToken, 'transport-e2e-secret');
expect(payload?.profile).toBe('che:default');
expect(payload?.user.roles).toEqual(['user', grant, 'admin.playAudit.accounts']);
const rejected = await postTrpc(
harness.baseUrl,
'admin.users.updateRoles',
{
userId: harness.target.id,
roles: ['admin.playAudit.read:*'],
mode: 'grant',
reason: '범위 확대 거부 검증',
},
harness.adminSessionToken
);
expect(rejected.response.status).toBe(403);
expect(harness.flushes).toHaveLength(1);
});
it('accepts an equal scoped role and rejects wildcard escalation without mutating roles', async () => {
const harness = await createHarness();