@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
|
@@ -561,6 +561,8 @@ import { AddTelemetrySourceMap1790000000000 } from "./1790000000000-AddTelemetry
|
|
|
561
561
|
import { AddSnmpConfigsToNetworkDeviceDiscoveryScan1790000000001 } from "./1790000000001-AddSnmpConfigsToNetworkDeviceDiscoveryScan";
|
|
562
562
|
import { AddSnmpEnabledToNetworkDeviceDiscoveryScan1790003445000 } from "./1790003445000-AddSnmpEnabledToNetworkDeviceDiscoveryScan";
|
|
563
563
|
import { AddResourceActivityFeeds1790100000000 } from "./1790100000000-AddResourceActivityFeeds";
|
|
564
|
+
import { AllowNullMonitorNameOnMonitorTemplate1790200000000 } from "./1790200000000-AllowNullMonitorNameOnMonitorTemplate";
|
|
565
|
+
import { AddCertificateReissueRequestedAtToDomains1790300000000 } from "./1790300000000-AddCertificateReissueRequestedAtToDomains";
|
|
564
566
|
|
|
565
567
|
export default [
|
|
566
568
|
InitialMigration,
|
|
@@ -1126,4 +1128,6 @@ export default [
|
|
|
1126
1128
|
AddSnmpConfigsToNetworkDeviceDiscoveryScan1790000000001,
|
|
1127
1129
|
AddSnmpEnabledToNetworkDeviceDiscoveryScan1790003445000,
|
|
1128
1130
|
AddResourceActivityFeeds1790100000000,
|
|
1131
|
+
AllowNullMonitorNameOnMonitorTemplate1790200000000,
|
|
1132
|
+
AddCertificateReissueRequestedAtToDomains1790300000000,
|
|
1129
1133
|
];
|
|
@@ -199,7 +199,12 @@ export default class ProjectMiddleware {
|
|
|
199
199
|
(req as OneUptimeRequest).userType = UserType.API;
|
|
200
200
|
|
|
201
201
|
/*
|
|
202
|
-
*
|
|
202
|
+
* The two halves of an API key's authority. The global half is the
|
|
203
|
+
* fixed marker set every key carries (see APIKeyAccessPermission); the
|
|
204
|
+
* per-key grants an administrator actually configured are read from
|
|
205
|
+
* ApiKeyPermission just below. That second half is what the long-lived
|
|
206
|
+
* "TODO: Add API key permissions" here was asking for, and it is done -
|
|
207
|
+
* the TODO outlived the work.
|
|
203
208
|
*/
|
|
204
209
|
(req as OneUptimeRequest).userGlobalAccessPermission =
|
|
205
210
|
await APIKeyAccessPermission.getDefaultApiGlobalPermission(tenantId);
|
|
@@ -20,6 +20,10 @@ import Telemetry, { Span } from "../Utils/Telemetry";
|
|
|
20
20
|
import CaptureSpan from "../Utils/Telemetry/CaptureSpan";
|
|
21
21
|
import { DashboardCNameRecord } from "../EnvironmentConfig";
|
|
22
22
|
import Domain from "../Types/Domain";
|
|
23
|
+
import OneUptimeDate from "../../Types/Date";
|
|
24
|
+
import CertificateReissueUtil from "../../Utils/CertificateReissue";
|
|
25
|
+
import TooManyRequestsException from "../../Types/Exception/TooManyRequestsException";
|
|
26
|
+
import QueryHelper from "../Types/Database/QueryHelper";
|
|
23
27
|
|
|
24
28
|
const DASHBOARD_DOMAIN_EGRESS_LABEL: string = "Dashboard domain";
|
|
25
29
|
|
|
@@ -247,6 +251,133 @@ export class Service extends DatabaseService<DashboardDomain> {
|
|
|
247
251
|
});
|
|
248
252
|
}
|
|
249
253
|
|
|
254
|
+
/*
|
|
255
|
+
* Reissue this domain's Let's Encrypt certificate because a customer asked
|
|
256
|
+
* for it, rather than because it is close to expiring.
|
|
257
|
+
*
|
|
258
|
+
* Every order spends from a Let's Encrypt account that is shared by the
|
|
259
|
+
* whole installation - including by the cron that keeps everybody else's
|
|
260
|
+
* certificates alive - so this is throttled to one request per domain per
|
|
261
|
+
* CertificateReissueUtil.COOLDOWN_IN_HOURS.
|
|
262
|
+
*
|
|
263
|
+
* Throws with a customer-readable reason for every refusal, so the API layer
|
|
264
|
+
* can hand the message straight back without classifying the failure:
|
|
265
|
+
* BadDataException for a domain that is not eligible at all, and
|
|
266
|
+
* TooManyRequestsException (429) for one that is simply too soon.
|
|
267
|
+
*/
|
|
268
|
+
@CaptureSpan()
|
|
269
|
+
public async reissueCert(domainId: ObjectID): Promise<void> {
|
|
270
|
+
const now: Date = OneUptimeDate.getCurrentDate();
|
|
271
|
+
|
|
272
|
+
const dashboardDomain: DashboardDomain | null = await this.findOneBy({
|
|
273
|
+
query: {
|
|
274
|
+
_id: domainId.toString(),
|
|
275
|
+
},
|
|
276
|
+
select: {
|
|
277
|
+
_id: true,
|
|
278
|
+
fullDomain: true,
|
|
279
|
+
isCnameVerified: true,
|
|
280
|
+
isSslOrdered: true,
|
|
281
|
+
isCustomCertificate: true,
|
|
282
|
+
certificateReissueRequestedAt: true,
|
|
283
|
+
},
|
|
284
|
+
props: {
|
|
285
|
+
isRoot: true,
|
|
286
|
+
},
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
if (!dashboardDomain) {
|
|
290
|
+
throw new BadDataException("Domain not found.");
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
if (!dashboardDomain.fullDomain) {
|
|
294
|
+
throw new BadDataException(
|
|
295
|
+
"Unable to reissue certificate because domain is null",
|
|
296
|
+
);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/*
|
|
300
|
+
* Nothing to reissue: the certificate on a custom-certificate domain is
|
|
301
|
+
* the one the customer uploaded, and we have no way to obtain another.
|
|
302
|
+
*/
|
|
303
|
+
if (dashboardDomain.isCustomCertificate) {
|
|
304
|
+
throw new BadDataException(
|
|
305
|
+
"This domain uses a certificate you uploaded yourself, so there is no Let's Encrypt certificate to reissue. Please edit this domain and upload a new certificate instead.",
|
|
306
|
+
);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
if (!dashboardDomain.isCnameVerified) {
|
|
310
|
+
throw new BadDataException(
|
|
311
|
+
"CNAME is not verified. Please verify CNAME first before you reissue the SSL certificate.",
|
|
312
|
+
);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/*
|
|
316
|
+
* A domain that never ordered a certificate has nothing to reissue - the
|
|
317
|
+
* dashboard shows "Order Free SSL" for it, which is the correct button.
|
|
318
|
+
*/
|
|
319
|
+
if (!dashboardDomain.isSslOrdered) {
|
|
320
|
+
throw new BadDataException(
|
|
321
|
+
"No SSL certificate has been ordered for this domain yet. Please order one first.",
|
|
322
|
+
);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/*
|
|
326
|
+
* Claim the cooldown before ordering anything, and claim it by putting the
|
|
327
|
+
* cooldown condition in the WHERE clause rather than in an `if` above the
|
|
328
|
+
* write. Two clicks that arrive together both read a row that is not
|
|
329
|
+
* cooling down; only the write can decide which one of them gets to spend
|
|
330
|
+
* the CA's allowance.
|
|
331
|
+
*
|
|
332
|
+
* The stamp goes down BEFORE the order and is never rolled back on
|
|
333
|
+
* failure. A rejected order still costs a validation attempt against the
|
|
334
|
+
* shared account, and a domain whose order fails is exactly the domain
|
|
335
|
+
* somebody presses again - so a failed attempt has to start the clock the
|
|
336
|
+
* same way a successful one does.
|
|
337
|
+
*/
|
|
338
|
+
const claimedRowCount: number = await this.updateOneBy({
|
|
339
|
+
query: {
|
|
340
|
+
_id: domainId.toString(),
|
|
341
|
+
certificateReissueRequestedAt: QueryHelper.lessThanEqualToOrNull(
|
|
342
|
+
CertificateReissueUtil.getCooldownCutoff(now),
|
|
343
|
+
),
|
|
344
|
+
},
|
|
345
|
+
data: {
|
|
346
|
+
certificateReissueRequestedAt: now,
|
|
347
|
+
},
|
|
348
|
+
props: {
|
|
349
|
+
isRoot: true,
|
|
350
|
+
},
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
if (claimedRowCount === 0) {
|
|
354
|
+
if (dashboardDomain.certificateReissueRequestedAt) {
|
|
355
|
+
throw new TooManyRequestsException(
|
|
356
|
+
CertificateReissueUtil.getCooldownMessage(
|
|
357
|
+
dashboardDomain.certificateReissueRequestedAt,
|
|
358
|
+
now,
|
|
359
|
+
),
|
|
360
|
+
);
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
/*
|
|
364
|
+
* The row matched the cooldown a moment ago and does not now: another
|
|
365
|
+
* request claimed it in between, or the domain was deleted. Either way
|
|
366
|
+
* the caller must not order.
|
|
367
|
+
*/
|
|
368
|
+
throw new BadDataException(
|
|
369
|
+
"Could not start a certificate reissue for this domain. Please refresh the page and try again.",
|
|
370
|
+
);
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
logger.debug(
|
|
374
|
+
"Reissuing SSL certificate for domain: " + dashboardDomain.fullDomain,
|
|
375
|
+
{ fullDomain: dashboardDomain.fullDomain } as LogAttributes,
|
|
376
|
+
);
|
|
377
|
+
|
|
378
|
+
await this.orderCert(dashboardDomain);
|
|
379
|
+
}
|
|
380
|
+
|
|
250
381
|
@CaptureSpan()
|
|
251
382
|
public async updateSslProvisioningStatusForAllDomains(): Promise<void> {
|
|
252
383
|
const domains: Array<DashboardDomain> = await this.findBy({
|
|
@@ -1,10 +1,98 @@
|
|
|
1
1
|
import DatabaseService from "./DatabaseService";
|
|
2
2
|
import Model from "../../Models/DatabaseModels/LogPipelineProcessor";
|
|
3
|
+
import CreateBy from "../Types/Database/CreateBy";
|
|
4
|
+
import UpdateBy from "../Types/Database/UpdateBy";
|
|
5
|
+
import { OnCreate, OnUpdate } from "../Types/Database/Hooks";
|
|
6
|
+
import LIMIT_MAX from "../../Types/Database/LimitMax";
|
|
7
|
+
import { JSONObject } from "../../Types/JSON";
|
|
8
|
+
import { validateLogPipelineProcessor } from "../Utils/LogPipelineProcessorValidation";
|
|
9
|
+
|
|
10
|
+
/*
|
|
11
|
+
* `PartialEntity` values may be `() => string` raw SQL expressions rather
|
|
12
|
+
* than literals. One cannot be evaluated here, so an update that sets a
|
|
13
|
+
* validated field to an expression is skipped - validating a stringified
|
|
14
|
+
* function would be worse than not validating at all. Nothing in the
|
|
15
|
+
* product writes processor fields this way.
|
|
16
|
+
*/
|
|
17
|
+
function isSqlExpressionValue(value: unknown): boolean {
|
|
18
|
+
return typeof value === "function";
|
|
19
|
+
}
|
|
3
20
|
|
|
4
21
|
export class Service extends DatabaseService<Model> {
|
|
5
22
|
public constructor() {
|
|
6
23
|
super(Model);
|
|
7
24
|
}
|
|
25
|
+
|
|
26
|
+
protected override async onBeforeCreate(
|
|
27
|
+
createBy: CreateBy<Model>,
|
|
28
|
+
): Promise<OnCreate<Model>> {
|
|
29
|
+
validateLogPipelineProcessor({
|
|
30
|
+
processorType: createBy.data.processorType,
|
|
31
|
+
configuration: createBy.data.configuration as JSONObject | undefined,
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
return { createBy, carryForward: null };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
protected override async onBeforeUpdate(
|
|
38
|
+
updateBy: UpdateBy<Model>,
|
|
39
|
+
): Promise<OnUpdate<Model>> {
|
|
40
|
+
/*
|
|
41
|
+
* An edit can break a row without naming both fields: switching
|
|
42
|
+
* `processorType` to GrokParser while the stored configuration is
|
|
43
|
+
* still a severity-remapper blob, or replacing the pattern on a row
|
|
44
|
+
* that is already a GrokParser. So validate the MERGED row.
|
|
45
|
+
*
|
|
46
|
+
* Only pay for the SELECT when the update touches one of them.
|
|
47
|
+
* Processor edits are rare, human-driven config changes - this is
|
|
48
|
+
* not a hot path.
|
|
49
|
+
*/
|
|
50
|
+
const incomingProcessorType: unknown = updateBy.data.processorType;
|
|
51
|
+
const incomingConfiguration: unknown = updateBy.data.configuration;
|
|
52
|
+
|
|
53
|
+
if (
|
|
54
|
+
incomingProcessorType === undefined &&
|
|
55
|
+
incomingConfiguration === undefined
|
|
56
|
+
) {
|
|
57
|
+
return { updateBy, carryForward: null };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (
|
|
61
|
+
isSqlExpressionValue(incomingProcessorType) ||
|
|
62
|
+
isSqlExpressionValue(incomingConfiguration)
|
|
63
|
+
) {
|
|
64
|
+
return { updateBy, carryForward: null };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const existingRows: Array<Model> = await this.findBy({
|
|
68
|
+
query: updateBy.query,
|
|
69
|
+
skip: 0,
|
|
70
|
+
limit: LIMIT_MAX,
|
|
71
|
+
select: {
|
|
72
|
+
_id: true,
|
|
73
|
+
processorType: true,
|
|
74
|
+
configuration: true,
|
|
75
|
+
},
|
|
76
|
+
props: {
|
|
77
|
+
isRoot: true,
|
|
78
|
+
},
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
for (const existing of existingRows) {
|
|
82
|
+
validateLogPipelineProcessor({
|
|
83
|
+
processorType:
|
|
84
|
+
incomingProcessorType !== undefined
|
|
85
|
+
? (incomingProcessorType as string | null)
|
|
86
|
+
: existing.processorType,
|
|
87
|
+
configuration:
|
|
88
|
+
incomingConfiguration !== undefined
|
|
89
|
+
? (incomingConfiguration as JSONObject | string | null)
|
|
90
|
+
: (existing.configuration as JSONObject | undefined),
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return { updateBy, carryForward: null };
|
|
95
|
+
}
|
|
8
96
|
}
|
|
9
97
|
|
|
10
98
|
export default new Service();
|
|
@@ -6,6 +6,7 @@ import { OnCreate, OnUpdate } from "../Types/Database/Hooks";
|
|
|
6
6
|
import MonitorStepsProjectValidator from "../Utils/Monitor/MonitorStepsProjectValidator";
|
|
7
7
|
import DatabaseCommonInteractionProps from "../../Types/BaseDatabase/DatabaseCommonInteractionProps";
|
|
8
8
|
import BadDataException from "../../Types/Exception/BadDataException";
|
|
9
|
+
import Includes from "../../Types/BaseDatabase/Includes";
|
|
9
10
|
import LIMIT_MAX from "../../Types/Database/LimitMax";
|
|
10
11
|
import { JSONObject } from "../../Types/JSON";
|
|
11
12
|
import MonitorSteps from "../../Types/Monitor/MonitorSteps";
|
|
@@ -275,7 +276,7 @@ export class Service extends DatabaseService<Model> {
|
|
|
275
276
|
monitorSteps: true,
|
|
276
277
|
autoProvisionedNetworkDeviceId: true,
|
|
277
278
|
},
|
|
278
|
-
sort: { createdAt: SortOrder.Ascending },
|
|
279
|
+
sort: { createdAt: SortOrder.Ascending, _id: SortOrder.Ascending },
|
|
279
280
|
limit: LIMIT_MAX,
|
|
280
281
|
skip: skip,
|
|
281
282
|
props: { isRoot: true },
|
|
@@ -321,16 +322,74 @@ export class Service extends DatabaseService<Model> {
|
|
|
321
322
|
return { totalLinkedMonitors, syncedMonitors };
|
|
322
323
|
}
|
|
323
324
|
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
325
|
+
/*
|
|
326
|
+
* A single updateBy is capped at its `limit`, so a template with more
|
|
327
|
+
* linked monitors than LIMIT_MAX used to leave the remainder on the old
|
|
328
|
+
* config and still return success. Offset paging cannot fix that here:
|
|
329
|
+
* updateBy takes no sort, and rewriting a row moves its tuple, so a
|
|
330
|
+
* second page at a higher skip can step over rows the first page moved.
|
|
331
|
+
* Page the ids first instead, then update by id batch.
|
|
332
|
+
*
|
|
333
|
+
* `_id` rides along in the sort because skip/limit paging is only stable
|
|
334
|
+
* over a total order: a fleet provisioned by one auto-import run shares a
|
|
335
|
+
* createdAt, and without the tiebreaker a tie straddling a page boundary
|
|
336
|
+
* returns one row twice and another never — which here would inflate
|
|
337
|
+
* syncedMonitors by exactly the number of rows it dropped.
|
|
338
|
+
*/
|
|
339
|
+
const linkedMonitorIds: Array<ObjectID> = [];
|
|
340
|
+
|
|
341
|
+
for (let skip: number = 0; ; skip += LIMIT_MAX) {
|
|
342
|
+
const monitors: Array<Monitor> = await MonitorService.findBy({
|
|
343
|
+
query: {
|
|
344
|
+
monitorTemplateId: template.id!,
|
|
345
|
+
projectId: template.projectId,
|
|
346
|
+
},
|
|
347
|
+
select: { _id: true },
|
|
348
|
+
sort: { createdAt: SortOrder.Ascending, _id: SortOrder.Ascending },
|
|
349
|
+
limit: LIMIT_MAX,
|
|
350
|
+
skip: skip,
|
|
351
|
+
props: { isRoot: true },
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
for (const monitor of monitors) {
|
|
355
|
+
linkedMonitorIds.push(monitor.id!);
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
if (monitors.length < LIMIT_MAX) {
|
|
359
|
+
break;
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
let syncedMonitors: number = 0;
|
|
364
|
+
|
|
365
|
+
for (
|
|
366
|
+
let batchStart: number = 0;
|
|
367
|
+
batchStart < linkedMonitorIds.length;
|
|
368
|
+
batchStart += LIMIT_MAX
|
|
369
|
+
) {
|
|
370
|
+
const batch: Array<ObjectID> = linkedMonitorIds.slice(
|
|
371
|
+
batchStart,
|
|
372
|
+
batchStart + LIMIT_MAX,
|
|
373
|
+
);
|
|
374
|
+
|
|
375
|
+
/*
|
|
376
|
+
* Still scoped by template and project, so a stale id from the paging
|
|
377
|
+
* read above cannot widen the write beyond this template's fleet.
|
|
378
|
+
* props stays the caller's — updateBy narrows each batch to what they
|
|
379
|
+
* may actually update.
|
|
380
|
+
*/
|
|
381
|
+
syncedMonitors += await MonitorService.updateBy({
|
|
382
|
+
query: {
|
|
383
|
+
_id: new Includes(batch),
|
|
384
|
+
monitorTemplateId: template.id!,
|
|
385
|
+
projectId: template.projectId,
|
|
386
|
+
},
|
|
387
|
+
data: updateData as any,
|
|
388
|
+
limit: LIMIT_MAX,
|
|
389
|
+
skip: 0,
|
|
390
|
+
props: data.props,
|
|
391
|
+
});
|
|
392
|
+
}
|
|
334
393
|
|
|
335
394
|
return {
|
|
336
395
|
totalLinkedMonitors,
|
|
@@ -20,10 +20,24 @@ import Telemetry, { Span } from "../Utils/Telemetry";
|
|
|
20
20
|
import CaptureSpan from "../Utils/Telemetry/CaptureSpan";
|
|
21
21
|
import { StatusPageCNameRecord } from "../EnvironmentConfig";
|
|
22
22
|
import Domain from "../Types/Domain";
|
|
23
|
+
import ArrayUtil from "../../Utils/Array";
|
|
24
|
+
import OneUptimeDate from "../../Types/Date";
|
|
25
|
+
import CertificateReissueUtil from "../../Utils/CertificateReissue";
|
|
26
|
+
import TooManyRequestsException from "../../Types/Exception/TooManyRequestsException";
|
|
27
|
+
import QueryHelper from "../Types/Database/QueryHelper";
|
|
23
28
|
|
|
24
29
|
const STATUS_PAGE_DOMAIN_EGRESS_LABEL: string = "Status page domain";
|
|
25
30
|
|
|
26
31
|
export class Service extends DatabaseService<StatusPageDomain> {
|
|
32
|
+
/*
|
|
33
|
+
* How many status pages the provisioning sweep checks at once. Each check is
|
|
34
|
+
* one bounded request out to a customer domain, so this is about not letting
|
|
35
|
+
* the sweep's length grow with the size of the fleet, rather than about local
|
|
36
|
+
* work. Kept modest so a sweep cannot look like a burst of traffic to any
|
|
37
|
+
* shared infrastructure sitting in front of those domains.
|
|
38
|
+
*/
|
|
39
|
+
public static readonly SSL_PROVISIONING_CHECK_CONCURRENCY: number = 10;
|
|
40
|
+
|
|
27
41
|
public constructor() {
|
|
28
42
|
super(StatusPageDomain);
|
|
29
43
|
}
|
|
@@ -248,6 +262,133 @@ export class Service extends DatabaseService<StatusPageDomain> {
|
|
|
248
262
|
});
|
|
249
263
|
}
|
|
250
264
|
|
|
265
|
+
/*
|
|
266
|
+
* Reissue this domain's Let's Encrypt certificate because a customer asked
|
|
267
|
+
* for it, rather than because it is close to expiring.
|
|
268
|
+
*
|
|
269
|
+
* Every order spends from a Let's Encrypt account that is shared by the
|
|
270
|
+
* whole installation - including by the cron that keeps everybody else's
|
|
271
|
+
* certificates alive - so this is throttled to one request per domain per
|
|
272
|
+
* CertificateReissueUtil.COOLDOWN_IN_HOURS.
|
|
273
|
+
*
|
|
274
|
+
* Throws with a customer-readable reason for every refusal, so the API layer
|
|
275
|
+
* can hand the message straight back without classifying the failure:
|
|
276
|
+
* BadDataException for a domain that is not eligible at all, and
|
|
277
|
+
* TooManyRequestsException (429) for one that is simply too soon.
|
|
278
|
+
*/
|
|
279
|
+
@CaptureSpan()
|
|
280
|
+
public async reissueCert(domainId: ObjectID): Promise<void> {
|
|
281
|
+
const now: Date = OneUptimeDate.getCurrentDate();
|
|
282
|
+
|
|
283
|
+
const statusPageDomain: StatusPageDomain | null = await this.findOneBy({
|
|
284
|
+
query: {
|
|
285
|
+
_id: domainId.toString(),
|
|
286
|
+
},
|
|
287
|
+
select: {
|
|
288
|
+
_id: true,
|
|
289
|
+
fullDomain: true,
|
|
290
|
+
isCnameVerified: true,
|
|
291
|
+
isSslOrdered: true,
|
|
292
|
+
isCustomCertificate: true,
|
|
293
|
+
certificateReissueRequestedAt: true,
|
|
294
|
+
},
|
|
295
|
+
props: {
|
|
296
|
+
isRoot: true,
|
|
297
|
+
},
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
if (!statusPageDomain) {
|
|
301
|
+
throw new BadDataException("Domain not found.");
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
if (!statusPageDomain.fullDomain) {
|
|
305
|
+
throw new BadDataException(
|
|
306
|
+
"Unable to reissue certificate because domain is null",
|
|
307
|
+
);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/*
|
|
311
|
+
* Nothing to reissue: the certificate on a custom-certificate domain is
|
|
312
|
+
* the one the customer uploaded, and we have no way to obtain another.
|
|
313
|
+
*/
|
|
314
|
+
if (statusPageDomain.isCustomCertificate) {
|
|
315
|
+
throw new BadDataException(
|
|
316
|
+
"This domain uses a certificate you uploaded yourself, so there is no Let's Encrypt certificate to reissue. Please edit this domain and upload a new certificate instead.",
|
|
317
|
+
);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
if (!statusPageDomain.isCnameVerified) {
|
|
321
|
+
throw new BadDataException(
|
|
322
|
+
"CNAME is not verified. Please verify CNAME first before you reissue the SSL certificate.",
|
|
323
|
+
);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/*
|
|
327
|
+
* A domain that never ordered a certificate has nothing to reissue - the
|
|
328
|
+
* dashboard shows "Order Free SSL" for it, which is the correct button.
|
|
329
|
+
*/
|
|
330
|
+
if (!statusPageDomain.isSslOrdered) {
|
|
331
|
+
throw new BadDataException(
|
|
332
|
+
"No SSL certificate has been ordered for this domain yet. Please order one first.",
|
|
333
|
+
);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/*
|
|
337
|
+
* Claim the cooldown before ordering anything, and claim it by putting the
|
|
338
|
+
* cooldown condition in the WHERE clause rather than in an `if` above the
|
|
339
|
+
* write. Two clicks that arrive together both read a row that is not
|
|
340
|
+
* cooling down; only the write can decide which one of them gets to spend
|
|
341
|
+
* the CA's allowance.
|
|
342
|
+
*
|
|
343
|
+
* The stamp goes down BEFORE the order and is never rolled back on
|
|
344
|
+
* failure. A rejected order still costs a validation attempt against the
|
|
345
|
+
* shared account, and a domain whose order fails is exactly the domain
|
|
346
|
+
* somebody presses again - so a failed attempt has to start the clock the
|
|
347
|
+
* same way a successful one does.
|
|
348
|
+
*/
|
|
349
|
+
const claimedRowCount: number = await this.updateOneBy({
|
|
350
|
+
query: {
|
|
351
|
+
_id: domainId.toString(),
|
|
352
|
+
certificateReissueRequestedAt: QueryHelper.lessThanEqualToOrNull(
|
|
353
|
+
CertificateReissueUtil.getCooldownCutoff(now),
|
|
354
|
+
),
|
|
355
|
+
},
|
|
356
|
+
data: {
|
|
357
|
+
certificateReissueRequestedAt: now,
|
|
358
|
+
},
|
|
359
|
+
props: {
|
|
360
|
+
isRoot: true,
|
|
361
|
+
},
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
if (claimedRowCount === 0) {
|
|
365
|
+
if (statusPageDomain.certificateReissueRequestedAt) {
|
|
366
|
+
throw new TooManyRequestsException(
|
|
367
|
+
CertificateReissueUtil.getCooldownMessage(
|
|
368
|
+
statusPageDomain.certificateReissueRequestedAt,
|
|
369
|
+
now,
|
|
370
|
+
),
|
|
371
|
+
);
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
/*
|
|
375
|
+
* The row matched the cooldown a moment ago and does not now: another
|
|
376
|
+
* request claimed it in between, or the domain was deleted. Either way
|
|
377
|
+
* the caller must not order.
|
|
378
|
+
*/
|
|
379
|
+
throw new BadDataException(
|
|
380
|
+
"Could not start a certificate reissue for this domain. Please refresh the page and try again.",
|
|
381
|
+
);
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
logger.debug(
|
|
385
|
+
"Reissuing SSL certificate for domain: " + statusPageDomain.fullDomain,
|
|
386
|
+
{ fullDomain: statusPageDomain.fullDomain } as LogAttributes,
|
|
387
|
+
);
|
|
388
|
+
|
|
389
|
+
await this.orderCert(statusPageDomain);
|
|
390
|
+
}
|
|
391
|
+
|
|
251
392
|
@CaptureSpan()
|
|
252
393
|
public async updateSslProvisioningStatusForAllDomains(): Promise<void> {
|
|
253
394
|
const domains: Array<StatusPageDomain> = await this.findBy({
|
|
@@ -257,6 +398,7 @@ export class Service extends DatabaseService<StatusPageDomain> {
|
|
|
257
398
|
},
|
|
258
399
|
select: {
|
|
259
400
|
_id: true,
|
|
401
|
+
fullDomain: true,
|
|
260
402
|
},
|
|
261
403
|
limit: LIMIT_MAX,
|
|
262
404
|
skip: 0,
|
|
@@ -265,9 +407,27 @@ export class Service extends DatabaseService<StatusPageDomain> {
|
|
|
265
407
|
},
|
|
266
408
|
});
|
|
267
409
|
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
410
|
+
/*
|
|
411
|
+
* Each domain costs a request out to the customer's status page, so walking
|
|
412
|
+
* the fleet one at a time makes the sweep as long as the fleet is slow.
|
|
413
|
+
* That length is the delay before an expired certificate is noticed and
|
|
414
|
+
* reordered, so it is the outage a customer sees when renewal has not
|
|
415
|
+
* already happened - it must stay well inside the sweep's own interval.
|
|
416
|
+
*/
|
|
417
|
+
await ArrayUtil.forEachWithConcurrency(
|
|
418
|
+
domains,
|
|
419
|
+
Service.SSL_PROVISIONING_CHECK_CONCURRENCY,
|
|
420
|
+
async (domain: StatusPageDomain): Promise<void> => {
|
|
421
|
+
try {
|
|
422
|
+
await this.updateSslProvisioningStatus(domain);
|
|
423
|
+
} catch (err) {
|
|
424
|
+
// one unreachable domain must not end the sweep for the rest.
|
|
425
|
+
logger.error(err, {
|
|
426
|
+
fullDomain: domain.fullDomain,
|
|
427
|
+
} as LogAttributes);
|
|
428
|
+
}
|
|
429
|
+
},
|
|
430
|
+
);
|
|
271
431
|
}
|
|
272
432
|
|
|
273
433
|
private async isSSLProvisioned(
|