@oneuptime/common 12.0.1 → 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 (205) 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/PodmanResource.ts +6 -6
  5. package/Models/DatabaseModels/ServiceLevelObjective.ts +91 -0
  6. package/Models/DatabaseModels/StatusPage.ts +95 -0
  7. package/Server/API/TeamMemberAPI.ts +85 -0
  8. package/Server/Infrastructure/Postgres/SchemaMigrations/1785900000000-AddSloMonitorLabelRule.ts +84 -0
  9. package/Server/Infrastructure/Postgres/SchemaMigrations/1785915638551-IncreaseHostIpAddressesLength.ts +40 -0
  10. package/Server/Infrastructure/Postgres/SchemaMigrations/1785930000000-WidenInventoryResourceNameColumns.ts +131 -0
  11. package/Server/Infrastructure/Postgres/SchemaMigrations/1785930709405-AddStatusPageReportPeriod.ts +25 -0
  12. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +8 -0
  13. package/Server/Middleware/ProjectAuthorization.ts +66 -4
  14. package/Server/Services/BillingService.ts +600 -51
  15. package/Server/Services/CephClusterService.ts +47 -5
  16. package/Server/Services/CephResourceService.ts +43 -4
  17. package/Server/Services/CloudResourceService.ts +47 -5
  18. package/Server/Services/DatabaseService.ts +87 -6
  19. package/Server/Services/DockerHostService.ts +47 -5
  20. package/Server/Services/DockerResourceService.ts +62 -8
  21. package/Server/Services/DockerSwarmClusterService.ts +48 -5
  22. package/Server/Services/DockerSwarmResourceService.ts +53 -4
  23. package/Server/Services/HostService.ts +48 -5
  24. package/Server/Services/IoTDeviceService.ts +1 -17
  25. package/Server/Services/IoTFleetService.ts +47 -5
  26. package/Server/Services/KubernetesClusterService.ts +48 -5
  27. package/Server/Services/KubernetesContainerService.ts +50 -9
  28. package/Server/Services/KubernetesResourceService.ts +53 -7
  29. package/Server/Services/LabelService.ts +106 -1
  30. package/Server/Services/MonitorService.ts +57 -0
  31. package/Server/Services/PodmanHostService.ts +47 -5
  32. package/Server/Services/PodmanResourceService.ts +62 -8
  33. package/Server/Services/ProjectService.ts +165 -35
  34. package/Server/Services/ProxmoxClusterService.ts +47 -5
  35. package/Server/Services/ProxmoxResourceService.ts +43 -4
  36. package/Server/Services/RumApplicationService.ts +48 -5
  37. package/Server/Services/ServerlessFunctionService.ts +48 -5
  38. package/Server/Services/ServiceLevelObjectiveMonitorRuleEngineService.ts +426 -0
  39. package/Server/Services/ServiceLevelObjectiveService.ts +39 -0
  40. package/Server/Services/StatusPageService.ts +95 -36
  41. package/Server/Services/StatusPageSubscriberNotificationTemplateService.ts +18 -1
  42. package/Server/Services/StatusPageSubscriberService.ts +3 -0
  43. package/Server/Types/Database/JSONColumnQuery.ts +621 -0
  44. package/Server/Types/Database/Permissions/PublicPermission.ts +9 -2
  45. package/Server/Types/Database/QueryHelper.ts +17 -32
  46. package/Server/Utils/Database/TruncateColumnValue.ts +84 -0
  47. package/Tests/App/Dashboard/UsersTableGroupsByPerson.test.tsx +448 -0
  48. package/Tests/Server/API/BaseAPIApiKeyAuth.test.ts +704 -0
  49. package/Tests/Server/API/TeamMemberRemoveUserFromProjectAPI.test.ts +303 -0
  50. package/Tests/Server/Middleware/MasterAdminAuthorization.test.ts +350 -0
  51. package/Tests/Server/Middleware/ProjectAuthorizationApiKeyHeader.test.ts +362 -0
  52. package/Tests/Server/Middleware/ProjectAuthorizationApiKeyMiddleware.test.ts +687 -0
  53. package/Tests/Server/Services/BillingService.test.ts +78 -3
  54. package/Tests/Server/Services/BillingServiceChangePlanCancel.test.ts +645 -0
  55. package/Tests/Server/Services/BillingServiceTrialPlanChange.test.ts +1005 -0
  56. package/Tests/Server/Services/HookFreeWriteLengthClamp.test.ts +503 -0
  57. package/Tests/Server/Services/HostIpAddressesColumnWidth.test.ts +198 -0
  58. package/Tests/Server/Services/HostServiceUpdateLastSeen.test.ts +510 -0
  59. package/Tests/Server/Services/InventoryResourceNameColumnWidth.test.ts +500 -0
  60. package/Tests/Server/Services/InventoryUpsertSchemaParity.test.ts +586 -0
  61. package/Tests/Server/Services/InventoryUpsertTruncationGuard.test.ts +1242 -0
  62. package/Tests/Server/Services/ProjectServiceChangePlan.test.ts +650 -0
  63. package/Tests/Server/Services/ProjectServiceExtendTrial.test.ts +1 -0
  64. package/Tests/Server/Services/ResourceUpdateLastSeenLivenessFallback.test.ts +387 -0
  65. package/Tests/Server/Services/ServiceLevelObjectiveMonitorRuleEngineService.test.ts +770 -0
  66. package/Tests/Server/Services/ServiceLevelObjectiveService.test.ts +130 -0
  67. package/Tests/Server/Services/SloMonitorLabelRuleHooks.test.ts +352 -0
  68. package/Tests/Server/Services/StatusPageSubscriberReport.test.ts +29 -13
  69. package/Tests/Server/TestingUtils/Services/BillingServiceHelper.ts +69 -8
  70. package/Tests/Server/Types/Database/JSONColumnQuery.test.ts +593 -0
  71. package/Tests/Server/Types/Database/Permissions/PublicPermission.test.ts +495 -0
  72. package/Tests/Server/Types/Database/QueryUtil.test.ts +113 -0
  73. package/Tests/Server/Utils/Database/TruncateColumnValue.test.ts +179 -0
  74. package/Tests/Types/Events/Recurring.test.ts +157 -0
  75. package/Tests/UI/Components/ModelTable/ModelTableGroupsProjectUsers.test.tsx +375 -0
  76. package/Tests/UI/Components/ModelTable/useCustomFieldColumns.test.tsx +196 -0
  77. package/Tests/UI/Utils/ProjectUsersModelAPI.test.ts +739 -0
  78. package/Tests/UI/Utils/TeamMembersByUser.test.ts +633 -0
  79. package/Tests/Utils/StatusPage/ReportPeriod.test.ts +218 -0
  80. package/Tests/Utils/Telemetry/HostIpAddresses.test.ts +291 -0
  81. package/Types/CustomField/CustomFieldDefinition.ts +24 -0
  82. package/Types/Date.ts +112 -0
  83. package/Types/Events/Recurring.ts +99 -1
  84. package/Types/StatusPage/StatusPageReport.ts +11 -0
  85. package/Types/StatusPage/StatusPageReportPeriodType.ts +21 -0
  86. package/UI/Components/ModelTable/CustomFieldColumns.tsx +2 -6
  87. package/UI/Components/ModelTable/useCustomFieldColumns.ts +21 -2
  88. package/UI/Utils/ModelAPI/ProjectUsersModelAPI.ts +305 -0
  89. package/UI/Utils/TeamMembersByUser.ts +302 -0
  90. package/Utils/StatusPage/ReportPeriod.ts +236 -0
  91. package/Utils/Telemetry/HostIpAddresses.ts +98 -0
  92. package/build/dist/Models/DatabaseModels/Host.js +12 -4
  93. package/build/dist/Models/DatabaseModels/Host.js.map +1 -1
  94. package/build/dist/Models/DatabaseModels/KubernetesContainer.js +4 -4
  95. package/build/dist/Models/DatabaseModels/KubernetesContainer.js.map +1 -1
  96. package/build/dist/Models/DatabaseModels/KubernetesResource.js +7 -7
  97. package/build/dist/Models/DatabaseModels/KubernetesResource.js.map +1 -1
  98. package/build/dist/Models/DatabaseModels/PodmanResource.js +6 -6
  99. package/build/dist/Models/DatabaseModels/PodmanResource.js.map +1 -1
  100. package/build/dist/Models/DatabaseModels/ServiceLevelObjective.js +87 -0
  101. package/build/dist/Models/DatabaseModels/ServiceLevelObjective.js.map +1 -1
  102. package/build/dist/Models/DatabaseModels/StatusPage.js +98 -0
  103. package/build/dist/Models/DatabaseModels/StatusPage.js.map +1 -1
  104. package/build/dist/Server/API/TeamMemberAPI.js +57 -2
  105. package/build/dist/Server/API/TeamMemberAPI.js.map +1 -1
  106. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1785900000000-AddSloMonitorLabelRule.js +45 -0
  107. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1785900000000-AddSloMonitorLabelRule.js.map +1 -0
  108. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1785915638551-IncreaseHostIpAddressesLength.js +31 -0
  109. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1785915638551-IncreaseHostIpAddressesLength.js.map +1 -0
  110. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1785930000000-WidenInventoryResourceNameColumns.js +90 -0
  111. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1785930000000-WidenInventoryResourceNameColumns.js.map +1 -0
  112. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1785930709405-AddStatusPageReportPeriod.js +14 -0
  113. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1785930709405-AddStatusPageReportPeriod.js.map +1 -0
  114. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +8 -0
  115. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  116. package/build/dist/Server/Middleware/ProjectAuthorization.js +59 -4
  117. package/build/dist/Server/Middleware/ProjectAuthorization.js.map +1 -1
  118. package/build/dist/Server/Services/BillingService.js +466 -36
  119. package/build/dist/Server/Services/BillingService.js.map +1 -1
  120. package/build/dist/Server/Services/CephClusterService.js +42 -5
  121. package/build/dist/Server/Services/CephClusterService.js.map +1 -1
  122. package/build/dist/Server/Services/CephResourceService.js +28 -3
  123. package/build/dist/Server/Services/CephResourceService.js.map +1 -1
  124. package/build/dist/Server/Services/CloudResourceService.js +42 -5
  125. package/build/dist/Server/Services/CloudResourceService.js.map +1 -1
  126. package/build/dist/Server/Services/DatabaseService.js +69 -2
  127. package/build/dist/Server/Services/DatabaseService.js.map +1 -1
  128. package/build/dist/Server/Services/DockerHostService.js +42 -5
  129. package/build/dist/Server/Services/DockerHostService.js.map +1 -1
  130. package/build/dist/Server/Services/DockerResourceService.js +34 -4
  131. package/build/dist/Server/Services/DockerResourceService.js.map +1 -1
  132. package/build/dist/Server/Services/DockerSwarmClusterService.js +43 -5
  133. package/build/dist/Server/Services/DockerSwarmClusterService.js.map +1 -1
  134. package/build/dist/Server/Services/DockerSwarmResourceService.js +28 -3
  135. package/build/dist/Server/Services/DockerSwarmResourceService.js.map +1 -1
  136. package/build/dist/Server/Services/HostService.js +43 -5
  137. package/build/dist/Server/Services/HostService.js.map +1 -1
  138. package/build/dist/Server/Services/IoTDeviceService.js +1 -8
  139. package/build/dist/Server/Services/IoTDeviceService.js.map +1 -1
  140. package/build/dist/Server/Services/IoTFleetService.js +42 -5
  141. package/build/dist/Server/Services/IoTFleetService.js.map +1 -1
  142. package/build/dist/Server/Services/KubernetesClusterService.js +43 -5
  143. package/build/dist/Server/Services/KubernetesClusterService.js.map +1 -1
  144. package/build/dist/Server/Services/KubernetesContainerService.js +29 -3
  145. package/build/dist/Server/Services/KubernetesContainerService.js.map +1 -1
  146. package/build/dist/Server/Services/KubernetesResourceService.js +32 -4
  147. package/build/dist/Server/Services/KubernetesResourceService.js.map +1 -1
  148. package/build/dist/Server/Services/LabelService.js +92 -0
  149. package/build/dist/Server/Services/LabelService.js.map +1 -1
  150. package/build/dist/Server/Services/MonitorService.js +47 -0
  151. package/build/dist/Server/Services/MonitorService.js.map +1 -1
  152. package/build/dist/Server/Services/PodmanHostService.js +42 -5
  153. package/build/dist/Server/Services/PodmanHostService.js.map +1 -1
  154. package/build/dist/Server/Services/PodmanResourceService.js +34 -4
  155. package/build/dist/Server/Services/PodmanResourceService.js.map +1 -1
  156. package/build/dist/Server/Services/ProjectService.js +135 -21
  157. package/build/dist/Server/Services/ProjectService.js.map +1 -1
  158. package/build/dist/Server/Services/ProxmoxClusterService.js +42 -5
  159. package/build/dist/Server/Services/ProxmoxClusterService.js.map +1 -1
  160. package/build/dist/Server/Services/ProxmoxResourceService.js +28 -3
  161. package/build/dist/Server/Services/ProxmoxResourceService.js.map +1 -1
  162. package/build/dist/Server/Services/RumApplicationService.js +43 -5
  163. package/build/dist/Server/Services/RumApplicationService.js.map +1 -1
  164. package/build/dist/Server/Services/ServerlessFunctionService.js +43 -5
  165. package/build/dist/Server/Services/ServerlessFunctionService.js.map +1 -1
  166. package/build/dist/Server/Services/ServiceLevelObjectiveMonitorRuleEngineService.js +347 -0
  167. package/build/dist/Server/Services/ServiceLevelObjectiveMonitorRuleEngineService.js.map +1 -0
  168. package/build/dist/Server/Services/ServiceLevelObjectiveService.js +33 -1
  169. package/build/dist/Server/Services/ServiceLevelObjectiveService.js.map +1 -1
  170. package/build/dist/Server/Services/StatusPageService.js +69 -46
  171. package/build/dist/Server/Services/StatusPageService.js.map +1 -1
  172. package/build/dist/Server/Services/StatusPageSubscriberNotificationTemplateService.js +17 -1
  173. package/build/dist/Server/Services/StatusPageSubscriberNotificationTemplateService.js.map +1 -1
  174. package/build/dist/Server/Services/StatusPageSubscriberService.js +3 -0
  175. package/build/dist/Server/Services/StatusPageSubscriberService.js.map +1 -1
  176. package/build/dist/Server/Types/Database/JSONColumnQuery.js +402 -0
  177. package/build/dist/Server/Types/Database/JSONColumnQuery.js.map +1 -0
  178. package/build/dist/Server/Types/Database/Permissions/PublicPermission.js +9 -2
  179. package/build/dist/Server/Types/Database/Permissions/PublicPermission.js.map +1 -1
  180. package/build/dist/Server/Types/Database/QueryHelper.js +17 -27
  181. package/build/dist/Server/Types/Database/QueryHelper.js.map +1 -1
  182. package/build/dist/Server/Utils/Database/TruncateColumnValue.js +32 -0
  183. package/build/dist/Server/Utils/Database/TruncateColumnValue.js.map +1 -0
  184. package/build/dist/Types/CustomField/CustomFieldDefinition.js +2 -0
  185. package/build/dist/Types/CustomField/CustomFieldDefinition.js.map +1 -0
  186. package/build/dist/Types/Date.js +68 -0
  187. package/build/dist/Types/Date.js.map +1 -1
  188. package/build/dist/Types/Events/Recurring.js +63 -0
  189. package/build/dist/Types/Events/Recurring.js.map +1 -1
  190. package/build/dist/Types/StatusPage/StatusPageReportPeriodType.js +22 -0
  191. package/build/dist/Types/StatusPage/StatusPageReportPeriodType.js.map +1 -0
  192. package/build/dist/UI/Components/ModelTable/CustomFieldColumns.js.map +1 -1
  193. package/build/dist/UI/Components/ModelTable/useCustomFieldColumns.js +18 -2
  194. package/build/dist/UI/Components/ModelTable/useCustomFieldColumns.js.map +1 -1
  195. package/build/dist/UI/Utils/ModelAPI/ProjectUsersModelAPI.js +213 -0
  196. package/build/dist/UI/Utils/ModelAPI/ProjectUsersModelAPI.js.map +1 -0
  197. package/build/dist/UI/Utils/TeamMembersByUser.js +195 -0
  198. package/build/dist/UI/Utils/TeamMembersByUser.js.map +1 -0
  199. package/build/dist/Utils/StatusPage/ReportPeriod.js +131 -0
  200. package/build/dist/Utils/StatusPage/ReportPeriod.js.map +1 -0
  201. package/build/dist/Utils/Telemetry/HostIpAddresses.js +82 -0
  202. package/build/dist/Utils/Telemetry/HostIpAddresses.js.map +1 -0
  203. package/jest.config.json +2 -0
  204. package/package.json +1 -1
  205. package/tsconfig.json +12 -1
@@ -0,0 +1,687 @@
1
+ import APIKeyPermission from "../../../Models/DatabaseModels/ApiKeyPermission";
2
+ import GlobalConfig from "../../../Models/DatabaseModels/GlobalConfig";
3
+ import User from "../../../Models/DatabaseModels/User";
4
+ import ProjectMiddleware from "../../../Server/Middleware/ProjectAuthorization";
5
+ import ApiKeyPermissionService from "../../../Server/Services/ApiKeyPermissionService";
6
+ import ApiKeyService from "../../../Server/Services/ApiKeyService";
7
+ import GlobalConfigService from "../../../Server/Services/GlobalConfigService";
8
+ import UserService from "../../../Server/Services/UserService";
9
+ import FindOneBy from "../../../Server/Types/Database/FindOneBy";
10
+ import {
11
+ ExpressRequest,
12
+ ExpressResponse,
13
+ NextFunction,
14
+ OneUptimeRequest,
15
+ } from "../../../Server/Utils/Express";
16
+ import Dictionary from "../../../Types/Dictionary";
17
+ import Email from "../../../Types/Email";
18
+ import BadDataException from "../../../Types/Exception/BadDataException";
19
+ import Exception from "../../../Types/Exception/Exception";
20
+ import ExceptionCode from "../../../Types/Exception/ExceptionCode";
21
+ import NotAuthenticatedException from "../../../Types/Exception/NotAuthenticatedException";
22
+ import Name from "../../../Types/Name";
23
+ import ObjectID from "../../../Types/ObjectID";
24
+ import Permission, {
25
+ UserPermission,
26
+ UserTenantAccessPermission,
27
+ } from "../../../Types/Permission";
28
+ import UserType from "../../../Types/UserType";
29
+ import getJestMockFunction, { MockFunction } from "../../../Tests/MockType";
30
+ import { getJestSpyOn } from "../../../Tests/Spy";
31
+ import {
32
+ afterAll,
33
+ beforeEach,
34
+ describe,
35
+ expect,
36
+ jest,
37
+ test,
38
+ } from "@jest/globals";
39
+
40
+ /*
41
+ * ProjectAuthorization.isValidProjectIdAndApiKeyMiddleware is the single door
42
+ * every API-key request in the product walks through, and it had no test at all.
43
+ *
44
+ * Issue #3004 is what that costs. A caller sent a real project API key, the
45
+ * request never reached this middleware (hasApiKey used to mean "usable key",
46
+ * not "key header present"), and it fell through to the anonymous path where a
47
+ * per-model permission check answered 401 "A user should be logged in to read
48
+ * record of Log Pipeline." — an error about session auth, naming a model, at a
49
+ * caller who had attempted neither. It was reported as an RBAC bug in Log
50
+ * Pipeline. Issue #1754 was the same string on a different model.
51
+ *
52
+ * These tests pin the middleware's whole contract: what a resolved key puts on
53
+ * the request (including that the KEY decides the tenant, not the caller), and
54
+ * that every unusable key shape leaves as BadDataException("Invalid API Key")
55
+ * through next(err) rather than being thrown, mis-typed, or handed to Postgres.
56
+ */
57
+ describe("ProjectMiddleware.isValidProjectIdAndApiKeyMiddleware", () => {
58
+ const apiKeyValue: ObjectID = ObjectID.generate();
59
+ const apiKeyId: ObjectID = ObjectID.generate();
60
+ const keyProjectId: ObjectID = ObjectID.generate();
61
+ const masterApiKeyValue: ObjectID = ObjectID.generate();
62
+
63
+ const res: ExpressResponse = {} as ExpressResponse;
64
+ let next: MockFunction;
65
+
66
+ const spyFindApiKey: jest.SpyInstance = getJestSpyOn(
67
+ ApiKeyService,
68
+ "findApiKey",
69
+ );
70
+ const spyApiKeyPermissionFindBy: jest.SpyInstance = getJestSpyOn(
71
+ ApiKeyPermissionService,
72
+ "findBy",
73
+ );
74
+ const spyGlobalConfigFindOneBy: jest.SpyInstance = getJestSpyOn(
75
+ GlobalConfigService,
76
+ "findOneBy",
77
+ );
78
+ const spyUserFindOneBy: jest.SpyInstance = getJestSpyOn(
79
+ UserService,
80
+ "findOneBy",
81
+ );
82
+
83
+ /*
84
+ * Defaults describe the most hostile world: the key is unknown, the master
85
+ * key is disabled and there is no master admin. Every test opts in to the
86
+ * lookups it actually wants to succeed, so nothing passes by accident.
87
+ */
88
+ beforeEach(() => {
89
+ jest.clearAllMocks();
90
+ next = getJestMockFunction();
91
+ spyFindApiKey.mockResolvedValue(null);
92
+ spyApiKeyPermissionFindBy.mockResolvedValue([]);
93
+ spyGlobalConfigFindOneBy.mockResolvedValue(null);
94
+ spyUserFindOneBy.mockResolvedValue(null);
95
+ });
96
+
97
+ afterAll(() => {
98
+ jest.restoreAllMocks();
99
+ });
100
+
101
+ type RunMiddlewareFunction = (req: Partial<ExpressRequest>) => Promise<void>;
102
+
103
+ /*
104
+ * The middleware's own catch calls next(err); it must never reject. Every
105
+ * call site goes through here so that "errors arrive via next, not as a
106
+ * thrown exception" is asserted once per scenario rather than assumed.
107
+ */
108
+ const runMiddleware: RunMiddlewareFunction = async (
109
+ req: Partial<ExpressRequest>,
110
+ ): Promise<void> => {
111
+ await expect(
112
+ ProjectMiddleware.isValidProjectIdAndApiKeyMiddleware(
113
+ req as ExpressRequest,
114
+ res,
115
+ next as NextFunction,
116
+ ),
117
+ ).resolves.toBeUndefined();
118
+ };
119
+
120
+ type GetErrorPassedToNextFunction = () => Exception;
121
+
122
+ const getErrorPassedToNext: GetErrorPassedToNextFunction = (): Exception => {
123
+ expect(next).toHaveBeenCalledTimes(1);
124
+
125
+ return next.mock.calls[0]![0] as Exception;
126
+ };
127
+
128
+ describe("when the API key resolves to a project", () => {
129
+ type MockResolvedApiKeyFunction = () => void;
130
+
131
+ const mockResolvedApiKey: MockResolvedApiKeyFunction = (): void => {
132
+ const apiKeyPermission: APIKeyPermission = new APIKeyPermission();
133
+ apiKeyPermission.permission = Permission.ProjectMember;
134
+ apiKeyPermission.labels = [];
135
+ apiKeyPermission.isBlockPermission = false;
136
+
137
+ spyFindApiKey.mockResolvedValue({
138
+ id: apiKeyId,
139
+ projectId: keyProjectId,
140
+ });
141
+ spyApiKeyPermissionFindBy.mockResolvedValue([apiKeyPermission]);
142
+ };
143
+
144
+ test("should call next with no argument and stamp API identity, tenant and permissions on the request", async () => {
145
+ mockResolvedApiKey();
146
+
147
+ const req: Partial<ExpressRequest> = {
148
+ headers: { apikey: apiKeyValue.toString() },
149
+ };
150
+
151
+ await runMiddleware(req);
152
+
153
+ // next() with zero arguments — an argument would be routed as an error.
154
+ expect(next).toHaveBeenCalledTimes(1);
155
+ expect(next.mock.calls[0]).toEqual([]);
156
+
157
+ // The header value is what gets looked up, parsed into an ObjectID.
158
+ expect(spyFindApiKey).toHaveBeenCalledWith(
159
+ new ObjectID(apiKeyValue.toString()),
160
+ );
161
+
162
+ const oneUptimeRequest: OneUptimeRequest = req as OneUptimeRequest;
163
+
164
+ expect(oneUptimeRequest.userType).toEqual(UserType.API);
165
+ expect(oneUptimeRequest.tenantId?.toString()).toEqual(
166
+ keyProjectId.toString(),
167
+ );
168
+ expect(
169
+ oneUptimeRequest.userGlobalAccessPermission?.projectIds.map(
170
+ (projectId: ObjectID) => {
171
+ return projectId.toString();
172
+ },
173
+ ),
174
+ ).toEqual([keyProjectId.toString()]);
175
+
176
+ /*
177
+ * A project key gets the DEFAULT api global permission, never the master
178
+ * key's. The two builders differ by exactly one entry — ProjectOwner —
179
+ * so swapping them (they sit next to each other in
180
+ * Utils/APIKey/AccessPermission) would hand every project API key owner
181
+ * rights on its project with nothing else in the request looking wrong.
182
+ */
183
+ expect(
184
+ oneUptimeRequest.userGlobalAccessPermission?.globalPermissions,
185
+ ).not.toContain(Permission.ProjectOwner);
186
+
187
+ const tenantAccessPermission:
188
+ | Dictionary<UserTenantAccessPermission>
189
+ | undefined = oneUptimeRequest.userTenantAccessPermission;
190
+
191
+ expect(Object.keys(tenantAccessPermission || {})).toEqual([
192
+ keyProjectId.toString(),
193
+ ]);
194
+ expect(
195
+ tenantAccessPermission?.[keyProjectId.toString()]?.projectId.toString(),
196
+ ).toEqual(keyProjectId.toString());
197
+ });
198
+
199
+ /*
200
+ * The permissions attached to the request are looked up by the KEY's id,
201
+ * and the rows found are what the request is allowed to do. Without this
202
+ * the key would authenticate but carry no granted permission, which is the
203
+ * 422 "You do not have permissions to ..." the API answers.
204
+ */
205
+ test("should carry the permissions granted to that specific API key", async () => {
206
+ mockResolvedApiKey();
207
+
208
+ const req: Partial<ExpressRequest> = {
209
+ headers: { apikey: apiKeyValue.toString() },
210
+ };
211
+
212
+ await runMiddleware(req);
213
+
214
+ expect(spyApiKeyPermissionFindBy).toHaveBeenCalledWith(
215
+ expect.objectContaining({
216
+ query: { apiKeyId: apiKeyId },
217
+ }),
218
+ );
219
+
220
+ const permissions: Array<UserPermission> =
221
+ (req as OneUptimeRequest).userTenantAccessPermission?.[
222
+ keyProjectId.toString()
223
+ ]?.permissions || [];
224
+
225
+ expect(
226
+ permissions.map((permission: UserPermission) => {
227
+ return permission.permission;
228
+ }),
229
+ ).toContain(Permission.ProjectMember);
230
+ });
231
+
232
+ /*
233
+ * Header shapes that a real caller sends and that MUST still resolve.
234
+ *
235
+ * Padding: a key pasted out of the dashboard or read from a file arrives
236
+ * with surrounding whitespace or a trailing newline (`$(cat key.txt)`).
237
+ * Untrimmed, that string reached the `uuid` column verbatim and raised
238
+ * 22P02, so a copy/paste answered 500.
239
+ *
240
+ * Case: UUID_REGEX carries the `i` flag on purpose. Postgres `uuid`
241
+ * compares case-insensitively and plenty of clients upper-case ids, so
242
+ * losing that flag would start rejecting keys that the database itself
243
+ * considers identical — and the new shape check would turn that into a
244
+ * flat "Invalid API Key" for a key that is genuinely valid.
245
+ *
246
+ * Tuples are [label, header value, value the lookup must receive].
247
+ */
248
+ const resolvableHeaderShapes: Array<[string, string, string]> = [
249
+ [
250
+ "whitespace-padded",
251
+ ` ${apiKeyValue.toString()} `,
252
+ apiKeyValue.toString(),
253
+ ],
254
+ [
255
+ "newline-terminated",
256
+ `${apiKeyValue.toString()}\n`,
257
+ apiKeyValue.toString(),
258
+ ],
259
+ [
260
+ "upper-cased",
261
+ apiKeyValue.toString().toUpperCase(),
262
+ apiKeyValue.toString().toUpperCase(),
263
+ ],
264
+ ];
265
+
266
+ test.each(resolvableHeaderShapes)(
267
+ "should resolve a %s apikey header",
268
+ async (_label: string, headerValue: string, lookedUpValue: string) => {
269
+ mockResolvedApiKey();
270
+
271
+ const req: Partial<ExpressRequest> = {
272
+ headers: { apikey: headerValue },
273
+ };
274
+
275
+ await runMiddleware(req);
276
+
277
+ expect(next.mock.calls[0]).toEqual([]);
278
+ expect(spyFindApiKey).toHaveBeenCalledWith(new ObjectID(lookedUpValue));
279
+ expect((req as OneUptimeRequest).userType).toEqual(UserType.API);
280
+ },
281
+ );
282
+
283
+ /*
284
+ * SECURITY: the tenant is decided by the API key, never by the caller.
285
+ *
286
+ * The caller asks for a project they do not own via the `tenantid` header;
287
+ * the request is still scoped to the key's own project. #3004's reporter
288
+ * tried adding projectId to the body, query and headers to make the call
289
+ * work — this test documents why that could never have helped, and, more
290
+ * importantly, why it must never help.
291
+ */
292
+ test("should overwrite a caller-supplied tenantid with the project the key belongs to", async () => {
293
+ mockResolvedApiKey();
294
+
295
+ const otherProjectId: ObjectID = ObjectID.generate();
296
+
297
+ const req: Partial<ExpressRequest> = {
298
+ headers: {
299
+ apikey: apiKeyValue.toString(),
300
+ tenantid: otherProjectId.toString(),
301
+ },
302
+ };
303
+
304
+ await runMiddleware(req);
305
+
306
+ expect(next.mock.calls[0]).toEqual([]);
307
+
308
+ const oneUptimeRequest: OneUptimeRequest = req as OneUptimeRequest;
309
+
310
+ expect(oneUptimeRequest.tenantId?.toString()).toEqual(
311
+ keyProjectId.toString(),
312
+ );
313
+ expect(oneUptimeRequest.tenantId?.toString()).not.toEqual(
314
+ otherProjectId.toString(),
315
+ );
316
+ expect(
317
+ Object.keys(oneUptimeRequest.userTenantAccessPermission || {}),
318
+ ).toEqual([keyProjectId.toString()]);
319
+ expect(
320
+ oneUptimeRequest.userGlobalAccessPermission?.projectIds.map(
321
+ (projectId: ObjectID) => {
322
+ return projectId.toString();
323
+ },
324
+ ),
325
+ ).not.toContain(otherProjectId.toString());
326
+ });
327
+ });
328
+
329
+ describe("when the API key cannot be used", () => {
330
+ /*
331
+ * A syntactically fine key that no project owns (revoked, expired, typo'd,
332
+ * or copied from another instance) and that is not the master key. The
333
+ * caller must be told the key is bad, not that some model is unreadable.
334
+ */
335
+ test("should call next with BadDataException 'Invalid API Key' for an unknown but well-formed key", async () => {
336
+ const req: Partial<ExpressRequest> = {
337
+ headers: { apikey: apiKeyValue.toString() },
338
+ };
339
+
340
+ await runMiddleware(req);
341
+
342
+ const error: Exception = getErrorPassedToNext();
343
+
344
+ expect(error).toBeInstanceOf(BadDataException);
345
+ expect(error.message).toEqual("Invalid API Key");
346
+ expect(error.code).toEqual(ExceptionCode.BadDataException);
347
+
348
+ // The key was looked up, and the master-key fallback was consulted.
349
+ expect(spyFindApiKey).toHaveBeenCalledTimes(1);
350
+ expect(spyGlobalConfigFindOneBy).toHaveBeenCalledTimes(1);
351
+
352
+ /*
353
+ * A rejected request must not walk away wearing an identity. Express
354
+ * still owns this `req` object, and a later handler that reads userType
355
+ * or userTenantAccessPermission without re-checking would treat it as an
356
+ * authenticated API caller.
357
+ */
358
+ expect((req as OneUptimeRequest).userType).toBeUndefined();
359
+ expect(
360
+ (req as OneUptimeRequest).userTenantAccessPermission,
361
+ ).toBeUndefined();
362
+ expect(
363
+ (req as OneUptimeRequest).userGlobalAccessPermission,
364
+ ).toBeUndefined();
365
+ });
366
+
367
+ /*
368
+ * The #3004 fix at the middleware level.
369
+ *
370
+ * An `ApiKey` header that arrives blank — an unset shell variable, a client
371
+ * that always sends the header, our own CLI's partial-credential path — is
372
+ * a caller attempting key auth and getting it wrong. It used to be treated
373
+ * as no key at all, drop to the anonymous path, and surface as a 401 about
374
+ * being logged in against whichever model was asked for. It must fail here,
375
+ * as a 400 about the key.
376
+ */
377
+ const unusableHeaderValues: Array<[string, string]> = [
378
+ ["empty", ""],
379
+ ["whitespace-only", " "],
380
+ ["a non-UUID string", "garbage"],
381
+ ["a UUID with a typo", "not-a-uuid-at-all-1234"],
382
+ ];
383
+
384
+ test.each(unusableHeaderValues)(
385
+ "should call next with BadDataException 'Invalid API Key' when the apikey header is %s",
386
+ async (_label: string, headerValue: string) => {
387
+ const req: Partial<ExpressRequest> = {
388
+ headers: { apikey: headerValue },
389
+ };
390
+
391
+ await runMiddleware(req);
392
+
393
+ const error: Exception = getErrorPassedToNext();
394
+
395
+ expect(error).toBeInstanceOf(BadDataException);
396
+ expect(error.message).toEqual("Invalid API Key");
397
+
398
+ // Not the "no header" message — the header WAS sent, just unusable.
399
+ expect(error.message).not.toContain(
400
+ "API Key not found in the request header",
401
+ );
402
+ // And not a session-auth error, which is what #3004 reported.
403
+ expect(error).not.toBeInstanceOf(NotAuthenticatedException);
404
+ expect(error.code).toEqual(ExceptionCode.BadDataException);
405
+
406
+ // The request is rejected, so it carries no API identity onward.
407
+ expect((req as OneUptimeRequest).userType).toBeUndefined();
408
+ expect(
409
+ (req as OneUptimeRequest).userTenantAccessPermission,
410
+ ).toBeUndefined();
411
+ },
412
+ );
413
+
414
+ /*
415
+ * `ApiKey.apiKey` and `GlobalConfig.masterApiKey` are Postgres `uuid`
416
+ * columns. Passing a non-UUID string down to them raises 22P02 "invalid
417
+ * input syntax for type uuid" as a raw QueryFailedError, which is not a
418
+ * OneUptime Exception, so it escapes the error translator and answers 500
419
+ * to what is really a malformed request. The shape check must happen before
420
+ * any lookup — assert the lookups never even run.
421
+ */
422
+ test("should never hit the database for a non-UUID apikey header", async () => {
423
+ const req: Partial<ExpressRequest> = {
424
+ headers: { apikey: "garbage" },
425
+ };
426
+
427
+ await runMiddleware(req);
428
+
429
+ expect(spyFindApiKey).not.toHaveBeenCalled();
430
+ expect(spyGlobalConfigFindOneBy).not.toHaveBeenCalled();
431
+ expect(getErrorPassedToNext().message).toEqual("Invalid API Key");
432
+ });
433
+
434
+ /*
435
+ * Node collapses a header sent twice into an array. Joined, it is never a
436
+ * usable key — but it is unmistakably a caller trying to authenticate by
437
+ * key, so it belongs in the "Invalid API Key" bucket and not the anonymous
438
+ * path.
439
+ */
440
+ test("should call next with BadDataException 'Invalid API Key' when the apikey header was sent twice", async () => {
441
+ const req: Partial<ExpressRequest> = {
442
+ headers: {
443
+ apikey: [apiKeyValue.toString(), ObjectID.generate().toString()],
444
+ },
445
+ };
446
+
447
+ await runMiddleware(req);
448
+
449
+ const error: Exception = getErrorPassedToNext();
450
+
451
+ expect(error).toBeInstanceOf(BadDataException);
452
+ expect(error.message).toEqual("Invalid API Key");
453
+ expect(spyFindApiKey).not.toHaveBeenCalled();
454
+ });
455
+
456
+ /*
457
+ * Invoked directly with no `apikey` header at all (the middleware is also
458
+ * mounted on routes that require a key). The distinct "you sent nothing"
459
+ * message is the direct-call contract and must survive the #3004 change,
460
+ * which only re-classified headers that WERE sent.
461
+ */
462
+ test("should call next with the 'API Key not found' message when no apikey header was sent", async () => {
463
+ const req: Partial<ExpressRequest> = {
464
+ headers: {},
465
+ };
466
+
467
+ await runMiddleware(req);
468
+
469
+ const error: Exception = getErrorPassedToNext();
470
+
471
+ expect(error).toBeInstanceOf(BadDataException);
472
+ expect(error.message).toEqual(
473
+ "API Key not found in the request header. Please provide a valid API Key in the request header.",
474
+ );
475
+ expect(spyFindApiKey).not.toHaveBeenCalled();
476
+ });
477
+
478
+ /*
479
+ * The header read is `req.headers?.["apikey"]`. Some internal call sites
480
+ * and test doubles hand the middleware a request object with no `headers`
481
+ * at all; without the optional chain that is a TypeError, which the
482
+ * try/catch would dutifully forward to next() as a non-Exception and the
483
+ * error translator would answer 500. It must still be the clean 400.
484
+ */
485
+ test("should report the missing API key rather than throw when the request has no headers object", async () => {
486
+ const req: Partial<ExpressRequest> = {};
487
+
488
+ await runMiddleware(req);
489
+
490
+ const error: Exception = getErrorPassedToNext();
491
+
492
+ expect(error).toBeInstanceOf(BadDataException);
493
+ expect(error.message).toEqual(
494
+ "API Key not found in the request header. Please provide a valid API Key in the request header.",
495
+ );
496
+ });
497
+
498
+ /*
499
+ * Even a caller-supplied tenant does not soften the missing-key message:
500
+ * a project id is not a credential.
501
+ */
502
+ test("should still report the missing API key when only a tenantid header was sent", async () => {
503
+ const req: Partial<ExpressRequest> = {
504
+ headers: { tenantid: keyProjectId.toString() },
505
+ };
506
+
507
+ await runMiddleware(req);
508
+
509
+ expect(getErrorPassedToNext().message).toEqual(
510
+ "API Key not found in the request header. Please provide a valid API Key in the request header.",
511
+ );
512
+ });
513
+ });
514
+
515
+ describe("when the key is the instance master API key", () => {
516
+ const masterAdminUserId: ObjectID = ObjectID.generate();
517
+
518
+ type MockMasterKeyEnabledFunction = () => void;
519
+
520
+ /*
521
+ * The master key is enabled, and the lookup answers ONLY for the key that
522
+ * was actually presented — which is exactly what the `masterApiKey: apiKey`
523
+ * term in the real query does.
524
+ *
525
+ * A blanket "always return a row" stub would be a tautology dressed as a
526
+ * test: it would stay green if the query lost that term, or if the
527
+ * middleware stopped passing the caller's key to isMasterApiKey at all —
528
+ * i.e. if every well-formed key on an instance with the master key enabled
529
+ * were promoted to master admin. That is the worst regression this file
530
+ * could miss, so the fake has to be able to say no.
531
+ */
532
+ const mockMasterKeyEnabled: MockMasterKeyEnabledFunction = (): void => {
533
+ const globalConfig: GlobalConfig = new GlobalConfig();
534
+ globalConfig._id = ObjectID.getZeroObjectID().toString();
535
+
536
+ // findApiKey stays null: the master key is not a project API key row.
537
+ spyGlobalConfigFindOneBy.mockImplementation(
538
+ async (
539
+ findBy: FindOneBy<GlobalConfig>,
540
+ ): Promise<GlobalConfig | null> => {
541
+ const query: Record<string, unknown> = (findBy.query || {}) as Record<
542
+ string,
543
+ unknown
544
+ >;
545
+
546
+ const isScopedToPresentedKey: boolean =
547
+ String(query["masterApiKey"] ?? "") ===
548
+ masterApiKeyValue.toString();
549
+
550
+ const isScopedToEnabledMasterKey: boolean =
551
+ query["isMasterApiKeyEnabled"] === true;
552
+
553
+ if (isScopedToPresentedKey && isScopedToEnabledMasterKey) {
554
+ return globalConfig;
555
+ }
556
+
557
+ return null;
558
+ },
559
+ );
560
+ };
561
+
562
+ test("should promote the request to MasterAdmin and populate userAuthorization", async () => {
563
+ mockMasterKeyEnabled();
564
+
565
+ const masterAdmin: User = new User();
566
+ masterAdmin._id = masterAdminUserId.toString();
567
+ masterAdmin.email = new Email("master@oneuptime.com");
568
+ masterAdmin.name = new Name("Master Admin");
569
+
570
+ spyUserFindOneBy.mockResolvedValue(masterAdmin);
571
+
572
+ const req: Partial<ExpressRequest> = {
573
+ headers: { apikey: masterApiKeyValue.toString() },
574
+ };
575
+
576
+ await runMiddleware(req);
577
+
578
+ expect(next).toHaveBeenCalledTimes(1);
579
+ expect(next.mock.calls[0]).toEqual([]);
580
+
581
+ const oneUptimeRequest: OneUptimeRequest = req as OneUptimeRequest;
582
+
583
+ expect(oneUptimeRequest.userType).toEqual(UserType.MasterAdmin);
584
+ expect(oneUptimeRequest.userAuthorization?.userId.toString()).toEqual(
585
+ masterAdminUserId.toString(),
586
+ );
587
+ expect(oneUptimeRequest.userAuthorization?.isMasterAdmin).toStrictEqual(
588
+ true,
589
+ );
590
+ expect(oneUptimeRequest.userAuthorization?.isGlobalLogin).toStrictEqual(
591
+ true,
592
+ );
593
+ expect(oneUptimeRequest.userAuthorization?.email.toString()).toEqual(
594
+ "master@oneuptime.com",
595
+ );
596
+ expect(oneUptimeRequest.userAuthorization?.name?.toString()).toEqual(
597
+ "Master Admin",
598
+ );
599
+
600
+ // The project-key branch never ran: there is no tenant to scope this to.
601
+ expect(oneUptimeRequest.userTenantAccessPermission).toBeUndefined();
602
+ });
603
+
604
+ /*
605
+ * SECURITY: enabling the master key must not turn every other key into a
606
+ * master key. A well-formed key that is neither a project key nor THE
607
+ * master key is still just an invalid key, and the request must not leave
608
+ * this middleware wearing MasterAdmin.
609
+ */
610
+ test("should not promote a different well-formed key to MasterAdmin while the master key is enabled", async () => {
611
+ mockMasterKeyEnabled();
612
+
613
+ const masterAdmin: User = new User();
614
+ masterAdmin._id = masterAdminUserId.toString();
615
+ masterAdmin.email = new Email("master@oneuptime.com");
616
+ masterAdmin.name = new Name("Master Admin");
617
+
618
+ spyUserFindOneBy.mockResolvedValue(masterAdmin);
619
+
620
+ const req: Partial<ExpressRequest> = {
621
+ // Not the master key, and findApiKey does not know it either.
622
+ headers: { apikey: apiKeyValue.toString() },
623
+ };
624
+
625
+ await runMiddleware(req);
626
+
627
+ const error: Exception = getErrorPassedToNext();
628
+
629
+ expect(error).toBeInstanceOf(BadDataException);
630
+ expect(error.message).toEqual("Invalid API Key");
631
+
632
+ const oneUptimeRequest: OneUptimeRequest = req as OneUptimeRequest;
633
+
634
+ expect(oneUptimeRequest.userType).not.toEqual(UserType.MasterAdmin);
635
+ expect(oneUptimeRequest.userAuthorization).toBeUndefined();
636
+ // The master admin row is never fetched for a key that did not match.
637
+ expect(spyUserFindOneBy).not.toHaveBeenCalled();
638
+ });
639
+
640
+ /*
641
+ * The master key is enabled but the instance has no master admin row. The
642
+ * caller gets a message that names the actual misconfiguration, instead of
643
+ * falling through to a generic "Invalid API Key" that would send an
644
+ * operator hunting for a key problem that does not exist.
645
+ */
646
+ test("should call next with a BadDataException naming the missing master admin user", async () => {
647
+ mockMasterKeyEnabled();
648
+ spyUserFindOneBy.mockResolvedValue(null);
649
+
650
+ const req: Partial<ExpressRequest> = {
651
+ headers: { apikey: masterApiKeyValue.toString() },
652
+ };
653
+
654
+ await runMiddleware(req);
655
+
656
+ const error: Exception = getErrorPassedToNext();
657
+
658
+ expect(error).toBeInstanceOf(BadDataException);
659
+ expect(error.message).toEqual(
660
+ "Master Admin user not found. Please make sure you have created a master admin user.",
661
+ );
662
+ expect((req as OneUptimeRequest).userAuthorization).toBeUndefined();
663
+ });
664
+ });
665
+
666
+ /*
667
+ * Everything leaves through next(err). The middleware wraps its whole body in
668
+ * a try/catch, so even an unexpected failure from the cached key lookup must
669
+ * be handed to Express rather than rejecting — an unhandled rejection inside
670
+ * an async middleware never reaches the error translator, so the client would
671
+ * hang until the request times out.
672
+ */
673
+ test("should hand an unexpected lookup failure to next instead of rejecting", async () => {
674
+ const lookupFailure: Error = new Error("connection terminated");
675
+
676
+ spyFindApiKey.mockRejectedValue(lookupFailure);
677
+
678
+ const req: Partial<ExpressRequest> = {
679
+ headers: { apikey: apiKeyValue.toString() },
680
+ };
681
+
682
+ await expect(runMiddleware(req)).resolves.toBeUndefined();
683
+
684
+ expect(next).toHaveBeenCalledTimes(1);
685
+ expect(next.mock.calls[0]![0]).toBe(lookupFailure);
686
+ });
687
+ });