@oneuptime/common 13.0.8 → 14.0.0

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 (333) hide show
  1. package/Models/AnalyticsModels/AuditLog.ts +23 -15
  2. package/Models/DatabaseModels/GlobalConfig.ts +24 -0
  3. package/Models/DatabaseModels/Monitor.ts +3 -2
  4. package/Models/DatabaseModels/MonitorTemplate.ts +2 -1
  5. package/Scripts/generate-service-worker.js +65 -4
  6. package/Server/API/BaseAPI.ts +26 -5
  7. package/Server/API/GlobalConfigAPI.ts +228 -462
  8. package/Server/API/ProjectOIDC.ts +25 -16
  9. package/Server/API/ProjectSSO.ts +24 -16
  10. package/Server/API/StatusPageAPI.ts +100 -42
  11. package/Server/API/TeamMemberAPI.ts +10 -1
  12. package/Server/Enterprise/EnterpriseEdition.ts +649 -0
  13. package/Server/Enterprise/EnterpriseFeature.ts +48 -0
  14. package/Server/Enterprise/EnterpriseLicenseSnapshot.ts +192 -0
  15. package/Server/Enterprise/EnterpriseServerModule.ts +233 -0
  16. package/Server/EnvironmentConfig.ts +147 -2
  17. package/Server/Infrastructure/Postgres/SchemaMigrations/1793900000000-AddEnterpriseEditionFirstSeenAt.ts +25 -0
  18. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +2 -0
  19. package/Server/Middleware/UserAuthorization.ts +38 -16
  20. package/Server/Services/AuditLogService.ts +84 -1079
  21. package/Server/Services/EnterpriseLicenseService.ts +38 -0
  22. package/Server/Services/GlobalConfigService.ts +80 -1
  23. package/Server/Services/MonitorProbeService.ts +116 -20
  24. package/Server/Services/MonitorService.ts +56 -4
  25. package/Server/Services/MonitorStatusTimelineService.ts +284 -0
  26. package/Server/Services/MonitorTemplateService.ts +26 -0
  27. package/Server/Services/ProjectService.ts +284 -0
  28. package/Server/Services/RumSessionPinService.ts +25 -6
  29. package/Server/Services/StatusPagePrivateUserService.ts +7 -3
  30. package/Server/Services/StatusPageService.ts +78 -1
  31. package/Server/Services/TeamMemberService.ts +11 -0
  32. package/Server/Services/TeamService.ts +10 -0
  33. package/Server/Services/UserService.ts +10 -17
  34. package/Server/Types/Database/Permissions/CreatePermission.ts +15 -0
  35. package/Server/Types/Database/Permissions/EditionPermission.ts +208 -18
  36. package/Server/Types/Database/Permissions/TablePermission.ts +10 -3
  37. package/Server/Types/Database/Permissions/UpdatePermission.ts +29 -0
  38. package/Server/Types/Markdown.ts +41 -0
  39. package/Server/Utils/CommunityEditionSsoReport.ts +361 -0
  40. package/Server/Utils/EditionEnforcement.ts +233 -0
  41. package/Server/Utils/FrontendEnvironment.ts +41 -2
  42. package/Server/Utils/InstanceHealth/RedisHealth.ts +12 -277
  43. package/Server/Utils/Monitor/AffectedResourceList.ts +190 -0
  44. package/Server/Utils/Monitor/Criteria/EvaluateOverTime.ts +10 -1
  45. package/Server/Utils/Monitor/MonitorCriteriaEvaluator.ts +543 -333
  46. package/Server/Utils/Monitor/MonitoringIntervalValidator.ts +116 -0
  47. package/Server/Utils/Workspace/Slack/Slack.ts +423 -10
  48. package/Tests/App/Accounts/LoginPasskey.test.tsx +18 -0
  49. package/Tests/App/Accounts/LoginSsoEdition.test.tsx +616 -0
  50. package/Tests/App/AdminDashboard/AdminAuthenticationSsoEdition.test.tsx +217 -0
  51. package/Tests/App/AdminDashboard/AdminEnterprisePluginPage.test.tsx +357 -0
  52. package/Tests/App/AdminDashboard/EnterpriseLicensesShell.test.tsx +214 -0
  53. package/Tests/App/AdminDashboard/GlobalSSOEnterpriseShells.test.tsx +271 -0
  54. package/Tests/App/AdminDashboard/HealthShells.test.tsx +659 -0
  55. package/Tests/App/Dashboard/AuditLogsAndComplianceShells.test.tsx +572 -0
  56. package/Tests/App/Dashboard/EnterpriseFeatureUpgrade.test.tsx +547 -0
  57. package/Tests/App/Dashboard/EnterprisePluginPage.test.tsx +667 -0
  58. package/Tests/App/Dashboard/IdentityEnterpriseShells.test.tsx +319 -0
  59. package/Tests/App/Dashboard/OnboardingSsoEmptyState.test.tsx +185 -0
  60. package/Tests/App/Dashboard/SecurityEventConnectionDiagnostics.test.tsx +45 -9
  61. package/Tests/App/Dashboard/SecurityEventDetailPanel.test.tsx +399 -0
  62. package/Tests/App/Dashboard/SecurityEventListRow.test.tsx +214 -0
  63. package/Tests/App/Dashboard/SecurityEventsEmptyState.test.tsx +1 -83
  64. package/Tests/App/Dashboard/SecurityEventsNavTabs.test.tsx +248 -0
  65. package/Tests/App/Dashboard/SecurityEventsViewer.test.tsx +1203 -0
  66. package/Tests/App/Dashboard/UserEmailCallSites.test.tsx +11 -91
  67. package/Tests/App/StatusPage/StatusPageSsoEmptyState.test.tsx +319 -0
  68. package/Tests/Helpers/RefreshAwareApiScan.ts +757 -0
  69. package/Tests/Helpers/TenantScopedRequestScan.ts +515 -0
  70. package/Tests/Scripts/GenerateServiceWorker.test.ts +931 -0
  71. package/Tests/Server/API/BaseAPICreateItemDoesNotMutateModel.test.ts +130 -0
  72. package/Tests/Server/API/GlobalConfigLicense.test.ts +742 -497
  73. package/Tests/Server/API/OnCallReadinessAPI.test.ts +35 -909
  74. package/Tests/Server/API/SsoProviderListingsEdition.test.ts +432 -0
  75. package/Tests/Server/API/TeamMemberLeaveScimLockEdition.test.ts +252 -0
  76. package/Tests/Server/Enterprise/EnterpriseEdition.test.ts +824 -0
  77. package/Tests/Server/Enterprise/EnterpriseFeatureActive.test.ts +643 -0
  78. package/Tests/Server/Enterprise/EnterpriseLicenseSnapshot.test.ts +294 -0
  79. package/Tests/Server/Enterprise/EnterpriseModelFeatureMap.test.ts +93 -0
  80. package/Tests/Server/Enterprise/EnterpriseOnlyModulesLiveInEe.test.ts +582 -0
  81. package/Tests/Server/Enterprise/EnterpriseServerModule.test.ts +247 -0
  82. package/Tests/Server/Enterprise/FakeEnterpriseModule.ts +502 -0
  83. package/Tests/Server/Enterprise/TestBillingFlag.ts +70 -0
  84. package/Tests/Server/EnvironmentConfigEnterpriseEdition.test.ts +314 -0
  85. package/Tests/Server/Infrastructure/ContainerBootConfiguration.test.ts +289 -0
  86. package/Tests/Server/Infrastructure/E2EClickHouseFixtureAccess.test.ts +5 -1
  87. package/Tests/Server/Middleware/UserAuthorization.test.ts +37 -2
  88. package/Tests/Server/Middleware/UserAuthorizationEditionEnforcement.test.ts +516 -0
  89. package/Tests/Server/Services/AuditLogService.test.ts +460 -990
  90. package/Tests/Server/Services/CreateAndReadResponsesKeepIds.test.ts +263 -0
  91. package/Tests/Server/Services/EnterpriseLicenseServiceKeyGeneration.test.ts +151 -0
  92. package/Tests/Server/Services/GlobalConfigServiceLicenseColumns.test.ts +357 -0
  93. package/Tests/Server/Services/MonitorDestinationInfoIPv6.test.ts +101 -0
  94. package/Tests/Server/Services/MonitorProbeServiceIntervalScheduling.test.ts +333 -0
  95. package/Tests/Server/Services/MonitorStatusTimelineDailyAggregates.test.ts +303 -0
  96. package/Tests/Server/Services/ProjectServiceAuditLogLicense.test.ts +701 -0
  97. package/Tests/Server/Services/ProjectServiceAuditLogSettingsCache.test.ts +31 -35
  98. package/Tests/Server/Services/ScimPushGroupsTeamLocksEdition.test.ts +431 -0
  99. package/Tests/Server/Services/SsoRequirementReadMasking.test.ts +419 -0
  100. package/Tests/Server/Services/SsoRequirementWriteGuard.test.ts +769 -0
  101. package/Tests/Server/Services/StatusPagePrivateUserInvitationPolicy.test.ts +94 -0
  102. package/Tests/Server/Services/UserAuthenticationService.test.ts +35 -0
  103. package/Tests/Server/Services/UserServiceSeatLimit.test.ts +94 -56
  104. package/Tests/Server/Types/Database/Permissions/EditionPermission.test.ts +1823 -0
  105. package/Tests/Server/Types/Markdown.test.ts +181 -0
  106. package/Tests/Server/Utils/CommunityEditionSsoReport.test.ts +565 -0
  107. package/Tests/Server/Utils/EditionEnforcement.test.ts +303 -0
  108. package/Tests/Server/Utils/FrontendEnvironment.test.ts +367 -0
  109. package/Tests/Server/Utils/InstanceHealth/RedisHealth.test.ts +38 -418
  110. package/Tests/Server/Utils/Monitor/AffectedResourceList.test.ts +518 -0
  111. package/Tests/Server/Utils/Monitor/Criteria/EvaluateOverTime.test.ts +24 -4
  112. package/Tests/Server/Utils/Monitor/MonitorCriteriaEvaluator.test.ts +934 -96
  113. package/Tests/Server/Utils/Monitor/MonitoringIntervalValidator.test.ts +188 -0
  114. package/Tests/Server/Utils/Workspace/Slack/SlackMarkdownSectionSplit.test.ts +1209 -0
  115. package/Tests/Types/API/HostnameFromAuthority.test.ts +20 -10
  116. package/Tests/Types/API/HostnameIPv6.test.ts +149 -0
  117. package/Tests/Types/Database/CanonicalModelInstance.test.ts +190 -0
  118. package/Tests/UI/Components/EditionLabelLapseCopy.ts +104 -0
  119. package/Tests/UI/Components/EditionLabelLicenseManagerWiring.test.tsx +254 -0
  120. package/Tests/UI/Components/EditionLabelLicenseRefresh.test.tsx +84 -178
  121. package/Tests/UI/Components/EditionLabelLicenseStatus.test.tsx +1586 -0
  122. package/Tests/UI/Components/EditionLabelLicenseWritesGuard.test.tsx +157 -0
  123. package/Tests/UI/Components/Graphs/DayUptimeGraph.test.tsx +49 -2
  124. package/Tests/UI/Components/MarkdownConverters.test.ts +684 -0
  125. package/Tests/UI/Components/ModelListEmptyState.test.tsx +289 -0
  126. package/Tests/UI/Components/TelemetryViewer/TelemetryViewerEmptyContent.test.tsx +131 -0
  127. package/Tests/UI/EsbuildEnterpriseAlias.test.ts +1127 -0
  128. package/Tests/Utils/HostAddressUtil.test.ts +138 -0
  129. package/Tests/Utils/Monitor/MonitorDestinationUtil.test.ts +313 -0
  130. package/Tests/Utils/Monitor/MonitoringIntervalUtil.test.ts +304 -0
  131. package/Tests/Utils/StatusPage/UptimeDailyAggregateUtil.test.ts +248 -0
  132. package/Types/API/Hostname.ts +63 -16
  133. package/Types/Database/AccessControl/ColumnAccessControl.ts +19 -7
  134. package/Types/Database/AccessControl/ColumnBillingAccessControl.ts +19 -7
  135. package/Types/Database/AccessControl/OwnerOnlyColumn.ts +15 -6
  136. package/Types/Database/CanonicalModelInstance.ts +102 -0
  137. package/Types/Database/TableColumn.ts +20 -6
  138. package/Types/EnterpriseLicense/EnterpriseLicensePeriods.ts +29 -0
  139. package/Types/IP/IPv6.ts +6 -1
  140. package/Types/Monitor/MonitorStep.ts +6 -1
  141. package/Types/Monitor/ProxmoxAlertTemplates.ts +1 -1
  142. package/Types/StatusPage/UptimeDailyAggregate.ts +110 -0
  143. package/UI/Components/EditionLabel/EditionLabel.tsx +796 -1195
  144. package/UI/Components/EditionLabel/LicenseManager.ts +119 -0
  145. package/UI/Components/Graphs/DayUptimeGraph.tsx +123 -15
  146. package/UI/Components/Markdown.tsx/MarkdownConverters.ts +0 -0
  147. package/UI/Components/ModelList/ModelList.tsx +11 -1
  148. package/UI/Components/MonitorGraphs/Uptime.tsx +69 -0
  149. package/UI/Components/TelemetryViewer/TelemetryViewer.tsx +28 -12
  150. package/UI/esbuild-enterprise.js +263 -0
  151. package/Utils/EnterpriseLicense/EnterpriseLicenseUsage.ts +7 -2
  152. package/Utils/HostAddressUtil.ts +94 -0
  153. package/Utils/Monitor/MonitorDestinationUtil.ts +157 -0
  154. package/Utils/Monitor/MonitoringIntervalUtil.ts +436 -0
  155. package/Utils/StatusPage/UptimeDailyAggregateUtil.ts +192 -0
  156. package/build/dist/Models/AnalyticsModels/AuditLog.js +23 -15
  157. package/build/dist/Models/AnalyticsModels/AuditLog.js.map +1 -1
  158. package/build/dist/Models/DatabaseModels/GlobalConfig.js +25 -0
  159. package/build/dist/Models/DatabaseModels/GlobalConfig.js.map +1 -1
  160. package/build/dist/Models/DatabaseModels/Monitor.js +2 -2
  161. package/build/dist/Models/DatabaseModels/Monitor.js.map +1 -1
  162. package/build/dist/Models/DatabaseModels/MonitorTemplate.js +2 -1
  163. package/build/dist/Models/DatabaseModels/MonitorTemplate.js.map +1 -1
  164. package/build/dist/Server/API/BaseAPI.js +26 -5
  165. package/build/dist/Server/API/BaseAPI.js.map +1 -1
  166. package/build/dist/Server/API/GlobalConfigAPI.js +148 -344
  167. package/build/dist/Server/API/GlobalConfigAPI.js.map +1 -1
  168. package/build/dist/Server/API/ProjectOIDC.js +24 -16
  169. package/build/dist/Server/API/ProjectOIDC.js.map +1 -1
  170. package/build/dist/Server/API/ProjectSSO.js +24 -16
  171. package/build/dist/Server/API/ProjectSSO.js.map +1 -1
  172. package/build/dist/Server/API/StatusPageAPI.js +93 -43
  173. package/build/dist/Server/API/StatusPageAPI.js.map +1 -1
  174. package/build/dist/Server/API/TeamMemberAPI.js +8 -1
  175. package/build/dist/Server/API/TeamMemberAPI.js.map +1 -1
  176. package/build/dist/Server/Enterprise/EnterpriseEdition.js +466 -0
  177. package/build/dist/Server/Enterprise/EnterpriseEdition.js.map +1 -0
  178. package/build/dist/Server/Enterprise/EnterpriseFeature.js +39 -0
  179. package/build/dist/Server/Enterprise/EnterpriseFeature.js.map +1 -0
  180. package/build/dist/Server/Enterprise/EnterpriseLicenseSnapshot.js +70 -0
  181. package/build/dist/Server/Enterprise/EnterpriseLicenseSnapshot.js.map +1 -0
  182. package/build/dist/Server/Enterprise/EnterpriseServerModule.js +84 -0
  183. package/build/dist/Server/Enterprise/EnterpriseServerModule.js.map +1 -0
  184. package/build/dist/Server/EnvironmentConfig.js +84 -2
  185. package/build/dist/Server/EnvironmentConfig.js.map +1 -1
  186. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1793900000000-AddEnterpriseEditionFirstSeenAt.js +18 -0
  187. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1793900000000-AddEnterpriseEditionFirstSeenAt.js.map +1 -0
  188. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +2 -0
  189. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  190. package/build/dist/Server/Middleware/UserAuthorization.js +30 -11
  191. package/build/dist/Server/Middleware/UserAuthorization.js.map +1 -1
  192. package/build/dist/Server/Services/AuditLogService.js +71 -740
  193. package/build/dist/Server/Services/AuditLogService.js.map +1 -1
  194. package/build/dist/Server/Services/EnterpriseLicenseService.js +34 -0
  195. package/build/dist/Server/Services/EnterpriseLicenseService.js.map +1 -1
  196. package/build/dist/Server/Services/GlobalConfigService.js +63 -0
  197. package/build/dist/Server/Services/GlobalConfigService.js.map +1 -1
  198. package/build/dist/Server/Services/MonitorProbeService.js +90 -17
  199. package/build/dist/Server/Services/MonitorProbeService.js.map +1 -1
  200. package/build/dist/Server/Services/MonitorService.js +45 -4
  201. package/build/dist/Server/Services/MonitorService.js.map +1 -1
  202. package/build/dist/Server/Services/MonitorStatusTimelineService.js +242 -0
  203. package/build/dist/Server/Services/MonitorStatusTimelineService.js.map +1 -1
  204. package/build/dist/Server/Services/MonitorTemplateService.js +16 -0
  205. package/build/dist/Server/Services/MonitorTemplateService.js.map +1 -1
  206. package/build/dist/Server/Services/ProjectService.js +214 -0
  207. package/build/dist/Server/Services/ProjectService.js.map +1 -1
  208. package/build/dist/Server/Services/RumSessionPinService.js +24 -6
  209. package/build/dist/Server/Services/RumSessionPinService.js.map +1 -1
  210. package/build/dist/Server/Services/StatusPagePrivateUserService.js +7 -3
  211. package/build/dist/Server/Services/StatusPagePrivateUserService.js.map +1 -1
  212. package/build/dist/Server/Services/StatusPageService.js +71 -0
  213. package/build/dist/Server/Services/StatusPageService.js.map +1 -1
  214. package/build/dist/Server/Services/TeamMemberService.js +10 -0
  215. package/build/dist/Server/Services/TeamMemberService.js.map +1 -1
  216. package/build/dist/Server/Services/TeamService.js +9 -0
  217. package/build/dist/Server/Services/TeamService.js.map +1 -1
  218. package/build/dist/Server/Services/UserService.js +10 -16
  219. package/build/dist/Server/Services/UserService.js.map +1 -1
  220. package/build/dist/Server/Types/Database/Permissions/CreatePermission.js +10 -0
  221. package/build/dist/Server/Types/Database/Permissions/CreatePermission.js.map +1 -1
  222. package/build/dist/Server/Types/Database/Permissions/EditionPermission.js +154 -14
  223. package/build/dist/Server/Types/Database/Permissions/EditionPermission.js.map +1 -1
  224. package/build/dist/Server/Types/Database/Permissions/TablePermission.js +10 -3
  225. package/build/dist/Server/Types/Database/Permissions/TablePermission.js.map +1 -1
  226. package/build/dist/Server/Types/Database/Permissions/UpdatePermission.js +17 -0
  227. package/build/dist/Server/Types/Database/Permissions/UpdatePermission.js.map +1 -1
  228. package/build/dist/Server/Types/Markdown.js +31 -0
  229. package/build/dist/Server/Types/Markdown.js.map +1 -1
  230. package/build/dist/Server/Utils/CommunityEditionSsoReport.js +223 -0
  231. package/build/dist/Server/Utils/CommunityEditionSsoReport.js.map +1 -0
  232. package/build/dist/Server/Utils/EditionEnforcement.js +187 -0
  233. package/build/dist/Server/Utils/EditionEnforcement.js.map +1 -0
  234. package/build/dist/Server/Utils/FrontendEnvironment.js +31 -2
  235. package/build/dist/Server/Utils/FrontendEnvironment.js.map +1 -1
  236. package/build/dist/Server/Utils/InstanceHealth/RedisHealth.js +6 -193
  237. package/build/dist/Server/Utils/InstanceHealth/RedisHealth.js.map +1 -1
  238. package/build/dist/Server/Utils/Monitor/AffectedResourceList.js +96 -0
  239. package/build/dist/Server/Utils/Monitor/AffectedResourceList.js.map +1 -0
  240. package/build/dist/Server/Utils/Monitor/Criteria/EvaluateOverTime.js +8 -1
  241. package/build/dist/Server/Utils/Monitor/Criteria/EvaluateOverTime.js.map +1 -1
  242. package/build/dist/Server/Utils/Monitor/MonitorCriteriaEvaluator.js +449 -261
  243. package/build/dist/Server/Utils/Monitor/MonitorCriteriaEvaluator.js.map +1 -1
  244. package/build/dist/Server/Utils/Monitor/MonitoringIntervalValidator.js +88 -0
  245. package/build/dist/Server/Utils/Monitor/MonitoringIntervalValidator.js.map +1 -0
  246. package/build/dist/Server/Utils/Workspace/Slack/Slack.js +331 -11
  247. package/build/dist/Server/Utils/Workspace/Slack/Slack.js.map +1 -1
  248. package/build/dist/Types/API/Hostname.js +59 -11
  249. package/build/dist/Types/API/Hostname.js.map +1 -1
  250. package/build/dist/Types/Database/AccessControl/ColumnAccessControl.js +19 -7
  251. package/build/dist/Types/Database/AccessControl/ColumnAccessControl.js.map +1 -1
  252. package/build/dist/Types/Database/AccessControl/ColumnBillingAccessControl.js +19 -7
  253. package/build/dist/Types/Database/AccessControl/ColumnBillingAccessControl.js.map +1 -1
  254. package/build/dist/Types/Database/AccessControl/OwnerOnlyColumn.js +13 -6
  255. package/build/dist/Types/Database/AccessControl/OwnerOnlyColumn.js.map +1 -1
  256. package/build/dist/Types/Database/CanonicalModelInstance.js +88 -0
  257. package/build/dist/Types/Database/CanonicalModelInstance.js.map +1 -0
  258. package/build/dist/Types/Database/TableColumn.js +19 -6
  259. package/build/dist/Types/Database/TableColumn.js.map +1 -1
  260. package/build/dist/Types/EnterpriseLicense/EnterpriseLicensePeriods.js +28 -0
  261. package/build/dist/Types/EnterpriseLicense/EnterpriseLicensePeriods.js.map +1 -0
  262. package/build/dist/Types/IP/IPv6.js +6 -1
  263. package/build/dist/Types/IP/IPv6.js.map +1 -1
  264. package/build/dist/Types/Monitor/MonitorStep.js +5 -0
  265. package/build/dist/Types/Monitor/MonitorStep.js.map +1 -1
  266. package/build/dist/Types/Monitor/ProxmoxAlertTemplates.js +1 -1
  267. package/build/dist/Types/StatusPage/UptimeDailyAggregate.js +32 -0
  268. package/build/dist/Types/StatusPage/UptimeDailyAggregate.js.map +1 -0
  269. package/build/dist/UI/Components/EditionLabel/EditionLabel.js +414 -585
  270. package/build/dist/UI/Components/EditionLabel/EditionLabel.js.map +1 -1
  271. package/build/dist/UI/Components/EditionLabel/LicenseManager.js +2 -0
  272. package/build/dist/UI/Components/EditionLabel/LicenseManager.js.map +1 -0
  273. package/build/dist/UI/Components/Graphs/DayUptimeGraph.js +90 -17
  274. package/build/dist/UI/Components/Graphs/DayUptimeGraph.js.map +1 -1
  275. package/build/dist/UI/Components/Markdown.tsx/MarkdownConverters.js +0 -0
  276. package/build/dist/UI/Components/Markdown.tsx/MarkdownConverters.js.map +1 -1
  277. package/build/dist/UI/Components/ModelList/ModelList.js +4 -1
  278. package/build/dist/UI/Components/ModelList/ModelList.js.map +1 -1
  279. package/build/dist/UI/Components/MonitorGraphs/Uptime.js +42 -1
  280. package/build/dist/UI/Components/MonitorGraphs/Uptime.js.map +1 -1
  281. package/build/dist/UI/Components/TelemetryViewer/TelemetryViewer.js +2 -2
  282. package/build/dist/UI/Components/TelemetryViewer/TelemetryViewer.js.map +1 -1
  283. package/build/dist/Utils/EnterpriseLicense/EnterpriseLicenseUsage.js +6 -2
  284. package/build/dist/Utils/EnterpriseLicense/EnterpriseLicenseUsage.js.map +1 -1
  285. package/build/dist/Utils/HostAddressUtil.js +80 -0
  286. package/build/dist/Utils/HostAddressUtil.js.map +1 -0
  287. package/build/dist/Utils/Monitor/MonitorDestinationUtil.js +127 -0
  288. package/build/dist/Utils/Monitor/MonitorDestinationUtil.js.map +1 -0
  289. package/build/dist/Utils/Monitor/MonitoringIntervalUtil.js +374 -0
  290. package/build/dist/Utils/Monitor/MonitoringIntervalUtil.js.map +1 -0
  291. package/build/dist/Utils/StatusPage/UptimeDailyAggregateUtil.js +148 -0
  292. package/build/dist/Utils/StatusPage/UptimeDailyAggregateUtil.js.map +1 -0
  293. package/jest.config.json +3 -1
  294. package/package.json +1 -1
  295. package/test-setup.sh +103 -34
  296. package/tsconfig.json +13 -0
  297. package/Server/API/EnterpriseLicenseAPI.ts +0 -925
  298. package/Server/API/TeamComplianceAPI.ts +0 -158
  299. package/Server/Middleware/SCIMAuthorization.ts +0 -141
  300. package/Server/Services/TeamComplianceService.ts +0 -733
  301. package/Server/Utils/EnterpriseLicense/EnterpriseLicenseSeatUtil.ts +0 -186
  302. package/Server/Utils/InstanceHealth/PostgresHealth.ts +0 -286
  303. package/Tests/App/AdminDashboard/EnterpriseLicenseInstanceActivity.test.tsx +0 -451
  304. package/Tests/App/Dashboard/AuditLogsTable.test.tsx +0 -598
  305. package/Tests/App/Dashboard/SecurityEventsExplorer.test.tsx +0 -761
  306. package/Tests/App/Dashboard/SecurityEventsSideMenu.test.tsx +0 -247
  307. package/Tests/App/Dashboard/SecurityEventsTableColumns.test.tsx +0 -382
  308. package/Tests/App/Dashboard/SecurityEventsVolumeChart.test.tsx +0 -321
  309. package/Tests/Server/API/EnterpriseLicenseReportUserCount.test.ts +0 -1851
  310. package/Tests/Server/Middleware/SCIMAuthorization.test.ts +0 -525
  311. package/Tests/Server/Services/TeamComplianceServiceBehaviour.test.ts +0 -1845
  312. package/Tests/Server/Utils/EnterpriseLicense/EnterpriseLicenseSeatUtil.test.ts +0 -495
  313. package/Tests/Server/Utils/InstanceHealth/PostgresHealth.test.ts +0 -337
  314. package/Tests/Utils/EnterpriseLicenseSeats.test.ts +0 -576
  315. package/Tests/Utils/EnterpriseLicenseSync.test.ts +0 -568
  316. package/Utils/EnterpriseLicense/EnterpriseLicenseSeats.ts +0 -287
  317. package/Utils/EnterpriseLicense/EnterpriseLicenseSync.ts +0 -244
  318. package/build/dist/Server/API/EnterpriseLicenseAPI.js +0 -646
  319. package/build/dist/Server/API/EnterpriseLicenseAPI.js.map +0 -1
  320. package/build/dist/Server/API/TeamComplianceAPI.js +0 -111
  321. package/build/dist/Server/API/TeamComplianceAPI.js.map +0 -1
  322. package/build/dist/Server/Middleware/SCIMAuthorization.js +0 -124
  323. package/build/dist/Server/Middleware/SCIMAuthorization.js.map +0 -1
  324. package/build/dist/Server/Services/TeamComplianceService.js +0 -537
  325. package/build/dist/Server/Services/TeamComplianceService.js.map +0 -1
  326. package/build/dist/Server/Utils/EnterpriseLicense/EnterpriseLicenseSeatUtil.js +0 -145
  327. package/build/dist/Server/Utils/EnterpriseLicense/EnterpriseLicenseSeatUtil.js.map +0 -1
  328. package/build/dist/Server/Utils/InstanceHealth/PostgresHealth.js +0 -194
  329. package/build/dist/Server/Utils/InstanceHealth/PostgresHealth.js.map +0 -1
  330. package/build/dist/Utils/EnterpriseLicense/EnterpriseLicenseSeats.js +0 -168
  331. package/build/dist/Utils/EnterpriseLicense/EnterpriseLicenseSeats.js.map +0 -1
  332. package/build/dist/Utils/EnterpriseLicense/EnterpriseLicenseSync.js +0 -174
  333. package/build/dist/Utils/EnterpriseLicense/EnterpriseLicenseSync.js.map +0 -1
@@ -0,0 +1,1823 @@
1
+ import {
2
+ afterEach,
3
+ beforeEach,
4
+ describe,
5
+ expect,
6
+ jest,
7
+ test,
8
+ } from "@jest/globals";
9
+ import EditionPermissions, {
10
+ MIN_ROTATED_SCIM_BEARER_TOKEN_LENGTH,
11
+ } from "../../../../../Server/Types/Database/Permissions/EditionPermission";
12
+ import BasePermission from "../../../../../Server/Types/Database/Permissions/BasePermission";
13
+ import CreatePermission from "../../../../../Server/Types/Database/Permissions/CreatePermission";
14
+ import DeletePermission from "../../../../../Server/Types/Database/Permissions/DeletePermission";
15
+ import TablePermission from "../../../../../Server/Types/Database/Permissions/TablePermission";
16
+ import UpdatePermission from "../../../../../Server/Types/Database/Permissions/UpdatePermission";
17
+ import DatabaseRequestType from "../../../../../Server/Types/BaseDatabase/DatabaseRequestType";
18
+ import Query from "../../../../../Server/Types/Database/Query";
19
+ import EnterpriseEdition from "../../../../../Server/Enterprise/EnterpriseEdition";
20
+ import EnterpriseFeature, {
21
+ ALL_ENTERPRISE_FEATURES,
22
+ } from "../../../../../Server/Enterprise/EnterpriseFeature";
23
+ import {
24
+ EnterpriseLicenseSnapshot,
25
+ EnterpriseLicenseStatus,
26
+ } from "../../../../../Server/Enterprise/EnterpriseLicenseSnapshot";
27
+ import BaseModel, {
28
+ DatabaseBaseModelType,
29
+ } from "../../../../../Models/DatabaseModels/DatabaseBaseModel/DatabaseBaseModel";
30
+ import GlobalConfig from "../../../../../Models/DatabaseModels/GlobalConfig";
31
+ import GlobalOIDC from "../../../../../Models/DatabaseModels/GlobalOidc";
32
+ import GlobalOIDCProject from "../../../../../Models/DatabaseModels/GlobalOidcProject";
33
+ import GlobalSSO from "../../../../../Models/DatabaseModels/GlobalSso";
34
+ import GlobalSSOProject from "../../../../../Models/DatabaseModels/GlobalSsoProject";
35
+ import Monitor from "../../../../../Models/DatabaseModels/Monitor";
36
+ import Project from "../../../../../Models/DatabaseModels/Project";
37
+ import ProjectOIDC from "../../../../../Models/DatabaseModels/ProjectOidc";
38
+ import ProjectSCIM from "../../../../../Models/DatabaseModels/ProjectSCIM";
39
+ import ProjectSSO from "../../../../../Models/DatabaseModels/ProjectSso";
40
+ import StatusPageOIDC from "../../../../../Models/DatabaseModels/StatusPageOidc";
41
+ import StatusPageSCIM from "../../../../../Models/DatabaseModels/StatusPageSCIM";
42
+ import StatusPageSSO from "../../../../../Models/DatabaseModels/StatusPageSso";
43
+ import Team from "../../../../../Models/DatabaseModels/Team";
44
+ import TeamComplianceSetting from "../../../../../Models/DatabaseModels/TeamComplianceSetting";
45
+ import DatabaseCommonInteractionProps from "../../../../../Types/BaseDatabase/DatabaseCommonInteractionProps";
46
+ import NotAuthenticatedException from "../../../../../Types/Exception/NotAuthenticatedException";
47
+ import NotAuthorizedException from "../../../../../Types/Exception/NotAuthorizedException";
48
+ import PaymentRequiredException from "../../../../../Types/Exception/PaymentRequiredException";
49
+ import ObjectID from "../../../../../Types/ObjectID";
50
+ import Permission, {
51
+ UserTenantAccessPermission,
52
+ } from "../../../../../Types/Permission";
53
+ import FakeEnterpriseModule, {
54
+ createLicenseSnapshot,
55
+ createLicenseSnapshotWithStatus,
56
+ installFakeEnterpriseModule,
57
+ uninstallEnterpriseModule,
58
+ } from "../../../Enterprise/FakeEnterpriseModule";
59
+ import { setTestBillingEnabled } from "../../../Enterprise/TestBillingFlag";
60
+
61
+ /*
62
+ * EditionPermission is the license gate on enterprise CONFIGURATION: creating
63
+ * or updating the eleven @TableEditionAccessControl({ requiresEnterprise })
64
+ * models (project, global and status page SSO/OIDC, SCIM, team compliance).
65
+ *
66
+ * The rules under test (design v2 section 0 and 3):
67
+ * - billing on (the cloud): never refuses; plan gates live in
68
+ * BillingPermission.
69
+ * - internal root writes: never refused.
70
+ * - read and delete: always allowed, so a downgraded install can still see
71
+ * and remove what it configured.
72
+ * - create and update, INCLUDING by master admins: the Community Edition is
73
+ * refused with the Community message; the Enterprise Edition is allowed
74
+ * exactly when the license snapshot is valid or in grace AND entitles the
75
+ * model's feature, and refused with the license message otherwise.
76
+ * - an unknown snapshot (the first load has not finished, or reading it
77
+ * throws) refuses writes - fail closed.
78
+ * - the exception: an update that only tightens security (disabling an
79
+ * identity provider, rotating a SCIM bearer token) needs no license, in
80
+ * either edition. It is judged on what the update writes, which only
81
+ * UpdatePermission has; without the data an update gets the full check.
82
+ *
83
+ * Billing and the edition are pinned in every test: CI's config.env sets
84
+ * BILLING_ENABLED=true, so an unpinned suite would only test the cloud path
85
+ * there.
86
+ */
87
+ jest.mock("../../../../../Server/EnvironmentConfig", () => {
88
+ const billingFlag: typeof import("../../../Enterprise/TestBillingFlag") =
89
+ jest.requireActual(
90
+ "../../../Enterprise/TestBillingFlag",
91
+ ) as typeof import("../../../Enterprise/TestBillingFlag");
92
+
93
+ return billingFlag.withLiveBillingFlag(
94
+ jest.requireActual("../../../../../Server/EnvironmentConfig") as Record<
95
+ string,
96
+ unknown
97
+ >,
98
+ );
99
+ });
100
+
101
+ type ModelType = DatabaseBaseModelType;
102
+
103
+ type Caller = "regular user" | "master admin" | "root";
104
+
105
+ type Outcome = "allowed" | "community" | "license";
106
+
107
+ type EditionState =
108
+ | { kind: "community" }
109
+ | { kind: "enterprise"; snapshot: EnterpriseLicenseSnapshot | null };
110
+
111
+ const ENTERPRISE_MODELS: ReadonlyArray<[string, ModelType, EnterpriseFeature]> =
112
+ [
113
+ ["GlobalSSO", GlobalSSO, EnterpriseFeature.SSO],
114
+ ["GlobalOIDC", GlobalOIDC, EnterpriseFeature.SSO],
115
+ ["GlobalSSOProject", GlobalSSOProject, EnterpriseFeature.SSO],
116
+ ["GlobalOIDCProject", GlobalOIDCProject, EnterpriseFeature.SSO],
117
+ ["ProjectSSO", ProjectSSO, EnterpriseFeature.SSO],
118
+ ["ProjectOIDC", ProjectOIDC, EnterpriseFeature.SSO],
119
+ ["StatusPageSSO", StatusPageSSO, EnterpriseFeature.SSO],
120
+ ["StatusPageOIDC", StatusPageOIDC, EnterpriseFeature.SSO],
121
+ ["ProjectSCIM", ProjectSCIM, EnterpriseFeature.SCIM],
122
+ ["StatusPageSCIM", StatusPageSCIM, EnterpriseFeature.SCIM],
123
+ [
124
+ "TeamComplianceSetting",
125
+ TeamComplianceSetting,
126
+ EnterpriseFeature.TeamCompliance,
127
+ ],
128
+ ];
129
+
130
+ const ORDINARY_MODELS: ReadonlyArray<[string, ModelType]> = [
131
+ ["Project", Project],
132
+ ["Monitor", Monitor],
133
+ ["Team", Team],
134
+ ["GlobalConfig", GlobalConfig],
135
+ ];
136
+
137
+ const ALL_OPERATIONS: ReadonlyArray<DatabaseRequestType> = [
138
+ DatabaseRequestType.Create,
139
+ DatabaseRequestType.Read,
140
+ DatabaseRequestType.Update,
141
+ DatabaseRequestType.Delete,
142
+ ];
143
+
144
+ const ALL_CALLERS: ReadonlyArray<Caller> = [
145
+ "regular user",
146
+ "master admin",
147
+ "root",
148
+ ];
149
+
150
+ const ALL_STATUSES: ReadonlyArray<EnterpriseLicenseStatus> = [
151
+ "missing",
152
+ "valid",
153
+ "grace",
154
+ "expired",
155
+ "invalid",
156
+ ];
157
+
158
+ const USABLE_STATUSES: ReadonlyArray<EnterpriseLicenseStatus> = [
159
+ "valid",
160
+ "grace",
161
+ ];
162
+
163
+ const UNUSABLE_STATUSES: ReadonlyArray<EnterpriseLicenseStatus> = [
164
+ "missing",
165
+ "expired",
166
+ "invalid",
167
+ ];
168
+
169
+ const projectId: ObjectID = new ObjectID(
170
+ "11111111-1111-4111-8111-111111111111",
171
+ );
172
+ const userId: ObjectID = new ObjectID("22222222-2222-4222-8222-222222222222");
173
+
174
+ const buildUserProps: (
175
+ permissions: Array<Permission>,
176
+ ) => DatabaseCommonInteractionProps = (
177
+ permissions: Array<Permission>,
178
+ ): DatabaseCommonInteractionProps => {
179
+ const tenantPermission: UserTenantAccessPermission = {
180
+ projectId,
181
+ _type: "UserTenantAccessPermission",
182
+ permissions: permissions.map((permission: Permission) => {
183
+ return {
184
+ _type: "UserPermission",
185
+ permission: permission,
186
+ labelIds: [],
187
+ isBlockPermission: false,
188
+ };
189
+ }),
190
+ } as UserTenantAccessPermission;
191
+
192
+ return {
193
+ userId,
194
+ tenantId: projectId,
195
+ userTenantAccessPermission: {
196
+ [projectId.toString()]: tenantPermission,
197
+ },
198
+ };
199
+ };
200
+
201
+ const propsFor: (caller: Caller) => DatabaseCommonInteractionProps = (
202
+ caller: Caller,
203
+ ): DatabaseCommonInteractionProps => {
204
+ if (caller === "root") {
205
+ return { isRoot: true };
206
+ }
207
+
208
+ if (caller === "master admin") {
209
+ return { userId, isMasterAdmin: true };
210
+ }
211
+
212
+ return buildUserProps([Permission.ProjectOwner]);
213
+ };
214
+
215
+ const describeEdition: (edition: EditionState) => string = (
216
+ edition: EditionState,
217
+ ): string => {
218
+ if (edition.kind === "community") {
219
+ return "community";
220
+ }
221
+
222
+ if (!edition.snapshot) {
223
+ return "enterprise(no snapshot yet)";
224
+ }
225
+
226
+ const features: string =
227
+ edition.snapshot.features === "all"
228
+ ? "all"
229
+ : `[${edition.snapshot.features.join(",")}]`;
230
+
231
+ return `enterprise(${edition.snapshot.status}, features=${features})`;
232
+ };
233
+
234
+ const installEdition: (edition: EditionState) => void = (
235
+ edition: EditionState,
236
+ ): void => {
237
+ if (edition.kind === "community") {
238
+ uninstallEnterpriseModule();
239
+ return;
240
+ }
241
+
242
+ installFakeEnterpriseModule({ snapshot: edition.snapshot });
243
+ };
244
+
245
+ const entitles: (
246
+ snapshot: EnterpriseLicenseSnapshot | null,
247
+ feature: EnterpriseFeature,
248
+ ) => boolean = (
249
+ snapshot: EnterpriseLicenseSnapshot | null,
250
+ feature: EnterpriseFeature,
251
+ ): boolean => {
252
+ if (!snapshot) {
253
+ return false;
254
+ }
255
+
256
+ if (!USABLE_STATUSES.includes(snapshot.status)) {
257
+ return false;
258
+ }
259
+
260
+ return snapshot.features === "all" || snapshot.features.includes(feature);
261
+ };
262
+
263
+ // The rule, written independently of the implementation.
264
+ const expectedOutcome: (input: {
265
+ billing: boolean;
266
+ edition: EditionState;
267
+ operation: DatabaseRequestType;
268
+ caller: Caller;
269
+ feature: EnterpriseFeature | null;
270
+ }) => Outcome = (input: {
271
+ billing: boolean;
272
+ edition: EditionState;
273
+ operation: DatabaseRequestType;
274
+ caller: Caller;
275
+ feature: EnterpriseFeature | null;
276
+ }): Outcome => {
277
+ if (!input.feature) {
278
+ return "allowed";
279
+ }
280
+
281
+ if (input.billing || input.caller === "root") {
282
+ return "allowed";
283
+ }
284
+
285
+ if (
286
+ input.operation === DatabaseRequestType.Read ||
287
+ input.operation === DatabaseRequestType.Delete
288
+ ) {
289
+ return "allowed";
290
+ }
291
+
292
+ if (input.edition.kind === "community") {
293
+ return "community";
294
+ }
295
+
296
+ return entitles(input.edition.snapshot, input.feature)
297
+ ? "allowed"
298
+ : "license";
299
+ };
300
+
301
+ const runEditionCheck: (input: {
302
+ modelType: ModelType;
303
+ operation: DatabaseRequestType;
304
+ caller: Caller;
305
+ }) => Outcome = (input: {
306
+ modelType: ModelType;
307
+ operation: DatabaseRequestType;
308
+ caller: Caller;
309
+ }): Outcome => {
310
+ try {
311
+ const result: unknown = EditionPermissions.checkEditionPermissions(
312
+ input.modelType,
313
+ propsFor(input.caller),
314
+ input.operation,
315
+ );
316
+
317
+ // A Promise here would mean a refusal could be lost to a missed await.
318
+ expect(result).toBeUndefined();
319
+
320
+ return "allowed";
321
+ } catch (err) {
322
+ if (!(err instanceof PaymentRequiredException)) {
323
+ throw err;
324
+ }
325
+
326
+ if (err.message === EnterpriseEdition.COMMUNITY_EDITION_MESSAGE) {
327
+ return "community";
328
+ }
329
+
330
+ if (err.message === EnterpriseEdition.LICENSE_REQUIRED_MESSAGE) {
331
+ return "license";
332
+ }
333
+
334
+ throw err;
335
+ }
336
+ };
337
+
338
+ /*
339
+ * Runs every model x operation x caller combination for one billing/edition
340
+ * state and returns the combinations whose outcome differs from the rule.
341
+ * One readable list on failure instead of hundreds of generated tests.
342
+ */
343
+ const findMismatches: (input: {
344
+ billing: boolean;
345
+ edition: EditionState;
346
+ models: ReadonlyArray<[string, ModelType, EnterpriseFeature | null]>;
347
+ }) => Array<string> = (input: {
348
+ billing: boolean;
349
+ edition: EditionState;
350
+ models: ReadonlyArray<[string, ModelType, EnterpriseFeature | null]>;
351
+ }): Array<string> => {
352
+ setTestBillingEnabled(input.billing);
353
+ installEdition(input.edition);
354
+
355
+ const mismatches: Array<string> = [];
356
+
357
+ for (const [modelName, modelType, feature] of input.models) {
358
+ for (const operation of ALL_OPERATIONS) {
359
+ for (const caller of ALL_CALLERS) {
360
+ const expected: Outcome = expectedOutcome({
361
+ billing: input.billing,
362
+ edition: input.edition,
363
+ operation,
364
+ caller,
365
+ feature,
366
+ });
367
+ const actual: Outcome = runEditionCheck({
368
+ modelType,
369
+ operation,
370
+ caller,
371
+ });
372
+
373
+ if (expected !== actual) {
374
+ mismatches.push(
375
+ `billing=${input.billing} ${describeEdition(input.edition)} ${modelName} ${operation} as ${caller}: expected ${expected}, got ${actual}`,
376
+ );
377
+ }
378
+ }
379
+ }
380
+ }
381
+
382
+ return mismatches;
383
+ };
384
+
385
+ const allEditionStates: () => Array<EditionState> = (): Array<EditionState> => {
386
+ const states: Array<EditionState> = [{ kind: "community" }];
387
+
388
+ for (const status of ALL_STATUSES) {
389
+ states.push({
390
+ kind: "enterprise",
391
+ snapshot: createLicenseSnapshotWithStatus(status),
392
+ });
393
+ }
394
+
395
+ states.push({ kind: "enterprise", snapshot: null });
396
+
397
+ return states;
398
+ };
399
+
400
+ describe("EditionPermission.checkEditionPermissions", () => {
401
+ beforeEach(() => {
402
+ setTestBillingEnabled(false);
403
+ uninstallEnterpriseModule();
404
+ });
405
+
406
+ afterEach(() => {
407
+ uninstallEnterpriseModule();
408
+ setTestBillingEnabled(false);
409
+ jest.restoreAllMocks();
410
+ });
411
+
412
+ describe("the full matrix (billing x edition x license status x operation x caller x model)", () => {
413
+ test.each([true, false])(
414
+ "billing=%p: every enterprise model follows the rule in every edition state",
415
+ (billing: boolean) => {
416
+ const mismatches: Array<string> = [];
417
+
418
+ for (const edition of allEditionStates()) {
419
+ mismatches.push(
420
+ ...findMismatches({
421
+ billing,
422
+ edition,
423
+ models: ENTERPRISE_MODELS,
424
+ }),
425
+ );
426
+ }
427
+
428
+ expect(mismatches).toEqual([]);
429
+ },
430
+ );
431
+
432
+ test.each([true, false])(
433
+ "billing=%p: ordinary models are never gated in any edition state",
434
+ (billing: boolean) => {
435
+ const mismatches: Array<string> = [];
436
+
437
+ for (const edition of allEditionStates()) {
438
+ mismatches.push(
439
+ ...findMismatches({
440
+ billing,
441
+ edition,
442
+ models: ORDINARY_MODELS.map(
443
+ ([name, modelType]: [string, ModelType]): [
444
+ string,
445
+ ModelType,
446
+ null,
447
+ ] => {
448
+ return [name, modelType, null];
449
+ },
450
+ ),
451
+ }),
452
+ );
453
+ }
454
+
455
+ expect(mismatches).toEqual([]);
456
+ },
457
+ );
458
+ });
459
+
460
+ describe("billing on (the cloud)", () => {
461
+ test.each(ENTERPRISE_MODELS)(
462
+ "%s: never refused, even on the Community Edition with no license",
463
+ (_name: string, modelType: ModelType) => {
464
+ setTestBillingEnabled(true);
465
+ uninstallEnterpriseModule();
466
+
467
+ for (const caller of ALL_CALLERS) {
468
+ for (const operation of ALL_OPERATIONS) {
469
+ expect(runEditionCheck({ modelType, operation, caller })).toBe(
470
+ "allowed",
471
+ );
472
+ }
473
+ }
474
+ },
475
+ );
476
+
477
+ test("never refused on the Enterprise Edition whatever the license says", () => {
478
+ setTestBillingEnabled(true);
479
+
480
+ for (const status of ALL_STATUSES) {
481
+ installFakeEnterpriseModule({
482
+ snapshot: createLicenseSnapshotWithStatus(status, { features: [] }),
483
+ });
484
+
485
+ expect(
486
+ runEditionCheck({
487
+ modelType: ProjectSSO,
488
+ operation: DatabaseRequestType.Create,
489
+ caller: "regular user",
490
+ }),
491
+ ).toBe("allowed");
492
+ }
493
+ });
494
+ });
495
+
496
+ describe("billing off, Community Edition", () => {
497
+ test.each(ENTERPRISE_MODELS)(
498
+ "%s: create and update are refused with the Community Edition message, for users and master admins",
499
+ (_name: string, modelType: ModelType) => {
500
+ for (const caller of [
501
+ "regular user",
502
+ "master admin",
503
+ ] as Array<Caller>) {
504
+ for (const operation of [
505
+ DatabaseRequestType.Create,
506
+ DatabaseRequestType.Update,
507
+ ]) {
508
+ expect(() => {
509
+ EditionPermissions.checkEditionPermissions(
510
+ modelType,
511
+ propsFor(caller),
512
+ operation,
513
+ );
514
+ }).toThrow(
515
+ new PaymentRequiredException(
516
+ EnterpriseEdition.COMMUNITY_EDITION_MESSAGE,
517
+ ),
518
+ );
519
+ }
520
+ }
521
+ },
522
+ );
523
+
524
+ test.each(ENTERPRISE_MODELS)(
525
+ "%s: read and delete stay allowed so leftover configuration can be seen and removed",
526
+ (_name: string, modelType: ModelType) => {
527
+ for (const caller of ALL_CALLERS) {
528
+ for (const operation of [
529
+ DatabaseRequestType.Read,
530
+ DatabaseRequestType.Delete,
531
+ ]) {
532
+ expect(runEditionCheck({ modelType, operation, caller })).toBe(
533
+ "allowed",
534
+ );
535
+ }
536
+ }
537
+ },
538
+ );
539
+
540
+ test.each(ENTERPRISE_MODELS)(
541
+ "%s: internal root writes are never checked",
542
+ (_name: string, modelType: ModelType) => {
543
+ for (const operation of ALL_OPERATIONS) {
544
+ expect(
545
+ runEditionCheck({ modelType, operation, caller: "root" }),
546
+ ).toBe("allowed");
547
+ }
548
+
549
+ // Root wins even when the props also claim master admin.
550
+ expect(() => {
551
+ EditionPermissions.checkEditionPermissions(
552
+ modelType,
553
+ { isRoot: true, isMasterAdmin: true },
554
+ DatabaseRequestType.Create,
555
+ );
556
+ }).not.toThrow();
557
+ },
558
+ );
559
+ });
560
+
561
+ describe("billing off, Enterprise Edition", () => {
562
+ test.each(USABLE_STATUSES)(
563
+ "a %s license allows creating and updating every enterprise model",
564
+ (status: EnterpriseLicenseStatus) => {
565
+ installFakeEnterpriseModule({
566
+ snapshot: createLicenseSnapshotWithStatus(status),
567
+ });
568
+
569
+ for (const [, modelType] of ENTERPRISE_MODELS) {
570
+ for (const caller of ALL_CALLERS) {
571
+ expect(
572
+ runEditionCheck({
573
+ modelType,
574
+ operation: DatabaseRequestType.Create,
575
+ caller,
576
+ }),
577
+ ).toBe("allowed");
578
+ expect(
579
+ runEditionCheck({
580
+ modelType,
581
+ operation: DatabaseRequestType.Update,
582
+ caller,
583
+ }),
584
+ ).toBe("allowed");
585
+ }
586
+ }
587
+ },
588
+ );
589
+
590
+ test.each(UNUSABLE_STATUSES)(
591
+ "a %s license makes enterprise configuration read-only, including for master admins",
592
+ (status: EnterpriseLicenseStatus) => {
593
+ installFakeEnterpriseModule({
594
+ snapshot: createLicenseSnapshotWithStatus(status),
595
+ });
596
+
597
+ for (const [, modelType] of ENTERPRISE_MODELS) {
598
+ for (const caller of [
599
+ "regular user",
600
+ "master admin",
601
+ ] as Array<Caller>) {
602
+ expect(() => {
603
+ EditionPermissions.checkEditionPermissions(
604
+ modelType,
605
+ propsFor(caller),
606
+ DatabaseRequestType.Create,
607
+ );
608
+ }).toThrow(
609
+ new PaymentRequiredException(
610
+ EnterpriseEdition.LICENSE_REQUIRED_MESSAGE,
611
+ ),
612
+ );
613
+ expect(
614
+ runEditionCheck({
615
+ modelType,
616
+ operation: DatabaseRequestType.Update,
617
+ caller,
618
+ }),
619
+ ).toBe("license");
620
+ expect(
621
+ runEditionCheck({
622
+ modelType,
623
+ operation: DatabaseRequestType.Read,
624
+ caller,
625
+ }),
626
+ ).toBe("allowed");
627
+ expect(
628
+ runEditionCheck({
629
+ modelType,
630
+ operation: DatabaseRequestType.Delete,
631
+ caller,
632
+ }),
633
+ ).toBe("allowed");
634
+ }
635
+ }
636
+ },
637
+ );
638
+
639
+ test("before the first license load finishes (no snapshot) writes are refused - fail closed", () => {
640
+ installFakeEnterpriseModule({ snapshot: null });
641
+
642
+ expect(
643
+ runEditionCheck({
644
+ modelType: GlobalSSO,
645
+ operation: DatabaseRequestType.Create,
646
+ caller: "master admin",
647
+ }),
648
+ ).toBe("license");
649
+ expect(
650
+ runEditionCheck({
651
+ modelType: GlobalSSO,
652
+ operation: DatabaseRequestType.Read,
653
+ caller: "master admin",
654
+ }),
655
+ ).toBe("allowed");
656
+ });
657
+
658
+ test("a snapshot read that throws refuses writes - fail closed", () => {
659
+ const fake: FakeEnterpriseModule = installFakeEnterpriseModule();
660
+ fake.licensing.getCachedSnapshotError = new Error("cache exploded");
661
+
662
+ expect(
663
+ runEditionCheck({
664
+ modelType: ProjectSSO,
665
+ operation: DatabaseRequestType.Update,
666
+ caller: "regular user",
667
+ }),
668
+ ).toBe("license");
669
+ expect(
670
+ runEditionCheck({
671
+ modelType: ProjectSSO,
672
+ operation: DatabaseRequestType.Delete,
673
+ caller: "regular user",
674
+ }),
675
+ ).toBe("allowed");
676
+ });
677
+
678
+ test("the check follows the license as it changes, with no restart", () => {
679
+ const fake: FakeEnterpriseModule = installFakeEnterpriseModule({
680
+ snapshot: createLicenseSnapshotWithStatus("valid"),
681
+ });
682
+
683
+ expect(
684
+ runEditionCheck({
685
+ modelType: ProjectSCIM,
686
+ operation: DatabaseRequestType.Create,
687
+ caller: "regular user",
688
+ }),
689
+ ).toBe("allowed");
690
+
691
+ fake.setSnapshot(createLicenseSnapshotWithStatus("expired"));
692
+
693
+ expect(
694
+ runEditionCheck({
695
+ modelType: ProjectSCIM,
696
+ operation: DatabaseRequestType.Create,
697
+ caller: "regular user",
698
+ }),
699
+ ).toBe("license");
700
+
701
+ fake.setSnapshot(createLicenseSnapshotWithStatus("grace"));
702
+
703
+ expect(
704
+ runEditionCheck({
705
+ modelType: ProjectSCIM,
706
+ operation: DatabaseRequestType.Create,
707
+ caller: "regular user",
708
+ }),
709
+ ).toBe("allowed");
710
+ });
711
+ });
712
+
713
+ describe("each model is gated by its own license feature", () => {
714
+ test.each(ENTERPRISE_MODELS)(
715
+ "%s: a license entitling only %s allows writes",
716
+ (_name: string, modelType: ModelType, feature: EnterpriseFeature) => {
717
+ installFakeEnterpriseModule({
718
+ snapshot: createLicenseSnapshot({ features: [feature] }),
719
+ });
720
+
721
+ expect(EnterpriseEdition.getModelFeature(modelType)).toBe(feature);
722
+ expect(
723
+ runEditionCheck({
724
+ modelType,
725
+ operation: DatabaseRequestType.Create,
726
+ caller: "master admin",
727
+ }),
728
+ ).toBe("allowed");
729
+ expect(
730
+ runEditionCheck({
731
+ modelType,
732
+ operation: DatabaseRequestType.Update,
733
+ caller: "regular user",
734
+ }),
735
+ ).toBe("allowed");
736
+ },
737
+ );
738
+
739
+ test.each(ENTERPRISE_MODELS)(
740
+ "%s: a license entitling every feature except %s refuses writes",
741
+ (_name: string, modelType: ModelType, feature: EnterpriseFeature) => {
742
+ installFakeEnterpriseModule({
743
+ snapshot: createLicenseSnapshot({
744
+ features: ALL_ENTERPRISE_FEATURES.filter(
745
+ (candidate: EnterpriseFeature): boolean => {
746
+ return candidate !== feature;
747
+ },
748
+ ),
749
+ }),
750
+ });
751
+
752
+ expect(
753
+ runEditionCheck({
754
+ modelType,
755
+ operation: DatabaseRequestType.Create,
756
+ caller: "master admin",
757
+ }),
758
+ ).toBe("license");
759
+ expect(
760
+ runEditionCheck({
761
+ modelType,
762
+ operation: DatabaseRequestType.Update,
763
+ caller: "regular user",
764
+ }),
765
+ ).toBe("license");
766
+ expect(
767
+ runEditionCheck({
768
+ modelType,
769
+ operation: DatabaseRequestType.Read,
770
+ caller: "regular user",
771
+ }),
772
+ ).toBe("allowed");
773
+ },
774
+ );
775
+
776
+ test("an empty feature list entitles nothing", () => {
777
+ installFakeEnterpriseModule({
778
+ snapshot: createLicenseSnapshot({ features: [] }),
779
+ });
780
+
781
+ for (const [, modelType] of ENTERPRISE_MODELS) {
782
+ expect(
783
+ runEditionCheck({
784
+ modelType,
785
+ operation: DatabaseRequestType.Create,
786
+ caller: "regular user",
787
+ }),
788
+ ).toBe("license");
789
+ }
790
+ });
791
+ });
792
+
793
+ describe("an enterprise model the facade has no feature for", () => {
794
+ /*
795
+ * The facade's guard test should make this impossible; if it ever
796
+ * happens the gate must fail closed rather than let the write through.
797
+ */
798
+ class UnmappedEnterpriseModel extends Monitor {}
799
+
800
+ beforeEach(() => {
801
+ (UnmappedEnterpriseModel.prototype as BaseModel).requiresEnterprise =
802
+ true;
803
+ (UnmappedEnterpriseModel.prototype as BaseModel).tableName =
804
+ "UnmappedEnterpriseModelForTest";
805
+ });
806
+
807
+ test("maps to no feature", () => {
808
+ expect(
809
+ EnterpriseEdition.getModelFeature(UnmappedEnterpriseModel),
810
+ ).toBeNull();
811
+ expect(new UnmappedEnterpriseModel().requiresEnterprise).toBe(true);
812
+ });
813
+
814
+ test("is refused on the Community Edition", () => {
815
+ expect(
816
+ runEditionCheck({
817
+ modelType: UnmappedEnterpriseModel,
818
+ operation: DatabaseRequestType.Create,
819
+ caller: "regular user",
820
+ }),
821
+ ).toBe("community");
822
+ });
823
+
824
+ test("needs a license that entitles every enterprise feature", () => {
825
+ installFakeEnterpriseModule({
826
+ snapshot: createLicenseSnapshot({ features: "all" }),
827
+ });
828
+
829
+ expect(
830
+ runEditionCheck({
831
+ modelType: UnmappedEnterpriseModel,
832
+ operation: DatabaseRequestType.Update,
833
+ caller: "regular user",
834
+ }),
835
+ ).toBe("allowed");
836
+
837
+ installFakeEnterpriseModule({
838
+ snapshot: createLicenseSnapshot({
839
+ features: ALL_ENTERPRISE_FEATURES.filter(
840
+ (feature: EnterpriseFeature): boolean => {
841
+ return feature !== EnterpriseFeature.InstanceHealth;
842
+ },
843
+ ),
844
+ }),
845
+ });
846
+
847
+ expect(
848
+ runEditionCheck({
849
+ modelType: UnmappedEnterpriseModel,
850
+ operation: DatabaseRequestType.Update,
851
+ caller: "regular user",
852
+ }),
853
+ ).toBe("license");
854
+ });
855
+
856
+ test("is still readable and deletable", () => {
857
+ expect(
858
+ runEditionCheck({
859
+ modelType: UnmappedEnterpriseModel,
860
+ operation: DatabaseRequestType.Read,
861
+ caller: "regular user",
862
+ }),
863
+ ).toBe("allowed");
864
+ expect(
865
+ runEditionCheck({
866
+ modelType: UnmappedEnterpriseModel,
867
+ operation: DatabaseRequestType.Delete,
868
+ caller: "regular user",
869
+ }),
870
+ ).toBe("allowed");
871
+ });
872
+ });
873
+ });
874
+
875
+ describe("EditionPermission wiring through the permission entry points", () => {
876
+ beforeEach(() => {
877
+ setTestBillingEnabled(false);
878
+ uninstallEnterpriseModule();
879
+ });
880
+
881
+ afterEach(() => {
882
+ uninstallEnterpriseModule();
883
+ setTestBillingEnabled(false);
884
+ jest.restoreAllMocks();
885
+ });
886
+
887
+ describe("CreatePermission: master admins are checked before their early return", () => {
888
+ test("a master admin cannot create a global SSO provider on the Community Edition", () => {
889
+ expect(() => {
890
+ CreatePermission.checkCreatePermissions(GlobalSSO, new GlobalSSO(), {
891
+ userId,
892
+ isMasterAdmin: true,
893
+ });
894
+ }).toThrow(
895
+ new PaymentRequiredException(
896
+ EnterpriseEdition.COMMUNITY_EDITION_MESSAGE,
897
+ ),
898
+ );
899
+ });
900
+
901
+ test("a master admin cannot create a global OIDC provider when the license expired", () => {
902
+ installFakeEnterpriseModule({
903
+ snapshot: createLicenseSnapshotWithStatus("expired"),
904
+ });
905
+
906
+ expect(() => {
907
+ CreatePermission.checkCreatePermissions(GlobalOIDC, new GlobalOIDC(), {
908
+ userId,
909
+ isMasterAdmin: true,
910
+ });
911
+ }).toThrow(
912
+ new PaymentRequiredException(
913
+ EnterpriseEdition.LICENSE_REQUIRED_MESSAGE,
914
+ ),
915
+ );
916
+ });
917
+
918
+ test("a master admin can create one with a valid license", () => {
919
+ installFakeEnterpriseModule();
920
+
921
+ expect(() => {
922
+ CreatePermission.checkCreatePermissions(GlobalSSO, new GlobalSSO(), {
923
+ userId,
924
+ isMasterAdmin: true,
925
+ });
926
+ }).not.toThrow();
927
+ });
928
+
929
+ test("internal root writes are not checked", () => {
930
+ expect(() => {
931
+ CreatePermission.checkCreatePermissions(GlobalSSO, new GlobalSSO(), {
932
+ isRoot: true,
933
+ });
934
+ }).not.toThrow();
935
+ expect(() => {
936
+ CreatePermission.checkCreatePermissions(GlobalSSO, new GlobalSSO(), {
937
+ isRoot: true,
938
+ isMasterAdmin: true,
939
+ });
940
+ }).not.toThrow();
941
+ });
942
+
943
+ test("a master admin creating an ordinary model is unaffected", () => {
944
+ expect(() => {
945
+ CreatePermission.checkCreatePermissions(Project, new Project(), {
946
+ userId,
947
+ isMasterAdmin: true,
948
+ });
949
+ }).not.toThrow();
950
+ });
951
+
952
+ test("billing on leaves master admin creates to the plan gates", () => {
953
+ setTestBillingEnabled(true);
954
+
955
+ expect(() => {
956
+ CreatePermission.checkCreatePermissions(GlobalSSO, new GlobalSSO(), {
957
+ userId,
958
+ isMasterAdmin: true,
959
+ });
960
+ }).not.toThrow();
961
+ });
962
+ });
963
+
964
+ /*
965
+ * These use { isEnabled: true }: switching a provider ON is configuration.
966
+ * Switching one off is a tighten-only update, allowed without the license
967
+ * (see "tighten-only updates" below).
968
+ */
969
+ describe("UpdatePermission: master admins are checked before their early return", () => {
970
+ const query: Query<GlobalSSO> = { name: "Okta" } as Query<GlobalSSO>;
971
+
972
+ test("a master admin cannot update a global SSO provider on the Community Edition", async () => {
973
+ await expect(
974
+ UpdatePermission.checkUpdatePermissions(
975
+ GlobalSSO,
976
+ query,
977
+ { isEnabled: true },
978
+ { userId, isMasterAdmin: true },
979
+ ),
980
+ ).rejects.toThrow(
981
+ new PaymentRequiredException(
982
+ EnterpriseEdition.COMMUNITY_EDITION_MESSAGE,
983
+ ),
984
+ );
985
+ });
986
+
987
+ test.each(UNUSABLE_STATUSES)(
988
+ "a master admin cannot update one with a %s license",
989
+ async (status: EnterpriseLicenseStatus) => {
990
+ installFakeEnterpriseModule({
991
+ snapshot: createLicenseSnapshotWithStatus(status),
992
+ });
993
+
994
+ await expect(
995
+ UpdatePermission.checkUpdatePermissions(
996
+ GlobalSSO,
997
+ query,
998
+ { isEnabled: true },
999
+ { userId, isMasterAdmin: true },
1000
+ ),
1001
+ ).rejects.toThrow(
1002
+ new PaymentRequiredException(
1003
+ EnterpriseEdition.LICENSE_REQUIRED_MESSAGE,
1004
+ ),
1005
+ );
1006
+ },
1007
+ );
1008
+
1009
+ test.each(USABLE_STATUSES)(
1010
+ "a master admin can update one with a %s license, and the query is returned unchanged",
1011
+ async (status: EnterpriseLicenseStatus) => {
1012
+ installFakeEnterpriseModule({
1013
+ snapshot: createLicenseSnapshotWithStatus(status),
1014
+ });
1015
+
1016
+ await expect(
1017
+ UpdatePermission.checkUpdatePermissions(
1018
+ GlobalSSO,
1019
+ query,
1020
+ { isEnabled: true },
1021
+ { userId, isMasterAdmin: true },
1022
+ ),
1023
+ ).resolves.toBe(query);
1024
+ },
1025
+ );
1026
+
1027
+ test("internal root updates are not checked", async () => {
1028
+ await expect(
1029
+ UpdatePermission.checkUpdatePermissions(
1030
+ GlobalSSO,
1031
+ query,
1032
+ { isEnabled: false },
1033
+ { isRoot: true },
1034
+ ),
1035
+ ).resolves.toBe(query);
1036
+ });
1037
+ });
1038
+
1039
+ describe("DeletePermission: removing leftover configuration always works", () => {
1040
+ test.each(ENTERPRISE_MODELS)(
1041
+ "a master admin can delete %s on the Community Edition",
1042
+ async (_name: string, modelType: ModelType) => {
1043
+ await expect(
1044
+ DeletePermission.checkDeletePermission(
1045
+ modelType,
1046
+ {},
1047
+ { userId, isMasterAdmin: true },
1048
+ ),
1049
+ ).resolves.toEqual({});
1050
+ },
1051
+ );
1052
+
1053
+ test("a project owner passes the table-level delete check on the Community Edition", () => {
1054
+ expect(() => {
1055
+ TablePermission.checkTableLevelPermissions(
1056
+ ProjectSSO,
1057
+ buildUserProps([Permission.ProjectOwner]),
1058
+ DatabaseRequestType.Delete,
1059
+ );
1060
+ }).not.toThrow();
1061
+ });
1062
+ });
1063
+
1064
+ describe("TablePermission: the operation reaches the edition check", () => {
1065
+ test("a project owner's create is refused on the Community Edition", () => {
1066
+ expect(() => {
1067
+ TablePermission.checkTableLevelPermissions(
1068
+ ProjectSSO,
1069
+ buildUserProps([Permission.ProjectOwner]),
1070
+ DatabaseRequestType.Create,
1071
+ );
1072
+ }).toThrow(PaymentRequiredException);
1073
+ });
1074
+
1075
+ test("a project owner's read is allowed on the Community Edition", () => {
1076
+ expect(() => {
1077
+ TablePermission.checkTableLevelPermissions(
1078
+ ProjectSSO,
1079
+ buildUserProps([Permission.ProjectOwner]),
1080
+ DatabaseRequestType.Read,
1081
+ );
1082
+ }).not.toThrow();
1083
+ });
1084
+
1085
+ test("a project owner's create passes with a valid license", () => {
1086
+ installFakeEnterpriseModule();
1087
+
1088
+ expect(() => {
1089
+ TablePermission.checkTableLevelPermissions(
1090
+ ProjectSSO,
1091
+ buildUserProps([Permission.ProjectOwner]),
1092
+ DatabaseRequestType.Create,
1093
+ );
1094
+ }).not.toThrow();
1095
+ });
1096
+
1097
+ test("the edition check does not replace the permission check: a licensed install still refuses a user without permission", () => {
1098
+ installFakeEnterpriseModule();
1099
+
1100
+ expect(() => {
1101
+ TablePermission.checkTableLevelPermissions(
1102
+ ProjectSSO,
1103
+ buildUserProps([Permission.ProjectMember]),
1104
+ DatabaseRequestType.Create,
1105
+ );
1106
+ }).toThrow(NotAuthorizedException);
1107
+ });
1108
+
1109
+ test("a signed-out caller still gets 'not logged in' before any edition answer", () => {
1110
+ expect(() => {
1111
+ TablePermission.checkTableLevelPermissions(
1112
+ ProjectSSO,
1113
+ {},
1114
+ DatabaseRequestType.Create,
1115
+ );
1116
+ }).toThrow(NotAuthenticatedException);
1117
+ });
1118
+
1119
+ /*
1120
+ * TablePermission is not handed the update's data, so it cannot tell a
1121
+ * tighten-only update from any other. It leaves updates to
1122
+ * UpdatePermission, which is (see the next block).
1123
+ */
1124
+ test("updates are left to UpdatePermission: TablePermission does not ask the edition check", () => {
1125
+ const editionCheck: ReturnType<typeof jest.spyOn> = jest.spyOn(
1126
+ EditionPermissions,
1127
+ "checkEditionPermissions",
1128
+ );
1129
+
1130
+ expect(() => {
1131
+ TablePermission.checkTableLevelPermissions(
1132
+ ProjectSSO,
1133
+ buildUserProps([Permission.ProjectOwner]),
1134
+ DatabaseRequestType.Update,
1135
+ );
1136
+ }).not.toThrow();
1137
+ expect(editionCheck).not.toHaveBeenCalled();
1138
+
1139
+ expect(() => {
1140
+ TablePermission.checkTableLevelPermissions(
1141
+ ProjectSSO,
1142
+ buildUserProps([Permission.ProjectOwner]),
1143
+ DatabaseRequestType.Create,
1144
+ );
1145
+ }).toThrow(PaymentRequiredException);
1146
+ expect(editionCheck).toHaveBeenCalledTimes(1);
1147
+ });
1148
+ });
1149
+
1150
+ describe("UpdatePermission: every caller's update is edition-checked with its data", () => {
1151
+ const query: Query<ProjectSSO> = {
1152
+ _id: "33333333-3333-4333-8333-333333333333",
1153
+ } as Query<ProjectSSO>;
1154
+
1155
+ const ownerProps: () => DatabaseCommonInteractionProps =
1156
+ (): DatabaseCommonInteractionProps => {
1157
+ return buildUserProps([Permission.ProjectOwner]);
1158
+ };
1159
+
1160
+ beforeEach(() => {
1161
+ // What follows the edition check needs a database; it is not under test.
1162
+ jest.spyOn(BasePermission, "checkPermissions").mockImplementation((async (
1163
+ _modelType: unknown,
1164
+ checkedQuery: Query<ProjectSSO>,
1165
+ ) => {
1166
+ return { query: checkedQuery };
1167
+ }) as never);
1168
+ });
1169
+
1170
+ test("a project owner's ordinary update is refused on the Community Edition", async () => {
1171
+ await expect(
1172
+ UpdatePermission.checkUpdatePermissions(
1173
+ ProjectSSO,
1174
+ query,
1175
+ { name: "Okta (renamed)" },
1176
+ ownerProps(),
1177
+ ),
1178
+ ).rejects.toThrow(
1179
+ new PaymentRequiredException(
1180
+ EnterpriseEdition.COMMUNITY_EDITION_MESSAGE,
1181
+ ),
1182
+ );
1183
+ });
1184
+
1185
+ test.each(UNUSABLE_STATUSES)(
1186
+ "a project owner cannot switch a provider on with a %s license",
1187
+ async (status: EnterpriseLicenseStatus) => {
1188
+ installFakeEnterpriseModule({
1189
+ snapshot: createLicenseSnapshotWithStatus(status),
1190
+ });
1191
+
1192
+ await expect(
1193
+ UpdatePermission.checkUpdatePermissions(
1194
+ ProjectSSO,
1195
+ query,
1196
+ { isEnabled: true },
1197
+ ownerProps(),
1198
+ ),
1199
+ ).rejects.toThrow(
1200
+ new PaymentRequiredException(
1201
+ EnterpriseEdition.LICENSE_REQUIRED_MESSAGE,
1202
+ ),
1203
+ );
1204
+ },
1205
+ );
1206
+
1207
+ test.each(UNUSABLE_STATUSES)(
1208
+ "a project owner can switch a provider off with a %s license",
1209
+ async (status: EnterpriseLicenseStatus) => {
1210
+ installFakeEnterpriseModule({
1211
+ snapshot: createLicenseSnapshotWithStatus(status),
1212
+ });
1213
+
1214
+ await expect(
1215
+ UpdatePermission.checkUpdatePermissions(
1216
+ ProjectSSO,
1217
+ query,
1218
+ { isEnabled: false },
1219
+ ownerProps(),
1220
+ ),
1221
+ ).resolves.toEqual(query);
1222
+ },
1223
+ );
1224
+
1225
+ test("a project owner can rotate a leaked SCIM bearer token with an expired license", async () => {
1226
+ installFakeEnterpriseModule({
1227
+ snapshot: createLicenseSnapshotWithStatus("expired"),
1228
+ });
1229
+
1230
+ await expect(
1231
+ UpdatePermission.checkUpdatePermissions(
1232
+ ProjectSCIM,
1233
+ query as unknown as Query<ProjectSCIM>,
1234
+ { bearerToken: ObjectID.generate().toString() },
1235
+ ownerProps(),
1236
+ ),
1237
+ ).resolves.toEqual(query);
1238
+ });
1239
+
1240
+ test("a project owner cannot rotate the token and change anything else in one update", async () => {
1241
+ installFakeEnterpriseModule({
1242
+ snapshot: createLicenseSnapshotWithStatus("expired"),
1243
+ });
1244
+
1245
+ await expect(
1246
+ UpdatePermission.checkUpdatePermissions(
1247
+ ProjectSCIM,
1248
+ query as unknown as Query<ProjectSCIM>,
1249
+ {
1250
+ bearerToken: ObjectID.generate().toString(),
1251
+ autoProvisionUsers: true,
1252
+ },
1253
+ ownerProps(),
1254
+ ),
1255
+ ).rejects.toThrow(PaymentRequiredException);
1256
+ });
1257
+
1258
+ /*
1259
+ * The allowance is about the license, never about who may write: a
1260
+ * caller without update permission is still turned away.
1261
+ */
1262
+ test("switching a provider off still needs permission to update it", async () => {
1263
+ installFakeEnterpriseModule({
1264
+ snapshot: createLicenseSnapshotWithStatus("expired"),
1265
+ });
1266
+
1267
+ await expect(
1268
+ UpdatePermission.checkUpdatePermissions(
1269
+ ProjectSSO,
1270
+ query,
1271
+ { isEnabled: false },
1272
+ buildUserProps([Permission.ProjectMember]),
1273
+ ),
1274
+ ).rejects.toThrow(NotAuthorizedException);
1275
+ });
1276
+
1277
+ test("a signed-out caller gets 'not logged in' before any edition answer", async () => {
1278
+ await expect(
1279
+ UpdatePermission.checkUpdatePermissions(
1280
+ ProjectSSO,
1281
+ query,
1282
+ { name: "Okta (renamed)" },
1283
+ {},
1284
+ ),
1285
+ ).rejects.toThrow(NotAuthenticatedException);
1286
+ });
1287
+
1288
+ test("a master admin can switch a global provider off with an expired license, and on the Community Edition", async () => {
1289
+ const globalQuery: Query<GlobalSSO> = {
1290
+ name: "Okta",
1291
+ } as Query<GlobalSSO>;
1292
+
1293
+ await expect(
1294
+ UpdatePermission.checkUpdatePermissions(
1295
+ GlobalSSO,
1296
+ globalQuery,
1297
+ { isEnabled: false },
1298
+ { userId, isMasterAdmin: true },
1299
+ ),
1300
+ ).resolves.toBe(globalQuery);
1301
+
1302
+ installFakeEnterpriseModule({
1303
+ snapshot: createLicenseSnapshotWithStatus("expired"),
1304
+ });
1305
+
1306
+ await expect(
1307
+ UpdatePermission.checkUpdatePermissions(
1308
+ GlobalSSO,
1309
+ globalQuery,
1310
+ { isEnabled: false },
1311
+ { userId, isMasterAdmin: true },
1312
+ ),
1313
+ ).resolves.toBe(globalQuery);
1314
+ await expect(
1315
+ UpdatePermission.checkUpdatePermissions(
1316
+ GlobalOIDC,
1317
+ globalQuery as unknown as Query<GlobalOIDC>,
1318
+ { isEnabled: false, restrictToAttachedProjects: false },
1319
+ { userId, isMasterAdmin: true },
1320
+ ),
1321
+ ).rejects.toThrow(PaymentRequiredException);
1322
+ });
1323
+
1324
+ test("the data reaches the edition check unchanged", async () => {
1325
+ installFakeEnterpriseModule();
1326
+
1327
+ const editionCheck: ReturnType<typeof jest.spyOn> = jest.spyOn(
1328
+ EditionPermissions,
1329
+ "checkEditionPermissions",
1330
+ );
1331
+ const data: { isEnabled: boolean } = { isEnabled: false };
1332
+
1333
+ await UpdatePermission.checkUpdatePermissions(
1334
+ ProjectSSO,
1335
+ query,
1336
+ data,
1337
+ ownerProps(),
1338
+ );
1339
+
1340
+ const calls: Array<Array<unknown>> = editionCheck.mock.calls as Array<
1341
+ Array<unknown>
1342
+ >;
1343
+
1344
+ expect(calls).toHaveLength(1);
1345
+ expect(calls[0]![2]).toBe(DatabaseRequestType.Update);
1346
+ expect(calls[0]![3]).toBe(data);
1347
+ });
1348
+ });
1349
+ });
1350
+
1351
+ /*
1352
+ * Tighten-only updates: the incident-response moves (disable an identity
1353
+ * provider, rotate a leaked SCIM bearer token) need no license. Everything
1354
+ * else about enterprise configuration still does.
1355
+ */
1356
+ const IDENTITY_PROVIDER_MODELS: ReadonlyArray<[string, ModelType]> = [
1357
+ ["GlobalSSO", GlobalSSO],
1358
+ ["GlobalOIDC", GlobalOIDC],
1359
+ ["GlobalSSOProject", GlobalSSOProject],
1360
+ ["GlobalOIDCProject", GlobalOIDCProject],
1361
+ ["ProjectSSO", ProjectSSO],
1362
+ ["ProjectOIDC", ProjectOIDC],
1363
+ ["StatusPageSSO", StatusPageSSO],
1364
+ ["StatusPageOIDC", StatusPageOIDC],
1365
+ ];
1366
+
1367
+ const SCIM_MODELS: ReadonlyArray<[string, ModelType]> = [
1368
+ ["ProjectSCIM", ProjectSCIM],
1369
+ ["StatusPageSCIM", StatusPageSCIM],
1370
+ ];
1371
+
1372
+ const newBearerToken: () => string = (): string => {
1373
+ return ObjectID.generate().toString();
1374
+ };
1375
+
1376
+ const runUpdateCheck: (input: {
1377
+ modelType: ModelType;
1378
+ caller: Caller;
1379
+ data: unknown;
1380
+ }) => Outcome = (input: {
1381
+ modelType: ModelType;
1382
+ caller: Caller;
1383
+ data: unknown;
1384
+ }): Outcome => {
1385
+ try {
1386
+ EditionPermissions.checkEditionPermissions(
1387
+ input.modelType,
1388
+ propsFor(input.caller),
1389
+ DatabaseRequestType.Update,
1390
+ input.data,
1391
+ );
1392
+
1393
+ return "allowed";
1394
+ } catch (err) {
1395
+ if (!(err instanceof PaymentRequiredException)) {
1396
+ throw err;
1397
+ }
1398
+
1399
+ return err.message === EnterpriseEdition.COMMUNITY_EDITION_MESSAGE
1400
+ ? "community"
1401
+ : "license";
1402
+ }
1403
+ };
1404
+
1405
+ describe("EditionPermission: tighten-only updates", () => {
1406
+ beforeEach(() => {
1407
+ setTestBillingEnabled(false);
1408
+ uninstallEnterpriseModule();
1409
+ });
1410
+
1411
+ afterEach(() => {
1412
+ uninstallEnterpriseModule();
1413
+ setTestBillingEnabled(false);
1414
+ jest.restoreAllMocks();
1415
+ });
1416
+
1417
+ describe("isTightenOnlyUpdate", () => {
1418
+ test.each(IDENTITY_PROVIDER_MODELS)(
1419
+ "%s: only isEnabled=false is tighten-only",
1420
+ (_name: string, modelType: ModelType) => {
1421
+ const tableName: string = new modelType().tableName!;
1422
+
1423
+ expect(
1424
+ EditionPermissions.isTightenOnlyUpdate(tableName, {
1425
+ isEnabled: false,
1426
+ }),
1427
+ ).toBe(true);
1428
+
1429
+ for (const value of [true, "false", 0, null, "", {}]) {
1430
+ expect(
1431
+ EditionPermissions.isTightenOnlyUpdate(tableName, {
1432
+ isEnabled: value,
1433
+ }),
1434
+ ).toBe(false);
1435
+ }
1436
+
1437
+ // Another model's tighten-only column is not this model's.
1438
+ expect(
1439
+ EditionPermissions.isTightenOnlyUpdate(tableName, {
1440
+ bearerToken: newBearerToken(),
1441
+ }),
1442
+ ).toBe(false);
1443
+ },
1444
+ );
1445
+
1446
+ test.each(SCIM_MODELS)(
1447
+ "%s: only a new, long bearerToken is tighten-only",
1448
+ (_name: string, modelType: ModelType) => {
1449
+ const tableName: string = new modelType().tableName!;
1450
+
1451
+ expect(
1452
+ EditionPermissions.isTightenOnlyUpdate(tableName, {
1453
+ bearerToken: newBearerToken(),
1454
+ }),
1455
+ ).toBe(true);
1456
+ expect(
1457
+ EditionPermissions.isTightenOnlyUpdate(tableName, {
1458
+ bearerToken: "a".repeat(MIN_ROTATED_SCIM_BEARER_TOKEN_LENGTH),
1459
+ }),
1460
+ ).toBe(true);
1461
+
1462
+ for (const value of [
1463
+ "a".repeat(MIN_ROTATED_SCIM_BEARER_TOKEN_LENGTH - 1),
1464
+ ` ${"a".repeat(MIN_ROTATED_SCIM_BEARER_TOKEN_LENGTH - 1)} `,
1465
+ "",
1466
+ null,
1467
+ 12345,
1468
+ { token: newBearerToken() },
1469
+ ]) {
1470
+ expect(
1471
+ EditionPermissions.isTightenOnlyUpdate(tableName, {
1472
+ bearerToken: value,
1473
+ }),
1474
+ ).toBe(false);
1475
+ }
1476
+
1477
+ expect(
1478
+ EditionPermissions.isTightenOnlyUpdate(tableName, {
1479
+ isEnabled: false,
1480
+ }),
1481
+ ).toBe(false);
1482
+ },
1483
+ );
1484
+
1485
+ test("one more column makes it an ordinary update", () => {
1486
+ expect(
1487
+ EditionPermissions.isTightenOnlyUpdate("ProjectSSO", {
1488
+ isEnabled: false,
1489
+ name: "Okta",
1490
+ }),
1491
+ ).toBe(false);
1492
+ expect(
1493
+ EditionPermissions.isTightenOnlyUpdate("ProjectOIDC", {
1494
+ isEnabled: false,
1495
+ clientSecret: "new-secret",
1496
+ }),
1497
+ ).toBe(false);
1498
+ expect(
1499
+ EditionPermissions.isTightenOnlyUpdate("ProjectSCIM", {
1500
+ bearerToken: newBearerToken(),
1501
+ enablePushGroups: true,
1502
+ }),
1503
+ ).toBe(false);
1504
+ });
1505
+
1506
+ test("columns set to undefined are not written, so they do not count", () => {
1507
+ expect(
1508
+ EditionPermissions.isTightenOnlyUpdate("ProjectSSO", {
1509
+ isEnabled: false,
1510
+ name: undefined,
1511
+ }),
1512
+ ).toBe(true);
1513
+ });
1514
+
1515
+ test("an update that writes nothing, or is not an object of columns, is not tighten-only", () => {
1516
+ for (const data of [
1517
+ undefined,
1518
+ null,
1519
+ {},
1520
+ { name: undefined },
1521
+ [],
1522
+ [{ isEnabled: false }],
1523
+ "isEnabled=false",
1524
+ false,
1525
+ ]) {
1526
+ expect(EditionPermissions.isTightenOnlyUpdate("ProjectSSO", data)).toBe(
1527
+ false,
1528
+ );
1529
+ }
1530
+ });
1531
+
1532
+ /*
1533
+ * Object's own members are not rules: an update naming them must not be
1534
+ * judged by Object.prototype.constructor or toString.
1535
+ */
1536
+ test("inherited object members are never treated as rules", () => {
1537
+ for (const column of [
1538
+ "constructor",
1539
+ "toString",
1540
+ "hasOwnProperty",
1541
+ "__proto__",
1542
+ ]) {
1543
+ const data: Record<string, unknown> = JSON.parse(
1544
+ `{"isEnabled": false, "${column}": false}`,
1545
+ );
1546
+
1547
+ expect(EditionPermissions.isTightenOnlyUpdate("ProjectSSO", data)).toBe(
1548
+ false,
1549
+ );
1550
+ }
1551
+ });
1552
+
1553
+ test("team compliance has no tighten-only update: switching a rule off relaxes it", () => {
1554
+ expect(
1555
+ EditionPermissions.isTightenOnlyUpdate("TeamComplianceSetting", {
1556
+ enabled: false,
1557
+ }),
1558
+ ).toBe(false);
1559
+ expect(
1560
+ EditionPermissions.isTightenOnlyUpdate("TeamComplianceSetting", {
1561
+ isEnabled: false,
1562
+ }),
1563
+ ).toBe(false);
1564
+ });
1565
+
1566
+ test("tables that are not listed have none", () => {
1567
+ for (const tableName of ["Monitor", "Project", "", null, undefined]) {
1568
+ expect(
1569
+ EditionPermissions.isTightenOnlyUpdate(tableName, {
1570
+ isEnabled: false,
1571
+ }),
1572
+ ).toBe(false);
1573
+ }
1574
+ });
1575
+ });
1576
+
1577
+ describe("the list itself", () => {
1578
+ test("covers exactly the identity provider and SCIM models", () => {
1579
+ expect(
1580
+ Array.from(EditionPermissions.getTightenOnlyColumns().keys()).sort(),
1581
+ ).toEqual(
1582
+ [...IDENTITY_PROVIDER_MODELS, ...SCIM_MODELS]
1583
+ .map(([name]: [string, ModelType]): string => {
1584
+ return name;
1585
+ })
1586
+ .sort(),
1587
+ );
1588
+ });
1589
+
1590
+ test("every listed table is an enterprise model, and every listed column is a real column of it", () => {
1591
+ const byTableName: Map<string, ModelType> = new Map<string, ModelType>(
1592
+ ENTERPRISE_MODELS.map(
1593
+ ([, modelType]: [string, ModelType, EnterpriseFeature]): [
1594
+ string,
1595
+ ModelType,
1596
+ ] => {
1597
+ return [new modelType().tableName!, modelType];
1598
+ },
1599
+ ),
1600
+ );
1601
+
1602
+ for (const [
1603
+ tableName,
1604
+ columns,
1605
+ ] of EditionPermissions.getTightenOnlyColumns()) {
1606
+ const modelType: ModelType | undefined = byTableName.get(tableName);
1607
+
1608
+ expect(modelType).toBeDefined();
1609
+
1610
+ const model: BaseModel = new modelType!();
1611
+
1612
+ expect(model.requiresEnterprise).toBe(true);
1613
+ expect(columns.length).toBeGreaterThan(0);
1614
+
1615
+ for (const column of columns) {
1616
+ expect(model.isTableColumn(column)).toBe(true);
1617
+ }
1618
+ }
1619
+ });
1620
+ });
1621
+
1622
+ describe("checkEditionPermissions with the update's data", () => {
1623
+ const UNAVAILABLE_STATES: ReadonlyArray<[string, () => void]> = [
1624
+ [
1625
+ "the Community Edition",
1626
+ (): void => {
1627
+ uninstallEnterpriseModule();
1628
+ },
1629
+ ],
1630
+ ...UNUSABLE_STATUSES.map(
1631
+ (status: EnterpriseLicenseStatus): [string, () => void] => {
1632
+ return [
1633
+ `a ${status} license`,
1634
+ (): void => {
1635
+ installFakeEnterpriseModule({
1636
+ snapshot: createLicenseSnapshotWithStatus(status),
1637
+ });
1638
+ },
1639
+ ];
1640
+ },
1641
+ ),
1642
+ [
1643
+ "no license snapshot yet",
1644
+ (): void => {
1645
+ installFakeEnterpriseModule({ snapshot: null });
1646
+ },
1647
+ ],
1648
+ [
1649
+ "a license without SSO, SCIM or team compliance",
1650
+ (): void => {
1651
+ installFakeEnterpriseModule({
1652
+ snapshot: createLicenseSnapshot({
1653
+ features: [EnterpriseFeature.AuditLogs],
1654
+ }),
1655
+ });
1656
+ },
1657
+ ],
1658
+ ];
1659
+
1660
+ test.each(UNAVAILABLE_STATES)(
1661
+ "with %s, switching any identity provider off is allowed, for users and master admins",
1662
+ (_state: string, install: () => void) => {
1663
+ install();
1664
+
1665
+ for (const [, modelType] of IDENTITY_PROVIDER_MODELS) {
1666
+ for (const caller of [
1667
+ "regular user",
1668
+ "master admin",
1669
+ ] as Array<Caller>) {
1670
+ expect(
1671
+ runUpdateCheck({
1672
+ modelType,
1673
+ caller,
1674
+ data: { isEnabled: false },
1675
+ }),
1676
+ ).toBe("allowed");
1677
+ }
1678
+ }
1679
+ },
1680
+ );
1681
+
1682
+ test.each(UNAVAILABLE_STATES)(
1683
+ "with %s, rotating a SCIM bearer token is allowed",
1684
+ (_state: string, install: () => void) => {
1685
+ install();
1686
+
1687
+ for (const [, modelType] of SCIM_MODELS) {
1688
+ expect(
1689
+ runUpdateCheck({
1690
+ modelType,
1691
+ caller: "regular user",
1692
+ data: { bearerToken: newBearerToken() },
1693
+ }),
1694
+ ).toBe("allowed");
1695
+ }
1696
+ },
1697
+ );
1698
+
1699
+ test.each(UNAVAILABLE_STATES)(
1700
+ "with %s, everything else is still refused",
1701
+ (_state: string, install: () => void) => {
1702
+ install();
1703
+
1704
+ const expected: Outcome = EnterpriseEdition.isLoaded()
1705
+ ? "license"
1706
+ : "community";
1707
+
1708
+ for (const [, modelType] of IDENTITY_PROVIDER_MODELS) {
1709
+ for (const data of [
1710
+ { isEnabled: true },
1711
+ { isEnabled: false, name: "Okta" },
1712
+ { name: "Okta" },
1713
+ {},
1714
+ undefined,
1715
+ ]) {
1716
+ expect(
1717
+ runUpdateCheck({ modelType, caller: "regular user", data }),
1718
+ ).toBe(expected);
1719
+ }
1720
+ }
1721
+
1722
+ for (const [, modelType] of SCIM_MODELS) {
1723
+ for (const data of [
1724
+ { bearerToken: "short" },
1725
+ { bearerToken: newBearerToken(), autoDeprovisionUsers: false },
1726
+ { autoProvisionUsers: false },
1727
+ ]) {
1728
+ expect(
1729
+ runUpdateCheck({ modelType, caller: "master admin", data }),
1730
+ ).toBe(expected);
1731
+ }
1732
+ }
1733
+
1734
+ expect(
1735
+ runUpdateCheck({
1736
+ modelType: TeamComplianceSetting,
1737
+ caller: "regular user",
1738
+ data: { enabled: false },
1739
+ }),
1740
+ ).toBe(expected);
1741
+ },
1742
+ );
1743
+
1744
+ // Tighten-only is about updates: a create is configuration, whatever it holds.
1745
+ test("a create is never tighten-only", () => {
1746
+ installFakeEnterpriseModule({
1747
+ snapshot: createLicenseSnapshotWithStatus("expired"),
1748
+ });
1749
+
1750
+ expect(() => {
1751
+ EditionPermissions.checkEditionPermissions(
1752
+ ProjectSSO,
1753
+ propsFor("regular user"),
1754
+ DatabaseRequestType.Create,
1755
+ { isEnabled: false },
1756
+ );
1757
+ }).toThrow(
1758
+ new PaymentRequiredException(
1759
+ EnterpriseEdition.LICENSE_REQUIRED_MESSAGE,
1760
+ ),
1761
+ );
1762
+ });
1763
+
1764
+ test("without the data an update gets the full check (fail closed)", () => {
1765
+ installFakeEnterpriseModule({
1766
+ snapshot: createLicenseSnapshotWithStatus("expired"),
1767
+ });
1768
+
1769
+ expect(
1770
+ runUpdateCheck({
1771
+ modelType: ProjectSSO,
1772
+ caller: "regular user",
1773
+ data: undefined,
1774
+ }),
1775
+ ).toBe("license");
1776
+ });
1777
+
1778
+ test("a usable license, billing and root writes are unaffected", () => {
1779
+ installFakeEnterpriseModule();
1780
+
1781
+ expect(
1782
+ runUpdateCheck({
1783
+ modelType: ProjectSSO,
1784
+ caller: "regular user",
1785
+ data: { isEnabled: true, name: "Okta" },
1786
+ }),
1787
+ ).toBe("allowed");
1788
+
1789
+ uninstallEnterpriseModule();
1790
+ setTestBillingEnabled(true);
1791
+
1792
+ expect(
1793
+ runUpdateCheck({
1794
+ modelType: ProjectSSO,
1795
+ caller: "regular user",
1796
+ data: { isEnabled: true },
1797
+ }),
1798
+ ).toBe("allowed");
1799
+
1800
+ setTestBillingEnabled(false);
1801
+
1802
+ expect(
1803
+ runUpdateCheck({
1804
+ modelType: ProjectSSO,
1805
+ caller: "root",
1806
+ data: { isEnabled: true },
1807
+ }),
1808
+ ).toBe("allowed");
1809
+ });
1810
+
1811
+ test("ordinary models are unaffected by the data", () => {
1812
+ for (const [, modelType] of ORDINARY_MODELS) {
1813
+ expect(
1814
+ runUpdateCheck({
1815
+ modelType,
1816
+ caller: "regular user",
1817
+ data: { isEnabled: true },
1818
+ }),
1819
+ ).toBe("allowed");
1820
+ }
1821
+ });
1822
+ });
1823
+ });