@oneuptime/common 12.0.27 → 12.0.29

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (204) hide show
  1. package/Models/AnalyticsModels/RumSessionChunk.ts +52 -0
  2. package/Models/DatabaseModels/CloudResource.ts +17 -1
  3. package/Models/DatabaseModels/DashboardDomain.ts +43 -0
  4. package/Models/DatabaseModels/MonitorTemplate.ts +15 -3
  5. package/Models/DatabaseModels/RumApplication.ts +35 -2
  6. package/Models/DatabaseModels/StatusPageDomain.ts +43 -0
  7. package/Server/API/DashboardDomainAPI.ts +74 -0
  8. package/Server/API/MicrosoftTeamsAPI.ts +81 -180
  9. package/Server/API/StatusPageDomainAPI.ts +74 -0
  10. package/Server/API/TelemetryAPI.ts +104 -15
  11. package/Server/Infrastructure/Postgres/SchemaMigrations/1790200000000-AllowNullMonitorNameOnMonitorTemplate.ts +43 -0
  12. package/Server/Infrastructure/Postgres/SchemaMigrations/1790300000000-AddCertificateReissueRequestedAtToDomains.ts +40 -0
  13. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +4 -0
  14. package/Server/Middleware/ProjectAuthorization.ts +6 -1
  15. package/Server/Services/DashboardDomainService.ts +131 -0
  16. package/Server/Services/LogPipelineProcessorService.ts +88 -0
  17. package/Server/Services/MonitorTemplateService.ts +70 -11
  18. package/Server/Services/StatusPageDomainService.ts +163 -3
  19. package/Server/Services/StatusPagePrivateUserSessionService.ts +197 -56
  20. package/Server/Types/Domain.ts +17 -0
  21. package/Server/Utils/APIKey/AccessPermission.ts +24 -0
  22. package/Server/Utils/Greenlock/Greenlock.ts +156 -58
  23. package/Server/Utils/LogPipelineProcessorValidation.ts +112 -0
  24. package/Server/Utils/SecurityEvent/GoogleSecOps/GoogleSecOpsClient.ts +1094 -45
  25. package/Server/Utils/SecurityEvent/GoogleSecOps/GoogleSecOpsPoller.ts +153 -17
  26. package/Server/Utils/SessionReplay/SessionReplayGateCache.ts +12 -5
  27. package/Server/Utils/SessionReplay/SessionReplayIdentity.ts +102 -0
  28. package/Server/Utils/SessionReplay/SessionReplayReadService.ts +11 -2
  29. package/Server/Utils/Workspace/MicrosoftTeams/Actions/Alert.ts +23 -11
  30. package/Server/Utils/Workspace/MicrosoftTeams/Actions/Auth.ts +0 -170
  31. package/Server/Utils/Workspace/MicrosoftTeams/Actions/Authorization.ts +158 -0
  32. package/Server/Utils/Workspace/MicrosoftTeams/Actions/Incident.ts +23 -11
  33. package/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.ts +35 -13
  34. package/Tests/App/Dashboard/MonitorTemplateColumnAndFacet.test.tsx +505 -0
  35. package/Tests/App/StatusPage/StatusPageLoginCodeBootstrap.test.ts +106 -0
  36. package/Tests/App/StatusPage/StatusPageLoginCodeUtil.test.ts +84 -0
  37. package/Tests/App/StatusPage/StatusPageRedirectSafety.test.ts +119 -0
  38. package/Tests/Models/DatabaseModels/SessionReplayModels.test.ts +81 -0
  39. package/Tests/Models/MonitorTemplateMonitorNameColumn.test.ts +258 -0
  40. package/Tests/Server/API/CustomDomainReissueSslAPI.test.ts +469 -0
  41. package/Tests/Server/API/MicrosoftTeamsBotMessagesEndpoint.test.ts +729 -0
  42. package/Tests/Server/API/MicrosoftTeamsBotTestEndpoint.test.ts +26 -0
  43. package/Tests/Server/API/MicrosoftTeamsLegacyWebhookRemoval.test.ts +402 -0
  44. package/Tests/Server/API/SessionReplayAPI.test.ts +232 -0
  45. package/Tests/Server/Middleware/ProjectAuthorizationApiKeyMiddleware.test.ts +4 -0
  46. package/Tests/Server/Services/AddCertificateReissueRequestedAtToDomainsMigration.test.ts +287 -0
  47. package/Tests/Server/Services/CustomDomainCertificateReissue.test.ts +477 -0
  48. package/Tests/Server/Services/LogPipelineProcessorSaveValidation.test.ts +249 -0
  49. package/Tests/Server/Services/MonitorTemplateServiceBulkSync.test.ts +549 -0
  50. package/Tests/Server/Services/MonitorTemplateServiceNetworkDeviceSync.test.ts +16 -3
  51. package/Tests/Server/Services/NetworkDeviceAutoImportRuleEngineService.test.ts +57 -0
  52. package/Tests/Server/Services/SecurityEventLastErrorColumnWidth.test.ts +151 -15
  53. package/Tests/Server/Services/StatusPagePrivateUserSessionService.test.ts +587 -0
  54. package/Tests/Server/Types/Database/Permissions/ApiKeyFileAccess.test.ts +286 -0
  55. package/Tests/Server/Types/Database/Permissions/MonitorTemplateColumnAccess.test.ts +328 -0
  56. package/Tests/Server/Types/Database/QueryHelperFindWithSameTextAndIsNull.test.ts +83 -0
  57. package/Tests/Server/Utils/AI/SRE/ConfidenceSignal.test.ts +377 -0
  58. package/Tests/Server/Utils/APIKey/AccessPermission.test.ts +16 -1
  59. package/Tests/Server/Utils/Greenlock/CertificateRenewalSchedule.test.ts +250 -0
  60. package/Tests/Server/Utils/LogPipelineProcessorValidation.test.ts +198 -0
  61. package/Tests/Server/Utils/SecurityEvent/ConnectorErrorMessage.test.ts +48 -9
  62. package/Tests/Server/Utils/SecurityEvent/GoogleSecOpsAuth.test.ts +726 -0
  63. package/Tests/Server/Utils/SecurityEvent/GoogleSecOpsClient.test.ts +74 -11
  64. package/Tests/Server/Utils/SecurityEvent/GoogleSecOpsErrorHandling.test.ts +1107 -0
  65. package/Tests/Server/Utils/SecurityEvent/GoogleSecOpsPoller.test.ts +20 -3
  66. package/Tests/Server/Utils/SecurityEvent/GoogleSecOpsPollerFailureTaxonomy.test.ts +51 -11
  67. package/Tests/Server/Utils/SecurityEvent/GoogleSecOpsPollerHardening.test.ts +1246 -0
  68. package/Tests/Server/Utils/SecurityEvent/GoogleSecOpsRequestContract.test.ts +961 -0
  69. package/Tests/Server/Utils/SecurityEvent/GoogleSecOpsResponseParsing.test.ts +653 -0
  70. package/Tests/Server/Utils/Workspace/MicrosoftTeamsActionAuthorization.test.ts +880 -0
  71. package/Tests/Types/API/HTTPResponse.test.ts +175 -0
  72. package/Tests/Types/API/Route.test.ts +36 -4
  73. package/Tests/Types/Database/DatabaseProperty.test.ts +171 -0
  74. package/Tests/Types/DateLocalShortDateTimeString.test.ts +518 -0
  75. package/Tests/Types/DateLocalTimeString.test.ts +259 -0
  76. package/Tests/Types/DateUserPrefers12HourFormat.test.ts +316 -0
  77. package/Tests/UI/Components/LogsHistogram.test.tsx +41 -2
  78. package/Tests/UI/Components/LogsViewerEmptyStateTimeRange.test.tsx +212 -0
  79. package/Tests/UI/Components/TelemetryChartClockFormat.test.tsx +359 -0
  80. package/Tests/UI/Components/TimeRangePickerClockFormat.test.tsx +422 -0
  81. package/Tests/UI/Components/TimeRangePickerDropdown.test.tsx +146 -8
  82. package/Tests/UI/MicrosoftTeams/MicrosoftTeamsMessagingEndpointGuidance.test.tsx +520 -0
  83. package/Tests/UI/Utils/Navigation.test.ts +82 -0
  84. package/Tests/Utils/Array.test.ts +100 -0
  85. package/Tests/Utils/CertificateReissue.test.ts +225 -0
  86. package/Tests/Utils/Grok/Grok.test.ts +492 -0
  87. package/Tests/Utils/Monitor/NetworkDeviceMonitorTemplateUtil.test.ts +181 -13
  88. package/Types/API/Route.ts +20 -5
  89. package/Types/Date.ts +143 -13
  90. package/Types/Rum/SessionReplay.ts +76 -1
  91. package/UI/Components/Date/TimeRangePickerDropdown.tsx +9 -5
  92. package/UI/Components/LogsViewer/LogsViewer.tsx +12 -7
  93. package/UI/Components/LogsViewer/components/HistogramTooltip.tsx +18 -17
  94. package/UI/Components/LogsViewer/components/LogsAnalyticsView.tsx +21 -28
  95. package/UI/Components/LogsViewer/components/LogsHistogram.tsx +2 -4
  96. package/UI/Components/TelemetryViewer/components/TelemetryHistogram.tsx +2 -4
  97. package/UI/Components/TelemetryViewer/components/TelemetryHistogramTooltip.tsx +17 -16
  98. package/UI/Utils/Navigation.ts +42 -0
  99. package/Utils/Array.ts +37 -0
  100. package/Utils/CertificateReissue.ts +128 -0
  101. package/Utils/Grok/Grok.ts +461 -0
  102. package/Utils/Grok/GrokPatterns.ts +118 -0
  103. package/Utils/Monitor/NetworkDeviceMonitorTemplateUtil.ts +58 -6
  104. package/build/dist/Models/AnalyticsModels/RumSessionChunk.js +48 -0
  105. package/build/dist/Models/AnalyticsModels/RumSessionChunk.js.map +1 -1
  106. package/build/dist/Models/DatabaseModels/CloudResource.js +19 -2
  107. package/build/dist/Models/DatabaseModels/CloudResource.js.map +1 -1
  108. package/build/dist/Models/DatabaseModels/DashboardDomain.js +44 -0
  109. package/build/dist/Models/DatabaseModels/DashboardDomain.js.map +1 -1
  110. package/build/dist/Models/DatabaseModels/MonitorTemplate.js +17 -4
  111. package/build/dist/Models/DatabaseModels/MonitorTemplate.js.map +1 -1
  112. package/build/dist/Models/DatabaseModels/RumApplication.js +37 -3
  113. package/build/dist/Models/DatabaseModels/RumApplication.js.map +1 -1
  114. package/build/dist/Models/DatabaseModels/StatusPageDomain.js +44 -0
  115. package/build/dist/Models/DatabaseModels/StatusPageDomain.js.map +1 -1
  116. package/build/dist/Server/API/DashboardDomainAPI.js +43 -1
  117. package/build/dist/Server/API/DashboardDomainAPI.js.map +1 -1
  118. package/build/dist/Server/API/MicrosoftTeamsAPI.js +74 -118
  119. package/build/dist/Server/API/MicrosoftTeamsAPI.js.map +1 -1
  120. package/build/dist/Server/API/StatusPageDomainAPI.js +43 -1
  121. package/build/dist/Server/API/StatusPageDomainAPI.js.map +1 -1
  122. package/build/dist/Server/API/TelemetryAPI.js +67 -14
  123. package/build/dist/Server/API/TelemetryAPI.js.map +1 -1
  124. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1790200000000-AllowNullMonitorNameOnMonitorTemplate.js +34 -0
  125. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1790200000000-AllowNullMonitorNameOnMonitorTemplate.js.map +1 -0
  126. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1790300000000-AddCertificateReissueRequestedAtToDomains.js +28 -0
  127. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1790300000000-AddCertificateReissueRequestedAtToDomains.js.map +1 -0
  128. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +4 -0
  129. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  130. package/build/dist/Server/Middleware/ProjectAuthorization.js +6 -1
  131. package/build/dist/Server/Middleware/ProjectAuthorization.js.map +1 -1
  132. package/build/dist/Server/Services/DashboardDomainService.js +104 -0
  133. package/build/dist/Server/Services/DashboardDomainService.js.map +1 -1
  134. package/build/dist/Server/Services/LogPipelineProcessorService.js +65 -0
  135. package/build/dist/Server/Services/LogPipelineProcessorService.js.map +1 -1
  136. package/build/dist/Server/Services/MonitorTemplateService.js +57 -11
  137. package/build/dist/Server/Services/MonitorTemplateService.js.map +1 -1
  138. package/build/dist/Server/Services/StatusPageDomainService.js +132 -3
  139. package/build/dist/Server/Services/StatusPageDomainService.js.map +1 -1
  140. package/build/dist/Server/Services/StatusPagePrivateUserSessionService.js +112 -32
  141. package/build/dist/Server/Services/StatusPagePrivateUserSessionService.js.map +1 -1
  142. package/build/dist/Server/Types/Domain.js +16 -0
  143. package/build/dist/Server/Types/Domain.js.map +1 -1
  144. package/build/dist/Server/Utils/APIKey/AccessPermission.js +24 -0
  145. package/build/dist/Server/Utils/APIKey/AccessPermission.js.map +1 -1
  146. package/build/dist/Server/Utils/Greenlock/Greenlock.js +110 -35
  147. package/build/dist/Server/Utils/Greenlock/Greenlock.js.map +1 -1
  148. package/build/dist/Server/Utils/LogPipelineProcessorValidation.js +77 -0
  149. package/build/dist/Server/Utils/LogPipelineProcessorValidation.js.map +1 -0
  150. package/build/dist/Server/Utils/SecurityEvent/GoogleSecOps/GoogleSecOpsClient.js +777 -36
  151. package/build/dist/Server/Utils/SecurityEvent/GoogleSecOps/GoogleSecOpsClient.js.map +1 -1
  152. package/build/dist/Server/Utils/SecurityEvent/GoogleSecOps/GoogleSecOpsPoller.js +108 -17
  153. package/build/dist/Server/Utils/SecurityEvent/GoogleSecOps/GoogleSecOpsPoller.js.map +1 -1
  154. package/build/dist/Server/Utils/SessionReplay/SessionReplayGateCache.js.map +1 -1
  155. package/build/dist/Server/Utils/SessionReplay/SessionReplayIdentity.js +59 -0
  156. package/build/dist/Server/Utils/SessionReplay/SessionReplayIdentity.js.map +1 -0
  157. package/build/dist/Server/Utils/SessionReplay/SessionReplayReadService.js +9 -2
  158. package/build/dist/Server/Utils/SessionReplay/SessionReplayReadService.js.map +1 -1
  159. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/Alert.js +17 -6
  160. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/Alert.js.map +1 -1
  161. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/Auth.js +0 -159
  162. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/Auth.js.map +1 -1
  163. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/Authorization.js +128 -0
  164. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/Authorization.js.map +1 -0
  165. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/Incident.js +17 -6
  166. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/Incident.js.map +1 -1
  167. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.js +25 -13
  168. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.js.map +1 -1
  169. package/build/dist/Types/API/Route.js +16 -5
  170. package/build/dist/Types/API/Route.js.map +1 -1
  171. package/build/dist/Types/Date.js +115 -13
  172. package/build/dist/Types/Date.js.map +1 -1
  173. package/build/dist/Types/Rum/SessionReplay.js +38 -0
  174. package/build/dist/Types/Rum/SessionReplay.js.map +1 -1
  175. package/build/dist/UI/Components/Date/TimeRangePickerDropdown.js +9 -5
  176. package/build/dist/UI/Components/Date/TimeRangePickerDropdown.js.map +1 -1
  177. package/build/dist/UI/Components/LogsViewer/LogsViewer.js +12 -7
  178. package/build/dist/UI/Components/LogsViewer/LogsViewer.js.map +1 -1
  179. package/build/dist/UI/Components/LogsViewer/components/HistogramTooltip.js +15 -14
  180. package/build/dist/UI/Components/LogsViewer/components/HistogramTooltip.js.map +1 -1
  181. package/build/dist/UI/Components/LogsViewer/components/LogsAnalyticsView.js +17 -22
  182. package/build/dist/UI/Components/LogsViewer/components/LogsAnalyticsView.js.map +1 -1
  183. package/build/dist/UI/Components/LogsViewer/components/LogsHistogram.js +2 -4
  184. package/build/dist/UI/Components/LogsViewer/components/LogsHistogram.js.map +1 -1
  185. package/build/dist/UI/Components/TelemetryViewer/components/TelemetryHistogram.js +2 -4
  186. package/build/dist/UI/Components/TelemetryViewer/components/TelemetryHistogram.js.map +1 -1
  187. package/build/dist/UI/Components/TelemetryViewer/components/TelemetryHistogramTooltip.js +15 -14
  188. package/build/dist/UI/Components/TelemetryViewer/components/TelemetryHistogramTooltip.js.map +1 -1
  189. package/build/dist/UI/Utils/Navigation.js +30 -0
  190. package/build/dist/UI/Utils/Navigation.js.map +1 -1
  191. package/build/dist/Utils/Array.js +25 -0
  192. package/build/dist/Utils/Array.js.map +1 -1
  193. package/build/dist/Utils/CertificateReissue.js +97 -0
  194. package/build/dist/Utils/CertificateReissue.js.map +1 -0
  195. package/build/dist/Utils/Grok/Grok.js +324 -0
  196. package/build/dist/Utils/Grok/Grok.js.map +1 -0
  197. package/build/dist/Utils/Grok/GrokPatterns.js +112 -0
  198. package/build/dist/Utils/Grok/GrokPatterns.js.map +1 -0
  199. package/build/dist/Utils/Monitor/NetworkDeviceMonitorTemplateUtil.js +63 -3
  200. package/build/dist/Utils/Monitor/NetworkDeviceMonitorTemplateUtil.js.map +1 -1
  201. package/package.json +1 -1
  202. package/UI/Utils/JsonWebToken.ts +0 -14
  203. package/build/dist/UI/Utils/JsonWebToken.js +0 -10
  204. package/build/dist/UI/Utils/JsonWebToken.js.map +0 -1
@@ -0,0 +1,286 @@
1
+ import { describe, expect, jest, test } from "@jest/globals";
2
+
3
+ /*
4
+ * PasswordHash has a known, pre-existing TS5.9 compile failure under ts-jest
5
+ * (Buffer vs BinaryLike) that breaks every suite whose import graph reaches
6
+ * it. APIKeyAccessPermission pulls in ApiKeyPermissionService, which reaches
7
+ * it; nothing here touches password hashing, so stub it before that graph is
8
+ * compiled. Same reason and same shape as the stub in
9
+ * Tests/Server/Utils/APIKey/AccessPermission.test.ts.
10
+ */
11
+ jest.mock("../../../../../Server/Utils/PasswordHash", () => {
12
+ return {
13
+ __esModule: true,
14
+ default: class PasswordHashStub {},
15
+ };
16
+ });
17
+
18
+ import DatabaseRequestType from "../../../../../Server/Types/BaseDatabase/DatabaseRequestType";
19
+ import CreatePermission from "../../../../../Server/Types/Database/Permissions/CreatePermission";
20
+ import TenantPermission from "../../../../../Server/Types/Database/Permissions/TenantPermission";
21
+ import APIKeyAccessPermission from "../../../../../Server/Utils/APIKey/AccessPermission";
22
+ import AllModelTypes from "../../../../../Models/DatabaseModels/Index";
23
+ import BaseModel from "../../../../../Models/DatabaseModels/DatabaseBaseModel/DatabaseBaseModel";
24
+ import File from "../../../../../Models/DatabaseModels/File";
25
+ import DatabaseCommonInteractionProps from "../../../../../Types/BaseDatabase/DatabaseCommonInteractionProps";
26
+ import MimeType from "../../../../../Types/File/MimeType";
27
+ import ObjectID from "../../../../../Types/ObjectID";
28
+ import Permission, {
29
+ UserGlobalAccessPermission,
30
+ UserPermission,
31
+ UserTenantAccessPermission,
32
+ } from "../../../../../Types/Permission";
33
+ import UserType from "../../../../../Types/UserType";
34
+
35
+ /*
36
+ * Contract under test: an API key is not a logged-in user, and File's gate must
37
+ * not treat it as one.
38
+ *
39
+ * File and FileModel gate on [CurrentUser, AuthenticatedRequest]. CurrentUser is
40
+ * auto-granted to every authenticated caller, so a key that holds nothing else
41
+ * from that list intersects down to CurrentUser alone — and that is exactly the
42
+ * condition TenantPermission.isAccessGrantedOnlyByCurrentUser reports as "this
43
+ * caller is here as nothing more than some logged-in user". A caller in that
44
+ * state is confined to rows they own, which a key can never be: it carries no
45
+ * userId, and both the query-side scope and the create-side ownership check
46
+ * reject it outright rather than run unscoped.
47
+ *
48
+ * Granting Permission.AuthenticatedRequest to API keys is what makes that
49
+ * predicate answer the truth. The permission exists for precisely this - it is a
50
+ * marker for "some authenticated principal made this call", not a capability an
51
+ * administrator grants, and File/FileModel are the only models that name it.
52
+ *
53
+ * Worth being exact about what is and is not broken today, because it decides
54
+ * what this test is for. File declares no user column, so
55
+ * CreatePermission.checkCreateOwnership returns before the userId guard and file
56
+ * creation by API key does NOT currently fail - the third test below pins that
57
+ * it passes both before and after. The predicate is still answering wrongly, and
58
+ * the moment File gains a user column, or a second model adopts the same
59
+ * [CurrentUser, AuthenticatedRequest] pair, the wrong answer stops being
60
+ * harmless. This suite pins the predicate itself rather than the symptom, so
61
+ * that the grant cannot be quietly dropped as unused.
62
+ */
63
+
64
+ const projectId: ObjectID = ObjectID.generate();
65
+ const userId: ObjectID = ObjectID.generate();
66
+
67
+ /*
68
+ * The tenant half of an API key's permissions. getApiTenantAccessPermission
69
+ * returns the default baseline plus whatever rows the key was configured with;
70
+ * built here rather than called so the suite stays free of the service and its
71
+ * cache. A key with no configured rows is the interesting case - it is the one
72
+ * that has nothing but the auto-granted permissions to intersect with.
73
+ */
74
+ function tenantPermissions(): UserTenantAccessPermission {
75
+ const permissions: Array<UserPermission> = [
76
+ Permission.CurrentUser,
77
+ Permission.UnAuthorizedSsoUser,
78
+ ].map((permission: Permission) => {
79
+ return {
80
+ permission: permission,
81
+ labelIds: [],
82
+ isBlockPermission: false,
83
+ _type: "UserPermission",
84
+ };
85
+ });
86
+
87
+ return {
88
+ projectId,
89
+ permissions,
90
+ isBlockPermission: false,
91
+ _type: "UserTenantAccessPermission",
92
+ };
93
+ }
94
+
95
+ function apiKeyProps(
96
+ globalPermissions: Array<Permission>,
97
+ ): DatabaseCommonInteractionProps {
98
+ return {
99
+ tenantId: projectId,
100
+ userType: UserType.API,
101
+ userGlobalAccessPermission: {
102
+ projectIds: [projectId],
103
+ globalPermissions: globalPermissions,
104
+ _type: "UserGlobalAccessPermission",
105
+ },
106
+ userTenantAccessPermission: {
107
+ [projectId.toString()]: tenantPermissions(),
108
+ },
109
+ } as unknown as DatabaseCommonInteractionProps;
110
+ }
111
+
112
+ /*
113
+ * The list as it stood before AuthenticatedRequest was granted. Kept literal
114
+ * rather than derived: it is the negative control, and a control that tracks the
115
+ * production list would silently stop controlling for anything.
116
+ */
117
+ const GLOBAL_PERMISSIONS_WITHOUT_MARKER: Array<Permission> = [
118
+ Permission.Public,
119
+ Permission.User,
120
+ Permission.CurrentUser,
121
+ ];
122
+
123
+ function newFile(): File {
124
+ const file: File = new File();
125
+ file.name = "screenshot.png";
126
+ file.fileType = MimeType.png;
127
+ file.file = Buffer.from("not-really-a-png");
128
+ file.isPublic = false;
129
+ return file;
130
+ }
131
+
132
+ /*
133
+ * Every model that names AuthenticatedRequest anywhere in its table or column
134
+ * access control, by model name.
135
+ */
136
+ function modelsNamingTheMarker(): Array<string> {
137
+ const naming: Array<string> = [];
138
+
139
+ for (const modelType of AllModelTypes as Array<{ new (): BaseModel }>) {
140
+ const model: BaseModel = new modelType();
141
+
142
+ const permissions: Array<Permission> = [
143
+ ...(model.createRecordPermissions || []),
144
+ ...(model.readRecordPermissions || []),
145
+ ...(model.updateRecordPermissions || []),
146
+ ...(model.deleteRecordPermissions || []),
147
+ ];
148
+
149
+ for (const column of model.getTableColumns().columns) {
150
+ const accessControl: ReturnType<BaseModel["getColumnAccessControlFor"]> =
151
+ model.getColumnAccessControlFor(column);
152
+
153
+ if (!accessControl) {
154
+ continue;
155
+ }
156
+
157
+ permissions.push(
158
+ ...(accessControl.create || []),
159
+ ...(accessControl.read || []),
160
+ ...(accessControl.update || []),
161
+ );
162
+ }
163
+
164
+ if (permissions.includes(Permission.AuthenticatedRequest)) {
165
+ naming.push(modelType.name);
166
+ }
167
+ }
168
+
169
+ return naming.sort();
170
+ }
171
+
172
+ describe("API key access to File", () => {
173
+ test("no model other than File gates on the marker", () => {
174
+ /*
175
+ * The grant is unconditional - every API key in every project holds
176
+ * AuthenticatedRequest, and unlike a role permission no administrator can
177
+ * revoke it. What keeps that safe is not the grant but this list: the
178
+ * marker is deliberately absent from
179
+ * TenantPermission.AUTO_GRANTED_TENANT_PERMISSIONS, so any model that
180
+ * starts naming it hands every API key a role-grade way through its gate.
181
+ *
182
+ * Sweeping the models rather than trusting a grep is the point. A new
183
+ * model quietly adopting the [CurrentUser, AuthenticatedRequest] pair is
184
+ * the one change that turns this from bounded to unbounded, and it should
185
+ * fail here and be argued for rather than land unnoticed.
186
+ *
187
+ * This list must only ever shrink. A new entry needs its own review.
188
+ */
189
+ expect(modelsNamingTheMarker()).toEqual(["File"]);
190
+ });
191
+
192
+ test("the production global permission list carries the AuthenticatedRequest marker", async () => {
193
+ const permission: UserGlobalAccessPermission =
194
+ await APIKeyAccessPermission.getDefaultApiGlobalPermission(projectId);
195
+
196
+ expect(permission.globalPermissions).toContain(
197
+ Permission.AuthenticatedRequest,
198
+ );
199
+ });
200
+
201
+ test("a key holding the marker is not mistaken for a bare logged-in user", async () => {
202
+ const permission: UserGlobalAccessPermission =
203
+ await APIKeyAccessPermission.getDefaultApiGlobalPermission(projectId);
204
+
205
+ /*
206
+ * Driven off the real list rather than a copy of it: this is the assertion
207
+ * that fails if the grant is ever removed.
208
+ */
209
+ expect(
210
+ TenantPermission.isAccessGrantedOnlyByCurrentUser(
211
+ File,
212
+ apiKeyProps(permission.globalPermissions),
213
+ DatabaseRequestType.Create,
214
+ ),
215
+ ).toBe(false);
216
+
217
+ // The negative control: without the marker the predicate answers wrongly.
218
+ expect(
219
+ TenantPermission.isAccessGrantedOnlyByCurrentUser(
220
+ File,
221
+ apiKeyProps(GLOBAL_PERMISSIONS_WITHOUT_MARKER),
222
+ DatabaseRequestType.Create,
223
+ ),
224
+ ).toBe(true);
225
+ });
226
+
227
+ test("file creation by API key is allowed either way, because File has no user column", async () => {
228
+ /*
229
+ * Pinned deliberately. The PR that added the grant described this path as
230
+ * broken; it is not, and recording that here keeps the next reader from
231
+ * re-diagnosing a failure that was never happening. What holds it up is
232
+ * File declaring no user column - assert that too, so if File ever gains
233
+ * one this test says which assumption moved.
234
+ */
235
+ expect(new File().getUserColumn()).toBeFalsy();
236
+
237
+ const permission: UserGlobalAccessPermission =
238
+ await APIKeyAccessPermission.getDefaultApiGlobalPermission(projectId);
239
+
240
+ expect(() => {
241
+ return CreatePermission.checkCreatePermissions(
242
+ File,
243
+ newFile(),
244
+ apiKeyProps(permission.globalPermissions),
245
+ );
246
+ }).not.toThrow();
247
+
248
+ expect(() => {
249
+ return CreatePermission.checkCreatePermissions(
250
+ File,
251
+ newFile(),
252
+ apiKeyProps(GLOBAL_PERMISSIONS_WITHOUT_MARKER),
253
+ );
254
+ }).not.toThrow();
255
+ });
256
+
257
+ test("a logged-in user still reaches File without the marker", () => {
258
+ /*
259
+ * AccessTokenService hands a user session [Public, User, CurrentUser] and no
260
+ * AuthenticatedRequest, so users continue to arrive through CurrentUser and
261
+ * are unaffected by the grant. Pinned because the asymmetry is deliberate
262
+ * and reads like an oversight otherwise.
263
+ */
264
+ const userProps: DatabaseCommonInteractionProps = {
265
+ tenantId: projectId,
266
+ userId: userId,
267
+ userType: UserType.User,
268
+ userGlobalAccessPermission: {
269
+ projectIds: [projectId],
270
+ globalPermissions: GLOBAL_PERMISSIONS_WITHOUT_MARKER,
271
+ _type: "UserGlobalAccessPermission",
272
+ },
273
+ userTenantAccessPermission: {
274
+ [projectId.toString()]: tenantPermissions(),
275
+ },
276
+ } as unknown as DatabaseCommonInteractionProps;
277
+
278
+ expect(() => {
279
+ return CreatePermission.checkCreatePermissions(
280
+ File,
281
+ newFile(),
282
+ userProps,
283
+ );
284
+ }).not.toThrow();
285
+ });
286
+ });
@@ -0,0 +1,328 @@
1
+ import QueryPermission from "../../../../../Server/Types/Database/Permissions/QueryPermission";
2
+ import Query from "../../../../../Server/Types/Database/Query";
3
+ import Select from "../../../../../Server/Types/Database/Select";
4
+ import Monitor from "../../../../../Models/DatabaseModels/Monitor";
5
+ import MonitorTemplate from "../../../../../Models/DatabaseModels/MonitorTemplate";
6
+ import BaseModel from "../../../../../Models/DatabaseModels/DatabaseBaseModel/DatabaseBaseModel";
7
+ import DatabaseCommonInteractionProps from "../../../../../Types/BaseDatabase/DatabaseCommonInteractionProps";
8
+ import Includes from "../../../../../Types/BaseDatabase/Includes";
9
+ import IsNull from "../../../../../Types/BaseDatabase/IsNull";
10
+ import NotNull from "../../../../../Types/BaseDatabase/NotNull";
11
+ import { TableColumnMetadata } from "../../../../../Types/Database/TableColumn";
12
+ import TableColumnType from "../../../../../Types/Database/TableColumnType";
13
+ import ObjectID from "../../../../../Types/ObjectID";
14
+ import Permission from "../../../../../Types/Permission";
15
+ import { describe, expect, it } from "@jest/globals";
16
+
17
+ /*
18
+ * The server half of the monitor list's Template column and Template chip
19
+ * (issue #3491).
20
+ *
21
+ * Both of them are one line of UI and one permission rule, and the permission
22
+ * rule is the half that fails loudly for everybody at once:
23
+ *
24
+ * - the column selects `monitorTemplate: { _id, templateName }`, and
25
+ * QueryPermission.checkRelationQueryPermission THROWS on the first inner key
26
+ * that is not marked `canReadOnRelationQuery` rather than dropping it — so a
27
+ * missing flag does not hide the column, it fails the whole monitors list
28
+ * request, for every caller up to ProjectOwner.
29
+ *
30
+ * - the chip filters `monitorTemplateId`, and a WHERE clause is held to the
31
+ * same standard as a SELECT: the column has to be one the caller may read,
32
+ * or the list 403s the moment the chip is set.
33
+ *
34
+ * The selects and queries below are written out as literals rather than
35
+ * imported from the dashboard, so that changing the UI without changing the
36
+ * model surfaces here.
37
+ */
38
+
39
+ const projectId: ObjectID = ObjectID.generate();
40
+ const userId: ObjectID = ObjectID.generate();
41
+ const templateId: ObjectID = ObjectID.generate();
42
+
43
+ /*
44
+ * Fresh props per assertion: DatabaseCommonInteractionPropsUtil MUTATES what it
45
+ * is handed, pushing the auto-granted Public and CurrentUser onto the list, so
46
+ * a shared object carries state between tests.
47
+ */
48
+ function makeProps(
49
+ permissions: Array<Permission>,
50
+ ): DatabaseCommonInteractionProps {
51
+ return {
52
+ userId: userId,
53
+ tenantId: projectId,
54
+ userTenantAccessPermission: {
55
+ [projectId.toString()]: {
56
+ projectId: projectId,
57
+ permissions: permissions.map((permission: Permission) => {
58
+ return {
59
+ permission: permission,
60
+ labelIds: [],
61
+ isBlockPermission: false,
62
+ _type: "UserPermission" as const,
63
+ };
64
+ }),
65
+ _type: "UserTenantAccessPermission",
66
+ },
67
+ },
68
+ };
69
+ }
70
+
71
+ /**
72
+ * Everyone the monitor list is readable by. The failures this file guards
73
+ * against are in the permission layer and are not role-specific, so every
74
+ * assertion that matters is made across the whole read list rather than for one
75
+ * role — the on-call schedule regression this mirrors hit ProjectOwner too.
76
+ */
77
+ const MONITOR_READERS: Array<Permission> = [
78
+ Permission.ProjectOwner,
79
+ Permission.ProjectAdmin,
80
+ Permission.ProjectMember,
81
+ Permission.Viewer,
82
+ Permission.MonitorAdmin,
83
+ Permission.MonitorMember,
84
+ Permission.MonitorViewer,
85
+ Permission.ReadProjectMonitor,
86
+ ];
87
+
88
+ /*
89
+ * The select the Template column declares in
90
+ * App/FeatureSet/Dashboard/src/Components/Monitor/MonitorTable.tsx, alongside
91
+ * the fields the rest of that table asks for.
92
+ */
93
+ const MONITOR_TABLE_SELECT: Select<Monitor> = {
94
+ _id: true,
95
+ name: true,
96
+ monitorType: true,
97
+ monitorTemplate: {
98
+ _id: true,
99
+ templateName: true,
100
+ },
101
+ } as Select<Monitor>;
102
+
103
+ function checkRelationSelect(
104
+ select: Select<Monitor>,
105
+ props: DatabaseCommonInteractionProps,
106
+ ): void {
107
+ QueryPermission.checkRelationQueryPermission(
108
+ Monitor,
109
+ {} as Query<Monitor>,
110
+ select,
111
+ props,
112
+ );
113
+ }
114
+
115
+ function checkQuery(
116
+ query: Query<Monitor>,
117
+ props: DatabaseCommonInteractionProps,
118
+ ): void {
119
+ QueryPermission.checkQueryPermission(Monitor, query, props);
120
+ }
121
+
122
+ describe("selecting a monitor's template through the relation", () => {
123
+ it("accepts the monitor table's select", () => {
124
+ expect(() => {
125
+ return checkRelationSelect(
126
+ MONITOR_TABLE_SELECT,
127
+ makeProps([Permission.ProjectOwner]),
128
+ );
129
+ }).not.toThrow();
130
+ });
131
+
132
+ it("accepts it for every reader of the monitor list", () => {
133
+ for (const permission of MONITOR_READERS) {
134
+ expect(() => {
135
+ return checkRelationSelect(
136
+ MONITOR_TABLE_SELECT,
137
+ makeProps([permission]),
138
+ );
139
+ }).not.toThrow();
140
+ }
141
+ });
142
+
143
+ it("accepts the template name selected on its own", () => {
144
+ // Without `_id` alongside it, in case the cell stops linking to the template.
145
+ expect(() => {
146
+ return checkRelationSelect(
147
+ {
148
+ monitorTemplate: {
149
+ templateName: true,
150
+ },
151
+ } as Select<Monitor>,
152
+ makeProps([Permission.ProjectMember]),
153
+ );
154
+ }).not.toThrow();
155
+ });
156
+
157
+ /*
158
+ * The flag is what makes the select legal, and it is on the related model
159
+ * rather than on Monitor — so nothing in the dashboard or in Monitor.ts
160
+ * mentions it, and dropping it is an easy, silent edit.
161
+ */
162
+ it("is legal because templateName is readable through a relation", () => {
163
+ const template: BaseModel = new MonitorTemplate();
164
+ const metadata: TableColumnMetadata =
165
+ template.getTableColumnMetadata("templateName");
166
+
167
+ expect(metadata.canReadOnRelationQuery).toBe(true);
168
+ expect(metadata.type).toBe(TableColumnType.ShortText);
169
+ });
170
+
171
+ /*
172
+ * The relation traversal only recurses into Entity columns. If Monitor's
173
+ * `monitorTemplate` stopped being one, `monitorTemplate: { ... }` would no
174
+ * longer be a relation select at all and this whole gate would go vacuous.
175
+ */
176
+ it("travels a real Entity relation on Monitor", () => {
177
+ const monitor: BaseModel = new Monitor();
178
+ const metadata: TableColumnMetadata =
179
+ monitor.getTableColumnMetadata("monitorTemplate");
180
+
181
+ expect(metadata.type).toBe(TableColumnType.Entity);
182
+ expect(metadata.modelType).toBe(MonitorTemplate);
183
+ expect(metadata.manyToOneRelationColumn).toBe("monitorTemplateId");
184
+ });
185
+
186
+ /*
187
+ * The flag is not a blanket opening of the template table. A column that
188
+ * does not carry it still refuses, which is what keeps the surface to the
189
+ * one label the column renders.
190
+ */
191
+ it("still refuses a template column that is not marked for relation reads", () => {
192
+ expect(() => {
193
+ return checkRelationSelect(
194
+ {
195
+ monitorTemplate: {
196
+ monitorSteps: true,
197
+ },
198
+ } as Select<Monitor>,
199
+ makeProps([Permission.ProjectOwner]),
200
+ );
201
+ }).toThrow();
202
+ });
203
+
204
+ it("still refuses a column that does not exist on the template", () => {
205
+ expect(() => {
206
+ return checkRelationSelect(
207
+ {
208
+ monitorTemplate: {
209
+ noSuchColumn: true,
210
+ },
211
+ } as unknown as Select<Monitor>,
212
+ makeProps([Permission.ProjectOwner]),
213
+ );
214
+ }).toThrow();
215
+ });
216
+
217
+ it("still refuses a deep relation through the template", () => {
218
+ expect(() => {
219
+ return checkRelationSelect(
220
+ {
221
+ monitorTemplate: {
222
+ project: {
223
+ name: true,
224
+ },
225
+ },
226
+ } as unknown as Select<Monitor>,
227
+ makeProps([Permission.ProjectOwner]),
228
+ );
229
+ }).toThrow("You cannot query deep relations");
230
+ });
231
+ });
232
+
233
+ describe("filtering monitors by their template", () => {
234
+ /*
235
+ * The chip writes the foreign key. A WHERE clause reads a value as surely as
236
+ * a SELECT does, so this has to be a column the caller may read — otherwise
237
+ * setting the chip 403s a list that rendered fine a moment earlier.
238
+ */
239
+ it("accepts the chip's monitorTemplateId filter for every reader", () => {
240
+ for (const permission of MONITOR_READERS) {
241
+ expect(() => {
242
+ return checkQuery(
243
+ {
244
+ projectId: projectId,
245
+ monitorTemplateId: templateId,
246
+ } as Query<Monitor>,
247
+ makeProps([permission]),
248
+ );
249
+ }).not.toThrow();
250
+ }
251
+ });
252
+
253
+ it("accepts the multi-select form the chip actually sends", () => {
254
+ expect(() => {
255
+ return checkQuery(
256
+ {
257
+ projectId: projectId,
258
+ monitorTemplateId: new Includes([templateId, ObjectID.generate()]),
259
+ } as unknown as Query<Monitor>,
260
+ makeProps([Permission.ReadProjectMonitor]),
261
+ );
262
+ }).not.toThrow();
263
+ });
264
+
265
+ /*
266
+ * "Which monitors came from no template" — the rows the Template column
267
+ * renders as "—", and the ones a template rollout has not reached. Only the
268
+ * foreign key can be asked this: there is no template row to join against.
269
+ */
270
+ it("accepts the 'is empty' and 'is not empty' forms", () => {
271
+ expect(() => {
272
+ return checkQuery(
273
+ {
274
+ projectId: projectId,
275
+ monitorTemplateId: new IsNull(),
276
+ } as unknown as Query<Monitor>,
277
+ makeProps([Permission.ReadProjectMonitor]),
278
+ );
279
+ }).not.toThrow();
280
+
281
+ expect(() => {
282
+ return checkQuery(
283
+ {
284
+ projectId: projectId,
285
+ monitorTemplateId: new NotNull(),
286
+ } as unknown as Query<Monitor>,
287
+ makeProps([Permission.ReadProjectMonitor]),
288
+ );
289
+ }).not.toThrow();
290
+ });
291
+
292
+ it("filters a real, indexed column rather than a computed one", () => {
293
+ const monitor: BaseModel = new Monitor();
294
+ const metadata: TableColumnMetadata =
295
+ monitor.getTableColumnMetadata("monitorTemplateId");
296
+
297
+ expect(metadata.type).toBe(TableColumnType.ObjectID);
298
+ expect(monitor.getTableColumns().columns).toContain("monitorTemplateId");
299
+ });
300
+
301
+ /*
302
+ * The counterpart to the accept cases: the gate is really running, so the
303
+ * tests above pass because the column is readable rather than because
304
+ * nothing was checked.
305
+ */
306
+ it("still refuses a column that does not exist on Monitor", () => {
307
+ expect(() => {
308
+ return checkQuery(
309
+ {
310
+ projectId: projectId,
311
+ monitorTemplateName: "Production API Health",
312
+ } as unknown as Query<Monitor>,
313
+ makeProps([Permission.ProjectOwner]),
314
+ );
315
+ }).toThrow("Column does not exist");
316
+ });
317
+
318
+ it("refuses the filter for a caller with no read on monitors at all", () => {
319
+ expect(() => {
320
+ return checkQuery(
321
+ {
322
+ monitorTemplateId: templateId,
323
+ } as Query<Monitor>,
324
+ makeProps([Permission.ReadProjectIncident]),
325
+ );
326
+ }).toThrow();
327
+ });
328
+ });
@@ -0,0 +1,83 @@
1
+ import QueryHelper from "../../../../Server/Types/Database/QueryHelper";
2
+ import { describe, expect, it } from "@jest/globals";
3
+
4
+ /*
5
+ * Fills the last two gaps in QueryHelper coverage that the sibling
6
+ * QueryHelper*.test.ts files leave untouched: the case-insensitive single-value
7
+ * `findWithSameText` and the parameterless `isNull` predicate.
8
+ *
9
+ * As with every QueryHelper operator, the SQL is produced by a function on the
10
+ * FindOperator's `.value`, given a column alias, and the comparand must live in
11
+ * the bound-parameter bag rather than the SQL text — these operators filter on
12
+ * user-typed / ingest-stamped identifiers, so an interpolated value would be an
13
+ * injection vector.
14
+ */
15
+
16
+ interface RawOperator {
17
+ type: string;
18
+ objectLiteralParameters: Record<string, unknown>;
19
+ getSql: (aliasPath: string) => string;
20
+ }
21
+
22
+ function asRaw(operator: unknown): RawOperator {
23
+ return operator as unknown as RawOperator;
24
+ }
25
+
26
+ const ALIAS: string = '"Monitor"."name"';
27
+
28
+ function paramValues(operator: RawOperator): Array<unknown> {
29
+ return Object.values(operator.objectLiteralParameters ?? {});
30
+ }
31
+
32
+ describe("QueryHelper.findWithSameText", () => {
33
+ it("wraps the column in LOWER() and lower-cases a string comparand", () => {
34
+ const op: RawOperator = asRaw(QueryHelper.findWithSameText("Production"));
35
+ const sql: string = op.getSql(ALIAS);
36
+
37
+ // A string comparison must be case-insensitive on BOTH sides.
38
+ expect(sql.toUpperCase()).toContain("LOWER(");
39
+ expect(sql).toContain(`LOWER(${ALIAS}) =`);
40
+
41
+ // Value is bound, lower-cased, never interpolated.
42
+ expect(sql).not.toContain("Production");
43
+ expect(sql).not.toContain("production");
44
+ expect(paramValues(op)).toEqual(["production"]);
45
+ });
46
+
47
+ it("trims surrounding whitespace off a string comparand", () => {
48
+ const op: RawOperator = asRaw(QueryHelper.findWithSameText(" Prod "));
49
+ expect(paramValues(op)).toEqual(["prod"]);
50
+ });
51
+
52
+ it("compares a number without LOWER() and binds it as a string", () => {
53
+ const op: RawOperator = asRaw(QueryHelper.findWithSameText(42));
54
+ const sql: string = op.getSql(ALIAS);
55
+
56
+ // Numbers can't be lower-cased; the column must be compared directly.
57
+ expect(sql.toUpperCase()).not.toContain("LOWER(");
58
+ expect(sql).toContain(`(${ALIAS} =`);
59
+
60
+ // The value is still routed through the parameter bag, stringified.
61
+ expect(sql).not.toContain("42");
62
+ expect(paramValues(op)).toEqual(["42"]);
63
+ });
64
+
65
+ it("binds exactly one parameter", () => {
66
+ const op: RawOperator = asRaw(QueryHelper.findWithSameText("anything"));
67
+ expect(Object.keys(op.objectLiteralParameters)).toHaveLength(1);
68
+ });
69
+ });
70
+
71
+ describe("QueryHelper.isNull", () => {
72
+ it("emits IS NULL and binds nothing", () => {
73
+ const op: RawOperator = asRaw(QueryHelper.isNull());
74
+ const sql: string = op.getSql(ALIAS);
75
+
76
+ expect(sql.toUpperCase()).toContain("IS NULL");
77
+ expect(sql.toUpperCase()).not.toContain("IS NOT NULL");
78
+ expect(sql).toContain(ALIAS);
79
+
80
+ // A pure IS NULL predicate has no comparand to bind.
81
+ expect(Object.keys(op.objectLiteralParameters ?? {})).toHaveLength(0);
82
+ });
83
+ });