@oneuptime/common 12.0.34 → 13.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Models/DatabaseModels/GlobalConfig.ts +12 -12
- package/Models/DatabaseModels/InventoryItem.ts +44 -1
- package/Models/DatabaseModels/NetworkDevice.ts +126 -0
- package/Server/EnvironmentConfig.ts +47 -16
- package/Server/Infrastructure/Postgres/SchemaMigrations/1791700000000-AddMacAddressToNetworkDevice.ts +25 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +2 -0
- package/Server/Infrastructure/Redis.ts +8 -8
- package/Server/Infrastructure/Status.ts +10 -4
- package/Server/Services/DatabaseService.ts +65 -13
- package/Server/Services/InventoryItemService.ts +31 -1
- package/Server/Services/MonitorTemplateService.ts +140 -19
- package/Server/Services/NetworkDeviceAutoImportRuleEngineService.ts +349 -41
- package/Server/Services/NetworkDeviceDiscoveryScanService.ts +87 -2
- package/Server/Services/NetworkDeviceService.ts +69 -0
- package/Server/Services/UserNotificationRuleService.ts +76 -25
- package/Server/Types/Database/Permissions/ReadPermission.ts +45 -5
- package/Server/Types/Database/QueryUtil.ts +56 -0
- package/Server/Types/Markdown.ts +93 -1
- package/Server/Utils/Dashboard/PublicDashboardResourceListPolicy.ts +82 -17
- package/Server/Utils/EmailRollup/EmailRollupFlushRunner.ts +10 -1
- package/Server/Utils/EmailRollup/EmailRollupRenderer.ts +2 -2
- package/Server/Utils/Monitor/Criteria/CompareCriteria.ts +82 -35
- package/Server/Utils/Monitor/Criteria/MetricMonitorCriteria.ts +31 -5
- package/Server/Utils/Monitor/MonitorCriteriaEvaluator.ts +202 -37
- package/Server/Utils/Monitor/MonitorCriteriaExpectationBuilder.ts +82 -9
- package/Server/Utils/Monitor/MonitorCriteriaMessageBuilder.ts +16 -7
- package/Server/Utils/Monitor/MonitorCriteriaMessageFormatter.ts +29 -17
- package/Server/Utils/Monitor/MonitorCriteriaObservationBuilder.ts +107 -48
- package/Server/Utils/Monitor/NetworkDeviceMacLearningUtil.ts +158 -0
- package/Server/Utils/Monitor/NetworkInventoryUtil.ts +27 -0
- package/Server/Utils/Telemetry/EntityRegistry.ts +11 -1
- package/Server/Utils/Telemetry/TelemetryEntity.ts +83 -9
- package/Tests/App/Dashboard/AddNeighborToMonitoringModal.test.tsx +32 -0
- package/Tests/App/Dashboard/AdoptedDeviceDrawer.test.tsx +374 -0
- package/Tests/App/Dashboard/DashboardLabelVariables.test.tsx +580 -0
- package/Tests/App/Dashboard/DashboardMonitorLabelVariable.test.tsx +438 -0
- package/Tests/App/Dashboard/DashboardVariableAllContract.test.tsx +5 -2
- package/Tests/App/Dashboard/DeviceAttachmentCard.test.tsx +273 -0
- package/Tests/App/Dashboard/DiscoveryReviewInventoryRefresh.test.tsx +505 -0
- package/Tests/App/Dashboard/EntityDetailPanel.test.tsx +163 -0
- package/Tests/App/Dashboard/InfrastructureExplorer.test.tsx +401 -0
- package/Tests/App/Dashboard/InfrastructureGraph.test.tsx +232 -0
- package/Tests/App/Dashboard/InventoryItems.test.tsx +201 -0
- package/Tests/App/Dashboard/InventoryTypeAndStatusFacets.test.tsx +798 -0
- package/Tests/App/Dashboard/NetworkTopologyLiveView.test.tsx +320 -0
- package/Tests/App/Dashboard/NetworkTopologyToolbar.test.tsx +275 -0
- package/Tests/App/Dashboard/OnCallCalendarFeedEmptyState.test.tsx +975 -0
- package/Tests/App/Dashboard/OnCallCalendarFeedPlanGate.test.tsx +63 -0
- package/Tests/App/Dashboard/ServiceMapGraph.test.tsx +626 -0
- package/Tests/App/Dashboard/TopologyDataLoading.test.tsx +348 -0
- package/Tests/App/Dashboard/TopologyPageNavigation.test.tsx +331 -0
- package/Tests/App/Dashboard/UserSettingsEmailPreferences.test.tsx +603 -0
- package/Tests/App/Dashboard/UserSettingsNotificationSettings.test.tsx +324 -0
- package/Tests/App/PublicDashboard/DashboardVariableSelector.test.tsx +13 -4
- package/Tests/App/StatusPage/StatusPageOidcOrigin.test.tsx +223 -0
- package/Tests/Models/InventoryItemStatus.test.ts +147 -0
- package/Tests/Models/NetworkDeviceMacAddressColumn.test.ts +294 -0
- package/Tests/Server/API/DashboardPublicResourceListAPI.test.ts +129 -0
- package/Tests/Server/EnvironmentConfigValkey.test.ts +227 -0
- package/Tests/Server/Infrastructure/Postgres/AddMacAddressToNetworkDeviceMigration.test.ts +365 -0
- package/Tests/Server/Infrastructure/Postgres/CustomFieldValueMappingMigration.test.ts +62 -23
- package/Tests/Server/Infrastructure/ValkeyDeploymentConfig.test.ts +265 -0
- package/Tests/Server/Middleware/HttpMetricsMiddleware.test.ts +382 -0
- package/Tests/Server/Middleware/SCIMAuthorization.test.ts +525 -0
- package/Tests/Server/Services/DatabaseServiceAggregateBy.test.ts +291 -56
- package/Tests/Server/Services/DatabaseServiceSortTiebreaker.test.ts +151 -0
- package/Tests/Server/Services/InventoryItemManualCreate.test.ts +105 -0
- package/Tests/Server/Services/InventoryItemPromotionGate.test.ts +178 -0
- package/Tests/Server/Services/MonitorTemplateServiceCustomFieldSync.test.ts +502 -0
- package/Tests/Server/Services/NetworkDeviceAutoImportRuleEngineService.test.ts +1133 -81
- package/Tests/Server/Services/NetworkDeviceDiscoveryScanRegistration.test.ts +520 -0
- package/Tests/Server/Services/NetworkDeviceMacAddressNormalization.test.ts +572 -0
- package/Tests/Server/Types/Database/InventoryStatusPostgres.test.ts +368 -0
- package/Tests/Server/Types/Database/Permissions/ReadBlockPermission.test.ts +261 -14
- package/Tests/Server/Types/Database/QueryUtilIncludesAnyOfGroups.test.ts +169 -0
- package/Tests/Server/Types/Database/QueryUtilIncludesAnyOfGroupsPostgres.test.ts +297 -0
- package/Tests/Server/Types/Markdown.test.ts +194 -1
- package/Tests/Server/Types/Workflow/Components/IncomingWebhookUtils.test.ts +279 -0
- package/Tests/Server/Types/Workflow/Components/TextToJson.test.ts +212 -0
- package/Tests/Server/Utils/CustomField/CustomFieldMappingRegistry.test.ts +436 -0
- package/Tests/Server/Utils/Dashboard/PublicDashboardResourceListPolicy.test.ts +214 -0
- package/Tests/Server/Utils/EmailRollup/EmailRollupFlushRunnerBehaviour.test.ts +1 -1
- package/Tests/Server/Utils/EmailRollup/EmailRollupRenderer.test.ts +10 -3
- package/Tests/Server/Utils/EventLoop.test.ts +359 -0
- package/Tests/Server/Utils/JsonWebToken.test.ts +30 -0
- package/Tests/Server/Utils/Monitor/Criteria/CompareCriteria.test.ts +242 -0
- package/Tests/Server/Utils/Monitor/Criteria/CompareCriteriaAggregation.test.ts +13 -7
- package/Tests/Server/Utils/Monitor/Criteria/DatabaseMonitorCriteria.test.ts +29 -0
- package/Tests/Server/Utils/Monitor/Criteria/MetricMonitorCriteria.test.ts +30 -6
- package/Tests/Server/Utils/Monitor/Criteria/MetricMonitorCriteriaAnomalyUnits.test.ts +246 -0
- package/Tests/Server/Utils/Monitor/MonitorCriteriaEmailEvalLogAgreement.test.ts +280 -0
- package/Tests/Server/Utils/Monitor/MonitorCriteriaEvaluator.test.ts +224 -10
- package/Tests/Server/Utils/Monitor/MonitorCriteriaEvaluatorMetricUnits.test.ts +570 -0
- package/Tests/Server/Utils/Monitor/MonitorCriteriaExpectationBuilderUnits.test.ts +30 -5
- package/Tests/Server/Utils/Monitor/MonitorCriteriaMessageBuilderUnits.test.ts +68 -21
- package/Tests/Server/Utils/Monitor/MonitorCriteriaObservationBuilderUnits.test.ts +36 -19
- package/Tests/Server/Utils/Monitor/NetworkDeviceMacLearningUtil.test.ts +573 -0
- package/Tests/Server/Utils/Monitor/NetworkInventoryUtil.test.ts +317 -0
- package/Tests/Server/Utils/Telemetry/ContextSpanProcessor.test.ts +281 -0
- package/Tests/Server/Utils/Telemetry/InventoryDuplicateIdentity.test.ts +316 -0
- package/Tests/Server/Utils/Telemetry/TelemetryEntity.test.ts +111 -8
- package/Tests/Types/Database/IncludesAnyOfGroups.test.ts +106 -0
- package/Tests/Types/JSONFunctions.test.ts +40 -0
- package/Tests/Types/NetworkAutomation/AutoImportRunMerge.test.ts +235 -0
- package/Tests/Types/NetworkAutomation/RuleRunResult.test.ts +66 -0
- package/Tests/Types/NetworkAutomation/RuleRunResultDescribe.test.ts +309 -1
- package/Tests/Types/Telemetry/InventoryLiveness.test.ts +99 -0
- package/Tests/UI/Components/CustomFields/CustomFieldsDetail.test.tsx +160 -0
- package/Tests/UI/ReactRouterSingletonBuild.test.ts +133 -0
- package/Tests/UI/Rum/ReplayHeader.test.tsx +10 -2
- package/Tests/UI/Rum/ReplayPlayerChrome.test.tsx +514 -0
- package/Tests/UI/Rum/ReplayTimeFormat.test.ts +94 -0
- package/Tests/UI/Rum/ReplayUi.test.tsx +559 -0
- package/Tests/UI/Utils/Breadcrumb/fixtures/RealBreadcrumbTrails.ts +5 -0
- package/Tests/UI/Utils/Breadcrumb/fixtures/RealRoutePatterns.ts +1 -0
- package/Tests/Utils/Dashboard/Components/DashboardMonitorListComponent.test.ts +11 -1
- package/Tests/Utils/Dashboard/LabelVariable.test.ts +257 -0
- package/Tests/Utils/Dashboard/VariableUrlState.test.ts +144 -14
- package/Tests/Utils/MetricUnitUtil.test.ts +81 -0
- package/Tests/Utils/Monitor/DeviceMacLearningUtil.test.ts +497 -0
- package/Tests/Utils/Monitor/MetricValueFormatter.test.ts +546 -0
- package/Tests/Utils/Monitor/MonitorTemplateCustomFieldUtil.test.ts +218 -0
- package/Tests/Utils/Monitor/NetworkTopologyEndpointAdoption.test.ts +1755 -0
- package/Tests/Utils/NetworkAutomation/AutoImportRunChain.test.ts +320 -0
- package/Types/BaseDatabase/IncludesAnyOfGroups.ts +77 -0
- package/Types/Dashboard/DashboardComponents/ComponentArgument.ts +1 -0
- package/Types/Dashboard/DashboardComponents/DashboardMonitorListComponent.ts +1 -0
- package/Types/Dashboard/DashboardVariable.ts +8 -0
- package/Types/JSON.ts +4 -0
- package/Types/JSONFunctions.ts +16 -0
- package/Types/Monitor/SnmpMonitor/NetworkTopology.ts +13 -0
- package/Types/NetworkAutomation/RuleRunResult.ts +402 -13
- package/Types/SerializableObjectDictionary.ts +4 -0
- package/Types/Telemetry/InventoryLiveness.ts +56 -0
- package/UI/Components/CSVFileUpload/CSVFileUpload.tsx +9 -2
- package/UI/Components/CustomFields/CustomFieldsDetail.tsx +46 -21
- package/UI/Components/Dashboard/DashboardVariableControl.tsx +285 -0
- package/UI/esbuild-config.js +6 -0
- package/Utils/Dashboard/Components/DashboardMonitorListComponent.ts +12 -1
- package/Utils/Dashboard/LabelVariable.ts +152 -0
- package/Utils/Dashboard/VariableUrlState.ts +27 -5
- package/Utils/MetricUnitUtil.ts +34 -0
- package/Utils/Monitor/DeviceMacLearningUtil.ts +177 -0
- package/Utils/Monitor/MetricValueFormatter.ts +342 -0
- package/Utils/Monitor/MonitorTemplateCustomFieldUtil.ts +124 -0
- package/Utils/Monitor/NetworkDeviceMonitorTemplateUtil.ts +7 -2
- package/Utils/Monitor/NetworkTopologyUtil.ts +693 -0
- package/Utils/NetworkAutomation/AutoImportRunChain.ts +138 -0
- package/build/dist/Models/DatabaseModels/GlobalConfig.js +12 -12
- package/build/dist/Models/DatabaseModels/GlobalConfig.js.map +1 -1
- package/build/dist/Models/DatabaseModels/InventoryItem.js +34 -1
- package/build/dist/Models/DatabaseModels/InventoryItem.js.map +1 -1
- package/build/dist/Models/DatabaseModels/NetworkDevice.js +128 -0
- package/build/dist/Models/DatabaseModels/NetworkDevice.js.map +1 -1
- package/build/dist/Server/EnvironmentConfig.js +44 -12
- package/build/dist/Server/EnvironmentConfig.js.map +1 -1
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1791700000000-AddMacAddressToNetworkDevice.js +14 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1791700000000-AddMacAddressToNetworkDevice.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +2 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
- package/build/dist/Server/Infrastructure/Redis.js +8 -8
- package/build/dist/Server/Infrastructure/Redis.js.map +1 -1
- package/build/dist/Server/Infrastructure/Status.js +10 -4
- package/build/dist/Server/Infrastructure/Status.js.map +1 -1
- package/build/dist/Server/Services/DatabaseService.js +58 -13
- package/build/dist/Server/Services/DatabaseService.js.map +1 -1
- package/build/dist/Server/Services/InventoryItemService.js +28 -1
- package/build/dist/Server/Services/InventoryItemService.js.map +1 -1
- package/build/dist/Server/Services/MonitorTemplateService.js +116 -17
- package/build/dist/Server/Services/MonitorTemplateService.js.map +1 -1
- package/build/dist/Server/Services/NetworkDeviceAutoImportRuleEngineService.js +270 -37
- package/build/dist/Server/Services/NetworkDeviceAutoImportRuleEngineService.js.map +1 -1
- package/build/dist/Server/Services/NetworkDeviceDiscoveryScanService.js +65 -0
- package/build/dist/Server/Services/NetworkDeviceDiscoveryScanService.js.map +1 -1
- package/build/dist/Server/Services/NetworkDeviceService.js +54 -0
- package/build/dist/Server/Services/NetworkDeviceService.js.map +1 -1
- package/build/dist/Server/Services/UserNotificationRuleService.js +60 -25
- package/build/dist/Server/Services/UserNotificationRuleService.js.map +1 -1
- package/build/dist/Server/Types/Database/Permissions/ReadPermission.js +29 -4
- package/build/dist/Server/Types/Database/Permissions/ReadPermission.js.map +1 -1
- package/build/dist/Server/Types/Database/QueryUtil.js +36 -0
- package/build/dist/Server/Types/Database/QueryUtil.js.map +1 -1
- package/build/dist/Server/Types/Markdown.js +75 -0
- package/build/dist/Server/Types/Markdown.js.map +1 -1
- package/build/dist/Server/Utils/Dashboard/PublicDashboardResourceListPolicy.js +53 -18
- package/build/dist/Server/Utils/Dashboard/PublicDashboardResourceListPolicy.js.map +1 -1
- package/build/dist/Server/Utils/EmailRollup/EmailRollupFlushRunner.js +10 -1
- package/build/dist/Server/Utils/EmailRollup/EmailRollupFlushRunner.js.map +1 -1
- package/build/dist/Server/Utils/EmailRollup/EmailRollupRenderer.js +2 -2
- package/build/dist/Server/Utils/EmailRollup/EmailRollupRenderer.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/Criteria/CompareCriteria.js +58 -35
- package/build/dist/Server/Utils/Monitor/Criteria/CompareCriteria.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/Criteria/MetricMonitorCriteria.js +29 -5
- package/build/dist/Server/Utils/Monitor/Criteria/MetricMonitorCriteria.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/MonitorCriteriaEvaluator.js +166 -35
- package/build/dist/Server/Utils/Monitor/MonitorCriteriaEvaluator.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/MonitorCriteriaExpectationBuilder.js +58 -9
- package/build/dist/Server/Utils/Monitor/MonitorCriteriaExpectationBuilder.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/MonitorCriteriaMessageBuilder.js +9 -3
- package/build/dist/Server/Utils/Monitor/MonitorCriteriaMessageBuilder.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/MonitorCriteriaMessageFormatter.js +26 -12
- package/build/dist/Server/Utils/Monitor/MonitorCriteriaMessageFormatter.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/MonitorCriteriaObservationBuilder.js +74 -33
- package/build/dist/Server/Utils/Monitor/MonitorCriteriaObservationBuilder.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/NetworkDeviceMacLearningUtil.js +114 -0
- package/build/dist/Server/Utils/Monitor/NetworkDeviceMacLearningUtil.js.map +1 -0
- package/build/dist/Server/Utils/Monitor/NetworkInventoryUtil.js +25 -0
- package/build/dist/Server/Utils/Monitor/NetworkInventoryUtil.js.map +1 -1
- package/build/dist/Server/Utils/Telemetry/EntityRegistry.js +9 -1
- package/build/dist/Server/Utils/Telemetry/EntityRegistry.js.map +1 -1
- package/build/dist/Server/Utils/Telemetry/TelemetryEntity.js +70 -13
- package/build/dist/Server/Utils/Telemetry/TelemetryEntity.js.map +1 -1
- package/build/dist/Types/BaseDatabase/IncludesAnyOfGroups.js +54 -0
- package/build/dist/Types/BaseDatabase/IncludesAnyOfGroups.js.map +1 -0
- package/build/dist/Types/Dashboard/DashboardComponents/ComponentArgument.js +1 -0
- package/build/dist/Types/Dashboard/DashboardComponents/ComponentArgument.js.map +1 -1
- package/build/dist/Types/Dashboard/DashboardVariable.js +1 -0
- package/build/dist/Types/Dashboard/DashboardVariable.js.map +1 -1
- package/build/dist/Types/JSON.js +1 -0
- package/build/dist/Types/JSON.js.map +1 -1
- package/build/dist/Types/JSONFunctions.js +13 -0
- package/build/dist/Types/JSONFunctions.js.map +1 -1
- package/build/dist/Types/NetworkAutomation/RuleRunResult.js +291 -6
- package/build/dist/Types/NetworkAutomation/RuleRunResult.js.map +1 -1
- package/build/dist/Types/SerializableObjectDictionary.js +4 -0
- package/build/dist/Types/SerializableObjectDictionary.js.map +1 -1
- package/build/dist/Types/Telemetry/InventoryLiveness.js +37 -0
- package/build/dist/Types/Telemetry/InventoryLiveness.js.map +1 -0
- package/build/dist/UI/Components/CSVFileUpload/CSVFileUpload.js +7 -0
- package/build/dist/UI/Components/CSVFileUpload/CSVFileUpload.js.map +1 -1
- package/build/dist/UI/Components/CustomFields/CustomFieldsDetail.js +9 -2
- package/build/dist/UI/Components/CustomFields/CustomFieldsDetail.js.map +1 -1
- package/build/dist/UI/Components/Dashboard/DashboardVariableControl.js +137 -0
- package/build/dist/UI/Components/Dashboard/DashboardVariableControl.js.map +1 -0
- package/build/dist/Utils/Dashboard/Components/DashboardMonitorListComponent.js +9 -1
- package/build/dist/Utils/Dashboard/Components/DashboardMonitorListComponent.js.map +1 -1
- package/build/dist/Utils/Dashboard/LabelVariable.js +96 -0
- package/build/dist/Utils/Dashboard/LabelVariable.js.map +1 -0
- package/build/dist/Utils/Dashboard/VariableUrlState.js +19 -4
- package/build/dist/Utils/Dashboard/VariableUrlState.js.map +1 -1
- package/build/dist/Utils/MetricUnitUtil.js +26 -0
- package/build/dist/Utils/MetricUnitUtil.js.map +1 -1
- package/build/dist/Utils/Monitor/DeviceMacLearningUtil.js +112 -0
- package/build/dist/Utils/Monitor/DeviceMacLearningUtil.js.map +1 -0
- package/build/dist/Utils/Monitor/MetricValueFormatter.js +276 -0
- package/build/dist/Utils/Monitor/MetricValueFormatter.js.map +1 -0
- package/build/dist/Utils/Monitor/MonitorTemplateCustomFieldUtil.js +101 -0
- package/build/dist/Utils/Monitor/MonitorTemplateCustomFieldUtil.js.map +1 -0
- package/build/dist/Utils/Monitor/NetworkDeviceMonitorTemplateUtil.js +7 -2
- package/build/dist/Utils/Monitor/NetworkDeviceMonitorTemplateUtil.js.map +1 -1
- package/build/dist/Utils/Monitor/NetworkTopologyUtil.js +484 -0
- package/build/dist/Utils/Monitor/NetworkTopologyUtil.js.map +1 -1
- package/build/dist/Utils/NetworkAutomation/AutoImportRunChain.js +59 -0
- package/build/dist/Utils/NetworkAutomation/AutoImportRunChain.js.map +1 -0
- package/jest.config.json +2 -0
- package/package.json +1 -1
- package/test-setup.sh +9 -9
- package/tsconfig.json +51 -11
- package/Tests/App/Dashboard/NotificationEmailPreferences.test.tsx +0 -399
|
@@ -1,9 +1,20 @@
|
|
|
1
1
|
import NetworkDeviceService from "../../../Server/Services/NetworkDeviceService";
|
|
2
2
|
import AggregateBy from "../../../Server/Types/Database/AggregateBy";
|
|
3
|
+
import ModelPermission from "../../../Server/Types/Database/Permissions/Index";
|
|
4
|
+
import { CheckReadPermissionType } from "../../../Server/Types/Database/Permissions/ReadPermission";
|
|
3
5
|
import NetworkDevice from "../../../Models/DatabaseModels/NetworkDevice";
|
|
6
|
+
import DatabaseCommonInteractionProps from "../../../Types/BaseDatabase/DatabaseCommonInteractionProps";
|
|
7
|
+
import SortOrder from "../../../Types/BaseDatabase/SortOrder";
|
|
8
|
+
import { TableColumnMetadata } from "../../../Types/Database/TableColumn";
|
|
9
|
+
import TableColumnType from "../../../Types/Database/TableColumnType";
|
|
4
10
|
import BadDataException from "../../../Types/Exception/BadDataException";
|
|
5
11
|
import DatabaseNotConnectedException from "../../../Types/Exception/DatabaseNotConnectedException";
|
|
12
|
+
import ObjectID from "../../../Types/ObjectID";
|
|
13
|
+
import Permission, {
|
|
14
|
+
UserTenantAccessPermission,
|
|
15
|
+
} from "../../../Types/Permission";
|
|
6
16
|
import { describe, expect, it } from "@jest/globals";
|
|
17
|
+
import { FindOperator } from "typeorm";
|
|
7
18
|
import fs from "fs";
|
|
8
19
|
import path from "path";
|
|
9
20
|
|
|
@@ -318,20 +329,38 @@ describe("aggregateBy rejects an expression carrying a statement separator", ()
|
|
|
318
329
|
}
|
|
319
330
|
});
|
|
320
331
|
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
332
|
+
it("rejects a semicolon in an orderBy expression", async () => {
|
|
333
|
+
/*
|
|
334
|
+
* This case used to carry a comment here explaining that it could not be
|
|
335
|
+
* tested: the orderBy check ran inside the loop that APPLIES the order
|
|
336
|
+
* clauses, below `this.buildAggregateScope(...)`, so with no Postgres the
|
|
337
|
+
* call died on DatabaseNotConnectedException before ever reaching it.
|
|
338
|
+
*
|
|
339
|
+
* Production moved. aggregateBy now validates every orderBy expression in
|
|
340
|
+
* a loop of its own, above the permission call and above the query
|
|
341
|
+
* builder, with a comment saying it is placed there precisely so the
|
|
342
|
+
* guard is reachable without a live connection. So the gap closes by
|
|
343
|
+
* actually calling the method, which is worth more than the source-text
|
|
344
|
+
* substitute further down: this fails if the check is deleted, if it
|
|
345
|
+
* stops covering orderBy, or if it starts accepting a separator.
|
|
346
|
+
*
|
|
347
|
+
* The ordering that makes it reachable is itself asserted at the bottom
|
|
348
|
+
* of the file, so this cannot quietly start passing for the wrong reason.
|
|
349
|
+
*/
|
|
350
|
+
const error: Error = await aggregateError({
|
|
351
|
+
orderBy: [
|
|
352
|
+
{
|
|
353
|
+
expression: `COUNT(*); DROP TABLE "NetworkDevice"`,
|
|
354
|
+
sortOrder: SortOrder.Ascending,
|
|
355
|
+
},
|
|
356
|
+
],
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
expect(error).toBeInstanceOf(BadDataException);
|
|
360
|
+
expect(error.message).toBe(
|
|
361
|
+
"Aggregate expressions cannot contain statement separators.",
|
|
362
|
+
);
|
|
363
|
+
});
|
|
335
364
|
});
|
|
336
365
|
|
|
337
366
|
describe("aggregateBy rejects an empty expression", () => {
|
|
@@ -432,21 +461,6 @@ const DATABASE_SERVICE_SOURCE: string = fs.readFileSync(
|
|
|
432
461
|
"utf8",
|
|
433
462
|
);
|
|
434
463
|
|
|
435
|
-
const READ_PERMISSION_SOURCE: string = fs.readFileSync(
|
|
436
|
-
path.join(
|
|
437
|
-
__dirname,
|
|
438
|
-
"..",
|
|
439
|
-
"..",
|
|
440
|
-
"..",
|
|
441
|
-
"Server",
|
|
442
|
-
"Types",
|
|
443
|
-
"Database",
|
|
444
|
-
"Permissions",
|
|
445
|
-
"ReadPermission.ts",
|
|
446
|
-
),
|
|
447
|
-
"utf8",
|
|
448
|
-
);
|
|
449
|
-
|
|
450
464
|
/*
|
|
451
465
|
* Just the body of aggregateBy, and just the body of the helper that builds
|
|
452
466
|
* its scoped query builder. DatabaseService.ts is ~2,600 lines and uses
|
|
@@ -473,16 +487,101 @@ const BUILD_AGGREGATE_SCOPE_BODY: string = squash(
|
|
|
473
487
|
),
|
|
474
488
|
);
|
|
475
489
|
|
|
490
|
+
/*
|
|
491
|
+
* A reader whose grant on this model is LABEL-SCOPED: one tenant permission
|
|
492
|
+
* row carrying labelIds, and nothing else. That is the only kind of caller for
|
|
493
|
+
* whom the permission pipeline produces a condition on the access-control
|
|
494
|
+
* relation, which is the shape the assertions below are about.
|
|
495
|
+
*
|
|
496
|
+
* Built the way ReadBlockPermission.test.ts builds its props, and deliberately
|
|
497
|
+
* ProjectMember rather than a granular ReadNetworkDevice: ProjectMember is in
|
|
498
|
+
* the read list of the table AND of every column the query below names, so a
|
|
499
|
+
* failure here is a failure about the SHAPE of the returned query rather than
|
|
500
|
+
* about a column-permission detail that has nothing to do with aggregates.
|
|
501
|
+
*
|
|
502
|
+
* That is a coupling to NetworkDevice's permission lists, so the caller
|
|
503
|
+
* asserts it as an explicit precondition before calling the pipeline. A
|
|
504
|
+
* tightening of those lists then reports itself as "ProjectMember is no longer
|
|
505
|
+
* in the read list" rather than as a NotAuthorizedException from four files
|
|
506
|
+
* away.
|
|
507
|
+
*/
|
|
508
|
+
function labelScopedReaderProps(
|
|
509
|
+
projectId: ObjectID,
|
|
510
|
+
permittedLabelId: ObjectID,
|
|
511
|
+
): DatabaseCommonInteractionProps {
|
|
512
|
+
const tenantPermission: UserTenantAccessPermission = {
|
|
513
|
+
projectId: projectId,
|
|
514
|
+
_type: "UserTenantAccessPermission",
|
|
515
|
+
permissions: [
|
|
516
|
+
{
|
|
517
|
+
_type: "UserPermission",
|
|
518
|
+
permission: Permission.ProjectMember,
|
|
519
|
+
labelIds: [permittedLabelId],
|
|
520
|
+
isBlockPermission: false,
|
|
521
|
+
},
|
|
522
|
+
],
|
|
523
|
+
};
|
|
524
|
+
|
|
525
|
+
return {
|
|
526
|
+
userId: ObjectID.generate(),
|
|
527
|
+
tenantId: projectId,
|
|
528
|
+
userTenantAccessPermission: {
|
|
529
|
+
[projectId.toString()]: tenantPermission,
|
|
530
|
+
},
|
|
531
|
+
};
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
/**
|
|
535
|
+
* Every id a find operator carries, whatever slot the operator keeps them in.
|
|
536
|
+
*
|
|
537
|
+
* The point is to assert WHICH labels the permission pipeline let through
|
|
538
|
+
* without asserting how the operator spells itself in SQL. QueryHelper builds
|
|
539
|
+
* set membership as a TypeORM `Raw` today, whose ids live in
|
|
540
|
+
* `objectLiteralParameters` and whose `value` is an empty array; TypeORM's own
|
|
541
|
+
* `In([...])` would put the same ids in `value` and leave
|
|
542
|
+
* `objectLiteralParameters` undefined. Both are the same invariant. Reading
|
|
543
|
+
* both slots means a change of spelling inside QueryHelper — a file this suite
|
|
544
|
+
* does not own, and whose rendered SQL an earlier version of the test below
|
|
545
|
+
* pinned, to its cost — cannot redden this test while the invariant holds,
|
|
546
|
+
* while anything that changes which ids are bound still does.
|
|
547
|
+
*
|
|
548
|
+
* Only strings are collected: an id is the one thing these operators bind as a
|
|
549
|
+
* string, and a helper that also bound, say, a match count would bind that as
|
|
550
|
+
* a number.
|
|
551
|
+
*/
|
|
552
|
+
function boundIdsOf(operator: FindOperator<unknown>): Array<string> {
|
|
553
|
+
const parameters: Record<string, unknown> =
|
|
554
|
+
(operator.objectLiteralParameters as Record<string, unknown> | undefined) ||
|
|
555
|
+
{};
|
|
556
|
+
|
|
557
|
+
const carried: Array<unknown> = [
|
|
558
|
+
...Object.values(parameters),
|
|
559
|
+
operator.value,
|
|
560
|
+
];
|
|
561
|
+
|
|
562
|
+
const ids: Array<string> = [];
|
|
563
|
+
|
|
564
|
+
for (const entry of carried) {
|
|
565
|
+
for (const value of Array.isArray(entry) ? entry : [entry]) {
|
|
566
|
+
if (typeof value === "string") {
|
|
567
|
+
ids.push(value);
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
return ids.sort();
|
|
573
|
+
}
|
|
574
|
+
|
|
476
575
|
describe("the contract aggregateBy's own comments claim", () => {
|
|
477
576
|
it("applies the permission query through setFindOptions, never by handing the object to .where()", () => {
|
|
478
577
|
/*
|
|
479
578
|
* This is the security-relevant one, and it is invisible from a black-box
|
|
480
579
|
* call because both spellings produce a query that runs.
|
|
481
580
|
*
|
|
482
|
-
*
|
|
483
|
-
*
|
|
484
|
-
* label-
|
|
485
|
-
* `query[accessControlColumn] = { _id:
|
|
581
|
+
* The permission pipeline (run for real below) can hand back a NESTED
|
|
582
|
+
* condition on the access-control relation — for a user whose read grant
|
|
583
|
+
* is label-scoped, the query comes back carrying
|
|
584
|
+
* `query[accessControlColumn] = { _id: <the permitted labels> }`.
|
|
486
585
|
* TypeORM's `.where(object)` renders FLAT conditions only: it has no
|
|
487
586
|
* relation to join the nested object against, so it drops the clause
|
|
488
587
|
* rather than erroring. The aggregate would then count rows the equivalent
|
|
@@ -515,20 +614,150 @@ describe("the contract aggregateBy's own comments claim", () => {
|
|
|
515
614
|
);
|
|
516
615
|
});
|
|
517
616
|
|
|
518
|
-
it("still has a nested access-control condition to protect", () => {
|
|
617
|
+
it("still has a nested access-control condition to protect", async () => {
|
|
519
618
|
/*
|
|
520
|
-
* The assertion above is only worth having while
|
|
521
|
-
* really does
|
|
522
|
-
*
|
|
523
|
-
*
|
|
619
|
+
* The assertion above is only worth having while the permission pipeline
|
|
620
|
+
* really does hand back a condition keyed on the access-control RELATION.
|
|
621
|
+
* If every access-control predicate became a flat clause on a column of
|
|
622
|
+
* the table itself, `.where(object)` would apply it perfectly well, the
|
|
623
|
+
* de-duplicating subquery in buildAggregateScope would have nothing left
|
|
624
|
+
* to de-duplicate, and the comment above would become folklore.
|
|
625
|
+
*
|
|
626
|
+
* This used to be a source-text match on ReadPermission.ts, pinned to the
|
|
627
|
+
* exact expression checkReadBlockPermission wrote. That expression is
|
|
628
|
+
* gone: a label BLOCK is now a flat `_id NOT IN (SELECT ... FROM the join
|
|
629
|
+
* table)` predicate. That was a fix, not a regression — a relation join
|
|
630
|
+
* cannot express "has none of these labels" at all (a device labelled
|
|
631
|
+
* {blocked, other} still matches the join through its "other" row) — and
|
|
632
|
+
* a flat predicate needs no join, so the block half cannot inflate an
|
|
633
|
+
* aggregate either way.
|
|
634
|
+
*
|
|
635
|
+
* So nothing in this file asserts the block half any more, and that is a
|
|
636
|
+
* decision rather than an oversight. There is no aggregate-specific
|
|
637
|
+
* behaviour left to assert about it: with no relation key in the query,
|
|
638
|
+
* queryTouchesARelation says no, buildAggregateScope takes the flat
|
|
639
|
+
* `setFindOptions` branch, the same FindOptions machinery findBy uses
|
|
640
|
+
* applies the predicate, and there is no join to de-duplicate. A reader
|
|
641
|
+
* who holds a block AND a label-scoped allow puts the relation key back
|
|
642
|
+
* in, which is the case this test already covers. The block predicate
|
|
643
|
+
* itself belongs to ReadBlockPermission.test.ts, which owns it, and which
|
|
644
|
+
* has to mock QueryUtil.getManyToManyRelationMetadata to reach it —
|
|
645
|
+
* that helper returns null whenever Postgres is not connected, so a block
|
|
646
|
+
* assertion here would have to bring the mock with it, into a suite whose
|
|
647
|
+
* whole point is that it needs no database.
|
|
648
|
+
*
|
|
649
|
+
* The nested shape did not leave with it. It belongs to the ALLOW half,
|
|
650
|
+
* which is the half that has always produced the join aggregateBy has to
|
|
651
|
+
* survive: AccessControlPermission.addAccessControlIdsToQuery puts the
|
|
652
|
+
* permitted label ids on the access-control column, and
|
|
653
|
+
* QueryUtil.serializeQuery turns an id array on an EntityArray column
|
|
654
|
+
* into `{ _id: <operator> }`.
|
|
655
|
+
*
|
|
656
|
+
* So this asks the pipeline rather than grepping whichever file happens
|
|
657
|
+
* to spell it today: run the same call aggregateBy makes and look at what
|
|
658
|
+
* comes back. It goes red if the label-scoped read stops producing a
|
|
659
|
+
* relation-keyed condition — whether because the allow path moves to a
|
|
660
|
+
* flat `_id` predicate the way the block path did, because serializeQuery
|
|
661
|
+
* stops nesting the id filter under the relation, or because NetworkDevice
|
|
662
|
+
* loses its access-control relation.
|
|
663
|
+
*
|
|
664
|
+
* If that day comes, a red here is NOT permission to delete the
|
|
665
|
+
* setFindOptions requirement above, and it is not permission to quietly
|
|
666
|
+
* rewrite this expectation either. This test watches ONE producer of
|
|
667
|
+
* relation-keyed conditions, and there is at least one more:
|
|
668
|
+
* `@CanAccessIfCanReadOn` makes BasePermission write
|
|
669
|
+
* `query[relation] = { <the related model's access-control column>: ids }`
|
|
670
|
+
* for StatusPageResource, IncidentInternalNote, AlertEpisodeMember,
|
|
671
|
+
* OnCallDutyPolicyExecutionLogTimeline and others. queryTouchesARelation
|
|
672
|
+
* cannot tell that key apart from this one, and `.where(object)` would
|
|
673
|
+
* drop it just as silently — no exception, no log line, an aggregate over
|
|
674
|
+
* rows the caller may not read.
|
|
675
|
+
*
|
|
676
|
+
* What has to be RE-ESTABLISHED before anything here is deleted is
|
|
677
|
+
* therefore the whole claim rather than this one case: that NO read path —
|
|
678
|
+
* label allow, label block, `@CanAccessIfCanReadOn`, owner scoping,
|
|
679
|
+
* whatever has been added since — can still put a relation key in the
|
|
680
|
+
* permitted query. Nobody has shown that. Until somebody does, a red here
|
|
681
|
+
* means this particular case moved, and the test should follow it to
|
|
682
|
+
* wherever the nested condition is produced now.
|
|
683
|
+
*
|
|
684
|
+
* No database is needed: the permission pipeline is pure right up to the
|
|
685
|
+
* point DatabaseService hands the query to TypeORM.
|
|
686
|
+
*/
|
|
687
|
+
const projectId: ObjectID = ObjectID.generate();
|
|
688
|
+
const permittedLabelId: ObjectID = ObjectID.generate();
|
|
689
|
+
const model: NetworkDevice = new NetworkDevice();
|
|
690
|
+
|
|
691
|
+
/*
|
|
692
|
+
* The two preconditions this test rests on, asserted rather than assumed.
|
|
693
|
+
* ProjectMember is the permission the reader below is given; the moment it
|
|
694
|
+
* stops being enough to read this table or the projectId column the query
|
|
695
|
+
* filters on, checkReadQueryPermission throws NotAuthorizedException with
|
|
696
|
+
* a message about permission names and nothing about aggregates. Whoever
|
|
697
|
+
* tightens those lists should learn it from one line here rather than from
|
|
698
|
+
* a confusing exception inside a test named after access-control
|
|
699
|
+
* conditions — and the fix then is to give this reader a permission that
|
|
700
|
+
* IS in both lists, not to change the lists back.
|
|
524
701
|
*/
|
|
525
|
-
|
|
526
|
-
|
|
702
|
+
expect(model.getReadPermissions()).toContain(Permission.ProjectMember);
|
|
703
|
+
expect(model.getColumnAccessControlFor("projectId")?.read || []).toContain(
|
|
704
|
+
Permission.ProjectMember,
|
|
527
705
|
);
|
|
528
706
|
|
|
529
|
-
|
|
530
|
-
|
|
707
|
+
const permitted: CheckReadPermissionType<NetworkDevice> =
|
|
708
|
+
await ModelPermission.checkReadQueryPermission(
|
|
709
|
+
NetworkDevice,
|
|
710
|
+
{ projectId: projectId },
|
|
711
|
+
null,
|
|
712
|
+
labelScopedReaderProps(projectId, permittedLabelId),
|
|
713
|
+
);
|
|
714
|
+
|
|
715
|
+
const accessControlColumn: string | null = model.getAccessControlColumn();
|
|
716
|
+
|
|
717
|
+
expect(accessControlColumn).toBeTruthy();
|
|
718
|
+
|
|
719
|
+
/*
|
|
720
|
+
* The key names a RELATION, not a column of this table. That is the whole
|
|
721
|
+
* reason `.where(object)` cannot apply the condition and FindOptions has
|
|
722
|
+
* to join for it.
|
|
723
|
+
*/
|
|
724
|
+
const columnMetadata: TableColumnMetadata = model.getTableColumnMetadata(
|
|
725
|
+
accessControlColumn as string,
|
|
531
726
|
);
|
|
727
|
+
|
|
728
|
+
expect(columnMetadata.type).toBe(TableColumnType.EntityArray);
|
|
729
|
+
|
|
730
|
+
// And the condition under that key is NESTED: a filter on the RELATED row.
|
|
731
|
+
const accessControlCondition: Record<string, unknown> = (
|
|
732
|
+
permitted.query as any
|
|
733
|
+
)[accessControlColumn as string];
|
|
734
|
+
|
|
735
|
+
/*
|
|
736
|
+
* `toBeTruthy`, not `toBeDefined`: the latter passes on null, and a null
|
|
737
|
+
* under that key is one of the ways this could break. The `typeof` check
|
|
738
|
+
* is the load-bearing half — a plain id string here instead of an object
|
|
739
|
+
* would BE the "it went flat" regression this test exists to catch, and
|
|
740
|
+
* a truthiness check alone would sail past it.
|
|
741
|
+
*/
|
|
742
|
+
expect(accessControlCondition).toBeTruthy();
|
|
743
|
+
expect(typeof accessControlCondition).toBe("object");
|
|
744
|
+
|
|
745
|
+
const nestedIdFilter: unknown = accessControlCondition["_id"];
|
|
746
|
+
|
|
747
|
+
expect(nestedIdFilter).toBeInstanceOf(FindOperator);
|
|
748
|
+
|
|
749
|
+
/*
|
|
750
|
+
* The identity above and the payload below, and deliberately nothing in
|
|
751
|
+
* between. An earlier version of this assertion also read
|
|
752
|
+
* `getSql("probe")` and pinned the substring `probe IN (`, which is
|
|
753
|
+
* QueryHelper.in's chosen spelling — in a file this suite does not own.
|
|
754
|
+
* Rendering the same set membership as `= ANY(:x)` would have reddened
|
|
755
|
+
* this test with the invariant perfectly intact, which is precisely the
|
|
756
|
+
* kind of coupling that made this test red once already.
|
|
757
|
+
*/
|
|
758
|
+
expect(boundIdsOf(nestedIdFilter as FindOperator<unknown>)).toEqual([
|
|
759
|
+
permittedLabelId.toString(),
|
|
760
|
+
]);
|
|
532
761
|
});
|
|
533
762
|
|
|
534
763
|
it("runs the query through ModelPermission.checkReadQueryPermission, like findBy and countBy", () => {
|
|
@@ -543,24 +772,30 @@ describe("the contract aggregateBy's own comments claim", () => {
|
|
|
543
772
|
expect(AGGREGATE_BY_BODY).toContain("checkReadPermissionType.query");
|
|
544
773
|
});
|
|
545
774
|
|
|
546
|
-
it("validates orderBy expressions
|
|
775
|
+
it("validates orderBy expressions above the permission call, which is what makes them testable", () => {
|
|
547
776
|
/*
|
|
548
|
-
* The
|
|
549
|
-
*
|
|
550
|
-
*
|
|
551
|
-
*
|
|
552
|
-
*
|
|
777
|
+
* The black-box test in the statement-separator block calls aggregateBy
|
|
778
|
+
* with a semicolon in an orderBy expression and expects BadDataException.
|
|
779
|
+
* It only reaches that check because the orderBy validation loop sits
|
|
780
|
+
* ABOVE checkReadQueryPermission and above buildAggregateScope, which is
|
|
781
|
+
* what keeps this suite database-free. Move the loop back down beside
|
|
782
|
+
* addOrderBy and that test starts failing on a missing connection instead
|
|
783
|
+
* — a confusing failure about jest rather than about the guard — so the
|
|
784
|
+
* position is pinned here, where the message is about the position.
|
|
553
785
|
*/
|
|
554
|
-
const
|
|
555
|
-
"aggregateBy.orderBy",
|
|
556
|
-
);
|
|
557
|
-
const orderByLoopEnd: number = AGGREGATE_BY_BODY.indexOf("addOrderBy");
|
|
558
|
-
|
|
559
|
-
expect(orderByLoopStart).toBeGreaterThan(-1);
|
|
560
|
-
expect(orderByLoopEnd).toBeGreaterThan(orderByLoopStart);
|
|
561
|
-
expect(AGGREGATE_BY_BODY.slice(orderByLoopStart, orderByLoopEnd)).toContain(
|
|
786
|
+
const orderByValidationAt: number = AGGREGATE_BY_BODY.indexOf(
|
|
562
787
|
"assertSafeAggregateExpression(order.expression)",
|
|
563
788
|
);
|
|
789
|
+
const permissionAt: number = AGGREGATE_BY_BODY.indexOf(
|
|
790
|
+
"checkReadQueryPermission",
|
|
791
|
+
);
|
|
792
|
+
const applyLoopAt: number = AGGREGATE_BY_BODY.indexOf("addOrderBy");
|
|
793
|
+
|
|
794
|
+
expect(orderByValidationAt).toBeGreaterThan(-1);
|
|
795
|
+
expect(permissionAt).toBeGreaterThan(-1);
|
|
796
|
+
expect(applyLoopAt).toBeGreaterThan(-1);
|
|
797
|
+
expect(orderByValidationAt).toBeLessThan(permissionAt);
|
|
798
|
+
expect(applyLoopAt).toBeGreaterThan(permissionAt);
|
|
564
799
|
});
|
|
565
800
|
|
|
566
801
|
it("validates aliases and expressions before it touches permissions or the database", () => {
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import DatabaseService from "../../../Server/Services/DatabaseService";
|
|
2
|
+
import InventoryItem from "../../../Models/DatabaseModels/InventoryItem";
|
|
3
|
+
import SortOrder from "../../../Types/BaseDatabase/SortOrder";
|
|
4
|
+
import { afterEach, beforeEach, describe, expect, it } from "@jest/globals";
|
|
5
|
+
|
|
6
|
+
/*
|
|
7
|
+
* Every paged read here is LIMIT/OFFSET, and OFFSET only means anything
|
|
8
|
+
* against an ordering with no ties. Rows that compare equal may come back in
|
|
9
|
+
* any order, and nothing obliges Postgres to pick the same order for the query
|
|
10
|
+
* that fetches page 1 and the query that fetches page 2 — so a tied row can be
|
|
11
|
+
* served on both pages while another is served on neither. The user sees one
|
|
12
|
+
* item listed twice.
|
|
13
|
+
*
|
|
14
|
+
* The Inventory list is where this bites hardest, and it is why it belongs in
|
|
15
|
+
* the duplicate-items fix: it sorts on `lastSeenAt`, which the entity
|
|
16
|
+
* reconciler rewrites for every live entity every few minutes, so the sort key
|
|
17
|
+
* is being mutated between the two fetches by design rather than by bad luck.
|
|
18
|
+
* The inventory mirror also stamps one identical `lastSeenAt` across a whole
|
|
19
|
+
* page of rows at a time, which produces large exact-tie groups outright.
|
|
20
|
+
*
|
|
21
|
+
* The fix is to complete whatever sort the caller asked for into a total order
|
|
22
|
+
* by appending the primary key, in the one place every paged read goes
|
|
23
|
+
* through.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
type FindArgs = {
|
|
27
|
+
order?: Record<string, unknown> | undefined;
|
|
28
|
+
select?: Record<string, unknown> | undefined;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
describe("DatabaseService._findBy completes a partial sort into a total order", () => {
|
|
32
|
+
let service: DatabaseService<InventoryItem>;
|
|
33
|
+
let find: jest.Mock;
|
|
34
|
+
|
|
35
|
+
type FindByFunction = (
|
|
36
|
+
sort?: Record<string, unknown> | undefined,
|
|
37
|
+
) => Promise<Array<InventoryItem>>;
|
|
38
|
+
|
|
39
|
+
const findBy: FindByFunction = (
|
|
40
|
+
sort?: Record<string, unknown> | undefined,
|
|
41
|
+
): Promise<Array<InventoryItem>> => {
|
|
42
|
+
return service.findBy({
|
|
43
|
+
query: {},
|
|
44
|
+
limit: 10,
|
|
45
|
+
skip: 0,
|
|
46
|
+
select: { displayName: true } as any,
|
|
47
|
+
...(sort ? { sort: sort as any } : {}),
|
|
48
|
+
props: { isRoot: true },
|
|
49
|
+
});
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const orderPassedToDatabase: () => Record<string, unknown> = (): Record<
|
|
53
|
+
string,
|
|
54
|
+
unknown
|
|
55
|
+
> => {
|
|
56
|
+
return (find.mock.calls[0]![0] as FindArgs).order || {};
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
beforeEach(() => {
|
|
60
|
+
jest.restoreAllMocks();
|
|
61
|
+
|
|
62
|
+
service = new DatabaseService<InventoryItem>(InventoryItem);
|
|
63
|
+
|
|
64
|
+
find = jest
|
|
65
|
+
.fn()
|
|
66
|
+
.mockImplementation(async (): Promise<Array<InventoryItem>> => {
|
|
67
|
+
return [];
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
jest.spyOn(service, "getRepository").mockReturnValue({ find: find } as any);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
afterEach(() => {
|
|
74
|
+
jest.restoreAllMocks();
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it("appends the primary key to a single-column sort", async () => {
|
|
78
|
+
await findBy({ lastSeenAt: SortOrder.Descending });
|
|
79
|
+
|
|
80
|
+
expect(orderPassedToDatabase()).toEqual({
|
|
81
|
+
lastSeenAt: SortOrder.Descending,
|
|
82
|
+
_id: SortOrder.Ascending,
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it("keeps the caller's column first so the requested ordering is unchanged", async () => {
|
|
87
|
+
await findBy({ lastSeenAt: SortOrder.Descending });
|
|
88
|
+
|
|
89
|
+
expect(Object.keys(orderPassedToDatabase())).toEqual(["lastSeenAt", "_id"]);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it("appends the primary key to a multi-column sort", async () => {
|
|
93
|
+
await findBy({
|
|
94
|
+
entityType: SortOrder.Ascending,
|
|
95
|
+
lastSeenAt: SortOrder.Descending,
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
expect(Object.keys(orderPassedToDatabase())).toEqual([
|
|
99
|
+
"entityType",
|
|
100
|
+
"lastSeenAt",
|
|
101
|
+
"_id",
|
|
102
|
+
]);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it("appends the primary key to the default sort as well", async () => {
|
|
106
|
+
await findBy(undefined);
|
|
107
|
+
|
|
108
|
+
expect(orderPassedToDatabase()).toEqual({
|
|
109
|
+
createdAt: SortOrder.Descending,
|
|
110
|
+
_id: SortOrder.Ascending,
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it("leaves an explicit primary-key sort alone", async () => {
|
|
115
|
+
await findBy({ _id: SortOrder.Descending });
|
|
116
|
+
|
|
117
|
+
expect(orderPassedToDatabase()).toEqual({ _id: SortOrder.Descending });
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it("does not add the primary key twice when it is already the tiebreaker", async () => {
|
|
121
|
+
await findBy({
|
|
122
|
+
lastSeenAt: SortOrder.Descending,
|
|
123
|
+
_id: SortOrder.Descending,
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
expect(orderPassedToDatabase()).toEqual({
|
|
127
|
+
lastSeenAt: SortOrder.Descending,
|
|
128
|
+
_id: SortOrder.Descending,
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it("does not widen the projection — the primary key is already selected", async () => {
|
|
133
|
+
await findBy({ lastSeenAt: SortOrder.Descending });
|
|
134
|
+
|
|
135
|
+
const select: Record<string, unknown> = (find.mock.calls[0]![0] as FindArgs)
|
|
136
|
+
.select as Record<string, unknown>;
|
|
137
|
+
|
|
138
|
+
expect(select["_id"]).toBe(true);
|
|
139
|
+
expect(select["displayName"]).toBe(true);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it("does not mutate the caller's own sort object", async () => {
|
|
143
|
+
const callerSort: Record<string, unknown> = {
|
|
144
|
+
lastSeenAt: SortOrder.Descending,
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
await findBy(callerSort);
|
|
148
|
+
|
|
149
|
+
expect(callerSort).toEqual({ lastSeenAt: SortOrder.Descending });
|
|
150
|
+
});
|
|
151
|
+
});
|
|
@@ -246,3 +246,108 @@ describe("machine creates are left alone", () => {
|
|
|
246
246
|
expect(createBy.data.source).toBe(EntitySource.Inventory);
|
|
247
247
|
});
|
|
248
248
|
});
|
|
249
|
+
|
|
250
|
+
/*
|
|
251
|
+
* The escape hatch above is for the two machine writers, which reach the
|
|
252
|
+
* service directly with `props: { isRoot: true }`. It used to be keyed on the
|
|
253
|
+
* PAYLOAD instead — "did the caller send an entityKey" — which let anyone
|
|
254
|
+
* posting to the public CRUD endpoint skip every check in the hook simply by
|
|
255
|
+
* inventing one, and claim a source only ingest is supposed to write. A row
|
|
256
|
+
* that says `discovered` is a row the prune sweep is entitled to delete on a
|
|
257
|
+
* TTL, so this was not only a lie in the Source column.
|
|
258
|
+
*/
|
|
259
|
+
describe("non-root creates cannot impersonate a machine writer", () => {
|
|
260
|
+
function apiCreate(data: InventoryItem): CreateBy<InventoryItem> {
|
|
261
|
+
return { data, props: {} } as CreateBy<InventoryItem>;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function externalService(): InventoryItem {
|
|
265
|
+
const data: InventoryItem = new InventoryItem();
|
|
266
|
+
data.projectId = PROJECT_ID;
|
|
267
|
+
data.entityType = EntityType.ExternalService;
|
|
268
|
+
data.displayName = "Stripe Payments API";
|
|
269
|
+
return data;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
test("a client-supplied entity key is discarded and the key re-derived", async () => {
|
|
273
|
+
const data: InventoryItem = externalService();
|
|
274
|
+
data.entityKey = "deadbeefdeadbeef";
|
|
275
|
+
|
|
276
|
+
const createBy: CreateBy<InventoryItem> = apiCreate(data);
|
|
277
|
+
await callOnBeforeCreate(createBy);
|
|
278
|
+
|
|
279
|
+
expect(createBy.data.entityKey).toBe(
|
|
280
|
+
keyForManualEntity(
|
|
281
|
+
PROJECT_ID.toString(),
|
|
282
|
+
EntityType.ExternalService,
|
|
283
|
+
"Stripe Payments API",
|
|
284
|
+
),
|
|
285
|
+
);
|
|
286
|
+
expect(createBy.data.source).toBe(EntitySource.Manual);
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
test("a client-supplied source of discovered is overridden to manual", async () => {
|
|
290
|
+
const data: InventoryItem = externalService();
|
|
291
|
+
data.entityKey = "deadbeefdeadbeef";
|
|
292
|
+
data.source = EntitySource.Discovered;
|
|
293
|
+
|
|
294
|
+
const createBy: CreateBy<InventoryItem> = apiCreate(data);
|
|
295
|
+
await callOnBeforeCreate(createBy);
|
|
296
|
+
|
|
297
|
+
expect(createBy.data.source).toBe(EntitySource.Manual);
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
test("a client-supplied source of inventory is overridden to manual", async () => {
|
|
301
|
+
const data: InventoryItem = externalService();
|
|
302
|
+
data.entityKey = "deadbeefdeadbeef";
|
|
303
|
+
data.source = EntitySource.Inventory;
|
|
304
|
+
|
|
305
|
+
const createBy: CreateBy<InventoryItem> = apiCreate(data);
|
|
306
|
+
await callOnBeforeCreate(createBy);
|
|
307
|
+
|
|
308
|
+
expect(createBy.data.source).toBe(EntitySource.Manual);
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
test.each([
|
|
312
|
+
EntityType.Service,
|
|
313
|
+
EntityType.Host,
|
|
314
|
+
EntityType.KubernetesPod,
|
|
315
|
+
EntityType.KubernetesNode,
|
|
316
|
+
EntityType.NetworkDevice,
|
|
317
|
+
])(
|
|
318
|
+
"the manual-type restriction still applies to %s when a key is supplied",
|
|
319
|
+
async (entityType: EntityType) => {
|
|
320
|
+
const data: InventoryItem = externalService();
|
|
321
|
+
data.entityType = entityType;
|
|
322
|
+
data.entityKey = "deadbeefdeadbeef";
|
|
323
|
+
data.source = EntitySource.Discovered;
|
|
324
|
+
|
|
325
|
+
await expect(callOnBeforeCreate(apiCreate(data))).rejects.toThrow(
|
|
326
|
+
/discovered automatically and cannot be created manually/,
|
|
327
|
+
);
|
|
328
|
+
},
|
|
329
|
+
);
|
|
330
|
+
|
|
331
|
+
test("the display-name requirement still applies when a key is supplied", async () => {
|
|
332
|
+
const data: InventoryItem = externalService();
|
|
333
|
+
data.displayName = " ";
|
|
334
|
+
data.entityKey = "deadbeefdeadbeef";
|
|
335
|
+
data.source = EntitySource.Discovered;
|
|
336
|
+
|
|
337
|
+
await expect(callOnBeforeCreate(apiCreate(data))).rejects.toThrow(
|
|
338
|
+
/Name is required/,
|
|
339
|
+
);
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
test("firstSeenAt and lastSeenAt are stamped rather than left to the caller", async () => {
|
|
343
|
+
const data: InventoryItem = externalService();
|
|
344
|
+
data.entityKey = "deadbeefdeadbeef";
|
|
345
|
+
data.source = EntitySource.Discovered;
|
|
346
|
+
|
|
347
|
+
const createBy: CreateBy<InventoryItem> = apiCreate(data);
|
|
348
|
+
await callOnBeforeCreate(createBy);
|
|
349
|
+
|
|
350
|
+
expect(createBy.data.firstSeenAt).toBeInstanceOf(Date);
|
|
351
|
+
expect(createBy.data.lastSeenAt).toBeInstanceOf(Date);
|
|
352
|
+
});
|
|
353
|
+
});
|