@babylonjs-toolkit/server 0.0.0-stage → 9.35.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CUSTOM.md +1690 -0
- package/DEPLOY.md +2012 -0
- package/README.md +1081 -2
- package/lib/AbuseLimits.d.ts +64 -0
- package/lib/AbuseLimits.js +121 -0
- package/lib/Authentication.d.ts +86 -0
- package/lib/Authentication.js +272 -0
- package/lib/BootSessionOwner.d.ts +86 -0
- package/lib/BootSessionOwner.js +116 -0
- package/lib/ColyseusLoggerAdapter.d.ts +32 -0
- package/lib/ColyseusLoggerAdapter.js +73 -0
- package/lib/DedicatedSceneRegistry.d.ts +52 -0
- package/lib/DedicatedSceneRegistry.js +74 -0
- package/lib/DedicatedSession.d.ts +152 -0
- package/lib/DedicatedSession.js +173 -0
- package/lib/DualTransport.d.ts +112 -0
- package/lib/DualTransport.js +197 -0
- package/lib/GameServer.d.ts +64 -0
- package/lib/GameServer.js +647 -0
- package/lib/GameServerConfig.d.ts +387 -0
- package/lib/GameServerConfig.js +1059 -0
- package/lib/GameServerLobbyRoom.d.ts +33 -0
- package/lib/GameServerLobbyRoom.js +56 -0
- package/lib/GameServerLogger.d.ts +111 -0
- package/lib/GameServerLogger.js +190 -0
- package/lib/GameServerMetrics.d.ts +158 -0
- package/lib/GameServerMetrics.js +276 -0
- package/lib/GameServerState.d.ts +164 -0
- package/lib/GameServerState.js +26 -0
- package/lib/HeadlessRuntime.d.ts +316 -0
- package/lib/HeadlessRuntime.js +380 -0
- package/lib/JoinGate.d.ts +34 -0
- package/lib/JoinGate.js +221 -0
- package/lib/NetworkAddresses.d.ts +33 -0
- package/lib/NetworkAddresses.js +90 -0
- package/lib/PlayerTokens.d.ts +17 -0
- package/lib/PlayerTokens.js +24 -0
- package/lib/RoomServerTransport.d.ts +73 -0
- package/lib/RoomServerTransport.js +90 -0
- package/lib/ServerEndpoints.d.ts +133 -0
- package/lib/ServerEndpoints.js +247 -0
- package/lib/ServerInit.d.ts +28 -0
- package/lib/ServerInit.js +113 -0
- package/lib/SessionRoom.d.ts +391 -0
- package/lib/SessionRoom.js +930 -0
- package/lib/TlsCertificates.d.ts +35 -0
- package/lib/TlsCertificates.js +130 -0
- package/lib/admin/AccountRoutes.d.ts +8 -0
- package/lib/admin/AccountRoutes.js +502 -0
- package/lib/admin/AdminAccounts.d.ts +329 -0
- package/lib/admin/AdminAccounts.js +607 -0
- package/lib/admin/AdminApiRouter.d.ts +202 -0
- package/lib/admin/AdminApiRouter.js +465 -0
- package/lib/admin/AdminApiTypes.d.ts +1782 -0
- package/lib/admin/AdminApiTypes.js +4 -0
- package/lib/admin/AdminAudit.d.ts +113 -0
- package/lib/admin/AdminAudit.js +266 -0
- package/lib/admin/AdminCli.d.ts +7 -0
- package/lib/admin/AdminCli.js +351 -0
- package/lib/admin/AdminClock.d.ts +51 -0
- package/lib/admin/AdminClock.js +10 -0
- package/lib/admin/AdminConsole.d.ts +39 -0
- package/lib/admin/AdminConsole.js +325 -0
- package/lib/admin/AdminConstants.d.ts +147 -0
- package/lib/admin/AdminConstants.js +167 -0
- package/lib/admin/AdminCrypto.d.ts +19 -0
- package/lib/admin/AdminCrypto.js +29 -0
- package/lib/admin/AdminExtensionsHost.d.ts +176 -0
- package/lib/admin/AdminExtensionsHost.js +307 -0
- package/lib/admin/AdminPasswords.d.ts +33 -0
- package/lib/admin/AdminPasswords.js +92 -0
- package/lib/admin/AdminPermissions.d.ts +28 -0
- package/lib/admin/AdminPermissions.js +74 -0
- package/lib/admin/AdminRecords.d.ts +196 -0
- package/lib/admin/AdminRecords.js +3 -0
- package/lib/admin/AdminRequestBodies.d.ts +47 -0
- package/lib/admin/AdminRequestBodies.js +84 -0
- package/lib/admin/AdminRequestGate.d.ts +38 -0
- package/lib/admin/AdminRequestGate.js +59 -0
- package/lib/admin/AdminResponses.d.ts +103 -0
- package/lib/admin/AdminResponses.js +164 -0
- package/lib/admin/AdminServices.d.ts +99 -0
- package/lib/admin/AdminServices.js +4 -0
- package/lib/admin/AdminStaticFiles.d.ts +29 -0
- package/lib/admin/AdminStaticFiles.js +97 -0
- package/lib/admin/AdminStore.d.ts +152 -0
- package/lib/admin/AdminStore.js +46 -0
- package/lib/admin/AlertChannels.d.ts +43 -0
- package/lib/admin/AlertChannels.js +120 -0
- package/lib/admin/AlertEngine.d.ts +231 -0
- package/lib/admin/AlertEngine.js +681 -0
- package/lib/admin/AlertRoutes.d.ts +8 -0
- package/lib/admin/AlertRoutes.js +180 -0
- package/lib/admin/ApiKeyRoutes.d.ts +8 -0
- package/lib/admin/ApiKeyRoutes.js +53 -0
- package/lib/admin/ApiKeys.d.ts +63 -0
- package/lib/admin/ApiKeys.js +167 -0
- package/lib/admin/AuditRoutes.d.ts +8 -0
- package/lib/admin/AuditRoutes.js +106 -0
- package/lib/admin/AuthToolRoutes.d.ts +13 -0
- package/lib/admin/AuthToolRoutes.js +164 -0
- package/lib/admin/BanList.d.ts +87 -0
- package/lib/admin/BanList.js +152 -0
- package/lib/admin/CborDecoder.d.ts +18 -0
- package/lib/admin/CborDecoder.js +201 -0
- package/lib/admin/ColyseusCloudClient.d.ts +313 -0
- package/lib/admin/ColyseusCloudClient.js +272 -0
- package/lib/admin/ColyseusCloudRoutes.d.ts +8 -0
- package/lib/admin/ColyseusCloudRoutes.js +91 -0
- package/lib/admin/ColyseusCloudService.d.ts +180 -0
- package/lib/admin/ColyseusCloudService.js +476 -0
- package/lib/admin/ColyseusCloudSettings.d.ts +16 -0
- package/lib/admin/ColyseusCloudSettings.js +29 -0
- package/lib/admin/ColyseusCloudViews.d.ts +105 -0
- package/lib/admin/ColyseusCloudViews.js +371 -0
- package/lib/admin/CommonPasswords.d.ts +6 -0
- package/lib/admin/CommonPasswords.js +19 -0
- package/lib/admin/ConfigOverrides.d.ts +174 -0
- package/lib/admin/ConfigOverrides.js +406 -0
- package/lib/admin/ConfigRoutes.d.ts +8 -0
- package/lib/admin/ConfigRoutes.js +91 -0
- package/lib/admin/ConsoleSessions.d.ts +112 -0
- package/lib/admin/ConsoleSessions.js +201 -0
- package/lib/admin/DedicatedRoutes.d.ts +21 -0
- package/lib/admin/DedicatedRoutes.js +254 -0
- package/lib/admin/ExtensionRoutes.d.ts +8 -0
- package/lib/admin/ExtensionRoutes.js +145 -0
- package/lib/admin/FileAdminStore.d.ts +157 -0
- package/lib/admin/FileAdminStore.js +349 -0
- package/lib/admin/FleetDirectory.d.ts +323 -0
- package/lib/admin/FleetDirectory.js +489 -0
- package/lib/admin/FleetLeader.d.ts +43 -0
- package/lib/admin/FleetLeader.js +89 -0
- package/lib/admin/FleetRoutes.d.ts +55 -0
- package/lib/admin/FleetRoutes.js +465 -0
- package/lib/admin/HealthWarnings.d.ts +10 -0
- package/lib/admin/HealthWarnings.js +95 -0
- package/lib/admin/LeaderboardRoutes.d.ts +8 -0
- package/lib/admin/LeaderboardRoutes.js +569 -0
- package/lib/admin/LogBuffer.d.ts +60 -0
- package/lib/admin/LogBuffer.js +92 -0
- package/lib/admin/LogRedaction.d.ts +15 -0
- package/lib/admin/LogRedaction.js +72 -0
- package/lib/admin/LogRoutes.d.ts +45 -0
- package/lib/admin/LogRoutes.js +307 -0
- package/lib/admin/MaintenanceMode.d.ts +41 -0
- package/lib/admin/MaintenanceMode.js +74 -0
- package/lib/admin/MaintenanceWindows.d.ts +91 -0
- package/lib/admin/MaintenanceWindows.js +207 -0
- package/lib/admin/MetricsRoutes.d.ts +8 -0
- package/lib/admin/MetricsRoutes.js +209 -0
- package/lib/admin/MetricsSampler.d.ts +124 -0
- package/lib/admin/MetricsSampler.js +211 -0
- package/lib/admin/OneTimeCodes.d.ts +60 -0
- package/lib/admin/OneTimeCodes.js +162 -0
- package/lib/admin/OpenIdConnect.d.ts +146 -0
- package/lib/admin/OpenIdConnect.js +335 -0
- package/lib/admin/Passkeys.d.ts +130 -0
- package/lib/admin/Passkeys.js +432 -0
- package/lib/admin/PlayerRoutes.d.ts +20 -0
- package/lib/admin/PlayerRoutes.js +676 -0
- package/lib/admin/RedisAdminStore.d.ts +114 -0
- package/lib/admin/RedisAdminStore.js +258 -0
- package/lib/admin/RolloutCoordinator.d.ts +99 -0
- package/lib/admin/RolloutCoordinator.js +321 -0
- package/lib/admin/ServerEvents.d.ts +57 -0
- package/lib/admin/ServerEvents.js +89 -0
- package/lib/admin/SessionRoutes.d.ts +113 -0
- package/lib/admin/SessionRoutes.js +386 -0
- package/lib/admin/UserRoutes.d.ts +8 -0
- package/lib/admin/UserRoutes.js +214 -0
- package/lib/cli.d.ts +2 -0
- package/lib/cli.js +221 -0
- package/lib/console/assets/index-CI5N7c-T.js +21 -0
- package/lib/console/assets/index-DPitvzLp.css +1 -0
- package/lib/console/favicon.svg +1 -0
- package/lib/console/index.html +16 -0
- package/lib/discord/DiscordOAuth.d.ts +57 -0
- package/lib/discord/DiscordOAuth.js +127 -0
- package/lib/discord/DiscordSessions.d.ts +79 -0
- package/lib/discord/DiscordSessions.js +135 -0
- package/lib/discord/DiscordSignInConstants.d.ts +54 -0
- package/lib/discord/DiscordSignInConstants.js +55 -0
- package/lib/discord/DiscordSignInRoutes.d.ts +51 -0
- package/lib/discord/DiscordSignInRoutes.js +359 -0
- package/lib/index.d.ts +24 -0
- package/lib/index.js +18 -0
- package/lib/leaderboards/FileLeaderboardRankStore.d.ts +293 -0
- package/lib/leaderboards/FileLeaderboardRankStore.js +808 -0
- package/lib/leaderboards/LeaderboardConstants.d.ts +63 -0
- package/lib/leaderboards/LeaderboardConstants.js +70 -0
- package/lib/leaderboards/LeaderboardError.d.ts +15 -0
- package/lib/leaderboards/LeaderboardError.js +20 -0
- package/lib/leaderboards/LeaderboardIdentity.d.ts +44 -0
- package/lib/leaderboards/LeaderboardIdentity.js +72 -0
- package/lib/leaderboards/LeaderboardPublicApi.d.ts +17 -0
- package/lib/leaderboards/LeaderboardPublicApi.js +389 -0
- package/lib/leaderboards/LeaderboardRankStore.d.ts +296 -0
- package/lib/leaderboards/LeaderboardRankStore.js +18 -0
- package/lib/leaderboards/LeaderboardRules.d.ts +122 -0
- package/lib/leaderboards/LeaderboardRules.js +472 -0
- package/lib/leaderboards/LeaderboardScheduler.d.ts +43 -0
- package/lib/leaderboards/LeaderboardScheduler.js +103 -0
- package/lib/leaderboards/LeaderboardService.d.ts +669 -0
- package/lib/leaderboards/LeaderboardService.js +1667 -0
- package/lib/leaderboards/LeaderboardTransfer.d.ts +35 -0
- package/lib/leaderboards/LeaderboardTransfer.js +580 -0
- package/lib/leaderboards/RedisLeaderboardRankStore.d.ts +282 -0
- package/lib/leaderboards/RedisLeaderboardRankStore.js +691 -0
- package/package.json +64 -4
- package/templates/init/Caddyfile +6 -0
- package/templates/init/DEPLOY.md +233 -0
- package/templates/init/Dockerfile +12 -0
- package/templates/init/app.json +21 -0
- package/templates/init/aws/README.md +118 -0
- package/templates/init/aws/ecs-fargate-alb.yml +207 -0
- package/templates/init/aws/ecs-fargate-nlb-quic.yml +210 -0
- package/templates/init/babylon-game-server.config.mjs +158 -0
- package/templates/init/colyseus-cloud/README.md +133 -0
- package/templates/init/colyseus-cloud/babylon-game-server.config.mjs +36 -0
- package/templates/init/colyseus-cloud/ecosystem.config.cjs +35 -0
- package/templates/init/colyseus-cloud/gitignore +6 -0
- package/templates/init/colyseus-cloud/package.json +15 -0
- package/templates/init/digitalocean-app.yaml +30 -0
- package/templates/init/docker-compose.full.yml +45 -0
- package/templates/init/docker-compose.websocket.yml +41 -0
- package/templates/init/fly.toml +56 -0
- package/templates/init/heroku.yml +11 -0
- package/templates/init/kubernetes/deployment.yaml +71 -0
- package/templates/init/kubernetes/redis.yaml +73 -0
- package/templates/init/kubernetes/service.yaml +24 -0
- package/templates/init/monitoring/grafana-dashboard.json +539 -0
- package/templates/init/monitoring/prometheus-alerts.yml +51 -0
- package/templates/init/monitoring/prometheus-scrape.yml +17 -0
- package/templates/init/railway.json +13 -0
- package/templates/init/render.yaml +19 -0
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { AdminApiRouter } from "./AdminApiRouter.js";
|
|
2
|
+
import type { AdminServices } from "./AdminServices.js";
|
|
3
|
+
/**
|
|
4
|
+
* Registers every alerts/*, maintenance/windows* and rollouts* route.
|
|
5
|
+
* @param router The admin API router.
|
|
6
|
+
* @param services The shared services.
|
|
7
|
+
*/
|
|
8
|
+
export declare function registerAlertRoutes(router: AdminApiRouter, services: AdminServices): void;
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
// The alerts, maintenance windows and rolling update routes (game-server-admin-console DR §6.11 alerts/*,
|
|
2
|
+
// maintenance/windows* and rollouts* rows, D24).
|
|
3
|
+
import { AdminRequestError } from "./AdminApiRouter.js";
|
|
4
|
+
import { bodyObject, booleanField, numberField, optionalStringField, stringField, stringListField } from "./AdminRequestBodies.js";
|
|
5
|
+
import { noticeTextField, nonNegativeNumberField, requireScaledOut } from "./FleetRoutes.js";
|
|
6
|
+
/** HTTP status of a successful answer. */
|
|
7
|
+
const HTTP_OK = 200;
|
|
8
|
+
/** HTTP status of an invalid request. */
|
|
9
|
+
const HTTP_BAD_REQUEST = 400;
|
|
10
|
+
/** The ok body of an answer with nothing else to say. */
|
|
11
|
+
const OK_BODY = { ok: true };
|
|
12
|
+
/** The alert states the history can be filtered by. */
|
|
13
|
+
const ALERT_STATES = new Set(["firing", "resolved"]);
|
|
14
|
+
/**
|
|
15
|
+
* Registers every alerts/*, maintenance/windows* and rollouts* route.
|
|
16
|
+
* @param router The admin API router.
|
|
17
|
+
* @param services The shared services.
|
|
18
|
+
*/
|
|
19
|
+
export function registerAlertRoutes(router, services) {
|
|
20
|
+
const routes = [
|
|
21
|
+
{ method: "GET", pattern: "alerts/rules", permission: "alerts.view", requiresStepUp: false, auditAction: null, handler: async () => ok(await services.alerts.listRules()) },
|
|
22
|
+
{ method: "PUT", pattern: "alerts/rules/:ruleId", permission: "alerts.manage", requiresStepUp: false, auditAction: "alert.rule", handler: async (request) => updateRule(services, request) },
|
|
23
|
+
{ method: "POST", pattern: "alerts/rules/:ruleId/silence", permission: "alerts.manage", requiresStepUp: false, auditAction: "alert.silence", handler: async (request) => silenceRule(services, request) },
|
|
24
|
+
{ method: "GET", pattern: "alerts/channels", permission: "alerts.view", requiresStepUp: false, auditAction: null, handler: async () => ok(await services.alerts.listChannels()) },
|
|
25
|
+
{ method: "POST", pattern: "alerts/channels", permission: "alerts.manage", requiresStepUp: true, auditAction: "alert.channelCreate", handler: async (request) => createChannel(services, request) },
|
|
26
|
+
{ method: "DELETE", pattern: "alerts/channels/:channelId", permission: "alerts.manage", requiresStepUp: false, auditAction: "alert.channelDelete", handler: async (request) => okAfter(services.alerts.deleteChannel(request.params.channelId)) },
|
|
27
|
+
{ method: "POST", pattern: "alerts/channels/:channelId/test", permission: "alerts.manage", requiresStepUp: false, auditAction: "alert.channelTest", handler: async (request) => ok(await services.alerts.testChannel(request.params.channelId)) },
|
|
28
|
+
{ method: "GET", pattern: "alerts/history", permission: "alerts.view", requiresStepUp: false, auditAction: null, handler: async (request) => ok(await services.alerts.listAlerts(alertStateOf(request.query), pageOf(request.query))) },
|
|
29
|
+
{ method: "POST", pattern: "alerts/history/:alertId/acknowledge", permission: "alerts.manage", requiresStepUp: false, auditAction: "alert.acknowledge", handler: async (request) => acknowledgeAlert(services, request) },
|
|
30
|
+
{ method: "GET", pattern: "maintenance/windows", permission: "fleet.view", requiresStepUp: false, auditAction: null, handler: async () => ok(await services.maintenanceWindows.list()) },
|
|
31
|
+
{ method: "POST", pattern: "maintenance/windows", permission: "fleet.manage", requiresStepUp: true, auditAction: "maintenance.schedule", handler: async (request) => createWindow(services, request) },
|
|
32
|
+
{ method: "DELETE", pattern: "maintenance/windows/:windowId", permission: "fleet.manage", requiresStepUp: false, auditAction: "maintenance.cancel", handler: async (request) => okAfter(services.maintenanceWindows.cancel(request.params.windowId)) },
|
|
33
|
+
{ method: "GET", pattern: "rollouts", permission: "fleet.view", requiresStepUp: false, auditAction: null, handler: async () => ok(await services.rollouts.list()) },
|
|
34
|
+
{ method: "POST", pattern: "rollouts", permission: "fleet.manage", requiresStepUp: true, auditAction: "rollout.start", handler: async (request) => startRollout(services, request) },
|
|
35
|
+
{ method: "POST", pattern: "rollouts/:rolloutId/:command", permission: "fleet.manage", requiresStepUp: false, auditAction: (request) => "rollout." + request.params.command, handler: async (request) => ok(await services.rollouts.command(request.params.rolloutId, request.params.command)) }
|
|
36
|
+
];
|
|
37
|
+
for (const route of routes)
|
|
38
|
+
router.register(route);
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* A 200 answer.
|
|
42
|
+
* @param body The JSON body.
|
|
43
|
+
* @returns The result.
|
|
44
|
+
*/
|
|
45
|
+
function ok(body) {
|
|
46
|
+
return { status: HTTP_OK, body: body };
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Waits for an action, then answers ok.
|
|
50
|
+
* @param action The action.
|
|
51
|
+
* @returns The ok result.
|
|
52
|
+
*/
|
|
53
|
+
async function okAfter(action) {
|
|
54
|
+
await action;
|
|
55
|
+
return ok(OK_BODY);
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* PUT alerts/rules/:ruleId.
|
|
59
|
+
* @param services The shared services.
|
|
60
|
+
* @param request The request.
|
|
61
|
+
* @returns The rule.
|
|
62
|
+
*/
|
|
63
|
+
async function updateRule(services, request) {
|
|
64
|
+
const body = bodyObject(request.body);
|
|
65
|
+
const update = {
|
|
66
|
+
enabled: booleanField(body, "enabled"),
|
|
67
|
+
threshold: (body.threshold == null) ? null : numberField(body, "threshold"),
|
|
68
|
+
forMinutes: nonNegativeNumberField(body, "forMinutes"),
|
|
69
|
+
severity: stringField(body, "severity"),
|
|
70
|
+
channelIds: (body.channelIds == null) ? [] : stringListField(body, "channelIds")
|
|
71
|
+
};
|
|
72
|
+
const ruleId = request.params.ruleId;
|
|
73
|
+
return { status: HTTP_OK, body: await services.alerts.updateRule(ruleId, update), audit: { target: ruleId, parameters: { ...update } } };
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* POST alerts/rules/:ruleId/silence.
|
|
77
|
+
* @param services The shared services.
|
|
78
|
+
* @param request The request.
|
|
79
|
+
* @returns The rule.
|
|
80
|
+
*/
|
|
81
|
+
async function silenceRule(services, request) {
|
|
82
|
+
const minutes = nonNegativeNumberField(bodyObject(request.body), "minutes");
|
|
83
|
+
const ruleId = request.params.ruleId;
|
|
84
|
+
return { status: HTTP_OK, body: await services.alerts.silenceRule(ruleId, minutes), audit: { target: ruleId, parameters: { minutes: minutes } } };
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* POST alerts/channels: the audit record leaves out the URL and routing key (secrets).
|
|
88
|
+
* @param services The shared services.
|
|
89
|
+
* @param request The request.
|
|
90
|
+
* @returns The channel, secrets masked.
|
|
91
|
+
*/
|
|
92
|
+
async function createChannel(services, request) {
|
|
93
|
+
const body = bodyObject(request.body);
|
|
94
|
+
const channel = {
|
|
95
|
+
kind: stringField(body, "kind"),
|
|
96
|
+
name: stringField(body, "name"),
|
|
97
|
+
url: optionalStringField(body, "url"),
|
|
98
|
+
routingKey: optionalStringField(body, "routingKey")
|
|
99
|
+
};
|
|
100
|
+
const view = await services.alerts.createChannel(channel, callerNameOf(request));
|
|
101
|
+
return { status: HTTP_OK, body: view, audit: { target: view.channelId, parameters: { kind: channel.kind, name: channel.name } } };
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* POST alerts/history/:alertId/acknowledge.
|
|
105
|
+
* @param services The shared services.
|
|
106
|
+
* @param request The request.
|
|
107
|
+
* @returns The alert.
|
|
108
|
+
*/
|
|
109
|
+
async function acknowledgeAlert(services, request) {
|
|
110
|
+
const alert = await services.alerts.acknowledge(request.params.alertId, callerNameOf(request));
|
|
111
|
+
return { status: HTTP_OK, body: alert, audit: { target: alert.alertId, parameters: { ruleId: alert.ruleId } } };
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* POST maintenance/windows.
|
|
115
|
+
* @param services The shared services.
|
|
116
|
+
* @param request The request.
|
|
117
|
+
* @returns The window.
|
|
118
|
+
*/
|
|
119
|
+
async function createWindow(services, request) {
|
|
120
|
+
const body = bodyObject(request.body);
|
|
121
|
+
const window = {
|
|
122
|
+
startsAt: stringField(body, "startsAt"),
|
|
123
|
+
endsAt: stringField(body, "endsAt"),
|
|
124
|
+
message: noticeTextField(body, "message") ?? "",
|
|
125
|
+
warningMinutes: (body.warningMinutes == null) ? null : nonNegativeNumberField(body, "warningMinutes"),
|
|
126
|
+
allowUserIds: (body.allowUserIds == null) ? [] : stringListField(body, "allowUserIds"),
|
|
127
|
+
allowAddresses: (body.allowAddresses == null) ? [] : stringListField(body, "allowAddresses"),
|
|
128
|
+
leaveReadiness: (body.leaveReadiness == null) ? false : booleanField(body, "leaveReadiness")
|
|
129
|
+
};
|
|
130
|
+
const view = await services.maintenanceWindows.create(window, callerNameOf(request));
|
|
131
|
+
return { status: HTTP_OK, body: view, audit: { target: view.windowId, parameters: { ...window } } };
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* POST rollouts: needs Redis (R33) and at least two live instances (DR §7.8).
|
|
135
|
+
* @param services The shared services.
|
|
136
|
+
* @param request The request.
|
|
137
|
+
* @returns The rollout.
|
|
138
|
+
*/
|
|
139
|
+
async function startRollout(services, request) {
|
|
140
|
+
requireScaledOut(services);
|
|
141
|
+
const body = bodyObject(request.body);
|
|
142
|
+
const rollout = {
|
|
143
|
+
noticeText: noticeTextField(body, "noticeText"),
|
|
144
|
+
noticeLeadSeconds: (body.noticeLeadSeconds == null) ? 0 : nonNegativeNumberField(body, "noticeLeadSeconds"),
|
|
145
|
+
perInstanceTimeoutMinutes: numberField(body, "perInstanceTimeoutMinutes")
|
|
146
|
+
};
|
|
147
|
+
const view = await services.rollouts.start(rollout, callerNameOf(request));
|
|
148
|
+
return { status: HTTP_OK, body: view, audit: { target: view.rolloutId, parameters: { ...rollout } } };
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* The name of the admin making a request, for createdBy and acknowledgedBy fields.
|
|
152
|
+
* @param request The request.
|
|
153
|
+
* @returns The caller's name.
|
|
154
|
+
*/
|
|
155
|
+
function callerNameOf(request) {
|
|
156
|
+
return request.caller?.name ?? "unknown";
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* The state filter of GET alerts/history.
|
|
160
|
+
* @param query The query.
|
|
161
|
+
* @returns "firing", "resolved" or null for both.
|
|
162
|
+
* @throws AdminRequestError 400 for another value.
|
|
163
|
+
*/
|
|
164
|
+
function alertStateOf(query) {
|
|
165
|
+
const state = query.get("state");
|
|
166
|
+
if (state == null || state === "")
|
|
167
|
+
return null;
|
|
168
|
+
if (!ALERT_STATES.has(state))
|
|
169
|
+
throw new AdminRequestError(HTTP_BAD_REQUEST, "state must be firing or resolved");
|
|
170
|
+
return state;
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* The page of a query string.
|
|
174
|
+
* @param query The query.
|
|
175
|
+
* @returns The page, from 1.
|
|
176
|
+
*/
|
|
177
|
+
function pageOf(query) {
|
|
178
|
+
const page = Number(query.get("page") ?? 1);
|
|
179
|
+
return (Number.isInteger(page) && page >= 1) ? page : 1;
|
|
180
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { type AdminApiRouter } from "./AdminApiRouter.js";
|
|
2
|
+
import type { AdminServices } from "./AdminServices.js";
|
|
3
|
+
/**
|
|
4
|
+
* Registers every api-keys/* route.
|
|
5
|
+
* @param router The admin API router.
|
|
6
|
+
* @param services The shared services.
|
|
7
|
+
*/
|
|
8
|
+
export declare function registerApiKeyRoutes(router: AdminApiRouter, services: AdminServices): void;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
// The API key routes of the admin API (game-server-admin-console DR §6.11 api-keys/* rows, D18).
|
|
2
|
+
import { AdminRequestError } from "./AdminApiRouter.js";
|
|
3
|
+
import { bodyObject, optionalStringField, stringField, stringListField } from "./AdminRequestBodies.js";
|
|
4
|
+
/** HTTP status of a successful read or revoke. */
|
|
5
|
+
const HTTP_OK = 200;
|
|
6
|
+
/** HTTP status of a created key. */
|
|
7
|
+
const HTTP_CREATED = 201;
|
|
8
|
+
/** HTTP status of a caller without an identity (never reached: the route needs a caller). */
|
|
9
|
+
const HTTP_UNAUTHORIZED = 401;
|
|
10
|
+
/**
|
|
11
|
+
* Registers every api-keys/* route.
|
|
12
|
+
* @param router The admin API router.
|
|
13
|
+
* @param services The shared services.
|
|
14
|
+
*/
|
|
15
|
+
export function registerApiKeyRoutes(router, services) {
|
|
16
|
+
const routes = [
|
|
17
|
+
{ method: "GET", pattern: "api-keys", permission: "apiKeys.manage", requiresStepUp: false, auditAction: null, handler: async () => ({ status: HTTP_OK, body: (await services.apiKeys.list()).map((key) => services.apiKeys.toView(key)) }) },
|
|
18
|
+
{ method: "POST", pattern: "api-keys", permission: "apiKeys.manage", requiresStepUp: true, auditAction: "apiKey.create", handler: async (request) => createKey(services, request) },
|
|
19
|
+
{ method: "DELETE", pattern: "api-keys/:keyId", permission: "apiKeys.manage", requiresStepUp: true, auditAction: "apiKey.revoke", handler: async (request) => revokeKey(services, request) }
|
|
20
|
+
];
|
|
21
|
+
for (const route of routes)
|
|
22
|
+
router.register(route);
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* POST api-keys: the secret is in this answer only.
|
|
26
|
+
* @param services The shared services.
|
|
27
|
+
* @param request The request.
|
|
28
|
+
* @returns The key and its secret.
|
|
29
|
+
*/
|
|
30
|
+
async function createKey(services, request) {
|
|
31
|
+
const caller = request.caller;
|
|
32
|
+
if (caller == null)
|
|
33
|
+
throw new AdminRequestError(HTTP_UNAUTHORIZED, "sign in first");
|
|
34
|
+
const body = bodyObject(request.body);
|
|
35
|
+
const created = await services.apiKeys.create({
|
|
36
|
+
name: stringField(body, "name"),
|
|
37
|
+
permissions: stringListField(body, "permissions"),
|
|
38
|
+
expiresAt: optionalStringField(body, "expiresAt"),
|
|
39
|
+
allowedNetworks: (body.allowedNetworks != null) ? stringListField(body, "allowedNetworks") : []
|
|
40
|
+
}, caller.id, caller.permissions);
|
|
41
|
+
const view = { key: services.apiKeys.toView(created.record), secret: created.secret };
|
|
42
|
+
return { status: HTTP_CREATED, body: view, audit: { target: created.record.name, parameters: { keyId: created.record.keyId, permissions: created.record.permissions } } };
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* DELETE api-keys/:keyId: refused at once from then on.
|
|
46
|
+
* @param services The shared services.
|
|
47
|
+
* @param request The request.
|
|
48
|
+
* @returns ok.
|
|
49
|
+
*/
|
|
50
|
+
async function revokeKey(services, request) {
|
|
51
|
+
const revoked = await services.apiKeys.revoke(request.params.keyId);
|
|
52
|
+
return { status: HTTP_OK, body: { ok: true }, audit: { target: revoked.name, parameters: { keyId: revoked.keyId } } };
|
|
53
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { type AdminCaller } from "./AdminApiRouter.js";
|
|
2
|
+
import type { AdminPermission, ApiKeyView, CreateApiKeyRequest } from "./AdminApiTypes.js";
|
|
3
|
+
import type { AdminClock } from "./AdminClock.js";
|
|
4
|
+
import type { ApiKeyRecord } from "./AdminRecords.js";
|
|
5
|
+
import type { AdminStore } from "./AdminStore.js";
|
|
6
|
+
/** Prefix of every API key bearer value. */
|
|
7
|
+
export declare const API_KEY_PREFIX: string;
|
|
8
|
+
/** The API keys of the admin store. */
|
|
9
|
+
export declare class ApiKeys {
|
|
10
|
+
/** The admin store. */
|
|
11
|
+
private readonly store;
|
|
12
|
+
/** The clock of expiries and lastUsedAt. */
|
|
13
|
+
private readonly clock;
|
|
14
|
+
/**
|
|
15
|
+
* Creates the key service.
|
|
16
|
+
* @param store The admin store.
|
|
17
|
+
* @param clock The clock.
|
|
18
|
+
*/
|
|
19
|
+
constructor(store: AdminStore, clock: AdminClock);
|
|
20
|
+
/**
|
|
21
|
+
* Creates a key; its secret is returned once and only its hash is stored.
|
|
22
|
+
* @param request The key's name, permissions, expiry and networks.
|
|
23
|
+
* @param createdBy The creating user's id, or "cli".
|
|
24
|
+
* @param creatorPermissions What the creator may do; a key never gets more.
|
|
25
|
+
* @returns The stored key and the full bearer value.
|
|
26
|
+
* @throws AdminRequestError 400 for an invalid request, 403 for a permission the creator lacks.
|
|
27
|
+
*/
|
|
28
|
+
create(request: CreateApiKeyRequest, createdBy: string, creatorPermissions: ReadonlySet<AdminPermission>): Promise<{
|
|
29
|
+
record: ApiKeyRecord;
|
|
30
|
+
secret: string;
|
|
31
|
+
}>;
|
|
32
|
+
/**
|
|
33
|
+
* The caller of a `bgsk_` bearer value: the key must exist, match, be unrevoked, unexpired and allow the address.
|
|
34
|
+
* lastUsedAt is written at most once per API_KEY_TOUCH_INTERVAL_MS.
|
|
35
|
+
* @param bearer The bearer value.
|
|
36
|
+
* @param clientAddress The client address.
|
|
37
|
+
* @returns The caller, or null.
|
|
38
|
+
*/
|
|
39
|
+
verify(bearer: string, clientAddress: string): Promise<AdminCaller | null>;
|
|
40
|
+
/**
|
|
41
|
+
* Revokes a key at once.
|
|
42
|
+
* @param keyId The key.
|
|
43
|
+
* @returns The revoked key.
|
|
44
|
+
* @throws AdminRequestError 404 for an unknown key.
|
|
45
|
+
*/
|
|
46
|
+
revoke(keyId: string): Promise<ApiKeyRecord>;
|
|
47
|
+
/**
|
|
48
|
+
* Every key, newest first.
|
|
49
|
+
* @returns The keys.
|
|
50
|
+
*/
|
|
51
|
+
list(): Promise<ApiKeyRecord[]>;
|
|
52
|
+
/**
|
|
53
|
+
* The configured admin.token as a full-access key (D18).
|
|
54
|
+
* @returns Its caller.
|
|
55
|
+
*/
|
|
56
|
+
legacyCaller(): AdminCaller;
|
|
57
|
+
/**
|
|
58
|
+
* The console's view of a key (never its hash).
|
|
59
|
+
* @param key The key.
|
|
60
|
+
* @returns The view.
|
|
61
|
+
*/
|
|
62
|
+
toView(key: ApiKeyRecord): ApiKeyView;
|
|
63
|
+
}
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
// Scoped API keys of the admin API (game-server-admin-console D18): `bgsk_<keyId>_<secret>` bearer values whose secret
|
|
2
|
+
// is stored only as a SHA-256 hash, each with a permission subset, an optional expiry and optional allowed networks.
|
|
3
|
+
import { randomBytes } from "node:crypto";
|
|
4
|
+
import { BlockList } from "node:net";
|
|
5
|
+
import { addNetworkEntry, createNetworkList, isAddressInList } from "../NetworkAddresses.js";
|
|
6
|
+
import { AdminRequestError } from "./AdminApiRouter.js";
|
|
7
|
+
import { API_KEY_TOUCH_INTERVAL_MS } from "./AdminConstants.js";
|
|
8
|
+
import { randomToken, sha256Hex, textsMatch } from "./AdminCrypto.js";
|
|
9
|
+
import { LEGACY_TOKEN_PERMISSIONS, isAdminPermission } from "./AdminPermissions.js";
|
|
10
|
+
/** Prefix of every API key bearer value. */
|
|
11
|
+
export const API_KEY_PREFIX = "bgsk_";
|
|
12
|
+
/** Random bytes of a key id (hex, so 12 characters without "_"). */
|
|
13
|
+
const KEY_ID_BYTES = 6;
|
|
14
|
+
/** Characters of a key id. */
|
|
15
|
+
const KEY_ID_LENGTH = 12;
|
|
16
|
+
/** Random bytes of a key secret. */
|
|
17
|
+
const SECRET_BYTES = 32;
|
|
18
|
+
/** HTTP status of an invalid key request. */
|
|
19
|
+
const HTTP_BAD_REQUEST = 400;
|
|
20
|
+
/** HTTP status of a key granting more than its creator holds. */
|
|
21
|
+
const HTTP_FORBIDDEN = 403;
|
|
22
|
+
/** HTTP status of an unknown key. */
|
|
23
|
+
const HTTP_NOT_FOUND = 404;
|
|
24
|
+
/** The API keys of the admin store. */
|
|
25
|
+
export class ApiKeys {
|
|
26
|
+
/** The admin store. */
|
|
27
|
+
store;
|
|
28
|
+
/** The clock of expiries and lastUsedAt. */
|
|
29
|
+
clock;
|
|
30
|
+
/**
|
|
31
|
+
* Creates the key service.
|
|
32
|
+
* @param store The admin store.
|
|
33
|
+
* @param clock The clock.
|
|
34
|
+
*/
|
|
35
|
+
constructor(store, clock) {
|
|
36
|
+
this.store = store;
|
|
37
|
+
this.clock = clock;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Creates a key; its secret is returned once and only its hash is stored.
|
|
41
|
+
* @param request The key's name, permissions, expiry and networks.
|
|
42
|
+
* @param createdBy The creating user's id, or "cli".
|
|
43
|
+
* @param creatorPermissions What the creator may do; a key never gets more.
|
|
44
|
+
* @returns The stored key and the full bearer value.
|
|
45
|
+
* @throws AdminRequestError 400 for an invalid request, 403 for a permission the creator lacks.
|
|
46
|
+
*/
|
|
47
|
+
async create(request, createdBy, creatorPermissions) {
|
|
48
|
+
const name = request.name.trim();
|
|
49
|
+
if (name === "")
|
|
50
|
+
throw new AdminRequestError(HTTP_BAD_REQUEST, "the key needs a name");
|
|
51
|
+
const unknown = request.permissions.find((permission) => !isAdminPermission(permission));
|
|
52
|
+
if (unknown != null)
|
|
53
|
+
throw new AdminRequestError(HTTP_BAD_REQUEST, "unknown permission " + unknown);
|
|
54
|
+
const beyondCreator = request.permissions.find((permission) => !creatorPermissions.has(permission));
|
|
55
|
+
if (beyondCreator != null)
|
|
56
|
+
throw new AdminRequestError(HTTP_FORBIDDEN, "you cannot grant a permission you do not hold", beyondCreator);
|
|
57
|
+
if (request.expiresAt != null && Number.isNaN(Date.parse(request.expiresAt)))
|
|
58
|
+
throw new AdminRequestError(HTTP_BAD_REQUEST, "expiresAt must be an ISO time or null");
|
|
59
|
+
const probeList = new BlockList();
|
|
60
|
+
const badNetwork = request.allowedNetworks.find((entry) => !addNetworkEntry(probeList, entry));
|
|
61
|
+
if (badNetwork != null)
|
|
62
|
+
throw new AdminRequestError(HTTP_BAD_REQUEST, "not an address or CIDR range: " + badNetwork);
|
|
63
|
+
const keyId = randomBytes(KEY_ID_BYTES).toString("hex");
|
|
64
|
+
const secretPart = randomToken(SECRET_BYTES);
|
|
65
|
+
const record = {
|
|
66
|
+
keyId: keyId,
|
|
67
|
+
name: name,
|
|
68
|
+
secretHash: sha256Hex(secretPart),
|
|
69
|
+
permissions: Array.from(new Set(request.permissions)),
|
|
70
|
+
expiresAt: request.expiresAt,
|
|
71
|
+
allowedNetworks: request.allowedNetworks,
|
|
72
|
+
createdBy: createdBy,
|
|
73
|
+
createdAt: new Date(this.clock.now()).toISOString(),
|
|
74
|
+
lastUsedAt: null,
|
|
75
|
+
revoked: false
|
|
76
|
+
};
|
|
77
|
+
await this.store.putDocument("apiKeys", keyId, record, 0);
|
|
78
|
+
return { record: record, secret: API_KEY_PREFIX + keyId + "_" + secretPart };
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* The caller of a `bgsk_` bearer value: the key must exist, match, be unrevoked, unexpired and allow the address.
|
|
82
|
+
* lastUsedAt is written at most once per API_KEY_TOUCH_INTERVAL_MS.
|
|
83
|
+
* @param bearer The bearer value.
|
|
84
|
+
* @param clientAddress The client address.
|
|
85
|
+
* @returns The caller, or null.
|
|
86
|
+
*/
|
|
87
|
+
async verify(bearer, clientAddress) {
|
|
88
|
+
const parsed = parseKey(bearer);
|
|
89
|
+
if (parsed == null)
|
|
90
|
+
return null;
|
|
91
|
+
const stored = await this.store.getDocument("apiKeys", parsed.keyId);
|
|
92
|
+
if (stored == null)
|
|
93
|
+
return null;
|
|
94
|
+
const key = stored.value;
|
|
95
|
+
const now = this.clock.now();
|
|
96
|
+
if (!textsMatch(sha256Hex(parsed.secretPart), key.secretHash) || key.revoked)
|
|
97
|
+
return null;
|
|
98
|
+
if (key.expiresAt != null && Date.parse(key.expiresAt) <= now)
|
|
99
|
+
return null;
|
|
100
|
+
if (key.allowedNetworks.length > 0 && !isAddressInList(createNetworkList(key.allowedNetworks), clientAddress))
|
|
101
|
+
return null;
|
|
102
|
+
if (key.lastUsedAt == null || now - Date.parse(key.lastUsedAt) >= API_KEY_TOUCH_INTERVAL_MS) {
|
|
103
|
+
await this.store.putDocument("apiKeys", key.keyId, Object.assign({}, key, { lastUsedAt: new Date(now).toISOString() }), null);
|
|
104
|
+
}
|
|
105
|
+
return { kind: "apiKey", id: key.keyId, name: key.name, permissions: new Set(key.permissions), session: null, user: null };
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Revokes a key at once.
|
|
109
|
+
* @param keyId The key.
|
|
110
|
+
* @returns The revoked key.
|
|
111
|
+
* @throws AdminRequestError 404 for an unknown key.
|
|
112
|
+
*/
|
|
113
|
+
async revoke(keyId) {
|
|
114
|
+
const stored = await this.store.getDocument("apiKeys", keyId);
|
|
115
|
+
if (stored == null)
|
|
116
|
+
throw new AdminRequestError(HTTP_NOT_FOUND, "no such API key");
|
|
117
|
+
const revoked = Object.assign({}, stored.value, { revoked: true });
|
|
118
|
+
await this.store.putDocument("apiKeys", keyId, revoked, stored.revision);
|
|
119
|
+
return revoked;
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Every key, newest first.
|
|
123
|
+
* @returns The keys.
|
|
124
|
+
*/
|
|
125
|
+
async list() {
|
|
126
|
+
const keys = await this.store.listDocuments("apiKeys");
|
|
127
|
+
return keys.map((stored) => stored.value).sort((left, right) => right.createdAt.localeCompare(left.createdAt));
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* The configured admin.token as a full-access key (D18).
|
|
131
|
+
* @returns Its caller.
|
|
132
|
+
*/
|
|
133
|
+
legacyCaller() {
|
|
134
|
+
return { kind: "legacyToken", id: "legacy", name: "legacy admin token", permissions: LEGACY_TOKEN_PERMISSIONS, session: null, user: null };
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* The console's view of a key (never its hash).
|
|
138
|
+
* @param key The key.
|
|
139
|
+
* @returns The view.
|
|
140
|
+
*/
|
|
141
|
+
toView(key) {
|
|
142
|
+
return {
|
|
143
|
+
keyId: key.keyId,
|
|
144
|
+
name: key.name,
|
|
145
|
+
permissions: key.permissions,
|
|
146
|
+
expiresAt: key.expiresAt,
|
|
147
|
+
allowedNetworks: key.allowedNetworks,
|
|
148
|
+
createdBy: key.createdBy,
|
|
149
|
+
createdAt: key.createdAt,
|
|
150
|
+
lastUsedAt: key.lastUsedAt,
|
|
151
|
+
revoked: key.revoked
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Splits a bearer value into its key id and secret.
|
|
157
|
+
* @param bearer `bgsk_<keyId>_<secret>`.
|
|
158
|
+
* @returns The parts, or null when the value is not a key.
|
|
159
|
+
*/
|
|
160
|
+
function parseKey(bearer) {
|
|
161
|
+
if (!bearer.startsWith(API_KEY_PREFIX))
|
|
162
|
+
return null;
|
|
163
|
+
const rest = bearer.substring(API_KEY_PREFIX.length);
|
|
164
|
+
if (rest.length <= KEY_ID_LENGTH + 1 || rest[KEY_ID_LENGTH] !== "_")
|
|
165
|
+
return null;
|
|
166
|
+
return { keyId: rest.substring(0, KEY_ID_LENGTH), secretPart: rest.substring(KEY_ID_LENGTH + 1) };
|
|
167
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { type AdminApiRouter } from "./AdminApiRouter.js";
|
|
2
|
+
import type { AdminServices } from "./AdminServices.js";
|
|
3
|
+
/**
|
|
4
|
+
* Registers every audit/* route and activity.
|
|
5
|
+
* @param router The admin API router.
|
|
6
|
+
* @param services The shared services.
|
|
7
|
+
*/
|
|
8
|
+
export declare function registerAuditRoutes(router: AdminApiRouter, services: AdminServices): void;
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
// The audit and activity routes of the admin API (game-server-admin-console DR §6.11 audit/* and activity rows, D19).
|
|
2
|
+
// There is deliberately no route that edits or deletes an audit record.
|
|
3
|
+
import { AdminRequestError } from "./AdminApiRouter.js";
|
|
4
|
+
import { bodyObject, numberField } from "./AdminRequestBodies.js";
|
|
5
|
+
/** HTTP status of a successful answer. */
|
|
6
|
+
const HTTP_OK = 200;
|
|
7
|
+
/** HTTP status of an invalid request. */
|
|
8
|
+
const HTTP_BAD_REQUEST = 400;
|
|
9
|
+
/** Activity items answered when the query names no limit. */
|
|
10
|
+
const DEFAULT_ACTIVITY_LIMIT = 20;
|
|
11
|
+
/** Most activity items one call answers. */
|
|
12
|
+
const MAXIMUM_ACTIVITY_LIMIT = 200;
|
|
13
|
+
/** Content types of the export formats. */
|
|
14
|
+
const EXPORT_CONTENT_TYPES = { csv: "text/csv; charset=utf-8", jsonl: "application/jsonl; charset=utf-8" };
|
|
15
|
+
/**
|
|
16
|
+
* Registers every audit/* route and activity.
|
|
17
|
+
* @param router The admin API router.
|
|
18
|
+
* @param services The shared services.
|
|
19
|
+
*/
|
|
20
|
+
export function registerAuditRoutes(router, services) {
|
|
21
|
+
const routes = [
|
|
22
|
+
{ method: "GET", pattern: "audit", permission: "audit.view", requiresStepUp: false, auditAction: null, handler: async (request) => ok(await services.audit.query(auditQueryOf(request.query), pageOf(request.query))) },
|
|
23
|
+
{ method: "GET", pattern: "audit/export", permission: "audit.view", requiresStepUp: false, auditAction: "audit.export", handler: async (request) => exportAudit(services, request) },
|
|
24
|
+
{ method: "GET", pattern: "audit/verify", permission: "audit.view", requiresStepUp: false, auditAction: null, handler: async () => ok(await services.audit.verifyChain()) },
|
|
25
|
+
{ method: "PUT", pattern: "audit/retention", permission: "audit.retention", requiresStepUp: true, auditAction: "audit.retention", handler: async (request) => setRetention(services, request) },
|
|
26
|
+
{ method: "GET", pattern: "activity", permission: "fleet.view", requiresStepUp: false, auditAction: null, handler: async (request) => ok(await services.audit.activity(activityLimitOf(request.query))) }
|
|
27
|
+
];
|
|
28
|
+
for (const route of routes)
|
|
29
|
+
router.register(route);
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* A 200 answer.
|
|
33
|
+
* @param body The JSON body.
|
|
34
|
+
* @returns The result.
|
|
35
|
+
*/
|
|
36
|
+
function ok(body) {
|
|
37
|
+
return { status: HTTP_OK, body: body };
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* GET audit/export: a CSV or JSON-lines download of the matching records.
|
|
41
|
+
* @param services The shared services.
|
|
42
|
+
* @param request The request.
|
|
43
|
+
* @returns The download.
|
|
44
|
+
* @throws AdminRequestError 400 for an unknown format.
|
|
45
|
+
*/
|
|
46
|
+
async function exportAudit(services, request) {
|
|
47
|
+
const format = request.query.get("format") ?? "csv";
|
|
48
|
+
if (format !== "csv" && format !== "jsonl")
|
|
49
|
+
throw new AdminRequestError(HTTP_BAD_REQUEST, "format must be csv or jsonl");
|
|
50
|
+
const bytes = await services.audit.export(format, auditQueryOf(request.query));
|
|
51
|
+
return { status: HTTP_OK, body: null, download: { bytes: bytes, contentType: EXPORT_CONTENT_TYPES[format], fileName: "audit." + format }, audit: { target: "audit." + format, parameters: { format: format } } };
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* PUT audit/retention.
|
|
55
|
+
* @param services The shared services.
|
|
56
|
+
* @param request The request.
|
|
57
|
+
* @returns The stored retention.
|
|
58
|
+
* @throws AdminRequestError 400 unless days is a whole number of at least 1.
|
|
59
|
+
*/
|
|
60
|
+
async function setRetention(services, request) {
|
|
61
|
+
const days = numberField(bodyObject(request.body), "days");
|
|
62
|
+
if (!Number.isInteger(days) || days < 1)
|
|
63
|
+
throw new AdminRequestError(HTTP_BAD_REQUEST, "days must be a whole number of at least 1");
|
|
64
|
+
await services.audit.setRetentionDays(days);
|
|
65
|
+
return { status: HTTP_OK, body: { days: days }, audit: { target: "audit", parameters: { days: days } } };
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* The audit filters of a query string.
|
|
69
|
+
* @param query The query.
|
|
70
|
+
* @returns The filters given.
|
|
71
|
+
* @throws AdminRequestError 400 for a result other than ok or failed.
|
|
72
|
+
*/
|
|
73
|
+
function auditQueryOf(query) {
|
|
74
|
+
const filters = {};
|
|
75
|
+
const result = query.get("result");
|
|
76
|
+
if (result != null && result !== "ok" && result !== "failed")
|
|
77
|
+
throw new AdminRequestError(HTTP_BAD_REQUEST, "result must be ok or failed");
|
|
78
|
+
if (result != null)
|
|
79
|
+
filters.result = result;
|
|
80
|
+
for (const name of ["actor", "action", "target", "from", "to"]) {
|
|
81
|
+
const value = query.get(name);
|
|
82
|
+
if (value != null && value !== "")
|
|
83
|
+
filters[name] = value;
|
|
84
|
+
}
|
|
85
|
+
return filters;
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* The page of a query string.
|
|
89
|
+
* @param query The query.
|
|
90
|
+
* @returns The page, from 1.
|
|
91
|
+
*/
|
|
92
|
+
function pageOf(query) {
|
|
93
|
+
const page = Number(query.get("page") ?? 1);
|
|
94
|
+
return (Number.isInteger(page) && page >= 1) ? page : 1;
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* The activity limit of a query string.
|
|
98
|
+
* @param query The query.
|
|
99
|
+
* @returns 1 to MAXIMUM_ACTIVITY_LIMIT.
|
|
100
|
+
*/
|
|
101
|
+
function activityLimitOf(query) {
|
|
102
|
+
const limit = Number(query.get("limit") ?? DEFAULT_ACTIVITY_LIMIT);
|
|
103
|
+
if (!Number.isInteger(limit) || limit < 1)
|
|
104
|
+
return DEFAULT_ACTIVITY_LIMIT;
|
|
105
|
+
return Math.min(limit, MAXIMUM_ACTIVITY_LIMIT);
|
|
106
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { type AdminApiRouter } from "./AdminApiRouter.js";
|
|
2
|
+
import type { AdminServices } from "./AdminServices.js";
|
|
3
|
+
/**
|
|
4
|
+
* Registers the auth-tools/* routes.
|
|
5
|
+
* @param router The admin API router.
|
|
6
|
+
* @param services The shared services.
|
|
7
|
+
*/
|
|
8
|
+
export declare function registerAuthToolRoutes(router: AdminApiRouter, services: AdminServices): void;
|
|
9
|
+
/**
|
|
10
|
+
* Registers this instance's reloadAuthKeys fleet command (R15).
|
|
11
|
+
* @param services The shared services.
|
|
12
|
+
*/
|
|
13
|
+
export declare function registerAuthToolCommands(services: AdminServices): void;
|