@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.
Files changed (204) hide show
  1. package/Models/AnalyticsModels/RumSessionChunk.ts +52 -0
  2. package/Models/DatabaseModels/CloudResource.ts +17 -1
  3. package/Models/DatabaseModels/DashboardDomain.ts +43 -0
  4. package/Models/DatabaseModels/MonitorTemplate.ts +15 -3
  5. package/Models/DatabaseModels/RumApplication.ts +35 -2
  6. package/Models/DatabaseModels/StatusPageDomain.ts +43 -0
  7. package/Server/API/DashboardDomainAPI.ts +74 -0
  8. package/Server/API/MicrosoftTeamsAPI.ts +81 -180
  9. package/Server/API/StatusPageDomainAPI.ts +74 -0
  10. package/Server/API/TelemetryAPI.ts +104 -15
  11. package/Server/Infrastructure/Postgres/SchemaMigrations/1790200000000-AllowNullMonitorNameOnMonitorTemplate.ts +43 -0
  12. package/Server/Infrastructure/Postgres/SchemaMigrations/1790300000000-AddCertificateReissueRequestedAtToDomains.ts +40 -0
  13. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +4 -0
  14. package/Server/Middleware/ProjectAuthorization.ts +6 -1
  15. package/Server/Services/DashboardDomainService.ts +131 -0
  16. package/Server/Services/LogPipelineProcessorService.ts +88 -0
  17. package/Server/Services/MonitorTemplateService.ts +70 -11
  18. package/Server/Services/StatusPageDomainService.ts +163 -3
  19. package/Server/Services/StatusPagePrivateUserSessionService.ts +197 -56
  20. package/Server/Types/Domain.ts +17 -0
  21. package/Server/Utils/APIKey/AccessPermission.ts +24 -0
  22. package/Server/Utils/Greenlock/Greenlock.ts +156 -58
  23. package/Server/Utils/LogPipelineProcessorValidation.ts +112 -0
  24. package/Server/Utils/SecurityEvent/GoogleSecOps/GoogleSecOpsClient.ts +1094 -45
  25. package/Server/Utils/SecurityEvent/GoogleSecOps/GoogleSecOpsPoller.ts +153 -17
  26. package/Server/Utils/SessionReplay/SessionReplayGateCache.ts +12 -5
  27. package/Server/Utils/SessionReplay/SessionReplayIdentity.ts +102 -0
  28. package/Server/Utils/SessionReplay/SessionReplayReadService.ts +11 -2
  29. package/Server/Utils/Workspace/MicrosoftTeams/Actions/Alert.ts +23 -11
  30. package/Server/Utils/Workspace/MicrosoftTeams/Actions/Auth.ts +0 -170
  31. package/Server/Utils/Workspace/MicrosoftTeams/Actions/Authorization.ts +158 -0
  32. package/Server/Utils/Workspace/MicrosoftTeams/Actions/Incident.ts +23 -11
  33. package/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.ts +35 -13
  34. package/Tests/App/Dashboard/MonitorTemplateColumnAndFacet.test.tsx +505 -0
  35. package/Tests/App/StatusPage/StatusPageLoginCodeBootstrap.test.ts +106 -0
  36. package/Tests/App/StatusPage/StatusPageLoginCodeUtil.test.ts +84 -0
  37. package/Tests/App/StatusPage/StatusPageRedirectSafety.test.ts +119 -0
  38. package/Tests/Models/DatabaseModels/SessionReplayModels.test.ts +81 -0
  39. package/Tests/Models/MonitorTemplateMonitorNameColumn.test.ts +258 -0
  40. package/Tests/Server/API/CustomDomainReissueSslAPI.test.ts +469 -0
  41. package/Tests/Server/API/MicrosoftTeamsBotMessagesEndpoint.test.ts +729 -0
  42. package/Tests/Server/API/MicrosoftTeamsBotTestEndpoint.test.ts +26 -0
  43. package/Tests/Server/API/MicrosoftTeamsLegacyWebhookRemoval.test.ts +402 -0
  44. package/Tests/Server/API/SessionReplayAPI.test.ts +232 -0
  45. package/Tests/Server/Middleware/ProjectAuthorizationApiKeyMiddleware.test.ts +4 -0
  46. package/Tests/Server/Services/AddCertificateReissueRequestedAtToDomainsMigration.test.ts +287 -0
  47. package/Tests/Server/Services/CustomDomainCertificateReissue.test.ts +477 -0
  48. package/Tests/Server/Services/LogPipelineProcessorSaveValidation.test.ts +249 -0
  49. package/Tests/Server/Services/MonitorTemplateServiceBulkSync.test.ts +549 -0
  50. package/Tests/Server/Services/MonitorTemplateServiceNetworkDeviceSync.test.ts +16 -3
  51. package/Tests/Server/Services/NetworkDeviceAutoImportRuleEngineService.test.ts +57 -0
  52. package/Tests/Server/Services/SecurityEventLastErrorColumnWidth.test.ts +151 -15
  53. package/Tests/Server/Services/StatusPagePrivateUserSessionService.test.ts +587 -0
  54. package/Tests/Server/Types/Database/Permissions/ApiKeyFileAccess.test.ts +286 -0
  55. package/Tests/Server/Types/Database/Permissions/MonitorTemplateColumnAccess.test.ts +328 -0
  56. package/Tests/Server/Types/Database/QueryHelperFindWithSameTextAndIsNull.test.ts +83 -0
  57. package/Tests/Server/Utils/AI/SRE/ConfidenceSignal.test.ts +377 -0
  58. package/Tests/Server/Utils/APIKey/AccessPermission.test.ts +16 -1
  59. package/Tests/Server/Utils/Greenlock/CertificateRenewalSchedule.test.ts +250 -0
  60. package/Tests/Server/Utils/LogPipelineProcessorValidation.test.ts +198 -0
  61. package/Tests/Server/Utils/SecurityEvent/ConnectorErrorMessage.test.ts +48 -9
  62. package/Tests/Server/Utils/SecurityEvent/GoogleSecOpsAuth.test.ts +726 -0
  63. package/Tests/Server/Utils/SecurityEvent/GoogleSecOpsClient.test.ts +74 -11
  64. package/Tests/Server/Utils/SecurityEvent/GoogleSecOpsErrorHandling.test.ts +1107 -0
  65. package/Tests/Server/Utils/SecurityEvent/GoogleSecOpsPoller.test.ts +20 -3
  66. package/Tests/Server/Utils/SecurityEvent/GoogleSecOpsPollerFailureTaxonomy.test.ts +51 -11
  67. package/Tests/Server/Utils/SecurityEvent/GoogleSecOpsPollerHardening.test.ts +1246 -0
  68. package/Tests/Server/Utils/SecurityEvent/GoogleSecOpsRequestContract.test.ts +961 -0
  69. package/Tests/Server/Utils/SecurityEvent/GoogleSecOpsResponseParsing.test.ts +653 -0
  70. package/Tests/Server/Utils/Workspace/MicrosoftTeamsActionAuthorization.test.ts +880 -0
  71. package/Tests/Types/API/HTTPResponse.test.ts +175 -0
  72. package/Tests/Types/API/Route.test.ts +36 -4
  73. package/Tests/Types/Database/DatabaseProperty.test.ts +171 -0
  74. package/Tests/Types/DateLocalShortDateTimeString.test.ts +518 -0
  75. package/Tests/Types/DateLocalTimeString.test.ts +259 -0
  76. package/Tests/Types/DateUserPrefers12HourFormat.test.ts +316 -0
  77. package/Tests/UI/Components/LogsHistogram.test.tsx +41 -2
  78. package/Tests/UI/Components/LogsViewerEmptyStateTimeRange.test.tsx +212 -0
  79. package/Tests/UI/Components/TelemetryChartClockFormat.test.tsx +359 -0
  80. package/Tests/UI/Components/TimeRangePickerClockFormat.test.tsx +422 -0
  81. package/Tests/UI/Components/TimeRangePickerDropdown.test.tsx +146 -8
  82. package/Tests/UI/MicrosoftTeams/MicrosoftTeamsMessagingEndpointGuidance.test.tsx +520 -0
  83. package/Tests/UI/Utils/Navigation.test.ts +82 -0
  84. package/Tests/Utils/Array.test.ts +100 -0
  85. package/Tests/Utils/CertificateReissue.test.ts +225 -0
  86. package/Tests/Utils/Grok/Grok.test.ts +492 -0
  87. package/Tests/Utils/Monitor/NetworkDeviceMonitorTemplateUtil.test.ts +181 -13
  88. package/Types/API/Route.ts +20 -5
  89. package/Types/Date.ts +143 -13
  90. package/Types/Rum/SessionReplay.ts +76 -1
  91. package/UI/Components/Date/TimeRangePickerDropdown.tsx +9 -5
  92. package/UI/Components/LogsViewer/LogsViewer.tsx +12 -7
  93. package/UI/Components/LogsViewer/components/HistogramTooltip.tsx +18 -17
  94. package/UI/Components/LogsViewer/components/LogsAnalyticsView.tsx +21 -28
  95. package/UI/Components/LogsViewer/components/LogsHistogram.tsx +2 -4
  96. package/UI/Components/TelemetryViewer/components/TelemetryHistogram.tsx +2 -4
  97. package/UI/Components/TelemetryViewer/components/TelemetryHistogramTooltip.tsx +17 -16
  98. package/UI/Utils/Navigation.ts +42 -0
  99. package/Utils/Array.ts +37 -0
  100. package/Utils/CertificateReissue.ts +128 -0
  101. package/Utils/Grok/Grok.ts +461 -0
  102. package/Utils/Grok/GrokPatterns.ts +118 -0
  103. package/Utils/Monitor/NetworkDeviceMonitorTemplateUtil.ts +58 -6
  104. package/build/dist/Models/AnalyticsModels/RumSessionChunk.js +48 -0
  105. package/build/dist/Models/AnalyticsModels/RumSessionChunk.js.map +1 -1
  106. package/build/dist/Models/DatabaseModels/CloudResource.js +19 -2
  107. package/build/dist/Models/DatabaseModels/CloudResource.js.map +1 -1
  108. package/build/dist/Models/DatabaseModels/DashboardDomain.js +44 -0
  109. package/build/dist/Models/DatabaseModels/DashboardDomain.js.map +1 -1
  110. package/build/dist/Models/DatabaseModels/MonitorTemplate.js +17 -4
  111. package/build/dist/Models/DatabaseModels/MonitorTemplate.js.map +1 -1
  112. package/build/dist/Models/DatabaseModels/RumApplication.js +37 -3
  113. package/build/dist/Models/DatabaseModels/RumApplication.js.map +1 -1
  114. package/build/dist/Models/DatabaseModels/StatusPageDomain.js +44 -0
  115. package/build/dist/Models/DatabaseModels/StatusPageDomain.js.map +1 -1
  116. package/build/dist/Server/API/DashboardDomainAPI.js +43 -1
  117. package/build/dist/Server/API/DashboardDomainAPI.js.map +1 -1
  118. package/build/dist/Server/API/MicrosoftTeamsAPI.js +74 -118
  119. package/build/dist/Server/API/MicrosoftTeamsAPI.js.map +1 -1
  120. package/build/dist/Server/API/StatusPageDomainAPI.js +43 -1
  121. package/build/dist/Server/API/StatusPageDomainAPI.js.map +1 -1
  122. package/build/dist/Server/API/TelemetryAPI.js +67 -14
  123. package/build/dist/Server/API/TelemetryAPI.js.map +1 -1
  124. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1790200000000-AllowNullMonitorNameOnMonitorTemplate.js +34 -0
  125. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1790200000000-AllowNullMonitorNameOnMonitorTemplate.js.map +1 -0
  126. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1790300000000-AddCertificateReissueRequestedAtToDomains.js +28 -0
  127. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1790300000000-AddCertificateReissueRequestedAtToDomains.js.map +1 -0
  128. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +4 -0
  129. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  130. package/build/dist/Server/Middleware/ProjectAuthorization.js +6 -1
  131. package/build/dist/Server/Middleware/ProjectAuthorization.js.map +1 -1
  132. package/build/dist/Server/Services/DashboardDomainService.js +104 -0
  133. package/build/dist/Server/Services/DashboardDomainService.js.map +1 -1
  134. package/build/dist/Server/Services/LogPipelineProcessorService.js +65 -0
  135. package/build/dist/Server/Services/LogPipelineProcessorService.js.map +1 -1
  136. package/build/dist/Server/Services/MonitorTemplateService.js +57 -11
  137. package/build/dist/Server/Services/MonitorTemplateService.js.map +1 -1
  138. package/build/dist/Server/Services/StatusPageDomainService.js +132 -3
  139. package/build/dist/Server/Services/StatusPageDomainService.js.map +1 -1
  140. package/build/dist/Server/Services/StatusPagePrivateUserSessionService.js +112 -32
  141. package/build/dist/Server/Services/StatusPagePrivateUserSessionService.js.map +1 -1
  142. package/build/dist/Server/Types/Domain.js +16 -0
  143. package/build/dist/Server/Types/Domain.js.map +1 -1
  144. package/build/dist/Server/Utils/APIKey/AccessPermission.js +24 -0
  145. package/build/dist/Server/Utils/APIKey/AccessPermission.js.map +1 -1
  146. package/build/dist/Server/Utils/Greenlock/Greenlock.js +110 -35
  147. package/build/dist/Server/Utils/Greenlock/Greenlock.js.map +1 -1
  148. package/build/dist/Server/Utils/LogPipelineProcessorValidation.js +77 -0
  149. package/build/dist/Server/Utils/LogPipelineProcessorValidation.js.map +1 -0
  150. package/build/dist/Server/Utils/SecurityEvent/GoogleSecOps/GoogleSecOpsClient.js +777 -36
  151. package/build/dist/Server/Utils/SecurityEvent/GoogleSecOps/GoogleSecOpsClient.js.map +1 -1
  152. package/build/dist/Server/Utils/SecurityEvent/GoogleSecOps/GoogleSecOpsPoller.js +108 -17
  153. package/build/dist/Server/Utils/SecurityEvent/GoogleSecOps/GoogleSecOpsPoller.js.map +1 -1
  154. package/build/dist/Server/Utils/SessionReplay/SessionReplayGateCache.js.map +1 -1
  155. package/build/dist/Server/Utils/SessionReplay/SessionReplayIdentity.js +59 -0
  156. package/build/dist/Server/Utils/SessionReplay/SessionReplayIdentity.js.map +1 -0
  157. package/build/dist/Server/Utils/SessionReplay/SessionReplayReadService.js +9 -2
  158. package/build/dist/Server/Utils/SessionReplay/SessionReplayReadService.js.map +1 -1
  159. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/Alert.js +17 -6
  160. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/Alert.js.map +1 -1
  161. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/Auth.js +0 -159
  162. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/Auth.js.map +1 -1
  163. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/Authorization.js +128 -0
  164. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/Authorization.js.map +1 -0
  165. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/Incident.js +17 -6
  166. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/Incident.js.map +1 -1
  167. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.js +25 -13
  168. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.js.map +1 -1
  169. package/build/dist/Types/API/Route.js +16 -5
  170. package/build/dist/Types/API/Route.js.map +1 -1
  171. package/build/dist/Types/Date.js +115 -13
  172. package/build/dist/Types/Date.js.map +1 -1
  173. package/build/dist/Types/Rum/SessionReplay.js +38 -0
  174. package/build/dist/Types/Rum/SessionReplay.js.map +1 -1
  175. package/build/dist/UI/Components/Date/TimeRangePickerDropdown.js +9 -5
  176. package/build/dist/UI/Components/Date/TimeRangePickerDropdown.js.map +1 -1
  177. package/build/dist/UI/Components/LogsViewer/LogsViewer.js +12 -7
  178. package/build/dist/UI/Components/LogsViewer/LogsViewer.js.map +1 -1
  179. package/build/dist/UI/Components/LogsViewer/components/HistogramTooltip.js +15 -14
  180. package/build/dist/UI/Components/LogsViewer/components/HistogramTooltip.js.map +1 -1
  181. package/build/dist/UI/Components/LogsViewer/components/LogsAnalyticsView.js +17 -22
  182. package/build/dist/UI/Components/LogsViewer/components/LogsAnalyticsView.js.map +1 -1
  183. package/build/dist/UI/Components/LogsViewer/components/LogsHistogram.js +2 -4
  184. package/build/dist/UI/Components/LogsViewer/components/LogsHistogram.js.map +1 -1
  185. package/build/dist/UI/Components/TelemetryViewer/components/TelemetryHistogram.js +2 -4
  186. package/build/dist/UI/Components/TelemetryViewer/components/TelemetryHistogram.js.map +1 -1
  187. package/build/dist/UI/Components/TelemetryViewer/components/TelemetryHistogramTooltip.js +15 -14
  188. package/build/dist/UI/Components/TelemetryViewer/components/TelemetryHistogramTooltip.js.map +1 -1
  189. package/build/dist/UI/Utils/Navigation.js +30 -0
  190. package/build/dist/UI/Utils/Navigation.js.map +1 -1
  191. package/build/dist/Utils/Array.js +25 -0
  192. package/build/dist/Utils/Array.js.map +1 -1
  193. package/build/dist/Utils/CertificateReissue.js +97 -0
  194. package/build/dist/Utils/CertificateReissue.js.map +1 -0
  195. package/build/dist/Utils/Grok/Grok.js +324 -0
  196. package/build/dist/Utils/Grok/Grok.js.map +1 -0
  197. package/build/dist/Utils/Grok/GrokPatterns.js +112 -0
  198. package/build/dist/Utils/Grok/GrokPatterns.js.map +1 -0
  199. package/build/dist/Utils/Monitor/NetworkDeviceMonitorTemplateUtil.js +63 -3
  200. package/build/dist/Utils/Monitor/NetworkDeviceMonitorTemplateUtil.js.map +1 -1
  201. package/package.json +1 -1
  202. package/UI/Utils/JsonWebToken.ts +0 -14
  203. package/build/dist/UI/Utils/JsonWebToken.js +0 -10
  204. package/build/dist/UI/Utils/JsonWebToken.js.map +0 -1
@@ -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
- const syncedMonitors: number = await MonitorService.updateBy({
325
- query: {
326
- monitorTemplateId: template.id!,
327
- projectId: template.projectId,
328
- },
329
- data: updateData as any,
330
- limit: LIMIT_MAX,
331
- skip: 0,
332
- props: data.props,
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
- for (const domain of domains) {
269
- await this.updateSslProvisioningStatus(domain);
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(
@@ -9,6 +9,7 @@ import Text from "../../Types/Text";
9
9
  import logger from "../Utils/Logger";
10
10
  import Exception from "../../Types/Exception/Exception";
11
11
  import BadDataException from "../../Types/Exception/BadDataException";
12
+ import { JsonContains, MoreThan, UpdateResult } from "typeorm";
12
13
 
13
14
  export interface SessionMetadata {
14
15
  session: Model;
@@ -52,8 +53,24 @@ export interface RevokeSessionOptions {
52
53
  reason?: string | undefined;
53
54
  }
54
55
 
56
+ export interface ExchangeLoginCodeOptions {
57
+ statusPageId: ObjectID;
58
+ ipAddress?: string | undefined;
59
+ userAgent?: string | undefined;
60
+ deviceName?: string | undefined;
61
+ deviceType?: string | undefined;
62
+ deviceOS?: string | undefined;
63
+ deviceBrowser?: string | undefined;
64
+ additionalInfo?: JSONObject | undefined;
65
+ }
66
+
67
+ export const STATUS_PAGE_LOGIN_CODE_TTL_MINUTES: number = 5;
68
+
55
69
  export class Service extends DatabaseService<Model> {
56
70
  private static readonly DEFAULT_REFRESH_TOKEN_TTL_DAYS: number = 30;
71
+ private static readonly LOGIN_CODE_PURPOSE_KEY: string =
72
+ "oneuptimeStatusPageSessionPurpose";
73
+ private static readonly LOGIN_CODE_PURPOSE_VALUE: string = "login-code";
57
74
  private static readonly SHORT_TEXT_LIMIT: number = 100;
58
75
 
59
76
  public constructor() {
@@ -91,6 +108,31 @@ export class Service extends DatabaseService<Model> {
91
108
  }
92
109
  }
93
110
 
111
+ public async createLoginCodeSession(
112
+ options: Omit<
113
+ CreateSessionOptions,
114
+ "refreshToken" | "refreshTokenExpiresAt"
115
+ >,
116
+ ): Promise<SessionMetadata> {
117
+ return await this.createSession({
118
+ ...options,
119
+ additionalInfo: {
120
+ ...(options.additionalInfo || {}),
121
+ [Service.LOGIN_CODE_PURPOSE_KEY]: Service.LOGIN_CODE_PURPOSE_VALUE,
122
+ } as JSONObject,
123
+ refreshTokenExpiresAt: OneUptimeDate.getSomeMinutesAfter(
124
+ STATUS_PAGE_LOGIN_CODE_TTL_MINUTES,
125
+ ),
126
+ });
127
+ }
128
+
129
+ public isLoginCodeSession(session: Model): boolean {
130
+ return (
131
+ session.additionalInfo?.[Service.LOGIN_CODE_PURPOSE_KEY] ===
132
+ Service.LOGIN_CODE_PURPOSE_VALUE
133
+ );
134
+ }
135
+
94
136
  public async findActiveSessionByRefreshToken(
95
137
  refreshToken: string,
96
138
  ): Promise<Model | null> {
@@ -146,79 +188,111 @@ export class Service extends DatabaseService<Model> {
146
188
  const refreshTokenExpiresAt: Date =
147
189
  options.refreshTokenExpiresAt || Service.getRefreshTokenExpiry();
148
190
 
149
- const updatePayload: Partial<Model> = {
150
- refreshToken: HashedString.fromString(refreshToken),
151
- refreshTokenExpiresAt: refreshTokenExpiresAt,
152
- lastActiveAt: OneUptimeDate.getCurrentDate(),
153
- isRevoked: false,
154
- };
191
+ const updatePayload: Partial<Model> = this.buildRenewalUpdatePayload({
192
+ options,
193
+ refreshToken,
194
+ refreshTokenExpiresAt,
195
+ });
155
196
 
156
- const ipAddress: string | undefined = Text.truncate(
157
- options.ipAddress,
158
- Service.SHORT_TEXT_LIMIT,
159
- );
197
+ const updatedSession: Model | null = await this.updateOneByIdAndFetch({
198
+ id: options.session.id!,
199
+ data: updatePayload as any,
200
+ props: {
201
+ isRoot: true,
202
+ },
203
+ });
160
204
 
161
- if (ipAddress) {
162
- updatePayload.ipAddress = ipAddress;
205
+ if (!updatedSession) {
206
+ throw new BadDataException("Unable to renew status page user session");
163
207
  }
164
208
 
165
- if (options.userAgent) {
166
- updatePayload.userAgent = options.userAgent;
167
- }
209
+ return {
210
+ session: updatedSession,
211
+ refreshToken,
212
+ refreshTokenExpiresAt,
213
+ };
214
+ }
168
215
 
169
- const deviceName: string | undefined = Text.truncate(
170
- options.deviceName,
171
- Service.SHORT_TEXT_LIMIT,
172
- );
173
- if (deviceName) {
174
- updatePayload.deviceName = deviceName;
216
+ /**
217
+ * Atomically consume the short-lived login code and replace it with the
218
+ * long-lived refresh credential that is delivered only as an HttpOnly
219
+ * cookie. The refresh-token column is unique and included in the UPDATE
220
+ * predicate, so concurrent redemption attempts cannot both succeed.
221
+ */
222
+ public async exchangeLoginCode(
223
+ loginCode: string,
224
+ options: ExchangeLoginCodeOptions,
225
+ ): Promise<SessionMetadata | null> {
226
+ if (!ObjectID.isValidUUID(loginCode)) {
227
+ return null;
175
228
  }
176
229
 
177
- const deviceType: string | undefined = Text.truncate(
178
- options.deviceType,
179
- Service.SHORT_TEXT_LIMIT,
180
- );
181
- if (deviceType) {
182
- updatePayload.deviceType = deviceType;
230
+ const session: Model | null =
231
+ await this.findActiveSessionByRefreshToken(loginCode);
232
+
233
+ if (
234
+ !session?.id ||
235
+ !session.statusPageId ||
236
+ !this.isLoginCodeSession(session) ||
237
+ session.statusPageId.toString() !== options.statusPageId.toString()
238
+ ) {
239
+ return null;
183
240
  }
184
241
 
185
- const deviceOS: string | undefined = Text.truncate(
186
- options.deviceOS,
187
- Service.SHORT_TEXT_LIMIT,
242
+ const refreshToken: string = Service.generateRefreshToken();
243
+ const refreshTokenExpiresAt: Date = Service.getRefreshTokenExpiry();
244
+ const renewalOptions: RenewSessionOptions = {
245
+ session,
246
+ ipAddress: options.ipAddress,
247
+ userAgent: options.userAgent,
248
+ deviceName: options.deviceName,
249
+ deviceType: options.deviceType,
250
+ deviceOS: options.deviceOS,
251
+ deviceBrowser: options.deviceBrowser,
252
+ additionalInfo: options.additionalInfo,
253
+ };
254
+ const updatePayload: Partial<Model> = this.buildRenewalUpdatePayload({
255
+ options: renewalOptions,
256
+ refreshToken,
257
+ refreshTokenExpiresAt,
258
+ });
259
+ const refreshTokenHash: string = await HashedString.hashValue(
260
+ refreshToken,
261
+ EncryptionSecret,
262
+ );
263
+ updatePayload.refreshToken = new HashedString(refreshTokenHash, true);
264
+ const additionalInfo: JSONObject = {
265
+ ...(updatePayload.additionalInfo || {}),
266
+ } as JSONObject;
267
+ delete additionalInfo[Service.LOGIN_CODE_PURPOSE_KEY];
268
+ updatePayload.additionalInfo = additionalInfo;
269
+ const loginCodeHash: string = await HashedString.hashValue(
270
+ loginCode,
271
+ EncryptionSecret,
188
272
  );
189
- if (deviceOS) {
190
- updatePayload.deviceOS = deviceOS;
191
- }
192
273
 
193
- const deviceBrowser: string | undefined = Text.truncate(
194
- options.deviceBrowser,
195
- Service.SHORT_TEXT_LIMIT,
274
+ const updateResult: UpdateResult = await this.getRepository().update(
275
+ {
276
+ _id: session._id!,
277
+ statusPageId: options.statusPageId,
278
+ refreshToken: new HashedString(loginCodeHash, true),
279
+ refreshTokenExpiresAt: MoreThan(OneUptimeDate.getCurrentDate()),
280
+ isRevoked: false,
281
+ additionalInfo: JsonContains({
282
+ [Service.LOGIN_CODE_PURPOSE_KEY]: Service.LOGIN_CODE_PURPOSE_VALUE,
283
+ }),
284
+ } as any,
285
+ updatePayload as any,
196
286
  );
197
- if (deviceBrowser) {
198
- updatePayload.deviceBrowser = deviceBrowser;
199
- }
200
287
 
201
- if (options.additionalInfo || options.session.additionalInfo) {
202
- updatePayload.additionalInfo = {
203
- ...(options.session.additionalInfo || {}),
204
- ...(options.additionalInfo || {}),
205
- } as JSONObject;
288
+ if (updateResult.affected !== 1) {
289
+ return null;
206
290
  }
207
291
 
208
- const updatedSession: Model | null = await this.updateOneByIdAndFetch({
209
- id: options.session.id!,
210
- data: updatePayload as any,
211
- props: {
212
- isRoot: true,
213
- },
214
- });
215
-
216
- if (!updatedSession) {
217
- throw new BadDataException("Unable to renew status page user session");
218
- }
292
+ Object.assign(session, updatePayload);
219
293
 
220
294
  return {
221
- session: updatedSession,
295
+ session,
222
296
  refreshToken,
223
297
  refreshTokenExpiresAt,
224
298
  };
@@ -354,6 +428,73 @@ export class Service extends DatabaseService<Model> {
354
428
  return session;
355
429
  }
356
430
 
431
+ private buildRenewalUpdatePayload(data: {
432
+ options: RenewSessionOptions;
433
+ refreshToken: string;
434
+ refreshTokenExpiresAt: Date;
435
+ }): Partial<Model> {
436
+ const updatePayload: Partial<Model> = {
437
+ refreshToken: HashedString.fromString(data.refreshToken),
438
+ refreshTokenExpiresAt: data.refreshTokenExpiresAt,
439
+ lastActiveAt: OneUptimeDate.getCurrentDate(),
440
+ isRevoked: false,
441
+ };
442
+
443
+ const ipAddress: string | undefined = Text.truncate(
444
+ data.options.ipAddress,
445
+ Service.SHORT_TEXT_LIMIT,
446
+ );
447
+
448
+ if (ipAddress) {
449
+ updatePayload.ipAddress = ipAddress;
450
+ }
451
+
452
+ if (data.options.userAgent) {
453
+ updatePayload.userAgent = data.options.userAgent;
454
+ }
455
+
456
+ const deviceName: string | undefined = Text.truncate(
457
+ data.options.deviceName,
458
+ Service.SHORT_TEXT_LIMIT,
459
+ );
460
+ if (deviceName) {
461
+ updatePayload.deviceName = deviceName;
462
+ }
463
+
464
+ const deviceType: string | undefined = Text.truncate(
465
+ data.options.deviceType,
466
+ Service.SHORT_TEXT_LIMIT,
467
+ );
468
+ if (deviceType) {
469
+ updatePayload.deviceType = deviceType;
470
+ }
471
+
472
+ const deviceOS: string | undefined = Text.truncate(
473
+ data.options.deviceOS,
474
+ Service.SHORT_TEXT_LIMIT,
475
+ );
476
+ if (deviceOS) {
477
+ updatePayload.deviceOS = deviceOS;
478
+ }
479
+
480
+ const deviceBrowser: string | undefined = Text.truncate(
481
+ data.options.deviceBrowser,
482
+ Service.SHORT_TEXT_LIMIT,
483
+ );
484
+ if (deviceBrowser) {
485
+ updatePayload.deviceBrowser = deviceBrowser;
486
+ }
487
+
488
+ if (data.options.additionalInfo || data.options.session.additionalInfo) {
489
+ updatePayload.additionalInfo = {
490
+ ...(data.options.session.additionalInfo || {}),
491
+ ...(data.options.additionalInfo || {}),
492
+ } as JSONObject;
493
+ }
494
+
495
+ return updatePayload;
496
+ }
497
+
357
498
  private static generateRefreshToken(): string {
358
499
  return ObjectID.generate().toString();
359
500
  }
@@ -11,6 +11,13 @@ import URL from "../../Types/API/URL";
11
11
  import API from "../../Utils/API";
12
12
  import DataSourceEgressGuard from "../Utils/DataSource/EgressGuard";
13
13
 
14
+ /*
15
+ * A status page that is up answers the verification path well inside this.
16
+ * It is a ceiling on how long one unreachable domain may hold up the rest of a
17
+ * certificate run, not a latency target.
18
+ */
19
+ export const DOMAIN_VERIFICATION_TIMEOUT_IN_MS: number = 10000;
20
+
14
21
  export default class Domain extends DomainCommon {
15
22
  /*
16
23
  * GET a domain-verification URL built from a tenant-supplied domain.
@@ -26,6 +33,15 @@ export default class Domain extends DomainCommon {
26
33
  *
27
34
  * Private ranges stay reachable on self-hosted installs, where a status page
28
35
  * domain legitimately resolves inside the corporate network.
36
+ *
37
+ * The call is bounded. A customer domain that has been pointed at an address
38
+ * which silently drops packets - a decommissioned load balancer, a firewall
39
+ * that blackholes instead of refusing - never answers and never resets, so an
40
+ * unbounded request sits until the kernel gives up on the TCP connect, which
41
+ * is a little over two minutes on Linux. The certificate crons walk every
42
+ * status page domain in turn, so each such domain would burn that timeout out
43
+ * of the run's budget and push the domains behind it into the next run - the
44
+ * renewals that matter get starved by the domains that will never answer.
29
45
  */
30
46
  @CaptureSpan()
31
47
  public static async getForDomainVerification(data: {
@@ -41,6 +57,7 @@ export default class Domain extends DomainCommon {
41
57
  url: URL.fromString(data.url),
42
58
  options: {
43
59
  doNotFollowRedirects: true,
60
+ timeout: DOMAIN_VERIFICATION_TIMEOUT_IN_MS,
44
61
  httpAgent,
45
62
  httpsAgent,
46
63
  },
@@ -10,6 +10,28 @@ import ApiKeyPermissionService, {
10
10
  import CaptureSpan from "../Telemetry/CaptureSpan";
11
11
  import UserPermissionUtil from "../UserPermission/UserPermission";
12
12
 
13
+ /*
14
+ * Permission.AuthenticatedRequest is a marker, not a capability. It says "some
15
+ * authenticated principal made this call" - its own definition names API keys
16
+ * first - and unlike every other entry in these lists it has no PermissionProps,
17
+ * so it never appears in the admin picker and nobody can grant or revoke it per
18
+ * key. File and FileModel are the only models that name it, which is what bounds
19
+ * what including it here can widen.
20
+ *
21
+ * It is in the list because of TenantPermission.isAccessGrantedOnlyByCurrentUser.
22
+ * That predicate asks whether the ONLY thing letting a caller through a model's
23
+ * gate is the auto-granted CurrentUser - i.e. whether they are here as nothing
24
+ * more than some logged-in user - and when it says yes the caller is held to
25
+ * rows they own, which an API key can never satisfy because it has no userId.
26
+ * Without AuthenticatedRequest an API key intersects File's create list down to
27
+ * CurrentUser alone and answers yes to that question, which is simply the wrong
28
+ * description of a key: it is not a user acting on their own rows.
29
+ *
30
+ * On File as it stands today the wrong answer is harmless - File declares no
31
+ * user column, so the ownership branch returns before it can bite. The grant is
32
+ * here so that stays a fact about File's schema rather than the only thing
33
+ * holding API-key file access up.
34
+ */
13
35
  export default class APIKeyAccessPermission {
14
36
  @CaptureSpan()
15
37
  public static async getDefaultApiGlobalPermission(
@@ -21,6 +43,7 @@ export default class APIKeyAccessPermission {
21
43
  Permission.Public,
22
44
  Permission.User,
23
45
  Permission.CurrentUser,
46
+ Permission.AuthenticatedRequest,
24
47
  ],
25
48
  _type: "UserGlobalAccessPermission",
26
49
  };
@@ -36,6 +59,7 @@ export default class APIKeyAccessPermission {
36
59
  Permission.Public,
37
60
  Permission.User,
38
61
  Permission.CurrentUser,
62
+ Permission.AuthenticatedRequest,
39
63
  Permission.ProjectOwner,
40
64
  ],
41
65
  _type: "UserGlobalAccessPermission",