@oneuptime/common 12.0.0 → 12.0.2

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 (237) hide show
  1. package/Models/DatabaseModels/Host.ts +12 -4
  2. package/Models/DatabaseModels/KubernetesContainer.ts +5 -4
  3. package/Models/DatabaseModels/KubernetesResource.ts +8 -7
  4. package/Models/DatabaseModels/OnCallDutyPolicyScheduleLayer.ts +12 -2
  5. package/Models/DatabaseModels/PodmanResource.ts +6 -6
  6. package/Models/DatabaseModels/ServiceLevelObjective.ts +91 -0
  7. package/Models/DatabaseModels/StatusPage.ts +95 -0
  8. package/Server/API/TeamMemberAPI.ts +85 -0
  9. package/Server/Infrastructure/Postgres/LocalMigrationGenerationDataSource.ts +12 -2
  10. package/Server/Infrastructure/Postgres/SchemaMigrations/1785900000000-AddSloMonitorLabelRule.ts +84 -0
  11. package/Server/Infrastructure/Postgres/SchemaMigrations/1785915638551-IncreaseHostIpAddressesLength.ts +40 -0
  12. package/Server/Infrastructure/Postgres/SchemaMigrations/1785930000000-WidenInventoryResourceNameColumns.ts +131 -0
  13. package/Server/Infrastructure/Postgres/SchemaMigrations/1785930709405-AddStatusPageReportPeriod.ts +25 -0
  14. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +8 -0
  15. package/Server/Middleware/ProjectAuthorization.ts +66 -4
  16. package/Server/Services/BillingService.ts +600 -51
  17. package/Server/Services/CephClusterService.ts +47 -5
  18. package/Server/Services/CephResourceService.ts +43 -4
  19. package/Server/Services/CloudResourceService.ts +47 -5
  20. package/Server/Services/DatabaseService.ts +87 -6
  21. package/Server/Services/DockerHostService.ts +47 -5
  22. package/Server/Services/DockerResourceService.ts +62 -8
  23. package/Server/Services/DockerSwarmClusterService.ts +48 -5
  24. package/Server/Services/DockerSwarmResourceService.ts +53 -4
  25. package/Server/Services/HostService.ts +48 -5
  26. package/Server/Services/IoTDeviceService.ts +1 -17
  27. package/Server/Services/IoTFleetService.ts +47 -5
  28. package/Server/Services/KubernetesClusterService.ts +48 -5
  29. package/Server/Services/KubernetesContainerService.ts +50 -9
  30. package/Server/Services/KubernetesResourceService.ts +53 -7
  31. package/Server/Services/LabelService.ts +106 -1
  32. package/Server/Services/LlmProviderService.ts +69 -9
  33. package/Server/Services/MonitorService.ts +57 -0
  34. package/Server/Services/PodmanHostService.ts +47 -5
  35. package/Server/Services/PodmanResourceService.ts +62 -8
  36. package/Server/Services/ProjectService.ts +165 -35
  37. package/Server/Services/ProxmoxClusterService.ts +47 -5
  38. package/Server/Services/ProxmoxResourceService.ts +43 -4
  39. package/Server/Services/RumApplicationService.ts +48 -5
  40. package/Server/Services/RunnerService.ts +1 -7
  41. package/Server/Services/ServerlessFunctionService.ts +48 -5
  42. package/Server/Services/ServiceLevelObjectiveMonitorRuleEngineService.ts +426 -0
  43. package/Server/Services/ServiceLevelObjectiveService.ts +39 -0
  44. package/Server/Services/StatusPageService.ts +95 -36
  45. package/Server/Services/StatusPageSubscriberNotificationTemplateService.ts +18 -1
  46. package/Server/Services/StatusPageSubscriberService.ts +3 -0
  47. package/Server/Types/Database/JSONColumnQuery.ts +621 -0
  48. package/Server/Types/Database/Permissions/PublicPermission.ts +9 -2
  49. package/Server/Types/Database/QueryHelper.ts +17 -32
  50. package/Server/Utils/Database/TruncateColumnValue.ts +84 -0
  51. package/Server/Utils/Monitor/Criteria/CompareCriteria.ts +12 -0
  52. package/Tests/App/Dashboard/RunnerInstallInstructions.test.tsx +119 -0
  53. package/Tests/App/Dashboard/RunnerStatus.test.tsx +274 -0
  54. package/Tests/App/Dashboard/UsersTableGroupsByPerson.test.tsx +448 -0
  55. package/Tests/Server/API/BaseAPIApiKeyAuth.test.ts +704 -0
  56. package/Tests/Server/API/TeamMemberRemoveUserFromProjectAPI.test.ts +303 -0
  57. package/Tests/Server/Middleware/MasterAdminAuthorization.test.ts +350 -0
  58. package/Tests/Server/Middleware/ProjectAuthorizationApiKeyHeader.test.ts +362 -0
  59. package/Tests/Server/Middleware/ProjectAuthorizationApiKeyMiddleware.test.ts +687 -0
  60. package/Tests/Server/Services/BillingService.test.ts +78 -3
  61. package/Tests/Server/Services/BillingServiceChangePlanCancel.test.ts +645 -0
  62. package/Tests/Server/Services/BillingServiceTrialPlanChange.test.ts +1005 -0
  63. package/Tests/Server/Services/HookFreeWriteLengthClamp.test.ts +503 -0
  64. package/Tests/Server/Services/HostIpAddressesColumnWidth.test.ts +198 -0
  65. package/Tests/Server/Services/HostServiceUpdateLastSeen.test.ts +510 -0
  66. package/Tests/Server/Services/InventoryResourceNameColumnWidth.test.ts +500 -0
  67. package/Tests/Server/Services/InventoryUpsertSchemaParity.test.ts +586 -0
  68. package/Tests/Server/Services/InventoryUpsertTruncationGuard.test.ts +1242 -0
  69. package/Tests/Server/Services/LlmProviderUsableByProject.test.ts +293 -0
  70. package/Tests/Server/Services/ProjectServiceChangePlan.test.ts +650 -0
  71. package/Tests/Server/Services/ProjectServiceExtendTrial.test.ts +1 -0
  72. package/Tests/Server/Services/ResourceUpdateLastSeenLivenessFallback.test.ts +387 -0
  73. package/Tests/Server/Services/ServiceLevelObjectiveMonitorRuleEngineService.test.ts +770 -0
  74. package/Tests/Server/Services/ServiceLevelObjectiveService.test.ts +130 -0
  75. package/Tests/Server/Services/SloMonitorLabelRuleHooks.test.ts +352 -0
  76. package/Tests/Server/Services/StatusPageSubscriberReport.test.ts +29 -13
  77. package/Tests/Server/TestingUtils/Services/BillingServiceHelper.ts +69 -8
  78. package/Tests/Server/Types/Database/JSONColumnQuery.test.ts +593 -0
  79. package/Tests/Server/Types/Database/Permissions/PublicPermission.test.ts +495 -0
  80. package/Tests/Server/Types/Database/QueryUtil.test.ts +113 -0
  81. package/Tests/Server/Utils/AI/ToolArgsExtractors.test.ts +141 -0
  82. package/Tests/Server/Utils/AI/Toolbox/WidgetBuilder.test.ts +205 -0
  83. package/Tests/Server/Utils/Database/TruncateColumnValue.test.ts +179 -0
  84. package/Tests/Server/Utils/Monitor/Criteria/CompareCriteria.test.ts +897 -0
  85. package/Tests/Types/Events/Recurring.test.ts +157 -0
  86. package/Tests/Types/Runner/RunnerLiveStatus.test.ts +320 -0
  87. package/Tests/UI/Components/AiInvestigationSettingsCard.test.tsx +285 -0
  88. package/Tests/UI/Components/Detail/EntityFields.test.tsx +166 -0
  89. package/Tests/UI/Components/ModelTable/ModelTableGroupsProjectUsers.test.tsx +375 -0
  90. package/Tests/UI/Components/ModelTable/useCustomFieldColumns.test.tsx +196 -0
  91. package/Tests/UI/Utils/ProjectUsersModelAPI.test.ts +739 -0
  92. package/Tests/UI/Utils/TeamMembersByUser.test.ts +633 -0
  93. package/Tests/Utils/StatusPage/ReportPeriod.test.ts +218 -0
  94. package/Tests/Utils/Telemetry/HostIpAddresses.test.ts +291 -0
  95. package/Types/CustomField/CustomFieldDefinition.ts +24 -0
  96. package/Types/Date.ts +112 -0
  97. package/Types/Events/Recurring.ts +99 -1
  98. package/Types/Runbook/RunbookStep.ts +15 -0
  99. package/Types/Runner/RunnerLiveStatus.ts +114 -0
  100. package/Types/StatusPage/StatusPageReport.ts +11 -0
  101. package/Types/StatusPage/StatusPageReportPeriodType.ts +21 -0
  102. package/UI/Components/Detail/Detail.tsx +155 -0
  103. package/UI/Components/ModelTable/CustomFieldColumns.tsx +2 -6
  104. package/UI/Components/ModelTable/useCustomFieldColumns.ts +21 -2
  105. package/UI/Utils/ModelAPI/ProjectUsersModelAPI.ts +305 -0
  106. package/UI/Utils/TeamMembersByUser.ts +302 -0
  107. package/Utils/StatusPage/ReportPeriod.ts +236 -0
  108. package/Utils/Telemetry/HostIpAddresses.ts +98 -0
  109. package/build/dist/Models/DatabaseModels/Host.js +12 -4
  110. package/build/dist/Models/DatabaseModels/Host.js.map +1 -1
  111. package/build/dist/Models/DatabaseModels/KubernetesContainer.js +4 -4
  112. package/build/dist/Models/DatabaseModels/KubernetesContainer.js.map +1 -1
  113. package/build/dist/Models/DatabaseModels/KubernetesResource.js +7 -7
  114. package/build/dist/Models/DatabaseModels/KubernetesResource.js.map +1 -1
  115. package/build/dist/Models/DatabaseModels/OnCallDutyPolicyScheduleLayer.js +16 -4
  116. package/build/dist/Models/DatabaseModels/OnCallDutyPolicyScheduleLayer.js.map +1 -1
  117. package/build/dist/Models/DatabaseModels/PodmanResource.js +6 -6
  118. package/build/dist/Models/DatabaseModels/PodmanResource.js.map +1 -1
  119. package/build/dist/Models/DatabaseModels/ServiceLevelObjective.js +87 -0
  120. package/build/dist/Models/DatabaseModels/ServiceLevelObjective.js.map +1 -1
  121. package/build/dist/Models/DatabaseModels/StatusPage.js +98 -0
  122. package/build/dist/Models/DatabaseModels/StatusPage.js.map +1 -1
  123. package/build/dist/Server/API/TeamMemberAPI.js +57 -2
  124. package/build/dist/Server/API/TeamMemberAPI.js.map +1 -1
  125. package/build/dist/Server/Infrastructure/Postgres/LocalMigrationGenerationDataSource.js +11 -1
  126. package/build/dist/Server/Infrastructure/Postgres/LocalMigrationGenerationDataSource.js.map +1 -1
  127. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1785900000000-AddSloMonitorLabelRule.js +45 -0
  128. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1785900000000-AddSloMonitorLabelRule.js.map +1 -0
  129. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1785915638551-IncreaseHostIpAddressesLength.js +31 -0
  130. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1785915638551-IncreaseHostIpAddressesLength.js.map +1 -0
  131. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1785930000000-WidenInventoryResourceNameColumns.js +90 -0
  132. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1785930000000-WidenInventoryResourceNameColumns.js.map +1 -0
  133. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1785930709405-AddStatusPageReportPeriod.js +14 -0
  134. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1785930709405-AddStatusPageReportPeriod.js.map +1 -0
  135. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +8 -0
  136. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  137. package/build/dist/Server/Middleware/ProjectAuthorization.js +59 -4
  138. package/build/dist/Server/Middleware/ProjectAuthorization.js.map +1 -1
  139. package/build/dist/Server/Services/BillingService.js +466 -36
  140. package/build/dist/Server/Services/BillingService.js.map +1 -1
  141. package/build/dist/Server/Services/CephClusterService.js +42 -5
  142. package/build/dist/Server/Services/CephClusterService.js.map +1 -1
  143. package/build/dist/Server/Services/CephResourceService.js +28 -3
  144. package/build/dist/Server/Services/CephResourceService.js.map +1 -1
  145. package/build/dist/Server/Services/CloudResourceService.js +42 -5
  146. package/build/dist/Server/Services/CloudResourceService.js.map +1 -1
  147. package/build/dist/Server/Services/DatabaseService.js +69 -2
  148. package/build/dist/Server/Services/DatabaseService.js.map +1 -1
  149. package/build/dist/Server/Services/DockerHostService.js +42 -5
  150. package/build/dist/Server/Services/DockerHostService.js.map +1 -1
  151. package/build/dist/Server/Services/DockerResourceService.js +34 -4
  152. package/build/dist/Server/Services/DockerResourceService.js.map +1 -1
  153. package/build/dist/Server/Services/DockerSwarmClusterService.js +43 -5
  154. package/build/dist/Server/Services/DockerSwarmClusterService.js.map +1 -1
  155. package/build/dist/Server/Services/DockerSwarmResourceService.js +28 -3
  156. package/build/dist/Server/Services/DockerSwarmResourceService.js.map +1 -1
  157. package/build/dist/Server/Services/HostService.js +43 -5
  158. package/build/dist/Server/Services/HostService.js.map +1 -1
  159. package/build/dist/Server/Services/IoTDeviceService.js +1 -8
  160. package/build/dist/Server/Services/IoTDeviceService.js.map +1 -1
  161. package/build/dist/Server/Services/IoTFleetService.js +42 -5
  162. package/build/dist/Server/Services/IoTFleetService.js.map +1 -1
  163. package/build/dist/Server/Services/KubernetesClusterService.js +43 -5
  164. package/build/dist/Server/Services/KubernetesClusterService.js.map +1 -1
  165. package/build/dist/Server/Services/KubernetesContainerService.js +29 -3
  166. package/build/dist/Server/Services/KubernetesContainerService.js.map +1 -1
  167. package/build/dist/Server/Services/KubernetesResourceService.js +32 -4
  168. package/build/dist/Server/Services/KubernetesResourceService.js.map +1 -1
  169. package/build/dist/Server/Services/LabelService.js +92 -0
  170. package/build/dist/Server/Services/LabelService.js.map +1 -1
  171. package/build/dist/Server/Services/LlmProviderService.js +61 -8
  172. package/build/dist/Server/Services/LlmProviderService.js.map +1 -1
  173. package/build/dist/Server/Services/MonitorService.js +47 -0
  174. package/build/dist/Server/Services/MonitorService.js.map +1 -1
  175. package/build/dist/Server/Services/PodmanHostService.js +42 -5
  176. package/build/dist/Server/Services/PodmanHostService.js.map +1 -1
  177. package/build/dist/Server/Services/PodmanResourceService.js +34 -4
  178. package/build/dist/Server/Services/PodmanResourceService.js.map +1 -1
  179. package/build/dist/Server/Services/ProjectService.js +135 -21
  180. package/build/dist/Server/Services/ProjectService.js.map +1 -1
  181. package/build/dist/Server/Services/ProxmoxClusterService.js +42 -5
  182. package/build/dist/Server/Services/ProxmoxClusterService.js.map +1 -1
  183. package/build/dist/Server/Services/ProxmoxResourceService.js +28 -3
  184. package/build/dist/Server/Services/ProxmoxResourceService.js.map +1 -1
  185. package/build/dist/Server/Services/RumApplicationService.js +43 -5
  186. package/build/dist/Server/Services/RumApplicationService.js.map +1 -1
  187. package/build/dist/Server/Services/RunnerService.js +1 -6
  188. package/build/dist/Server/Services/RunnerService.js.map +1 -1
  189. package/build/dist/Server/Services/ServerlessFunctionService.js +43 -5
  190. package/build/dist/Server/Services/ServerlessFunctionService.js.map +1 -1
  191. package/build/dist/Server/Services/ServiceLevelObjectiveMonitorRuleEngineService.js +347 -0
  192. package/build/dist/Server/Services/ServiceLevelObjectiveMonitorRuleEngineService.js.map +1 -0
  193. package/build/dist/Server/Services/ServiceLevelObjectiveService.js +33 -1
  194. package/build/dist/Server/Services/ServiceLevelObjectiveService.js.map +1 -1
  195. package/build/dist/Server/Services/StatusPageService.js +69 -46
  196. package/build/dist/Server/Services/StatusPageService.js.map +1 -1
  197. package/build/dist/Server/Services/StatusPageSubscriberNotificationTemplateService.js +17 -1
  198. package/build/dist/Server/Services/StatusPageSubscriberNotificationTemplateService.js.map +1 -1
  199. package/build/dist/Server/Services/StatusPageSubscriberService.js +3 -0
  200. package/build/dist/Server/Services/StatusPageSubscriberService.js.map +1 -1
  201. package/build/dist/Server/Types/Database/JSONColumnQuery.js +402 -0
  202. package/build/dist/Server/Types/Database/JSONColumnQuery.js.map +1 -0
  203. package/build/dist/Server/Types/Database/Permissions/PublicPermission.js +9 -2
  204. package/build/dist/Server/Types/Database/Permissions/PublicPermission.js.map +1 -1
  205. package/build/dist/Server/Types/Database/QueryHelper.js +17 -27
  206. package/build/dist/Server/Types/Database/QueryHelper.js.map +1 -1
  207. package/build/dist/Server/Utils/Database/TruncateColumnValue.js +32 -0
  208. package/build/dist/Server/Utils/Database/TruncateColumnValue.js.map +1 -0
  209. package/build/dist/Server/Utils/Monitor/Criteria/CompareCriteria.js +11 -0
  210. package/build/dist/Server/Utils/Monitor/Criteria/CompareCriteria.js.map +1 -1
  211. package/build/dist/Types/CustomField/CustomFieldDefinition.js +2 -0
  212. package/build/dist/Types/CustomField/CustomFieldDefinition.js.map +1 -0
  213. package/build/dist/Types/Date.js +68 -0
  214. package/build/dist/Types/Date.js.map +1 -1
  215. package/build/dist/Types/Events/Recurring.js +63 -0
  216. package/build/dist/Types/Events/Recurring.js.map +1 -1
  217. package/build/dist/Types/Runbook/RunbookStep.js.map +1 -1
  218. package/build/dist/Types/Runner/RunnerLiveStatus.js +69 -0
  219. package/build/dist/Types/Runner/RunnerLiveStatus.js.map +1 -0
  220. package/build/dist/Types/StatusPage/StatusPageReportPeriodType.js +22 -0
  221. package/build/dist/Types/StatusPage/StatusPageReportPeriodType.js.map +1 -0
  222. package/build/dist/UI/Components/Detail/Detail.js +97 -0
  223. package/build/dist/UI/Components/Detail/Detail.js.map +1 -1
  224. package/build/dist/UI/Components/ModelTable/CustomFieldColumns.js.map +1 -1
  225. package/build/dist/UI/Components/ModelTable/useCustomFieldColumns.js +18 -2
  226. package/build/dist/UI/Components/ModelTable/useCustomFieldColumns.js.map +1 -1
  227. package/build/dist/UI/Utils/ModelAPI/ProjectUsersModelAPI.js +213 -0
  228. package/build/dist/UI/Utils/ModelAPI/ProjectUsersModelAPI.js.map +1 -0
  229. package/build/dist/UI/Utils/TeamMembersByUser.js +195 -0
  230. package/build/dist/UI/Utils/TeamMembersByUser.js.map +1 -0
  231. package/build/dist/Utils/StatusPage/ReportPeriod.js +131 -0
  232. package/build/dist/Utils/StatusPage/ReportPeriod.js.map +1 -0
  233. package/build/dist/Utils/Telemetry/HostIpAddresses.js +82 -0
  234. package/build/dist/Utils/Telemetry/HostIpAddresses.js.map +1 -0
  235. package/jest.config.json +2 -0
  236. package/package.json +1 -1
  237. package/tsconfig.json +12 -1
@@ -0,0 +1,495 @@
1
+ import ModelPermission from "../../../../../Server/Types/AnalyticsDatabase/ModelPermission";
2
+ import DatabaseRequestType from "../../../../../Server/Types/BaseDatabase/DatabaseRequestType";
3
+ import PublicPermission from "../../../../../Server/Types/Database/Permissions/PublicPermission";
4
+ import Log from "../../../../../Models/AnalyticsModels/Log";
5
+ import LogPipeline from "../../../../../Models/DatabaseModels/LogPipeline";
6
+ import Probe from "../../../../../Models/DatabaseModels/Probe";
7
+ import StatusPageSubscriber from "../../../../../Models/DatabaseModels/StatusPageSubscriber";
8
+ import DatabaseCommonInteractionProps from "../../../../../Types/BaseDatabase/DatabaseCommonInteractionProps";
9
+ import Exception from "../../../../../Types/Exception/Exception";
10
+ import ExceptionCode from "../../../../../Types/Exception/ExceptionCode";
11
+ import NotAuthenticatedException from "../../../../../Types/Exception/NotAuthenticatedException";
12
+ import ObjectID from "../../../../../Types/ObjectID";
13
+ import Permission from "../../../../../Types/Permission";
14
+ import UserType from "../../../../../Types/UserType";
15
+ import { describe, expect, it, jest } from "@jest/globals";
16
+
17
+ /*
18
+ * Every throw below is a deliberate, asserted-on throw, but @CaptureSpan on
19
+ * these methods hands each one to Telemetry, which logs the whole stack via
20
+ * Server/Utils/Logger. Auto-mocking Logger (the same thing
21
+ * Tests/Server/Middleware/UserAuthorization.test.ts does) keeps the run
22
+ * readable. Nothing under test is mocked: PublicPermission,
23
+ * AnalyticsDatabase/ModelPermission, TablePermission and the real models all
24
+ * run for real.
25
+ */
26
+ jest.mock("../../../../../Server/Utils/Logger");
27
+
28
+ /*
29
+ * PublicPermission is the very first gate every Postgres-backed read/write
30
+ * passes through, and until now nothing tested it directly — the only test
31
+ * that touched checkIfUserIsLoggedIn (BasePermission.test.ts) stubs it out.
32
+ *
33
+ * It is also the function that produced the 401 in GitHub issue #3004: a
34
+ * caller with a perfectly good project API key was told "A user should be
35
+ * logged in to read record of Log Pipeline." Their key had never reached the
36
+ * server, but the sentence talked only about sessions and named the model, so
37
+ * it read as an RBAC bug on Log Pipeline specifically. Issue #1754 was the
38
+ * same sentence on a different model (axios dropping the headers object).
39
+ * The wording is therefore load-bearing, and so is the API early return the
40
+ * reporter believed was broken. Both are pinned here.
41
+ */
42
+ describe("PublicPermission", () => {
43
+ const userId: ObjectID = ObjectID.generate();
44
+ const projectId: ObjectID = ObjectID.generate();
45
+
46
+ const allRequestTypes: Array<DatabaseRequestType> = [
47
+ DatabaseRequestType.Create,
48
+ DatabaseRequestType.Read,
49
+ DatabaseRequestType.Update,
50
+ DatabaseRequestType.Delete,
51
+ ];
52
+
53
+ // A caller carrying neither a session nor an API key.
54
+ function anonymousProps(): DatabaseCommonInteractionProps {
55
+ return {
56
+ tenantId: projectId,
57
+ userType: UserType.Public,
58
+ };
59
+ }
60
+
61
+ /*
62
+ * A caller authenticated by API key. This is exactly what
63
+ * ProjectAuthorization.isValidProjectIdAndApiKeyMiddleware builds once it
64
+ * resolves a key: userType = API, tenantId from the key's project, and NO
65
+ * userId, because an API key is not a user.
66
+ */
67
+ function apiKeyProps(): DatabaseCommonInteractionProps {
68
+ return {
69
+ tenantId: projectId,
70
+ userType: UserType.API,
71
+ };
72
+ }
73
+
74
+ // Runs the check and returns the message it threw with.
75
+ function messageFromCheck(runCheck: () => void): string {
76
+ try {
77
+ runCheck();
78
+ } catch (err) {
79
+ return (err as Exception).message;
80
+ }
81
+
82
+ throw new Error(
83
+ "Expected checkIfUserIsLoggedIn to throw, but it returned normally.",
84
+ );
85
+ }
86
+
87
+ describe("isPublicPermissionAllowed", () => {
88
+ /*
89
+ * LogPipeline is the issue-3004 model. Its read list is project-scoped
90
+ * only — no Permission.Public — which is why an anonymous read of it
91
+ * reaches the throw below rather than being waved through.
92
+ */
93
+ it("is false for a model whose read list has no Permission.Public", () => {
94
+ /*
95
+ * Stated against the model config as well as the function, so that if
96
+ * someone ever adds Permission.Public to LogPipeline the failure here
97
+ * reads as "the fixture changed" rather than "the function broke" —
98
+ * that would be a real security change and this file would need
99
+ * updating, not the production code.
100
+ */
101
+ expect(new LogPipeline().readRecordPermissions).not.toContain(
102
+ Permission.Public,
103
+ );
104
+
105
+ expect(
106
+ PublicPermission.isPublicPermissionAllowed(
107
+ LogPipeline,
108
+ DatabaseRequestType.Read,
109
+ ),
110
+ ).toBe(false);
111
+ });
112
+
113
+ /*
114
+ * Probe genuinely carries Permission.Public in its TableAccessControl
115
+ * read list (status pages render probe names to logged-out visitors).
116
+ */
117
+ it("is true for a model whose read list contains Permission.Public", () => {
118
+ expect(new Probe().readRecordPermissions).toContain(Permission.Public);
119
+
120
+ expect(
121
+ PublicPermission.isPublicPermissionAllowed(
122
+ Probe,
123
+ DatabaseRequestType.Read,
124
+ ),
125
+ ).toBe(true);
126
+ });
127
+
128
+ /*
129
+ * The lookup is per-operation, not per-model. StatusPageSubscriber is the
130
+ * clearest case in the codebase: anyone may subscribe to a status page
131
+ * (Public in `create`) but the subscriber list itself is not public
132
+ * (no Public in `read`). A model-level answer here would leak the list.
133
+ */
134
+ it("answers per operation, not per model", () => {
135
+ expect(
136
+ PublicPermission.isPublicPermissionAllowed(
137
+ StatusPageSubscriber,
138
+ DatabaseRequestType.Create,
139
+ ),
140
+ ).toBe(true);
141
+
142
+ expect(
143
+ PublicPermission.isPublicPermissionAllowed(
144
+ StatusPageSubscriber,
145
+ DatabaseRequestType.Read,
146
+ ),
147
+ ).toBe(false);
148
+ });
149
+
150
+ it("is false for every non-public operation of the issue-3004 model", () => {
151
+ for (const type of allRequestTypes) {
152
+ expect(
153
+ PublicPermission.isPublicPermissionAllowed(LogPipeline, type),
154
+ ).toBe(false);
155
+ }
156
+ });
157
+ });
158
+
159
+ describe("checkIfUserIsLoggedIn", () => {
160
+ /*
161
+ * ===== THE MESSAGE =====
162
+ *
163
+ * This is the sentence the reporter of #3004 saw. The old wording — "A
164
+ * user should be logged in to read record of Log Pipeline." — described
165
+ * only one of the two ways to authenticate and named the model, so two
166
+ * separate reporters (#3004 and #1754) spent their time auditing that
167
+ * model's RBAC config when the real cause was an API key that never
168
+ * arrived. The replacement names both credentials. If anyone shortens it
169
+ * back to a login-only phrasing, this fails.
170
+ */
171
+ it("names both a session and an API key in the anonymous-read message", () => {
172
+ const message: string = messageFromCheck(() => {
173
+ PublicPermission.checkIfUserIsLoggedIn(
174
+ LogPipeline,
175
+ anonymousProps(),
176
+ DatabaseRequestType.Read,
177
+ );
178
+ });
179
+
180
+ expect(message).toBe(
181
+ "Authenticated user or a valid API key is needed to read record of Log Pipeline.",
182
+ );
183
+ expect(message).not.toContain("should be logged in");
184
+ });
185
+
186
+ /*
187
+ * ===== THE API EARLY RETURN =====
188
+ *
189
+ * This is the invariant #3004 claimed was broken: a request authenticated
190
+ * by API key has userType API and NO userId, and must sail straight past
191
+ * this gate for every operation even on a model that is not public. If
192
+ * this ever regressed, every single API-key call in the product would 401
193
+ * with the sentence above, and the report would look exactly like #3004.
194
+ *
195
+ * The early return sits inside the not-public/no-userId branch and is
196
+ * keyed on `props.userType === UserType.API`; no userId is consulted,
197
+ * which is exactly the props shape ProjectAuthorization produces once it
198
+ * resolves a key.
199
+ */
200
+ it("lets an API-key caller with no userId through for every operation", () => {
201
+ for (const type of allRequestTypes) {
202
+ const props: DatabaseCommonInteractionProps = apiKeyProps();
203
+ expect(props.userId).toBeUndefined();
204
+
205
+ expect(() => {
206
+ PublicPermission.checkIfUserIsLoggedIn(LogPipeline, props, type);
207
+ }).not.toThrow();
208
+ }
209
+ });
210
+
211
+ it("lets a logged-in user through for every operation", () => {
212
+ for (const type of allRequestTypes) {
213
+ expect(() => {
214
+ PublicPermission.checkIfUserIsLoggedIn(
215
+ LogPipeline,
216
+ {
217
+ userId: userId,
218
+ tenantId: projectId,
219
+ userType: UserType.User,
220
+ },
221
+ type,
222
+ );
223
+ }).not.toThrow();
224
+ }
225
+ });
226
+
227
+ /*
228
+ * The gate has exactly two halves — `!isPublicPermissionAllowed` and
229
+ * `!props.userId` — and it is the *presence of a userId* that lifts it,
230
+ * not the userType label. A session established before userType was
231
+ * stamped on the props (SSO callback, older cached tokens) still carries
232
+ * a userId, and must not be treated as anonymous. If the condition were
233
+ * ever rewritten as `props.userType !== UserType.User`, this fails.
234
+ */
235
+ it("lets any caller carrying a userId through, whatever the userType says", () => {
236
+ expect(() => {
237
+ PublicPermission.checkIfUserIsLoggedIn(
238
+ LogPipeline,
239
+ {
240
+ userId: userId,
241
+ tenantId: projectId,
242
+ userType: UserType.Public,
243
+ },
244
+ DatabaseRequestType.Read,
245
+ );
246
+ }).not.toThrow();
247
+ });
248
+
249
+ /*
250
+ * The 401 has to stay a 401. Response.sendErrorResponse reads
251
+ * exception.code straight through to the HTTP status, so a wrong
252
+ * exception class here would silently change the status callers see and
253
+ * break client retry/re-auth logic.
254
+ */
255
+ it("throws NotAuthenticatedException carrying status 401 for an anonymous caller", () => {
256
+ let thrown: Exception | null = null;
257
+
258
+ try {
259
+ PublicPermission.checkIfUserIsLoggedIn(
260
+ LogPipeline,
261
+ anonymousProps(),
262
+ DatabaseRequestType.Read,
263
+ );
264
+ } catch (err) {
265
+ thrown = err as Exception;
266
+ }
267
+
268
+ expect(thrown).toBeInstanceOf(NotAuthenticatedException);
269
+ expect(thrown?.code).toBe(ExceptionCode.NotAuthenticatedException);
270
+ expect(thrown?.code).toBe(401);
271
+ });
272
+
273
+ /*
274
+ * The operation verb is interpolated from DatabaseRequestType and the
275
+ * model name from singularName. Support triages these reports by the
276
+ * sentence, so both halves have to be the real values — "Log Pipeline"
277
+ * with its space, not the class name "LogPipeline".
278
+ */
279
+ it("interpolates the operation and the model's singular name", () => {
280
+ const expectedByType: Array<[DatabaseRequestType, string]> = [
281
+ [
282
+ DatabaseRequestType.Create,
283
+ "Authenticated user or a valid API key is needed to create record of Log Pipeline.",
284
+ ],
285
+ [
286
+ DatabaseRequestType.Read,
287
+ "Authenticated user or a valid API key is needed to read record of Log Pipeline.",
288
+ ],
289
+ [
290
+ DatabaseRequestType.Update,
291
+ "Authenticated user or a valid API key is needed to update record of Log Pipeline.",
292
+ ],
293
+ [
294
+ DatabaseRequestType.Delete,
295
+ "Authenticated user or a valid API key is needed to delete record of Log Pipeline.",
296
+ ],
297
+ ];
298
+
299
+ for (const [type, expected] of expectedByType) {
300
+ const message: string = messageFromCheck(() => {
301
+ PublicPermission.checkIfUserIsLoggedIn(
302
+ LogPipeline,
303
+ anonymousProps(),
304
+ type,
305
+ );
306
+ });
307
+
308
+ expect(message).toBe(expected);
309
+ }
310
+
311
+ // The human-readable name, not the class name.
312
+ expect(new LogPipeline().singularName).toBe("Log Pipeline");
313
+ });
314
+
315
+ /*
316
+ * A model that IS public for the operation lets an anonymous caller
317
+ * through — that is what keeps logged-out status pages working — but only
318
+ * for that operation. Probe is public to read and not public to create.
319
+ */
320
+ it("lets an anonymous caller read a publicly-readable model but not create it", () => {
321
+ expect(() => {
322
+ PublicPermission.checkIfUserIsLoggedIn(
323
+ Probe,
324
+ anonymousProps(),
325
+ DatabaseRequestType.Read,
326
+ );
327
+ }).not.toThrow();
328
+
329
+ expect(() => {
330
+ PublicPermission.checkIfUserIsLoggedIn(
331
+ Probe,
332
+ anonymousProps(),
333
+ DatabaseRequestType.Create,
334
+ );
335
+ }).toThrow(NotAuthenticatedException);
336
+
337
+ /*
338
+ * Every other message assertion in this file uses LogPipeline, so this
339
+ * is the one that proves the sentence is built from the modelType
340
+ * argument — `new modelType().singularName` — rather than from a name
341
+ * captured anywhere else.
342
+ */
343
+ const message: string = messageFromCheck(() => {
344
+ PublicPermission.checkIfUserIsLoggedIn(
345
+ Probe,
346
+ anonymousProps(),
347
+ DatabaseRequestType.Create,
348
+ );
349
+ });
350
+
351
+ expect(message).toBe(
352
+ "Authenticated user or a valid API key is needed to create record of Probe.",
353
+ );
354
+ });
355
+
356
+ /*
357
+ * An entirely absent userType behaves the same as UserType.Public: only
358
+ * UserType.API short-circuits. Callers that forget to set userType must
359
+ * not accidentally get the API bypass.
360
+ */
361
+ it("treats props with no userType as anonymous", () => {
362
+ expect(() => {
363
+ PublicPermission.checkIfUserIsLoggedIn(
364
+ LogPipeline,
365
+ { tenantId: projectId },
366
+ DatabaseRequestType.Read,
367
+ );
368
+ }).toThrow(NotAuthenticatedException);
369
+ });
370
+
371
+ /*
372
+ * ===== MasterAdmin =====
373
+ *
374
+ * Documenting what the code actually does rather than what one might
375
+ * assume: the early return checks `props.userType === UserType.API` and
376
+ * nothing else, so a MasterAdmin (or an isMasterAdmin flag) with no
377
+ * userId is NOT waved through here — it throws the same 401. In practice
378
+ * the master-admin path always carries a userId, and the actual
379
+ * master-key bypass lives upstream in the middleware, not in this gate.
380
+ * If someone later adds a MasterAdmin short-circuit here, this test will
381
+ * fail and force the decision to be explicit.
382
+ */
383
+ it("does not grant MasterAdmin an early return when there is no userId", () => {
384
+ expect(() => {
385
+ PublicPermission.checkIfUserIsLoggedIn(
386
+ LogPipeline,
387
+ {
388
+ tenantId: projectId,
389
+ userType: UserType.MasterAdmin,
390
+ isMasterAdmin: true,
391
+ },
392
+ DatabaseRequestType.Read,
393
+ );
394
+ }).toThrow(NotAuthenticatedException);
395
+
396
+ // ...but a MasterAdmin with a session passes, like any logged-in user.
397
+ expect(() => {
398
+ PublicPermission.checkIfUserIsLoggedIn(
399
+ LogPipeline,
400
+ {
401
+ userId: userId,
402
+ tenantId: projectId,
403
+ userType: UserType.MasterAdmin,
404
+ isMasterAdmin: true,
405
+ },
406
+ DatabaseRequestType.Read,
407
+ );
408
+ }).not.toThrow();
409
+ });
410
+ });
411
+
412
+ /*
413
+ * ===== DRIFT GUARD: Postgres message vs analytics message =====
414
+ *
415
+ * There are two copies of this check — PublicPermission (Postgres tables)
416
+ * and AnalyticsDatabase/ModelPermission (ClickHouse tables). They are meant
417
+ * to be the same gate, and a caller has no idea which storage engine backs
418
+ * the model they just called. The two sentences silently diverged for
419
+ * years: the analytics side already said "Authenticated user or a valid API
420
+ * key is needed to ...", while the Postgres side still said "A user should
421
+ * be logged in to ...". Searching the codebase for the string a user
422
+ * pasted therefore turned up only one of the two call sites, which is a
423
+ * large part of why #3004 was hard to diagnose.
424
+ *
425
+ * This drives both real functions with equivalent props and compares the
426
+ * sentences they actually produce, rather than grepping the source, so it
427
+ * still catches a divergence introduced by refactoring either one.
428
+ */
429
+ describe("message parity with the analytics-side checkIfUserIsLoggedIn", () => {
430
+ // Replace the only part that legitimately differs: the model's own name.
431
+ function sentenceTemplate(message: string, singularName: string): string {
432
+ return message.replace(
433
+ `record of ${singularName}.`,
434
+ "record of <singularName>.",
435
+ );
436
+ }
437
+
438
+ it("produces the same sentence on both storage engines for every operation", () => {
439
+ const postgresSingularName: string = new LogPipeline().singularName!;
440
+ const analyticsSingularName: string = new Log().singularName!;
441
+
442
+ // Different names, so the comparison below is not trivially true.
443
+ expect(postgresSingularName).not.toBe(analyticsSingularName);
444
+
445
+ for (const type of allRequestTypes) {
446
+ const postgresMessage: string = messageFromCheck(() => {
447
+ PublicPermission.checkIfUserIsLoggedIn(
448
+ LogPipeline,
449
+ anonymousProps(),
450
+ type,
451
+ );
452
+ });
453
+
454
+ const analyticsMessage: string = messageFromCheck(() => {
455
+ ModelPermission.checkIfUserIsLoggedIn(Log, anonymousProps(), type);
456
+ });
457
+
458
+ const postgresTemplate: string = sentenceTemplate(
459
+ postgresMessage,
460
+ postgresSingularName,
461
+ );
462
+ const analyticsTemplate: string = sentenceTemplate(
463
+ analyticsMessage,
464
+ analyticsSingularName,
465
+ );
466
+
467
+ // The normalisation actually fired on both sides.
468
+ expect(postgresTemplate).toContain("<singularName>");
469
+ expect(analyticsTemplate).toContain("<singularName>");
470
+
471
+ expect(postgresTemplate).toBe(analyticsTemplate);
472
+ expect(postgresTemplate).toBe(
473
+ `Authenticated user or a valid API key is needed to ${type} record of <singularName>.`,
474
+ );
475
+ }
476
+ });
477
+
478
+ // The API early return has to agree across both engines too.
479
+ it("lets an API-key caller with no userId through on both storage engines", () => {
480
+ for (const type of allRequestTypes) {
481
+ expect(() => {
482
+ PublicPermission.checkIfUserIsLoggedIn(
483
+ LogPipeline,
484
+ apiKeyProps(),
485
+ type,
486
+ );
487
+ }).not.toThrow();
488
+
489
+ expect(() => {
490
+ ModelPermission.checkIfUserIsLoggedIn(Log, apiKeyProps(), type);
491
+ }).not.toThrow();
492
+ }
493
+ });
494
+ });
495
+ });
@@ -1,5 +1,8 @@
1
1
  import QueryUtil from "../../../../Server/Types/Database/QueryUtil";
2
+ import Incident from "../../../../Models/DatabaseModels/Incident";
2
3
  import TeamMember from "../../../../Models/DatabaseModels/TeamMember";
4
+ import Includes from "../../../../Types/BaseDatabase/Includes";
5
+ import IsNull from "../../../../Types/BaseDatabase/IsNull";
3
6
  import Search from "../../../../Types/BaseDatabase/Search";
4
7
  import ObjectID from "../../../../Types/ObjectID";
5
8
  import { FindOperator } from "typeorm";
@@ -72,3 +75,113 @@ describe("QueryUtil.serializeQuery — nested relation operators", () => {
72
75
  expect(result["user"]._id).toBe(userId.toString());
73
76
  });
74
77
  });
78
+
79
+ /*
80
+ * Custom field filters ride the `customFields` jsonb column, so they reach the
81
+ * database through serializeQuery's JSON branch. These pin the routing: the
82
+ * branch has to fire for an operator-valued object (the new custom-field
83
+ * chips) exactly as it always has for a plain one, and the branches above it
84
+ * have to keep winning for a whole-column IsNull.
85
+ */
86
+ describe("QueryUtil.serializeQuery — customFields jsonb column", () => {
87
+ type RawOperatorSqlFunction = (operator: unknown) => string;
88
+
89
+ const rawOperatorSql: RawOperatorSqlFunction = (
90
+ operator: unknown,
91
+ ): string => {
92
+ const getSql: ((aliasPath: string) => string) | undefined = (
93
+ operator as FindOperator<unknown>
94
+ ).getSql;
95
+
96
+ if (!getSql) {
97
+ throw new Error("Expected a Raw FindOperator with a SQL generator.");
98
+ }
99
+
100
+ return getSql("Incident.customFields");
101
+ };
102
+
103
+ it("compiles a plain key/value object into a raw jsonb predicate", () => {
104
+ const query: Record<string, unknown> = {
105
+ projectId: ObjectID.generate(),
106
+ customFields: { Team: "Payments" },
107
+ };
108
+
109
+ const result: Record<string, any> = QueryUtil.serializeQuery(
110
+ Incident,
111
+ query as any,
112
+ ) as unknown as Record<string, any>;
113
+
114
+ expect(result["customFields"]).toBeInstanceOf(FindOperator);
115
+ expect(result["customFields"].type).toBe("raw");
116
+ expect(rawOperatorSql(result["customFields"])).toContain(
117
+ '"Incident"."customFields" ->>',
118
+ );
119
+ expect(
120
+ Object.values(result["customFields"].objectLiteralParameters),
121
+ ).toContain("Payments");
122
+ });
123
+
124
+ it("compiles an operator nested under a key into the same raw predicate", () => {
125
+ const query: Record<string, unknown> = {
126
+ projectId: ObjectID.generate(),
127
+ customFields: { Team: new Includes(["Payments", "Billing"]) },
128
+ };
129
+
130
+ const result: Record<string, any> = QueryUtil.serializeQuery(
131
+ Incident,
132
+ query as any,
133
+ ) as unknown as Record<string, any>;
134
+
135
+ const sql: string = rawOperatorSql(result["customFields"]);
136
+
137
+ // Two values means two OR-ed equality arms, each with a containment check.
138
+ expect(sql).toContain(" OR ");
139
+ expect(sql).toContain("@> CAST(");
140
+ expect(
141
+ Object.values(result["customFields"].objectLiteralParameters),
142
+ ).toEqual(expect.arrayContaining(["Team", "Payments", "Billing"]));
143
+ });
144
+
145
+ it("never leaves a custom field name in the statement text", () => {
146
+ const query: Record<string, unknown> = {
147
+ projectId: ObjectID.generate(),
148
+ customFields: { "x' OR 1=1 --": "v" },
149
+ };
150
+
151
+ const result: Record<string, any> = QueryUtil.serializeQuery(
152
+ Incident,
153
+ query as any,
154
+ ) as unknown as Record<string, any>;
155
+
156
+ expect(rawOperatorSql(result["customFields"])).not.toContain("OR 1=1");
157
+ });
158
+
159
+ it("still routes a whole-column IsNull to the branch above the JSON one", () => {
160
+ const query: Record<string, unknown> = {
161
+ projectId: ObjectID.generate(),
162
+ customFields: new IsNull(),
163
+ };
164
+
165
+ const result: Record<string, any> = QueryUtil.serializeQuery(
166
+ Incident,
167
+ query as any,
168
+ ) as unknown as Record<string, any>;
169
+
170
+ expect(rawOperatorSql(result["customFields"])).toContain("IS NULL");
171
+ expect(rawOperatorSql(result["customFields"])).not.toContain("->>");
172
+ });
173
+
174
+ it("still routes an explicit null to isNull", () => {
175
+ const query: Record<string, unknown> = {
176
+ projectId: ObjectID.generate(),
177
+ customFields: null,
178
+ };
179
+
180
+ const result: Record<string, any> = QueryUtil.serializeQuery(
181
+ Incident,
182
+ query as any,
183
+ ) as unknown as Record<string, any>;
184
+
185
+ expect(rawOperatorSql(result["customFields"])).toContain("IS NULL");
186
+ });
187
+ });