@oneuptime/common 12.0.14 → 12.0.15
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/GlobalOidc.ts +20 -0
- package/Models/DatabaseModels/GlobalSso.ts +20 -0
- package/Models/DatabaseModels/MarketingConversion.ts +19 -9
- package/Models/DatabaseModels/NetworkDeviceLinkRule.ts +46 -0
- package/Models/DatabaseModels/StatusPage.ts +7 -4
- package/Server/API/StatusPageAPI.ts +29 -4
- package/Server/EnvironmentConfig.ts +7 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1787800000000-AddScopeToNetworkDeviceLinkRule.ts +19 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1787900000000-AddRestrictToAttachedProjectsToGlobalSso.ts +40 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +4 -0
- package/Server/Middleware/SlackAuthorization.ts +43 -6
- package/Server/Middleware/UserAuthorization.ts +236 -26
- package/Server/Middleware/WhatsAppAuthorization.ts +43 -3
- package/Server/Services/GlobalOidcProjectService.ts +126 -1
- package/Server/Services/GlobalOidcService.ts +132 -0
- package/Server/Services/GlobalSsoProjectService.ts +126 -1
- package/Server/Services/GlobalSsoService.ts +132 -0
- package/Server/Services/UserEmailService.ts +4 -4
- package/Server/Utils/CronTab.ts +73 -0
- package/Server/Utils/GlobalSsoAuthorization.ts +175 -0
- package/Server/Utils/Marketing/ConversionUploadProvider.ts +35 -5
- package/Server/Utils/Marketing/Providers/GoogleAds.ts +1 -1
- package/Server/Utils/Marketing/Providers/LinkedIn.ts +1 -1
- package/Server/Utils/Marketing/Providers/Meta.ts +1 -1
- package/Server/Utils/Marketing/Providers/MicrosoftAds.ts +1 -1
- package/Server/Utils/Marketing/Providers/Reddit.ts +1 -1
- package/Server/Utils/Monitor/Criteria/APIRequestCriteria.ts +48 -33
- package/Server/Utils/Monitor/Criteria/DnsMonitorCriteria.ts +35 -30
- package/Server/Utils/Monitor/Criteria/DomainMonitorCriteria.ts +33 -28
- package/Server/Utils/Monitor/Criteria/EvaluateOverTime.ts +475 -47
- package/Server/Utils/Monitor/Criteria/ExternalStatusPageMonitorCriteria.ts +35 -30
- package/Server/Utils/Monitor/Criteria/IncomingRequestCriteria.ts +34 -28
- package/Server/Utils/Monitor/Criteria/SSLMonitorCriteria.ts +33 -28
- package/Server/Utils/Monitor/Criteria/ServerMonitorCriteria.ts +69 -40
- package/Server/Utils/Monitor/Criteria/SnmpMonitorCriteria.ts +35 -29
- package/Server/Utils/Monitor/MonitorCriteriaEvaluator.ts +8 -0
- package/Server/Utils/Monitor/MonitorResource.ts +161 -21
- package/Server/Utils/StatusPageContentSecurityPolicy.ts +56 -0
- package/Server/Utils/StatusPageCustomizationAccess.ts +117 -0
- package/Tests/Server/API/StatusPageCustomizationOrigin.test.ts +348 -0
- package/Tests/Server/Infrastructure/Postgres/FixTotpOtpUrlAlgorithmMigration.test.ts +9 -59
- package/Tests/Server/Infrastructure/Postgres/SchemaMigrationsOrdering.test.ts +227 -0
- package/Tests/Server/Middleware/SlackAuthorization.test.ts +149 -0
- package/Tests/Server/Middleware/UserAuthorization.test.ts +27 -13
- package/Tests/Server/Middleware/UserAuthorizationGlobalSsoGovernance.test.ts +1455 -0
- package/Tests/Server/Middleware/UserAuthorizationSSOProvider.test.ts +91 -2
- package/Tests/Server/Middleware/WhatsAppAuthorization.test.ts +233 -0
- package/Tests/Server/Services/DiscoveryScanClaimHookFreeSafety.test.ts +21 -8
- package/Tests/Server/Services/GlobalSsoProviderAuthorization.test.ts +1242 -0
- package/Tests/Server/Services/MonitorResourceProbeAgreementHydration.test.ts +459 -0
- package/Tests/Server/Services/MonitorResourceProbeAgreementReuse.test.ts +10 -0
- package/Tests/Server/Types/AnalyticsDatabase/AggregateUtilBucketTimestamp.test.ts +416 -0
- package/Tests/Server/Types/Database/QueryHelperManyToManyAndEmptyValues.test.ts +551 -0
- package/Tests/Server/Types/Database/SelectUtil.test.ts +348 -0
- package/Tests/Server/Utils/CronTab.test.ts +116 -0
- package/Tests/Server/Utils/GlobalSSOToken.test.ts +581 -0
- package/Tests/Server/Utils/GlobalSsoAuthorization.test.ts +1401 -0
- package/Tests/Server/Utils/Marketing/AdUploadableConversionTypes.test.ts +254 -0
- package/Tests/Server/Utils/Marketing/ConversionUploadProvider.test.ts +1 -1
- package/Tests/Server/Utils/Monitor/Criteria/APIRequestCriteriaEvaluateOverTime.test.ts +508 -0
- package/Tests/Server/Utils/Monitor/Criteria/APIRequestCriteriaPortTimings.test.ts +30 -9
- package/Tests/Server/Utils/Monitor/Criteria/EvaluateOverTime.test.ts +987 -0
- package/Tests/Server/Utils/Monitor/Criteria/ExternalStatusPageMonitorCriteria.test.ts +916 -0
- package/Tests/Server/Utils/Monitor/Criteria/ProfileMonitorCriteria.test.ts +382 -0
- package/Tests/Server/Utils/Monitor/Criteria/ServerMonitorCriteria.test.ts +1219 -0
- package/Tests/Server/Utils/Monitor/MonitorResourceIngestedStepSelection.test.ts +86 -0
- package/Tests/Server/Utils/StatusPageContentSecurityPolicy.test.ts +136 -0
- package/Tests/Server/Utils/StatusPageCustomizationAccess.test.ts +223 -0
- package/Tests/Types/Monitor/MonitorStepConfigHelpers.test.ts +165 -0
- package/Tests/Types/NetworkDevice/NetworkDeviceLinkRuleScope.test.ts +215 -0
- package/Tests/UI/Components/Forms/FormSubmitAnalytics.test.tsx +206 -0
- package/Tests/UI/Utils/AIChatExport/ConversationMarkdown.test.ts +774 -0
- package/Tests/UI/Utils/AIChatExport/MarkdownBlocks.test.ts +791 -0
- package/Tests/UI/Utils/AIChatExport/MarkdownSafety.test.ts +544 -0
- package/Tests/Utils/Analytics.test.ts +187 -0
- package/Tests/Utils/Monitor/MonitorMetricType.test.ts +80 -0
- package/Tests/Utils/Monitor/NetworkDeviceLinkRuleUtil.test.ts +1291 -0
- package/Tests/Utils/NetworkDiscovery/UnclaimedScanDiagnosis.test.ts +214 -0
- package/Types/Marketing/MarketingConversion.ts +12 -0
- package/Types/Monitor/CriteriaFilter.ts +9 -0
- package/Types/NetworkDevice/NetworkDeviceLinkRuleScope.ts +51 -0
- package/UI/Components/FormModal/BasicFormModal.tsx +12 -0
- package/UI/Components/Forms/BasicForm.tsx +13 -1
- package/UI/Components/Forms/BasicModelForm.tsx +2 -1
- package/UI/Components/Forms/ModelForm.tsx +10 -1
- package/UI/Components/Forms/Utils/FormAnalyticsName.ts +39 -0
- package/UI/Components/ModelFormModal/ModelFormModal.tsx +6 -1
- package/Utils/Analytics.ts +55 -2
- package/Utils/Monitor/MonitorMetricType.ts +40 -0
- package/Utils/Monitor/NetworkDeviceLinkRuleUtil.ts +587 -37
- package/Utils/NetworkDiscovery/UnclaimedScanDiagnosis.ts +115 -0
- package/build/dist/Models/DatabaseModels/GlobalOidc.js +21 -0
- package/build/dist/Models/DatabaseModels/GlobalOidc.js.map +1 -1
- package/build/dist/Models/DatabaseModels/GlobalSso.js +21 -0
- package/build/dist/Models/DatabaseModels/GlobalSso.js.map +1 -1
- package/build/dist/Models/DatabaseModels/MarketingConversion.js +20 -10
- package/build/dist/Models/DatabaseModels/MarketingConversion.js.map +1 -1
- package/build/dist/Models/DatabaseModels/NetworkDeviceLinkRule.js +47 -0
- package/build/dist/Models/DatabaseModels/NetworkDeviceLinkRule.js.map +1 -1
- package/build/dist/Models/DatabaseModels/StatusPage.js +4 -4
- package/build/dist/Models/DatabaseModels/StatusPage.js.map +1 -1
- package/build/dist/Server/API/StatusPageAPI.js +25 -4
- package/build/dist/Server/API/StatusPageAPI.js.map +1 -1
- package/build/dist/Server/EnvironmentConfig.js +6 -0
- package/build/dist/Server/EnvironmentConfig.js.map +1 -1
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1787800000000-AddScopeToNetworkDeviceLinkRule.js +12 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1787800000000-AddScopeToNetworkDeviceLinkRule.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1787900000000-AddRestrictToAttachedProjectsToGlobalSso.js +29 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1787900000000-AddRestrictToAttachedProjectsToGlobalSso.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/SlackAuthorization.js +28 -4
- package/build/dist/Server/Middleware/SlackAuthorization.js.map +1 -1
- package/build/dist/Server/Middleware/UserAuthorization.js +197 -22
- package/build/dist/Server/Middleware/UserAuthorization.js.map +1 -1
- package/build/dist/Server/Middleware/WhatsAppAuthorization.js +32 -2
- package/build/dist/Server/Middleware/WhatsAppAuthorization.js.map +1 -1
- package/build/dist/Server/Services/GlobalOidcProjectService.js +105 -0
- package/build/dist/Server/Services/GlobalOidcProjectService.js.map +1 -1
- package/build/dist/Server/Services/GlobalOidcService.js +127 -0
- package/build/dist/Server/Services/GlobalOidcService.js.map +1 -1
- package/build/dist/Server/Services/GlobalSsoProjectService.js +105 -0
- package/build/dist/Server/Services/GlobalSsoProjectService.js.map +1 -1
- package/build/dist/Server/Services/GlobalSsoService.js +127 -0
- package/build/dist/Server/Services/GlobalSsoService.js.map +1 -1
- package/build/dist/Server/Services/UserEmailService.js +4 -4
- package/build/dist/Server/Utils/CronTab.js +66 -0
- package/build/dist/Server/Utils/CronTab.js.map +1 -1
- package/build/dist/Server/Utils/GlobalSsoAuthorization.js +112 -0
- package/build/dist/Server/Utils/GlobalSsoAuthorization.js.map +1 -0
- package/build/dist/Server/Utils/Marketing/ConversionUploadProvider.js +17 -1
- package/build/dist/Server/Utils/Marketing/ConversionUploadProvider.js.map +1 -1
- package/build/dist/Server/Utils/Marketing/Providers/GoogleAds.js +1 -1
- package/build/dist/Server/Utils/Marketing/Providers/GoogleAds.js.map +1 -1
- package/build/dist/Server/Utils/Marketing/Providers/LinkedIn.js +1 -1
- package/build/dist/Server/Utils/Marketing/Providers/LinkedIn.js.map +1 -1
- package/build/dist/Server/Utils/Marketing/Providers/Meta.js +1 -1
- package/build/dist/Server/Utils/Marketing/Providers/Meta.js.map +1 -1
- package/build/dist/Server/Utils/Marketing/Providers/MicrosoftAds.js +1 -1
- package/build/dist/Server/Utils/Marketing/Providers/MicrosoftAds.js.map +1 -1
- package/build/dist/Server/Utils/Marketing/Providers/Reddit.js +1 -1
- package/build/dist/Server/Utils/Marketing/Providers/Reddit.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/Criteria/APIRequestCriteria.js +37 -35
- package/build/dist/Server/Utils/Monitor/Criteria/APIRequestCriteria.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/Criteria/DnsMonitorCriteria.js +21 -25
- package/build/dist/Server/Utils/Monitor/Criteria/DnsMonitorCriteria.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/Criteria/DomainMonitorCriteria.js +20 -22
- package/build/dist/Server/Utils/Monitor/Criteria/DomainMonitorCriteria.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/Criteria/EvaluateOverTime.js +370 -42
- package/build/dist/Server/Utils/Monitor/Criteria/EvaluateOverTime.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/Criteria/ExternalStatusPageMonitorCriteria.js +21 -25
- package/build/dist/Server/Utils/Monitor/Criteria/ExternalStatusPageMonitorCriteria.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/Criteria/IncomingRequestCriteria.js +21 -25
- package/build/dist/Server/Utils/Monitor/Criteria/IncomingRequestCriteria.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/Criteria/SSLMonitorCriteria.js +21 -25
- package/build/dist/Server/Utils/Monitor/Criteria/SSLMonitorCriteria.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/Criteria/ServerMonitorCriteria.js +64 -52
- package/build/dist/Server/Utils/Monitor/Criteria/ServerMonitorCriteria.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/Criteria/SnmpMonitorCriteria.js +25 -29
- package/build/dist/Server/Utils/Monitor/Criteria/SnmpMonitorCriteria.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/MonitorCriteriaEvaluator.js +8 -0
- package/build/dist/Server/Utils/Monitor/MonitorCriteriaEvaluator.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/MonitorResource.js +123 -23
- package/build/dist/Server/Utils/Monitor/MonitorResource.js.map +1 -1
- package/build/dist/Server/Utils/StatusPageContentSecurityPolicy.js +39 -0
- package/build/dist/Server/Utils/StatusPageContentSecurityPolicy.js.map +1 -0
- package/build/dist/Server/Utils/StatusPageCustomizationAccess.js +85 -0
- package/build/dist/Server/Utils/StatusPageCustomizationAccess.js.map +1 -0
- package/build/dist/Types/Marketing/MarketingConversion.js +10 -0
- package/build/dist/Types/Marketing/MarketingConversion.js.map +1 -1
- package/build/dist/Types/Monitor/CriteriaFilter.js +1 -0
- package/build/dist/Types/Monitor/CriteriaFilter.js.map +1 -1
- package/build/dist/Types/NetworkDevice/NetworkDeviceLinkRuleScope.js +46 -0
- package/build/dist/Types/NetworkDevice/NetworkDeviceLinkRuleScope.js.map +1 -0
- package/build/dist/UI/Components/FormModal/BasicFormModal.js +2 -1
- package/build/dist/UI/Components/FormModal/BasicFormModal.js.map +1 -1
- package/build/dist/UI/Components/Forms/BasicForm.js +10 -1
- package/build/dist/UI/Components/Forms/BasicForm.js.map +1 -1
- package/build/dist/UI/Components/Forms/BasicModelForm.js +2 -1
- package/build/dist/UI/Components/Forms/BasicModelForm.js.map +1 -1
- package/build/dist/UI/Components/Forms/ModelForm.js +4 -1
- package/build/dist/UI/Components/Forms/ModelForm.js.map +1 -1
- package/build/dist/UI/Components/Forms/Utils/FormAnalyticsName.js +33 -0
- package/build/dist/UI/Components/Forms/Utils/FormAnalyticsName.js.map +1 -0
- package/build/dist/UI/Components/ModelFormModal/ModelFormModal.js +2 -1
- package/build/dist/UI/Components/ModelFormModal/ModelFormModal.js.map +1 -1
- package/build/dist/Utils/Analytics.js +50 -2
- package/build/dist/Utils/Analytics.js.map +1 -1
- package/build/dist/Utils/Monitor/MonitorMetricType.js +37 -0
- package/build/dist/Utils/Monitor/MonitorMetricType.js.map +1 -1
- package/build/dist/Utils/Monitor/NetworkDeviceLinkRuleUtil.js +318 -37
- package/build/dist/Utils/Monitor/NetworkDeviceLinkRuleUtil.js.map +1 -1
- package/build/dist/Utils/NetworkDiscovery/UnclaimedScanDiagnosis.js +87 -0
- package/build/dist/Utils/NetworkDiscovery/UnclaimedScanDiagnosis.js.map +1 -0
- package/package.json +1 -1
|
@@ -33,20 +33,29 @@ describe("UserMiddleware.doesSsoTokenForProjectExist - requiredSsoProviderId", (
|
|
|
33
33
|
return u;
|
|
34
34
|
};
|
|
35
35
|
|
|
36
|
-
|
|
36
|
+
/*
|
|
37
|
+
* Build a request whose cookies carry a real `sso-<projectId>` token.
|
|
38
|
+
*
|
|
39
|
+
* The type defaults to ProjectSSO because that is what a PROJECT SSO login
|
|
40
|
+
* actually mints, and the project-scoped slot now refuses Global-typed
|
|
41
|
+
* credentials outright - see the "legacy Global-typed token" block below for
|
|
42
|
+
* why, and for the tests that pin it.
|
|
43
|
+
*/
|
|
37
44
|
const buildRequestWithSsoToken: (data: {
|
|
38
45
|
tokenProjectId: ObjectID;
|
|
39
46
|
discriminatorProviderId?: ObjectID | undefined;
|
|
47
|
+
ssoProviderType?: SsoProviderType | undefined;
|
|
40
48
|
}) => ExpressRequest = (data: {
|
|
41
49
|
tokenProjectId: ObjectID;
|
|
42
50
|
discriminatorProviderId?: ObjectID | undefined;
|
|
51
|
+
ssoProviderType?: SsoProviderType | undefined;
|
|
43
52
|
}): ExpressRequest => {
|
|
44
53
|
const token: string = CookieUtil.getSSOToken({
|
|
45
54
|
user: buildUser(),
|
|
46
55
|
projectId: data.tokenProjectId,
|
|
47
56
|
ssoProviderId: data.discriminatorProviderId,
|
|
48
57
|
ssoProviderType: data.discriminatorProviderId
|
|
49
|
-
? SsoProviderType.
|
|
58
|
+
? data.ssoProviderType || SsoProviderType.ProjectSSO
|
|
50
59
|
: undefined,
|
|
51
60
|
});
|
|
52
61
|
|
|
@@ -170,6 +179,86 @@ describe("UserMiddleware.doesSsoTokenForProjectExist - requiredSsoProviderId", (
|
|
|
170
179
|
* a matching token. The token is sourced from the `global-sso-token` cookie
|
|
171
180
|
* (web) or the `x-global-sso-token` header (mobile).
|
|
172
181
|
*/
|
|
182
|
+
/*
|
|
183
|
+
* A Global-typed credential must never be accepted from the per-project slot.
|
|
184
|
+
*
|
|
185
|
+
* Before the single-token redesign, the Global SSO router fanned out one
|
|
186
|
+
* per-project `sso-<projectId>` cookie per project the user belonged to, typed
|
|
187
|
+
* GlobalSSO and signed for 30 days. Those are still in browsers, and the
|
|
188
|
+
* mobile app still replays every non-expired stored token as `x-sso-tokens`.
|
|
189
|
+
* If the project slot accepted them, they would short-circuit past the
|
|
190
|
+
* provider-trust check - so disabling a provider would still not revoke them,
|
|
191
|
+
* for up to a month. They are refused here instead, which sends the user
|
|
192
|
+
* through a fresh global login that mints a properly-typed token.
|
|
193
|
+
*/
|
|
194
|
+
describe("UserMiddleware.doesSsoTokenForProjectExist - legacy Global-typed token in the project slot", () => {
|
|
195
|
+
const legacyProjectId: ObjectID = ObjectID.generate();
|
|
196
|
+
const legacyUserId: ObjectID = ObjectID.generate();
|
|
197
|
+
const legacyProviderId: ObjectID = ObjectID.generate();
|
|
198
|
+
|
|
199
|
+
const buildLegacyRequest: (
|
|
200
|
+
ssoProviderType: SsoProviderType,
|
|
201
|
+
) => ExpressRequest = (ssoProviderType: SsoProviderType): ExpressRequest => {
|
|
202
|
+
const user: User = new User();
|
|
203
|
+
user.id = legacyUserId;
|
|
204
|
+
user.email = new Email("legacy-sso-user@oneuptime.com");
|
|
205
|
+
|
|
206
|
+
const token: string = CookieUtil.getSSOToken({
|
|
207
|
+
user,
|
|
208
|
+
projectId: legacyProjectId,
|
|
209
|
+
ssoProviderId: legacyProviderId,
|
|
210
|
+
ssoProviderType,
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
return {
|
|
214
|
+
cookies: {
|
|
215
|
+
[CookieUtil.getUserSSOKey(legacyProjectId)]: token,
|
|
216
|
+
},
|
|
217
|
+
headers: {},
|
|
218
|
+
} as unknown as ExpressRequest;
|
|
219
|
+
};
|
|
220
|
+
|
|
221
|
+
test("a GlobalSSO-typed token in the project slot is refused", () => {
|
|
222
|
+
expect(
|
|
223
|
+
UserMiddleware.doesSsoTokenForProjectExist(
|
|
224
|
+
buildLegacyRequest(SsoProviderType.GlobalSSO),
|
|
225
|
+
legacyProjectId,
|
|
226
|
+
legacyUserId,
|
|
227
|
+
),
|
|
228
|
+
).toBe(false);
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
test("a GlobalOIDC-typed token in the project slot is refused", () => {
|
|
232
|
+
expect(
|
|
233
|
+
UserMiddleware.doesSsoTokenForProjectExist(
|
|
234
|
+
buildLegacyRequest(SsoProviderType.GlobalOIDC),
|
|
235
|
+
legacyProjectId,
|
|
236
|
+
legacyUserId,
|
|
237
|
+
),
|
|
238
|
+
).toBe(false);
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
test("the SAME token typed ProjectSSO is accepted, so the refusal is about the type and nothing else", () => {
|
|
242
|
+
expect(
|
|
243
|
+
UserMiddleware.doesSsoTokenForProjectExist(
|
|
244
|
+
buildLegacyRequest(SsoProviderType.ProjectSSO),
|
|
245
|
+
legacyProjectId,
|
|
246
|
+
legacyUserId,
|
|
247
|
+
),
|
|
248
|
+
).toBe(true);
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
test("a ProjectOIDC-typed token in the project slot is accepted", () => {
|
|
252
|
+
expect(
|
|
253
|
+
UserMiddleware.doesSsoTokenForProjectExist(
|
|
254
|
+
buildLegacyRequest(SsoProviderType.ProjectOIDC),
|
|
255
|
+
legacyProjectId,
|
|
256
|
+
legacyUserId,
|
|
257
|
+
),
|
|
258
|
+
).toBe(true);
|
|
259
|
+
});
|
|
260
|
+
});
|
|
261
|
+
|
|
173
262
|
describe("UserMiddleware.doesSsoTokenForProjectExist - global SSO token", () => {
|
|
174
263
|
const projectId: ObjectID = ObjectID.generate();
|
|
175
264
|
const otherProjectId: ObjectID = ObjectID.generate();
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
import { describe, expect, test, beforeEach } from "@jest/globals";
|
|
2
|
+
import crypto from "crypto";
|
|
3
|
+
import WhatsAppAuthorization from "../../../Server/Middleware/WhatsAppAuthorization";
|
|
4
|
+
import GlobalConfigService from "../../../Server/Services/GlobalConfigService";
|
|
5
|
+
import Response from "../../../Server/Utils/Response";
|
|
6
|
+
import {
|
|
7
|
+
ExpressResponse,
|
|
8
|
+
NextFunction,
|
|
9
|
+
OneUptimeRequest,
|
|
10
|
+
} from "../../../Server/Utils/Express";
|
|
11
|
+
import BadDataException from "../../../Types/Exception/BadDataException";
|
|
12
|
+
|
|
13
|
+
/*
|
|
14
|
+
* The config lookup is mocked so the signature branches can be exercised
|
|
15
|
+
* without Postgres; the middleware under test must never need a database.
|
|
16
|
+
*/
|
|
17
|
+
jest.mock("../../../Server/Services/GlobalConfigService", () => {
|
|
18
|
+
return {
|
|
19
|
+
__esModule: true,
|
|
20
|
+
default: {
|
|
21
|
+
findOneBy: jest.fn(),
|
|
22
|
+
},
|
|
23
|
+
};
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
jest.mock("../../../Server/Utils/Response", () => {
|
|
27
|
+
return {
|
|
28
|
+
__esModule: true,
|
|
29
|
+
default: {
|
|
30
|
+
sendErrorResponse: jest.fn(),
|
|
31
|
+
},
|
|
32
|
+
};
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
/*
|
|
36
|
+
* Regression cover for the unguarded crypto.timingSafeEqual in
|
|
37
|
+
* isAuthorizedWhatsAppRequest. timingSafeEqual throws RangeError when the two
|
|
38
|
+
* buffers differ in byte length, and the compared value came straight off the
|
|
39
|
+
* attacker-controlled X-Hub-Signature-256 header with no try/catch anywhere
|
|
40
|
+
* above it. The endpoint is unauthenticated by design - the signature IS the
|
|
41
|
+
* authentication - so anyone could send `x-hub-signature-256: nope` and throw
|
|
42
|
+
* the middleware instead of receiving the intended 400.
|
|
43
|
+
*/
|
|
44
|
+
describe("WhatsAppAuthorization.isAuthorizedWhatsAppRequest", () => {
|
|
45
|
+
const APP_SECRET: string = "meta-whatsapp-app-secret";
|
|
46
|
+
const RAW_BODY: string = JSON.stringify({
|
|
47
|
+
object: "whatsapp_business_account",
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
const findOneBy: jest.Mock =
|
|
51
|
+
GlobalConfigService.findOneBy as unknown as jest.Mock;
|
|
52
|
+
const sendErrorResponse: jest.Mock =
|
|
53
|
+
Response.sendErrorResponse as unknown as jest.Mock;
|
|
54
|
+
|
|
55
|
+
const res: ExpressResponse = {} as ExpressResponse;
|
|
56
|
+
let next: NextFunction;
|
|
57
|
+
|
|
58
|
+
type SignFunction = (body: string) => string;
|
|
59
|
+
|
|
60
|
+
const sign: SignFunction = (body: string): string => {
|
|
61
|
+
return `sha256=${crypto
|
|
62
|
+
.createHmac("sha256", APP_SECRET)
|
|
63
|
+
.update(body)
|
|
64
|
+
.digest("hex")}`;
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
type BuildRequestFunction = (
|
|
68
|
+
signature: string | undefined,
|
|
69
|
+
body?: string,
|
|
70
|
+
) => OneUptimeRequest;
|
|
71
|
+
|
|
72
|
+
const buildRequest: BuildRequestFunction = (
|
|
73
|
+
signature: string | undefined,
|
|
74
|
+
body: string = RAW_BODY,
|
|
75
|
+
): OneUptimeRequest => {
|
|
76
|
+
return {
|
|
77
|
+
headers:
|
|
78
|
+
signature === undefined ? {} : { "x-hub-signature-256": signature },
|
|
79
|
+
rawBody: body,
|
|
80
|
+
} as unknown as OneUptimeRequest;
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
beforeEach(() => {
|
|
84
|
+
jest.clearAllMocks();
|
|
85
|
+
next = jest.fn();
|
|
86
|
+
findOneBy.mockResolvedValue({ metaWhatsAppAppSecret: APP_SECRET });
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
test("calls next when the signature matches the body", async () => {
|
|
90
|
+
const req: OneUptimeRequest = buildRequest(sign(RAW_BODY));
|
|
91
|
+
|
|
92
|
+
await WhatsAppAuthorization.isAuthorizedWhatsAppRequest(req, res, next);
|
|
93
|
+
|
|
94
|
+
expect(next).toHaveBeenCalled();
|
|
95
|
+
expect(sendErrorResponse).not.toHaveBeenCalled();
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
test("calls next when the signature matches but the digest is upper-case", async () => {
|
|
99
|
+
const signature: string = sign(RAW_BODY);
|
|
100
|
+
const req: OneUptimeRequest = buildRequest(
|
|
101
|
+
`sha256=${signature.slice("sha256=".length).toUpperCase()}`,
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
await WhatsAppAuthorization.isAuthorizedWhatsAppRequest(req, res, next);
|
|
105
|
+
|
|
106
|
+
expect(next).toHaveBeenCalled();
|
|
107
|
+
expect(sendErrorResponse).not.toHaveBeenCalled();
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
test("rejects a well-formed signature computed over a different body", async () => {
|
|
111
|
+
const req: OneUptimeRequest = buildRequest(
|
|
112
|
+
sign(`${RAW_BODY} tampered`),
|
|
113
|
+
RAW_BODY,
|
|
114
|
+
);
|
|
115
|
+
|
|
116
|
+
await WhatsAppAuthorization.isAuthorizedWhatsAppRequest(req, res, next);
|
|
117
|
+
|
|
118
|
+
expect(next).not.toHaveBeenCalled();
|
|
119
|
+
expect(sendErrorResponse).toHaveBeenCalledWith(
|
|
120
|
+
req,
|
|
121
|
+
res,
|
|
122
|
+
new BadDataException("WhatsApp webhook signature verification failed."),
|
|
123
|
+
);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
test("rejects a well-formed signature computed with a different secret", async () => {
|
|
127
|
+
const req: OneUptimeRequest = buildRequest(
|
|
128
|
+
`sha256=${crypto
|
|
129
|
+
.createHmac("sha256", "some-other-secret")
|
|
130
|
+
.update(RAW_BODY)
|
|
131
|
+
.digest("hex")}`,
|
|
132
|
+
);
|
|
133
|
+
|
|
134
|
+
await WhatsAppAuthorization.isAuthorizedWhatsAppRequest(req, res, next);
|
|
135
|
+
|
|
136
|
+
expect(next).not.toHaveBeenCalled();
|
|
137
|
+
expect(sendErrorResponse).toHaveBeenCalledWith(
|
|
138
|
+
req,
|
|
139
|
+
res,
|
|
140
|
+
new BadDataException("WhatsApp webhook signature verification failed."),
|
|
141
|
+
);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
/*
|
|
145
|
+
* An empty header value is indistinguishable from an absent one, so both
|
|
146
|
+
* take the missing-header branch rather than the malformed-digest one.
|
|
147
|
+
*/
|
|
148
|
+
test.each([
|
|
149
|
+
["absent", undefined],
|
|
150
|
+
["empty", ""],
|
|
151
|
+
])(
|
|
152
|
+
"rejects an %s signature header",
|
|
153
|
+
async (_label: string, signature: string | undefined) => {
|
|
154
|
+
const req: OneUptimeRequest = buildRequest(signature);
|
|
155
|
+
|
|
156
|
+
await WhatsAppAuthorization.isAuthorizedWhatsAppRequest(req, res, next);
|
|
157
|
+
|
|
158
|
+
expect(next).not.toHaveBeenCalled();
|
|
159
|
+
expect(sendErrorResponse).toHaveBeenCalledWith(
|
|
160
|
+
req,
|
|
161
|
+
res,
|
|
162
|
+
new BadDataException("Missing X-Hub-Signature-256 header."),
|
|
163
|
+
);
|
|
164
|
+
},
|
|
165
|
+
);
|
|
166
|
+
|
|
167
|
+
/*
|
|
168
|
+
* Every one of these is a byte length other than the 71 of a real
|
|
169
|
+
* "sha256=" + 64 hex header, which is exactly what used to reach
|
|
170
|
+
* timingSafeEqual and throw.
|
|
171
|
+
*/
|
|
172
|
+
const malformedSignatures: Array<[string, string]> = [
|
|
173
|
+
["a bare word", "nope"],
|
|
174
|
+
["the prefix with no digest", "sha256="],
|
|
175
|
+
["a short digest", "sha256=abc123"],
|
|
176
|
+
["a long digest", `sha256=${"a".repeat(65)}`],
|
|
177
|
+
["the right length but non-hex characters", `sha256=${"z".repeat(64)}`],
|
|
178
|
+
["64 hex characters with no prefix", "a".repeat(64)],
|
|
179
|
+
["the wrong prefix", `sha1=${"a".repeat(64)}`],
|
|
180
|
+
["a whitespace-padded digest", ` sha256=${"a".repeat(64)}`],
|
|
181
|
+
];
|
|
182
|
+
|
|
183
|
+
describe("rejects a malformed signature with 400 rather than throwing", () => {
|
|
184
|
+
test.each(malformedSignatures)(
|
|
185
|
+
"%s",
|
|
186
|
+
async (_label: string, signature: string) => {
|
|
187
|
+
const req: OneUptimeRequest = buildRequest(signature);
|
|
188
|
+
|
|
189
|
+
await expect(
|
|
190
|
+
WhatsAppAuthorization.isAuthorizedWhatsAppRequest(req, res, next),
|
|
191
|
+
).resolves.toBeUndefined();
|
|
192
|
+
|
|
193
|
+
expect(next).not.toHaveBeenCalled();
|
|
194
|
+
expect(sendErrorResponse).toHaveBeenCalledWith(
|
|
195
|
+
req,
|
|
196
|
+
res,
|
|
197
|
+
new BadDataException(
|
|
198
|
+
"WhatsApp webhook signature verification failed.",
|
|
199
|
+
),
|
|
200
|
+
);
|
|
201
|
+
},
|
|
202
|
+
);
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
/*
|
|
206
|
+
* The shape check sits in front of the config lookup, so an unauthenticated
|
|
207
|
+
* flood of garbage signatures costs no database round trips.
|
|
208
|
+
*/
|
|
209
|
+
test("does not query the global config for a malformed signature", async () => {
|
|
210
|
+
await WhatsAppAuthorization.isAuthorizedWhatsAppRequest(
|
|
211
|
+
buildRequest("nope"),
|
|
212
|
+
res,
|
|
213
|
+
next,
|
|
214
|
+
);
|
|
215
|
+
|
|
216
|
+
expect(findOneBy).not.toHaveBeenCalled();
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
test("rejects when the app secret is not configured", async () => {
|
|
220
|
+
findOneBy.mockResolvedValue({ metaWhatsAppAppSecret: " " });
|
|
221
|
+
|
|
222
|
+
const req: OneUptimeRequest = buildRequest(sign(RAW_BODY));
|
|
223
|
+
|
|
224
|
+
await WhatsAppAuthorization.isAuthorizedWhatsAppRequest(req, res, next);
|
|
225
|
+
|
|
226
|
+
expect(next).not.toHaveBeenCalled();
|
|
227
|
+
expect(sendErrorResponse).toHaveBeenCalledWith(
|
|
228
|
+
req,
|
|
229
|
+
res,
|
|
230
|
+
new BadDataException("Meta WhatsApp App Secret is not configured."),
|
|
231
|
+
);
|
|
232
|
+
});
|
|
233
|
+
});
|
|
@@ -8,7 +8,8 @@ import { describe, expect, test } from "@jest/globals";
|
|
|
8
8
|
/*
|
|
9
9
|
* The /probe-ingest/probe/discovery-scan/list route hands the requesting
|
|
10
10
|
* probe its pending subnet scans and claims them (status "In Progress" +
|
|
11
|
-
* startedAt
|
|
11
|
+
* startedAt, and a cleared statusMessage) via
|
|
12
|
+
* DatabaseService.updateColumnsByIdWithoutHooks — one raw
|
|
12
13
|
* parameterized UPDATE that skips ALL on-update hooks: workflow HTTP
|
|
13
14
|
* triggers, audit-log inserts, realtime events, service
|
|
14
15
|
* onBeforeUpdate/onUpdateSuccess. The probe synchronously waits on this
|
|
@@ -18,8 +19,8 @@ import { describe, expect, test } from "@jest/globals";
|
|
|
18
19
|
* pre-fetch SELECT + row re-fetch + save() transaction).
|
|
19
20
|
*
|
|
20
21
|
* - App/FeatureSet/Telemetry/API/ProbeIngest/DiscoveryScan.ts
|
|
21
|
-
* NetworkDeviceDiscoveryScan.status/startedAt when a
|
|
22
|
-
* Pending scan
|
|
22
|
+
* NetworkDeviceDiscoveryScan.status/startedAt/statusMessage when a
|
|
23
|
+
* probe claims a Pending scan
|
|
23
24
|
*
|
|
24
25
|
* The conversion dropped NOTHING: the model declares no update workflow, no
|
|
25
26
|
* audit logging and no realtime events. But the fast path skips hooks
|
|
@@ -36,8 +37,8 @@ import { describe, expect, test } from "@jest/globals";
|
|
|
36
37
|
* be written by a root caller that bypasses the create-time check. That hook
|
|
37
38
|
* is deliberately safe to skip here, and the assertions below pin exactly
|
|
38
39
|
* why rather than merely restating that it exists: the claim write stamps
|
|
39
|
-
* only `status` and `
|
|
40
|
-
* hook is a pass-through for that payload. If the hook ever grows to
|
|
40
|
+
* only `status`, `startedAt` and `statusMessage`, none of which the hook
|
|
41
|
+
* looks at, and the hook is a pass-through for that payload. If the hook ever grows to
|
|
41
42
|
* validate a column the claim write touches — or the claim write grows to
|
|
42
43
|
* touch `cidr` — these fail.
|
|
43
44
|
*
|
|
@@ -79,7 +80,11 @@ describe("discovery-scan claim hookless write safety preconditions", () => {
|
|
|
79
80
|
*/
|
|
80
81
|
test("NetworkDeviceDiscoveryScan has every column the claim write stamps", () => {
|
|
81
82
|
const scan: NetworkDeviceDiscoveryScan = new NetworkDeviceDiscoveryScan();
|
|
82
|
-
const fastPathColumns: Array<string> = [
|
|
83
|
+
const fastPathColumns: Array<string> = [
|
|
84
|
+
"status",
|
|
85
|
+
"startedAt",
|
|
86
|
+
"statusMessage",
|
|
87
|
+
];
|
|
83
88
|
for (const column of fastPathColumns) {
|
|
84
89
|
expect(scan.isTableColumn(column)).toBe(true);
|
|
85
90
|
}
|
|
@@ -130,7 +135,11 @@ describe("discovery-scan claim hookless write safety preconditions", () => {
|
|
|
130
135
|
* overlap shows up here.
|
|
131
136
|
*/
|
|
132
137
|
test("the claim write's columns are disjoint from what onBeforeUpdate validates", () => {
|
|
133
|
-
const claimWriteColumns: Array<string> = [
|
|
138
|
+
const claimWriteColumns: Array<string> = [
|
|
139
|
+
"status",
|
|
140
|
+
"startedAt",
|
|
141
|
+
"statusMessage",
|
|
142
|
+
];
|
|
134
143
|
const columnsValidatedByHook: Array<string> = ["cidr"];
|
|
135
144
|
|
|
136
145
|
for (const column of claimWriteColumns) {
|
|
@@ -146,7 +155,11 @@ describe("discovery-scan claim hookless write safety preconditions", () => {
|
|
|
146
155
|
test("onBeforeUpdate is a pass-through for the exact claim payload", async () => {
|
|
147
156
|
const claimUpdateBy: unknown = {
|
|
148
157
|
query: { _id: "some-scan-id" },
|
|
149
|
-
data: {
|
|
158
|
+
data: {
|
|
159
|
+
status: "In Progress",
|
|
160
|
+
startedAt: new Date(0),
|
|
161
|
+
statusMessage: null,
|
|
162
|
+
},
|
|
150
163
|
props: { isRoot: true },
|
|
151
164
|
limit: 1,
|
|
152
165
|
skip: 0,
|