@oneuptime/common 12.0.18 → 12.0.19
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/DatabaseModels/EnterpriseLicense.ts +23 -0
- package/Models/DatabaseModels/Index.ts +4 -2
- package/Models/DatabaseModels/Monitor.ts +39 -9
- package/Models/DatabaseModels/UserMicrosoftTeams.ts +344 -0
- package/Models/DatabaseModels/UserNotificationRule.ts +144 -8
- package/Models/DatabaseModels/UserNotificationSetting.ts +34 -0
- package/Models/DatabaseModels/UserOnCallLogTimeline.ts +95 -0
- package/Models/DatabaseModels/UserSlack.ts +341 -0
- package/Models/DatabaseModels/WorkspaceProjectAuthToken.ts +9 -0
- package/Server/API/UserMicrosoftTeamsAPI.ts +127 -0
- package/Server/API/UserSlackAPI.ts +125 -0
- package/Server/EnvironmentConfig.ts +12 -107
- package/Server/Infrastructure/Postgres/SchemaMigrations/1788700000000-RemoveMarketingConversionUploadState.ts +33 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1788800000000-DropMarketingConversionAddEnterpriseLicenseEmail.ts +51 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1788900000000-RedactStoredMonitorIngestSecrets.ts +117 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1789000000000-AddUserSlackAndMicrosoftTeams.ts +197 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +8 -0
- package/Server/Infrastructure/Queue.ts +7 -0
- package/Server/Services/EnterpriseLicenseService.ts +60 -0
- package/Server/Services/Index.ts +6 -2
- package/Server/Services/LlmLogService.ts +25 -9
- package/Server/Services/OnCallReadinessService.ts +86 -2
- package/Server/Services/ProjectService.ts +67 -0
- package/Server/Services/UserMicrosoftTeamsService.ts +208 -0
- package/Server/Services/UserNotificationMethodAdminService.ts +167 -5
- package/Server/Services/UserNotificationRuleAdminService.ts +40 -6
- package/Server/Services/UserNotificationRuleService.ts +589 -11
- package/Server/Services/UserNotificationSettingService.ts +138 -0
- package/Server/Services/UserService.ts +28 -0
- package/Server/Services/UserSlackService.ts +218 -0
- package/Server/Services/WorkspaceProjectAuthTokenService.ts +67 -1
- package/Server/Services/WorkspaceUserAuthTokenService.ts +73 -0
- package/Server/Services/WorkspaceUserNotificationService.ts +190 -0
- package/Server/Utils/Marketing/MarketingEventUtil.ts +145 -0
- package/Server/Utils/Marketing/MarketingEventWebhook.ts +114 -0
- package/Server/Utils/Monitor/MonitorLogUtil.ts +14 -1
- package/Server/Utils/Monitor/MonitorPayloadRedaction.ts +321 -0
- package/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.ts +191 -4
- package/Tests/App/Dashboard/AdminNotificationRulesPage.test.tsx +22 -14
- package/Tests/App/Dashboard/AdminUserNotificationMethodsPage.test.tsx +22 -11
- package/Tests/App/Dashboard/OnCallPreventionGuards.test.tsx +37 -1
- package/Tests/App/Dashboard/OnCallRulesTable.test.tsx +18 -10
- package/Tests/Models/DatabaseModels/DomainRoleTierCoverage.test.ts +27 -1
- package/Tests/Models/DatabaseModels/MonitorSecretKeyColumnAccessControl.test.ts +214 -0
- package/Tests/Server/API/OnCallReadinessAPI.test.ts +145 -26
- package/Tests/Server/API/UserNotificationMethodAdminAPI.test.ts +2 -2
- package/Tests/Server/Services/AIServiceDailyBudget.test.ts +11 -1
- package/Tests/Server/Services/AdminRuleEditGuards.test.ts +22 -8
- package/Tests/Server/Services/DeliverNotificationForRuleExtraction.test.ts +18 -2
- package/Tests/Server/Services/EpisodeRuleSeverityRepair.test.ts +5 -1
- package/Tests/Server/Services/LlmLogServiceTokenAggregateParams.test.ts +640 -0
- package/Tests/Server/Services/NotificationChannelEventCoverage.test.ts +246 -22
- package/Tests/Server/Services/NotificationDeletionImpact.test.ts +136 -8
- package/Tests/Server/Services/OnCallNotificationFallback.test.ts +191 -8
- package/Tests/Server/Services/OnCallReadinessService.test.ts +343 -18
- package/Tests/Server/Services/SeverityRuleBackfill.test.ts +5 -1
- package/Tests/Server/Services/UserMicrosoftTeamsService.test.ts +343 -0
- package/Tests/Server/Services/UserNotificationMethodAdminService.test.ts +209 -8
- package/Tests/Server/Services/UserNotificationRuleAdminGuards.test.ts +22 -6
- package/Tests/Server/Services/UserNotificationRuleDefaultCreation.test.ts +12 -6
- package/Tests/Server/Services/UserNotificationRuleExecuteItem.test.ts +37 -3
- package/Tests/Server/Services/UserNotificationRuleWorkspaceDelivery.test.ts +669 -0
- package/Tests/Server/Services/UserNotificationSettingWorkspaceChannels.test.ts +410 -0
- package/Tests/Server/Services/UserSlackService.test.ts +407 -0
- package/Tests/Server/Services/WorkspaceProjectAuthTokenDisconnectCascade.test.ts +224 -0
- package/Tests/Server/Services/WorkspaceUserAuthTokenNotificationMethodCascade.test.ts +189 -0
- package/Tests/Server/Services/WorkspaceUserNotificationService.test.ts +496 -0
- package/Tests/Server/Types/Database/Permissions/AdminNotificationRuleAccess.test.ts +30 -17
- package/Tests/Server/Types/Database/Permissions/CreateOwnershipScoping.test.ts +8 -3
- package/Tests/Server/Types/Database/Permissions/OwnerOnlyColumns.test.ts +72 -3
- package/Tests/Server/Types/Database/Permissions/UserNotificationRuleScoping.test.ts +22 -18
- package/Tests/Server/Types/Database/Permissions/WorkspaceMethodStampedColumnCreate.test.ts +178 -0
- package/Tests/Server/Utils/Marketing/MarketingEventUtil.test.ts +259 -0
- package/Tests/Server/Utils/Marketing/MarketingEventWebhook.test.ts +257 -0
- package/Tests/Server/Utils/Monitor/MonitorLogUtilRedaction.test.ts +300 -0
- package/Tests/Server/Utils/Monitor/MonitorPayloadRedaction.test.ts +381 -0
- package/Tests/Server/Utils/Runbook/RunbookExecutePermission.test.ts +228 -0
- package/Tests/Server/Utils/SessionReplay/SessionReplayErasureTombstone.test.ts +189 -0
- package/Tests/Server/Utils/SessionReplay/SessionReplayUsage.test.ts +205 -0
- package/Tests/Server/Utils/Telemetry/AppMetrics.test.ts +275 -0
- package/Tests/Server/Utils/Telemetry/TelemetryContext.test.ts +279 -0
- package/Tests/Server/Utils/Workspace/MicrosoftTeamsDirectMessage.test.ts +454 -0
- package/Tests/UI/Utils/NotificationMethodUtil.test.ts +137 -9
- package/Tests/UI/Utils/PermissionGate.test.ts +105 -0
- package/Types/Marketing/MarketingEvent.ts +97 -0
- package/UI/Utils/NotificationMethodUtil.ts +41 -1
- package/UI/Utils/PermissionGate.ts +56 -0
- package/build/dist/Models/DatabaseModels/EnterpriseLicense.js +24 -0
- package/build/dist/Models/DatabaseModels/EnterpriseLicense.js.map +1 -1
- package/build/dist/Models/DatabaseModels/Index.js +4 -2
- package/build/dist/Models/DatabaseModels/Index.js.map +1 -1
- package/build/dist/Models/DatabaseModels/Monitor.js +39 -9
- package/build/dist/Models/DatabaseModels/Monitor.js.map +1 -1
- package/build/dist/Models/DatabaseModels/UserMicrosoftTeams.js +363 -0
- package/build/dist/Models/DatabaseModels/UserMicrosoftTeams.js.map +1 -0
- package/build/dist/Models/DatabaseModels/UserNotificationRule.js +145 -8
- package/build/dist/Models/DatabaseModels/UserNotificationRule.js.map +1 -1
- package/build/dist/Models/DatabaseModels/UserNotificationSetting.js +38 -0
- package/build/dist/Models/DatabaseModels/UserNotificationSetting.js.map +1 -1
- package/build/dist/Models/DatabaseModels/UserOnCallLogTimeline.js +96 -0
- package/build/dist/Models/DatabaseModels/UserOnCallLogTimeline.js.map +1 -1
- package/build/dist/Models/DatabaseModels/UserSlack.js +361 -0
- package/build/dist/Models/DatabaseModels/UserSlack.js.map +1 -0
- package/build/dist/Models/DatabaseModels/WorkspaceProjectAuthToken.js.map +1 -1
- package/build/dist/Server/API/UserMicrosoftTeamsAPI.js +82 -0
- package/build/dist/Server/API/UserMicrosoftTeamsAPI.js.map +1 -0
- package/build/dist/Server/API/UserSlackAPI.js +81 -0
- package/build/dist/Server/API/UserSlackAPI.js.map +1 -0
- package/build/dist/Server/EnvironmentConfig.js +10 -70
- package/build/dist/Server/EnvironmentConfig.js.map +1 -1
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1788700000000-RemoveMarketingConversionUploadState.js +26 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1788700000000-RemoveMarketingConversionUploadState.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1788800000000-DropMarketingConversionAddEnterpriseLicenseEmail.js +37 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1788800000000-DropMarketingConversionAddEnterpriseLicenseEmail.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1788900000000-RedactStoredMonitorIngestSecrets.js +106 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1788900000000-RedactStoredMonitorIngestSecrets.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1789000000000-AddUserSlackAndMicrosoftTeams.js +78 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1789000000000-AddUserSlackAndMicrosoftTeams.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +8 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
- package/build/dist/Server/Infrastructure/Queue.js +7 -0
- package/build/dist/Server/Infrastructure/Queue.js.map +1 -1
- package/build/dist/Server/Services/EnterpriseLicenseService.js +65 -0
- package/build/dist/Server/Services/EnterpriseLicenseService.js.map +1 -1
- package/build/dist/Server/Services/Index.js +6 -2
- package/build/dist/Server/Services/Index.js.map +1 -1
- package/build/dist/Server/Services/LlmLogService.js +25 -9
- package/build/dist/Server/Services/LlmLogService.js.map +1 -1
- package/build/dist/Server/Services/OnCallReadinessService.js +74 -2
- package/build/dist/Server/Services/OnCallReadinessService.js.map +1 -1
- package/build/dist/Server/Services/ProjectService.js +56 -0
- package/build/dist/Server/Services/ProjectService.js.map +1 -1
- package/build/dist/Server/Services/UserMicrosoftTeamsService.js +183 -0
- package/build/dist/Server/Services/UserMicrosoftTeamsService.js.map +1 -0
- package/build/dist/Server/Services/UserNotificationMethodAdminService.js +138 -2
- package/build/dist/Server/Services/UserNotificationMethodAdminService.js.map +1 -1
- package/build/dist/Server/Services/UserNotificationRuleAdminService.js +32 -4
- package/build/dist/Server/Services/UserNotificationRuleAdminService.js.map +1 -1
- package/build/dist/Server/Services/UserNotificationRuleService.js +499 -67
- package/build/dist/Server/Services/UserNotificationRuleService.js.map +1 -1
- package/build/dist/Server/Services/UserNotificationSettingService.js +118 -0
- package/build/dist/Server/Services/UserNotificationSettingService.js.map +1 -1
- package/build/dist/Server/Services/UserService.js +26 -1
- package/build/dist/Server/Services/UserService.js.map +1 -1
- package/build/dist/Server/Services/UserSlackService.js +195 -0
- package/build/dist/Server/Services/UserSlackService.js.map +1 -0
- package/build/dist/Server/Services/WorkspaceProjectAuthTokenService.js +64 -1
- package/build/dist/Server/Services/WorkspaceProjectAuthTokenService.js.map +1 -1
- package/build/dist/Server/Services/WorkspaceUserAuthTokenService.js +70 -0
- package/build/dist/Server/Services/WorkspaceUserAuthTokenService.js.map +1 -1
- package/build/dist/Server/Services/WorkspaceUserNotificationService.js +129 -0
- package/build/dist/Server/Services/WorkspaceUserNotificationService.js.map +1 -0
- package/build/dist/Server/Utils/Marketing/MarketingEventUtil.js +94 -0
- package/build/dist/Server/Utils/Marketing/MarketingEventUtil.js.map +1 -0
- package/build/dist/Server/Utils/Marketing/MarketingEventWebhook.js +93 -0
- package/build/dist/Server/Utils/Marketing/MarketingEventWebhook.js.map +1 -0
- package/build/dist/Server/Utils/Monitor/MonitorLogUtil.js +12 -1
- package/build/dist/Server/Utils/Monitor/MonitorLogUtil.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/MonitorPayloadRedaction.js +233 -0
- package/build/dist/Server/Utils/Monitor/MonitorPayloadRedaction.js.map +1 -0
- package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.js +141 -4
- package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.js.map +1 -1
- package/build/dist/Types/Marketing/MarketingEvent.js +43 -0
- package/build/dist/Types/Marketing/MarketingEvent.js.map +1 -0
- package/build/dist/UI/Utils/NotificationMethodUtil.js +33 -1
- package/build/dist/UI/Utils/NotificationMethodUtil.js.map +1 -1
- package/build/dist/UI/Utils/PermissionGate.js +32 -0
- package/build/dist/UI/Utils/PermissionGate.js.map +1 -1
- package/package.json +1 -1
- package/Models/DatabaseModels/MarketingConversion.ts +0 -410
- package/Server/Services/MarketingConversionService.ts +0 -10
- package/Server/Utils/Marketing/ConversionUploadProvider.ts +0 -215
- package/Server/Utils/Marketing/ConversionUploadProviders.ts +0 -22
- package/Server/Utils/Marketing/Providers/GoogleAds.ts +0 -333
- package/Server/Utils/Marketing/Providers/LinkedIn.ts +0 -185
- package/Server/Utils/Marketing/Providers/Meta.ts +0 -182
- package/Server/Utils/Marketing/Providers/MicrosoftAds.ts +0 -223
- package/Server/Utils/Marketing/Providers/Reddit.ts +0 -208
- package/Tests/Server/Utils/Marketing/AdUploadableConversionTypes.test.ts +0 -315
- package/Tests/Server/Utils/Marketing/ConversionUploadProvider.test.ts +0 -300
- package/Tests/Server/Utils/Marketing/GoogleAds.test.ts +0 -592
- package/Tests/Server/Utils/Marketing/LinkedIn.test.ts +0 -282
- package/Tests/Server/Utils/Marketing/Meta.test.ts +0 -304
- package/Tests/Server/Utils/Marketing/MicrosoftAds.test.ts +0 -263
- package/Tests/Server/Utils/Marketing/Reddit.test.ts +0 -259
- package/Types/Marketing/MarketingConversion.ts +0 -53
- package/build/dist/Models/DatabaseModels/MarketingConversion.js +0 -445
- package/build/dist/Models/DatabaseModels/MarketingConversion.js.map +0 -1
- package/build/dist/Server/Services/MarketingConversionService.js +0 -9
- package/build/dist/Server/Services/MarketingConversionService.js.map +0 -1
- package/build/dist/Server/Utils/Marketing/ConversionUploadProvider.js +0 -93
- package/build/dist/Server/Utils/Marketing/ConversionUploadProvider.js.map +0 -1
- package/build/dist/Server/Utils/Marketing/ConversionUploadProviders.js +0 -20
- package/build/dist/Server/Utils/Marketing/ConversionUploadProviders.js.map +0 -1
- package/build/dist/Server/Utils/Marketing/Providers/GoogleAds.js +0 -221
- package/build/dist/Server/Utils/Marketing/Providers/GoogleAds.js.map +0 -1
- package/build/dist/Server/Utils/Marketing/Providers/LinkedIn.js +0 -137
- package/build/dist/Server/Utils/Marketing/Providers/LinkedIn.js.map +0 -1
- package/build/dist/Server/Utils/Marketing/Providers/Meta.js +0 -133
- package/build/dist/Server/Utils/Marketing/Providers/Meta.js.map +0 -1
- package/build/dist/Server/Utils/Marketing/Providers/MicrosoftAds.js +0 -143
- package/build/dist/Server/Utils/Marketing/Providers/MicrosoftAds.js.map +0 -1
- package/build/dist/Server/Utils/Marketing/Providers/Reddit.js +0 -145
- package/build/dist/Server/Utils/Marketing/Providers/Reddit.js.map +0 -1
- package/build/dist/Types/Marketing/MarketingConversion.js +0 -52
- package/build/dist/Types/Marketing/MarketingConversion.js.map +0 -1
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
import Monitor from "../../../Models/DatabaseModels/Monitor";
|
|
2
|
+
import { ColumnAccessControl } from "../../../Types/BaseDatabase/AccessControl";
|
|
3
|
+
import Dictionary from "../../../Types/Dictionary";
|
|
4
|
+
import Permission, { PermissionHelper } from "../../../Types/Permission";
|
|
5
|
+
import { describe, expect, it } from "@jest/globals";
|
|
6
|
+
|
|
7
|
+
/*
|
|
8
|
+
* https://github.com/OneUptime/oneuptime/issues/3360
|
|
9
|
+
*
|
|
10
|
+
* Monitor carries three bearer credentials as ordinary columns:
|
|
11
|
+
*
|
|
12
|
+
* serverMonitorSecretKey - authenticates every host agent reporting in
|
|
13
|
+
* incomingRequestSecretKey - IS the heartbeat URL
|
|
14
|
+
* incomingEmailSecretKey - IS the monitor's inbound address
|
|
15
|
+
*
|
|
16
|
+
* All three used to list Permission.Viewer in their read ACL. Viewer is the
|
|
17
|
+
* least privilege OneUptime offers and is precisely what gets handed to a
|
|
18
|
+
* dashboard, an auditor, a contractor or an automation -- so "read-only" also
|
|
19
|
+
* meant "can read a live credential and then forge or suppress this monitor's
|
|
20
|
+
* heartbeats". Redacting the secret out of MonitorLog.logBody (the other half
|
|
21
|
+
* of the fix) does nothing about this path: it is a plain column select.
|
|
22
|
+
*
|
|
23
|
+
* The rule these tests encode is a single sentence: reading a monitor secret
|
|
24
|
+
* requires the ability to ROTATE it. Anything weaker is a credential handed to
|
|
25
|
+
* someone who cannot revoke it.
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
const SECRET_KEY_COLUMNS: Array<string> = [
|
|
29
|
+
"serverMonitorSecretKey",
|
|
30
|
+
"incomingRequestSecretKey",
|
|
31
|
+
"incomingEmailSecretKey",
|
|
32
|
+
];
|
|
33
|
+
|
|
34
|
+
/*
|
|
35
|
+
* The read-only roles. Every one of these was on the read list before the fix,
|
|
36
|
+
* and none of them can rotate a key.
|
|
37
|
+
*/
|
|
38
|
+
const READ_ONLY_PERMISSIONS: Array<Permission> = [
|
|
39
|
+
Permission.Viewer,
|
|
40
|
+
Permission.MonitorViewer,
|
|
41
|
+
Permission.ReadProjectMonitor,
|
|
42
|
+
];
|
|
43
|
+
|
|
44
|
+
// The roles that can reset a monitor secret, and so may also see it.
|
|
45
|
+
const ROTATION_PERMISSIONS: Array<Permission> = [
|
|
46
|
+
Permission.ProjectOwner,
|
|
47
|
+
Permission.ProjectAdmin,
|
|
48
|
+
Permission.ProjectMember,
|
|
49
|
+
Permission.MonitorAdmin,
|
|
50
|
+
Permission.MonitorMember,
|
|
51
|
+
Permission.EditProjectMonitor,
|
|
52
|
+
];
|
|
53
|
+
|
|
54
|
+
const accessControl: Dictionary<ColumnAccessControl> =
|
|
55
|
+
new Monitor().getColumnAccessControlForAllColumns();
|
|
56
|
+
|
|
57
|
+
type CanReadFunction = (
|
|
58
|
+
columnName: string,
|
|
59
|
+
userPermissions: Array<Permission>,
|
|
60
|
+
) => boolean;
|
|
61
|
+
|
|
62
|
+
/*
|
|
63
|
+
* ColumnPermission.checkDataColumnPermissions, reduced to the question that
|
|
64
|
+
* matters. Note what the server does when this is false: it THROWS
|
|
65
|
+
* `User is not allowed to read on <column> column of Monitor` and the whole
|
|
66
|
+
* request fails. There is no partial response -- which is why the dashboard
|
|
67
|
+
* has to gate the select rather than the render.
|
|
68
|
+
*/
|
|
69
|
+
const canRead: CanReadFunction = (
|
|
70
|
+
columnName: string,
|
|
71
|
+
userPermissions: Array<Permission>,
|
|
72
|
+
): boolean => {
|
|
73
|
+
const columnPermissions: Array<Permission> =
|
|
74
|
+
accessControl[columnName]?.read || [];
|
|
75
|
+
|
|
76
|
+
return PermissionHelper.doesPermissionsIntersect(
|
|
77
|
+
userPermissions,
|
|
78
|
+
columnPermissions,
|
|
79
|
+
);
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
describe("Monitor secret key columns are not readable by read-only roles", () => {
|
|
83
|
+
it.each(SECRET_KEY_COLUMNS)(
|
|
84
|
+
"%s is unreadable by a Viewer-only principal",
|
|
85
|
+
(columnName: string) => {
|
|
86
|
+
/*
|
|
87
|
+
* This is step 2 of the reproduction in the issue: an API key whose only
|
|
88
|
+
* permission is Viewer.
|
|
89
|
+
*/
|
|
90
|
+
expect(canRead(columnName, [Permission.Viewer])).toBe(false);
|
|
91
|
+
},
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
it.each(SECRET_KEY_COLUMNS)(
|
|
95
|
+
"%s is unreadable by every read-only role, individually",
|
|
96
|
+
(columnName: string) => {
|
|
97
|
+
for (const permission of READ_ONLY_PERMISSIONS) {
|
|
98
|
+
expect(canRead(columnName, [permission])).toBe(false);
|
|
99
|
+
}
|
|
100
|
+
},
|
|
101
|
+
);
|
|
102
|
+
|
|
103
|
+
it.each(SECRET_KEY_COLUMNS)(
|
|
104
|
+
"%s is unreadable even when all the read-only roles are held at once",
|
|
105
|
+
(columnName: string) => {
|
|
106
|
+
/*
|
|
107
|
+
* Intersection semantics mean any single surviving read-only permission
|
|
108
|
+
* re-opens the hole, so hold them all and assert the union is still
|
|
109
|
+
* refused.
|
|
110
|
+
*/
|
|
111
|
+
expect(canRead(columnName, READ_ONLY_PERMISSIONS)).toBe(false);
|
|
112
|
+
},
|
|
113
|
+
);
|
|
114
|
+
|
|
115
|
+
it.each(SECRET_KEY_COLUMNS)(
|
|
116
|
+
"%s names none of the read-only roles in its read ACL",
|
|
117
|
+
(columnName: string) => {
|
|
118
|
+
const readPermissions: Array<Permission> =
|
|
119
|
+
accessControl[columnName]?.read || [];
|
|
120
|
+
|
|
121
|
+
for (const permission of READ_ONLY_PERMISSIONS) {
|
|
122
|
+
expect(readPermissions).not.toContain(permission);
|
|
123
|
+
}
|
|
124
|
+
},
|
|
125
|
+
);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
describe("Monitor secret key columns stay readable by the roles that can rotate them", () => {
|
|
129
|
+
it.each(SECRET_KEY_COLUMNS)(
|
|
130
|
+
"%s is readable by each rotation-capable role",
|
|
131
|
+
(columnName: string) => {
|
|
132
|
+
/*
|
|
133
|
+
* The other failure mode. Locking these down too far breaks Monitor >
|
|
134
|
+
* Settings for a project member, who legitimately needs the key to set
|
|
135
|
+
* an agent up.
|
|
136
|
+
*/
|
|
137
|
+
for (const permission of ROTATION_PERMISSIONS) {
|
|
138
|
+
expect(canRead(columnName, [permission])).toBe(true);
|
|
139
|
+
}
|
|
140
|
+
},
|
|
141
|
+
);
|
|
142
|
+
|
|
143
|
+
it.each(SECRET_KEY_COLUMNS)(
|
|
144
|
+
"%s gates read on exactly the same roles as update",
|
|
145
|
+
(columnName: string) => {
|
|
146
|
+
/*
|
|
147
|
+
* The invariant, stated directly: you may see a monitor secret if and
|
|
148
|
+
* only if you may replace it. If someone later widens read without
|
|
149
|
+
* widening update, this is the test that should stop them and make them
|
|
150
|
+
* say why.
|
|
151
|
+
*/
|
|
152
|
+
const column: ColumnAccessControl | undefined = accessControl[columnName];
|
|
153
|
+
|
|
154
|
+
expect(column).toBeDefined();
|
|
155
|
+
expect([...(column?.read || [])].sort()).toEqual(
|
|
156
|
+
[...(column?.update || [])].sort(),
|
|
157
|
+
);
|
|
158
|
+
},
|
|
159
|
+
);
|
|
160
|
+
|
|
161
|
+
it.each(SECRET_KEY_COLUMNS)(
|
|
162
|
+
"%s is never writable on create",
|
|
163
|
+
(columnName: string) => {
|
|
164
|
+
// The keys are computed server-side by MonitorService, never supplied.
|
|
165
|
+
expect(accessControl[columnName]?.create || []).toEqual([]);
|
|
166
|
+
},
|
|
167
|
+
);
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
describe("the rest of the monitor page still loads for a Viewer", () => {
|
|
171
|
+
/*
|
|
172
|
+
* Tightening the secret columns must not collaterally break the monitor view
|
|
173
|
+
* for read-only users: an unreadable column in a select fails the WHOLE
|
|
174
|
+
* request, so if any of these regressed, a Viewer would get an error screen
|
|
175
|
+
* instead of a monitor. These are the other columns
|
|
176
|
+
* Pages/Monitor/View/Index.tsx asks for.
|
|
177
|
+
*/
|
|
178
|
+
const VIEWER_READABLE_COLUMNS: Array<string> = [
|
|
179
|
+
"monitorType",
|
|
180
|
+
"serverMonitorRequestReceivedAt",
|
|
181
|
+
"incomingRequestMonitorHeartbeatCheckedAt",
|
|
182
|
+
"incomingEmailMonitorHeartbeatCheckedAt",
|
|
183
|
+
"incomingEmailMonitorLastEmailReceivedAt",
|
|
184
|
+
"monitorSteps",
|
|
185
|
+
];
|
|
186
|
+
|
|
187
|
+
it.each(VIEWER_READABLE_COLUMNS)(
|
|
188
|
+
"%s is still readable by a Viewer",
|
|
189
|
+
(columnName: string) => {
|
|
190
|
+
expect(canRead(columnName, [Permission.Viewer])).toBe(true);
|
|
191
|
+
},
|
|
192
|
+
);
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
describe("the payload columns rely on redaction, not on access control", () => {
|
|
196
|
+
/*
|
|
197
|
+
* serverMonitorResponse and incomingMonitorRequest are the OTHER two places
|
|
198
|
+
* the agent payload lands, and both remain Viewer-readable -- deliberately:
|
|
199
|
+
* they are what the monitor page renders. That is exactly why the secret has
|
|
200
|
+
* to be stripped at the ingest boundary (see MonitorPayloadRedaction) rather
|
|
201
|
+
* than merely hidden behind a permission. If someone ever removes that
|
|
202
|
+
* strip, no ACL here will catch it.
|
|
203
|
+
*
|
|
204
|
+
* This test is a signpost, not a guard. If these columns are deliberately
|
|
205
|
+
* locked down later, delete it -- but do not delete the redaction.
|
|
206
|
+
*/
|
|
207
|
+
it("serverMonitorResponse is readable by a Viewer", () => {
|
|
208
|
+
expect(canRead("serverMonitorResponse", [Permission.Viewer])).toBe(true);
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
it("incomingMonitorRequest is readable by a Viewer", () => {
|
|
212
|
+
expect(canRead("incomingMonitorRequest", [Permission.Viewer])).toBe(true);
|
|
213
|
+
});
|
|
214
|
+
});
|
|
@@ -26,9 +26,11 @@ import TeamMemberService from "../../../Server/Services/TeamMemberService";
|
|
|
26
26
|
import TeamService from "../../../Server/Services/TeamService";
|
|
27
27
|
import UserCallService from "../../../Server/Services/UserCallService";
|
|
28
28
|
import UserEmailService from "../../../Server/Services/UserEmailService";
|
|
29
|
+
import UserMicrosoftTeamsService from "../../../Server/Services/UserMicrosoftTeamsService";
|
|
29
30
|
import UserNotificationRuleService from "../../../Server/Services/UserNotificationRuleService";
|
|
30
31
|
import UserPushService from "../../../Server/Services/UserPushService";
|
|
31
32
|
import UserService from "../../../Server/Services/UserService";
|
|
33
|
+
import UserSlackService from "../../../Server/Services/UserSlackService";
|
|
32
34
|
import UserSmsService from "../../../Server/Services/UserSmsService";
|
|
33
35
|
import UserTelegramService from "../../../Server/Services/UserTelegramService";
|
|
34
36
|
import UserWebhookService from "../../../Server/Services/UserWebhookService";
|
|
@@ -41,7 +43,9 @@ import {
|
|
|
41
43
|
import Response from "../../../Server/Utils/Response";
|
|
42
44
|
import UserCall from "../../../Models/DatabaseModels/UserCall";
|
|
43
45
|
import UserEmail from "../../../Models/DatabaseModels/UserEmail";
|
|
46
|
+
import UserMicrosoftTeams from "../../../Models/DatabaseModels/UserMicrosoftTeams";
|
|
44
47
|
import UserPush from "../../../Models/DatabaseModels/UserPush";
|
|
48
|
+
import UserSlack from "../../../Models/DatabaseModels/UserSlack";
|
|
45
49
|
import UserSMS from "../../../Models/DatabaseModels/UserSMS";
|
|
46
50
|
import UserTelegram from "../../../Models/DatabaseModels/UserTelegram";
|
|
47
51
|
import UserWebhook from "../../../Models/DatabaseModels/UserWebhook";
|
|
@@ -316,6 +320,8 @@ let userSmsFindBy: jest.SpyInstance;
|
|
|
316
320
|
let userCallFindBy: jest.SpyInstance;
|
|
317
321
|
let userWhatsAppFindBy: jest.SpyInstance;
|
|
318
322
|
let userTelegramFindBy: jest.SpyInstance;
|
|
323
|
+
let userSlackFindBy: jest.SpyInstance;
|
|
324
|
+
let userMicrosoftTeamsFindBy: jest.SpyInstance;
|
|
319
325
|
let userWebhookFindBy: jest.SpyInstance;
|
|
320
326
|
let notificationRuleFindBy: jest.SpyInstance;
|
|
321
327
|
let incidentSeverityFindBy: jest.SpyInstance;
|
|
@@ -340,6 +346,8 @@ function everyFindBySpy(): Array<jest.SpyInstance> {
|
|
|
340
346
|
userCallFindBy,
|
|
341
347
|
userWhatsAppFindBy,
|
|
342
348
|
userTelegramFindBy,
|
|
349
|
+
userSlackFindBy,
|
|
350
|
+
userMicrosoftTeamsFindBy,
|
|
343
351
|
userWebhookFindBy,
|
|
344
352
|
notificationRuleFindBy,
|
|
345
353
|
incidentSeverityFindBy,
|
|
@@ -445,6 +453,12 @@ beforeEach(() => {
|
|
|
445
453
|
userTelegramFindBy = jest
|
|
446
454
|
.spyOn(UserTelegramService, "findBy")
|
|
447
455
|
.mockResolvedValue([] as never);
|
|
456
|
+
userSlackFindBy = jest
|
|
457
|
+
.spyOn(UserSlackService, "findBy")
|
|
458
|
+
.mockResolvedValue([] as never);
|
|
459
|
+
userMicrosoftTeamsFindBy = jest
|
|
460
|
+
.spyOn(UserMicrosoftTeamsService, "findBy")
|
|
461
|
+
.mockResolvedValue([] as never);
|
|
448
462
|
userWebhookFindBy = jest
|
|
449
463
|
.spyOn(UserWebhookService, "findBy")
|
|
450
464
|
.mockResolvedValue([] as never);
|
|
@@ -759,13 +773,14 @@ describe("GET /on-call-readiness/policy/:policyId", () => {
|
|
|
759
773
|
|
|
760
774
|
/*
|
|
761
775
|
* Four fields and no fifth. `methods` is the part of this payload that
|
|
762
|
-
* describes rows an administrator is NOT permitted to read - the
|
|
776
|
+
* describes rows an administrator is NOT permitted to read - the nine
|
|
763
777
|
* method models are owner-scoped precisely because their columns are the raw
|
|
764
|
-
* phone number, the webhook bearer url, the push token, the telegram chat
|
|
765
|
-
*
|
|
766
|
-
* rather than field by field. A
|
|
767
|
-
*
|
|
768
|
-
* pass through here on
|
|
778
|
+
* phone number, the webhook bearer url, the push token, the telegram chat
|
|
779
|
+
* id, the slack member id, the teams object id and the verification code -
|
|
780
|
+
* so the wire shape is asserted exhaustively rather than field by field. A
|
|
781
|
+
* field added to ReadinessMethod is a field that ships to every
|
|
782
|
+
* administrator of the project, and it should have to pass through here on
|
|
783
|
+
* the way.
|
|
769
784
|
*/
|
|
770
785
|
expect(user["methods"]).toEqual([
|
|
771
786
|
{
|
|
@@ -1437,6 +1452,15 @@ describe("GET /on-call-readiness/user/:userId - identifier masking", () => {
|
|
|
1437
1452
|
const RAW_WHATSAPP: string = "+14155559999";
|
|
1438
1453
|
const RAW_HANDLE: string = "@jamesbond";
|
|
1439
1454
|
const RAW_DEVICE: string = "Jane's iPhone 15 Pro";
|
|
1455
|
+
const RAW_SLACK_USERNAME: string = "jane.oncall.slack";
|
|
1456
|
+
const RAW_TEAMS_USERNAME: string = "Jane Doe [Overnight]";
|
|
1457
|
+
/*
|
|
1458
|
+
* The addressable targets behind the two workspace channels - the Slack
|
|
1459
|
+
* member id the bot DMs and the Entra object id the Teams bot resolves. On
|
|
1460
|
+
* the rows, like the webhook's bearer url, and never selected.
|
|
1461
|
+
*/
|
|
1462
|
+
const RAW_SLACK_USER_ID: string = "U0MEMBERIDLEAK";
|
|
1463
|
+
const RAW_TEAMS_USER_ID: string = "aad-ENTRALEAK-7788";
|
|
1440
1464
|
const RAW_WEBHOOK_NAME: string = "Payments Slack Hook";
|
|
1441
1465
|
const RAW_WEBHOOK_URL: string =
|
|
1442
1466
|
"https://hooks.slack.com/services/T000/B000/XXXXsecretXXXX";
|
|
@@ -1445,7 +1469,7 @@ describe("GET /on-call-readiness/user/:userId - identifier masking", () => {
|
|
|
1445
1469
|
* One fixed id per channel, and fixed rather than generated so that a failure
|
|
1446
1470
|
* prints a value a reader can match against the fixture that produced it.
|
|
1447
1471
|
*
|
|
1448
|
-
* These are the ids UserNotificationRule's
|
|
1472
|
+
* These are the ids UserNotificationRule's nine foreign keys reference -
|
|
1449
1473
|
* userEmailId points at a UserEmail row, userSmsId at a UserSMS row - and they
|
|
1450
1474
|
* are the reason `methodId` exists at all. An administrator building a rule
|
|
1451
1475
|
* for somebody else may not READ any of these rows; what they get instead is
|
|
@@ -1475,10 +1499,16 @@ describe("GET /on-call-readiness/user/:userId - identifier masking", () => {
|
|
|
1475
1499
|
const WEBHOOK_METHOD_ID: ObjectID = new ObjectID(
|
|
1476
1500
|
"1a000000-0000-4000-8000-000000000007",
|
|
1477
1501
|
);
|
|
1502
|
+
const SLACK_METHOD_ID: ObjectID = new ObjectID(
|
|
1503
|
+
"1a000000-0000-4000-8000-000000000008",
|
|
1504
|
+
);
|
|
1505
|
+
const MICROSOFT_TEAMS_METHOD_ID: ObjectID = new ObjectID(
|
|
1506
|
+
"1a000000-0000-4000-8000-000000000009",
|
|
1507
|
+
);
|
|
1478
1508
|
|
|
1479
1509
|
/*
|
|
1480
1510
|
* The expected id per channel, in one place, so the sweep below is a sweep
|
|
1481
|
-
* over all
|
|
1511
|
+
* over all nine rather than nine assertions that can each be forgotten
|
|
1482
1512
|
* individually. Keyed by the wire's `methodType` string.
|
|
1483
1513
|
*/
|
|
1484
1514
|
const METHOD_ID_BY_TYPE: Dictionary<string> = {
|
|
@@ -1488,6 +1518,8 @@ describe("GET /on-call-readiness/user/:userId - identifier masking", () => {
|
|
|
1488
1518
|
[ReadinessMethodType.Call]: CALL_METHOD_ID.toString(),
|
|
1489
1519
|
[ReadinessMethodType.WhatsApp]: WHATSAPP_METHOD_ID.toString(),
|
|
1490
1520
|
[ReadinessMethodType.Telegram]: TELEGRAM_METHOD_ID.toString(),
|
|
1521
|
+
[ReadinessMethodType.Slack]: SLACK_METHOD_ID.toString(),
|
|
1522
|
+
[ReadinessMethodType.MicrosoftTeams]: MICROSOFT_TEAMS_METHOD_ID.toString(),
|
|
1491
1523
|
[ReadinessMethodType.Webhook]: WEBHOOK_METHOD_ID.toString(),
|
|
1492
1524
|
};
|
|
1493
1525
|
|
|
@@ -1548,6 +1580,24 @@ describe("GET /on-call-readiness/user/:userId - identifier masking", () => {
|
|
|
1548
1580
|
telegram.isVerified = true;
|
|
1549
1581
|
userTelegramFindBy.mockResolvedValue([telegram] as never);
|
|
1550
1582
|
|
|
1583
|
+
const slack: UserSlack = new UserSlack();
|
|
1584
|
+
slack.id = SLACK_METHOD_ID;
|
|
1585
|
+
slack.userId = subjectUserId;
|
|
1586
|
+
slack.slackUserName = RAW_SLACK_USERNAME;
|
|
1587
|
+
// On the row, and never selected: the member id is the addressable target.
|
|
1588
|
+
slack.slackUserId = RAW_SLACK_USER_ID;
|
|
1589
|
+
slack.isVerified = true;
|
|
1590
|
+
userSlackFindBy.mockResolvedValue([slack] as never);
|
|
1591
|
+
|
|
1592
|
+
const microsoftTeams: UserMicrosoftTeams = new UserMicrosoftTeams();
|
|
1593
|
+
microsoftTeams.id = MICROSOFT_TEAMS_METHOD_ID;
|
|
1594
|
+
microsoftTeams.userId = subjectUserId;
|
|
1595
|
+
microsoftTeams.microsoftTeamsUserName = RAW_TEAMS_USERNAME;
|
|
1596
|
+
// On the row, and never selected: the Entra id is the addressable target.
|
|
1597
|
+
microsoftTeams.microsoftTeamsUserId = RAW_TEAMS_USER_ID;
|
|
1598
|
+
microsoftTeams.isVerified = true;
|
|
1599
|
+
userMicrosoftTeamsFindBy.mockResolvedValue([microsoftTeams] as never);
|
|
1600
|
+
|
|
1551
1601
|
const push: UserPush = new UserPush();
|
|
1552
1602
|
push.id = PUSH_METHOD_ID;
|
|
1553
1603
|
push.userId = subjectUserId;
|
|
@@ -1597,6 +1647,24 @@ describe("GET /on-call-readiness/user/:userId - identifier masking", () => {
|
|
|
1597
1647
|
maskedIdentifier: `j${IDENTIFIER_MASK}@personal.example.com`,
|
|
1598
1648
|
isVerified: true,
|
|
1599
1649
|
},
|
|
1650
|
+
/*
|
|
1651
|
+
* The two workspace channels sit in the zero-cost tier beside Push and
|
|
1652
|
+
* Email, ahead of every paid channel. Their masked identifier is the
|
|
1653
|
+
* human-facing NAME column under the handle rule; the addressable ids
|
|
1654
|
+
* are never selected at all - see the select tests below.
|
|
1655
|
+
*/
|
|
1656
|
+
{
|
|
1657
|
+
methodId: SLACK_METHOD_ID.toString(),
|
|
1658
|
+
methodType: ReadinessMethodType.Slack,
|
|
1659
|
+
maskedIdentifier: `ja${IDENTIFIER_MASK}`,
|
|
1660
|
+
isVerified: true,
|
|
1661
|
+
},
|
|
1662
|
+
{
|
|
1663
|
+
methodId: MICROSOFT_TEAMS_METHOD_ID.toString(),
|
|
1664
|
+
methodType: ReadinessMethodType.MicrosoftTeams,
|
|
1665
|
+
maskedIdentifier: `Ja${IDENTIFIER_MASK}`,
|
|
1666
|
+
isVerified: true,
|
|
1667
|
+
},
|
|
1600
1668
|
{
|
|
1601
1669
|
methodId: SMS_METHOD_ID.toString(),
|
|
1602
1670
|
methodType: ReadinessMethodType.SMS,
|
|
@@ -1634,10 +1702,10 @@ describe("GET /on-call-readiness/user/:userId - identifier masking", () => {
|
|
|
1634
1702
|
]);
|
|
1635
1703
|
});
|
|
1636
1704
|
|
|
1637
|
-
test("all
|
|
1705
|
+
test("all nine channels reach the wire carrying the id of their OWN row", async () => {
|
|
1638
1706
|
/*
|
|
1639
1707
|
* The dropdown's entire premise, swept across every channel rather than
|
|
1640
|
-
* spot-checked on one, because the
|
|
1708
|
+
* spot-checked on one, because the nine are nine separate code paths that
|
|
1641
1709
|
* each build their own row and each have their own opportunity to hand over
|
|
1642
1710
|
* somebody else's id - or none at all.
|
|
1643
1711
|
*
|
|
@@ -1650,7 +1718,7 @@ describe("GET /on-call-readiness/user/:userId - identifier masking", () => {
|
|
|
1650
1718
|
const payload: Record<string, unknown> = await readUser();
|
|
1651
1719
|
const methods: Array<Record<string, unknown>> = methodsOf(payload);
|
|
1652
1720
|
|
|
1653
|
-
expect(methods).toHaveLength(
|
|
1721
|
+
expect(methods).toHaveLength(9);
|
|
1654
1722
|
|
|
1655
1723
|
for (const method of methods) {
|
|
1656
1724
|
const methodType: string = method["methodType"] as string;
|
|
@@ -1682,7 +1750,7 @@ describe("GET /on-call-readiness/user/:userId - identifier masking", () => {
|
|
|
1682
1750
|
const payload: Record<string, unknown> = await readUser();
|
|
1683
1751
|
const methods: Array<Record<string, unknown>> = methodsOf(payload);
|
|
1684
1752
|
|
|
1685
|
-
expect(methods).toHaveLength(
|
|
1753
|
+
expect(methods).toHaveLength(9);
|
|
1686
1754
|
|
|
1687
1755
|
for (const method of methods) {
|
|
1688
1756
|
expect(method["methodId"]).not.toBe(subjectUserId.toString());
|
|
@@ -1691,23 +1759,23 @@ describe("GET /on-call-readiness/user/:userId - identifier masking", () => {
|
|
|
1691
1759
|
expect(method["methodId"]).not.toBe(projectId.toString());
|
|
1692
1760
|
}
|
|
1693
1761
|
|
|
1694
|
-
// And all
|
|
1762
|
+
// And all nine are distinct - one id copied across channels is the same bug.
|
|
1695
1763
|
const ids: Array<unknown> = methods.map(
|
|
1696
1764
|
(method: Record<string, unknown>): unknown => {
|
|
1697
1765
|
return method["methodId"];
|
|
1698
1766
|
},
|
|
1699
1767
|
);
|
|
1700
|
-
expect(new Set(ids).size).toBe(
|
|
1768
|
+
expect(new Set(ids).size).toBe(9);
|
|
1701
1769
|
});
|
|
1702
1770
|
|
|
1703
1771
|
test("a method carries its id, its type, its mask and its verification - and nothing else", async () => {
|
|
1704
1772
|
/*
|
|
1705
1773
|
* The containment assertion. Every field on this object ships to every
|
|
1706
1774
|
* administrator of the project, about a row that administrator is not
|
|
1707
|
-
* allowed to read, so the key set is pinned exhaustively on all
|
|
1775
|
+
* allowed to read, so the key set is pinned exhaustively on all nine rather
|
|
1708
1776
|
* than left to whatever the serialiser happens to copy. A `phone`, a
|
|
1709
|
-
* `webhookUrl` or a `
|
|
1710
|
-
* change; it is the exposure this design was built to avoid.
|
|
1777
|
+
* `webhookUrl`, a `telegramChatId` or a `slackUserId` appearing here is not
|
|
1778
|
+
* a formatting change; it is the exposure this design was built to avoid.
|
|
1711
1779
|
*/
|
|
1712
1780
|
const payload: Record<string, unknown> = await readUser();
|
|
1713
1781
|
|
|
@@ -1729,10 +1797,10 @@ describe("GET /on-call-readiness/user/:userId - identifier masking", () => {
|
|
|
1729
1797
|
* Guards the guard, and it is not a formality here: the service drops any
|
|
1730
1798
|
* method row it cannot find an id on, so a fixture regression that emptied
|
|
1731
1799
|
* `methods` would satisfy every "not present" assertion below for the one
|
|
1732
|
-
* reason that proves nothing at all. The
|
|
1800
|
+
* reason that proves nothing at all. The nine masks have to be in the body
|
|
1733
1801
|
* before their absence of raw values means anything.
|
|
1734
1802
|
*/
|
|
1735
|
-
expect(methodsOf(payload)).toHaveLength(
|
|
1803
|
+
expect(methodsOf(payload)).toHaveLength(9);
|
|
1736
1804
|
expect(body).toContain(IDENTIFIER_MASK);
|
|
1737
1805
|
|
|
1738
1806
|
/*
|
|
@@ -1747,6 +1815,8 @@ describe("GET /on-call-readiness/user/:userId - identifier masking", () => {
|
|
|
1747
1815
|
RAW_WHATSAPP,
|
|
1748
1816
|
RAW_HANDLE,
|
|
1749
1817
|
RAW_DEVICE,
|
|
1818
|
+
RAW_SLACK_USERNAME,
|
|
1819
|
+
RAW_TEAMS_USERNAME,
|
|
1750
1820
|
RAW_WEBHOOK_NAME,
|
|
1751
1821
|
RAW_WEBHOOK_URL,
|
|
1752
1822
|
// The notification address's local part on its own, and the hook's host.
|
|
@@ -1754,6 +1824,14 @@ describe("GET /on-call-readiness/user/:userId - identifier masking", () => {
|
|
|
1754
1824
|
"hooks.slack.com",
|
|
1755
1825
|
// The telegram chat id - the addressable target, never the label.
|
|
1756
1826
|
"998877",
|
|
1827
|
+
// The workspace targets - the Slack member id and the Entra object id.
|
|
1828
|
+
RAW_SLACK_USER_ID,
|
|
1829
|
+
RAW_TEAMS_USER_ID,
|
|
1830
|
+
"MEMBERIDLEAK",
|
|
1831
|
+
"ENTRALEAK",
|
|
1832
|
+
// The workspace names' revealing middles, past what a handle mask keeps.
|
|
1833
|
+
"oncall.slack",
|
|
1834
|
+
"[Overnight]",
|
|
1757
1835
|
/*
|
|
1758
1836
|
* The unmasked tails of the four phone-shaped identifiers. maskIdentifier
|
|
1759
1837
|
* keeps the last four digits on purpose, so the numbers themselves are
|
|
@@ -1795,7 +1873,7 @@ describe("GET /on-call-readiness/user/:userId - identifier masking", () => {
|
|
|
1795
1873
|
const payload: Record<string, unknown> = await readUser();
|
|
1796
1874
|
const methods: Array<Record<string, unknown>> = methodsOf(payload);
|
|
1797
1875
|
|
|
1798
|
-
expect(methods).toHaveLength(
|
|
1876
|
+
expect(methods).toHaveLength(9);
|
|
1799
1877
|
|
|
1800
1878
|
for (const method of methods) {
|
|
1801
1879
|
expect(method["methodId"]).not.toBe(ruleId.toString());
|
|
@@ -1870,6 +1948,24 @@ describe("GET /on-call-readiness/user/:userId - identifier masking", () => {
|
|
|
1870
1948
|
expect(call.select?.["telegramUserHandle"]).toBe(true);
|
|
1871
1949
|
expect(call.select?.["telegramChatId"]).toBeUndefined();
|
|
1872
1950
|
});
|
|
1951
|
+
|
|
1952
|
+
test("the slack read takes the username, never the addressable member id", async () => {
|
|
1953
|
+
await readUser();
|
|
1954
|
+
|
|
1955
|
+
const call: CapturedFindBy = firstCall(userSlackFindBy);
|
|
1956
|
+
|
|
1957
|
+
expect(call.select?.["slackUserName"]).toBe(true);
|
|
1958
|
+
expect(call.select?.["slackUserId"]).toBeUndefined();
|
|
1959
|
+
});
|
|
1960
|
+
|
|
1961
|
+
test("the microsoft teams read takes the display name, never the addressable entra id", async () => {
|
|
1962
|
+
await readUser();
|
|
1963
|
+
|
|
1964
|
+
const call: CapturedFindBy = firstCall(userMicrosoftTeamsFindBy);
|
|
1965
|
+
|
|
1966
|
+
expect(call.select?.["microsoftTeamsUserName"]).toBe(true);
|
|
1967
|
+
expect(call.select?.["microsoftTeamsUserId"]).toBeUndefined();
|
|
1968
|
+
});
|
|
1873
1969
|
});
|
|
1874
1970
|
|
|
1875
1971
|
/*
|
|
@@ -1898,6 +1994,8 @@ interface StubRule {
|
|
|
1898
1994
|
userEmailId?: ObjectID | undefined;
|
|
1899
1995
|
userPushId?: ObjectID | undefined;
|
|
1900
1996
|
userTelegramId?: ObjectID | undefined;
|
|
1997
|
+
userSlackId?: ObjectID | undefined;
|
|
1998
|
+
userMicrosoftTeamsId?: ObjectID | undefined;
|
|
1901
1999
|
userWhatsAppId?: ObjectID | undefined;
|
|
1902
2000
|
userWebhookId?: ObjectID | undefined;
|
|
1903
2001
|
}
|
|
@@ -2037,17 +2135,22 @@ describe("GET /team/compliance-status/:teamId - the rebuilt service", () => {
|
|
|
2037
2135
|
]);
|
|
2038
2136
|
});
|
|
2039
2137
|
|
|
2040
|
-
test("DEFECT closed: a rule on Telegram, WhatsApp or a webhook now counts", async () => {
|
|
2138
|
+
test("DEFECT closed: a rule on Telegram, WhatsApp, Slack, Teams or a webhook now counts", async () => {
|
|
2041
2139
|
/*
|
|
2042
2140
|
* The old check read userCallId/userSmsId/userEmailId/userPushId off the
|
|
2043
2141
|
* rule row and treated a row carrying none of them as no rule at all, so a
|
|
2044
2142
|
* responder reachable only on Telegram, WhatsApp or a webhook was reported
|
|
2045
2143
|
* non-compliant while the runtime was quite happily paging them. A false RED
|
|
2046
|
-
* teaches admins to ignore the table, which is worse than no table.
|
|
2144
|
+
* teaches admins to ignore the table, which is worse than no table. Slack
|
|
2145
|
+
* and Microsoft Teams arrived after the fix and are staged alongside so the
|
|
2146
|
+
* defect cannot return for the channels that never lived through it.
|
|
2047
2147
|
*
|
|
2048
|
-
* All
|
|
2148
|
+
* All five channels are staged onto one user at once: whichever column the
|
|
2049
2149
|
* rule carries, the row is a rule.
|
|
2050
2150
|
*/
|
|
2151
|
+
const minor: StubSeverity = { id: ObjectID.generate(), name: "Minor" };
|
|
2152
|
+
const warn: StubSeverity = { id: ObjectID.generate(), name: "Warn" };
|
|
2153
|
+
|
|
2051
2154
|
stage({
|
|
2052
2155
|
settings: [
|
|
2053
2156
|
{ ruleType: ComplianceRuleType.HasIncidentOnCallRules, enabled: true },
|
|
@@ -2069,6 +2172,13 @@ describe("GET /team/compliance-status/:teamId - the rebuilt service", () => {
|
|
|
2069
2172
|
incidentSeverityId: major.id,
|
|
2070
2173
|
userWhatsAppId: ObjectID.generate(),
|
|
2071
2174
|
},
|
|
2175
|
+
{
|
|
2176
|
+
_id: "rule-slack",
|
|
2177
|
+
userId: ada.id,
|
|
2178
|
+
ruleType: NotificationRuleType.ON_CALL_EXECUTED_INCIDENT,
|
|
2179
|
+
incidentSeverityId: minor.id,
|
|
2180
|
+
userSlackId: ObjectID.generate(),
|
|
2181
|
+
},
|
|
2072
2182
|
{
|
|
2073
2183
|
_id: "rule-webhook",
|
|
2074
2184
|
userId: ada.id,
|
|
@@ -2076,9 +2186,16 @@ describe("GET /team/compliance-status/:teamId - the rebuilt service", () => {
|
|
|
2076
2186
|
alertSeverityId: page.id,
|
|
2077
2187
|
userWebhookId: ObjectID.generate(),
|
|
2078
2188
|
},
|
|
2189
|
+
{
|
|
2190
|
+
_id: "rule-microsoft-teams",
|
|
2191
|
+
userId: ada.id,
|
|
2192
|
+
ruleType: NotificationRuleType.ON_CALL_EXECUTED_ALERT,
|
|
2193
|
+
alertSeverityId: warn.id,
|
|
2194
|
+
userMicrosoftTeamsId: ObjectID.generate(),
|
|
2195
|
+
},
|
|
2079
2196
|
],
|
|
2080
|
-
incidentSeverities: [critical, major],
|
|
2081
|
-
alertSeverities: [page],
|
|
2197
|
+
incidentSeverities: [critical, major, minor],
|
|
2198
|
+
alertSeverities: [page, warn],
|
|
2082
2199
|
});
|
|
2083
2200
|
|
|
2084
2201
|
const statuses: Array<Record<string, unknown>> = statusesOf(
|
|
@@ -2093,7 +2210,7 @@ describe("GET /team/compliance-status/:teamId - the rebuilt service", () => {
|
|
|
2093
2210
|
/*
|
|
2094
2211
|
* The structural half of the fix. The three formerly-invisible channels were
|
|
2095
2212
|
* invisible because they were never SELECTed; asserting that NONE of the
|
|
2096
|
-
*
|
|
2213
|
+
* nine is selected means no future edit can reintroduce a partial column
|
|
2097
2214
|
* list and quietly start under-counting again.
|
|
2098
2215
|
*/
|
|
2099
2216
|
stage({
|
|
@@ -2116,6 +2233,8 @@ describe("GET /team/compliance-status/:teamId - the rebuilt service", () => {
|
|
|
2116
2233
|
"userEmailId",
|
|
2117
2234
|
"userPushId",
|
|
2118
2235
|
"userTelegramId",
|
|
2236
|
+
"userSlackId",
|
|
2237
|
+
"userMicrosoftTeamsId",
|
|
2119
2238
|
"userWhatsAppId",
|
|
2120
2239
|
"userWebhookId",
|
|
2121
2240
|
]) {
|
|
@@ -30,7 +30,7 @@ import { beforeEach, describe, expect, test } from "@jest/globals";
|
|
|
30
30
|
* methods".
|
|
31
31
|
*
|
|
32
32
|
* This router is the ONLY gate under these routes, and that is not a stylistic
|
|
33
|
-
* observation. The
|
|
33
|
+
* observation. The nine notification method models are scoped to the person
|
|
34
34
|
* who owns the device — the essay at the top of UserEmail.ts is the record of
|
|
35
35
|
* what happened the one time that scope was widened — so the service behind
|
|
36
36
|
* these handlers does every read and write with `isRoot: true`, which means the
|
|
@@ -362,7 +362,7 @@ describe("the routes it registers", () => {
|
|
|
362
362
|
* Extending BaseAPI to inherit `this.router` would register a whole model's
|
|
363
363
|
* CRUD routes as a side effect — dead code that is shadowed by the real
|
|
364
364
|
* registration until the day the mount order changes. There is no model to
|
|
365
|
-
* inherit here in any case: the
|
|
365
|
+
* inherit here in any case: the nine method models stay owner-scoped and
|
|
366
366
|
* this router deliberately never exposes them.
|
|
367
367
|
*/
|
|
368
368
|
expect(UserNotificationMethodAdminAPI).toBe(mockRouter);
|
|
@@ -459,7 +459,17 @@ describe("LlmLogService autonomous-token lane isolation", () => {
|
|
|
459
459
|
expect(sql).toContain('FROM "AIRun" AS "run"');
|
|
460
460
|
expect(sql).toContain('"run"."triggeredByIncidentId" IS NOT NULL');
|
|
461
461
|
expect(params[3]).toEqual(legacyIncidentFeatures);
|
|
462
|
-
|
|
462
|
+
|
|
463
|
+
/*
|
|
464
|
+
* The incident lane reads only its own legacy list, so it must bind
|
|
465
|
+
* exactly four parameters. Binding the unread alert list as $5 is what
|
|
466
|
+
* PostgreSQL rejected with "bind message supplies 5 parameters, but
|
|
467
|
+
* prepared statement "" requires 4", which silently disabled every capped
|
|
468
|
+
* incident investigation. LlmLogServiceTokenAggregateParams.test.ts holds
|
|
469
|
+
* the exhaustive placeholder/binding contract.
|
|
470
|
+
*/
|
|
471
|
+
expect(params).toHaveLength(4);
|
|
472
|
+
expect(sql).not.toContain("$5");
|
|
463
473
|
});
|
|
464
474
|
|
|
465
475
|
test("subjectless totals exclude identifiable legacy incident and alert rows", async () => {
|