@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.
- package/Models/DatabaseModels/Host.ts +12 -4
- package/Models/DatabaseModels/KubernetesContainer.ts +5 -4
- package/Models/DatabaseModels/KubernetesResource.ts +8 -7
- package/Models/DatabaseModels/OnCallDutyPolicyScheduleLayer.ts +12 -2
- package/Models/DatabaseModels/PodmanResource.ts +6 -6
- package/Models/DatabaseModels/ServiceLevelObjective.ts +91 -0
- package/Models/DatabaseModels/StatusPage.ts +95 -0
- package/Server/API/TeamMemberAPI.ts +85 -0
- package/Server/Infrastructure/Postgres/LocalMigrationGenerationDataSource.ts +12 -2
- package/Server/Infrastructure/Postgres/SchemaMigrations/1785900000000-AddSloMonitorLabelRule.ts +84 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1785915638551-IncreaseHostIpAddressesLength.ts +40 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1785930000000-WidenInventoryResourceNameColumns.ts +131 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1785930709405-AddStatusPageReportPeriod.ts +25 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +8 -0
- package/Server/Middleware/ProjectAuthorization.ts +66 -4
- package/Server/Services/BillingService.ts +600 -51
- package/Server/Services/CephClusterService.ts +47 -5
- package/Server/Services/CephResourceService.ts +43 -4
- package/Server/Services/CloudResourceService.ts +47 -5
- package/Server/Services/DatabaseService.ts +87 -6
- package/Server/Services/DockerHostService.ts +47 -5
- package/Server/Services/DockerResourceService.ts +62 -8
- package/Server/Services/DockerSwarmClusterService.ts +48 -5
- package/Server/Services/DockerSwarmResourceService.ts +53 -4
- package/Server/Services/HostService.ts +48 -5
- package/Server/Services/IoTDeviceService.ts +1 -17
- package/Server/Services/IoTFleetService.ts +47 -5
- package/Server/Services/KubernetesClusterService.ts +48 -5
- package/Server/Services/KubernetesContainerService.ts +50 -9
- package/Server/Services/KubernetesResourceService.ts +53 -7
- package/Server/Services/LabelService.ts +106 -1
- package/Server/Services/LlmProviderService.ts +69 -9
- package/Server/Services/MonitorService.ts +57 -0
- package/Server/Services/PodmanHostService.ts +47 -5
- package/Server/Services/PodmanResourceService.ts +62 -8
- package/Server/Services/ProjectService.ts +165 -35
- package/Server/Services/ProxmoxClusterService.ts +47 -5
- package/Server/Services/ProxmoxResourceService.ts +43 -4
- package/Server/Services/RumApplicationService.ts +48 -5
- package/Server/Services/RunnerService.ts +1 -7
- package/Server/Services/ServerlessFunctionService.ts +48 -5
- package/Server/Services/ServiceLevelObjectiveMonitorRuleEngineService.ts +426 -0
- package/Server/Services/ServiceLevelObjectiveService.ts +39 -0
- package/Server/Services/StatusPageService.ts +95 -36
- package/Server/Services/StatusPageSubscriberNotificationTemplateService.ts +18 -1
- package/Server/Services/StatusPageSubscriberService.ts +3 -0
- package/Server/Types/Database/JSONColumnQuery.ts +621 -0
- package/Server/Types/Database/Permissions/PublicPermission.ts +9 -2
- package/Server/Types/Database/QueryHelper.ts +17 -32
- package/Server/Utils/Database/TruncateColumnValue.ts +84 -0
- package/Server/Utils/Monitor/Criteria/CompareCriteria.ts +12 -0
- package/Tests/App/Dashboard/RunnerInstallInstructions.test.tsx +119 -0
- package/Tests/App/Dashboard/RunnerStatus.test.tsx +274 -0
- package/Tests/App/Dashboard/UsersTableGroupsByPerson.test.tsx +448 -0
- package/Tests/Server/API/BaseAPIApiKeyAuth.test.ts +704 -0
- package/Tests/Server/API/TeamMemberRemoveUserFromProjectAPI.test.ts +303 -0
- package/Tests/Server/Middleware/MasterAdminAuthorization.test.ts +350 -0
- package/Tests/Server/Middleware/ProjectAuthorizationApiKeyHeader.test.ts +362 -0
- package/Tests/Server/Middleware/ProjectAuthorizationApiKeyMiddleware.test.ts +687 -0
- package/Tests/Server/Services/BillingService.test.ts +78 -3
- package/Tests/Server/Services/BillingServiceChangePlanCancel.test.ts +645 -0
- package/Tests/Server/Services/BillingServiceTrialPlanChange.test.ts +1005 -0
- package/Tests/Server/Services/HookFreeWriteLengthClamp.test.ts +503 -0
- package/Tests/Server/Services/HostIpAddressesColumnWidth.test.ts +198 -0
- package/Tests/Server/Services/HostServiceUpdateLastSeen.test.ts +510 -0
- package/Tests/Server/Services/InventoryResourceNameColumnWidth.test.ts +500 -0
- package/Tests/Server/Services/InventoryUpsertSchemaParity.test.ts +586 -0
- package/Tests/Server/Services/InventoryUpsertTruncationGuard.test.ts +1242 -0
- package/Tests/Server/Services/LlmProviderUsableByProject.test.ts +293 -0
- package/Tests/Server/Services/ProjectServiceChangePlan.test.ts +650 -0
- package/Tests/Server/Services/ProjectServiceExtendTrial.test.ts +1 -0
- package/Tests/Server/Services/ResourceUpdateLastSeenLivenessFallback.test.ts +387 -0
- package/Tests/Server/Services/ServiceLevelObjectiveMonitorRuleEngineService.test.ts +770 -0
- package/Tests/Server/Services/ServiceLevelObjectiveService.test.ts +130 -0
- package/Tests/Server/Services/SloMonitorLabelRuleHooks.test.ts +352 -0
- package/Tests/Server/Services/StatusPageSubscriberReport.test.ts +29 -13
- package/Tests/Server/TestingUtils/Services/BillingServiceHelper.ts +69 -8
- package/Tests/Server/Types/Database/JSONColumnQuery.test.ts +593 -0
- package/Tests/Server/Types/Database/Permissions/PublicPermission.test.ts +495 -0
- package/Tests/Server/Types/Database/QueryUtil.test.ts +113 -0
- package/Tests/Server/Utils/AI/ToolArgsExtractors.test.ts +141 -0
- package/Tests/Server/Utils/AI/Toolbox/WidgetBuilder.test.ts +205 -0
- package/Tests/Server/Utils/Database/TruncateColumnValue.test.ts +179 -0
- package/Tests/Server/Utils/Monitor/Criteria/CompareCriteria.test.ts +897 -0
- package/Tests/Types/Events/Recurring.test.ts +157 -0
- package/Tests/Types/Runner/RunnerLiveStatus.test.ts +320 -0
- package/Tests/UI/Components/AiInvestigationSettingsCard.test.tsx +285 -0
- package/Tests/UI/Components/Detail/EntityFields.test.tsx +166 -0
- package/Tests/UI/Components/ModelTable/ModelTableGroupsProjectUsers.test.tsx +375 -0
- package/Tests/UI/Components/ModelTable/useCustomFieldColumns.test.tsx +196 -0
- package/Tests/UI/Utils/ProjectUsersModelAPI.test.ts +739 -0
- package/Tests/UI/Utils/TeamMembersByUser.test.ts +633 -0
- package/Tests/Utils/StatusPage/ReportPeriod.test.ts +218 -0
- package/Tests/Utils/Telemetry/HostIpAddresses.test.ts +291 -0
- package/Types/CustomField/CustomFieldDefinition.ts +24 -0
- package/Types/Date.ts +112 -0
- package/Types/Events/Recurring.ts +99 -1
- package/Types/Runbook/RunbookStep.ts +15 -0
- package/Types/Runner/RunnerLiveStatus.ts +114 -0
- package/Types/StatusPage/StatusPageReport.ts +11 -0
- package/Types/StatusPage/StatusPageReportPeriodType.ts +21 -0
- package/UI/Components/Detail/Detail.tsx +155 -0
- package/UI/Components/ModelTable/CustomFieldColumns.tsx +2 -6
- package/UI/Components/ModelTable/useCustomFieldColumns.ts +21 -2
- package/UI/Utils/ModelAPI/ProjectUsersModelAPI.ts +305 -0
- package/UI/Utils/TeamMembersByUser.ts +302 -0
- package/Utils/StatusPage/ReportPeriod.ts +236 -0
- package/Utils/Telemetry/HostIpAddresses.ts +98 -0
- package/build/dist/Models/DatabaseModels/Host.js +12 -4
- package/build/dist/Models/DatabaseModels/Host.js.map +1 -1
- package/build/dist/Models/DatabaseModels/KubernetesContainer.js +4 -4
- package/build/dist/Models/DatabaseModels/KubernetesContainer.js.map +1 -1
- package/build/dist/Models/DatabaseModels/KubernetesResource.js +7 -7
- package/build/dist/Models/DatabaseModels/KubernetesResource.js.map +1 -1
- package/build/dist/Models/DatabaseModels/OnCallDutyPolicyScheduleLayer.js +16 -4
- package/build/dist/Models/DatabaseModels/OnCallDutyPolicyScheduleLayer.js.map +1 -1
- package/build/dist/Models/DatabaseModels/PodmanResource.js +6 -6
- package/build/dist/Models/DatabaseModels/PodmanResource.js.map +1 -1
- package/build/dist/Models/DatabaseModels/ServiceLevelObjective.js +87 -0
- package/build/dist/Models/DatabaseModels/ServiceLevelObjective.js.map +1 -1
- package/build/dist/Models/DatabaseModels/StatusPage.js +98 -0
- package/build/dist/Models/DatabaseModels/StatusPage.js.map +1 -1
- package/build/dist/Server/API/TeamMemberAPI.js +57 -2
- package/build/dist/Server/API/TeamMemberAPI.js.map +1 -1
- package/build/dist/Server/Infrastructure/Postgres/LocalMigrationGenerationDataSource.js +11 -1
- package/build/dist/Server/Infrastructure/Postgres/LocalMigrationGenerationDataSource.js.map +1 -1
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1785900000000-AddSloMonitorLabelRule.js +45 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1785900000000-AddSloMonitorLabelRule.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1785915638551-IncreaseHostIpAddressesLength.js +31 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1785915638551-IncreaseHostIpAddressesLength.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1785930000000-WidenInventoryResourceNameColumns.js +90 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1785930000000-WidenInventoryResourceNameColumns.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1785930709405-AddStatusPageReportPeriod.js +14 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1785930709405-AddStatusPageReportPeriod.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +8 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
- package/build/dist/Server/Middleware/ProjectAuthorization.js +59 -4
- package/build/dist/Server/Middleware/ProjectAuthorization.js.map +1 -1
- package/build/dist/Server/Services/BillingService.js +466 -36
- package/build/dist/Server/Services/BillingService.js.map +1 -1
- package/build/dist/Server/Services/CephClusterService.js +42 -5
- package/build/dist/Server/Services/CephClusterService.js.map +1 -1
- package/build/dist/Server/Services/CephResourceService.js +28 -3
- package/build/dist/Server/Services/CephResourceService.js.map +1 -1
- package/build/dist/Server/Services/CloudResourceService.js +42 -5
- package/build/dist/Server/Services/CloudResourceService.js.map +1 -1
- package/build/dist/Server/Services/DatabaseService.js +69 -2
- package/build/dist/Server/Services/DatabaseService.js.map +1 -1
- package/build/dist/Server/Services/DockerHostService.js +42 -5
- package/build/dist/Server/Services/DockerHostService.js.map +1 -1
- package/build/dist/Server/Services/DockerResourceService.js +34 -4
- package/build/dist/Server/Services/DockerResourceService.js.map +1 -1
- package/build/dist/Server/Services/DockerSwarmClusterService.js +43 -5
- package/build/dist/Server/Services/DockerSwarmClusterService.js.map +1 -1
- package/build/dist/Server/Services/DockerSwarmResourceService.js +28 -3
- package/build/dist/Server/Services/DockerSwarmResourceService.js.map +1 -1
- package/build/dist/Server/Services/HostService.js +43 -5
- package/build/dist/Server/Services/HostService.js.map +1 -1
- package/build/dist/Server/Services/IoTDeviceService.js +1 -8
- package/build/dist/Server/Services/IoTDeviceService.js.map +1 -1
- package/build/dist/Server/Services/IoTFleetService.js +42 -5
- package/build/dist/Server/Services/IoTFleetService.js.map +1 -1
- package/build/dist/Server/Services/KubernetesClusterService.js +43 -5
- package/build/dist/Server/Services/KubernetesClusterService.js.map +1 -1
- package/build/dist/Server/Services/KubernetesContainerService.js +29 -3
- package/build/dist/Server/Services/KubernetesContainerService.js.map +1 -1
- package/build/dist/Server/Services/KubernetesResourceService.js +32 -4
- package/build/dist/Server/Services/KubernetesResourceService.js.map +1 -1
- package/build/dist/Server/Services/LabelService.js +92 -0
- package/build/dist/Server/Services/LabelService.js.map +1 -1
- package/build/dist/Server/Services/LlmProviderService.js +61 -8
- package/build/dist/Server/Services/LlmProviderService.js.map +1 -1
- package/build/dist/Server/Services/MonitorService.js +47 -0
- package/build/dist/Server/Services/MonitorService.js.map +1 -1
- package/build/dist/Server/Services/PodmanHostService.js +42 -5
- package/build/dist/Server/Services/PodmanHostService.js.map +1 -1
- package/build/dist/Server/Services/PodmanResourceService.js +34 -4
- package/build/dist/Server/Services/PodmanResourceService.js.map +1 -1
- package/build/dist/Server/Services/ProjectService.js +135 -21
- package/build/dist/Server/Services/ProjectService.js.map +1 -1
- package/build/dist/Server/Services/ProxmoxClusterService.js +42 -5
- package/build/dist/Server/Services/ProxmoxClusterService.js.map +1 -1
- package/build/dist/Server/Services/ProxmoxResourceService.js +28 -3
- package/build/dist/Server/Services/ProxmoxResourceService.js.map +1 -1
- package/build/dist/Server/Services/RumApplicationService.js +43 -5
- package/build/dist/Server/Services/RumApplicationService.js.map +1 -1
- package/build/dist/Server/Services/RunnerService.js +1 -6
- package/build/dist/Server/Services/RunnerService.js.map +1 -1
- package/build/dist/Server/Services/ServerlessFunctionService.js +43 -5
- package/build/dist/Server/Services/ServerlessFunctionService.js.map +1 -1
- package/build/dist/Server/Services/ServiceLevelObjectiveMonitorRuleEngineService.js +347 -0
- package/build/dist/Server/Services/ServiceLevelObjectiveMonitorRuleEngineService.js.map +1 -0
- package/build/dist/Server/Services/ServiceLevelObjectiveService.js +33 -1
- package/build/dist/Server/Services/ServiceLevelObjectiveService.js.map +1 -1
- package/build/dist/Server/Services/StatusPageService.js +69 -46
- package/build/dist/Server/Services/StatusPageService.js.map +1 -1
- package/build/dist/Server/Services/StatusPageSubscriberNotificationTemplateService.js +17 -1
- package/build/dist/Server/Services/StatusPageSubscriberNotificationTemplateService.js.map +1 -1
- package/build/dist/Server/Services/StatusPageSubscriberService.js +3 -0
- package/build/dist/Server/Services/StatusPageSubscriberService.js.map +1 -1
- package/build/dist/Server/Types/Database/JSONColumnQuery.js +402 -0
- package/build/dist/Server/Types/Database/JSONColumnQuery.js.map +1 -0
- package/build/dist/Server/Types/Database/Permissions/PublicPermission.js +9 -2
- package/build/dist/Server/Types/Database/Permissions/PublicPermission.js.map +1 -1
- package/build/dist/Server/Types/Database/QueryHelper.js +17 -27
- package/build/dist/Server/Types/Database/QueryHelper.js.map +1 -1
- package/build/dist/Server/Utils/Database/TruncateColumnValue.js +32 -0
- package/build/dist/Server/Utils/Database/TruncateColumnValue.js.map +1 -0
- package/build/dist/Server/Utils/Monitor/Criteria/CompareCriteria.js +11 -0
- package/build/dist/Server/Utils/Monitor/Criteria/CompareCriteria.js.map +1 -1
- package/build/dist/Types/CustomField/CustomFieldDefinition.js +2 -0
- package/build/dist/Types/CustomField/CustomFieldDefinition.js.map +1 -0
- package/build/dist/Types/Date.js +68 -0
- package/build/dist/Types/Date.js.map +1 -1
- package/build/dist/Types/Events/Recurring.js +63 -0
- package/build/dist/Types/Events/Recurring.js.map +1 -1
- package/build/dist/Types/Runbook/RunbookStep.js.map +1 -1
- package/build/dist/Types/Runner/RunnerLiveStatus.js +69 -0
- package/build/dist/Types/Runner/RunnerLiveStatus.js.map +1 -0
- package/build/dist/Types/StatusPage/StatusPageReportPeriodType.js +22 -0
- package/build/dist/Types/StatusPage/StatusPageReportPeriodType.js.map +1 -0
- package/build/dist/UI/Components/Detail/Detail.js +97 -0
- package/build/dist/UI/Components/Detail/Detail.js.map +1 -1
- package/build/dist/UI/Components/ModelTable/CustomFieldColumns.js.map +1 -1
- package/build/dist/UI/Components/ModelTable/useCustomFieldColumns.js +18 -2
- package/build/dist/UI/Components/ModelTable/useCustomFieldColumns.js.map +1 -1
- package/build/dist/UI/Utils/ModelAPI/ProjectUsersModelAPI.js +213 -0
- package/build/dist/UI/Utils/ModelAPI/ProjectUsersModelAPI.js.map +1 -0
- package/build/dist/UI/Utils/TeamMembersByUser.js +195 -0
- package/build/dist/UI/Utils/TeamMembersByUser.js.map +1 -0
- package/build/dist/Utils/StatusPage/ReportPeriod.js +131 -0
- package/build/dist/Utils/StatusPage/ReportPeriod.js.map +1 -0
- package/build/dist/Utils/Telemetry/HostIpAddresses.js +82 -0
- package/build/dist/Utils/Telemetry/HostIpAddresses.js.map +1 -0
- package/jest.config.json +2 -0
- package/package.json +1 -1
- package/tsconfig.json +12 -1
|
@@ -0,0 +1,704 @@
|
|
|
1
|
+
import BaseAPI from "../../../Server/API/BaseAPI";
|
|
2
|
+
import ApiKeyPermissionService from "../../../Server/Services/ApiKeyPermissionService";
|
|
3
|
+
import ApiKeyService from "../../../Server/Services/ApiKeyService";
|
|
4
|
+
import DatabaseService from "../../../Server/Services/DatabaseService";
|
|
5
|
+
import GlobalConfigService from "../../../Server/Services/GlobalConfigService";
|
|
6
|
+
import ProjectService from "../../../Server/Services/ProjectService";
|
|
7
|
+
import JSONWebToken from "../../../Server/Utils/JsonWebToken";
|
|
8
|
+
import {
|
|
9
|
+
ExpressResponse,
|
|
10
|
+
NextFunction,
|
|
11
|
+
OneUptimeRequest,
|
|
12
|
+
} from "../../../Server/Utils/Express";
|
|
13
|
+
import Response from "../../../Server/Utils/Response";
|
|
14
|
+
import { mockRouter } from "./Helpers";
|
|
15
|
+
import ApiKeyPermission from "../../../Models/DatabaseModels/ApiKeyPermission";
|
|
16
|
+
import LogPipeline from "../../../Models/DatabaseModels/LogPipeline";
|
|
17
|
+
import Monitor from "../../../Models/DatabaseModels/Monitor";
|
|
18
|
+
import BadDataException from "../../../Types/Exception/BadDataException";
|
|
19
|
+
import Exception from "../../../Types/Exception/Exception";
|
|
20
|
+
import NotAuthenticatedException from "../../../Types/Exception/NotAuthenticatedException";
|
|
21
|
+
import NotAuthorizedException from "../../../Types/Exception/NotAuthorizedException";
|
|
22
|
+
import ObjectID from "../../../Types/ObjectID";
|
|
23
|
+
import Permission from "../../../Types/Permission";
|
|
24
|
+
import UserType from "../../../Types/UserType";
|
|
25
|
+
import { getJestSpyOn } from "../../Spy";
|
|
26
|
+
import { beforeEach, describe, expect, it, jest } from "@jest/globals";
|
|
27
|
+
|
|
28
|
+
jest.mock("../../../Server/Utils/Express", () => {
|
|
29
|
+
return {
|
|
30
|
+
getRouter: () => {
|
|
31
|
+
return mockRouter;
|
|
32
|
+
},
|
|
33
|
+
};
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
jest.mock("../../../Server/Utils/Response", () => {
|
|
37
|
+
return {
|
|
38
|
+
sendEmptySuccessResponse: jest.fn(),
|
|
39
|
+
sendEntityResponse: jest.fn(),
|
|
40
|
+
sendEntityArrayResponse: jest.fn(),
|
|
41
|
+
sendJsonArrayResponse: jest.fn(),
|
|
42
|
+
sendJsonObjectResponse: jest.fn(),
|
|
43
|
+
sendErrorResponse: jest.fn(),
|
|
44
|
+
};
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
/*
|
|
48
|
+
* Issue #3004: a caller sent a valid project API key to
|
|
49
|
+
* POST /api/log-pipeline/get-list and got
|
|
50
|
+
* 401 {"error":"A user should be logged in to read record of Log Pipeline."}
|
|
51
|
+
* They read that as a broken RBAC path on Log Pipeline specifically.
|
|
52
|
+
*
|
|
53
|
+
* It was neither Log-Pipeline-specific nor an RBAC failure. That message was
|
|
54
|
+
* what the codebase produced for ANY request it had already decided was
|
|
55
|
+
* anonymous - and a request whose `ApiKey` header arrived empty or malformed
|
|
56
|
+
* was decided to be anonymous, because the "does this caller present a key"
|
|
57
|
+
* test asked whether the key was USABLE rather than whether it was PRESENT.
|
|
58
|
+
* The caller was then told to log in (they never tried to) about a model
|
|
59
|
+
* (which was never the problem). Issue #1754 is the same string on a
|
|
60
|
+
* different model.
|
|
61
|
+
*
|
|
62
|
+
* Nothing below is Log-Pipeline-specific. This is the generic CRUD auth
|
|
63
|
+
* contract of every model registered through BaseAPI; LogPipeline is here
|
|
64
|
+
* only because it is the resource named in the report, and the control block
|
|
65
|
+
* at the bottom re-runs the two decisive cases against Monitor to show the
|
|
66
|
+
* outcome does not depend on the model.
|
|
67
|
+
*
|
|
68
|
+
* The whole real chain runs: the route's own middleware is
|
|
69
|
+
* UserMiddleware.getUserMiddleware, which routes to
|
|
70
|
+
* ProjectMiddleware.isValidProjectIdAndApiKeyMiddleware, and the handler runs
|
|
71
|
+
* the real DatabaseService read with the real permission pipeline. Only the
|
|
72
|
+
* TypeORM repository and the three lookup services are stubbed.
|
|
73
|
+
*
|
|
74
|
+
* On the status codes asserted here: every failure below leaves via
|
|
75
|
+
* `next(err)`, and Express's error handler in Server/Utils/StartServer.ts
|
|
76
|
+
* answers `res.status(err.code).send({ error: err.message })`. So the
|
|
77
|
+
* exception's `code` IS the HTTP status the caller sees -
|
|
78
|
+
* BadDataException=400, NotAuthenticatedException=401,
|
|
79
|
+
* NotAuthorizedException=422 (Types/Exception/ExceptionCode.ts).
|
|
80
|
+
*/
|
|
81
|
+
|
|
82
|
+
/*
|
|
83
|
+
* Any hex UUID is accepted by ObjectID.isValidUUID (any version,
|
|
84
|
+
* case-insensitive), so these only have to be well formed.
|
|
85
|
+
*/
|
|
86
|
+
const API_KEY_ID: ObjectID = new ObjectID(
|
|
87
|
+
"11111111-1111-4111-8111-111111111111",
|
|
88
|
+
);
|
|
89
|
+
const PROJECT_ID: ObjectID = new ObjectID(
|
|
90
|
+
"22222222-2222-4222-8222-222222222222",
|
|
91
|
+
);
|
|
92
|
+
const KNOWN_API_KEY: string = "33333333-3333-4333-8333-333333333333";
|
|
93
|
+
const UNKNOWN_API_KEY: string = "44444444-4444-4444-8444-444444444444";
|
|
94
|
+
|
|
95
|
+
/*
|
|
96
|
+
* A project the key does NOT belong to, used only to prove a caller-supplied
|
|
97
|
+
* `tenantid` header cannot redirect a key at another project.
|
|
98
|
+
*/
|
|
99
|
+
const OTHER_PROJECT_ID: string = "55555555-5555-4555-8555-555555555555";
|
|
100
|
+
|
|
101
|
+
/*
|
|
102
|
+
* Registered once, at module load, because mockRouter.match returns the FIRST
|
|
103
|
+
* route with a given method+uri and that route closes over the instance that
|
|
104
|
+
* registered it. Re-registering per test would leave the matched route
|
|
105
|
+
* pointing at a stale service that the per-test spies never touch.
|
|
106
|
+
*/
|
|
107
|
+
const logPipelineService: DatabaseService<LogPipeline> =
|
|
108
|
+
new DatabaseService<LogPipeline>(LogPipeline);
|
|
109
|
+
const logPipelineApi: BaseAPI<
|
|
110
|
+
LogPipeline,
|
|
111
|
+
DatabaseService<LogPipeline>
|
|
112
|
+
> = new BaseAPI<LogPipeline, DatabaseService<LogPipeline>>(
|
|
113
|
+
LogPipeline,
|
|
114
|
+
logPipelineService,
|
|
115
|
+
);
|
|
116
|
+
|
|
117
|
+
const monitorService: DatabaseService<Monitor> = new DatabaseService<Monitor>(
|
|
118
|
+
Monitor,
|
|
119
|
+
);
|
|
120
|
+
const monitorApi: BaseAPI<Monitor, DatabaseService<Monitor>> = new BaseAPI<
|
|
121
|
+
Monitor,
|
|
122
|
+
DatabaseService<Monitor>
|
|
123
|
+
>(Monitor, monitorService);
|
|
124
|
+
|
|
125
|
+
const LOG_PIPELINE_PATH: string = new logPipelineApi.entityType()
|
|
126
|
+
.getCrudApiPath()!
|
|
127
|
+
.toString();
|
|
128
|
+
|
|
129
|
+
const MONITOR_PATH: string = new monitorApi.entityType()
|
|
130
|
+
.getCrudApiPath()!
|
|
131
|
+
.toString();
|
|
132
|
+
|
|
133
|
+
/*
|
|
134
|
+
* Stands in for the TypeORM repository so the read stops at the DB boundary
|
|
135
|
+
* and everything above it - permission checks included - is the real thing.
|
|
136
|
+
* Mocking service.findBy instead would delete the very checks under test.
|
|
137
|
+
*/
|
|
138
|
+
type RepositoryStub = {
|
|
139
|
+
find: () => Promise<Array<never>>;
|
|
140
|
+
count: () => Promise<number>;
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
const repository: RepositoryStub = {
|
|
144
|
+
find: (): Promise<Array<never>> => {
|
|
145
|
+
return Promise.resolve([]);
|
|
146
|
+
},
|
|
147
|
+
count: (): Promise<number> => {
|
|
148
|
+
return Promise.resolve(0);
|
|
149
|
+
},
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
type HeaderBag = Record<string, string | Array<string> | undefined>;
|
|
153
|
+
|
|
154
|
+
type RouteOutcome = {
|
|
155
|
+
error: Exception | null;
|
|
156
|
+
request: OneUptimeRequest;
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
type CallGetListFunction = (data: {
|
|
160
|
+
crudApiPath: string;
|
|
161
|
+
headers: HeaderBag;
|
|
162
|
+
}) => Promise<RouteOutcome>;
|
|
163
|
+
|
|
164
|
+
/*
|
|
165
|
+
* Drives the registered POST /<model>/get-list route the way Express would:
|
|
166
|
+
* middleware first, then - only if the middleware did not hand an error to
|
|
167
|
+
* next() - the handler. Both report failure through next(err).
|
|
168
|
+
*/
|
|
169
|
+
const callGetList: CallGetListFunction = async (data: {
|
|
170
|
+
crudApiPath: string;
|
|
171
|
+
headers: HeaderBag;
|
|
172
|
+
}): Promise<RouteOutcome> => {
|
|
173
|
+
const route: ReturnType<typeof mockRouter.match> = mockRouter.match(
|
|
174
|
+
"POST",
|
|
175
|
+
`${data.crudApiPath}/get-list`,
|
|
176
|
+
);
|
|
177
|
+
|
|
178
|
+
const request: OneUptimeRequest = {
|
|
179
|
+
headers: data.headers,
|
|
180
|
+
params: {},
|
|
181
|
+
query: {},
|
|
182
|
+
body: { query: {}, select: { _id: true }, sort: {} },
|
|
183
|
+
} as unknown as OneUptimeRequest;
|
|
184
|
+
|
|
185
|
+
const response: ExpressResponse = {
|
|
186
|
+
status: jest.fn().mockReturnThis(),
|
|
187
|
+
json: jest.fn().mockReturnThis(),
|
|
188
|
+
send: jest.fn().mockReturnThis(),
|
|
189
|
+
set: jest.fn().mockReturnThis(),
|
|
190
|
+
} as unknown as ExpressResponse;
|
|
191
|
+
|
|
192
|
+
const captured: { error: Exception | null } = { error: null };
|
|
193
|
+
|
|
194
|
+
const next: NextFunction = ((error?: unknown): void => {
|
|
195
|
+
if (error) {
|
|
196
|
+
captured.error = error as Exception;
|
|
197
|
+
}
|
|
198
|
+
}) as unknown as NextFunction;
|
|
199
|
+
|
|
200
|
+
await route.middleware(request, response, next);
|
|
201
|
+
|
|
202
|
+
if (!captured.error) {
|
|
203
|
+
await route.handlerFunction(request, response, next);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
return { error: captured.error, request: request };
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
describe("BaseAPI CRUD auth contract for the ApiKey header (issue #3004)", () => {
|
|
210
|
+
let repositoryFind: ReturnType<typeof getJestSpyOn>;
|
|
211
|
+
let findApiKey: ReturnType<typeof getJestSpyOn>;
|
|
212
|
+
let findApiKeyPermissions: ReturnType<typeof getJestSpyOn>;
|
|
213
|
+
let decodeAccessToken: ReturnType<typeof getJestSpyOn>;
|
|
214
|
+
|
|
215
|
+
type GrantPermissionFunction = (permission: Permission) => void;
|
|
216
|
+
|
|
217
|
+
/*
|
|
218
|
+
* Makes KNOWN_API_KEY resolve to a key on PROJECT_ID carrying exactly one
|
|
219
|
+
* permission. isBlockPermission has to be an explicit false: getUserPermissions
|
|
220
|
+
* keeps a row only when `isBlockPermission === (permissionType === Block)`, so
|
|
221
|
+
* an undefined here silently drops the grant and turns a 200 into a 422.
|
|
222
|
+
*/
|
|
223
|
+
const grantApiKeyPermission: GrantPermissionFunction = (
|
|
224
|
+
permission: Permission,
|
|
225
|
+
): void => {
|
|
226
|
+
findApiKey.mockResolvedValue({ id: API_KEY_ID, projectId: PROJECT_ID });
|
|
227
|
+
|
|
228
|
+
const apiKeyPermission: ApiKeyPermission = new ApiKeyPermission();
|
|
229
|
+
apiKeyPermission.permission = permission;
|
|
230
|
+
apiKeyPermission.labels = [];
|
|
231
|
+
apiKeyPermission.isBlockPermission = false;
|
|
232
|
+
|
|
233
|
+
findApiKeyPermissions.mockResolvedValue([apiKeyPermission]);
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
beforeEach(() => {
|
|
237
|
+
jest.restoreAllMocks();
|
|
238
|
+
jest.clearAllMocks();
|
|
239
|
+
|
|
240
|
+
repositoryFind = getJestSpyOn(repository, "find");
|
|
241
|
+
|
|
242
|
+
getJestSpyOn(logPipelineService, "getRepository").mockReturnValue(
|
|
243
|
+
repository,
|
|
244
|
+
);
|
|
245
|
+
getJestSpyOn(monitorService, "getRepository").mockReturnValue(repository);
|
|
246
|
+
|
|
247
|
+
// Default: no key in the table, no permissions, and no master key configured.
|
|
248
|
+
findApiKey = getJestSpyOn(ApiKeyService, "findApiKey").mockResolvedValue(
|
|
249
|
+
null,
|
|
250
|
+
);
|
|
251
|
+
findApiKeyPermissions = getJestSpyOn(
|
|
252
|
+
ApiKeyPermissionService,
|
|
253
|
+
"findBy",
|
|
254
|
+
).mockResolvedValue([]);
|
|
255
|
+
getJestSpyOn(GlobalConfigService, "findOneBy").mockResolvedValue(null);
|
|
256
|
+
|
|
257
|
+
/*
|
|
258
|
+
* getUserMiddleware fires this off (unawaited) whenever a `tenantid`
|
|
259
|
+
* header is present. It is a write we never assert on; stubbing it keeps
|
|
260
|
+
* a stray Postgres round-trip from landing after the test has finished.
|
|
261
|
+
*/
|
|
262
|
+
getJestSpyOn(ProjectService, "updateLastActive").mockResolvedValue(
|
|
263
|
+
undefined,
|
|
264
|
+
);
|
|
265
|
+
|
|
266
|
+
/*
|
|
267
|
+
* getDatabaseCommonInteractionProps reads the project's billing plan on
|
|
268
|
+
* every request that resolved a tenant, but ONLY when BILLING_ENABLED is
|
|
269
|
+
* set - and the Common Test CI job sets it while a bare local jest run
|
|
270
|
+
* does not. Unstubbed, that is a real ProjectService.findOneById on a
|
|
271
|
+
* service this file never stubs, so the five cases that get as far as a
|
|
272
|
+
* tenant died with "Database not connected" in CI and passed locally.
|
|
273
|
+
*
|
|
274
|
+
* Answering as the no-plan project keeps the auth contract below reading
|
|
275
|
+
* the same either way: BillingPermissions short-circuits on an undefined
|
|
276
|
+
* currentPlan, which is exactly the billing-disabled path.
|
|
277
|
+
*/
|
|
278
|
+
getJestSpyOn(ProjectService, "getCurrentPlan").mockResolvedValue({
|
|
279
|
+
plan: null,
|
|
280
|
+
isSubscriptionUnpaid: false,
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
/*
|
|
284
|
+
* Never stubbed to succeed - it exists so tests can assert whether the
|
|
285
|
+
* session branch of getUserMiddleware was entered at all. Reaching it is
|
|
286
|
+
* always the regression, never the expectation.
|
|
287
|
+
*/
|
|
288
|
+
decodeAccessToken = getJestSpyOn(JSONWebToken, "decode");
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
it("registers the route the issue reported against", () => {
|
|
292
|
+
expect(LOG_PIPELINE_PATH).toBe("/log-pipeline");
|
|
293
|
+
expect(
|
|
294
|
+
mockRouter.match("POST", `${LOG_PIPELINE_PATH}/get-list`),
|
|
295
|
+
).toBeTruthy();
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
describe("A) no ApiKey header at all", () => {
|
|
299
|
+
/*
|
|
300
|
+
* The genuinely anonymous request - no key, no session. This is the one
|
|
301
|
+
* case where the "authenticate first" answer is correct, and it must name
|
|
302
|
+
* BOTH accepted credentials so an API caller who reads it is not sent off
|
|
303
|
+
* to audit session login or the model's roles. The old wording
|
|
304
|
+
* ("A user should be logged in to read record of Log Pipeline.") is what
|
|
305
|
+
* made #3004 and #1754 look like per-model RBAC bugs.
|
|
306
|
+
*/
|
|
307
|
+
it("answers 401 naming both a session and an API key", async () => {
|
|
308
|
+
const outcome: RouteOutcome = await callGetList({
|
|
309
|
+
crudApiPath: LOG_PIPELINE_PATH,
|
|
310
|
+
headers: {},
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
expect(outcome.error).toBeInstanceOf(NotAuthenticatedException);
|
|
314
|
+
expect(outcome.error?.message).toBe(
|
|
315
|
+
"Authenticated user or a valid API key is needed to read record of Log Pipeline.",
|
|
316
|
+
);
|
|
317
|
+
expect(outcome.error?.code).toBe(401);
|
|
318
|
+
expect(outcome.error?.message).not.toContain("should be logged in");
|
|
319
|
+
expect(repositoryFind).not.toHaveBeenCalled();
|
|
320
|
+
expect(Response.sendEntityArrayResponse).not.toHaveBeenCalled();
|
|
321
|
+
});
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
describe("B) ApiKey header present but empty", () => {
|
|
325
|
+
/*
|
|
326
|
+
* THE #3004 FIX, stated as an assertion.
|
|
327
|
+
*
|
|
328
|
+
* Before it, an empty header was "no usable key" and therefore treated as
|
|
329
|
+
* "no key", so the request fell through to the anonymous path and produced
|
|
330
|
+
* case A above: a 401 about logging in, naming Log Pipeline. An unset shell
|
|
331
|
+
* variable, a client that serializes a missing credential as "", and our own
|
|
332
|
+
* CLI's partial-credential path all land here.
|
|
333
|
+
*
|
|
334
|
+
* Presence now claims the request for the API-key branch, so it fails where
|
|
335
|
+
* the caller actually went wrong: 400 Invalid API Key.
|
|
336
|
+
*/
|
|
337
|
+
it("answers 400 Invalid API Key instead of the anonymous 401", async () => {
|
|
338
|
+
const outcome: RouteOutcome = await callGetList({
|
|
339
|
+
crudApiPath: LOG_PIPELINE_PATH,
|
|
340
|
+
headers: { apikey: "" },
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
expect(outcome.error).toBeInstanceOf(BadDataException);
|
|
344
|
+
expect(outcome.error?.message).toBe("Invalid API Key");
|
|
345
|
+
expect(outcome.error?.code).toBe(400);
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
it("does not mention logging in, or the model, for a key problem", async () => {
|
|
349
|
+
const outcome: RouteOutcome = await callGetList({
|
|
350
|
+
crudApiPath: LOG_PIPELINE_PATH,
|
|
351
|
+
headers: { apikey: "" },
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
expect(outcome.error?.message).not.toContain("logged in");
|
|
355
|
+
expect(outcome.error?.message).not.toContain("Log Pipeline");
|
|
356
|
+
expect(outcome.error).not.toBeInstanceOf(NotAuthenticatedException);
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
/*
|
|
360
|
+
* Where the request is decided, not just what it is told.
|
|
361
|
+
*
|
|
362
|
+
* The empty key must be claimed by the API-key branch: the request is
|
|
363
|
+
* never classified as an anonymous `Public` caller (which is exactly what
|
|
364
|
+
* the pre-fix `hasApiKey` did to it, and the only reason the permission
|
|
365
|
+
* layer ever saw it), and it never reaches the key table either, because
|
|
366
|
+
* "" is not a lookup-able value. Nothing is disclosed and no response is
|
|
367
|
+
* written - the failure leaves via next(err).
|
|
368
|
+
*
|
|
369
|
+
* Stopping at "no rows were read" would not have pinned this down: the
|
|
370
|
+
* old anonymous path did not read any rows either, it just failed later
|
|
371
|
+
* and in the wrong words.
|
|
372
|
+
*/
|
|
373
|
+
it("is claimed by the API key branch rather than treated as an anonymous caller", async () => {
|
|
374
|
+
const outcome: RouteOutcome = await callGetList({
|
|
375
|
+
crudApiPath: LOG_PIPELINE_PATH,
|
|
376
|
+
headers: { apikey: "" },
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
expect(outcome.request.userType).not.toBe(UserType.Public);
|
|
380
|
+
expect(findApiKey).not.toHaveBeenCalled();
|
|
381
|
+
expect(repositoryFind).not.toHaveBeenCalled();
|
|
382
|
+
expect(Response.sendEntityArrayResponse).not.toHaveBeenCalled();
|
|
383
|
+
expect(Response.sendErrorResponse).not.toHaveBeenCalled();
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
/*
|
|
387
|
+
* Which credential wins when the caller presents two.
|
|
388
|
+
*
|
|
389
|
+
* This is the one behaviour the presence-vs-usability change alters for
|
|
390
|
+
* callers who were NOT broken before: a request carrying a session token
|
|
391
|
+
* AND a blank `ApiKey` header used to ignore the blank header and
|
|
392
|
+
* authenticate by session; now the key branch claims it and it fails 400.
|
|
393
|
+
* That is the intended reading - a credential that was sent and is
|
|
394
|
+
* unusable is an error, not something to silently paper over with another
|
|
395
|
+
* credential, because the alternative is a caller whose key has quietly
|
|
396
|
+
* stopped being used and who has no way to find out.
|
|
397
|
+
*
|
|
398
|
+
* It is asserted rather than left implicit because it is the change most
|
|
399
|
+
* likely to be "fixed" back by someone who sees only half the picture
|
|
400
|
+
* (e.g. re-gating line ~303 of UserAuthorization on getApiKey, or on the
|
|
401
|
+
* absence of an access token). Both of those rewrites reach
|
|
402
|
+
* JSONWebToken.decode, which is exactly what this pins shut.
|
|
403
|
+
*/
|
|
404
|
+
it("prefers the malformed key over a session token rather than silently falling back", async () => {
|
|
405
|
+
const outcome: RouteOutcome = await callGetList({
|
|
406
|
+
crudApiPath: LOG_PIPELINE_PATH,
|
|
407
|
+
headers: { apikey: "", authorization: "Bearer some.session.token" },
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
expect(outcome.error).toBeInstanceOf(BadDataException);
|
|
411
|
+
expect(outcome.error?.message).toBe("Invalid API Key");
|
|
412
|
+
expect(outcome.error?.code).toBe(400);
|
|
413
|
+
|
|
414
|
+
// The session branch was never entered, so no token was ever decoded.
|
|
415
|
+
expect(decodeAccessToken).not.toHaveBeenCalled();
|
|
416
|
+
expect(outcome.request.userType).not.toBe(UserType.Public);
|
|
417
|
+
expect(repositoryFind).not.toHaveBeenCalled();
|
|
418
|
+
});
|
|
419
|
+
});
|
|
420
|
+
|
|
421
|
+
describe("C) whitespace-only ApiKey header", () => {
|
|
422
|
+
/*
|
|
423
|
+
* `ApiKey: $ONEUPTIME_API_KEY` with the variable unset arrives as spaces,
|
|
424
|
+
* not as an absent header. Trimming makes it empty, so it is the same
|
|
425
|
+
* malformed-key failure as B - not the anonymous 401.
|
|
426
|
+
*/
|
|
427
|
+
it("answers 400 Invalid API Key", async () => {
|
|
428
|
+
const outcome: RouteOutcome = await callGetList({
|
|
429
|
+
crudApiPath: LOG_PIPELINE_PATH,
|
|
430
|
+
headers: { apikey: " " },
|
|
431
|
+
});
|
|
432
|
+
|
|
433
|
+
expect(outcome.error).toBeInstanceOf(BadDataException);
|
|
434
|
+
expect(outcome.error?.message).toBe("Invalid API Key");
|
|
435
|
+
expect(outcome.error?.code).toBe(400);
|
|
436
|
+
expect(repositoryFind).not.toHaveBeenCalled();
|
|
437
|
+
});
|
|
438
|
+
});
|
|
439
|
+
|
|
440
|
+
describe("D) non-UUID ApiKey header", () => {
|
|
441
|
+
/*
|
|
442
|
+
* ApiKey.apiKey is a Postgres `uuid` column. A non-UUID value used to be
|
|
443
|
+
* handed straight to the lookup, which raised 22P02 "invalid input syntax
|
|
444
|
+
* for type uuid" as a raw QueryFailedError - not a OneUptime Exception, so
|
|
445
|
+
* it slipped past the error translator and answered 500 to what is really a
|
|
446
|
+
* malformed request. The shape is rejected before the query now, which is
|
|
447
|
+
* why findApiKey must not be called at all.
|
|
448
|
+
*/
|
|
449
|
+
it("answers 400 Invalid API Key without ever querying the key table", async () => {
|
|
450
|
+
const outcome: RouteOutcome = await callGetList({
|
|
451
|
+
crudApiPath: LOG_PIPELINE_PATH,
|
|
452
|
+
headers: { apikey: "not-a-uuid" },
|
|
453
|
+
});
|
|
454
|
+
|
|
455
|
+
expect(outcome.error).toBeInstanceOf(BadDataException);
|
|
456
|
+
expect(outcome.error?.message).toBe("Invalid API Key");
|
|
457
|
+
expect(outcome.error?.code).toBe(400);
|
|
458
|
+
expect(findApiKey).not.toHaveBeenCalled();
|
|
459
|
+
expect(outcome.error?.code).not.toBe(500);
|
|
460
|
+
});
|
|
461
|
+
|
|
462
|
+
/*
|
|
463
|
+
* A caller that sets the header twice. Node normally collapses repeated
|
|
464
|
+
* headers into one comma-separated value, and IncomingHttpHeaders also
|
|
465
|
+
* types a header as string[]; the middleware handles both by joining
|
|
466
|
+
* before it validates. Either way it is never a usable key - but it is
|
|
467
|
+
* unmistakably someone authenticating by key, so it has to be claimed as a
|
|
468
|
+
* key problem rather than fall through to the anonymous 401.
|
|
469
|
+
*/
|
|
470
|
+
it("treats a duplicated ApiKey header as a malformed key, not as an absent one", async () => {
|
|
471
|
+
const outcome: RouteOutcome = await callGetList({
|
|
472
|
+
crudApiPath: LOG_PIPELINE_PATH,
|
|
473
|
+
headers: { apikey: [KNOWN_API_KEY, UNKNOWN_API_KEY] },
|
|
474
|
+
});
|
|
475
|
+
|
|
476
|
+
expect(outcome.error).toBeInstanceOf(BadDataException);
|
|
477
|
+
expect(outcome.error?.message).toBe("Invalid API Key");
|
|
478
|
+
expect(outcome.error?.code).toBe(400);
|
|
479
|
+
expect(findApiKey).not.toHaveBeenCalled();
|
|
480
|
+
});
|
|
481
|
+
});
|
|
482
|
+
|
|
483
|
+
describe("E) well-formed but unknown API key", () => {
|
|
484
|
+
/*
|
|
485
|
+
* A key that is the right shape but is not in the table (revoked, expired,
|
|
486
|
+
* or from another install) and is not the instance master key. Same 400 as
|
|
487
|
+
* a malformed one: the API deliberately does not distinguish "wrong shape"
|
|
488
|
+
* from "unknown key" to a caller.
|
|
489
|
+
*/
|
|
490
|
+
it("answers 400 Invalid API Key after the lookup misses", async () => {
|
|
491
|
+
const outcome: RouteOutcome = await callGetList({
|
|
492
|
+
crudApiPath: LOG_PIPELINE_PATH,
|
|
493
|
+
headers: { apikey: UNKNOWN_API_KEY },
|
|
494
|
+
});
|
|
495
|
+
|
|
496
|
+
expect(findApiKey).toHaveBeenCalledTimes(1);
|
|
497
|
+
expect(outcome.error).toBeInstanceOf(BadDataException);
|
|
498
|
+
expect(outcome.error?.message).toBe("Invalid API Key");
|
|
499
|
+
expect(outcome.error?.code).toBe(400);
|
|
500
|
+
expect(repositoryFind).not.toHaveBeenCalled();
|
|
501
|
+
});
|
|
502
|
+
});
|
|
503
|
+
|
|
504
|
+
describe("F) valid key holding a permission the model accepts", () => {
|
|
505
|
+
/*
|
|
506
|
+
* TelemetryAdmin is in LogPipeline's read list. The request must reach the
|
|
507
|
+
* handler and complete: this is the case the reporter believed was broken,
|
|
508
|
+
* and it is what proves the 401 they saw was about the header never
|
|
509
|
+
* arriving, not about the key's roles.
|
|
510
|
+
*/
|
|
511
|
+
it("reaches the handler and returns the list", async () => {
|
|
512
|
+
grantApiKeyPermission(Permission.TelemetryAdmin);
|
|
513
|
+
|
|
514
|
+
const outcome: RouteOutcome = await callGetList({
|
|
515
|
+
crudApiPath: LOG_PIPELINE_PATH,
|
|
516
|
+
headers: { apikey: KNOWN_API_KEY },
|
|
517
|
+
});
|
|
518
|
+
|
|
519
|
+
expect(outcome.error).toBeNull();
|
|
520
|
+
expect(repositoryFind).toHaveBeenCalledTimes(1);
|
|
521
|
+
expect(Response.sendEntityArrayResponse).toHaveBeenCalledTimes(1);
|
|
522
|
+
});
|
|
523
|
+
|
|
524
|
+
/*
|
|
525
|
+
* The tenant is taken from the KEY, and the caller is typed as an API
|
|
526
|
+
* principal - that UserType.API is what lets the logged-in check in
|
|
527
|
+
* PublicPermission return early instead of throwing case A at a caller
|
|
528
|
+
* who has no userId and never will.
|
|
529
|
+
*/
|
|
530
|
+
it("scopes the request to the key's own project and marks it an API caller", async () => {
|
|
531
|
+
grantApiKeyPermission(Permission.TelemetryAdmin);
|
|
532
|
+
|
|
533
|
+
const outcome: RouteOutcome = await callGetList({
|
|
534
|
+
crudApiPath: LOG_PIPELINE_PATH,
|
|
535
|
+
headers: { apikey: KNOWN_API_KEY },
|
|
536
|
+
});
|
|
537
|
+
|
|
538
|
+
expect(outcome.request.userType).toBe(UserType.API);
|
|
539
|
+
expect(outcome.request.tenantId?.toString()).toBe(PROJECT_ID.toString());
|
|
540
|
+
});
|
|
541
|
+
|
|
542
|
+
/*
|
|
543
|
+
* ...and never from a caller-supplied header. getUserMiddleware sets
|
|
544
|
+
* req.tenantId from the `tenantid` header before it has looked at the key
|
|
545
|
+
* at all, so the key's own projectId has to overwrite it - otherwise a key
|
|
546
|
+
* for project A would be evaluated against project B's id just because the
|
|
547
|
+
* caller said so, and the key's permissions (registered under its real
|
|
548
|
+
* project) would not be found for the claimed one.
|
|
549
|
+
*/
|
|
550
|
+
it("ignores a tenantid header that points at a different project", async () => {
|
|
551
|
+
grantApiKeyPermission(Permission.TelemetryAdmin);
|
|
552
|
+
|
|
553
|
+
const outcome: RouteOutcome = await callGetList({
|
|
554
|
+
crudApiPath: LOG_PIPELINE_PATH,
|
|
555
|
+
headers: { apikey: KNOWN_API_KEY, tenantid: OTHER_PROJECT_ID },
|
|
556
|
+
});
|
|
557
|
+
|
|
558
|
+
expect(outcome.request.tenantId?.toString()).toBe(PROJECT_ID.toString());
|
|
559
|
+
expect(outcome.request.tenantId?.toString()).not.toBe(OTHER_PROJECT_ID);
|
|
560
|
+
expect(outcome.error).toBeNull();
|
|
561
|
+
expect(repositoryFind).toHaveBeenCalledTimes(1);
|
|
562
|
+
});
|
|
563
|
+
|
|
564
|
+
/*
|
|
565
|
+
* A padded value is a usable key, not a malformed one - trimming happens
|
|
566
|
+
* before validation, so a header that picked up whitespace still works.
|
|
567
|
+
* The lookup has to receive the TRIMMED value: the padded string is not a
|
|
568
|
+
* valid uuid literal, so passing it through would raise 22P02 in the query
|
|
569
|
+
* layer and answer 500 to a request that was only untidy.
|
|
570
|
+
*/
|
|
571
|
+
it("trims a key padded with whitespace instead of querying the padded value", async () => {
|
|
572
|
+
grantApiKeyPermission(Permission.TelemetryAdmin);
|
|
573
|
+
|
|
574
|
+
const outcome: RouteOutcome = await callGetList({
|
|
575
|
+
crudApiPath: LOG_PIPELINE_PATH,
|
|
576
|
+
headers: { apikey: ` ${KNOWN_API_KEY} ` },
|
|
577
|
+
});
|
|
578
|
+
|
|
579
|
+
expect(findApiKey).toHaveBeenCalledWith(new ObjectID(KNOWN_API_KEY));
|
|
580
|
+
expect(outcome.error).toBeNull();
|
|
581
|
+
expect(Response.sendEntityArrayResponse).toHaveBeenCalledTimes(1);
|
|
582
|
+
});
|
|
583
|
+
});
|
|
584
|
+
|
|
585
|
+
describe("G) valid key holding ProjectAdmin", () => {
|
|
586
|
+
// A second accepted permission, so F is not passing off one lucky enum.
|
|
587
|
+
it("reaches the handler and returns the list", async () => {
|
|
588
|
+
grantApiKeyPermission(Permission.ProjectAdmin);
|
|
589
|
+
|
|
590
|
+
const outcome: RouteOutcome = await callGetList({
|
|
591
|
+
crudApiPath: LOG_PIPELINE_PATH,
|
|
592
|
+
headers: { apikey: KNOWN_API_KEY },
|
|
593
|
+
});
|
|
594
|
+
|
|
595
|
+
expect(outcome.error).toBeNull();
|
|
596
|
+
expect(repositoryFind).toHaveBeenCalledTimes(1);
|
|
597
|
+
expect(Response.sendEntityArrayResponse).toHaveBeenCalledTimes(1);
|
|
598
|
+
});
|
|
599
|
+
});
|
|
600
|
+
|
|
601
|
+
describe("H) valid key holding an unrelated permission", () => {
|
|
602
|
+
/*
|
|
603
|
+
* This is what a real per-model RBAC failure looks like, and it is nothing
|
|
604
|
+
* like what #3004 saw: 422, not 401; it names the operation and the model
|
|
605
|
+
* AND lists the permissions that would have worked, so the key's owner can
|
|
606
|
+
* fix it. If the reported bug had been what it appeared to be, this is the
|
|
607
|
+
* response they would have received.
|
|
608
|
+
*/
|
|
609
|
+
it("answers 422 listing the permissions that would have worked", async () => {
|
|
610
|
+
grantApiKeyPermission(Permission.CreateProjectApiKey);
|
|
611
|
+
|
|
612
|
+
const outcome: RouteOutcome = await callGetList({
|
|
613
|
+
crudApiPath: LOG_PIPELINE_PATH,
|
|
614
|
+
headers: { apikey: KNOWN_API_KEY },
|
|
615
|
+
});
|
|
616
|
+
|
|
617
|
+
expect(outcome.error).toBeInstanceOf(NotAuthorizedException);
|
|
618
|
+
expect(outcome.error?.code).toBe(422);
|
|
619
|
+
expect(outcome.error?.message).toContain(
|
|
620
|
+
"You do not have permissions to read Log Pipeline",
|
|
621
|
+
);
|
|
622
|
+
expect(outcome.error?.message).toContain(
|
|
623
|
+
"You need one of these permissions:",
|
|
624
|
+
);
|
|
625
|
+
expect(outcome.error?.message).toContain("Telemetry Admin");
|
|
626
|
+
expect(Response.sendEntityArrayResponse).not.toHaveBeenCalled();
|
|
627
|
+
});
|
|
628
|
+
|
|
629
|
+
/*
|
|
630
|
+
* An authenticated key that lacks a role is never reported as
|
|
631
|
+
* unauthenticated. The request must still be refused - asserting only the
|
|
632
|
+
* two negatives would pass if the read had been allowed through, so the
|
|
633
|
+
* refusal itself is asserted first.
|
|
634
|
+
*/
|
|
635
|
+
it("does not answer the anonymous 401 for an authenticated key", async () => {
|
|
636
|
+
grantApiKeyPermission(Permission.CreateProjectApiKey);
|
|
637
|
+
|
|
638
|
+
const outcome: RouteOutcome = await callGetList({
|
|
639
|
+
crudApiPath: LOG_PIPELINE_PATH,
|
|
640
|
+
headers: { apikey: KNOWN_API_KEY },
|
|
641
|
+
});
|
|
642
|
+
|
|
643
|
+
expect(outcome.error).not.toBeNull();
|
|
644
|
+
expect(repositoryFind).not.toHaveBeenCalled();
|
|
645
|
+
expect(outcome.error).not.toBeInstanceOf(NotAuthenticatedException);
|
|
646
|
+
expect(outcome.error?.code).not.toBe(401);
|
|
647
|
+
expect(outcome.error?.message).not.toContain("needed to read record of");
|
|
648
|
+
});
|
|
649
|
+
});
|
|
650
|
+
|
|
651
|
+
/*
|
|
652
|
+
* I) CONTROL.
|
|
653
|
+
*
|
|
654
|
+
* The issue premise was that the fault was "specific to the Log Pipeline CRUD
|
|
655
|
+
* authentication path". Monitor is registered through the same BaseAPI in the
|
|
656
|
+
* same way and behaves identically - same exception, same status, same
|
|
657
|
+
* wording apart from the model name. One run of this block refutes that
|
|
658
|
+
* premise: nothing above belongs to Log Pipeline.
|
|
659
|
+
*/
|
|
660
|
+
describe("I) control: a different model registered the same way", () => {
|
|
661
|
+
it("answers the same 401 with no ApiKey header, differing only in the model name", async () => {
|
|
662
|
+
const logPipeline: RouteOutcome = await callGetList({
|
|
663
|
+
crudApiPath: LOG_PIPELINE_PATH,
|
|
664
|
+
headers: {},
|
|
665
|
+
});
|
|
666
|
+
|
|
667
|
+
const monitor: RouteOutcome = await callGetList({
|
|
668
|
+
crudApiPath: MONITOR_PATH,
|
|
669
|
+
headers: {},
|
|
670
|
+
});
|
|
671
|
+
|
|
672
|
+
expect(monitor.error).toBeInstanceOf(NotAuthenticatedException);
|
|
673
|
+
expect(monitor.error?.code).toBe(401);
|
|
674
|
+
expect(monitor.error?.message).toBe(
|
|
675
|
+
"Authenticated user or a valid API key is needed to read record of Monitor.",
|
|
676
|
+
);
|
|
677
|
+
|
|
678
|
+
expect(monitor.error?.message.replace("Monitor", "Log Pipeline")).toBe(
|
|
679
|
+
logPipeline.error?.message,
|
|
680
|
+
);
|
|
681
|
+
expect(monitor.error?.code).toBe(logPipeline.error?.code);
|
|
682
|
+
});
|
|
683
|
+
|
|
684
|
+
it("answers the same 400 Invalid API Key for an empty ApiKey header", async () => {
|
|
685
|
+
const logPipeline: RouteOutcome = await callGetList({
|
|
686
|
+
crudApiPath: LOG_PIPELINE_PATH,
|
|
687
|
+
headers: { apikey: "" },
|
|
688
|
+
});
|
|
689
|
+
|
|
690
|
+
const monitor: RouteOutcome = await callGetList({
|
|
691
|
+
crudApiPath: MONITOR_PATH,
|
|
692
|
+
headers: { apikey: "" },
|
|
693
|
+
});
|
|
694
|
+
|
|
695
|
+
expect(monitor.error).toBeInstanceOf(BadDataException);
|
|
696
|
+
expect(monitor.error?.code).toBe(400);
|
|
697
|
+
expect(monitor.error?.message).toBe("Invalid API Key");
|
|
698
|
+
|
|
699
|
+
// Byte for byte the same answer, because the model plays no part in it.
|
|
700
|
+
expect(monitor.error?.message).toBe(logPipeline.error?.message);
|
|
701
|
+
expect(monitor.error?.code).toBe(logPipeline.error?.code);
|
|
702
|
+
});
|
|
703
|
+
});
|
|
704
|
+
});
|