@oneuptime/common 13.0.8 → 14.0.1

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 (514) hide show
  1. package/Models/AnalyticsModels/AuditLog.ts +23 -15
  2. package/Models/DatabaseModels/GlobalConfig.ts +24 -0
  3. package/Models/DatabaseModels/Index.ts +4 -0
  4. package/Models/DatabaseModels/Monitor.ts +3 -2
  5. package/Models/DatabaseModels/MonitorTemplate.ts +2 -1
  6. package/Models/DatabaseModels/ServiceLevelObjectiveFeed.ts +2 -0
  7. package/Models/DatabaseModels/ServiceLevelObjectiveLabelRule.ts +522 -0
  8. package/Models/DatabaseModels/ServiceLevelObjectiveMonitorRule.ts +38 -4
  9. package/Models/DatabaseModels/ServiceLevelObjectiveOwnerRule.ts +603 -0
  10. package/Scripts/generate-service-worker.js +65 -4
  11. package/Server/API/BaseAPI.ts +26 -5
  12. package/Server/API/GlobalConfigAPI.ts +228 -462
  13. package/Server/API/ProjectOIDC.ts +25 -16
  14. package/Server/API/ProjectSSO.ts +24 -16
  15. package/Server/API/StatusPageAPI.ts +100 -42
  16. package/Server/API/TeamMemberAPI.ts +10 -1
  17. package/Server/Enterprise/EnterpriseEdition.ts +649 -0
  18. package/Server/Enterprise/EnterpriseFeature.ts +48 -0
  19. package/Server/Enterprise/EnterpriseLicenseSnapshot.ts +192 -0
  20. package/Server/Enterprise/EnterpriseServerModule.ts +233 -0
  21. package/Server/EnvironmentConfig.ts +147 -2
  22. package/Server/Infrastructure/Postgres/SchemaMigrations/1793900000000-AddEnterpriseEditionFirstSeenAt.ts +25 -0
  23. package/Server/Infrastructure/Postgres/SchemaMigrations/1794100000000-AddSloLabelAndOwnerRules.ts +248 -0
  24. package/Server/Infrastructure/Postgres/SchemaMigrations/1794200000000-AddSloMonitorRuleMonitorType.ts +19 -0
  25. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +6 -0
  26. package/Server/Middleware/UserAuthorization.ts +38 -16
  27. package/Server/Services/AuditLogService.ts +84 -1079
  28. package/Server/Services/DatabaseService.ts +10 -0
  29. package/Server/Services/EnterpriseLicenseService.ts +38 -0
  30. package/Server/Services/GlobalConfigService.ts +80 -1
  31. package/Server/Services/Index.ts +4 -0
  32. package/Server/Services/MonitorProbeService.ts +116 -20
  33. package/Server/Services/MonitorService.ts +59 -6
  34. package/Server/Services/MonitorStatusTimelineService.ts +284 -0
  35. package/Server/Services/MonitorTemplateService.ts +26 -0
  36. package/Server/Services/ProjectService.ts +284 -0
  37. package/Server/Services/RumSessionPinService.ts +25 -6
  38. package/Server/Services/ServiceLevelObjectiveLabelRuleEngineService.ts +385 -0
  39. package/Server/Services/ServiceLevelObjectiveLabelRuleService.ts +51 -0
  40. package/Server/Services/ServiceLevelObjectiveMonitorRuleEngineService.ts +92 -16
  41. package/Server/Services/ServiceLevelObjectiveMonitorRuleService.ts +36 -1
  42. package/Server/Services/ServiceLevelObjectiveOwnerRuleEngineService.ts +521 -0
  43. package/Server/Services/ServiceLevelObjectiveOwnerRuleService.ts +51 -0
  44. package/Server/Services/ServiceLevelObjectiveService.ts +28 -0
  45. package/Server/Services/StatusPagePrivateUserService.ts +7 -3
  46. package/Server/Services/StatusPageService.ts +78 -1
  47. package/Server/Services/TeamMemberService.ts +11 -0
  48. package/Server/Services/TeamService.ts +10 -0
  49. package/Server/Services/UserService.ts +10 -17
  50. package/Server/Types/Database/Permissions/CreatePermission.ts +15 -0
  51. package/Server/Types/Database/Permissions/EditionPermission.ts +208 -18
  52. package/Server/Types/Database/Permissions/TablePermission.ts +10 -3
  53. package/Server/Types/Database/Permissions/UpdatePermission.ts +29 -0
  54. package/Server/Types/Markdown.ts +41 -0
  55. package/Server/Types/Workflow/Components/Telegram/SendMessageToChat.ts +27 -1
  56. package/Server/Utils/AI/Toolbox/SloTools.ts +1 -0
  57. package/Server/Utils/CommunityEditionSsoReport.ts +361 -0
  58. package/Server/Utils/EditionEnforcement.ts +233 -0
  59. package/Server/Utils/FrontendEnvironment.ts +41 -2
  60. package/Server/Utils/InstanceHealth/RedisHealth.ts +12 -277
  61. package/Server/Utils/Monitor/AffectedResourceList.ts +148 -0
  62. package/Server/Utils/Monitor/Criteria/CompareCriteria.ts +1 -1
  63. package/Server/Utils/Monitor/Criteria/EvaluateOverTime.ts +10 -1
  64. package/Server/Utils/Monitor/Criteria/MetricMonitorCriteria.ts +1 -1
  65. package/Server/Utils/Monitor/MonitorCriteriaEvaluator.ts +720 -465
  66. package/Server/Utils/Monitor/MonitoringIntervalValidator.ts +116 -0
  67. package/Server/Utils/Monitor/RootCauseList.ts +139 -0
  68. package/Server/Utils/Rules/RuleRun/RuleRunRegistry.ts +28 -0
  69. package/Server/Utils/Slo/SloRulePatternValidator.ts +42 -0
  70. package/Server/Utils/Workspace/Slack/Slack.ts +423 -10
  71. package/Tests/App/Accounts/LoginPasskey.test.tsx +18 -0
  72. package/Tests/App/Accounts/LoginSsoEdition.test.tsx +616 -0
  73. package/Tests/App/AdminDashboard/AdminAuthenticationSsoEdition.test.tsx +217 -0
  74. package/Tests/App/AdminDashboard/AdminEnterprisePluginPage.test.tsx +357 -0
  75. package/Tests/App/AdminDashboard/EnterpriseLicensesShell.test.tsx +214 -0
  76. package/Tests/App/AdminDashboard/GlobalSSOEnterpriseShells.test.tsx +271 -0
  77. package/Tests/App/AdminDashboard/HealthShells.test.tsx +659 -0
  78. package/Tests/App/Dashboard/AuditLogsAndComplianceShells.test.tsx +572 -0
  79. package/Tests/App/Dashboard/EnterpriseFeatureUpgrade.test.tsx +547 -0
  80. package/Tests/App/Dashboard/EnterprisePluginPage.test.tsx +667 -0
  81. package/Tests/App/Dashboard/FeedFilterSortIntegration.test.tsx +1632 -0
  82. package/Tests/App/Dashboard/FeedOptionsLocales.test.ts +250 -0
  83. package/Tests/App/Dashboard/FeedOrdering.test.ts +279 -9
  84. package/Tests/App/Dashboard/IdentityEnterpriseShells.test.tsx +319 -0
  85. package/Tests/App/Dashboard/InvestigationFeedRefresh.test.tsx +60 -1
  86. package/Tests/App/Dashboard/InvestigationPanel.test.tsx +594 -3
  87. package/Tests/App/Dashboard/OnboardingSsoEmptyState.test.tsx +185 -0
  88. package/Tests/App/Dashboard/ProductOrientation.test.tsx +646 -0
  89. package/Tests/App/Dashboard/ResourceFeed.test.tsx +612 -2
  90. package/Tests/App/Dashboard/ResourceFeedPageWiring.test.ts +30 -0
  91. package/Tests/App/Dashboard/ResourceFeedPagesFilterSort.test.tsx +1202 -0
  92. package/Tests/App/Dashboard/ScheduledMaintenanceFeedRefresh.test.tsx +52 -1
  93. package/Tests/App/Dashboard/SecurityEventConnectionDiagnostics.test.tsx +45 -9
  94. package/Tests/App/Dashboard/SecurityEventDetailPanel.test.tsx +399 -0
  95. package/Tests/App/Dashboard/SecurityEventListRow.test.tsx +214 -0
  96. package/Tests/App/Dashboard/SecurityEventsEmptyState.test.tsx +1 -83
  97. package/Tests/App/Dashboard/SecurityEventsNavTabs.test.tsx +248 -0
  98. package/Tests/App/Dashboard/SecurityEventsViewer.test.tsx +1203 -0
  99. package/Tests/App/Dashboard/SloFeed.test.tsx +106 -1
  100. package/Tests/App/Dashboard/SloMonitorRuleCriteria.test.tsx +566 -0
  101. package/Tests/App/Dashboard/SloNavigationWiring.test.tsx +6 -4
  102. package/Tests/App/Dashboard/SloNoticeBanner.test.tsx +186 -0
  103. package/Tests/App/Dashboard/SloOverviewKpiStrip.test.tsx +388 -44
  104. package/Tests/App/Dashboard/SloOverviewLayout.test.tsx +613 -0
  105. package/Tests/App/Dashboard/UserEmailCallSites.test.tsx +11 -91
  106. package/Tests/App/StatusPage/StatusPageSsoEmptyState.test.tsx +319 -0
  107. package/Tests/Helpers/RefreshAwareApiScan.ts +757 -0
  108. package/Tests/Helpers/TenantScopedRequestScan.ts +515 -0
  109. package/Tests/Models/DatabaseModels/ResourceFeedModels.test.ts +0 -8
  110. package/Tests/Models/DatabaseModels/SloLabelAndOwnerRuleModels.test.ts +404 -0
  111. package/Tests/Models/DatabaseModels/SloOverhaulModels.test.ts +17 -1
  112. package/Tests/Models/DatabaseModels/StaticRuleCriteriaInheritance.test.ts +4 -2
  113. package/Tests/Scripts/GenerateServiceWorker.test.ts +931 -0
  114. package/Tests/Server/API/BaseAPICreateItemDoesNotMutateModel.test.ts +130 -0
  115. package/Tests/Server/API/GlobalConfigLicense.test.ts +742 -497
  116. package/Tests/Server/API/OnCallReadinessAPI.test.ts +35 -909
  117. package/Tests/Server/API/SsoProviderListingsEdition.test.ts +432 -0
  118. package/Tests/Server/API/TeamMemberLeaveScimLockEdition.test.ts +252 -0
  119. package/Tests/Server/Enterprise/EnterpriseEdition.test.ts +824 -0
  120. package/Tests/Server/Enterprise/EnterpriseFeatureActive.test.ts +643 -0
  121. package/Tests/Server/Enterprise/EnterpriseLicenseSnapshot.test.ts +294 -0
  122. package/Tests/Server/Enterprise/EnterpriseModelFeatureMap.test.ts +93 -0
  123. package/Tests/Server/Enterprise/EnterpriseOnlyModulesLiveInEe.test.ts +582 -0
  124. package/Tests/Server/Enterprise/EnterpriseServerModule.test.ts +247 -0
  125. package/Tests/Server/Enterprise/FakeEnterpriseModule.ts +502 -0
  126. package/Tests/Server/Enterprise/TestBillingFlag.ts +70 -0
  127. package/Tests/Server/EnvironmentConfigEnterpriseEdition.test.ts +314 -0
  128. package/Tests/Server/Infrastructure/ContainerBootConfiguration.test.ts +289 -0
  129. package/Tests/Server/Infrastructure/E2EClickHouseFixtureAccess.test.ts +5 -1
  130. package/Tests/Server/Infrastructure/Postgres/AddConfigurableRuleCriteriaMigration.test.ts +3 -0
  131. package/Tests/Server/Infrastructure/Postgres/AddSloLabelAndOwnerRulesMigration.test.ts +407 -0
  132. package/Tests/Server/Infrastructure/Postgres/AddSloMonitorRuleMonitorTypeMigration.test.ts +171 -0
  133. package/Tests/Server/Infrastructure/Postgres/SloProductOverhaulMigration.test.ts +88 -4
  134. package/Tests/Server/Middleware/UserAuthorization.test.ts +37 -2
  135. package/Tests/Server/Middleware/UserAuthorizationEditionEnforcement.test.ts +516 -0
  136. package/Tests/Server/Services/AuditLogService.test.ts +460 -990
  137. package/Tests/Server/Services/CreateAndReadResponsesKeepIds.test.ts +263 -0
  138. package/Tests/Server/Services/DatabaseServiceRuleCriteriaValidation.test.ts +104 -0
  139. package/Tests/Server/Services/EnterpriseLicenseServiceKeyGeneration.test.ts +151 -0
  140. package/Tests/Server/Services/GlobalConfigServiceLicenseColumns.test.ts +357 -0
  141. package/Tests/Server/Services/MonitorDestinationInfoIPv6.test.ts +101 -0
  142. package/Tests/Server/Services/MonitorProbeServiceIntervalScheduling.test.ts +333 -0
  143. package/Tests/Server/Services/MonitorStatusTimelineDailyAggregates.test.ts +303 -0
  144. package/Tests/Server/Services/ProjectServiceAuditLogLicense.test.ts +701 -0
  145. package/Tests/Server/Services/ProjectServiceAuditLogSettingsCache.test.ts +31 -35
  146. package/Tests/Server/Services/RuleRun/LabelRuleEnginesGroupBRun.test.ts +48 -1
  147. package/Tests/Server/Services/RuleRun/OwnerRuleEnginesGroupBRun.test.ts +30 -0
  148. package/Tests/Server/Services/ScimPushGroupsTeamLocksEdition.test.ts +431 -0
  149. package/Tests/Server/Services/ServiceLevelObjectiveLabelAndOwnerRuleEngines.test.ts +1170 -0
  150. package/Tests/Server/Services/ServiceLevelObjectiveLabelAndOwnerRuleServices.test.ts +172 -0
  151. package/Tests/Server/Services/ServiceLevelObjectiveMonitorRuleEngineService.test.ts +323 -0
  152. package/Tests/Server/Services/ServiceLevelObjectiveMonitorRuleService.test.ts +115 -0
  153. package/Tests/Server/Services/ServiceLevelObjectiveMonitorRuleTriggers.test.ts +21 -2
  154. package/Tests/Server/Services/ServiceLevelObjectiveMonitorTypePostgres.test.ts +480 -0
  155. package/Tests/Server/Services/ServiceLevelObjectiveService.test.ts +162 -0
  156. package/Tests/Server/Services/SsoRequirementReadMasking.test.ts +419 -0
  157. package/Tests/Server/Services/SsoRequirementWriteGuard.test.ts +769 -0
  158. package/Tests/Server/Services/StaticRuleCriteriaRuntimeCoverage.test.ts +45 -6
  159. package/Tests/Server/Services/StatusPagePrivateUserInvitationPolicy.test.ts +94 -0
  160. package/Tests/Server/Services/UserAuthenticationService.test.ts +35 -0
  161. package/Tests/Server/Services/UserServiceSeatLimit.test.ts +94 -56
  162. package/Tests/Server/Types/Database/FeedEventTypeFilterQuery.test.ts +745 -0
  163. package/Tests/Server/Types/Database/Permissions/EditionPermission.test.ts +1823 -0
  164. package/Tests/Server/Types/Markdown.test.ts +183 -1
  165. package/Tests/Server/Types/Workflow/Components/ApiComponentSsrf.test.ts +49 -0
  166. package/Tests/Server/Types/Workflow/Components/TelegramSendMessageToChat.test.ts +331 -0
  167. package/Tests/Server/Utils/AI/SloTools.test.ts +10 -0
  168. package/Tests/Server/Utils/CommunityEditionSsoReport.test.ts +565 -0
  169. package/Tests/Server/Utils/EditionEnforcement.test.ts +303 -0
  170. package/Tests/Server/Utils/FrontendEnvironment.test.ts +367 -0
  171. package/Tests/Server/Utils/InstanceHealth/RedisHealth.test.ts +38 -418
  172. package/Tests/Server/Utils/Monitor/AffectedResourceList.test.ts +518 -0
  173. package/Tests/Server/Utils/Monitor/Criteria/EvaluateOverTime.test.ts +24 -4
  174. package/Tests/Server/Utils/Monitor/Criteria/MetricMonitorCriteriaAnomalyUnits.test.ts +1 -1
  175. package/Tests/Server/Utils/Monitor/MonitorCriteriaEvaluator.test.ts +1339 -96
  176. package/Tests/Server/Utils/Monitor/MonitorCriteriaEvaluatorBreachingSamples.test.ts +1101 -0
  177. package/Tests/Server/Utils/Monitor/MonitorCriteriaEvaluatorMetricUnits.test.ts +106 -80
  178. package/Tests/Server/Utils/Monitor/MonitoringIntervalValidator.test.ts +188 -0
  179. package/Tests/Server/Utils/Monitor/RootCauseList.test.ts +252 -0
  180. package/Tests/Server/Utils/Rules/RuleRun/RuleRunPermission.test.ts +154 -0
  181. package/Tests/Server/Utils/SSRFProtection.test.ts +111 -0
  182. package/Tests/Server/Utils/Slo/SloRulePatternValidator.test.ts +67 -0
  183. package/Tests/Server/Utils/VM/VMRunnerSsrf.test.ts +213 -0
  184. package/Tests/Server/Utils/Workspace/Slack/SlackMarkdownSectionSplit.test.ts +1209 -0
  185. package/Tests/Types/API/HostnameFromAuthority.test.ts +20 -10
  186. package/Tests/Types/API/HostnameIPv6.test.ts +149 -0
  187. package/Tests/Types/API/Route.test.ts +56 -0
  188. package/Tests/Types/API/URLSchemeLikePath.test.ts +304 -0
  189. package/Tests/Types/Database/CanonicalModelInstance.test.ts +190 -0
  190. package/Tests/UI/Components/EditionLabelLapseCopy.ts +104 -0
  191. package/Tests/UI/Components/EditionLabelLicenseManagerWiring.test.tsx +254 -0
  192. package/Tests/UI/Components/EditionLabelLicenseRefresh.test.tsx +84 -178
  193. package/Tests/UI/Components/EditionLabelLicenseStatus.test.tsx +1586 -0
  194. package/Tests/UI/Components/EditionLabelLicenseWritesGuard.test.tsx +157 -0
  195. package/Tests/UI/Components/FeedOptions.test.ts +1404 -0
  196. package/Tests/UI/Components/FeedOptionsButton.test.tsx +2603 -0
  197. package/Tests/UI/Components/Graphs/DayUptimeGraph.test.tsx +49 -2
  198. package/Tests/UI/Components/Icon.test.tsx +26 -0
  199. package/Tests/UI/Components/MarkdownConverters.test.ts +684 -0
  200. package/Tests/UI/Components/ModelListEmptyState.test.tsx +289 -0
  201. package/Tests/UI/Components/RuleCriteria/RuleCriteriaModelTable.test.tsx +3 -3
  202. package/Tests/UI/Components/TelemetryViewer/TelemetryViewerEmptyContent.test.tsx +131 -0
  203. package/Tests/UI/Components/useFeedItemsViewKey.test.tsx +1017 -0
  204. package/Tests/UI/Components/useFeedOptions.test.tsx +1107 -0
  205. package/Tests/UI/EsbuildEnterpriseAlias.test.ts +1127 -0
  206. package/Tests/Utils/HostAddressUtil.test.ts +138 -0
  207. package/Tests/Utils/LabelRuleImportExport.test.ts +102 -0
  208. package/Tests/Utils/Monitor/MonitorDestinationUtil.test.ts +313 -0
  209. package/Tests/Utils/Monitor/MonitoringIntervalUtil.test.ts +304 -0
  210. package/Tests/Utils/Slo/SloDuration.test.ts +115 -24
  211. package/Tests/Utils/Slo/SloHealth.test.ts +93 -10
  212. package/Tests/Utils/Slo/SloMonitorRuleCriteria.test.ts +109 -0
  213. package/Tests/Utils/Slo/SloOverviewText.test.ts +64 -0
  214. package/Tests/Utils/StatusPage/UptimeDailyAggregateUtil.test.ts +248 -0
  215. package/Types/API/Hostname.ts +63 -16
  216. package/Types/API/Route.ts +19 -2
  217. package/Types/API/URL.ts +26 -2
  218. package/Types/Database/AccessControl/ColumnAccessControl.ts +19 -7
  219. package/Types/Database/AccessControl/ColumnBillingAccessControl.ts +19 -7
  220. package/Types/Database/AccessControl/OwnerOnlyColumn.ts +15 -6
  221. package/Types/Database/CanonicalModelInstance.ts +102 -0
  222. package/Types/Database/TableColumn.ts +20 -6
  223. package/Types/EnterpriseLicense/EnterpriseLicensePeriods.ts +29 -0
  224. package/Types/IP/IPv6.ts +6 -1
  225. package/Types/Monitor/MetricMonitor/MetricCriteriaContext.ts +6 -5
  226. package/Types/Monitor/MetricMonitor/MetricMonitorResponse.ts +9 -0
  227. package/Types/Monitor/MonitorStep.ts +6 -1
  228. package/Types/Monitor/ProxmoxAlertTemplates.ts +1 -1
  229. package/Types/Permission.ts +90 -0
  230. package/Types/Rules/RuleCriteriaFieldRegistry.ts +11 -0
  231. package/Types/Rules/RuleRun.ts +4 -0
  232. package/Types/StatusPage/UptimeDailyAggregate.ts +110 -0
  233. package/UI/Components/EditionLabel/EditionLabel.tsx +796 -1195
  234. package/UI/Components/EditionLabel/LicenseManager.ts +119 -0
  235. package/UI/Components/Feed/FeedOptions.ts +427 -0
  236. package/UI/Components/Feed/FeedOptionsButton.tsx +692 -0
  237. package/UI/Components/Feed/useFeedItems.ts +28 -9
  238. package/UI/Components/Feed/useFeedOptions.ts +225 -0
  239. package/UI/Components/Graphs/DayUptimeGraph.tsx +123 -15
  240. package/UI/Components/Icon/Icon.tsx +1 -1
  241. package/UI/Components/Markdown.tsx/MarkdownConverters.ts +0 -0
  242. package/UI/Components/ModelList/ModelList.tsx +11 -1
  243. package/UI/Components/MonitorGraphs/Uptime.tsx +69 -0
  244. package/UI/Components/TelemetryViewer/TelemetryViewer.tsx +28 -12
  245. package/UI/esbuild-enterprise.js +263 -0
  246. package/Utils/EnterpriseLicense/EnterpriseLicenseUsage.ts +7 -2
  247. package/Utils/HostAddressUtil.ts +94 -0
  248. package/Utils/LabelRuleImportExport.ts +27 -9
  249. package/Utils/Monitor/MetricValueFormatter.ts +2 -2
  250. package/Utils/Monitor/MonitorDestinationUtil.ts +157 -0
  251. package/Utils/Monitor/MonitoringIntervalUtil.ts +436 -0
  252. package/Utils/Rules/MonitorTypeRuleCriteria.ts +31 -0
  253. package/Utils/Slo/SloDuration.ts +22 -6
  254. package/Utils/Slo/SloHealth.ts +8 -7
  255. package/Utils/Slo/SloMonitorRuleCriteria.ts +33 -1
  256. package/Utils/Slo/SloOverviewText.ts +33 -0
  257. package/Utils/Slo/SloProjection.ts +4 -3
  258. package/Utils/StatusPage/UptimeDailyAggregateUtil.ts +192 -0
  259. package/build/dist/Models/AnalyticsModels/AuditLog.js +23 -15
  260. package/build/dist/Models/AnalyticsModels/AuditLog.js.map +1 -1
  261. package/build/dist/Models/DatabaseModels/GlobalConfig.js +25 -0
  262. package/build/dist/Models/DatabaseModels/GlobalConfig.js.map +1 -1
  263. package/build/dist/Models/DatabaseModels/Index.js +4 -0
  264. package/build/dist/Models/DatabaseModels/Index.js.map +1 -1
  265. package/build/dist/Models/DatabaseModels/Monitor.js +2 -2
  266. package/build/dist/Models/DatabaseModels/Monitor.js.map +1 -1
  267. package/build/dist/Models/DatabaseModels/MonitorTemplate.js +2 -1
  268. package/build/dist/Models/DatabaseModels/MonitorTemplate.js.map +1 -1
  269. package/build/dist/Models/DatabaseModels/ServiceLevelObjectiveFeed.js +2 -0
  270. package/build/dist/Models/DatabaseModels/ServiceLevelObjectiveFeed.js.map +1 -1
  271. package/build/dist/Models/DatabaseModels/ServiceLevelObjectiveLabelRule.js +530 -0
  272. package/build/dist/Models/DatabaseModels/ServiceLevelObjectiveLabelRule.js.map +1 -0
  273. package/build/dist/Models/DatabaseModels/ServiceLevelObjectiveMonitorRule.js +39 -4
  274. package/build/dist/Models/DatabaseModels/ServiceLevelObjectiveMonitorRule.js.map +1 -1
  275. package/build/dist/Models/DatabaseModels/ServiceLevelObjectiveOwnerRule.js +612 -0
  276. package/build/dist/Models/DatabaseModels/ServiceLevelObjectiveOwnerRule.js.map +1 -0
  277. package/build/dist/Server/API/BaseAPI.js +26 -5
  278. package/build/dist/Server/API/BaseAPI.js.map +1 -1
  279. package/build/dist/Server/API/GlobalConfigAPI.js +148 -344
  280. package/build/dist/Server/API/GlobalConfigAPI.js.map +1 -1
  281. package/build/dist/Server/API/ProjectOIDC.js +24 -16
  282. package/build/dist/Server/API/ProjectOIDC.js.map +1 -1
  283. package/build/dist/Server/API/ProjectSSO.js +24 -16
  284. package/build/dist/Server/API/ProjectSSO.js.map +1 -1
  285. package/build/dist/Server/API/StatusPageAPI.js +93 -43
  286. package/build/dist/Server/API/StatusPageAPI.js.map +1 -1
  287. package/build/dist/Server/API/TeamMemberAPI.js +8 -1
  288. package/build/dist/Server/API/TeamMemberAPI.js.map +1 -1
  289. package/build/dist/Server/Enterprise/EnterpriseEdition.js +466 -0
  290. package/build/dist/Server/Enterprise/EnterpriseEdition.js.map +1 -0
  291. package/build/dist/Server/Enterprise/EnterpriseFeature.js +39 -0
  292. package/build/dist/Server/Enterprise/EnterpriseFeature.js.map +1 -0
  293. package/build/dist/Server/Enterprise/EnterpriseLicenseSnapshot.js +70 -0
  294. package/build/dist/Server/Enterprise/EnterpriseLicenseSnapshot.js.map +1 -0
  295. package/build/dist/Server/Enterprise/EnterpriseServerModule.js +84 -0
  296. package/build/dist/Server/Enterprise/EnterpriseServerModule.js.map +1 -0
  297. package/build/dist/Server/EnvironmentConfig.js +84 -2
  298. package/build/dist/Server/EnvironmentConfig.js.map +1 -1
  299. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1793900000000-AddEnterpriseEditionFirstSeenAt.js +18 -0
  300. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1793900000000-AddEnterpriseEditionFirstSeenAt.js.map +1 -0
  301. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1794100000000-AddSloLabelAndOwnerRules.js +93 -0
  302. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1794100000000-AddSloLabelAndOwnerRules.js.map +1 -0
  303. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1794200000000-AddSloMonitorRuleMonitorType.js +12 -0
  304. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1794200000000-AddSloMonitorRuleMonitorType.js.map +1 -0
  305. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +6 -0
  306. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  307. package/build/dist/Server/Middleware/UserAuthorization.js +30 -11
  308. package/build/dist/Server/Middleware/UserAuthorization.js.map +1 -1
  309. package/build/dist/Server/Services/AuditLogService.js +71 -740
  310. package/build/dist/Server/Services/AuditLogService.js.map +1 -1
  311. package/build/dist/Server/Services/DatabaseService.js +7 -0
  312. package/build/dist/Server/Services/DatabaseService.js.map +1 -1
  313. package/build/dist/Server/Services/EnterpriseLicenseService.js +34 -0
  314. package/build/dist/Server/Services/EnterpriseLicenseService.js.map +1 -1
  315. package/build/dist/Server/Services/GlobalConfigService.js +63 -0
  316. package/build/dist/Server/Services/GlobalConfigService.js.map +1 -1
  317. package/build/dist/Server/Services/Index.js +4 -0
  318. package/build/dist/Server/Services/Index.js.map +1 -1
  319. package/build/dist/Server/Services/MonitorProbeService.js +90 -17
  320. package/build/dist/Server/Services/MonitorProbeService.js.map +1 -1
  321. package/build/dist/Server/Services/MonitorService.js +48 -6
  322. package/build/dist/Server/Services/MonitorService.js.map +1 -1
  323. package/build/dist/Server/Services/MonitorStatusTimelineService.js +242 -0
  324. package/build/dist/Server/Services/MonitorStatusTimelineService.js.map +1 -1
  325. package/build/dist/Server/Services/MonitorTemplateService.js +16 -0
  326. package/build/dist/Server/Services/MonitorTemplateService.js.map +1 -1
  327. package/build/dist/Server/Services/ProjectService.js +214 -0
  328. package/build/dist/Server/Services/ProjectService.js.map +1 -1
  329. package/build/dist/Server/Services/RumSessionPinService.js +24 -6
  330. package/build/dist/Server/Services/RumSessionPinService.js.map +1 -1
  331. package/build/dist/Server/Services/ServiceLevelObjectiveLabelRuleEngineService.js +295 -0
  332. package/build/dist/Server/Services/ServiceLevelObjectiveLabelRuleEngineService.js.map +1 -0
  333. package/build/dist/Server/Services/ServiceLevelObjectiveLabelRuleService.js +56 -0
  334. package/build/dist/Server/Services/ServiceLevelObjectiveLabelRuleService.js.map +1 -0
  335. package/build/dist/Server/Services/ServiceLevelObjectiveMonitorRuleEngineService.js +59 -14
  336. package/build/dist/Server/Services/ServiceLevelObjectiveMonitorRuleEngineService.js.map +1 -1
  337. package/build/dist/Server/Services/ServiceLevelObjectiveMonitorRuleService.js +22 -1
  338. package/build/dist/Server/Services/ServiceLevelObjectiveMonitorRuleService.js.map +1 -1
  339. package/build/dist/Server/Services/ServiceLevelObjectiveOwnerRuleEngineService.js +400 -0
  340. package/build/dist/Server/Services/ServiceLevelObjectiveOwnerRuleEngineService.js.map +1 -0
  341. package/build/dist/Server/Services/ServiceLevelObjectiveOwnerRuleService.js +56 -0
  342. package/build/dist/Server/Services/ServiceLevelObjectiveOwnerRuleService.js.map +1 -0
  343. package/build/dist/Server/Services/ServiceLevelObjectiveService.js +21 -0
  344. package/build/dist/Server/Services/ServiceLevelObjectiveService.js.map +1 -1
  345. package/build/dist/Server/Services/StatusPagePrivateUserService.js +7 -3
  346. package/build/dist/Server/Services/StatusPagePrivateUserService.js.map +1 -1
  347. package/build/dist/Server/Services/StatusPageService.js +71 -0
  348. package/build/dist/Server/Services/StatusPageService.js.map +1 -1
  349. package/build/dist/Server/Services/TeamMemberService.js +10 -0
  350. package/build/dist/Server/Services/TeamMemberService.js.map +1 -1
  351. package/build/dist/Server/Services/TeamService.js +9 -0
  352. package/build/dist/Server/Services/TeamService.js.map +1 -1
  353. package/build/dist/Server/Services/UserService.js +10 -16
  354. package/build/dist/Server/Services/UserService.js.map +1 -1
  355. package/build/dist/Server/Types/Database/Permissions/CreatePermission.js +10 -0
  356. package/build/dist/Server/Types/Database/Permissions/CreatePermission.js.map +1 -1
  357. package/build/dist/Server/Types/Database/Permissions/EditionPermission.js +154 -14
  358. package/build/dist/Server/Types/Database/Permissions/EditionPermission.js.map +1 -1
  359. package/build/dist/Server/Types/Database/Permissions/TablePermission.js +10 -3
  360. package/build/dist/Server/Types/Database/Permissions/TablePermission.js.map +1 -1
  361. package/build/dist/Server/Types/Database/Permissions/UpdatePermission.js +17 -0
  362. package/build/dist/Server/Types/Database/Permissions/UpdatePermission.js.map +1 -1
  363. package/build/dist/Server/Types/Markdown.js +31 -0
  364. package/build/dist/Server/Types/Markdown.js.map +1 -1
  365. package/build/dist/Server/Types/Workflow/Components/Telegram/SendMessageToChat.js +20 -1
  366. package/build/dist/Server/Types/Workflow/Components/Telegram/SendMessageToChat.js.map +1 -1
  367. package/build/dist/Server/Utils/AI/Toolbox/SloTools.js +1 -0
  368. package/build/dist/Server/Utils/AI/Toolbox/SloTools.js.map +1 -1
  369. package/build/dist/Server/Utils/CommunityEditionSsoReport.js +223 -0
  370. package/build/dist/Server/Utils/CommunityEditionSsoReport.js.map +1 -0
  371. package/build/dist/Server/Utils/EditionEnforcement.js +187 -0
  372. package/build/dist/Server/Utils/EditionEnforcement.js.map +1 -0
  373. package/build/dist/Server/Utils/FrontendEnvironment.js +31 -2
  374. package/build/dist/Server/Utils/FrontendEnvironment.js.map +1 -1
  375. package/build/dist/Server/Utils/InstanceHealth/RedisHealth.js +6 -193
  376. package/build/dist/Server/Utils/InstanceHealth/RedisHealth.js.map +1 -1
  377. package/build/dist/Server/Utils/Monitor/AffectedResourceList.js +67 -0
  378. package/build/dist/Server/Utils/Monitor/AffectedResourceList.js.map +1 -0
  379. package/build/dist/Server/Utils/Monitor/Criteria/CompareCriteria.js +1 -1
  380. package/build/dist/Server/Utils/Monitor/Criteria/EvaluateOverTime.js +8 -1
  381. package/build/dist/Server/Utils/Monitor/Criteria/EvaluateOverTime.js.map +1 -1
  382. package/build/dist/Server/Utils/Monitor/Criteria/MetricMonitorCriteria.js +1 -1
  383. package/build/dist/Server/Utils/Monitor/MonitorCriteriaEvaluator.js +585 -367
  384. package/build/dist/Server/Utils/Monitor/MonitorCriteriaEvaluator.js.map +1 -1
  385. package/build/dist/Server/Utils/Monitor/MonitoringIntervalValidator.js +88 -0
  386. package/build/dist/Server/Utils/Monitor/MonitoringIntervalValidator.js.map +1 -0
  387. package/build/dist/Server/Utils/Monitor/RootCauseList.js +101 -0
  388. package/build/dist/Server/Utils/Monitor/RootCauseList.js.map +1 -0
  389. package/build/dist/Server/Utils/Rules/RuleRun/RuleRunRegistry.js +28 -0
  390. package/build/dist/Server/Utils/Rules/RuleRun/RuleRunRegistry.js.map +1 -1
  391. package/build/dist/Server/Utils/Slo/SloRulePatternValidator.js +28 -0
  392. package/build/dist/Server/Utils/Slo/SloRulePatternValidator.js.map +1 -0
  393. package/build/dist/Server/Utils/Workspace/Slack/Slack.js +331 -11
  394. package/build/dist/Server/Utils/Workspace/Slack/Slack.js.map +1 -1
  395. package/build/dist/Types/API/Hostname.js +59 -11
  396. package/build/dist/Types/API/Hostname.js.map +1 -1
  397. package/build/dist/Types/API/Route.js +18 -2
  398. package/build/dist/Types/API/Route.js.map +1 -1
  399. package/build/dist/Types/API/URL.js +23 -2
  400. package/build/dist/Types/API/URL.js.map +1 -1
  401. package/build/dist/Types/Database/AccessControl/ColumnAccessControl.js +19 -7
  402. package/build/dist/Types/Database/AccessControl/ColumnAccessControl.js.map +1 -1
  403. package/build/dist/Types/Database/AccessControl/ColumnBillingAccessControl.js +19 -7
  404. package/build/dist/Types/Database/AccessControl/ColumnBillingAccessControl.js.map +1 -1
  405. package/build/dist/Types/Database/AccessControl/OwnerOnlyColumn.js +13 -6
  406. package/build/dist/Types/Database/AccessControl/OwnerOnlyColumn.js.map +1 -1
  407. package/build/dist/Types/Database/CanonicalModelInstance.js +88 -0
  408. package/build/dist/Types/Database/CanonicalModelInstance.js.map +1 -0
  409. package/build/dist/Types/Database/TableColumn.js +19 -6
  410. package/build/dist/Types/Database/TableColumn.js.map +1 -1
  411. package/build/dist/Types/EnterpriseLicense/EnterpriseLicensePeriods.js +28 -0
  412. package/build/dist/Types/EnterpriseLicense/EnterpriseLicensePeriods.js.map +1 -0
  413. package/build/dist/Types/IP/IPv6.js +6 -1
  414. package/build/dist/Types/IP/IPv6.js.map +1 -1
  415. package/build/dist/Types/Monitor/MonitorStep.js +5 -0
  416. package/build/dist/Types/Monitor/MonitorStep.js.map +1 -1
  417. package/build/dist/Types/Monitor/ProxmoxAlertTemplates.js +1 -1
  418. package/build/dist/Types/Permission.js +84 -0
  419. package/build/dist/Types/Permission.js.map +1 -1
  420. package/build/dist/Types/Rules/RuleCriteriaFieldRegistry.js +11 -0
  421. package/build/dist/Types/Rules/RuleCriteriaFieldRegistry.js.map +1 -1
  422. package/build/dist/Types/Rules/RuleRun.js +4 -0
  423. package/build/dist/Types/Rules/RuleRun.js.map +1 -1
  424. package/build/dist/Types/StatusPage/UptimeDailyAggregate.js +32 -0
  425. package/build/dist/Types/StatusPage/UptimeDailyAggregate.js.map +1 -0
  426. package/build/dist/UI/Components/EditionLabel/EditionLabel.js +414 -585
  427. package/build/dist/UI/Components/EditionLabel/EditionLabel.js.map +1 -1
  428. package/build/dist/UI/Components/EditionLabel/LicenseManager.js +2 -0
  429. package/build/dist/UI/Components/EditionLabel/LicenseManager.js.map +1 -0
  430. package/build/dist/UI/Components/Feed/FeedOptions.js +235 -0
  431. package/build/dist/UI/Components/Feed/FeedOptions.js.map +1 -0
  432. package/build/dist/UI/Components/Feed/FeedOptionsButton.js +346 -0
  433. package/build/dist/UI/Components/Feed/FeedOptionsButton.js.map +1 -0
  434. package/build/dist/UI/Components/Feed/useFeedItems.js +17 -7
  435. package/build/dist/UI/Components/Feed/useFeedItems.js.map +1 -1
  436. package/build/dist/UI/Components/Feed/useFeedOptions.js +130 -0
  437. package/build/dist/UI/Components/Feed/useFeedOptions.js.map +1 -0
  438. package/build/dist/UI/Components/Graphs/DayUptimeGraph.js +90 -17
  439. package/build/dist/UI/Components/Graphs/DayUptimeGraph.js.map +1 -1
  440. package/build/dist/UI/Components/Icon/Icon.js +1 -1
  441. package/build/dist/UI/Components/Icon/Icon.js.map +1 -1
  442. package/build/dist/UI/Components/Markdown.tsx/MarkdownConverters.js +0 -0
  443. package/build/dist/UI/Components/Markdown.tsx/MarkdownConverters.js.map +1 -1
  444. package/build/dist/UI/Components/ModelList/ModelList.js +4 -1
  445. package/build/dist/UI/Components/ModelList/ModelList.js.map +1 -1
  446. package/build/dist/UI/Components/MonitorGraphs/Uptime.js +42 -1
  447. package/build/dist/UI/Components/MonitorGraphs/Uptime.js.map +1 -1
  448. package/build/dist/UI/Components/TelemetryViewer/TelemetryViewer.js +2 -2
  449. package/build/dist/UI/Components/TelemetryViewer/TelemetryViewer.js.map +1 -1
  450. package/build/dist/Utils/EnterpriseLicense/EnterpriseLicenseUsage.js +6 -2
  451. package/build/dist/Utils/EnterpriseLicense/EnterpriseLicenseUsage.js.map +1 -1
  452. package/build/dist/Utils/HostAddressUtil.js +80 -0
  453. package/build/dist/Utils/HostAddressUtil.js.map +1 -0
  454. package/build/dist/Utils/LabelRuleImportExport.js +22 -6
  455. package/build/dist/Utils/LabelRuleImportExport.js.map +1 -1
  456. package/build/dist/Utils/Monitor/MetricValueFormatter.js +2 -2
  457. package/build/dist/Utils/Monitor/MonitorDestinationUtil.js +127 -0
  458. package/build/dist/Utils/Monitor/MonitorDestinationUtil.js.map +1 -0
  459. package/build/dist/Utils/Monitor/MonitoringIntervalUtil.js +374 -0
  460. package/build/dist/Utils/Monitor/MonitoringIntervalUtil.js.map +1 -0
  461. package/build/dist/Utils/Rules/MonitorTypeRuleCriteria.js +18 -0
  462. package/build/dist/Utils/Rules/MonitorTypeRuleCriteria.js.map +1 -0
  463. package/build/dist/Utils/Slo/SloDuration.js +13 -5
  464. package/build/dist/Utils/Slo/SloDuration.js.map +1 -1
  465. package/build/dist/Utils/Slo/SloHealth.js +8 -7
  466. package/build/dist/Utils/Slo/SloHealth.js.map +1 -1
  467. package/build/dist/Utils/Slo/SloMonitorRuleCriteria.js +16 -1
  468. package/build/dist/Utils/Slo/SloMonitorRuleCriteria.js.map +1 -1
  469. package/build/dist/Utils/Slo/SloOverviewText.js +16 -0
  470. package/build/dist/Utils/Slo/SloOverviewText.js.map +1 -1
  471. package/build/dist/Utils/Slo/SloProjection.js.map +1 -1
  472. package/build/dist/Utils/StatusPage/UptimeDailyAggregateUtil.js +148 -0
  473. package/build/dist/Utils/StatusPage/UptimeDailyAggregateUtil.js.map +1 -0
  474. package/jest.config.json +3 -1
  475. package/package.json +1 -1
  476. package/test-setup.sh +103 -34
  477. package/tsconfig.json +13 -0
  478. package/Server/API/EnterpriseLicenseAPI.ts +0 -925
  479. package/Server/API/TeamComplianceAPI.ts +0 -158
  480. package/Server/Middleware/SCIMAuthorization.ts +0 -141
  481. package/Server/Services/TeamComplianceService.ts +0 -733
  482. package/Server/Utils/EnterpriseLicense/EnterpriseLicenseSeatUtil.ts +0 -186
  483. package/Server/Utils/InstanceHealth/PostgresHealth.ts +0 -286
  484. package/Tests/App/AdminDashboard/EnterpriseLicenseInstanceActivity.test.tsx +0 -451
  485. package/Tests/App/Dashboard/AuditLogsTable.test.tsx +0 -598
  486. package/Tests/App/Dashboard/SecurityEventsExplorer.test.tsx +0 -761
  487. package/Tests/App/Dashboard/SecurityEventsSideMenu.test.tsx +0 -247
  488. package/Tests/App/Dashboard/SecurityEventsTableColumns.test.tsx +0 -382
  489. package/Tests/App/Dashboard/SecurityEventsVolumeChart.test.tsx +0 -321
  490. package/Tests/Server/API/EnterpriseLicenseReportUserCount.test.ts +0 -1851
  491. package/Tests/Server/Middleware/SCIMAuthorization.test.ts +0 -525
  492. package/Tests/Server/Services/TeamComplianceServiceBehaviour.test.ts +0 -1845
  493. package/Tests/Server/Utils/EnterpriseLicense/EnterpriseLicenseSeatUtil.test.ts +0 -495
  494. package/Tests/Server/Utils/InstanceHealth/PostgresHealth.test.ts +0 -337
  495. package/Tests/Utils/EnterpriseLicenseSeats.test.ts +0 -576
  496. package/Tests/Utils/EnterpriseLicenseSync.test.ts +0 -568
  497. package/Utils/EnterpriseLicense/EnterpriseLicenseSeats.ts +0 -287
  498. package/Utils/EnterpriseLicense/EnterpriseLicenseSync.ts +0 -244
  499. package/build/dist/Server/API/EnterpriseLicenseAPI.js +0 -646
  500. package/build/dist/Server/API/EnterpriseLicenseAPI.js.map +0 -1
  501. package/build/dist/Server/API/TeamComplianceAPI.js +0 -111
  502. package/build/dist/Server/API/TeamComplianceAPI.js.map +0 -1
  503. package/build/dist/Server/Middleware/SCIMAuthorization.js +0 -124
  504. package/build/dist/Server/Middleware/SCIMAuthorization.js.map +0 -1
  505. package/build/dist/Server/Services/TeamComplianceService.js +0 -537
  506. package/build/dist/Server/Services/TeamComplianceService.js.map +0 -1
  507. package/build/dist/Server/Utils/EnterpriseLicense/EnterpriseLicenseSeatUtil.js +0 -145
  508. package/build/dist/Server/Utils/EnterpriseLicense/EnterpriseLicenseSeatUtil.js.map +0 -1
  509. package/build/dist/Server/Utils/InstanceHealth/PostgresHealth.js +0 -194
  510. package/build/dist/Server/Utils/InstanceHealth/PostgresHealth.js.map +0 -1
  511. package/build/dist/Utils/EnterpriseLicense/EnterpriseLicenseSeats.js +0 -168
  512. package/build/dist/Utils/EnterpriseLicense/EnterpriseLicenseSeats.js.map +0 -1
  513. package/build/dist/Utils/EnterpriseLicense/EnterpriseLicenseSync.js +0 -174
  514. package/build/dist/Utils/EnterpriseLicense/EnterpriseLicenseSync.js.map +0 -1
@@ -1,1124 +1,594 @@
1
- import { AuditLogService } from "../../../Server/Services/AuditLogService";
2
- import DatabaseService from "../../../Server/Services/DatabaseService";
3
- import FindBy from "../../../Server/Types/Database/FindBy";
1
+ import { afterEach, beforeEach, describe, expect, test } from "@jest/globals";
2
+ import AuditLogServiceInstance, {
3
+ AUDIT_LOG_CREATE_REFUSED_MESSAGE,
4
+ AuditLogService,
5
+ } from "../../../Server/Services/AuditLogService";
6
+ import AnalyticsDatabaseService from "../../../Server/Services/AnalyticsDatabaseService";
7
+ import ModelPermission from "../../../Server/Types/AnalyticsDatabase/ModelPermission";
8
+ import EnterpriseEdition from "../../../Server/Enterprise/EnterpriseEdition";
9
+ import logger from "../../../Server/Utils/Logger";
4
10
  import AuditLog from "../../../Models/AnalyticsModels/AuditLog";
5
- import BaseModel from "../../../Models/DatabaseModels/DatabaseBaseModel/DatabaseBaseModel";
6
- import AlertSeverity from "../../../Models/DatabaseModels/AlertSeverity";
7
- import Label from "../../../Models/DatabaseModels/Label";
8
11
  import Monitor from "../../../Models/DatabaseModels/Monitor";
9
- import Project from "../../../Models/DatabaseModels/Project";
10
- import ServiceLevelObjective from "../../../Models/DatabaseModels/ServiceLevelObjective";
11
- import ServiceLevelObjectiveBurnRateRule from "../../../Models/DatabaseModels/ServiceLevelObjectiveBurnRateRule";
12
- import ServiceLevelObjectiveMonitorRule from "../../../Models/DatabaseModels/ServiceLevelObjectiveMonitorRule";
13
- import ServiceLevelObjectiveOwnerTeam from "../../../Models/DatabaseModels/ServiceLevelObjectiveOwnerTeam";
14
- import ServiceLevelObjectiveOwnerUser from "../../../Models/DatabaseModels/ServiceLevelObjectiveOwnerUser";
15
- import Team from "../../../Models/DatabaseModels/Team";
16
- import User from "../../../Models/DatabaseModels/User";
17
- import AuditLogAction from "../../../Types/AuditLog/AuditLogAction";
12
+ import AnalyticsTableColumn from "../../../Types/AnalyticsDatabase/TableColumn";
18
13
  import DatabaseCommonInteractionProps from "../../../Types/BaseDatabase/DatabaseCommonInteractionProps";
19
- import { PlanType } from "../../../Types/Billing/SubscriptionPlan";
20
- import Email from "../../../Types/Email";
21
- import { JSONObject } from "../../../Types/JSON";
22
- import Name from "../../../Types/Name";
14
+ import NotAuthorizedException from "../../../Types/Exception/NotAuthorizedException";
23
15
  import ObjectID from "../../../Types/ObjectID";
16
+ import Permission, {
17
+ UserTenantAccessPermission,
18
+ } from "../../../Types/Permission";
24
19
  import UserType from "../../../Types/UserType";
25
- import { afterEach, beforeEach, describe, expect, test } from "@jest/globals";
20
+ import FakeEnterpriseModule, {
21
+ createAuditLogRecorderSpy,
22
+ installFakeEnterpriseModule,
23
+ MockedAuditLogRecorder,
24
+ uninstallEnterpriseModule,
25
+ } from "../Enterprise/FakeEnterpriseModule";
26
+ import { setTestBillingEnabled } from "../Enterprise/TestBillingFlag";
26
27
 
27
28
  /*
28
- * AuditLogService decides whether a change is recorded, and what the entry
29
- * says. Every failure mode here is silent - record* swallows its errors and
30
- * an empty audit page looks exactly like "nobody changed anything" - so each
31
- * rule is pinned against the entry that actually reaches the insert:
29
+ * Core's AuditLogService after the Community / Enterprise split.
32
30
  *
33
- * - eligibility: Enterprise gate, the project's switch, system events;
34
- * - identity: resourceType is the model's singularName, and every entry
35
- * carries the root resource it rolls up to (an SLO's burn-rate rules,
36
- * monitor rules and owners roll up to the SLO);
37
- * - noise: the evaluation worker's columns never produce an entry;
38
- * - readability: relations are recorded as { _id, name }, nameless owner
39
- * rows are named after their user or team, and names are only ever read
40
- * from inside the audited project.
31
+ * Audit-log RECORDING is an Enterprise Edition feature: which changes are
32
+ * recorded, the diff, redaction, names and the settings cache live in ee/
33
+ * (ee/Server/AuditLog/AuditLogRecorder.ts, pinned by
34
+ * ee/Tests/Server/AuditLog/AuditLogRecorder.test.ts). What stays here, and is
35
+ * pinned here, is the seam every audited write goes through:
41
36
  *
42
- * No ClickHouse or Postgres: the insert, the project and user reads and the
43
- * related-row lookups are all stubbed. ProjectService and UserService are
44
- * module stubs rather than spies: the real modules pull in most of the service
45
- * graph, none of which a settings or user read here needs.
37
+ * - Community Edition: no recorder, every call is a no-op and nothing is
38
+ * written - there is no recording code left in core to write it;
39
+ * - Enterprise Edition: each call reaches the recorder with exactly the
40
+ * arguments DatabaseService passed, looked up on every call (the module
41
+ * registers after core loaded);
42
+ * - never a failed write because of its audit entry, whatever the recorder
43
+ * does.
44
+ *
45
+ * Billing and the edition are pinned in every test (CI's config.env sets
46
+ * BILLING_ENABLED=true). This suite must pass with ee/ deleted: it uses only
47
+ * the fake enterprise module.
46
48
  */
47
-
48
- const findProjectMock: jest.Mock = jest.fn();
49
- const findUserMock: jest.Mock = jest.fn();
50
-
51
- jest.mock("../../../Server/Services/ProjectService", () => {
52
- return {
53
- __esModule: true,
54
- default: {
55
- findOneById: (...args: Array<unknown>): unknown => {
56
- return findProjectMock(...args);
57
- },
58
- },
59
- };
60
- });
61
-
62
- jest.mock("../../../Server/Services/UserService", () => {
63
- return {
64
- __esModule: true,
65
- default: {
66
- findOneById: (...args: Array<unknown>): unknown => {
67
- return findUserMock(...args);
68
- },
69
- },
70
- };
71
- });
72
-
73
49
  jest.mock("../../../Server/EnvironmentConfig", () => {
74
- const actual: Record<string, unknown> = jest.requireActual(
75
- "../../../Server/EnvironmentConfig",
76
- ) as Record<string, unknown>;
77
-
78
- // A writable copy: each test sets the build flags it needs.
79
- return { ...actual, IsEnterpriseEdition: true, IsBillingEnabled: false };
50
+ const billingFlag: typeof import("../Enterprise/TestBillingFlag") =
51
+ jest.requireActual(
52
+ "../Enterprise/TestBillingFlag",
53
+ ) as typeof import("../Enterprise/TestBillingFlag");
54
+
55
+ return billingFlag.withLiveBillingFlag(
56
+ jest.requireActual("../../../Server/EnvironmentConfig") as Record<
57
+ string,
58
+ unknown
59
+ >,
60
+ );
80
61
  });
81
62
 
82
- type EnvironmentFlags = {
83
- IsEnterpriseEdition: boolean;
84
- IsBillingEnabled: boolean;
85
- };
86
-
87
- const environment: EnvironmentFlags = jest.requireMock(
88
- "../../../Server/EnvironmentConfig",
89
- ) as EnvironmentFlags;
90
-
91
63
  const PROJECT_ID: ObjectID = new ObjectID(
92
64
  "11111111-1111-4111-8111-111111111111",
93
65
  );
94
- const USER_ID: ObjectID = new ObjectID("22222222-2222-4222-8222-222222222222");
95
- const SLO_ID: ObjectID = new ObjectID("33333333-3333-4333-8333-333333333333");
96
- const RULE_ID: ObjectID = new ObjectID("44444444-4444-4444-8444-444444444444");
97
- const OWNER_ROW_ID: ObjectID = new ObjectID(
98
- "55555555-5555-4555-8555-555555555555",
99
- );
100
- const OWNER_USER_ID: ObjectID = new ObjectID(
101
- "66666666-6666-4666-8666-666666666666",
102
- );
103
- const TEAM_ID: ObjectID = new ObjectID("77777777-7777-4777-8777-777777777777");
104
66
  const MONITOR_ID: ObjectID = new ObjectID(
105
67
  "88888888-8888-4888-8888-888888888888",
106
68
  );
107
69
 
108
- const LABEL_A: string = "aaaaaaaa-0000-4000-8000-00000000000a";
109
- const LABEL_B: string = "aaaaaaaa-0000-4000-8000-00000000000b";
110
- const LABEL_C: string = "aaaaaaaa-0000-4000-8000-00000000000c";
111
-
112
- const SEVERITY_1: string = "bbbbbbbb-0000-4000-8000-000000000001";
113
- const SEVERITY_2: string = "bbbbbbbb-0000-4000-8000-000000000002";
114
-
115
- const SLO_RESOURCE_TYPE: string = "Service Level Objective";
116
-
117
- const USER_PROPS: DatabaseCommonInteractionProps = {
118
- userId: USER_ID,
70
+ const PROPS: DatabaseCommonInteractionProps = {
71
+ userId: new ObjectID("22222222-2222-4222-8222-222222222222"),
119
72
  userType: UserType.User,
120
73
  tenantId: PROJECT_ID,
121
74
  };
122
75
 
123
- // How the evaluation worker and the monitor-rule engine write.
124
- const SYSTEM_PROPS: DatabaseCommonInteractionProps = { isRoot: true };
125
-
126
- interface RelatedLookup {
127
- tableName: string;
128
- query: JSONObject;
129
- select: JSONObject;
130
- props: JSONObject;
131
- }
132
-
133
- interface Harness {
134
- service: AuditLogService;
135
- inserted: Array<AuditLog>;
136
- findProject: jest.Mock;
137
- findUser: jest.Mock;
138
- insert: jest.SpyInstance;
139
- relatedLookups: Array<RelatedLookup>;
140
- }
141
-
142
- let project: Project;
143
- let usersById: Map<string, User>;
144
- let relatedRows: Map<string, Array<BaseModel>>;
145
- let failRelatedLookups: boolean;
146
- let harness: Harness;
147
-
148
- function makeProject(settings: {
149
- enableAuditLogs?: boolean;
150
- storeSystemEventsInAuditLogs?: boolean;
151
- planName?: PlanType;
152
- }): Project {
153
- const item: Project = new Project();
154
- item._id = PROJECT_ID.toString();
155
- if (settings.enableAuditLogs !== undefined) {
156
- item.enableAuditLogs = settings.enableAuditLogs;
157
- }
158
- if (settings.storeSystemEventsInAuditLogs !== undefined) {
159
- item.storeSystemEventsInAuditLogs = settings.storeSystemEventsInAuditLogs;
160
- }
161
- item.auditLogsRetentionInDays = 30;
162
- if (settings.planName) {
163
- item.planName = settings.planName;
164
- }
165
- return item;
166
- }
167
-
168
- function makeUser(id: ObjectID, name: string | null, email: string): User {
169
- const user: User = new User();
170
- user._id = id.toString();
171
- if (name) {
172
- user.name = new Name(name);
173
- }
174
- user.email = new Email(email);
175
- return user;
176
- }
177
-
178
- function createHarness(): Harness {
179
- const service: AuditLogService = new AuditLogService();
180
- const inserted: Array<AuditLog> = [];
181
-
182
- const insert: jest.SpyInstance = jest
183
- .spyOn(service, "create")
184
- .mockImplementation(((createBy: { data: AuditLog }) => {
185
- inserted.push(createBy.data);
186
- return Promise.resolve(createBy.data);
187
- }) as never);
76
+ type CreateData = {
77
+ model: Monitor;
78
+ createdItem: Monitor;
79
+ props: DatabaseCommonInteractionProps;
80
+ };
188
81
 
189
- const findProject: jest.Mock = findProjectMock;
190
- findProject.mockReset();
191
- findProject.mockImplementation(() => {
192
- return Promise.resolve(project);
193
- });
82
+ type UpdateData = {
83
+ model: Monitor;
84
+ before: Monitor;
85
+ updatedFields: { name: string };
86
+ itemId: ObjectID;
87
+ props: DatabaseCommonInteractionProps;
88
+ };
194
89
 
195
- const findUser: jest.Mock = findUserMock;
196
- findUser.mockReset();
197
- findUser.mockImplementation(((findOneById: { id: ObjectID }) => {
198
- return Promise.resolve(usersById.get(findOneById.id.toString()) || null);
199
- }) as never);
200
-
201
- const relatedLookups: Array<RelatedLookup> = [];
202
-
203
- jest.spyOn(DatabaseService.prototype, "findBy").mockImplementation(function (
204
- this: DatabaseService<BaseModel>,
205
- findBy: FindBy<BaseModel>,
206
- ): Promise<Array<BaseModel>> {
207
- const tableName: string = this.getModel().tableName!;
208
-
209
- relatedLookups.push({
210
- tableName,
211
- query: findBy.query as unknown as JSONObject,
212
- select: findBy.select as unknown as JSONObject,
213
- props: findBy.props as unknown as JSONObject,
214
- });
90
+ type DeleteData = {
91
+ model: Monitor;
92
+ deletedItem: Monitor;
93
+ itemId: ObjectID;
94
+ props: DatabaseCommonInteractionProps;
95
+ };
215
96
 
216
- if (failRelatedLookups) {
217
- return Promise.reject(new Error("database unavailable"));
218
- }
97
+ const makeMonitor: () => Monitor = (): Monitor => {
98
+ const monitor: Monitor = new Monitor();
99
+ monitor._id = MONITOR_ID.toString();
100
+ monitor.projectId = PROJECT_ID;
101
+ monitor.name = "Checkout API";
102
+ return monitor;
103
+ };
219
104
 
220
- return Promise.resolve(relatedRows.get(tableName) || []);
221
- } as never);
105
+ const createData: () => CreateData = (): CreateData => {
106
+ return { model: new Monitor(), createdItem: makeMonitor(), props: PROPS };
107
+ };
222
108
 
223
- return { service, inserted, findProject, findUser, insert, relatedLookups };
224
- }
109
+ const updateData: () => UpdateData = (): UpdateData => {
110
+ return {
111
+ model: new Monitor(),
112
+ before: makeMonitor(),
113
+ updatedFields: { name: "Checkout API (primary)" },
114
+ itemId: MONITOR_ID,
115
+ props: PROPS,
116
+ };
117
+ };
225
118
 
226
- // The ids a tenant-scoped lookup asked for, read back out of QueryHelper.any.
227
- function idsInLookup(lookup: RelatedLookup): Array<string> {
228
- const operator: { objectLiteralParameters?: Record<string, unknown> } = lookup
229
- .query["_id"] as unknown as {
230
- objectLiteralParameters?: Record<string, unknown>;
119
+ const deleteData: () => DeleteData = (): DeleteData => {
120
+ return {
121
+ model: new Monitor(),
122
+ deletedItem: makeMonitor(),
123
+ itemId: MONITOR_ID,
124
+ props: PROPS,
231
125
  };
126
+ };
232
127
 
233
- return Object.values(operator.objectLiteralParameters || {})
234
- .flat()
235
- .map((id: unknown) => {
236
- return String(id);
237
- })
238
- .sort();
239
- }
240
-
241
- function label(id: string, name?: string): Label {
242
- const item: Label = new Label();
243
- item._id = id;
244
- if (name) {
245
- item.name = name;
246
- }
247
- return item;
248
- }
249
-
250
- function makeSlo(): ServiceLevelObjective {
251
- const slo: ServiceLevelObjective = new ServiceLevelObjective();
252
- slo._id = SLO_ID.toString();
253
- slo.projectId = PROJECT_ID;
254
- slo.name = "Checkout availability";
255
- slo.targetPercentage = 99.9;
256
- return slo;
257
- }
258
-
259
- function makeBurnRateRule(): ServiceLevelObjectiveBurnRateRule {
260
- const rule: ServiceLevelObjectiveBurnRateRule =
261
- new ServiceLevelObjectiveBurnRateRule();
262
- rule._id = RULE_ID.toString();
263
- rule.projectId = PROJECT_ID;
264
- rule.serviceLevelObjectiveId = SLO_ID;
265
- rule.name = "Fast burn";
266
- rule.burnRateThreshold = 14.4;
267
- return rule;
268
- }
269
-
270
- function makeMonitorRule(): ServiceLevelObjectiveMonitorRule {
271
- const rule: ServiceLevelObjectiveMonitorRule =
272
- new ServiceLevelObjectiveMonitorRule();
273
- rule._id = RULE_ID.toString();
274
- rule.projectId = PROJECT_ID;
275
- rule.serviceLevelObjectiveId = SLO_ID;
276
- rule.name = "Production APIs";
277
- return rule;
278
- }
279
-
280
- function makeOwnerUser(): ServiceLevelObjectiveOwnerUser {
281
- const owner: ServiceLevelObjectiveOwnerUser =
282
- new ServiceLevelObjectiveOwnerUser();
283
- owner._id = OWNER_ROW_ID.toString();
284
- owner.projectId = PROJECT_ID;
285
- owner.serviceLevelObjectiveId = SLO_ID;
286
- owner.userId = OWNER_USER_ID;
287
- owner.isOwnerNotified = false;
288
- return owner;
289
- }
290
-
291
- function makeOwnerTeam(): ServiceLevelObjectiveOwnerTeam {
292
- const owner: ServiceLevelObjectiveOwnerTeam =
293
- new ServiceLevelObjectiveOwnerTeam();
294
- owner._id = OWNER_ROW_ID.toString();
295
- owner.projectId = PROJECT_ID;
296
- owner.serviceLevelObjectiveId = SLO_ID;
297
- owner.teamId = TEAM_ID;
298
- owner.isOwnerNotified = false;
299
- return owner;
300
- }
301
-
302
- function onlyEntry(): AuditLog {
303
- expect(harness.inserted).toHaveLength(1);
304
- return harness.inserted[0]!;
305
- }
306
-
307
- function changesOf(entry: AuditLog): Array<JSONObject> {
308
- return (entry.changes || []) as Array<JSONObject>;
309
- }
310
-
311
- function fieldsOf(entry: AuditLog): Array<string> {
312
- return changesOf(entry).map((change: JSONObject) => {
313
- return String(change["field"]);
314
- });
315
- }
128
+ // The four entry points, each driven the way its caller drives it.
129
+ type EntryPoint = {
130
+ name: "recordCreate" | "recordUpdate" | "recordDelete";
131
+ call: (service: AuditLogService) => Promise<void>;
132
+ };
316
133
 
317
- function changeFor(entry: AuditLog, field: string): JSONObject | undefined {
318
- return changesOf(entry).find((change: JSONObject) => {
319
- return change["field"] === field;
320
- });
321
- }
134
+ const RECORD_ENTRY_POINTS: Array<EntryPoint> = [
135
+ {
136
+ name: "recordCreate",
137
+ call: (service: AuditLogService): Promise<void> => {
138
+ return service.recordCreate(createData());
139
+ },
140
+ },
141
+ {
142
+ name: "recordUpdate",
143
+ call: (service: AuditLogService): Promise<void> => {
144
+ return service.recordUpdate(updateData());
145
+ },
146
+ },
147
+ {
148
+ name: "recordDelete",
149
+ call: (service: AuditLogService): Promise<void> => {
150
+ return service.recordDelete(deleteData());
151
+ },
152
+ },
153
+ ];
154
+
155
+ let insertSpy: jest.SpyInstance;
156
+ let warnSpy: jest.SpyInstance;
322
157
 
323
158
  beforeEach(() => {
324
- environment.IsEnterpriseEdition = true;
325
- environment.IsBillingEnabled = false;
326
- project = makeProject({ enableAuditLogs: true });
327
- usersById = new Map<string, User>([
328
- [USER_ID.toString(), makeUser(USER_ID, "Ada Lovelace", "ada@example.com")],
329
- ]);
330
- relatedRows = new Map<string, Array<BaseModel>>();
331
- failRelatedLookups = false;
332
- harness = createHarness();
159
+ setTestBillingEnabled(false);
160
+ uninstallEnterpriseModule();
161
+
162
+ // Nothing here may ever reach ClickHouse: a write would be a bug.
163
+ insertSpy = jest
164
+ .spyOn(AuditLogServiceInstance, "create")
165
+ .mockImplementation(((createBy: { data: AuditLog }) => {
166
+ return Promise.resolve(createBy.data);
167
+ }) as never);
168
+
169
+ warnSpy = jest.spyOn(logger, "warn").mockImplementation((): void => {
170
+ return undefined;
171
+ });
333
172
  });
334
173
 
335
174
  afterEach(() => {
175
+ uninstallEnterpriseModule();
176
+ setTestBillingEnabled(false);
336
177
  jest.restoreAllMocks();
337
178
  });
338
179
 
339
- describe("which changes are recorded", () => {
340
- test("a person's change on an eligible project is recorded with its actor and retention", async () => {
341
- await harness.service.recordCreate({
342
- model: new ServiceLevelObjective(),
343
- createdItem: makeSlo(),
344
- props: USER_PROPS,
345
- });
180
+ describe("on the Community Edition (no enterprise module)", () => {
181
+ test.each([false, true])(
182
+ "with billing=%s every record call resolves and writes nothing",
183
+ async (billing: boolean) => {
184
+ setTestBillingEnabled(billing);
346
185
 
347
- const entry: AuditLog = onlyEntry();
348
-
349
- expect(entry.projectId?.toString()).toBe(PROJECT_ID.toString());
350
- expect(entry.action).toBe(AuditLogAction.Create);
351
- expect(entry.resourceType).toBe(SLO_RESOURCE_TYPE);
352
- expect(entry.resourceId?.toString()).toBe(SLO_ID.toString());
353
- expect(entry.resourceName).toBe("Checkout availability");
354
- expect(entry.userId?.toString()).toBe(USER_ID.toString());
355
- expect(entry.userName).toBe("Ada Lovelace");
356
- expect(entry.userEmail).toBe("ada@example.com");
357
- expect(entry.userType).toBe(UserType.User);
358
- expect(entry.retentionDate).toBeInstanceOf(Date);
359
- expect(changeFor(entry, "targetPercentage")).toEqual({
360
- field: "targetPercentage",
361
- newValue: 99.9,
362
- });
363
- });
186
+ expect(EnterpriseEdition.isLoaded()).toBe(false);
364
187
 
365
- test("nothing is recorded while the project has audit logging off", async () => {
366
- project = makeProject({ enableAuditLogs: false });
188
+ for (const entryPoint of RECORD_ENTRY_POINTS) {
189
+ await expect(
190
+ entryPoint.call(AuditLogServiceInstance),
191
+ ).resolves.toBeUndefined();
192
+ }
367
193
 
368
- await harness.service.recordCreate({
369
- model: new ServiceLevelObjective(),
370
- createdItem: makeSlo(),
371
- props: USER_PROPS,
372
- });
194
+ expect(insertSpy).not.toHaveBeenCalled();
195
+ expect(warnSpy).not.toHaveBeenCalled();
196
+ },
197
+ );
373
198
 
374
- expect(harness.inserted).toHaveLength(0);
199
+ test("invalidating a project's settings is a harmless no-op", () => {
200
+ expect(() => {
201
+ AuditLogServiceInstance.invalidateProjectSettings(PROJECT_ID);
202
+ }).not.toThrow();
375
203
  });
376
204
 
377
- test("nothing is recorded for a project that no longer exists", async () => {
378
- harness.findProject.mockImplementation(() => {
379
- return Promise.resolve(null);
380
- });
205
+ test("a fresh instance behaves the same as the shared one", async () => {
206
+ const service: AuditLogService = new AuditLogService();
207
+ const freshInsert: jest.SpyInstance = jest
208
+ .spyOn(service, "create")
209
+ .mockImplementation(((createBy: { data: AuditLog }) => {
210
+ return Promise.resolve(createBy.data);
211
+ }) as never);
381
212
 
382
- await harness.service.recordCreate({
383
- model: new ServiceLevelObjective(),
384
- createdItem: makeSlo(),
385
- props: USER_PROPS,
386
- });
213
+ for (const entryPoint of RECORD_ENTRY_POINTS) {
214
+ await entryPoint.call(service);
215
+ }
387
216
 
388
- expect(harness.inserted).toHaveLength(0);
217
+ expect(freshInsert).not.toHaveBeenCalled();
389
218
  });
219
+ });
390
220
 
391
- test("system events are dropped unless the project stores them", async () => {
392
- await harness.service.recordCreate({
393
- model: new ServiceLevelObjective(),
394
- createdItem: makeSlo(),
395
- props: SYSTEM_PROPS,
396
- });
397
-
398
- expect(harness.inserted).toHaveLength(0);
399
-
400
- project = makeProject({
401
- enableAuditLogs: true,
402
- storeSystemEventsInAuditLogs: true,
403
- });
404
- harness.service.invalidateProjectSettings(PROJECT_ID);
405
-
406
- await harness.service.recordCreate({
407
- model: new ServiceLevelObjective(),
408
- createdItem: makeSlo(),
409
- props: SYSTEM_PROPS,
410
- });
221
+ describe("with the Enterprise Edition loaded", () => {
222
+ let recorder: MockedAuditLogRecorder;
411
223
 
412
- const entry: AuditLog = onlyEntry();
413
- expect(entry.userType).toBe("System");
414
- expect(entry.userId).toBeUndefined();
224
+ beforeEach(() => {
225
+ recorder = createAuditLogRecorderSpy();
226
+ installFakeEnterpriseModule({ auditLogRecorder: recorder });
415
227
  });
416
228
 
417
- test.each([
418
- {
419
- build: "the Enterprise Edition",
420
- isEnterpriseEdition: true,
421
- isBillingEnabled: false,
422
- planName: undefined,
423
- recorded: true,
424
- },
425
- {
426
- build: "cloud on the Enterprise plan",
427
- isEnterpriseEdition: false,
428
- isBillingEnabled: true,
429
- planName: PlanType.Enterprise,
430
- recorded: true,
431
- },
432
- {
433
- build: "cloud on the Growth plan",
434
- isEnterpriseEdition: false,
435
- isBillingEnabled: true,
436
- planName: PlanType.Growth,
437
- recorded: false,
438
- },
439
- {
440
- build: "the free self-hosted build",
441
- isEnterpriseEdition: false,
442
- isBillingEnabled: false,
443
- planName: PlanType.Enterprise,
444
- recorded: false,
445
- },
446
- ])(
447
- "on $build an entry is recorded: $recorded",
448
- async (data: {
449
- isEnterpriseEdition: boolean;
450
- isBillingEnabled: boolean;
451
- planName: PlanType | undefined;
452
- recorded: boolean;
453
- }) => {
454
- environment.IsEnterpriseEdition = data.isEnterpriseEdition;
455
- environment.IsBillingEnabled = data.isBillingEnabled;
456
- project = makeProject({
457
- enableAuditLogs: true,
458
- ...(data.planName ? { planName: data.planName } : {}),
459
- });
229
+ test.each([false, true])(
230
+ "with billing=%s recordCreate hands the recorder the very arguments it was given",
231
+ async (billing: boolean) => {
232
+ setTestBillingEnabled(billing);
233
+ const data: CreateData = createData();
460
234
 
461
- await harness.service.recordCreate({
462
- model: new ServiceLevelObjective(),
463
- createdItem: makeSlo(),
464
- props: USER_PROPS,
465
- });
235
+ await AuditLogServiceInstance.recordCreate(data);
466
236
 
467
- expect(harness.inserted).toHaveLength(data.recorded ? 1 : 0);
237
+ expect(recorder.recordCreate).toHaveBeenCalledTimes(1);
238
+ expect(recorder.recordCreate.mock.calls[0]![0]).toBe(data);
239
+ expect(recorder.recordUpdate).not.toHaveBeenCalled();
240
+ expect(recorder.recordDelete).not.toHaveBeenCalled();
468
241
  },
469
242
  );
470
243
 
471
- test("a change with no project to file it under records nothing and reads nothing", async () => {
472
- const slo: ServiceLevelObjective = makeSlo();
473
- (slo as unknown as Record<string, unknown>)["projectId"] = undefined;
474
-
475
- await harness.service.recordCreate({
476
- model: new ServiceLevelObjective(),
477
- createdItem: slo,
478
- props: { userId: USER_ID, userType: UserType.User },
479
- });
480
-
481
- expect(harness.inserted).toHaveLength(0);
482
- expect(harness.findProject).not.toHaveBeenCalled();
483
- });
484
-
485
- test("the project's settings are read once per cache period, and again after invalidation", async () => {
486
- for (let i: number = 0; i < 3; i++) {
487
- await harness.service.recordCreate({
488
- model: new ServiceLevelObjective(),
489
- createdItem: makeSlo(),
490
- props: USER_PROPS,
491
- });
492
- }
493
-
494
- expect(harness.findProject).toHaveBeenCalledTimes(1);
495
-
496
- harness.service.invalidateProjectSettings(PROJECT_ID);
244
+ test("recordUpdate hands the recorder the very arguments it was given", async () => {
245
+ const data: UpdateData = updateData();
497
246
 
498
- await harness.service.recordCreate({
499
- model: new ServiceLevelObjective(),
500
- createdItem: makeSlo(),
501
- props: USER_PROPS,
502
- });
247
+ await AuditLogServiceInstance.recordUpdate(data);
503
248
 
504
- expect(harness.findProject).toHaveBeenCalledTimes(2);
249
+ expect(recorder.recordUpdate).toHaveBeenCalledTimes(1);
250
+ expect(recorder.recordUpdate.mock.calls[0]![0]).toBe(data);
251
+ expect(recorder.recordCreate).not.toHaveBeenCalled();
505
252
  });
506
253
 
507
- test("a failed insert never fails the write that triggered it", async () => {
508
- harness.insert.mockImplementation((() => {
509
- return Promise.reject(new Error("clickhouse unavailable"));
510
- }) as never);
254
+ test("recordDelete hands the recorder the very arguments it was given", async () => {
255
+ const data: DeleteData = deleteData();
511
256
 
512
- await expect(
513
- harness.service.recordCreate({
514
- model: new ServiceLevelObjective(),
515
- createdItem: makeSlo(),
516
- props: USER_PROPS,
517
- }),
518
- ).resolves.toBeUndefined();
519
- });
520
- });
257
+ await AuditLogServiceInstance.recordDelete(data);
521
258
 
522
- describe("resource identity", () => {
523
- test("an SLO's entries carry its singularName, the value its children's root pointer names", () => {
524
- expect(new ServiceLevelObjective().singularName).toBe(SLO_RESOURCE_TYPE);
525
- expect(
526
- new ServiceLevelObjectiveBurnRateRule().enableAuditLogOn?.rootResource
527
- ?.resourceType,
528
- ).toBe(SLO_RESOURCE_TYPE);
259
+ expect(recorder.recordDelete).toHaveBeenCalledTimes(1);
260
+ expect(recorder.recordDelete.mock.calls[0]![0]).toBe(data);
261
+ expect(recorder.recordCreate).not.toHaveBeenCalled();
529
262
  });
530
- });
531
263
 
532
- describe("update diffs", () => {
533
- test("an update that changed nothing records nothing and costs no settings read", async () => {
534
- await harness.service.recordUpdate({
535
- model: new ServiceLevelObjective(),
536
- before: makeSlo(),
537
- updatedFields: { targetPercentage: 99.9 },
538
- itemId: SLO_ID,
539
- props: USER_PROPS,
540
- });
264
+ test("invalidateProjectSettings reaches the recorder's cache", () => {
265
+ AuditLogServiceInstance.invalidateProjectSettings(PROJECT_ID);
541
266
 
542
- expect(harness.inserted).toHaveLength(0);
543
- expect(harness.findProject).not.toHaveBeenCalled();
267
+ expect(recorder.invalidateProjectSettings).toHaveBeenCalledTimes(1);
268
+ expect(recorder.invalidateProjectSettings).toHaveBeenCalledWith(PROJECT_ID);
544
269
  });
545
270
 
546
- test("an evaluation tick that rewrites only worker-owned columns records nothing, even with system events stored", async () => {
547
- project = makeProject({
548
- enableAuditLogs: true,
549
- storeSystemEventsInAuditLogs: true,
550
- });
551
-
552
- const before: ServiceLevelObjective = makeSlo();
553
- before.currentSliPercentage = 99.95;
554
- before.errorBudgetRemainingPercentage = 60;
555
- before.currentBurnRate = 0.4;
556
- before.lastEvaluatedAt = new Date("2026-09-15T10:00:00.000Z");
557
-
558
- await harness.service.recordUpdate({
559
- model: new ServiceLevelObjective(),
560
- before,
561
- updatedFields: {
562
- currentSliPercentage: 99.91,
563
- errorBudgetRemainingPercentage: 55,
564
- errorBudgetRemainingSeconds: 1200,
565
- errorBudgetTotalSeconds: 2592,
566
- currentBurnRate: 1.3,
567
- sloStatus: "AtRisk",
568
- statusChangeNotificationSentAt: new Date("2026-09-15T10:05:00.000Z"),
569
- lastEvaluatedAt: new Date("2026-09-15T10:05:00.000Z"),
570
- nextEvaluationAt: new Date("2026-09-15T10:10:00.000Z"),
571
- lastAccumulatedBucketEndAt: new Date("2026-09-15T10:05:00.000Z"),
572
- autoAddedMonitors: [{ _id: MONITOR_ID.toString() }],
573
- },
574
- itemId: SLO_ID,
575
- props: SYSTEM_PROPS,
576
- });
271
+ test("core itself never writes: an entry is the recorder's to write", async () => {
272
+ for (const entryPoint of RECORD_ENTRY_POINTS) {
273
+ await entryPoint.call(AuditLogServiceInstance);
274
+ }
577
275
 
578
- expect(harness.inserted).toHaveLength(0);
579
- expect(harness.findProject).not.toHaveBeenCalled();
276
+ expect(insertSpy).not.toHaveBeenCalled();
580
277
  });
581
278
 
582
- test("a person's edit that lands with worker columns records the edit alone", async () => {
583
- const before: ServiceLevelObjective = makeSlo();
584
- before.currentSliPercentage = 99.95;
585
-
586
- await harness.service.recordUpdate({
587
- model: new ServiceLevelObjective(),
588
- before,
589
- updatedFields: {
590
- targetPercentage: 99.5,
591
- currentSliPercentage: 99.1,
592
- lastEvaluatedAt: new Date("2026-09-15T10:05:00.000Z"),
593
- },
594
- itemId: SLO_ID,
595
- props: USER_PROPS,
596
- });
597
-
598
- expect(changesOf(onlyEntry())).toEqual([
599
- { field: "targetPercentage", oldValue: 99.9, newValue: 99.5 },
600
- ]);
601
- });
279
+ test("the call waits for the recorder, so the entry is written before the write returns", async () => {
280
+ let finished: boolean = false;
602
281
 
603
- test("a burn-rate rule's refire bookkeeping records nothing; a threshold edit does", async () => {
604
- await harness.service.recordUpdate({
605
- model: new ServiceLevelObjectiveBurnRateRule(),
606
- before: makeBurnRateRule(),
607
- updatedFields: {
608
- lastAlertCreatedAt: new Date("2026-09-15T10:05:00.000Z"),
609
- lastAlertResolvedAt: new Date("2026-09-15T10:06:00.000Z"),
610
- lastIncidentCreatedAt: new Date("2026-09-15T10:07:00.000Z"),
611
- lastIncidentResolvedAt: new Date("2026-09-15T10:08:00.000Z"),
612
- },
613
- itemId: RULE_ID,
614
- props: USER_PROPS,
282
+ recorder.recordCreate.mockImplementation(async (): Promise<void> => {
283
+ await Promise.resolve();
284
+ finished = true;
615
285
  });
616
286
 
617
- expect(harness.inserted).toHaveLength(0);
618
-
619
- await harness.service.recordUpdate({
620
- model: new ServiceLevelObjectiveBurnRateRule(),
621
- before: makeBurnRateRule(),
622
- updatedFields: { burnRateThreshold: 6 },
623
- itemId: RULE_ID,
624
- props: USER_PROPS,
625
- });
287
+ await AuditLogServiceInstance.recordCreate(createData());
626
288
 
627
- expect(fieldsOf(onlyEntry())).toEqual(["burnRateThreshold"]);
289
+ expect(finished).toBe(true);
628
290
  });
629
291
 
630
- test("the owner notification job marking an owner notified records nothing", async () => {
631
- project = makeProject({
632
- enableAuditLogs: true,
633
- storeSystemEventsInAuditLogs: true,
634
- });
635
-
636
- await harness.service.recordUpdate({
637
- model: new ServiceLevelObjectiveOwnerUser(),
638
- before: makeOwnerUser(),
639
- updatedFields: { isOwnerNotified: true },
640
- itemId: OWNER_ROW_ID,
641
- props: SYSTEM_PROPS,
642
- });
292
+ test.each(RECORD_ENTRY_POINTS)(
293
+ "$name never fails the audited write when the recorder rejects",
294
+ async (entryPoint: EntryPoint) => {
295
+ recorder[entryPoint.name].mockImplementation(async (): Promise<void> => {
296
+ throw new Error("clickhouse unavailable");
297
+ });
643
298
 
644
- expect(harness.inserted).toHaveLength(0);
645
- });
299
+ await expect(
300
+ entryPoint.call(AuditLogServiceInstance),
301
+ ).resolves.toBeUndefined();
302
+ expect(warnSpy).toHaveBeenCalled();
303
+ },
304
+ );
646
305
 
647
- test("create and delete snapshots leave ignored and bookkeeping columns out", async () => {
648
- const slo: ServiceLevelObjective = makeSlo();
649
- slo.slug = "checkout-availability";
650
- slo.currentSliPercentage = 99.2;
651
- slo.nextEvaluationAt = new Date("2026-09-15T10:10:00.000Z");
652
-
653
- await harness.service.recordDelete({
654
- model: new ServiceLevelObjective(),
655
- deletedItem: slo,
656
- itemId: SLO_ID,
657
- props: USER_PROPS,
306
+ test("invalidateProjectSettings never throws when the recorder does", () => {
307
+ recorder.invalidateProjectSettings.mockImplementation((): void => {
308
+ throw new Error("cache unavailable");
658
309
  });
659
310
 
660
- const entry: AuditLog = onlyEntry();
661
-
662
- expect(entry.action).toBe(AuditLogAction.Delete);
663
- expect(fieldsOf(entry)).toEqual(
664
- expect.arrayContaining(["name", "targetPercentage", "projectId"]),
665
- );
666
- expect(fieldsOf(entry)).not.toContain("currentSliPercentage");
667
- expect(fieldsOf(entry)).not.toContain("nextEvaluationAt");
668
- expect(fieldsOf(entry)).not.toContain("_id");
669
- expect(fieldsOf(entry)).not.toContain("slug");
670
- expect(changeFor(entry, "name")).toEqual({
671
- field: "name",
672
- oldValue: "Checkout availability",
673
- });
311
+ expect(() => {
312
+ AuditLogServiceInstance.invalidateProjectSettings(PROJECT_ID);
313
+ }).not.toThrow();
314
+ expect(warnSpy).toHaveBeenCalled();
674
315
  });
675
316
  });
676
317
 
677
- describe("every entry points at the resource it rolls up to", () => {
678
- test("an SLO's own entries point at the SLO", async () => {
679
- await harness.service.recordCreate({
680
- model: new ServiceLevelObjective(),
681
- createdItem: makeSlo(),
682
- props: USER_PROPS,
683
- });
318
+ describe("the recorder is looked up on every call", () => {
319
+ test("installing, then removing, the enterprise module takes effect at once", async () => {
320
+ await AuditLogServiceInstance.recordCreate(createData());
684
321
 
685
- const entry: AuditLog = onlyEntry();
686
- expect(entry.rootResourceType).toBe(SLO_RESOURCE_TYPE);
687
- expect(entry.rootResourceId?.toString()).toBe(SLO_ID.toString());
688
- });
322
+ const recorder: MockedAuditLogRecorder = createAuditLogRecorderSpy();
323
+ installFakeEnterpriseModule({ auditLogRecorder: recorder });
689
324
 
690
- test("a top-level resource with no configuration points at itself", async () => {
691
- const before: Monitor = new Monitor();
692
- before._id = MONITOR_ID.toString();
693
- before.projectId = PROJECT_ID;
694
- before.name = "Checkout API";
695
-
696
- await harness.service.recordUpdate({
697
- model: new Monitor(),
698
- before,
699
- updatedFields: { name: "Checkout API (primary)" },
700
- itemId: MONITOR_ID,
701
- props: USER_PROPS,
702
- });
325
+ await AuditLogServiceInstance.recordCreate(createData());
326
+ expect(recorder.recordCreate).toHaveBeenCalledTimes(1);
703
327
 
704
- const entry: AuditLog = onlyEntry();
705
- expect(entry.rootResourceType).toBe("Monitor");
706
- expect(entry.rootResourceId?.toString()).toBe(MONITOR_ID.toString());
707
- });
328
+ uninstallEnterpriseModule();
708
329
 
709
- test.each([
710
- {
711
- name: "burn-rate rule",
712
- model: (): BaseModel => {
713
- return new ServiceLevelObjectiveBurnRateRule();
714
- },
715
- item: (): BaseModel => {
716
- return makeBurnRateRule();
717
- },
718
- resourceType: "SLO Burn Rate Rule",
719
- resourceId: RULE_ID,
720
- },
721
- {
722
- name: "monitor rule",
723
- model: (): BaseModel => {
724
- return new ServiceLevelObjectiveMonitorRule();
725
- },
726
- item: (): BaseModel => {
727
- return makeMonitorRule();
728
- },
729
- resourceType: "SLO Monitor Rule",
730
- resourceId: RULE_ID,
731
- },
732
- {
733
- name: "owner user",
734
- model: (): BaseModel => {
735
- return new ServiceLevelObjectiveOwnerUser();
736
- },
737
- item: (): BaseModel => {
738
- return makeOwnerUser();
739
- },
740
- resourceType: "Service Level Objective User Owner",
741
- resourceId: OWNER_ROW_ID,
742
- },
743
- {
744
- name: "owner team",
745
- model: (): BaseModel => {
746
- return new ServiceLevelObjectiveOwnerTeam();
747
- },
748
- item: (): BaseModel => {
749
- return makeOwnerTeam();
750
- },
751
- resourceType: "Service Level Objective Team Owner",
752
- resourceId: OWNER_ROW_ID,
753
- },
754
- ])(
755
- "an SLO $name keeps its own identity and rolls up to its SLO, on create and delete",
756
- async (data: {
757
- model: () => BaseModel;
758
- item: () => BaseModel;
759
- resourceType: string;
760
- resourceId: ObjectID;
761
- }) => {
762
- await harness.service.recordCreate({
763
- model: data.model(),
764
- createdItem: data.item(),
765
- props: USER_PROPS,
766
- });
767
-
768
- await harness.service.recordDelete({
769
- model: data.model(),
770
- deletedItem: data.item(),
771
- itemId: data.resourceId,
772
- props: USER_PROPS,
773
- });
774
-
775
- expect(harness.inserted).toHaveLength(2);
776
-
777
- for (const entry of harness.inserted) {
778
- expect(entry.resourceType).toBe(data.resourceType);
779
- expect(entry.resourceId?.toString()).toBe(data.resourceId.toString());
780
- expect(entry.rootResourceType).toBe(SLO_RESOURCE_TYPE);
781
- expect(entry.rootResourceId?.toString()).toBe(SLO_ID.toString());
782
- }
783
- },
784
- );
785
-
786
- test("a child's update rolls up through the parent id on its before-row", async () => {
787
- await harness.service.recordUpdate({
788
- model: new ServiceLevelObjectiveBurnRateRule(),
789
- before: makeBurnRateRule(),
790
- updatedFields: { name: "Fast burn (1h)" },
791
- itemId: RULE_ID,
792
- props: USER_PROPS,
793
- });
794
-
795
- const entry: AuditLog = onlyEntry();
796
- expect(entry.resourceName).toBe("Fast burn");
797
- expect(entry.rootResourceType).toBe(SLO_RESOURCE_TYPE);
798
- expect(entry.rootResourceId?.toString()).toBe(SLO_ID.toString());
330
+ await AuditLogServiceInstance.recordCreate(createData());
331
+ expect(recorder.recordCreate).toHaveBeenCalledTimes(1);
332
+ expect(insertSpy).not.toHaveBeenCalled();
799
333
  });
800
334
 
801
- test("a child that cannot name its parent is still recorded, with no pointer", async () => {
802
- const rule: ServiceLevelObjectiveBurnRateRule = makeBurnRateRule();
803
- (rule as unknown as Record<string, unknown>)["serviceLevelObjectiveId"] =
804
- undefined;
335
+ test("an enterprise module that has no recorder records nothing", async () => {
336
+ installFakeEnterpriseModule({ auditLogRecorder: null });
805
337
 
806
- await harness.service.recordCreate({
807
- model: new ServiceLevelObjectiveBurnRateRule(),
808
- createdItem: rule,
809
- props: USER_PROPS,
810
- });
338
+ for (const entryPoint of RECORD_ENTRY_POINTS) {
339
+ await expect(
340
+ entryPoint.call(AuditLogServiceInstance),
341
+ ).resolves.toBeUndefined();
342
+ }
811
343
 
812
- const entry: AuditLog = onlyEntry();
813
- expect(entry.resourceType).toBe("SLO Burn Rate Rule");
814
- expect(entry.rootResourceType).toBeUndefined();
815
- expect(entry.rootResourceId).toBeUndefined();
344
+ expect(() => {
345
+ AuditLogServiceInstance.invalidateProjectSettings(PROJECT_ID);
346
+ }).not.toThrow();
347
+ expect(insertSpy).not.toHaveBeenCalled();
816
348
  });
817
- });
818
-
819
- describe("relation values are recorded as named references", () => {
820
- test("a label swap records old names from the before-row and new ones borrowed or looked up in the project", async () => {
821
- relatedRows.set("Label", [label(LABEL_C, "Staging")]);
822
-
823
- const before: ServiceLevelObjective = makeSlo();
824
- before.labels = [label(LABEL_A, "Production"), label(LABEL_B, "Payments")];
825
-
826
- await harness.service.recordUpdate({
827
- model: new ServiceLevelObjective(),
828
- before,
829
- updatedFields: {
830
- labels: [label(LABEL_C), label(LABEL_A)],
831
- } as unknown as JSONObject,
832
- itemId: SLO_ID,
833
- props: USER_PROPS,
834
- });
835
-
836
- expect(changeFor(onlyEntry(), "labels")).toEqual({
837
- field: "labels",
838
- oldValue: [
839
- { _id: LABEL_A, name: "Production" },
840
- { _id: LABEL_B, name: "Payments" },
841
- ],
842
- newValue: [
843
- { _id: LABEL_C, name: "Staging" },
844
- { _id: LABEL_A, name: "Production" },
845
- ],
846
- });
847
349
 
848
- // One lookup, for the only id nothing else could name - inside the project.
849
- expect(harness.relatedLookups).toHaveLength(1);
850
- const lookup: RelatedLookup = harness.relatedLookups[0]!;
851
- expect(lookup.tableName).toBe("Label");
852
- expect(idsInLookup(lookup)).toEqual([LABEL_C]);
853
- expect(lookup.query["projectId"]).toEqual(PROJECT_ID);
854
- expect(lookup.select).toEqual({ _id: true, name: true });
855
- expect(lookup.props).toEqual({ isRoot: true, ignoreHooks: true });
856
- });
350
+ test("an enterprise module that fails to hand over its recorder records nothing, and the write still succeeds", async () => {
351
+ const fake: FakeEnterpriseModule = installFakeEnterpriseModule();
857
352
 
858
- test("a label the project does not own is recorded by id only", async () => {
859
- // The tenant-scoped lookup finds nothing for another project's label.
860
- await harness.service.recordUpdate({
861
- model: new ServiceLevelObjective(),
862
- before: Object.assign(makeSlo(), { labels: [] }),
863
- updatedFields: { labels: [LABEL_C] } as unknown as JSONObject,
864
- itemId: SLO_ID,
865
- props: USER_PROPS,
353
+ jest.spyOn(fake, "getAuditLogRecorder").mockImplementation(() => {
354
+ throw new Error("recorder unavailable");
866
355
  });
867
-
868
- expect(changeFor(onlyEntry(), "labels")).toEqual({
869
- field: "labels",
870
- oldValue: [],
871
- newValue: [{ _id: LABEL_C }],
356
+ jest.spyOn(logger, "error").mockImplementation((): void => {
357
+ return undefined;
872
358
  });
873
- });
874
359
 
875
- test("the same labels in another order and shape are not a change", async () => {
876
- const before: ServiceLevelObjective = makeSlo();
877
- before.labels = [label(LABEL_A, "Production"), label(LABEL_B, "Payments")];
878
-
879
- await harness.service.recordUpdate({
880
- model: new ServiceLevelObjective(),
881
- before,
882
- updatedFields: {
883
- labels: [{ _id: LABEL_B }, LABEL_A],
884
- } as unknown as JSONObject,
885
- itemId: SLO_ID,
886
- props: USER_PROPS,
887
- });
360
+ for (const entryPoint of RECORD_ENTRY_POINTS) {
361
+ await expect(
362
+ entryPoint.call(AuditLogServiceInstance),
363
+ ).resolves.toBeUndefined();
364
+ }
888
365
 
889
- expect(harness.inserted).toHaveLength(0);
366
+ expect(insertSpy).not.toHaveBeenCalled();
890
367
  });
368
+ });
891
369
 
892
- test("a failed name lookup still records the entry, with ids", async () => {
893
- failRelatedLookups = true;
894
-
895
- const before: ServiceLevelObjective = makeSlo();
896
- before.labels = [label(LABEL_A, "Production")];
897
-
898
- await harness.service.recordUpdate({
899
- model: new ServiceLevelObjective(),
900
- before,
901
- updatedFields: {
902
- labels: [label(LABEL_C), label(LABEL_A)],
903
- } as unknown as JSONObject,
904
- itemId: SLO_ID,
905
- props: USER_PROPS,
906
- });
907
-
908
- expect(changeFor(onlyEntry(), "labels")?.["newValue"]).toEqual([
909
- { _id: LABEL_C },
910
- { _id: LABEL_A, name: "Production" },
911
- ]);
370
+ describe("what core's AuditLogService still is", () => {
371
+ test("the audit log's analytics service, so reads, retention and migrations stay core", () => {
372
+ expect(AuditLogServiceInstance).toBeInstanceOf(AuditLogService);
373
+ expect(typeof AuditLogServiceInstance.create).toBe("function");
374
+ expect(typeof AuditLogServiceInstance.findBy).toBe("function");
912
375
  });
913
376
 
914
- test("users are named by their name, else their email, without a generic lookup", async () => {
915
- usersById.set(
916
- OWNER_USER_ID.toString(),
917
- makeUser(OWNER_USER_ID, null, "grace@example.com"),
377
+ test("it carries none of the recording logic, which is Enterprise code", () => {
378
+ const methods: Array<string> = Object.getOwnPropertyNames(
379
+ AuditLogService.prototype,
918
380
  );
919
381
 
920
- const before: ServiceLevelObjectiveBurnRateRule = makeBurnRateRule();
921
- before.alertOwnerUsers = [];
922
-
923
- await harness.service.recordUpdate({
924
- model: new ServiceLevelObjectiveBurnRateRule(),
925
- before,
926
- updatedFields: {
927
- alertOwnerUsers: [
928
- OWNER_USER_ID.toString(),
929
- { _id: USER_ID.toString() },
930
- ],
931
- } as unknown as JSONObject,
932
- itemId: RULE_ID,
933
- props: USER_PROPS,
934
- });
382
+ for (const recordingHelper of [
383
+ "insert",
384
+ "isEligible",
385
+ "getProjectSettings",
386
+ "buildSnapshotChanges",
387
+ "buildUpdateDiff",
388
+ "getRedactedFields",
389
+ "addRelationNames",
390
+ "resolveActor",
391
+ ]) {
392
+ expect(methods).not.toContain(recordingHelper);
393
+ }
935
394
 
936
- expect(changeFor(onlyEntry(), "alertOwnerUsers")?.["newValue"]).toEqual([
937
- { _id: OWNER_USER_ID.toString(), name: "grace@example.com" },
938
- { _id: USER_ID.toString(), name: "Ada Lovelace" },
939
- ]);
940
- expect(harness.relatedLookups).toHaveLength(0);
395
+ for (const entryPoint of [
396
+ "recordCreate",
397
+ "recordUpdate",
398
+ "recordDelete",
399
+ "invalidateProjectSettings",
400
+ ]) {
401
+ expect(methods).toContain(entryPoint);
402
+ }
941
403
  });
404
+ });
942
405
 
943
- test("a many-to-one relation is recorded as a single named reference", async () => {
944
- const minor: AlertSeverity = new AlertSeverity();
945
- minor._id = SEVERITY_2;
946
- minor.name = "Minor";
947
- relatedRows.set("AlertSeverity", [minor]);
948
-
949
- const critical: AlertSeverity = new AlertSeverity();
950
- critical._id = SEVERITY_1;
951
- critical.name = "Critical";
952
-
953
- const before: ServiceLevelObjectiveBurnRateRule = makeBurnRateRule();
954
- before.alertSeverity = critical;
955
-
956
- const next: AlertSeverity = new AlertSeverity();
957
- next._id = SEVERITY_2;
958
-
959
- await harness.service.recordUpdate({
960
- model: new ServiceLevelObjectiveBurnRateRule(),
961
- before,
962
- updatedFields: { alertSeverity: next } as unknown as JSONObject,
963
- itemId: RULE_ID,
964
- props: USER_PROPS,
965
- });
406
+ /*
407
+ * The audit trail cannot be forged. Entries are written only by the
408
+ * Enterprise recorder, as root. The model grants create to nobody (a project
409
+ * owner or admin used to be able to POST /audit-log), and AuditLogService
410
+ * refuses every create that is not root, master admins' included.
411
+ */
412
+ const auditEntry: () => AuditLog = (): AuditLog => {
413
+ const entry: AuditLog = new AuditLog();
414
+ entry.projectId = PROJECT_ID;
415
+ entry.resourceType = "Monitor";
416
+ entry.resourceId = MONITOR_ID;
417
+ entry.action = "Delete";
418
+ entry.changes = [];
419
+ return entry;
420
+ };
966
421
 
967
- expect(changeFor(onlyEntry(), "alertSeverity")).toEqual({
968
- field: "alertSeverity",
969
- oldValue: { _id: SEVERITY_1, name: "Critical" },
970
- newValue: { _id: SEVERITY_2, name: "Minor" },
971
- });
972
- });
422
+ const projectMemberProps: (
423
+ permissions: Array<Permission>,
424
+ ) => DatabaseCommonInteractionProps = (
425
+ permissions: Array<Permission>,
426
+ ): DatabaseCommonInteractionProps => {
427
+ return {
428
+ userId: PROPS.userId,
429
+ userType: UserType.User,
430
+ tenantId: PROJECT_ID,
431
+ userTenantAccessPermission: {
432
+ [PROJECT_ID.toString()]: {
433
+ projectId: PROJECT_ID,
434
+ _type: "UserTenantAccessPermission",
435
+ permissions: permissions.map((permission: Permission) => {
436
+ return {
437
+ _type: "UserPermission",
438
+ permission: permission,
439
+ labelIds: [],
440
+ isBlockPermission: false,
441
+ };
442
+ }),
443
+ } as UserTenantAccessPermission,
444
+ },
445
+ };
446
+ };
973
447
 
974
- test("a create snapshot names its relations", async () => {
975
- relatedRows.set("Label", [label(LABEL_A, "Production")]);
448
+ const NON_ROOT_CALLERS: Array<[string, DatabaseCommonInteractionProps]> = [
449
+ ["a project owner", projectMemberProps([Permission.ProjectOwner])],
450
+ ["a project admin", projectMemberProps([Permission.ProjectAdmin])],
451
+ [
452
+ "a master admin",
453
+ {
454
+ userId: PROPS.userId,
455
+ userType: UserType.MasterAdmin,
456
+ isMasterAdmin: true,
457
+ },
458
+ ],
459
+ [
460
+ "an API key",
461
+ {
462
+ userType: UserType.API,
463
+ tenantId: PROJECT_ID,
464
+ userTenantAccessPermission: projectMemberProps([Permission.ProjectOwner])
465
+ .userTenantAccessPermission!,
466
+ },
467
+ ],
468
+ ["a caller with no identity", {}],
469
+ [
470
+ "a project admin skipping hooks",
471
+ {
472
+ ...projectMemberProps([Permission.ProjectAdmin]),
473
+ ignoreHooks: true,
474
+ },
475
+ ],
476
+ ];
477
+
478
+ describe("nobody but root creates audit log entries", () => {
479
+ let analyticsCreateMany: jest.SpyInstance;
480
+
481
+ beforeEach(() => {
482
+ // The real create path here; the ClickHouse insert underneath is stubbed.
483
+ insertSpy.mockRestore();
484
+ analyticsCreateMany = jest
485
+ .spyOn(AnalyticsDatabaseService.prototype, "createMany")
486
+ .mockImplementation((async (createBy: { items: Array<AuditLog> }) => {
487
+ return createBy.items;
488
+ }) as never);
489
+ });
490
+
491
+ test.each(NON_ROOT_CALLERS)(
492
+ "%s cannot create one",
493
+ async (_caller: string, props: DatabaseCommonInteractionProps) => {
494
+ await expect(
495
+ AuditLogServiceInstance.create({ data: auditEntry(), props }),
496
+ ).rejects.toThrow(
497
+ new NotAuthorizedException(AUDIT_LOG_CREATE_REFUSED_MESSAGE),
498
+ );
499
+ await expect(
500
+ AuditLogServiceInstance.createMany({ items: [auditEntry()], props }),
501
+ ).rejects.toThrow(NotAuthorizedException);
502
+
503
+ expect(analyticsCreateMany).not.toHaveBeenCalled();
504
+ },
505
+ );
976
506
 
977
- const slo: ServiceLevelObjective = makeSlo();
978
- slo.labels = [label(LABEL_A)];
507
+ test("a root create - the recorder's - goes through", async () => {
508
+ const entry: AuditLog = auditEntry();
979
509
 
980
- await harness.service.recordCreate({
981
- model: new ServiceLevelObjective(),
982
- createdItem: slo,
983
- props: USER_PROPS,
984
- });
510
+ await expect(
511
+ AuditLogServiceInstance.create({ data: entry, props: { isRoot: true } }),
512
+ ).resolves.toBe(entry);
985
513
 
986
- expect(changeFor(onlyEntry(), "labels")).toEqual({
987
- field: "labels",
988
- newValue: [{ _id: LABEL_A, name: "Production" }],
514
+ expect(analyticsCreateMany).toHaveBeenCalledTimes(1);
515
+ expect(analyticsCreateMany.mock.calls[0]![0]).toEqual({
516
+ items: [entry],
517
+ props: { isRoot: true },
989
518
  });
990
519
  });
991
520
 
992
- test("name lookups are bounded, and the entry still records every reference", async () => {
993
- const manyIds: Array<string> = Array.from(
994
- { length: 150 },
995
- (_value: unknown, index: number) => {
996
- return `cccccccc-0000-4000-8000-${index.toString().padStart(12, "0")}`;
997
- },
998
- );
999
-
1000
- await harness.service.recordUpdate({
1001
- model: new ServiceLevelObjective(),
1002
- before: Object.assign(makeSlo(), { labels: [] }),
1003
- updatedFields: { labels: manyIds } as unknown as JSONObject,
1004
- itemId: SLO_ID,
1005
- props: USER_PROPS,
1006
- });
1007
-
1008
- const lookedUp: number = harness.relatedLookups.reduce(
1009
- (total: number, lookup: RelatedLookup) => {
1010
- return total + idsInLookup(lookup).length;
1011
- },
1012
- 0,
1013
- );
1014
-
1015
- expect(lookedUp).toBe(100);
1016
- expect(
1017
- (changeFor(onlyEntry(), "labels")?.["newValue"] as Array<JSONObject>)
1018
- .length,
1019
- ).toBe(150);
521
+ test("a root create that skips hooks goes through too", async () => {
522
+ await expect(
523
+ AuditLogServiceInstance.createMany({
524
+ items: [auditEntry(), auditEntry()],
525
+ props: { isRoot: true, ignoreHooks: true },
526
+ }),
527
+ ).resolves.toHaveLength(2);
1020
528
  });
1021
529
 
1022
- test("a JSON column holding an _id is recorded as plain JSON, not as a reference", async () => {
1023
- const before: ServiceLevelObjective = makeSlo();
1024
- before.metricQueryConfig = { _id: "query-1", query: "a" };
1025
-
1026
- await harness.service.recordUpdate({
1027
- model: new ServiceLevelObjective(),
1028
- before,
1029
- updatedFields: {
1030
- metricQueryConfig: { _id: "query-1", query: "b", name: "Errors" },
1031
- },
1032
- itemId: SLO_ID,
1033
- props: USER_PROPS,
1034
- });
1035
-
1036
- expect(changeFor(onlyEntry(), "metricQueryConfig")).toEqual({
1037
- field: "metricQueryConfig",
1038
- oldValue: { _id: "query-1", query: "a" },
1039
- newValue: { _id: "query-1", query: "b", name: "Errors" },
1040
- });
1041
- expect(harness.relatedLookups).toHaveLength(0);
530
+ test("a fresh instance refuses the same way", async () => {
531
+ await expect(
532
+ new AuditLogService().create({
533
+ data: auditEntry(),
534
+ props: projectMemberProps([Permission.ProjectAdmin]),
535
+ }),
536
+ ).rejects.toThrow(NotAuthorizedException);
1042
537
  });
1043
538
  });
1044
539
 
1045
- describe("rows with no name of their own are named after what they point at", () => {
1046
- test("an owner user row is named after the user", async () => {
1047
- usersById.set(
1048
- OWNER_USER_ID.toString(),
1049
- makeUser(OWNER_USER_ID, "Grace Hopper", "grace@example.com"),
1050
- );
1051
-
1052
- await harness.service.recordCreate({
1053
- model: new ServiceLevelObjectiveOwnerUser(),
1054
- createdItem: makeOwnerUser(),
1055
- props: USER_PROPS,
1056
- });
540
+ describe("the AuditLog model grants create to nobody", () => {
541
+ test("no create permission at table level", () => {
542
+ const model: AuditLog = new AuditLog();
1057
543
 
1058
- expect(onlyEntry().resourceName).toBe("Grace Hopper");
544
+ expect(model.getCreatePermissions()).toEqual([]);
545
+ expect(model.getUpdatePermissions()).toEqual([]);
546
+ expect(model.getDeletePermissions()).toEqual([]);
1059
547
  });
1060
548
 
1061
- test("a user with no name is named by email", async () => {
1062
- usersById.set(
1063
- OWNER_USER_ID.toString(),
1064
- makeUser(OWNER_USER_ID, null, "grace@example.com"),
1065
- );
1066
-
1067
- await harness.service.recordDelete({
1068
- model: new ServiceLevelObjectiveOwnerUser(),
1069
- deletedItem: makeOwnerUser(),
1070
- itemId: OWNER_ROW_ID,
1071
- props: USER_PROPS,
1072
- });
1073
-
1074
- expect(onlyEntry().resourceName).toBe("grace@example.com");
1075
- });
549
+ test("no create permission on any column", () => {
550
+ const columns: Array<AnalyticsTableColumn> = new AuditLog().tableColumns;
1076
551
 
1077
- test("an owner team row is named after the team, read inside the project", async () => {
1078
- const team: Team = new Team();
1079
- team._id = TEAM_ID.toString();
1080
- team.projectId = PROJECT_ID;
1081
- team.name = "Site Reliability";
1082
- relatedRows.set("Team", [team]);
1083
-
1084
- await harness.service.recordCreate({
1085
- model: new ServiceLevelObjectiveOwnerTeam(),
1086
- createdItem: makeOwnerTeam(),
1087
- props: USER_PROPS,
1088
- });
552
+ expect(columns.length).toBeGreaterThan(10);
1089
553
 
1090
- expect(onlyEntry().resourceName).toBe("Site Reliability");
554
+ const creatable: Array<string> = columns
555
+ .filter((column: AnalyticsTableColumn): boolean => {
556
+ return (column.accessControl?.create || []).length > 0;
557
+ })
558
+ .map((column: AnalyticsTableColumn): string => {
559
+ return column.key;
560
+ });
1091
561
 
1092
- const lookup: RelatedLookup = harness.relatedLookups[0]!;
1093
- expect(lookup.tableName).toBe("Team");
1094
- expect(idsInLookup(lookup)).toEqual([TEAM_ID.toString()]);
1095
- expect(lookup.query["projectId"]).toEqual(PROJECT_ID);
562
+ expect(creatable).toEqual([]);
1096
563
  });
1097
564
 
1098
- test("a failed team lookup still records the entry, unnamed", async () => {
1099
- failRelatedLookups = true;
1100
-
1101
- await harness.service.recordCreate({
1102
- model: new ServiceLevelObjectiveOwnerTeam(),
1103
- createdItem: makeOwnerTeam(),
1104
- props: USER_PROPS,
1105
- });
1106
-
1107
- const entry: AuditLog = onlyEntry();
1108
- expect(entry.resourceName).toBeUndefined();
1109
- expect(entry.rootResourceId?.toString()).toBe(SLO_ID.toString());
565
+ // Reading the trail is unchanged.
566
+ test("owners, admins, settings admins and audit readers can still read it", () => {
567
+ expect(new AuditLog().getReadPermissions()).toEqual([
568
+ Permission.ProjectOwner,
569
+ Permission.ProjectAdmin,
570
+ Permission.SettingsAdmin,
571
+ Permission.ReadAuditLog,
572
+ ]);
1110
573
  });
1111
574
 
1112
- test("a row with its own name never looks anywhere else", async () => {
1113
- await harness.service.recordCreate({
1114
- model: new ServiceLevelObjectiveBurnRateRule(),
1115
- createdItem: makeBurnRateRule(),
1116
- props: USER_PROPS,
1117
- });
1118
-
1119
- expect(onlyEntry().resourceName).toBe("Fast burn");
1120
- expect(harness.relatedLookups).toHaveLength(0);
1121
- // Only the actor was looked up.
1122
- expect(harness.findUser).toHaveBeenCalledTimes(1);
1123
- });
575
+ /*
576
+ * The permission layer on its own turns a project owner or admin away,
577
+ * before AuditLogService is even reached.
578
+ */
579
+ test.each([
580
+ ["a project owner", Permission.ProjectOwner],
581
+ ["a project admin", Permission.ProjectAdmin],
582
+ ])(
583
+ "the analytics create check refuses %s",
584
+ (_caller: string, permission: Permission) => {
585
+ expect(() => {
586
+ ModelPermission.checkCreatePermissions(
587
+ AuditLog,
588
+ auditEntry(),
589
+ projectMemberProps([permission]),
590
+ );
591
+ }).toThrow(NotAuthorizedException);
592
+ },
593
+ );
1124
594
  });