@oneuptime/common 12.0.28 → 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/DatabaseModels/DashboardDomain.ts +43 -0
- package/Models/DatabaseModels/MonitorTemplate.ts +15 -3
- 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/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/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/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/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 +62 -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/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/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/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/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
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { describe, expect, it } from "@jest/globals";
|
|
2
|
+
import ejs from "ejs";
|
|
3
|
+
import fs from "fs";
|
|
4
|
+
import { JSDOM } from "jsdom";
|
|
5
|
+
import path from "path";
|
|
6
|
+
|
|
7
|
+
const INDEX_TEMPLATE: string = path.join(
|
|
8
|
+
__dirname,
|
|
9
|
+
"..",
|
|
10
|
+
"..",
|
|
11
|
+
"..",
|
|
12
|
+
"..",
|
|
13
|
+
"App",
|
|
14
|
+
"FeatureSet",
|
|
15
|
+
"StatusPage",
|
|
16
|
+
"views",
|
|
17
|
+
"index.ejs",
|
|
18
|
+
);
|
|
19
|
+
|
|
20
|
+
const STORAGE_KEY: string = "oneuptime-status-page-login-code";
|
|
21
|
+
|
|
22
|
+
function renderIndexPage(enableGoogleTagManager: boolean): string {
|
|
23
|
+
return ejs.render(fs.readFileSync(INDEX_TEMPLATE, "utf8"), {
|
|
24
|
+
title: "Acme Status",
|
|
25
|
+
enableGoogleTagManager,
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function loadPage(url: string, existingCode?: string): JSDOM {
|
|
30
|
+
return new JSDOM(renderIndexPage(true), {
|
|
31
|
+
url,
|
|
32
|
+
runScripts: "dangerously",
|
|
33
|
+
beforeParse: (pageWindow: JSDOM["window"]): void => {
|
|
34
|
+
(pageWindow as any).tailwind = { config: {} };
|
|
35
|
+
|
|
36
|
+
if (existingCode) {
|
|
37
|
+
pageWindow.sessionStorage.setItem(STORAGE_KEY, existingCode);
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
describe("Status Page login-code head bootstrap", () => {
|
|
44
|
+
it("runs before every external script and declares a no-referrer policy", () => {
|
|
45
|
+
const html: string = renderIndexPage(true);
|
|
46
|
+
const bootstrap: number = html.indexOf(`var storageKey = "${STORAGE_KEY}"`);
|
|
47
|
+
const firstExternalScript: number = html.indexOf(
|
|
48
|
+
'<script src="/status-page/env.js">',
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
expect(html).toContain('<meta name="referrer" content="no-referrer" />');
|
|
52
|
+
expect(bootstrap).toBeGreaterThan(-1);
|
|
53
|
+
expect(firstExternalScript).toBeGreaterThan(bootstrap);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it("captures and removes the code and legacy token before page scripts run", () => {
|
|
57
|
+
const page: JSDOM = loadPage(
|
|
58
|
+
"https://status.example/incidents?keep=yes&loginCode=single-use-code&token=legacy#event",
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
expect(page.window.location.href).toBe(
|
|
62
|
+
"https://status.example/incidents?keep=yes#event",
|
|
63
|
+
);
|
|
64
|
+
expect(page.window.sessionStorage.getItem(STORAGE_KEY)).toBe(
|
|
65
|
+
"single-use-code",
|
|
66
|
+
);
|
|
67
|
+
expect(
|
|
68
|
+
(page.window as any).__ONEUPTIME_STATUS_PAGE_LOGIN_HANDOFF_PENDING__,
|
|
69
|
+
).toBe(true);
|
|
70
|
+
page.window.close();
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it("does not start Google Tag Manager while a bearer code is pending", () => {
|
|
74
|
+
const page: JSDOM = loadPage(
|
|
75
|
+
"https://status.example/?loginCode=single-use-code",
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
expect((page.window as any).dataLayer).toBeUndefined();
|
|
79
|
+
expect(
|
|
80
|
+
page.window.document.querySelector(
|
|
81
|
+
'script[src^="https://www.googletagmanager.com/gtm.js"]',
|
|
82
|
+
),
|
|
83
|
+
).toBeNull();
|
|
84
|
+
page.window.close();
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it("keeps analytics enabled on ordinary status-page visits", () => {
|
|
88
|
+
const page: JSDOM = loadPage("https://status.example/");
|
|
89
|
+
|
|
90
|
+
expect((page.window as any).dataLayer).toHaveLength(1);
|
|
91
|
+
page.window.close();
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it("continues suppressing analytics after a reload until React consumes the stored code", () => {
|
|
95
|
+
const page: JSDOM = loadPage(
|
|
96
|
+
"https://status.example/",
|
|
97
|
+
"stored-single-use-code",
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
expect((page.window as any).dataLayer).toBeUndefined();
|
|
101
|
+
expect(page.window.sessionStorage.getItem(STORAGE_KEY)).toBe(
|
|
102
|
+
"stored-single-use-code",
|
|
103
|
+
);
|
|
104
|
+
page.window.close();
|
|
105
|
+
});
|
|
106
|
+
});
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import LoginCodeUtil, {
|
|
2
|
+
STATUS_PAGE_LOGIN_CODE_STORAGE_KEY,
|
|
3
|
+
} from "../../../../App/FeatureSet/StatusPage/src/Utils/LoginCode";
|
|
4
|
+
import { beforeEach, describe, expect, it } from "@jest/globals";
|
|
5
|
+
|
|
6
|
+
type HandoffWindow = Window & {
|
|
7
|
+
__ONEUPTIME_STATUS_PAGE_LOGIN_CODE__?: string | undefined;
|
|
8
|
+
__ONEUPTIME_STATUS_PAGE_LOGIN_HANDOFF_PENDING__?: boolean | undefined;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
const handoffWindow: HandoffWindow = window as HandoffWindow;
|
|
12
|
+
|
|
13
|
+
describe("Status Page login-code consumption", () => {
|
|
14
|
+
beforeEach(() => {
|
|
15
|
+
window.history.replaceState(window.history.state, "", "/");
|
|
16
|
+
window.sessionStorage.clear();
|
|
17
|
+
delete handoffWindow.__ONEUPTIME_STATUS_PAGE_LOGIN_CODE__;
|
|
18
|
+
delete handoffWindow.__ONEUPTIME_STATUS_PAGE_LOGIN_HANDOFF_PENDING__;
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it("consumes the code captured in sessionStorage exactly once", () => {
|
|
22
|
+
window.sessionStorage.setItem(
|
|
23
|
+
STATUS_PAGE_LOGIN_CODE_STORAGE_KEY,
|
|
24
|
+
"captured-code",
|
|
25
|
+
);
|
|
26
|
+
handoffWindow.__ONEUPTIME_STATUS_PAGE_LOGIN_HANDOFF_PENDING__ = true;
|
|
27
|
+
|
|
28
|
+
expect(LoginCodeUtil.consume()).toBe("captured-code");
|
|
29
|
+
expect(LoginCodeUtil.consume()).toBeNull();
|
|
30
|
+
expect(
|
|
31
|
+
window.sessionStorage.getItem(STATUS_PAGE_LOGIN_CODE_STORAGE_KEY),
|
|
32
|
+
).toBeNull();
|
|
33
|
+
expect(handoffWindow.__ONEUPTIME_STATUS_PAGE_LOGIN_HANDOFF_PENDING__).toBe(
|
|
34
|
+
false,
|
|
35
|
+
);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it("can be read repeatedly during React Strict Mode initialization", () => {
|
|
39
|
+
window.sessionStorage.setItem(
|
|
40
|
+
STATUS_PAGE_LOGIN_CODE_STORAGE_KEY,
|
|
41
|
+
"strict-mode-code",
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
expect(LoginCodeUtil.peek()).toBe("strict-mode-code");
|
|
45
|
+
expect(LoginCodeUtil.peek()).toBe("strict-mode-code");
|
|
46
|
+
expect(
|
|
47
|
+
window.sessionStorage.getItem(STATUS_PAGE_LOGIN_CODE_STORAGE_KEY),
|
|
48
|
+
).toBe("strict-mode-code");
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it("uses and deletes the in-memory fallback when storage is unavailable", () => {
|
|
52
|
+
handoffWindow.__ONEUPTIME_STATUS_PAGE_LOGIN_CODE__ = "memory-code";
|
|
53
|
+
|
|
54
|
+
expect(LoginCodeUtil.consume()).toBe("memory-code");
|
|
55
|
+
expect(handoffWindow.__ONEUPTIME_STATUS_PAGE_LOGIN_CODE__).toBeUndefined();
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it("accepts a query code when the early bootstrap is not present", () => {
|
|
59
|
+
window.history.replaceState(
|
|
60
|
+
window.history.state,
|
|
61
|
+
"",
|
|
62
|
+
"/?loginCode=query-code",
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
expect(LoginCodeUtil.consume()).toBe("query-code");
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it("prefers the current query handoff over stale stored state and clears both", () => {
|
|
69
|
+
window.history.replaceState(
|
|
70
|
+
window.history.state,
|
|
71
|
+
"",
|
|
72
|
+
"/?loginCode=fresh-code",
|
|
73
|
+
);
|
|
74
|
+
window.sessionStorage.setItem(
|
|
75
|
+
STATUS_PAGE_LOGIN_CODE_STORAGE_KEY,
|
|
76
|
+
"stale-code",
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
expect(LoginCodeUtil.consume()).toBe("fresh-code");
|
|
80
|
+
expect(
|
|
81
|
+
window.sessionStorage.getItem(STATUS_PAGE_LOGIN_CODE_STORAGE_KEY),
|
|
82
|
+
).toBeNull();
|
|
83
|
+
});
|
|
84
|
+
});
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import StatusPageUtil from "../../../../App/FeatureSet/StatusPage/src/Utils/StatusPage";
|
|
2
|
+
import { beforeEach, describe, expect, test } from "@jest/globals";
|
|
3
|
+
|
|
4
|
+
type SetUrlFunction = (url: string) => void;
|
|
5
|
+
|
|
6
|
+
const setUrl: SetUrlFunction = (url: string): void => {
|
|
7
|
+
window.history.replaceState(window.history.state, "", url);
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
const setRedirectUrl: (redirectUrl: string | null) => void = (
|
|
11
|
+
redirectUrl: string | null,
|
|
12
|
+
): void => {
|
|
13
|
+
setUrl(
|
|
14
|
+
redirectUrl === null
|
|
15
|
+
? "/login"
|
|
16
|
+
: `/login?redirectUrl=${encodeURIComponent(redirectUrl)}`,
|
|
17
|
+
);
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
describe("Status Page redirect safety", () => {
|
|
21
|
+
beforeEach(() => {
|
|
22
|
+
setUrl("/login");
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
describe("getSafeRedirectPath", () => {
|
|
26
|
+
test.each([
|
|
27
|
+
"/",
|
|
28
|
+
"/incidents/incident-id",
|
|
29
|
+
"/scheduled-maintenance/event-id?tab=timeline#note",
|
|
30
|
+
"/status-page/status-page-id/incidents/incident-id",
|
|
31
|
+
"/incidents?next=https://evil.example/path",
|
|
32
|
+
"/literal/javascript:alert(1)",
|
|
33
|
+
])("accepts same-origin application path %s", (redirectPath: string) => {
|
|
34
|
+
expect(StatusPageUtil.getSafeRedirectPath(redirectPath)).toBe(
|
|
35
|
+
redirectPath,
|
|
36
|
+
);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test.each([
|
|
40
|
+
null,
|
|
41
|
+
"",
|
|
42
|
+
"incidents/incident-id",
|
|
43
|
+
"?tab=timeline",
|
|
44
|
+
"#note",
|
|
45
|
+
"javascript:alert(document.domain)",
|
|
46
|
+
"JaVaScRiPt:alert(1)",
|
|
47
|
+
"data:text/plain,hello",
|
|
48
|
+
"http:/evil.example/path",
|
|
49
|
+
"http://evil.example/path",
|
|
50
|
+
"https://evil.example/path",
|
|
51
|
+
"//evil.example/path",
|
|
52
|
+
"///evil.example/path",
|
|
53
|
+
"\\evil.example/path",
|
|
54
|
+
"/\\evil.example/path",
|
|
55
|
+
'/"',
|
|
56
|
+
"/☃",
|
|
57
|
+
"/\u0000control",
|
|
58
|
+
])("rejects unsafe redirect target %s", (redirectPath: string | null) => {
|
|
59
|
+
expect(StatusPageUtil.getSafeRedirectPath(redirectPath)).toBeNull();
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test("rejects an absolute URL on the current origin", () => {
|
|
63
|
+
expect(
|
|
64
|
+
StatusPageUtil.getSafeRedirectPath(
|
|
65
|
+
`${window.location.origin}/incidents/incident-id`,
|
|
66
|
+
),
|
|
67
|
+
).toBeNull();
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
test.each([
|
|
71
|
+
"/login",
|
|
72
|
+
"/LOGIN?next=/incidents/incident-id",
|
|
73
|
+
"/login#sign-in",
|
|
74
|
+
"/status-page/status-page-id/sso",
|
|
75
|
+
"/status-page/status-page-id/sso#provider",
|
|
76
|
+
"/master-password?redirectUrl=/incidents/incident-id",
|
|
77
|
+
"/status-page/status-page-id/forgot-password",
|
|
78
|
+
"/reset-password#token",
|
|
79
|
+
])("rejects authentication-loop path %s", (redirectPath: string) => {
|
|
80
|
+
expect(StatusPageUtil.getSafeRedirectPath(redirectPath)).toBeNull();
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
describe("getSafeRedirectUrl", () => {
|
|
85
|
+
test("reads and percent-decodes a safe redirect query value", () => {
|
|
86
|
+
const redirectPath: string =
|
|
87
|
+
"/scheduled-maintenance/event-id?tab=timeline#note";
|
|
88
|
+
|
|
89
|
+
setRedirectUrl(redirectPath);
|
|
90
|
+
|
|
91
|
+
expect(StatusPageUtil.getSafeRedirectUrl()).toBe(redirectPath);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
test.each([
|
|
95
|
+
"javascript:alert(document.domain)",
|
|
96
|
+
"//evil.example/path",
|
|
97
|
+
"/\\evil.example/path",
|
|
98
|
+
])(
|
|
99
|
+
"rejects percent-encoded unsafe redirect query value %s",
|
|
100
|
+
(redirectPath: string) => {
|
|
101
|
+
setRedirectUrl(redirectPath);
|
|
102
|
+
|
|
103
|
+
expect(StatusPageUtil.getSafeRedirectUrl()).toBeNull();
|
|
104
|
+
},
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
test("returns null when redirectUrl is missing", () => {
|
|
108
|
+
setRedirectUrl(null);
|
|
109
|
+
|
|
110
|
+
expect(StatusPageUtil.getSafeRedirectUrl()).toBeNull();
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
test("returns null when redirectUrl is present but empty", () => {
|
|
114
|
+
setUrl("/login?redirectUrl=");
|
|
115
|
+
|
|
116
|
+
expect(StatusPageUtil.getSafeRedirectUrl()).toBeNull();
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
});
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MonitorTemplate.monitorName column contract (issue #3486).
|
|
3
|
+
*
|
|
4
|
+
* A Network Device auto-import rule names every monitor it provisions
|
|
5
|
+
* "<device> - <this column>". While the column was required there was nothing
|
|
6
|
+
* an operator could type that would not become a suffix on every device the
|
|
7
|
+
* rule imported: a router already called UN0660WANRTR01 came back as
|
|
8
|
+
* "UN0660WANRTR01 - Unit Router", for the whole estate, because the field
|
|
9
|
+
* refused to be left empty.
|
|
10
|
+
*
|
|
11
|
+
* The properties pinned below are the ones that, quietly changed, put that
|
|
12
|
+
* wall back:
|
|
13
|
+
*
|
|
14
|
+
* - OPTIONAL, at BOTH layers. `TableColumn.required` is what
|
|
15
|
+
* DatabaseService.checkRequiredFields reads on create, and `Column.nullable`
|
|
16
|
+
* is the physical constraint. Either one alone still refuses the write.
|
|
17
|
+
* - NOT DEFAULTED and not backfilled. A default would hand the same invented
|
|
18
|
+
* suffix back to every template that never asked for one, and would rename
|
|
19
|
+
* nothing that already exists.
|
|
20
|
+
* - STILL UPDATABLE, and still a ShortText of the same width. The point of
|
|
21
|
+
* the change is that the box can be emptied, which needs an update
|
|
22
|
+
* permission; ModelForm drops a field whose column grants none.
|
|
23
|
+
*
|
|
24
|
+
* The migration half of the contract is pinned here too, because a model that
|
|
25
|
+
* says nullable over a column that is still NOT NULL fails at the database
|
|
26
|
+
* rather than in review — and because the Postgres schema-drift job only
|
|
27
|
+
* catches that when the migration is REGISTERED, not merely written.
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
import MonitorTemplate from "../../Models/DatabaseModels/MonitorTemplate";
|
|
31
|
+
import { TableColumnMetadata } from "../../Types/Database/TableColumn";
|
|
32
|
+
import TableColumnType from "../../Types/Database/TableColumnType";
|
|
33
|
+
import ColumnLength from "../../Types/Database/ColumnLength";
|
|
34
|
+
import Columns from "../../Types/Database/Columns";
|
|
35
|
+
import Permission from "../../Types/Permission";
|
|
36
|
+
import { describe, expect, test } from "@jest/globals";
|
|
37
|
+
import { getMetadataArgsStorage } from "typeorm";
|
|
38
|
+
import { ColumnMetadataArgs } from "typeorm/metadata-args/ColumnMetadataArgs";
|
|
39
|
+
import fs from "fs";
|
|
40
|
+
import path from "path";
|
|
41
|
+
|
|
42
|
+
const COLUMN: string = "monitorName";
|
|
43
|
+
|
|
44
|
+
const MIGRATIONS_DIR: string = path.join(
|
|
45
|
+
__dirname,
|
|
46
|
+
"..",
|
|
47
|
+
"..",
|
|
48
|
+
"Server",
|
|
49
|
+
"Infrastructure",
|
|
50
|
+
"Postgres",
|
|
51
|
+
"SchemaMigrations",
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
const MIGRATION_CLASS: string =
|
|
55
|
+
"AllowNullMonitorNameOnMonitorTemplate1790200000000";
|
|
56
|
+
|
|
57
|
+
const MIGRATION_PATH: string = path.join(
|
|
58
|
+
MIGRATIONS_DIR,
|
|
59
|
+
"1790200000000-AllowNullMonitorNameOnMonitorTemplate.ts",
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
function metadata(): TableColumnMetadata {
|
|
63
|
+
return new MonitorTemplate().getTableColumnMetadata(COLUMN);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function typeOrmColumn(): ColumnMetadataArgs | undefined {
|
|
67
|
+
return getMetadataArgsStorage().columns.find((column: ColumnMetadataArgs) => {
|
|
68
|
+
return column.target === MonitorTemplate && column.propertyName === COLUMN;
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
describe("MonitorTemplate.monitorName", () => {
|
|
73
|
+
test("exists as a ShortText column", () => {
|
|
74
|
+
expect(metadata()).toBeDefined();
|
|
75
|
+
expect(metadata().type).toBe(TableColumnType.ShortText);
|
|
76
|
+
expect(metadata().title).toBe("Monitor Name");
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
/*
|
|
80
|
+
* The whole point of the change. Both halves are asserted because they are
|
|
81
|
+
* enforced in different places and either one alone still rejects a
|
|
82
|
+
* template with no default monitor name: `required` by
|
|
83
|
+
* DatabaseService.checkRequiredFields on create, `nullable` by Postgres.
|
|
84
|
+
*/
|
|
85
|
+
test("is optional at the API layer, so a template can be created without one", () => {
|
|
86
|
+
expect(metadata().required).toBeFalsy();
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
test("is nullable in the database, so the column can actually hold no name", () => {
|
|
90
|
+
expect(typeOrmColumn()).toBeDefined();
|
|
91
|
+
expect(typeOrmColumn()?.options.nullable).toBe(true);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
test("still stores at the width the monitor name column itself allows", () => {
|
|
95
|
+
expect(typeOrmColumn()?.options.length).toBe(ColumnLength.ShortText);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
/*
|
|
99
|
+
* A default is the failure mode this issue is about, wearing a different
|
|
100
|
+
* hat: it would suffix every template that never asked to be suffixed, and
|
|
101
|
+
* it would do so invisibly, since nothing surfaces a defaulted value as
|
|
102
|
+
* "unset".
|
|
103
|
+
*/
|
|
104
|
+
test("has no default value", () => {
|
|
105
|
+
expect(typeOrmColumn()?.options.default).toBeUndefined();
|
|
106
|
+
expect(new MonitorTemplate().isDefaultValueColumn(COLUMN)).toBe(false);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
test("is not unique - a default name is a label, not a key", () => {
|
|
110
|
+
expect(typeOrmColumn()?.options.unique).toBeFalsy();
|
|
111
|
+
|
|
112
|
+
const uniqueColumns: Columns = new MonitorTemplate().getUniqueColumns();
|
|
113
|
+
|
|
114
|
+
expect(uniqueColumns.columns).not.toContain(COLUMN);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
/*
|
|
118
|
+
* The template's OWN name is what is slugged. Slugging the default monitor
|
|
119
|
+
* name would make an optional field decide a URL.
|
|
120
|
+
*/
|
|
121
|
+
test("is not the slugified column", () => {
|
|
122
|
+
expect(new MonitorTemplate().getSlugifyColumn()).toBe("templateName");
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
test("its description tells the operator what leaving it blank does", () => {
|
|
126
|
+
expect(metadata().description).toContain("Leave it blank");
|
|
127
|
+
});
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
describe("MonitorTemplate.monitorName access control", () => {
|
|
131
|
+
const CREATORS: Array<Permission> = [
|
|
132
|
+
Permission.ProjectOwner,
|
|
133
|
+
Permission.ProjectAdmin,
|
|
134
|
+
Permission.ProjectMember,
|
|
135
|
+
Permission.MonitorAdmin,
|
|
136
|
+
Permission.MonitorMember,
|
|
137
|
+
Permission.CreateMonitorTemplate,
|
|
138
|
+
];
|
|
139
|
+
|
|
140
|
+
const READERS: Array<Permission> = [
|
|
141
|
+
Permission.ProjectOwner,
|
|
142
|
+
Permission.ProjectAdmin,
|
|
143
|
+
Permission.ProjectMember,
|
|
144
|
+
Permission.Viewer,
|
|
145
|
+
Permission.MonitorAdmin,
|
|
146
|
+
Permission.MonitorMember,
|
|
147
|
+
Permission.MonitorViewer,
|
|
148
|
+
Permission.ReadMonitorTemplate,
|
|
149
|
+
];
|
|
150
|
+
|
|
151
|
+
const EDITORS: Array<Permission> = [
|
|
152
|
+
Permission.ProjectOwner,
|
|
153
|
+
Permission.ProjectAdmin,
|
|
154
|
+
Permission.ProjectMember,
|
|
155
|
+
Permission.MonitorAdmin,
|
|
156
|
+
Permission.MonitorMember,
|
|
157
|
+
Permission.EditMonitorTemplate,
|
|
158
|
+
];
|
|
159
|
+
|
|
160
|
+
test("is set by whoever may create a template", () => {
|
|
161
|
+
expect(
|
|
162
|
+
new MonitorTemplate().getColumnAccessControlFor(COLUMN)?.create,
|
|
163
|
+
).toEqual(CREATORS);
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
test("is read by everyone who may read a template, viewers included", () => {
|
|
167
|
+
expect(
|
|
168
|
+
new MonitorTemplate().getColumnAccessControlFor(COLUMN)?.read,
|
|
169
|
+
).toEqual(READERS);
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
/*
|
|
173
|
+
* Load-bearing for the fix, not incidental. Making the field optional is
|
|
174
|
+
* only half of it - an operator with an existing template needs to be able
|
|
175
|
+
* to CLEAR the name they were forced to invent, and ModelForm drops a field
|
|
176
|
+
* whose column grants no update permission, which would leave the Edit
|
|
177
|
+
* dialog unable to save the one thing it edits.
|
|
178
|
+
*/
|
|
179
|
+
test("can be cleared by whoever may edit the template", () => {
|
|
180
|
+
expect(
|
|
181
|
+
new MonitorTemplate().getColumnAccessControlFor(COLUMN)?.update,
|
|
182
|
+
).toEqual(EDITORS);
|
|
183
|
+
|
|
184
|
+
const tableUpdatePermissions: Array<Permission> =
|
|
185
|
+
new MonitorTemplate().getUpdatePermissions();
|
|
186
|
+
|
|
187
|
+
for (const permission of EDITORS) {
|
|
188
|
+
expect(tableUpdatePermissions).toContain(permission);
|
|
189
|
+
}
|
|
190
|
+
});
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
describe("MonitorTemplate.monitorName migration", () => {
|
|
194
|
+
test("drops NOT NULL without backfilling existing templates", () => {
|
|
195
|
+
const migration: string = fs.readFileSync(MIGRATION_PATH, "utf8");
|
|
196
|
+
|
|
197
|
+
expect(migration).toContain(
|
|
198
|
+
`ALTER TABLE "MonitorTemplate" ALTER COLUMN "${COLUMN}" DROP NOT NULL`,
|
|
199
|
+
);
|
|
200
|
+
|
|
201
|
+
/*
|
|
202
|
+
* Every template that already exists keeps the name it was created with,
|
|
203
|
+
* so nothing about an existing project's monitor naming changes when this
|
|
204
|
+
* ships. The only UPDATE in the file belongs to down().
|
|
205
|
+
*/
|
|
206
|
+
const up: string = migration.slice(
|
|
207
|
+
migration.indexOf("public async up("),
|
|
208
|
+
migration.indexOf("public async down("),
|
|
209
|
+
);
|
|
210
|
+
|
|
211
|
+
expect(up).not.toContain("UPDATE");
|
|
212
|
+
expect(up).not.toContain("DEFAULT");
|
|
213
|
+
expect(up).not.toContain("SET NOT NULL");
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
/*
|
|
217
|
+
* A rollback restores NOT NULL, which fails on every template written while
|
|
218
|
+
* the column was nullable unless those rows are given a value first.
|
|
219
|
+
* "Monitor" is the exact string the pre-#3486 code substituted for a blank
|
|
220
|
+
* name, so it reconstructs the old behaviour instead of inventing one.
|
|
221
|
+
*/
|
|
222
|
+
test("its down() makes the column non-null again, and can actually run", () => {
|
|
223
|
+
const migration: string = fs.readFileSync(MIGRATION_PATH, "utf8");
|
|
224
|
+
const down: string = migration.slice(
|
|
225
|
+
migration.indexOf("public async down("),
|
|
226
|
+
);
|
|
227
|
+
|
|
228
|
+
expect(down).toContain(
|
|
229
|
+
`UPDATE "MonitorTemplate" SET "${COLUMN}" = 'Monitor' WHERE "${COLUMN}" IS NULL`,
|
|
230
|
+
);
|
|
231
|
+
expect(down).toContain(
|
|
232
|
+
`ALTER TABLE "MonitorTemplate" ALTER COLUMN "${COLUMN}" SET NOT NULL`,
|
|
233
|
+
);
|
|
234
|
+
expect(down.indexOf("UPDATE")).toBeLessThan(down.indexOf("SET NOT NULL"));
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
/*
|
|
238
|
+
* TypeORM records a migration under its `public name` property, not its
|
|
239
|
+
* class name. A rename that updates one and forgets the other leaves the
|
|
240
|
+
* deployed identity on the old value.
|
|
241
|
+
*/
|
|
242
|
+
test("its class name, file name and recorded name all agree", () => {
|
|
243
|
+
const migration: string = fs.readFileSync(MIGRATION_PATH, "utf8");
|
|
244
|
+
|
|
245
|
+
expect(migration).toContain(`export class ${MIGRATION_CLASS}`);
|
|
246
|
+
expect(migration).toContain(`public name: string = "${MIGRATION_CLASS}"`);
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
test("is registered, so the column is actually nullable at runtime", () => {
|
|
250
|
+
const index: string = fs.readFileSync(
|
|
251
|
+
path.join(MIGRATIONS_DIR, "Index.ts"),
|
|
252
|
+
"utf8",
|
|
253
|
+
);
|
|
254
|
+
|
|
255
|
+
// Imported AND listed in the exported array - the import alone does nothing.
|
|
256
|
+
expect(index.match(new RegExp(MIGRATION_CLASS, "g"))?.length).toBe(2);
|
|
257
|
+
});
|
|
258
|
+
});
|