Files
fluxer/fluxer_api/src/api/admin/tests/AdminEndpointsAuthorization.test.ts
T

157 lines
7.5 KiB
TypeScript

// SPDX-License-Identifier: AGPL-3.0-or-later
import {createTestAccount, setUserACLs} from '@app/api/auth/tests/AuthTestUtils';
import {type ApiTestHarness, createApiTestHarness} from '@app/api/test/ApiTestHarness';
import {HTTP_STATUS} from '@app/api/test/TestConstants';
import {createBuilder, createBuilderWithoutAuth, type TestRequestBuilder} from '@app/api/test/TestRequestBuilder';
import {beforeEach, describe, test} from 'vitest';
function withMethod(builder: TestRequestBuilder, method: string, path: string, body: unknown = {}): TestRequestBuilder {
switch (method) {
case 'POST':
return builder.post(path).body(body);
case 'PATCH':
return builder.patch(path).body(body);
case 'PUT':
return builder.put(path).body(body);
case 'DELETE':
return builder.delete(path);
default:
return builder.get(path);
}
}
interface AdminEndpointCase {
method: string;
path: string;
requiredACL: string;
body?: unknown;
}
const adminEndpoints: Array<AdminEndpointCase> = [
{method: 'GET', path: '/admin/reports', requiredACL: 'report:view'},
{method: 'GET', path: '/admin/reports/1', requiredACL: 'report:view'},
{method: 'PATCH', path: '/admin/reports/1', requiredACL: 'report:resolve'},
{
method: 'POST',
path: '/admin/bulk-jobs',
requiredACL: 'bulk:update:user_flags',
body: {task: 'update_user_flags', user_ids: ['1']},
},
{
method: 'POST',
path: '/admin/bulk-jobs',
requiredACL: 'bulk:update:guild_features',
body: {task: 'update_guild_features', guild_ids: ['1']},
},
{
method: 'POST',
path: '/admin/bulk-jobs',
requiredACL: 'bulk:add:guild_members',
body: {task: 'add_guild_members', guild_id: '1', user_ids: ['2']},
},
{method: 'GET', path: '/admin/guilds', requiredACL: 'guild:lookup'},
{method: 'GET', path: '/admin/users', requiredACL: 'user:lookup'},
{method: 'GET', path: '/admin/messages?channel_id=1', requiredACL: 'message:lookup'},
{method: 'DELETE', path: '/admin/channels/1/messages/1', requiredACL: 'message:delete'},
{method: 'GET', path: '/admin/gateway/memory-stats', requiredACL: 'gateway:manage'},
{method: 'POST', path: '/admin/gateway/reloads', requiredACL: 'gateway:manage'},
{method: 'GET', path: '/admin/gateway/stats', requiredACL: 'gateway:manage'},
{method: 'GET', path: '/admin/gateway/voice-state-counts', requiredACL: 'gateway:manage'},
{method: 'GET', path: '/admin/audit-logs', requiredACL: 'audit_log:view'},
{method: 'GET', path: '/admin/audit-logs?q=example', requiredACL: 'audit_log:view'},
{method: 'GET', path: '/admin/guilds/1', requiredACL: 'guild:lookup'},
{method: 'GET', path: '/admin/guilds/1/members', requiredACL: 'guild:list:members'},
{method: 'PATCH', path: '/admin/guilds/1', requiredACL: 'guild:update'},
{method: 'DELETE', path: '/admin/guilds/1', requiredACL: 'guild:delete'},
{method: 'PUT', path: '/admin/guilds/1/members/2', requiredACL: 'guild:force_add_member'},
{method: 'DELETE', path: '/admin/guilds/1/members/2', requiredACL: 'guild:kick_member'},
{method: 'PUT', path: '/admin/guilds/1/bans/2', requiredACL: 'guild:ban_member'},
{method: 'GET', path: '/admin/guilds/1/audit-logs', requiredACL: 'guild:audit_log:view'},
{method: 'DELETE', path: '/admin/guilds/1/assets', requiredACL: 'asset:purge'},
{method: 'POST', path: '/admin/guilds/1/reloads', requiredACL: 'guild:reload'},
{method: 'POST', path: '/admin/guilds/1/shutdowns', requiredACL: 'guild:shutdown'},
{method: 'GET', path: '/admin/users/1', requiredACL: 'user:lookup'},
{method: 'GET', path: '/admin/users/1/guilds', requiredACL: 'user:list:guilds'},
{method: 'GET', path: '/admin/users/1/dm-channels', requiredACL: 'user:list:dm_channels'},
{method: 'DELETE', path: '/admin/users/1/mfa', requiredACL: 'user:update:mfa'},
{method: 'GET', path: '/admin/users/1/webauthn-credentials', requiredACL: 'user:update:mfa'},
{method: 'DELETE', path: '/admin/users/1/webauthn-credentials/credential', requiredACL: 'user:update:mfa'},
{method: 'DELETE', path: '/admin/users/1/profile-fields', requiredACL: 'user:update:profile'},
{method: 'PUT', path: '/admin/users/1/acls', requiredACL: 'acl:set:user'},
{method: 'PUT', path: '/admin/users/1/deletion', requiredACL: 'user:delete'},
{method: 'POST', path: '/admin/users/1/avatar-block', requiredACL: 'ban:avatar_hash:add'},
{method: 'GET', path: '/admin/guilds/1/emojis', requiredACL: 'asset:purge'},
{method: 'GET', path: '/admin/guilds/1/stickers', requiredACL: 'asset:purge'},
{method: 'GET', path: '/admin/blocklists', requiredACL: 'ban:ip:check'},
{method: 'GET', path: '/admin/blocklists/ip/entries', requiredACL: 'ban:ip:check'},
{method: 'POST', path: '/admin/blocklists/ip/entries', requiredACL: 'ban:ip:add', body: {ip: '198.51.100.9'}},
{method: 'GET', path: '/admin/discovery/applications', requiredACL: 'discovery:review'},
{method: 'GET', path: '/admin/discovery/listings', requiredACL: 'discovery:review'},
{method: 'PATCH', path: '/admin/discovery/listings/1', requiredACL: 'discovery:review'},
{method: 'DELETE', path: '/admin/discovery/listings/1', requiredACL: 'discovery:remove'},
{method: 'GET', path: '/admin/search/index-refreshes/1', requiredACL: 'guild:lookup'},
{
method: 'POST',
path: '/admin/search/indexes/users/refreshes',
requiredACL: 'guild:lookup',
body: {},
},
];
describe('Admin Endpoints Authorization', () => {
let harness: ApiTestHarness;
beforeEach(async () => {
harness = await createApiTestHarness({search: 'enabled'});
});
test('admin endpoints require authentication', async () => {
for (const endpoint of adminEndpoints.slice(0, 5)) {
await withMethod(createBuilderWithoutAuth(harness), endpoint.method, endpoint.path, endpoint.body)
.expect(HTTP_STATUS.UNAUTHORIZED)
.execute();
}
});
test('admin endpoints require proper ACLs', async () => {
const admin = await createTestAccount(harness);
await setUserACLs(harness, admin, ['admin:authenticate']);
for (const endpoint of adminEndpoints.slice(0, 10)) {
await withMethod(createBuilder(harness, `${admin.token}`), endpoint.method, endpoint.path, endpoint.body)
.expect(HTTP_STATUS.FORBIDDEN)
.execute();
}
});
test('admin endpoints succeed with proper ACLs', async () => {
const admin = await createTestAccount(harness);
await setUserACLs(harness, admin, ['admin:authenticate', 'user:lookup', 'guild:lookup']);
const endpointsToTest = ['/admin/users/123'];
for (const path of endpointsToTest) {
await createBuilder(harness, `${admin.token}`).get(path).expect(HTTP_STATUS.OK).execute();
}
await createBuilder(harness, `${admin.token}`).get('/admin/guilds/123').expect(HTTP_STATUS.OK).execute();
});
test('user lookup endpoint requires user:lookup ACL', async () => {
const admin = await createTestAccount(harness);
await setUserACLs(harness, admin, ['admin:authenticate', 'admin_api_key:manage', 'audit_log:view']);
await createBuilder(harness, `${admin.token}`)
.get(`/admin/users/${admin.userId}`)
.expect(HTTP_STATUS.FORBIDDEN)
.execute();
});
test('guild lookup endpoint requires guild:lookup ACL', async () => {
const admin = await createTestAccount(harness);
await setUserACLs(harness, admin, ['admin:authenticate', 'admin_api_key:manage', 'audit_log:view']);
await createBuilder(harness, `${admin.token}`)
.get('/admin/guilds/123456789')
.expect(HTTP_STATUS.FORBIDDEN)
.execute();
});
test('audit logs endpoint requires audit_log:view ACL', async () => {
const admin = await createTestAccount(harness);
await setUserACLs(harness, admin, ['admin:authenticate', 'admin_api_key:manage', 'user:lookup']);
await createBuilder(harness, `${admin.token}`)
.get('/admin/audit-logs?limit=10')
.expect(HTTP_STATUS.FORBIDDEN)
.execute();
});
});