@oneuptime/common 12.0.27 → 12.0.29
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Models/AnalyticsModels/RumSessionChunk.ts +52 -0
- package/Models/DatabaseModels/CloudResource.ts +17 -1
- package/Models/DatabaseModels/DashboardDomain.ts +43 -0
- package/Models/DatabaseModels/MonitorTemplate.ts +15 -3
- package/Models/DatabaseModels/RumApplication.ts +35 -2
- package/Models/DatabaseModels/StatusPageDomain.ts +43 -0
- package/Server/API/DashboardDomainAPI.ts +74 -0
- package/Server/API/MicrosoftTeamsAPI.ts +81 -180
- package/Server/API/StatusPageDomainAPI.ts +74 -0
- package/Server/API/TelemetryAPI.ts +104 -15
- package/Server/Infrastructure/Postgres/SchemaMigrations/1790200000000-AllowNullMonitorNameOnMonitorTemplate.ts +43 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1790300000000-AddCertificateReissueRequestedAtToDomains.ts +40 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +4 -0
- package/Server/Middleware/ProjectAuthorization.ts +6 -1
- package/Server/Services/DashboardDomainService.ts +131 -0
- package/Server/Services/LogPipelineProcessorService.ts +88 -0
- package/Server/Services/MonitorTemplateService.ts +70 -11
- package/Server/Services/StatusPageDomainService.ts +163 -3
- package/Server/Services/StatusPagePrivateUserSessionService.ts +197 -56
- package/Server/Types/Domain.ts +17 -0
- package/Server/Utils/APIKey/AccessPermission.ts +24 -0
- package/Server/Utils/Greenlock/Greenlock.ts +156 -58
- package/Server/Utils/LogPipelineProcessorValidation.ts +112 -0
- package/Server/Utils/SecurityEvent/GoogleSecOps/GoogleSecOpsClient.ts +1094 -45
- package/Server/Utils/SecurityEvent/GoogleSecOps/GoogleSecOpsPoller.ts +153 -17
- package/Server/Utils/SessionReplay/SessionReplayGateCache.ts +12 -5
- package/Server/Utils/SessionReplay/SessionReplayIdentity.ts +102 -0
- package/Server/Utils/SessionReplay/SessionReplayReadService.ts +11 -2
- package/Server/Utils/Workspace/MicrosoftTeams/Actions/Alert.ts +23 -11
- package/Server/Utils/Workspace/MicrosoftTeams/Actions/Auth.ts +0 -170
- package/Server/Utils/Workspace/MicrosoftTeams/Actions/Authorization.ts +158 -0
- package/Server/Utils/Workspace/MicrosoftTeams/Actions/Incident.ts +23 -11
- package/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.ts +35 -13
- package/Tests/App/Dashboard/MonitorTemplateColumnAndFacet.test.tsx +505 -0
- package/Tests/App/StatusPage/StatusPageLoginCodeBootstrap.test.ts +106 -0
- package/Tests/App/StatusPage/StatusPageLoginCodeUtil.test.ts +84 -0
- package/Tests/App/StatusPage/StatusPageRedirectSafety.test.ts +119 -0
- package/Tests/Models/DatabaseModels/SessionReplayModels.test.ts +81 -0
- package/Tests/Models/MonitorTemplateMonitorNameColumn.test.ts +258 -0
- package/Tests/Server/API/CustomDomainReissueSslAPI.test.ts +469 -0
- package/Tests/Server/API/MicrosoftTeamsBotMessagesEndpoint.test.ts +729 -0
- package/Tests/Server/API/MicrosoftTeamsBotTestEndpoint.test.ts +26 -0
- package/Tests/Server/API/MicrosoftTeamsLegacyWebhookRemoval.test.ts +402 -0
- package/Tests/Server/API/SessionReplayAPI.test.ts +232 -0
- package/Tests/Server/Middleware/ProjectAuthorizationApiKeyMiddleware.test.ts +4 -0
- package/Tests/Server/Services/AddCertificateReissueRequestedAtToDomainsMigration.test.ts +287 -0
- package/Tests/Server/Services/CustomDomainCertificateReissue.test.ts +477 -0
- package/Tests/Server/Services/LogPipelineProcessorSaveValidation.test.ts +249 -0
- package/Tests/Server/Services/MonitorTemplateServiceBulkSync.test.ts +549 -0
- package/Tests/Server/Services/MonitorTemplateServiceNetworkDeviceSync.test.ts +16 -3
- package/Tests/Server/Services/NetworkDeviceAutoImportRuleEngineService.test.ts +57 -0
- package/Tests/Server/Services/SecurityEventLastErrorColumnWidth.test.ts +151 -15
- package/Tests/Server/Services/StatusPagePrivateUserSessionService.test.ts +587 -0
- package/Tests/Server/Types/Database/Permissions/ApiKeyFileAccess.test.ts +286 -0
- package/Tests/Server/Types/Database/Permissions/MonitorTemplateColumnAccess.test.ts +328 -0
- package/Tests/Server/Types/Database/QueryHelperFindWithSameTextAndIsNull.test.ts +83 -0
- package/Tests/Server/Utils/AI/SRE/ConfidenceSignal.test.ts +377 -0
- package/Tests/Server/Utils/APIKey/AccessPermission.test.ts +16 -1
- package/Tests/Server/Utils/Greenlock/CertificateRenewalSchedule.test.ts +250 -0
- package/Tests/Server/Utils/LogPipelineProcessorValidation.test.ts +198 -0
- package/Tests/Server/Utils/SecurityEvent/ConnectorErrorMessage.test.ts +48 -9
- package/Tests/Server/Utils/SecurityEvent/GoogleSecOpsAuth.test.ts +726 -0
- package/Tests/Server/Utils/SecurityEvent/GoogleSecOpsClient.test.ts +74 -11
- package/Tests/Server/Utils/SecurityEvent/GoogleSecOpsErrorHandling.test.ts +1107 -0
- package/Tests/Server/Utils/SecurityEvent/GoogleSecOpsPoller.test.ts +20 -3
- package/Tests/Server/Utils/SecurityEvent/GoogleSecOpsPollerFailureTaxonomy.test.ts +51 -11
- package/Tests/Server/Utils/SecurityEvent/GoogleSecOpsPollerHardening.test.ts +1246 -0
- package/Tests/Server/Utils/SecurityEvent/GoogleSecOpsRequestContract.test.ts +961 -0
- package/Tests/Server/Utils/SecurityEvent/GoogleSecOpsResponseParsing.test.ts +653 -0
- package/Tests/Server/Utils/Workspace/MicrosoftTeamsActionAuthorization.test.ts +880 -0
- package/Tests/Types/API/HTTPResponse.test.ts +175 -0
- package/Tests/Types/API/Route.test.ts +36 -4
- package/Tests/Types/Database/DatabaseProperty.test.ts +171 -0
- package/Tests/Types/DateLocalShortDateTimeString.test.ts +518 -0
- package/Tests/Types/DateLocalTimeString.test.ts +259 -0
- package/Tests/Types/DateUserPrefers12HourFormat.test.ts +316 -0
- package/Tests/UI/Components/LogsHistogram.test.tsx +41 -2
- package/Tests/UI/Components/LogsViewerEmptyStateTimeRange.test.tsx +212 -0
- package/Tests/UI/Components/TelemetryChartClockFormat.test.tsx +359 -0
- package/Tests/UI/Components/TimeRangePickerClockFormat.test.tsx +422 -0
- package/Tests/UI/Components/TimeRangePickerDropdown.test.tsx +146 -8
- package/Tests/UI/MicrosoftTeams/MicrosoftTeamsMessagingEndpointGuidance.test.tsx +520 -0
- package/Tests/UI/Utils/Navigation.test.ts +82 -0
- package/Tests/Utils/Array.test.ts +100 -0
- package/Tests/Utils/CertificateReissue.test.ts +225 -0
- package/Tests/Utils/Grok/Grok.test.ts +492 -0
- package/Tests/Utils/Monitor/NetworkDeviceMonitorTemplateUtil.test.ts +181 -13
- package/Types/API/Route.ts +20 -5
- package/Types/Date.ts +143 -13
- package/Types/Rum/SessionReplay.ts +76 -1
- package/UI/Components/Date/TimeRangePickerDropdown.tsx +9 -5
- package/UI/Components/LogsViewer/LogsViewer.tsx +12 -7
- package/UI/Components/LogsViewer/components/HistogramTooltip.tsx +18 -17
- package/UI/Components/LogsViewer/components/LogsAnalyticsView.tsx +21 -28
- package/UI/Components/LogsViewer/components/LogsHistogram.tsx +2 -4
- package/UI/Components/TelemetryViewer/components/TelemetryHistogram.tsx +2 -4
- package/UI/Components/TelemetryViewer/components/TelemetryHistogramTooltip.tsx +17 -16
- package/UI/Utils/Navigation.ts +42 -0
- package/Utils/Array.ts +37 -0
- package/Utils/CertificateReissue.ts +128 -0
- package/Utils/Grok/Grok.ts +461 -0
- package/Utils/Grok/GrokPatterns.ts +118 -0
- package/Utils/Monitor/NetworkDeviceMonitorTemplateUtil.ts +58 -6
- package/build/dist/Models/AnalyticsModels/RumSessionChunk.js +48 -0
- package/build/dist/Models/AnalyticsModels/RumSessionChunk.js.map +1 -1
- package/build/dist/Models/DatabaseModels/CloudResource.js +19 -2
- package/build/dist/Models/DatabaseModels/CloudResource.js.map +1 -1
- package/build/dist/Models/DatabaseModels/DashboardDomain.js +44 -0
- package/build/dist/Models/DatabaseModels/DashboardDomain.js.map +1 -1
- package/build/dist/Models/DatabaseModels/MonitorTemplate.js +17 -4
- package/build/dist/Models/DatabaseModels/MonitorTemplate.js.map +1 -1
- package/build/dist/Models/DatabaseModels/RumApplication.js +37 -3
- package/build/dist/Models/DatabaseModels/RumApplication.js.map +1 -1
- package/build/dist/Models/DatabaseModels/StatusPageDomain.js +44 -0
- package/build/dist/Models/DatabaseModels/StatusPageDomain.js.map +1 -1
- package/build/dist/Server/API/DashboardDomainAPI.js +43 -1
- package/build/dist/Server/API/DashboardDomainAPI.js.map +1 -1
- package/build/dist/Server/API/MicrosoftTeamsAPI.js +74 -118
- package/build/dist/Server/API/MicrosoftTeamsAPI.js.map +1 -1
- package/build/dist/Server/API/StatusPageDomainAPI.js +43 -1
- package/build/dist/Server/API/StatusPageDomainAPI.js.map +1 -1
- package/build/dist/Server/API/TelemetryAPI.js +67 -14
- package/build/dist/Server/API/TelemetryAPI.js.map +1 -1
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1790200000000-AllowNullMonitorNameOnMonitorTemplate.js +34 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1790200000000-AllowNullMonitorNameOnMonitorTemplate.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1790300000000-AddCertificateReissueRequestedAtToDomains.js +28 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1790300000000-AddCertificateReissueRequestedAtToDomains.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +4 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
- package/build/dist/Server/Middleware/ProjectAuthorization.js +6 -1
- package/build/dist/Server/Middleware/ProjectAuthorization.js.map +1 -1
- package/build/dist/Server/Services/DashboardDomainService.js +104 -0
- package/build/dist/Server/Services/DashboardDomainService.js.map +1 -1
- package/build/dist/Server/Services/LogPipelineProcessorService.js +65 -0
- package/build/dist/Server/Services/LogPipelineProcessorService.js.map +1 -1
- package/build/dist/Server/Services/MonitorTemplateService.js +57 -11
- package/build/dist/Server/Services/MonitorTemplateService.js.map +1 -1
- package/build/dist/Server/Services/StatusPageDomainService.js +132 -3
- package/build/dist/Server/Services/StatusPageDomainService.js.map +1 -1
- package/build/dist/Server/Services/StatusPagePrivateUserSessionService.js +112 -32
- package/build/dist/Server/Services/StatusPagePrivateUserSessionService.js.map +1 -1
- package/build/dist/Server/Types/Domain.js +16 -0
- package/build/dist/Server/Types/Domain.js.map +1 -1
- package/build/dist/Server/Utils/APIKey/AccessPermission.js +24 -0
- package/build/dist/Server/Utils/APIKey/AccessPermission.js.map +1 -1
- package/build/dist/Server/Utils/Greenlock/Greenlock.js +110 -35
- package/build/dist/Server/Utils/Greenlock/Greenlock.js.map +1 -1
- package/build/dist/Server/Utils/LogPipelineProcessorValidation.js +77 -0
- package/build/dist/Server/Utils/LogPipelineProcessorValidation.js.map +1 -0
- package/build/dist/Server/Utils/SecurityEvent/GoogleSecOps/GoogleSecOpsClient.js +777 -36
- package/build/dist/Server/Utils/SecurityEvent/GoogleSecOps/GoogleSecOpsClient.js.map +1 -1
- package/build/dist/Server/Utils/SecurityEvent/GoogleSecOps/GoogleSecOpsPoller.js +108 -17
- package/build/dist/Server/Utils/SecurityEvent/GoogleSecOps/GoogleSecOpsPoller.js.map +1 -1
- package/build/dist/Server/Utils/SessionReplay/SessionReplayGateCache.js.map +1 -1
- package/build/dist/Server/Utils/SessionReplay/SessionReplayIdentity.js +59 -0
- package/build/dist/Server/Utils/SessionReplay/SessionReplayIdentity.js.map +1 -0
- package/build/dist/Server/Utils/SessionReplay/SessionReplayReadService.js +9 -2
- package/build/dist/Server/Utils/SessionReplay/SessionReplayReadService.js.map +1 -1
- package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/Alert.js +17 -6
- package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/Alert.js.map +1 -1
- package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/Auth.js +0 -159
- package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/Auth.js.map +1 -1
- package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/Authorization.js +128 -0
- package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/Authorization.js.map +1 -0
- package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/Incident.js +17 -6
- package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/Incident.js.map +1 -1
- package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.js +25 -13
- package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.js.map +1 -1
- package/build/dist/Types/API/Route.js +16 -5
- package/build/dist/Types/API/Route.js.map +1 -1
- package/build/dist/Types/Date.js +115 -13
- package/build/dist/Types/Date.js.map +1 -1
- package/build/dist/Types/Rum/SessionReplay.js +38 -0
- package/build/dist/Types/Rum/SessionReplay.js.map +1 -1
- package/build/dist/UI/Components/Date/TimeRangePickerDropdown.js +9 -5
- package/build/dist/UI/Components/Date/TimeRangePickerDropdown.js.map +1 -1
- package/build/dist/UI/Components/LogsViewer/LogsViewer.js +12 -7
- package/build/dist/UI/Components/LogsViewer/LogsViewer.js.map +1 -1
- package/build/dist/UI/Components/LogsViewer/components/HistogramTooltip.js +15 -14
- package/build/dist/UI/Components/LogsViewer/components/HistogramTooltip.js.map +1 -1
- package/build/dist/UI/Components/LogsViewer/components/LogsAnalyticsView.js +17 -22
- package/build/dist/UI/Components/LogsViewer/components/LogsAnalyticsView.js.map +1 -1
- package/build/dist/UI/Components/LogsViewer/components/LogsHistogram.js +2 -4
- package/build/dist/UI/Components/LogsViewer/components/LogsHistogram.js.map +1 -1
- package/build/dist/UI/Components/TelemetryViewer/components/TelemetryHistogram.js +2 -4
- package/build/dist/UI/Components/TelemetryViewer/components/TelemetryHistogram.js.map +1 -1
- package/build/dist/UI/Components/TelemetryViewer/components/TelemetryHistogramTooltip.js +15 -14
- package/build/dist/UI/Components/TelemetryViewer/components/TelemetryHistogramTooltip.js.map +1 -1
- package/build/dist/UI/Utils/Navigation.js +30 -0
- package/build/dist/UI/Utils/Navigation.js.map +1 -1
- package/build/dist/Utils/Array.js +25 -0
- package/build/dist/Utils/Array.js.map +1 -1
- package/build/dist/Utils/CertificateReissue.js +97 -0
- package/build/dist/Utils/CertificateReissue.js.map +1 -0
- package/build/dist/Utils/Grok/Grok.js +324 -0
- package/build/dist/Utils/Grok/Grok.js.map +1 -0
- package/build/dist/Utils/Grok/GrokPatterns.js +112 -0
- package/build/dist/Utils/Grok/GrokPatterns.js.map +1 -0
- package/build/dist/Utils/Monitor/NetworkDeviceMonitorTemplateUtil.js +63 -3
- package/build/dist/Utils/Monitor/NetworkDeviceMonitorTemplateUtil.js.map +1 -1
- package/package.json +1 -1
- package/UI/Utils/JsonWebToken.ts +0 -14
- package/build/dist/UI/Utils/JsonWebToken.js +0 -10
- package/build/dist/UI/Utils/JsonWebToken.js.map +0 -1
|
@@ -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
|
+
});
|
|
@@ -5,6 +5,7 @@ import AuditLog from "../../../Models/AnalyticsModels/AuditLog";
|
|
|
5
5
|
import BaseModel from "../../../Models/DatabaseModels/DatabaseBaseModel/DatabaseBaseModel";
|
|
6
6
|
import { PlanType } from "../../../Types/Billing/SubscriptionPlan";
|
|
7
7
|
import Project from "../../../Models/DatabaseModels/Project";
|
|
8
|
+
import CloudResource from "../../../Models/DatabaseModels/CloudResource";
|
|
8
9
|
import RumApplication from "../../../Models/DatabaseModels/RumApplication";
|
|
9
10
|
import RumSessionErasureRequest from "../../../Models/DatabaseModels/RumSessionErasureRequest";
|
|
10
11
|
import RumSessionPin from "../../../Models/DatabaseModels/RumSessionPin";
|
|
@@ -61,6 +62,86 @@ describe("RumApplication session replay configuration", () => {
|
|
|
61
62
|
return model.getTableColumnMetadata(columnName);
|
|
62
63
|
};
|
|
63
64
|
|
|
65
|
+
/*
|
|
66
|
+
* The identity column has to be CREATABLE and NEVER UPDATABLE.
|
|
67
|
+
*
|
|
68
|
+
* It shipped as `create: []` on a required column with no default, which
|
|
69
|
+
* made the documented "create an application by hand" flow impossible:
|
|
70
|
+
* ModelForm drops any field the caller has no create permission on, so the
|
|
71
|
+
* Dashboard's required "App Identifier" input never rendered, and the POST
|
|
72
|
+
* that followed was rejected by the server with "appIdentifier is
|
|
73
|
+
* required". The Create button was a dead end for every user.
|
|
74
|
+
*/
|
|
75
|
+
it("lets a user supply the app identifier at creation time", () => {
|
|
76
|
+
const accessControl: ColumnAccessControl | null =
|
|
77
|
+
model.getColumnAccessControlFor("appIdentifier");
|
|
78
|
+
|
|
79
|
+
expect(accessControl).toBeDefined();
|
|
80
|
+
expect(accessControl!.create.length).toBeGreaterThan(0);
|
|
81
|
+
expect(accessControl!.create).toContain(Permission.ProjectOwner);
|
|
82
|
+
expect(accessControl!.create).toContain(Permission.CreateRumApplication);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it("never lets the app identifier be edited afterwards", () => {
|
|
86
|
+
/*
|
|
87
|
+
* Deliberately immutable. Telemetry, sessions, traces and recordings are
|
|
88
|
+
* all filed under this value and the (projectId, appIdentifier) index is
|
|
89
|
+
* unique, so re-pointing it after the fact would orphan every row that
|
|
90
|
+
* already references it. If this list is ever populated, the orphaning
|
|
91
|
+
* has to be solved first - do not "tidy it up" to match `create`.
|
|
92
|
+
*/
|
|
93
|
+
const accessControl: ColumnAccessControl | null =
|
|
94
|
+
model.getColumnAccessControlFor("appIdentifier");
|
|
95
|
+
|
|
96
|
+
expect(accessControl!.update).toEqual([]);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
/*
|
|
100
|
+
* The create form declares an "App Identifier" field. That field can only
|
|
101
|
+
* render if the column is creatable, and the POST can only succeed if the
|
|
102
|
+
* required column is supplied - so a required column with an empty create
|
|
103
|
+
* list is always a dead form, whatever the page source says.
|
|
104
|
+
*
|
|
105
|
+
* CloudResource.resourceIdentifier is checked alongside because it had the
|
|
106
|
+
* identical defect on the identical page shape: an auto-discovered
|
|
107
|
+
* identity column whose dashboard page also offers a Create button.
|
|
108
|
+
*/
|
|
109
|
+
it.each([
|
|
110
|
+
["RumApplication", new RumApplication() as BaseModel],
|
|
111
|
+
["CloudResource", new CloudResource() as BaseModel],
|
|
112
|
+
])(
|
|
113
|
+
"%s has no required column that the user is forbidden to create",
|
|
114
|
+
(_name: string, subject: BaseModel) => {
|
|
115
|
+
const uncreatable: Array<string> = [];
|
|
116
|
+
|
|
117
|
+
for (const columnName of subject.getTableColumns().columns) {
|
|
118
|
+
const metadata: TableColumnMetadata =
|
|
119
|
+
subject.getTableColumnMetadata(columnName);
|
|
120
|
+
const accessControl: ColumnAccessControl | null =
|
|
121
|
+
subject.getColumnAccessControlFor(columnName);
|
|
122
|
+
|
|
123
|
+
if (!metadata.required || metadata.defaultValue !== undefined) {
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/* System columns nobody submits through a form. */
|
|
128
|
+
if (
|
|
129
|
+
["_id", "createdAt", "updatedAt", "version", "slug"].includes(
|
|
130
|
+
columnName,
|
|
131
|
+
)
|
|
132
|
+
) {
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (accessControl && accessControl.create.length === 0) {
|
|
137
|
+
uncreatable.push(columnName);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
expect(uncreatable).toEqual([]);
|
|
142
|
+
},
|
|
143
|
+
);
|
|
144
|
+
|
|
64
145
|
it("declares every session replay configuration column", () => {
|
|
65
146
|
const expectedColumns: Array<string> = [
|
|
66
147
|
"isSessionReplayEnabled",
|
|
@@ -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
|
+
});
|