@oneuptime/common 12.0.27 → 12.0.29
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/Models/AnalyticsModels/RumSessionChunk.ts +52 -0
- package/Models/DatabaseModels/CloudResource.ts +17 -1
- package/Models/DatabaseModels/DashboardDomain.ts +43 -0
- package/Models/DatabaseModels/MonitorTemplate.ts +15 -3
- package/Models/DatabaseModels/RumApplication.ts +35 -2
- package/Models/DatabaseModels/StatusPageDomain.ts +43 -0
- package/Server/API/DashboardDomainAPI.ts +74 -0
- package/Server/API/MicrosoftTeamsAPI.ts +81 -180
- package/Server/API/StatusPageDomainAPI.ts +74 -0
- package/Server/API/TelemetryAPI.ts +104 -15
- package/Server/Infrastructure/Postgres/SchemaMigrations/1790200000000-AllowNullMonitorNameOnMonitorTemplate.ts +43 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1790300000000-AddCertificateReissueRequestedAtToDomains.ts +40 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +4 -0
- package/Server/Middleware/ProjectAuthorization.ts +6 -1
- package/Server/Services/DashboardDomainService.ts +131 -0
- package/Server/Services/LogPipelineProcessorService.ts +88 -0
- package/Server/Services/MonitorTemplateService.ts +70 -11
- package/Server/Services/StatusPageDomainService.ts +163 -3
- package/Server/Services/StatusPagePrivateUserSessionService.ts +197 -56
- package/Server/Types/Domain.ts +17 -0
- package/Server/Utils/APIKey/AccessPermission.ts +24 -0
- package/Server/Utils/Greenlock/Greenlock.ts +156 -58
- package/Server/Utils/LogPipelineProcessorValidation.ts +112 -0
- package/Server/Utils/SecurityEvent/GoogleSecOps/GoogleSecOpsClient.ts +1094 -45
- package/Server/Utils/SecurityEvent/GoogleSecOps/GoogleSecOpsPoller.ts +153 -17
- package/Server/Utils/SessionReplay/SessionReplayGateCache.ts +12 -5
- package/Server/Utils/SessionReplay/SessionReplayIdentity.ts +102 -0
- package/Server/Utils/SessionReplay/SessionReplayReadService.ts +11 -2
- package/Server/Utils/Workspace/MicrosoftTeams/Actions/Alert.ts +23 -11
- package/Server/Utils/Workspace/MicrosoftTeams/Actions/Auth.ts +0 -170
- package/Server/Utils/Workspace/MicrosoftTeams/Actions/Authorization.ts +158 -0
- package/Server/Utils/Workspace/MicrosoftTeams/Actions/Incident.ts +23 -11
- package/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.ts +35 -13
- package/Tests/App/Dashboard/MonitorTemplateColumnAndFacet.test.tsx +505 -0
- package/Tests/App/StatusPage/StatusPageLoginCodeBootstrap.test.ts +106 -0
- package/Tests/App/StatusPage/StatusPageLoginCodeUtil.test.ts +84 -0
- package/Tests/App/StatusPage/StatusPageRedirectSafety.test.ts +119 -0
- package/Tests/Models/DatabaseModels/SessionReplayModels.test.ts +81 -0
- package/Tests/Models/MonitorTemplateMonitorNameColumn.test.ts +258 -0
- package/Tests/Server/API/CustomDomainReissueSslAPI.test.ts +469 -0
- package/Tests/Server/API/MicrosoftTeamsBotMessagesEndpoint.test.ts +729 -0
- package/Tests/Server/API/MicrosoftTeamsBotTestEndpoint.test.ts +26 -0
- package/Tests/Server/API/MicrosoftTeamsLegacyWebhookRemoval.test.ts +402 -0
- package/Tests/Server/API/SessionReplayAPI.test.ts +232 -0
- package/Tests/Server/Middleware/ProjectAuthorizationApiKeyMiddleware.test.ts +4 -0
- package/Tests/Server/Services/AddCertificateReissueRequestedAtToDomainsMigration.test.ts +287 -0
- package/Tests/Server/Services/CustomDomainCertificateReissue.test.ts +477 -0
- package/Tests/Server/Services/LogPipelineProcessorSaveValidation.test.ts +249 -0
- package/Tests/Server/Services/MonitorTemplateServiceBulkSync.test.ts +549 -0
- package/Tests/Server/Services/MonitorTemplateServiceNetworkDeviceSync.test.ts +16 -3
- package/Tests/Server/Services/NetworkDeviceAutoImportRuleEngineService.test.ts +57 -0
- package/Tests/Server/Services/SecurityEventLastErrorColumnWidth.test.ts +151 -15
- package/Tests/Server/Services/StatusPagePrivateUserSessionService.test.ts +587 -0
- package/Tests/Server/Types/Database/Permissions/ApiKeyFileAccess.test.ts +286 -0
- package/Tests/Server/Types/Database/Permissions/MonitorTemplateColumnAccess.test.ts +328 -0
- package/Tests/Server/Types/Database/QueryHelperFindWithSameTextAndIsNull.test.ts +83 -0
- package/Tests/Server/Utils/AI/SRE/ConfidenceSignal.test.ts +377 -0
- package/Tests/Server/Utils/APIKey/AccessPermission.test.ts +16 -1
- package/Tests/Server/Utils/Greenlock/CertificateRenewalSchedule.test.ts +250 -0
- package/Tests/Server/Utils/LogPipelineProcessorValidation.test.ts +198 -0
- package/Tests/Server/Utils/SecurityEvent/ConnectorErrorMessage.test.ts +48 -9
- package/Tests/Server/Utils/SecurityEvent/GoogleSecOpsAuth.test.ts +726 -0
- package/Tests/Server/Utils/SecurityEvent/GoogleSecOpsClient.test.ts +74 -11
- package/Tests/Server/Utils/SecurityEvent/GoogleSecOpsErrorHandling.test.ts +1107 -0
- package/Tests/Server/Utils/SecurityEvent/GoogleSecOpsPoller.test.ts +20 -3
- package/Tests/Server/Utils/SecurityEvent/GoogleSecOpsPollerFailureTaxonomy.test.ts +51 -11
- package/Tests/Server/Utils/SecurityEvent/GoogleSecOpsPollerHardening.test.ts +1246 -0
- package/Tests/Server/Utils/SecurityEvent/GoogleSecOpsRequestContract.test.ts +961 -0
- package/Tests/Server/Utils/SecurityEvent/GoogleSecOpsResponseParsing.test.ts +653 -0
- package/Tests/Server/Utils/Workspace/MicrosoftTeamsActionAuthorization.test.ts +880 -0
- package/Tests/Types/API/HTTPResponse.test.ts +175 -0
- package/Tests/Types/API/Route.test.ts +36 -4
- package/Tests/Types/Database/DatabaseProperty.test.ts +171 -0
- package/Tests/Types/DateLocalShortDateTimeString.test.ts +518 -0
- package/Tests/Types/DateLocalTimeString.test.ts +259 -0
- package/Tests/Types/DateUserPrefers12HourFormat.test.ts +316 -0
- package/Tests/UI/Components/LogsHistogram.test.tsx +41 -2
- package/Tests/UI/Components/LogsViewerEmptyStateTimeRange.test.tsx +212 -0
- package/Tests/UI/Components/TelemetryChartClockFormat.test.tsx +359 -0
- package/Tests/UI/Components/TimeRangePickerClockFormat.test.tsx +422 -0
- package/Tests/UI/Components/TimeRangePickerDropdown.test.tsx +146 -8
- package/Tests/UI/MicrosoftTeams/MicrosoftTeamsMessagingEndpointGuidance.test.tsx +520 -0
- package/Tests/UI/Utils/Navigation.test.ts +82 -0
- package/Tests/Utils/Array.test.ts +100 -0
- package/Tests/Utils/CertificateReissue.test.ts +225 -0
- package/Tests/Utils/Grok/Grok.test.ts +492 -0
- package/Tests/Utils/Monitor/NetworkDeviceMonitorTemplateUtil.test.ts +181 -13
- package/Types/API/Route.ts +20 -5
- package/Types/Date.ts +143 -13
- package/Types/Rum/SessionReplay.ts +76 -1
- package/UI/Components/Date/TimeRangePickerDropdown.tsx +9 -5
- package/UI/Components/LogsViewer/LogsViewer.tsx +12 -7
- package/UI/Components/LogsViewer/components/HistogramTooltip.tsx +18 -17
- package/UI/Components/LogsViewer/components/LogsAnalyticsView.tsx +21 -28
- package/UI/Components/LogsViewer/components/LogsHistogram.tsx +2 -4
- package/UI/Components/TelemetryViewer/components/TelemetryHistogram.tsx +2 -4
- package/UI/Components/TelemetryViewer/components/TelemetryHistogramTooltip.tsx +17 -16
- package/UI/Utils/Navigation.ts +42 -0
- package/Utils/Array.ts +37 -0
- package/Utils/CertificateReissue.ts +128 -0
- package/Utils/Grok/Grok.ts +461 -0
- package/Utils/Grok/GrokPatterns.ts +118 -0
- package/Utils/Monitor/NetworkDeviceMonitorTemplateUtil.ts +58 -6
- package/build/dist/Models/AnalyticsModels/RumSessionChunk.js +48 -0
- package/build/dist/Models/AnalyticsModels/RumSessionChunk.js.map +1 -1
- package/build/dist/Models/DatabaseModels/CloudResource.js +19 -2
- package/build/dist/Models/DatabaseModels/CloudResource.js.map +1 -1
- package/build/dist/Models/DatabaseModels/DashboardDomain.js +44 -0
- package/build/dist/Models/DatabaseModels/DashboardDomain.js.map +1 -1
- package/build/dist/Models/DatabaseModels/MonitorTemplate.js +17 -4
- package/build/dist/Models/DatabaseModels/MonitorTemplate.js.map +1 -1
- package/build/dist/Models/DatabaseModels/RumApplication.js +37 -3
- package/build/dist/Models/DatabaseModels/RumApplication.js.map +1 -1
- package/build/dist/Models/DatabaseModels/StatusPageDomain.js +44 -0
- package/build/dist/Models/DatabaseModels/StatusPageDomain.js.map +1 -1
- package/build/dist/Server/API/DashboardDomainAPI.js +43 -1
- package/build/dist/Server/API/DashboardDomainAPI.js.map +1 -1
- package/build/dist/Server/API/MicrosoftTeamsAPI.js +74 -118
- package/build/dist/Server/API/MicrosoftTeamsAPI.js.map +1 -1
- package/build/dist/Server/API/StatusPageDomainAPI.js +43 -1
- package/build/dist/Server/API/StatusPageDomainAPI.js.map +1 -1
- package/build/dist/Server/API/TelemetryAPI.js +67 -14
- package/build/dist/Server/API/TelemetryAPI.js.map +1 -1
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1790200000000-AllowNullMonitorNameOnMonitorTemplate.js +34 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1790200000000-AllowNullMonitorNameOnMonitorTemplate.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1790300000000-AddCertificateReissueRequestedAtToDomains.js +28 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1790300000000-AddCertificateReissueRequestedAtToDomains.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +4 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
- package/build/dist/Server/Middleware/ProjectAuthorization.js +6 -1
- package/build/dist/Server/Middleware/ProjectAuthorization.js.map +1 -1
- package/build/dist/Server/Services/DashboardDomainService.js +104 -0
- package/build/dist/Server/Services/DashboardDomainService.js.map +1 -1
- package/build/dist/Server/Services/LogPipelineProcessorService.js +65 -0
- package/build/dist/Server/Services/LogPipelineProcessorService.js.map +1 -1
- package/build/dist/Server/Services/MonitorTemplateService.js +57 -11
- package/build/dist/Server/Services/MonitorTemplateService.js.map +1 -1
- package/build/dist/Server/Services/StatusPageDomainService.js +132 -3
- package/build/dist/Server/Services/StatusPageDomainService.js.map +1 -1
- package/build/dist/Server/Services/StatusPagePrivateUserSessionService.js +112 -32
- package/build/dist/Server/Services/StatusPagePrivateUserSessionService.js.map +1 -1
- package/build/dist/Server/Types/Domain.js +16 -0
- package/build/dist/Server/Types/Domain.js.map +1 -1
- package/build/dist/Server/Utils/APIKey/AccessPermission.js +24 -0
- package/build/dist/Server/Utils/APIKey/AccessPermission.js.map +1 -1
- package/build/dist/Server/Utils/Greenlock/Greenlock.js +110 -35
- package/build/dist/Server/Utils/Greenlock/Greenlock.js.map +1 -1
- package/build/dist/Server/Utils/LogPipelineProcessorValidation.js +77 -0
- package/build/dist/Server/Utils/LogPipelineProcessorValidation.js.map +1 -0
- package/build/dist/Server/Utils/SecurityEvent/GoogleSecOps/GoogleSecOpsClient.js +777 -36
- package/build/dist/Server/Utils/SecurityEvent/GoogleSecOps/GoogleSecOpsClient.js.map +1 -1
- package/build/dist/Server/Utils/SecurityEvent/GoogleSecOps/GoogleSecOpsPoller.js +108 -17
- package/build/dist/Server/Utils/SecurityEvent/GoogleSecOps/GoogleSecOpsPoller.js.map +1 -1
- package/build/dist/Server/Utils/SessionReplay/SessionReplayGateCache.js.map +1 -1
- package/build/dist/Server/Utils/SessionReplay/SessionReplayIdentity.js +59 -0
- package/build/dist/Server/Utils/SessionReplay/SessionReplayIdentity.js.map +1 -0
- package/build/dist/Server/Utils/SessionReplay/SessionReplayReadService.js +9 -2
- package/build/dist/Server/Utils/SessionReplay/SessionReplayReadService.js.map +1 -1
- package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/Alert.js +17 -6
- package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/Alert.js.map +1 -1
- package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/Auth.js +0 -159
- package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/Auth.js.map +1 -1
- package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/Authorization.js +128 -0
- package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/Authorization.js.map +1 -0
- package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/Incident.js +17 -6
- package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/Incident.js.map +1 -1
- package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.js +25 -13
- package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.js.map +1 -1
- package/build/dist/Types/API/Route.js +16 -5
- package/build/dist/Types/API/Route.js.map +1 -1
- package/build/dist/Types/Date.js +115 -13
- package/build/dist/Types/Date.js.map +1 -1
- package/build/dist/Types/Rum/SessionReplay.js +38 -0
- package/build/dist/Types/Rum/SessionReplay.js.map +1 -1
- package/build/dist/UI/Components/Date/TimeRangePickerDropdown.js +9 -5
- package/build/dist/UI/Components/Date/TimeRangePickerDropdown.js.map +1 -1
- package/build/dist/UI/Components/LogsViewer/LogsViewer.js +12 -7
- package/build/dist/UI/Components/LogsViewer/LogsViewer.js.map +1 -1
- package/build/dist/UI/Components/LogsViewer/components/HistogramTooltip.js +15 -14
- package/build/dist/UI/Components/LogsViewer/components/HistogramTooltip.js.map +1 -1
- package/build/dist/UI/Components/LogsViewer/components/LogsAnalyticsView.js +17 -22
- package/build/dist/UI/Components/LogsViewer/components/LogsAnalyticsView.js.map +1 -1
- package/build/dist/UI/Components/LogsViewer/components/LogsHistogram.js +2 -4
- package/build/dist/UI/Components/LogsViewer/components/LogsHistogram.js.map +1 -1
- package/build/dist/UI/Components/TelemetryViewer/components/TelemetryHistogram.js +2 -4
- package/build/dist/UI/Components/TelemetryViewer/components/TelemetryHistogram.js.map +1 -1
- package/build/dist/UI/Components/TelemetryViewer/components/TelemetryHistogramTooltip.js +15 -14
- package/build/dist/UI/Components/TelemetryViewer/components/TelemetryHistogramTooltip.js.map +1 -1
- package/build/dist/UI/Utils/Navigation.js +30 -0
- package/build/dist/UI/Utils/Navigation.js.map +1 -1
- package/build/dist/Utils/Array.js +25 -0
- package/build/dist/Utils/Array.js.map +1 -1
- package/build/dist/Utils/CertificateReissue.js +97 -0
- package/build/dist/Utils/CertificateReissue.js.map +1 -0
- package/build/dist/Utils/Grok/Grok.js +324 -0
- package/build/dist/Utils/Grok/Grok.js.map +1 -0
- package/build/dist/Utils/Grok/GrokPatterns.js +112 -0
- package/build/dist/Utils/Grok/GrokPatterns.js.map +1 -0
- package/build/dist/Utils/Monitor/NetworkDeviceMonitorTemplateUtil.js +63 -3
- package/build/dist/Utils/Monitor/NetworkDeviceMonitorTemplateUtil.js.map +1 -1
- package/package.json +1 -1
- package/UI/Utils/JsonWebToken.ts +0 -14
- package/build/dist/UI/Utils/JsonWebToken.js +0 -10
- package/build/dist/UI/Utils/JsonWebToken.js.map +0 -1
|
@@ -238,6 +238,18 @@ describe("GET /microsoft-bot/test", () => {
|
|
|
238
238
|
"the installed Teams app package belonging to this deployment",
|
|
239
239
|
["installed", "this deployment"],
|
|
240
240
|
],
|
|
241
|
+
/*
|
|
242
|
+
* Reachability is listed apart from the endpoint being *configured*
|
|
243
|
+
* because the two fail separately and are indistinguishable from in
|
|
244
|
+
* here. A correctly configured endpoint on a deployment Azure cannot
|
|
245
|
+
* dial produces working outbound alerts and a completely dead bot —
|
|
246
|
+
* which reads as a half-broken integration rather than a network
|
|
247
|
+
* problem, and is how this endpoint's blessing gets misread.
|
|
248
|
+
*/
|
|
249
|
+
[
|
|
250
|
+
"Azure Bot Service being able to reach this deployment",
|
|
251
|
+
["reach this deployment", "public internet"],
|
|
252
|
+
],
|
|
241
253
|
])(
|
|
242
254
|
"notVerified explicitly disclaims %s",
|
|
243
255
|
async (_label: string, requiredFragments: Array<string>) => {
|
|
@@ -257,6 +269,20 @@ describe("GET /microsoft-bot/test", () => {
|
|
|
257
269
|
},
|
|
258
270
|
);
|
|
259
271
|
|
|
272
|
+
test("notVerified warns that a working alert does not test reachability", async () => {
|
|
273
|
+
/*
|
|
274
|
+
* The trap this endpoint exists to avoid: an admin whose alerts arrive
|
|
275
|
+
* in Teams reads that as proof the bot is wired up, when outbound
|
|
276
|
+
* notifications go out over a connection OneUptime opens itself and
|
|
277
|
+
* never touch the inbound path the bot depends on.
|
|
278
|
+
*/
|
|
279
|
+
const response: InvokedResponse = await callBotTest();
|
|
280
|
+
const joined: string = joinStrings(response.body["notVerified"]);
|
|
281
|
+
|
|
282
|
+
expect(joined).toContain("outbound notifications work without it");
|
|
283
|
+
expect(joined).toContain("working alert does not test this");
|
|
284
|
+
});
|
|
285
|
+
|
|
260
286
|
test("nextStep names the client id and tells the admin to compare it against the installed app's bot id", async () => {
|
|
261
287
|
const response: InvokedResponse = await callBotTest();
|
|
262
288
|
const nextStep: string = response.body["nextStep"] as string;
|
|
@@ -0,0 +1,402 @@
|
|
|
1
|
+
import { afterEach, describe, expect, jest, test } from "@jest/globals";
|
|
2
|
+
import type { TurnContext } from "botbuilder";
|
|
3
|
+
import type { SpyInstance } from "jest-mock";
|
|
4
|
+
import MicrosoftTeamsAPI from "../../../Server/API/MicrosoftTeamsAPI";
|
|
5
|
+
import WorkspaceProjectAuthToken from "../../../Models/DatabaseModels/WorkspaceProjectAuthToken";
|
|
6
|
+
import AccessTokenService from "../../../Server/Services/AccessTokenService";
|
|
7
|
+
import AlertService from "../../../Server/Services/AlertService";
|
|
8
|
+
import IncidentService from "../../../Server/Services/IncidentService";
|
|
9
|
+
import TeamMemberService from "../../../Server/Services/TeamMemberService";
|
|
10
|
+
import WorkspaceProjectAuthTokenService from "../../../Server/Services/WorkspaceProjectAuthTokenService";
|
|
11
|
+
import WorkspaceUserAuthTokenService from "../../../Server/Services/WorkspaceUserAuthTokenService";
|
|
12
|
+
import {
|
|
13
|
+
ExpressRequest,
|
|
14
|
+
ExpressResponse,
|
|
15
|
+
ExpressRouter,
|
|
16
|
+
} from "../../../Server/Utils/Express";
|
|
17
|
+
import {
|
|
18
|
+
MicrosoftTeamsAlertActionType,
|
|
19
|
+
MicrosoftTeamsIncidentActionType,
|
|
20
|
+
MicrosoftTeamsMonitorActionType,
|
|
21
|
+
} from "../../../Server/Utils/Workspace/MicrosoftTeams/Actions/ActionTypes";
|
|
22
|
+
import MicrosoftTeamsAuthAction from "../../../Server/Utils/Workspace/MicrosoftTeams/Actions/Auth";
|
|
23
|
+
import MicrosoftTeamsIncidentActions from "../../../Server/Utils/Workspace/MicrosoftTeams/Actions/Incident";
|
|
24
|
+
import MicrosoftTeamsMonitorActions from "../../../Server/Utils/Workspace/MicrosoftTeams/Actions/Monitor";
|
|
25
|
+
import MicrosoftTeamsUtil from "../../../Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams";
|
|
26
|
+
import DatabaseCommonInteractionProps from "../../../Types/BaseDatabase/DatabaseCommonInteractionProps";
|
|
27
|
+
import { JSONObject } from "../../../Types/JSON";
|
|
28
|
+
import ObjectID from "../../../Types/ObjectID";
|
|
29
|
+
|
|
30
|
+
jest.setTimeout(30_000);
|
|
31
|
+
|
|
32
|
+
type ExpressRouterLayer = {
|
|
33
|
+
route?: {
|
|
34
|
+
path: string;
|
|
35
|
+
methods: Record<string, boolean>;
|
|
36
|
+
stack: Array<{
|
|
37
|
+
handle: ExpressRouteHandler;
|
|
38
|
+
}>;
|
|
39
|
+
};
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
type ExpressRouteHandler = (
|
|
43
|
+
request: ExpressRequest,
|
|
44
|
+
response: ExpressResponse,
|
|
45
|
+
) => Promise<void> | void;
|
|
46
|
+
|
|
47
|
+
const LEGACY_WEBHOOK_PATH: string = "/microsoft-teams/webhook";
|
|
48
|
+
const BOT_FRAMEWORK_PATH: string = "/microsoft-bot/messages";
|
|
49
|
+
|
|
50
|
+
const getRouterLayers: () => Array<ExpressRouterLayer> =
|
|
51
|
+
(): Array<ExpressRouterLayer> => {
|
|
52
|
+
const router: ExpressRouter = new MicrosoftTeamsAPI().getRouter();
|
|
53
|
+
return (
|
|
54
|
+
router as unknown as {
|
|
55
|
+
stack: Array<ExpressRouterLayer>;
|
|
56
|
+
}
|
|
57
|
+
).stack;
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
const dispatchThroughRouter: (
|
|
61
|
+
path: string,
|
|
62
|
+
body: JSONObject,
|
|
63
|
+
) => Promise<void> = (path: string, body: JSONObject): Promise<void> => {
|
|
64
|
+
return new Promise<void>(
|
|
65
|
+
(resolve: () => void, reject: (error: Error) => void): void => {
|
|
66
|
+
const router: ExpressRouter = new MicrosoftTeamsAPI().getRouter();
|
|
67
|
+
const request: ExpressRequest = {
|
|
68
|
+
method: "POST",
|
|
69
|
+
url: path,
|
|
70
|
+
originalUrl: path,
|
|
71
|
+
baseUrl: "",
|
|
72
|
+
headers: {},
|
|
73
|
+
body: body,
|
|
74
|
+
} as unknown as ExpressRequest;
|
|
75
|
+
const response: ExpressResponse = {} as ExpressResponse;
|
|
76
|
+
|
|
77
|
+
router(request, response, (error?: unknown): void => {
|
|
78
|
+
if (error) {
|
|
79
|
+
reject(error as Error);
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
resolve();
|
|
84
|
+
});
|
|
85
|
+
},
|
|
86
|
+
);
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
const getPostRouteHandler: (path: string) => ExpressRouteHandler = (
|
|
90
|
+
path: string,
|
|
91
|
+
): ExpressRouteHandler => {
|
|
92
|
+
const layer: ExpressRouterLayer | undefined = getRouterLayers().find(
|
|
93
|
+
(routerLayer: ExpressRouterLayer): boolean => {
|
|
94
|
+
return (
|
|
95
|
+
routerLayer.route?.path === path &&
|
|
96
|
+
routerLayer.route.methods["post"] === true
|
|
97
|
+
);
|
|
98
|
+
},
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
if (!layer?.route?.stack[0]) {
|
|
102
|
+
throw new Error(`POST route handler not found for ${path}`);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return layer.route.stack[0].handle;
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
const forgedInvokeBody: (action: string) => JSONObject = (
|
|
109
|
+
action: string,
|
|
110
|
+
): JSONObject => {
|
|
111
|
+
return {
|
|
112
|
+
type: "invoke",
|
|
113
|
+
from: {
|
|
114
|
+
id: "attacker-controlled-linked-teams-user-id",
|
|
115
|
+
},
|
|
116
|
+
conversation: {
|
|
117
|
+
id: "attacker-controlled-conversation-id",
|
|
118
|
+
},
|
|
119
|
+
channelData: {
|
|
120
|
+
team: {
|
|
121
|
+
id: "any-non-empty-team-id",
|
|
122
|
+
},
|
|
123
|
+
tenant: {
|
|
124
|
+
id: "target-connected-tenant-id",
|
|
125
|
+
},
|
|
126
|
+
},
|
|
127
|
+
value: {
|
|
128
|
+
action: action,
|
|
129
|
+
actionValue: "11111111-2222-4333-8444-555555555555",
|
|
130
|
+
},
|
|
131
|
+
};
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
afterEach(() => {
|
|
135
|
+
jest.restoreAllMocks();
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
describe("Microsoft Teams inbound route authentication", () => {
|
|
139
|
+
test("does not register the unauthenticated legacy webhook for any method", () => {
|
|
140
|
+
const legacyLayers: Array<ExpressRouterLayer> = getRouterLayers().filter(
|
|
141
|
+
(layer: ExpressRouterLayer): boolean => {
|
|
142
|
+
return layer.route?.path === LEGACY_WEBHOOK_PATH;
|
|
143
|
+
},
|
|
144
|
+
);
|
|
145
|
+
|
|
146
|
+
expect(legacyLayers).toEqual([]);
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
test("keeps exactly one POST route for the authenticated Bot Framework adapter", () => {
|
|
150
|
+
/*
|
|
151
|
+
* Counted by method rather than by layer. What must stay unique is the
|
|
152
|
+
* inbound POST — a second one is how the unauthenticated legacy webhook
|
|
153
|
+
* got in, and this test exists to keep it out. The path itself carries a
|
|
154
|
+
* second layer on purpose since the 405 diagnostic landed: a GET handler
|
|
155
|
+
* that answers a browser probe instead of letting it fall through to the
|
|
156
|
+
* catch-all 404, which is what convinced the reporter of #3488 that the
|
|
157
|
+
* route had been dropped from the build.
|
|
158
|
+
*
|
|
159
|
+
* So: exactly one layer accepts POST, and no other layer on this path
|
|
160
|
+
* accepts POST either. The second assertion is the one doing the security
|
|
161
|
+
* work — a layer registered with router.all() would accept POST while
|
|
162
|
+
* reporting no `post` key of its own.
|
|
163
|
+
*/
|
|
164
|
+
const botFrameworkLayers: Array<ExpressRouterLayer> =
|
|
165
|
+
getRouterLayers().filter((layer: ExpressRouterLayer): boolean => {
|
|
166
|
+
return layer.route?.path === BOT_FRAMEWORK_PATH;
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
const postLayers: Array<ExpressRouterLayer> = botFrameworkLayers.filter(
|
|
170
|
+
(layer: ExpressRouterLayer): boolean => {
|
|
171
|
+
return layer.route?.methods["post"] === true;
|
|
172
|
+
},
|
|
173
|
+
);
|
|
174
|
+
|
|
175
|
+
expect(postLayers).toHaveLength(1);
|
|
176
|
+
expect(postLayers[0]!.route!.methods).toEqual({ post: true });
|
|
177
|
+
|
|
178
|
+
const otherLayerMethods: Array<string> = botFrameworkLayers
|
|
179
|
+
.filter((layer: ExpressRouterLayer): boolean => {
|
|
180
|
+
return layer !== postLayers[0];
|
|
181
|
+
})
|
|
182
|
+
.flatMap((layer: ExpressRouterLayer): Array<string> => {
|
|
183
|
+
return Object.keys(layer.route!.methods);
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
expect(otherLayerMethods).not.toContain("post");
|
|
187
|
+
expect(otherLayerMethods).not.toContain("_all");
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
test.each([
|
|
191
|
+
MicrosoftTeamsIncidentActionType.AckIncident,
|
|
192
|
+
MicrosoftTeamsIncidentActionType.ResolveIncident,
|
|
193
|
+
MicrosoftTeamsAlertActionType.AckAlert,
|
|
194
|
+
MicrosoftTeamsAlertActionType.ResolveAlert,
|
|
195
|
+
])(
|
|
196
|
+
"falls through without lookup or mutation for unsigned forged %s",
|
|
197
|
+
async (action: string) => {
|
|
198
|
+
const projectLookupSpy: SpyInstance<
|
|
199
|
+
typeof WorkspaceProjectAuthTokenService.findOneBy
|
|
200
|
+
> = jest
|
|
201
|
+
.spyOn(WorkspaceProjectAuthTokenService, "findOneBy")
|
|
202
|
+
.mockResolvedValue(null);
|
|
203
|
+
const userLookupSpy: SpyInstance<
|
|
204
|
+
typeof WorkspaceUserAuthTokenService.findOneBy
|
|
205
|
+
> = jest
|
|
206
|
+
.spyOn(WorkspaceUserAuthTokenService, "findOneBy")
|
|
207
|
+
.mockResolvedValue(null);
|
|
208
|
+
const acknowledgeIncidentSpy: SpyInstance<
|
|
209
|
+
typeof IncidentService.acknowledgeIncident
|
|
210
|
+
> = jest
|
|
211
|
+
.spyOn(IncidentService, "acknowledgeIncident")
|
|
212
|
+
.mockRejectedValue(new Error("unexpected incident mutation"));
|
|
213
|
+
const resolveIncidentSpy: SpyInstance<
|
|
214
|
+
typeof IncidentService.resolveIncident
|
|
215
|
+
> = jest
|
|
216
|
+
.spyOn(IncidentService, "resolveIncident")
|
|
217
|
+
.mockRejectedValue(new Error("unexpected incident mutation"));
|
|
218
|
+
const acknowledgeAlertSpy: SpyInstance<
|
|
219
|
+
typeof AlertService.acknowledgeAlert
|
|
220
|
+
> = jest
|
|
221
|
+
.spyOn(AlertService, "acknowledgeAlert")
|
|
222
|
+
.mockRejectedValue(new Error("unexpected alert mutation"));
|
|
223
|
+
const resolveAlertSpy: SpyInstance<typeof AlertService.resolveAlert> =
|
|
224
|
+
jest
|
|
225
|
+
.spyOn(AlertService, "resolveAlert")
|
|
226
|
+
.mockRejectedValue(new Error("unexpected alert mutation"));
|
|
227
|
+
const botFrameworkSpy: SpyInstance<
|
|
228
|
+
typeof MicrosoftTeamsUtil.processBotActivity
|
|
229
|
+
> = jest.spyOn(MicrosoftTeamsUtil, "processBotActivity");
|
|
230
|
+
|
|
231
|
+
await dispatchThroughRouter(
|
|
232
|
+
LEGACY_WEBHOOK_PATH,
|
|
233
|
+
forgedInvokeBody(action),
|
|
234
|
+
);
|
|
235
|
+
|
|
236
|
+
expect(projectLookupSpy).not.toHaveBeenCalled();
|
|
237
|
+
expect(userLookupSpy).not.toHaveBeenCalled();
|
|
238
|
+
expect(acknowledgeIncidentSpy).not.toHaveBeenCalled();
|
|
239
|
+
expect(resolveIncidentSpy).not.toHaveBeenCalled();
|
|
240
|
+
expect(acknowledgeAlertSpy).not.toHaveBeenCalled();
|
|
241
|
+
expect(resolveAlertSpy).not.toHaveBeenCalled();
|
|
242
|
+
expect(botFrameworkSpy).not.toHaveBeenCalled();
|
|
243
|
+
},
|
|
244
|
+
);
|
|
245
|
+
|
|
246
|
+
test("delegates the supported endpoint to the Bot Framework adapter", async () => {
|
|
247
|
+
const botFrameworkSpy: SpyInstance<
|
|
248
|
+
typeof MicrosoftTeamsUtil.processBotActivity
|
|
249
|
+
> = jest
|
|
250
|
+
.spyOn(MicrosoftTeamsUtil, "processBotActivity")
|
|
251
|
+
.mockResolvedValue();
|
|
252
|
+
const request: ExpressRequest = {
|
|
253
|
+
body: forgedInvokeBody(MicrosoftTeamsIncidentActionType.ResolveIncident),
|
|
254
|
+
} as unknown as ExpressRequest;
|
|
255
|
+
const response: ExpressResponse = {} as ExpressResponse;
|
|
256
|
+
const handler: ExpressRouteHandler =
|
|
257
|
+
getPostRouteHandler(BOT_FRAMEWORK_PATH);
|
|
258
|
+
|
|
259
|
+
await handler(request, response);
|
|
260
|
+
|
|
261
|
+
expect(botFrameworkSpy).toHaveBeenCalledTimes(1);
|
|
262
|
+
expect(botFrameworkSpy).toHaveBeenCalledWith(request, response);
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
test("adds the linked user's accepted team IDs to the Bot invoke permission context", async () => {
|
|
266
|
+
const projectId: ObjectID = ObjectID.generate();
|
|
267
|
+
const userId: ObjectID = ObjectID.generate();
|
|
268
|
+
const teamIds: Array<ObjectID> = [ObjectID.generate(), ObjectID.generate()];
|
|
269
|
+
const projectAuth: WorkspaceProjectAuthToken =
|
|
270
|
+
new WorkspaceProjectAuthToken();
|
|
271
|
+
projectAuth.projectId = projectId;
|
|
272
|
+
|
|
273
|
+
const databaseProps: DatabaseCommonInteractionProps = {
|
|
274
|
+
userId: userId,
|
|
275
|
+
tenantId: projectId,
|
|
276
|
+
};
|
|
277
|
+
|
|
278
|
+
jest
|
|
279
|
+
.spyOn(MicrosoftTeamsUtil, "resolveProjectByTenantId")
|
|
280
|
+
.mockResolvedValue({
|
|
281
|
+
projectAuth: projectAuth,
|
|
282
|
+
isAmbiguous: false,
|
|
283
|
+
candidateProjectIds: [projectId],
|
|
284
|
+
});
|
|
285
|
+
jest
|
|
286
|
+
.spyOn(MicrosoftTeamsAuthAction, "getOneUptimeUserIdFromTeamsUserId")
|
|
287
|
+
.mockResolvedValue(userId);
|
|
288
|
+
jest
|
|
289
|
+
.spyOn(
|
|
290
|
+
AccessTokenService,
|
|
291
|
+
"getDatabaseCommonInteractionPropsByUserAndProject",
|
|
292
|
+
)
|
|
293
|
+
.mockResolvedValue(databaseProps);
|
|
294
|
+
const getTeamIdsSpy: SpyInstance<
|
|
295
|
+
typeof TeamMemberService.getTeamIdsForUser
|
|
296
|
+
> = jest
|
|
297
|
+
.spyOn(TeamMemberService, "getTeamIdsForUser")
|
|
298
|
+
.mockResolvedValue(teamIds);
|
|
299
|
+
const handleIncidentSpy: SpyInstance<
|
|
300
|
+
typeof MicrosoftTeamsIncidentActions.handleBotIncidentAction
|
|
301
|
+
> = jest
|
|
302
|
+
.spyOn(MicrosoftTeamsIncidentActions, "handleBotIncidentAction")
|
|
303
|
+
.mockResolvedValue();
|
|
304
|
+
|
|
305
|
+
const turnContext: TurnContext = {
|
|
306
|
+
sendActivity: jest.fn(async (): Promise<void> => {}),
|
|
307
|
+
} as unknown as TurnContext;
|
|
308
|
+
|
|
309
|
+
await MicrosoftTeamsUtil.handleBotInvokeActivity({
|
|
310
|
+
activity: {
|
|
311
|
+
channelData: {
|
|
312
|
+
tenant: {
|
|
313
|
+
id: "connected-tenant-id",
|
|
314
|
+
},
|
|
315
|
+
},
|
|
316
|
+
from: {
|
|
317
|
+
aadObjectId: "linked-teams-user-aad-object-id",
|
|
318
|
+
},
|
|
319
|
+
value: {
|
|
320
|
+
action: MicrosoftTeamsIncidentActionType.AckIncident,
|
|
321
|
+
actionValue: ObjectID.generate().toString(),
|
|
322
|
+
},
|
|
323
|
+
},
|
|
324
|
+
turnContext: turnContext,
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
expect(getTeamIdsSpy).toHaveBeenCalledWith(userId, projectId);
|
|
328
|
+
expect(databaseProps.userTeamIds).toEqual(teamIds);
|
|
329
|
+
expect(handleIncidentSpy).toHaveBeenCalledWith(
|
|
330
|
+
expect.objectContaining({
|
|
331
|
+
projectId: projectId,
|
|
332
|
+
oneUptimeUserId: userId,
|
|
333
|
+
databaseProps: expect.objectContaining({
|
|
334
|
+
userTeamIds: teamIds,
|
|
335
|
+
}),
|
|
336
|
+
turnContext: turnContext,
|
|
337
|
+
}),
|
|
338
|
+
);
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
test("does not load incident or alert permissions for an unrelated Bot action", async () => {
|
|
342
|
+
const projectId: ObjectID = ObjectID.generate();
|
|
343
|
+
const userId: ObjectID = ObjectID.generate();
|
|
344
|
+
const projectAuth: WorkspaceProjectAuthToken =
|
|
345
|
+
new WorkspaceProjectAuthToken();
|
|
346
|
+
projectAuth.projectId = projectId;
|
|
347
|
+
|
|
348
|
+
jest
|
|
349
|
+
.spyOn(MicrosoftTeamsUtil, "resolveProjectByTenantId")
|
|
350
|
+
.mockResolvedValue({
|
|
351
|
+
projectAuth: projectAuth,
|
|
352
|
+
isAmbiguous: false,
|
|
353
|
+
candidateProjectIds: [projectId],
|
|
354
|
+
});
|
|
355
|
+
jest
|
|
356
|
+
.spyOn(MicrosoftTeamsAuthAction, "getOneUptimeUserIdFromTeamsUserId")
|
|
357
|
+
.mockResolvedValue(userId);
|
|
358
|
+
const databasePropsSpy: SpyInstance<
|
|
359
|
+
typeof AccessTokenService.getDatabaseCommonInteractionPropsByUserAndProject
|
|
360
|
+
> = jest
|
|
361
|
+
.spyOn(
|
|
362
|
+
AccessTokenService,
|
|
363
|
+
"getDatabaseCommonInteractionPropsByUserAndProject",
|
|
364
|
+
)
|
|
365
|
+
.mockRejectedValue(new Error("unexpected permission lookup"));
|
|
366
|
+
const getTeamIdsSpy: SpyInstance<
|
|
367
|
+
typeof TeamMemberService.getTeamIdsForUser
|
|
368
|
+
> = jest
|
|
369
|
+
.spyOn(TeamMemberService, "getTeamIdsForUser")
|
|
370
|
+
.mockRejectedValue(new Error("unexpected team lookup"));
|
|
371
|
+
const handleMonitorSpy: SpyInstance<
|
|
372
|
+
typeof MicrosoftTeamsMonitorActions.handleBotMonitorAction
|
|
373
|
+
> = jest
|
|
374
|
+
.spyOn(MicrosoftTeamsMonitorActions, "handleBotMonitorAction")
|
|
375
|
+
.mockResolvedValue();
|
|
376
|
+
const turnContext: TurnContext = {
|
|
377
|
+
sendActivity: jest.fn(async (): Promise<void> => {}),
|
|
378
|
+
} as unknown as TurnContext;
|
|
379
|
+
|
|
380
|
+
await MicrosoftTeamsUtil.handleBotInvokeActivity({
|
|
381
|
+
activity: {
|
|
382
|
+
channelData: {
|
|
383
|
+
tenant: {
|
|
384
|
+
id: "connected-tenant-id",
|
|
385
|
+
},
|
|
386
|
+
},
|
|
387
|
+
from: {
|
|
388
|
+
aadObjectId: "linked-teams-user-aad-object-id",
|
|
389
|
+
},
|
|
390
|
+
value: {
|
|
391
|
+
action: MicrosoftTeamsMonitorActionType.ViewMonitor,
|
|
392
|
+
actionValue: ObjectID.generate().toString(),
|
|
393
|
+
},
|
|
394
|
+
},
|
|
395
|
+
turnContext: turnContext,
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
expect(databasePropsSpy).not.toHaveBeenCalled();
|
|
399
|
+
expect(getTeamIdsSpy).not.toHaveBeenCalled();
|
|
400
|
+
expect(handleMonitorSpy).toHaveBeenCalledTimes(1);
|
|
401
|
+
});
|
|
402
|
+
});
|
|
@@ -7,6 +7,7 @@ import RumSessionReplayViewService from "../../../Server/Services/RumSessionRepl
|
|
|
7
7
|
import RumSessionService from "../../../Server/Services/RumSessionService";
|
|
8
8
|
import RumSessionChunkService from "../../../Server/Services/RumSessionChunkService";
|
|
9
9
|
import { Statement } from "../../../Server/Utils/AnalyticsDatabase/Statement";
|
|
10
|
+
import SessionReplayIdentity from "../../../Server/Utils/SessionReplay/SessionReplayIdentity";
|
|
10
11
|
import RumApplication from "../../../Models/DatabaseModels/RumApplication";
|
|
11
12
|
import RumSessionReplayView from "../../../Models/DatabaseModels/RumSessionReplayView";
|
|
12
13
|
import Label from "../../../Models/DatabaseModels/Label";
|
|
@@ -1086,6 +1087,196 @@ describe("Session replay playback API", () => {
|
|
|
1086
1087
|
expect(query).toContain("timeout_overflow_mode = 'throw'");
|
|
1087
1088
|
});
|
|
1088
1089
|
|
|
1090
|
+
/*
|
|
1091
|
+
* Watching implies listing.
|
|
1092
|
+
*
|
|
1093
|
+
* RumSession's table read ACL contains ReadRumSessionReplayPayload for a
|
|
1094
|
+
* reason it states outright: a role granted only the watch permission
|
|
1095
|
+
* could fetch payloads - the payload routes authorize on it alone - while
|
|
1096
|
+
* being 401'd on the manifest and the list, which is an incoherent grant
|
|
1097
|
+
* rather than a safer one. The list guard did not mirror it, so the
|
|
1098
|
+
* natural support-engineer role ("Watch Session Replays" + "Read RUM
|
|
1099
|
+
* Application") got a permission error on the session list and a silently
|
|
1100
|
+
* missing "Watch what the user saw" card on every exception page.
|
|
1101
|
+
*/
|
|
1102
|
+
test("a caller with only the watch permission can still list sessions", async () => {
|
|
1103
|
+
const principal: {
|
|
1104
|
+
request: JSONObject;
|
|
1105
|
+
databaseProps: DatabaseCommonInteractionProps;
|
|
1106
|
+
} = buildPrincipal({
|
|
1107
|
+
projectId: projectId,
|
|
1108
|
+
userId: userId,
|
|
1109
|
+
permissions: [Permission.ReadRumSessionReplayPayload],
|
|
1110
|
+
});
|
|
1111
|
+
|
|
1112
|
+
mockProps(principal.databaseProps);
|
|
1113
|
+
mockApplication({ id: applicationAId, labelIds: [] });
|
|
1114
|
+
|
|
1115
|
+
const result: CallResult = await callRoute({
|
|
1116
|
+
uri: LIST_ROUTE,
|
|
1117
|
+
request: principal.request,
|
|
1118
|
+
body: { rumApplicationId: applicationAId.toString() },
|
|
1119
|
+
});
|
|
1120
|
+
|
|
1121
|
+
expect(result.deniedWith).toBeUndefined();
|
|
1122
|
+
expect(headerQuerySpy).toHaveBeenCalledTimes(1);
|
|
1123
|
+
});
|
|
1124
|
+
|
|
1125
|
+
test("a Viewer still cannot list sessions", async () => {
|
|
1126
|
+
const principal: {
|
|
1127
|
+
request: JSONObject;
|
|
1128
|
+
databaseProps: DatabaseCommonInteractionProps;
|
|
1129
|
+
} = buildPrincipal({
|
|
1130
|
+
projectId: projectId,
|
|
1131
|
+
userId: userId,
|
|
1132
|
+
permissions: [Permission.Viewer],
|
|
1133
|
+
});
|
|
1134
|
+
|
|
1135
|
+
mockProps(principal.databaseProps);
|
|
1136
|
+
mockApplication({ id: applicationAId, labelIds: [] });
|
|
1137
|
+
|
|
1138
|
+
const result: CallResult = await callRoute({
|
|
1139
|
+
uri: LIST_ROUTE,
|
|
1140
|
+
request: principal.request,
|
|
1141
|
+
body: { rumApplicationId: applicationAId.toString() },
|
|
1142
|
+
});
|
|
1143
|
+
|
|
1144
|
+
expect(result.deniedWith).toBeInstanceOf(NotAuthorizedException);
|
|
1145
|
+
expect(headerQuerySpy).not.toHaveBeenCalled();
|
|
1146
|
+
});
|
|
1147
|
+
|
|
1148
|
+
/*
|
|
1149
|
+
* The identity filter takes the end-user REFERENCE a person can see in
|
|
1150
|
+
* the list, and the server derives the digest with the same per-project
|
|
1151
|
+
* HMAC the ingest used. It used to take the digest itself - a value
|
|
1152
|
+
* displayed nowhere in the product and computed by no endpoint - so
|
|
1153
|
+
* every input a human could plausibly type returned nothing.
|
|
1154
|
+
*/
|
|
1155
|
+
test("an end-user reference is hashed server side before it reaches the query", async () => {
|
|
1156
|
+
const principal: {
|
|
1157
|
+
request: JSONObject;
|
|
1158
|
+
databaseProps: DatabaseCommonInteractionProps;
|
|
1159
|
+
} = buildPrincipal({
|
|
1160
|
+
projectId: projectId,
|
|
1161
|
+
userId: userId,
|
|
1162
|
+
/* Holds the narrower identity permission - see the gate test below. */
|
|
1163
|
+
permissions: [Permission.ProjectOwner],
|
|
1164
|
+
});
|
|
1165
|
+
|
|
1166
|
+
mockProps(principal.databaseProps);
|
|
1167
|
+
mockApplication({ id: applicationAId, labelIds: [] });
|
|
1168
|
+
|
|
1169
|
+
await callRoute({
|
|
1170
|
+
uri: LIST_ROUTE,
|
|
1171
|
+
request: principal.request,
|
|
1172
|
+
body: {
|
|
1173
|
+
rumApplicationId: applicationAId.toString(),
|
|
1174
|
+
filters: { identifiedUserRef: "jane@example.com" },
|
|
1175
|
+
},
|
|
1176
|
+
});
|
|
1177
|
+
|
|
1178
|
+
const statement: Statement = headerQuerySpy.mock
|
|
1179
|
+
.calls[0]![0] as Statement;
|
|
1180
|
+
|
|
1181
|
+
const expectedKey: string = SessionReplayIdentity.buildUserKey({
|
|
1182
|
+
projectId: projectId,
|
|
1183
|
+
userRef: "jane@example.com",
|
|
1184
|
+
});
|
|
1185
|
+
|
|
1186
|
+
const bound: string = JSON.stringify(statement.query_params);
|
|
1187
|
+
|
|
1188
|
+
/* The raw reference must never reach the query. */
|
|
1189
|
+
expect(bound).not.toContain("jane@example.com");
|
|
1190
|
+
expect(bound).toContain(expectedKey);
|
|
1191
|
+
});
|
|
1192
|
+
|
|
1193
|
+
/*
|
|
1194
|
+
* The identity FILTER is gated by the same ACL as the identity COLUMN.
|
|
1195
|
+
*
|
|
1196
|
+
* Without this, a caller deliberately denied the label could still ask
|
|
1197
|
+
* "does jane@example.com have sessions here" and read every other field
|
|
1198
|
+
* of the answer - a dictionary attack that de-anonymises the list one
|
|
1199
|
+
* candidate at a time, and hands back identifiedUserKey as a stable
|
|
1200
|
+
* pseudonym to join against the route filter. It only became reachable
|
|
1201
|
+
* once the ingest started populating the column; before that the
|
|
1202
|
+
* predicate matched nothing whoever asked.
|
|
1203
|
+
*/
|
|
1204
|
+
test("a caller without the identity permission cannot filter by end user", async () => {
|
|
1205
|
+
const principal: {
|
|
1206
|
+
request: JSONObject;
|
|
1207
|
+
databaseProps: DatabaseCommonInteractionProps;
|
|
1208
|
+
} = buildPrincipal({
|
|
1209
|
+
projectId: projectId,
|
|
1210
|
+
userId: userId,
|
|
1211
|
+
/* Can list, deliberately excluded from SESSION_REPLAY_IDENTITY_PERMISSIONS. */
|
|
1212
|
+
permissions: [Permission.TelemetryAdmin],
|
|
1213
|
+
});
|
|
1214
|
+
|
|
1215
|
+
mockProps(principal.databaseProps);
|
|
1216
|
+
mockApplication({ id: applicationAId, labelIds: [] });
|
|
1217
|
+
|
|
1218
|
+
await callRoute({
|
|
1219
|
+
uri: LIST_ROUTE,
|
|
1220
|
+
request: principal.request,
|
|
1221
|
+
body: {
|
|
1222
|
+
rumApplicationId: applicationAId.toString(),
|
|
1223
|
+
filters: { identifiedUserRef: "jane@example.com" },
|
|
1224
|
+
},
|
|
1225
|
+
});
|
|
1226
|
+
|
|
1227
|
+
const statement: Statement = headerQuerySpy.mock
|
|
1228
|
+
.calls[0]![0] as Statement;
|
|
1229
|
+
|
|
1230
|
+
const expectedKey: string = SessionReplayIdentity.buildUserKey({
|
|
1231
|
+
projectId: projectId,
|
|
1232
|
+
userRef: "jane@example.com",
|
|
1233
|
+
});
|
|
1234
|
+
|
|
1235
|
+
/* Neither the reference nor its digest may reach the query. */
|
|
1236
|
+
const bound: string = JSON.stringify(statement.query_params);
|
|
1237
|
+
|
|
1238
|
+
expect(bound).not.toContain("jane@example.com");
|
|
1239
|
+
expect(bound).not.toContain(expectedKey);
|
|
1240
|
+
|
|
1241
|
+
/*
|
|
1242
|
+
* aggIdentifiedUserKey is always a SELECT alias; what must be absent
|
|
1243
|
+
* is the HAVING predicate over it.
|
|
1244
|
+
*/
|
|
1245
|
+
expect(statement.query).not.toContain("aggIdentifiedUserKey =");
|
|
1246
|
+
});
|
|
1247
|
+
|
|
1248
|
+
/*
|
|
1249
|
+
* A filter the server cannot honour must be refused, not dropped. The
|
|
1250
|
+
* silent-drop shape returns the WHOLE project's sessions with a 200 and
|
|
1251
|
+
* gives the caller no way to tell that the person they asked about was
|
|
1252
|
+
* not the one being answered about.
|
|
1253
|
+
*/
|
|
1254
|
+
test("an unusable end-user reference is refused, not silently ignored", async () => {
|
|
1255
|
+
const principal: {
|
|
1256
|
+
request: JSONObject;
|
|
1257
|
+
databaseProps: DatabaseCommonInteractionProps;
|
|
1258
|
+
} = buildPrincipal({
|
|
1259
|
+
projectId: projectId,
|
|
1260
|
+
userId: userId,
|
|
1261
|
+
permissions: [Permission.ProjectOwner],
|
|
1262
|
+
});
|
|
1263
|
+
|
|
1264
|
+
mockProps(principal.databaseProps);
|
|
1265
|
+
mockApplication({ id: applicationAId, labelIds: [] });
|
|
1266
|
+
|
|
1267
|
+
const result: CallResult = await callRoute({
|
|
1268
|
+
uri: LIST_ROUTE,
|
|
1269
|
+
request: principal.request,
|
|
1270
|
+
body: {
|
|
1271
|
+
rumApplicationId: applicationAId.toString(),
|
|
1272
|
+
filters: { identifiedUserRef: "z".repeat(4096) },
|
|
1273
|
+
},
|
|
1274
|
+
});
|
|
1275
|
+
|
|
1276
|
+
expect(result.deniedWith).toBeInstanceOf(BadDataException);
|
|
1277
|
+
expect(headerQuerySpy).not.toHaveBeenCalled();
|
|
1278
|
+
});
|
|
1279
|
+
|
|
1089
1280
|
test("omits identifiedUserLabel for a caller without the narrower identity permission", async () => {
|
|
1090
1281
|
const principal: {
|
|
1091
1282
|
request: JSONObject;
|
|
@@ -1263,6 +1454,47 @@ describe("Session replay playback API", () => {
|
|
|
1263
1454
|
"(aggRageClickCount + aggDeadClickCount + aggErrorClickCount + aggRefreshRageCount) > 0",
|
|
1264
1455
|
);
|
|
1265
1456
|
});
|
|
1457
|
+
|
|
1458
|
+
/*
|
|
1459
|
+
* `false` means "sessions with NO frustration signals" and must be
|
|
1460
|
+
* honoured, not dropped. The route admits any boolean and hasError /
|
|
1461
|
+
* isFinalized beside it both honour false, so accepting the value and
|
|
1462
|
+
* ignoring it returned the whole unfiltered list with a 200 and no
|
|
1463
|
+
* indication why. The Dashboard never sends it, so this branch has no
|
|
1464
|
+
* producer and this is its only cover.
|
|
1465
|
+
*/
|
|
1466
|
+
test("hasFrustration false selects the clean sessions rather than being dropped", async () => {
|
|
1467
|
+
const principal: {
|
|
1468
|
+
request: JSONObject;
|
|
1469
|
+
databaseProps: DatabaseCommonInteractionProps;
|
|
1470
|
+
} = buildPrincipal({
|
|
1471
|
+
projectId: projectId,
|
|
1472
|
+
userId: userId,
|
|
1473
|
+
permissions: [Permission.ProjectAdmin],
|
|
1474
|
+
});
|
|
1475
|
+
|
|
1476
|
+
mockProps(principal.databaseProps);
|
|
1477
|
+
mockApplication({ id: applicationAId, labelIds: [] });
|
|
1478
|
+
|
|
1479
|
+
await callRoute({
|
|
1480
|
+
uri: LIST_ROUTE,
|
|
1481
|
+
request: principal.request,
|
|
1482
|
+
body: {
|
|
1483
|
+
rumApplicationId: applicationAId.toString(),
|
|
1484
|
+
filters: { hasFrustration: false },
|
|
1485
|
+
},
|
|
1486
|
+
});
|
|
1487
|
+
|
|
1488
|
+
const statement: Statement = headerQuerySpy.mock
|
|
1489
|
+
.calls[0]![0] as Statement;
|
|
1490
|
+
|
|
1491
|
+
expect(statement.query).toContain(
|
|
1492
|
+
"(aggRageClickCount + aggDeadClickCount + aggErrorClickCount + aggRefreshRageCount) = 0",
|
|
1493
|
+
);
|
|
1494
|
+
expect(statement.query).not.toContain(
|
|
1495
|
+
"(aggRageClickCount + aggDeadClickCount + aggErrorClickCount + aggRefreshRageCount) > 0",
|
|
1496
|
+
);
|
|
1497
|
+
});
|
|
1266
1498
|
});
|
|
1267
1499
|
|
|
1268
1500
|
describe("manifest", () => {
|