@oneuptime/common 12.0.15 → 12.0.17
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/AnalyticsModels/Index.ts +10 -0
- package/Models/AnalyticsModels/LogCountBaseline.ts +174 -0
- package/Models/AnalyticsModels/SecurityEvent.ts +794 -0
- package/Models/AnalyticsModels/Span.ts +29 -1
- package/Models/AnalyticsModels/SpanCountBaseline.ts +198 -0
- package/Models/DatabaseModels/Alert.ts +42 -0
- package/Models/DatabaseModels/AlertMeasurement.ts +1114 -0
- package/Models/DatabaseModels/AlertMeasurementValue.ts +501 -0
- package/Models/DatabaseModels/AlertStateTimeline.ts +10 -2
- package/Models/DatabaseModels/BillingPaymentMethod.ts +8 -10
- package/Models/DatabaseModels/DetectionRule.ts +568 -0
- package/Models/DatabaseModels/GoogleSecOpsConnection.ts +382 -0
- package/Models/DatabaseModels/Incident.ts +42 -0
- package/Models/DatabaseModels/IncidentMeasurement.ts +1114 -0
- package/Models/DatabaseModels/IncidentMeasurementValue.ts +501 -0
- package/Models/DatabaseModels/IncidentStateTimeline.ts +10 -2
- package/Models/DatabaseModels/IncomingCallPolicyLabelRule.ts +36 -0
- package/Models/DatabaseModels/IncomingCallPolicyOwnerRule.ts +42 -0
- package/Models/DatabaseModels/Index.ts +20 -0
- package/Models/DatabaseModels/Label.ts +11 -0
- package/Models/DatabaseModels/LlmCostBudget.ts +655 -0
- package/Models/DatabaseModels/LlmModelPrice.ts +481 -0
- package/Models/DatabaseModels/LogSavedView.ts +87 -0
- package/Models/DatabaseModels/MarketingConversion.ts +210 -8
- package/Models/DatabaseModels/MetricSavedView.ts +59 -0
- package/Models/DatabaseModels/OnCallDutyPolicyLabelRule.ts +36 -0
- package/Models/DatabaseModels/OnCallDutyPolicyOwnerRule.ts +42 -0
- package/Models/DatabaseModels/OnCallDutyPolicyScheduleLabelRule.ts +36 -0
- package/Models/DatabaseModels/OnCallDutyPolicyScheduleOwnerRule.ts +42 -0
- package/Models/DatabaseModels/Probe.ts +21 -0
- package/Models/DatabaseModels/RunbookLabelRule.ts +36 -0
- package/Models/DatabaseModels/RunbookOwnerRule.ts +42 -0
- package/Models/DatabaseModels/ScheduledMaintenanceMeasurement.ts +1104 -0
- package/Models/DatabaseModels/ScheduledMaintenanceMeasurementValue.ts +491 -0
- package/Models/DatabaseModels/ScheduledMaintenanceStateTimeline.ts +10 -2
- package/Models/DatabaseModels/StatusPageLabelRule.ts +36 -0
- package/Models/DatabaseModels/StatusPageOwnerRule.ts +42 -0
- package/Models/DatabaseModels/TableView.ts +15 -0
- package/Models/DatabaseModels/Team.ts +13 -0
- package/Models/DatabaseModels/TeamMember.ts +8 -0
- package/Models/DatabaseModels/TraceSavedView.ts +52 -0
- package/Models/DatabaseModels/WorkflowLabelRule.ts +36 -0
- package/Models/DatabaseModels/WorkflowOwnerRule.ts +42 -0
- package/Server/API/TelemetryAPI.ts +252 -6
- package/Server/API/UserAPI.ts +88 -0
- package/Server/EnvironmentConfig.ts +36 -1
- package/Server/Infrastructure/Postgres/SchemaMigrations/1788000000000-AddDetectionRuleAndGoogleSecOpsConnection.ts +88 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1788100000000-AddMeasurements.ts +457 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1788200000000-AddLlmCostBudget.ts +97 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1788300000000-AddLlmModelPrice.ts +45 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1788400000000-AddMarketingConversionAttribution.ts +105 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1788500000000-RemoveLlmCostBudgetAlertColumns.ts +77 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1788600000000-AddDetectionRuleIncidentColumns.ts +39 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +14 -0
- package/Server/Middleware/IdentityRateLimit.ts +672 -0
- package/Server/Services/AccessTokenService.ts +15 -2
- package/Server/Services/AlertMeasurementService.ts +302 -0
- package/Server/Services/AlertMeasurementValueService.ts +508 -0
- package/Server/Services/AlertService.ts +50 -0
- package/Server/Services/AlertStateTimelineService.ts +54 -0
- package/Server/Services/DetectionRuleService.ts +74 -0
- package/Server/Services/GoogleSecOpsConnectionService.ts +89 -0
- package/Server/Services/IncidentMeasurementService.ts +304 -0
- package/Server/Services/IncidentMeasurementValueService.ts +523 -0
- package/Server/Services/IncidentService.ts +51 -0
- package/Server/Services/IncidentStateTimelineService.ts +54 -0
- package/Server/Services/Index.ts +30 -0
- package/Server/Services/LlmCostBudgetService.ts +93 -0
- package/Server/Services/LlmModelPriceService.ts +202 -0
- package/Server/Services/LogAggregationService.ts +1106 -2
- package/Server/Services/LogCountBaselineService.ts +163 -0
- package/Server/Services/MutableMetricService.ts +30 -9
- package/Server/Services/ScheduledMaintenanceMeasurementService.ts +311 -0
- package/Server/Services/ScheduledMaintenanceMeasurementValueService.ts +546 -0
- package/Server/Services/ScheduledMaintenanceService.ts +53 -0
- package/Server/Services/ScheduledMaintenanceStateTimelineService.ts +188 -1
- package/Server/Services/SecurityEventService.ts +104 -0
- package/Server/Services/SpanCountBaselineService.ts +173 -0
- package/Server/Services/StatusPageService.ts +4 -3
- package/Server/Services/TelemetryUsageBillingService.ts +17 -2
- package/Server/Services/UserService.ts +284 -51
- package/Server/Services/UserTotpAuthService.ts +27 -86
- package/Server/Services/UserWebAuthnService.ts +26 -86
- package/Server/Types/Billing/MeteredPlan/AllMeteredPlans.ts +14 -0
- package/Server/Types/Database/Permissions/TenantPermission.ts +10 -0
- package/Server/Types/Database/QueryHelper.ts +1 -1
- package/Server/Utils/AI/Chat/ObservabilityChatPrompt.ts +1 -1
- package/Server/Utils/AI/Toolbox/Index.ts +6 -0
- package/Server/Utils/AI/Toolbox/SecurityEventTools.ts +349 -0
- package/Server/Utils/AnalyticsDatabase/StatementGenerator.ts +13 -4
- package/Server/Utils/Attribution.ts +130 -34
- package/Server/Utils/Marketing/ConversionUploadProvider.ts +98 -16
- package/Server/Utils/Marketing/Providers/GoogleAds.ts +70 -7
- package/Server/Utils/Marketing/Providers/LinkedIn.ts +54 -16
- package/Server/Utils/Marketing/Providers/Meta.ts +52 -18
- package/Server/Utils/Marketing/Providers/MicrosoftAds.ts +37 -10
- package/Server/Utils/Marketing/Providers/Reddit.ts +45 -15
- package/Server/Utils/Measurement/MeasurementDefinitionValidator.ts +209 -0
- package/Server/Utils/Measurement/MeasurementMetricWriter.ts +189 -0
- package/Server/Utils/Monitor/Criteria/CountAnomaly.ts +182 -0
- package/Server/Utils/Monitor/Criteria/LogMonitorCriteria.ts +132 -0
- package/Server/Utils/Monitor/Criteria/SecurityEventsMonitorCriteria.ts +35 -0
- package/Server/Utils/Monitor/Criteria/TraceMonitorCriteria.ts +132 -0
- package/Server/Utils/Monitor/DataToProcess.ts +2 -0
- package/Server/Utils/Monitor/MonitorCriteriaDataExtractor.ts +14 -0
- package/Server/Utils/Monitor/MonitorCriteriaEvaluator.ts +15 -0
- package/Server/Utils/Monitor/MonitorCriteriaObservationBuilder.ts +20 -0
- package/Server/Utils/Monitor/MonitorResource.ts +16 -0
- package/Server/Utils/Monitor/MonitorStepResourceIdentity.ts +1 -0
- package/Server/Utils/Monitor/MonitorStepsReferenceExtractor.ts +1 -0
- package/Server/Utils/SecurityEvent/DetectionRuleEvaluator.ts +805 -0
- package/Server/Utils/SecurityEvent/GoogleSecOps/GoogleSecOpsClient.ts +262 -0
- package/Server/Utils/SecurityEvent/GoogleSecOps/GoogleSecOpsPoller.ts +212 -0
- package/Server/Utils/SecurityEvent/SecurityEventRow.ts +101 -0
- package/Server/Utils/SecurityEvent/Sigma/SigmaClickhouseCompiler.ts +635 -0
- package/Server/Utils/Telemetry/LlmCostBudgetEvaluator.ts +379 -0
- package/Server/Utils/Telemetry/LlmSpan.ts +83 -4
- package/Server/Utils/Telemetry/LogErrorPatternSql.ts +136 -0
- package/Server/Utils/UserPermission/UserPermission.ts +57 -1
- package/Tests/App/Dashboard/AlertsSideMenu.test.tsx +4 -0
- package/Tests/App/Dashboard/CreatePageEntryPointPermissions.test.tsx +306 -0
- package/Tests/App/Dashboard/DangerZone.test.tsx +11 -0
- package/Tests/App/Dashboard/IncidentsSideMenu.test.tsx +4 -0
- package/Tests/App/Dashboard/LogMonitorAttributeOperatorPreview.test.tsx +459 -0
- package/Tests/App/Dashboard/OverviewCustomFields.test.tsx +24 -0
- package/Tests/App/Dashboard/ScheduledMaintenanceSideMenu.test.tsx +6 -0
- package/Tests/App/Dashboard/SecurityEventsDetectionRulesPage.test.tsx +250 -0
- package/Tests/App/Dashboard/SecurityEventsMonitorStepForm.test.tsx +106 -0
- package/Tests/App/Dashboard/SecurityEventsMonitorsPage.test.tsx +185 -0
- package/Tests/App/Dashboard/SecurityEventsSetupGuide.test.tsx +200 -0
- package/Tests/Models/DatabaseModels/DomainRoleTierCoverage.test.ts +323 -0
- package/Tests/Models/DatabaseModels/ProjectSharedResources.test.ts +386 -0
- package/Tests/Models/DetectionRuleCreateContract.test.ts +35 -0
- package/Tests/Server/API/LogErrorPatternAPI.test.ts +811 -0
- package/Tests/Server/API/UserAuthenticationAPI.test.ts +11 -0
- package/Tests/Server/API/UserProjectsAPI.test.ts +11 -2
- package/Tests/Server/API/UserTwoFactorAuthAdminAPI.test.ts +918 -0
- package/Tests/Server/Infrastructure/LocalCache.test.ts +149 -0
- package/Tests/Server/Services/AccessTokenProjectUserGrant.test.ts +249 -0
- package/Tests/Server/Services/AlertMeasurementService.test.ts +665 -0
- package/Tests/Server/Services/AlertMeasurementValueService.test.ts +389 -0
- package/Tests/Server/Services/DetectionRuleService.test.ts +254 -0
- package/Tests/Server/Services/IncidentMeasurementService.test.ts +394 -0
- package/Tests/Server/Services/IncidentMeasurementValueService.test.ts +816 -0
- package/Tests/Server/Services/LlmCostBudgetService.test.ts +104 -0
- package/Tests/Server/Services/LogAggregationAttributeOperators.test.ts +405 -0
- package/Tests/Server/Services/LogErrorPatternAggregation.test.ts +804 -0
- package/Tests/Server/Services/MutableMetricNameRouting.test.ts +83 -0
- package/Tests/Server/Services/ScheduledMaintenanceMeasurementService.test.ts +750 -0
- package/Tests/Server/Services/ScheduledMaintenanceMeasurementValueService.test.ts +478 -0
- package/Tests/Server/Services/UserAuthenticationService.test.ts +216 -4
- package/Tests/Server/Services/UserTwoFactorAuthAdmin.test.ts +1190 -0
- package/Tests/Server/Types/Database/Permissions/ScopedRoleSharedResourceAccess.test.ts +466 -0
- package/Tests/Server/Types/Database/QueryHelperOperators.test.ts +265 -0
- package/Tests/Server/Types/Database/QueryHelperTextAndStructuralOperators.test.ts +206 -0
- package/Tests/Server/Utils/AnalyticsDatabase/ClusterAwareSchema.test.ts +6 -0
- package/Tests/Server/Utils/AnalyticsDatabase/FractionalAttributeThreshold.test.ts +224 -0
- package/Tests/Server/Utils/AnalyticsDatabase/StatementGenerator.test.ts +172 -2
- package/Tests/Server/Utils/Attribution.test.ts +228 -1
- package/Tests/Server/Utils/Marketing/AdUploadableConversionTypes.test.ts +153 -92
- package/Tests/Server/Utils/Marketing/ConversionUploadProvider.test.ts +123 -6
- package/Tests/Server/Utils/Marketing/GoogleAds.test.ts +253 -1
- package/Tests/Server/Utils/Marketing/LinkedIn.test.ts +282 -0
- package/Tests/Server/Utils/Marketing/Meta.test.ts +304 -0
- package/Tests/Server/Utils/Marketing/MicrosoftAds.test.ts +263 -0
- package/Tests/Server/Utils/Marketing/Reddit.test.ts +259 -0
- package/Tests/Server/Utils/Measurement/MeasurementDefinitionValidator.test.ts +322 -0
- package/Tests/Server/Utils/Measurement/MeasurementMetricWriter.test.ts +373 -0
- package/Tests/Server/Utils/Monitor/Criteria/CountAnomaly.test.ts +221 -0
- package/Tests/Server/Utils/Monitor/Criteria/LogMonitorCriteria.test.ts +236 -0
- package/Tests/Server/Utils/Monitor/Criteria/SecurityEventsMonitorCriteria.test.ts +147 -0
- package/Tests/Server/Utils/Monitor/Criteria/TraceMonitorCriteria.test.ts +274 -0
- package/Tests/Server/Utils/Monitor/SecurityEventsMonitorDataExtractor.test.ts +93 -0
- package/Tests/Server/Utils/SecurityEvent/DetectionRuleEvaluator.test.ts +1098 -0
- package/Tests/Server/Utils/SecurityEvent/GoogleSecOpsClient.test.ts +308 -0
- package/Tests/Server/Utils/SecurityEvent/GoogleSecOpsPoller.test.ts +271 -0
- package/Tests/Server/Utils/SecurityEvent/SecurityEventRow.test.ts +272 -0
- package/Tests/Server/Utils/SecurityEvent/SigmaClickhouseCompiler.test.ts +511 -0
- package/Tests/Server/Utils/Telemetry/LlmCostBudgetEvaluator.test.ts +497 -0
- package/Tests/Server/Utils/Telemetry/LlmSpan.test.ts +380 -0
- package/Tests/Server/Utils/Telemetry/LogErrorPatternSql.test.ts +207 -0
- package/Tests/Server/Utils/UserPermission/ProjectUserGrant.test.ts +293 -0
- package/Tests/Types/Monitor/MonitorStepInfraMonitorUtils.test.ts +194 -0
- package/Tests/Types/Monitor/MonitorStepSecurityEventsMonitor.test.ts +201 -0
- package/Tests/Types/SecurityEvent/OcsfEventClass.test.ts +105 -0
- package/Tests/Types/SecurityEvent/OcsfSeverity.test.ts +111 -0
- package/Tests/Types/Telemetry/LlmCostCatalog.test.ts +516 -0
- package/Tests/UI/Components/ActiveFilterChipsOperatorValues.test.tsx +222 -0
- package/Tests/UI/Components/BulkActionPermissionGating.test.tsx +296 -0
- package/Tests/UI/Components/CardModelDetailPermissionGating.test.tsx +305 -0
- package/Tests/UI/Components/CustomFields/CustomFieldsDetail.test.tsx +23 -0
- package/Tests/UI/Components/DictionaryFilterOperatorDisplay.test.ts +239 -0
- package/Tests/UI/Components/DisabledButtonTooltip.test.tsx +267 -0
- package/Tests/UI/Components/DuplicateModel.test.tsx +37 -0
- package/Tests/UI/Components/ModelCardPermissionGating.test.tsx +348 -0
- package/Tests/UI/Components/ModelDelete.test.tsx +25 -1
- package/Tests/UI/Components/ModelFormCreatePermission.test.tsx +342 -0
- package/Tests/UI/Components/ModelTable/BaseModelTablePermissionGating.test.tsx +680 -0
- package/Tests/UI/Components/OrderedStatesListCreateGate.test.tsx +174 -0
- package/Tests/UI/Components/SavedViewsDropdownSearch.test.tsx +1127 -0
- package/Tests/UI/Components/SavedViewsFacetSection.test.tsx +958 -0
- package/Tests/UI/Components/SavedViewsList.test.ts +699 -0
- package/Tests/UI/Components/SavedViewsSidebarIntegration.test.tsx +640 -0
- package/Tests/UI/Components/SavedViewsSurfaceParity.test.tsx +679 -0
- package/Tests/UI/Components/TableViewLoadFailure.test.tsx +248 -0
- package/Tests/UI/Types/UseComponentOutsideClick.test.tsx +226 -0
- package/Tests/UI/Utils/PermissionGate.test.ts +467 -0
- package/Tests/Utils/Measurement/MeasurementEvaluator.test.ts +639 -0
- package/Tests/Utils/Monitor/MonitorSummarySnapshotUtil.test.ts +1 -0
- package/Tests/Utils/Monitor/NetworkDeviceLinkRuleUtil.test.ts +285 -0
- package/Tests/Utils/NetworkDevice/DeviceHealthStateUtil.test.ts +433 -0
- package/Tests/Utils/SecurityEvent/GenericNormalizer.test.ts +204 -0
- package/Tests/Utils/SecurityEvent/GoogleSecOpsAlertNormalizer.test.ts +252 -0
- package/Tests/Utils/SecurityEvent/NormalizerHelpers.test.ts +274 -0
- package/Tests/Utils/SecurityEvent/OcsfNormalizer.test.ts +208 -0
- package/Tests/Utils/SecurityEvent/SecurityEventNormalizer.test.ts +172 -0
- package/Tests/Utils/SecurityEvent/SigmaRuleParser.test.ts +471 -0
- package/Tests/Utils/SecurityEvent/UdmNormalizer.test.ts +361 -0
- package/Tests/Utils/Telemetry/CrossSignalScope.test.ts +82 -0
- package/Tests/Utils/Telemetry/LogErrorPattern.test.ts +463 -0
- package/Types/AI/AIChatTypes.ts +1 -0
- package/Types/Alerts/AlertMeasurementAnchorType.ts +14 -0
- package/Types/Alerts/AlertStateRole.ts +11 -0
- package/Types/AnalyticsDatabase/AnalyticsTableName.ts +16 -0
- package/Types/Dashboard/Chart/ChartType.ts +1 -0
- package/Types/Dashboard/DashboardComponentType.ts +2 -0
- package/Types/Dashboard/DashboardComponents/DashboardSecurityEventsFlowComponent.ts +18 -0
- package/Types/Dashboard/DashboardComponents/DashboardSecurityEventsListComponent.ts +16 -0
- package/Types/Incident/IncidentMeasurementAnchorType.ts +34 -0
- package/Types/Incident/IncidentStateRole.ts +14 -0
- package/Types/Marketing/Attribution.ts +75 -0
- package/Types/Marketing/MarketingConversion.ts +37 -7
- package/Types/Measurement/MeasurementAggregationType.ts +43 -0
- package/Types/Measurement/MeasurementAnchorKind.ts +23 -0
- package/Types/Measurement/MeasurementOccurrence.ts +14 -0
- package/Types/Measurement/MeasurementStatus.ts +34 -0
- package/Types/MeteredPlan/ProductType.ts +1 -0
- package/Types/Monitor/CriteriaFilter.ts +3 -0
- package/Types/Monitor/MonitorCriteriaInstance.ts +64 -0
- package/Types/Monitor/MonitorStep.ts +28 -0
- package/Types/Monitor/MonitorStepSecurityEventsMonitor.ts +100 -0
- package/Types/Monitor/MonitorType.ts +20 -0
- package/Types/Monitor/SecurityEventsMonitor/SecurityEventsMonitorResponse.ts +17 -0
- package/Types/Permission.ts +399 -2
- package/Types/ScheduledMaintenance/ScheduledMaintenanceMeasurementAnchorType.ts +20 -0
- package/Types/ScheduledMaintenance/ScheduledMaintenanceStateRole.ts +16 -0
- package/Types/SecurityEvent/DetectionFindingConstants.ts +24 -0
- package/Types/SecurityEvent/NormalizedSecurityEvent.ts +92 -0
- package/Types/SecurityEvent/OcsfEventClass.ts +205 -0
- package/Types/SecurityEvent/OcsfSeverity.ts +88 -0
- package/Types/SecurityEvent/SecurityEventFormat.ts +23 -0
- package/Types/SecurityEvent/SigmaRule.ts +70 -0
- package/Types/Telemetry/LlmConventions.ts +20 -1
- package/Types/Telemetry/LlmCostCatalog.ts +557 -0
- package/Types/Telemetry/ServiceType.ts +1 -0
- package/Types/Telemetry/TelemetryRetentionConfig.ts +5 -1
- package/Types/Telemetry/TelemetryType.ts +1 -0
- package/Types/TwoFactorAuthStatus.ts +42 -0
- package/Types/UserAuthenticationStatus.ts +18 -0
- package/UI/Components/ActionButton/ActionButtonSchema.ts +7 -0
- package/UI/Components/BulkUpdate/BulkArchiveActions.tsx +35 -2
- package/UI/Components/BulkUpdate/BulkLabelActions.tsx +34 -1
- package/UI/Components/BulkUpdate/BulkOwnerActions.tsx +45 -1
- package/UI/Components/BulkUpdate/BulkUpdateForm.tsx +3 -0
- package/UI/Components/Button/Button.tsx +37 -2
- package/UI/Components/Card/Card.tsx +6 -0
- package/UI/Components/CustomFields/CustomFieldsDetail.tsx +24 -1
- package/UI/Components/Dictionary/DictionaryFilterOperator.ts +57 -0
- package/UI/Components/DuplicateModel/DuplicateModel.tsx +16 -0
- package/UI/Components/Forms/ModelForm.tsx +54 -2
- package/UI/Components/List/ListRow.tsx +6 -0
- package/UI/Components/LogsViewer/components/ActiveFilterChips.tsx +34 -7
- package/UI/Components/LogsViewer/components/LogsFacetSidebar.tsx +7 -40
- package/UI/Components/LogsViewer/components/SavedViewsDropdown.tsx +155 -3
- package/UI/Components/ModelDelete/ModelDelete.tsx +20 -0
- package/UI/Components/ModelDetail/CardModelDetail.tsx +36 -23
- package/UI/Components/ModelList/ModelList.tsx +21 -2
- package/UI/Components/ModelList/StaticModelList.tsx +11 -0
- package/UI/Components/ModelTable/BaseModelTable.tsx +168 -62
- package/UI/Components/ModelTable/ModelTable.tsx +16 -10
- package/UI/Components/ModelTable/TableView.tsx +36 -4
- package/UI/Components/MoreMenu/MoreMenuItem.tsx +36 -4
- package/UI/Components/OrderedStatesList/Item.tsx +6 -0
- package/UI/Components/OrderedStatesList/OrderedStatesList.tsx +81 -47
- package/UI/Components/ResetObjectID/ResetObjectID.tsx +16 -0
- package/UI/Components/SavedViews/SavedViewsFacetSection.tsx +242 -0
- package/UI/Components/SavedViews/SavedViewsList.ts +168 -0
- package/UI/Components/SavedViews/SavedViewsSearchInput.tsx +46 -0
- package/UI/Components/SavedViews/SavedViewsShowMoreButton.tsx +44 -0
- package/UI/Components/Table/TableRow.tsx +12 -0
- package/UI/Components/TelemetryViewer/components/SavedViewsDropdown.tsx +152 -2
- package/UI/Types/UseComponentOutsideClick.ts +47 -0
- package/UI/Utils/PermissionGate.ts +277 -0
- package/Utils/Dashboard/Components/DashboardSecurityEventsFlowComponent.ts +84 -0
- package/Utils/Dashboard/Components/DashboardSecurityEventsListComponent.ts +110 -0
- package/Utils/Dashboard/Components/Index.ts +14 -0
- package/Utils/Measurement/MeasurementEvaluator.ts +359 -0
- package/Utils/Monitor/MonitorSummarySnapshotUtil.ts +1 -0
- package/Utils/Monitor/NetworkDeviceLinkRuleUtil.ts +93 -11
- package/Utils/NetworkDevice/DeviceHealthStateUtil.ts +231 -0
- package/Utils/SecurityEvent/GenericNormalizer.ts +208 -0
- package/Utils/SecurityEvent/GoogleSecOpsAlertNormalizer.ts +206 -0
- package/Utils/SecurityEvent/NormalizerHelpers.ts +263 -0
- package/Utils/SecurityEvent/OcsfNormalizer.ts +194 -0
- package/Utils/SecurityEvent/SecurityEventNormalizer.ts +82 -0
- package/Utils/SecurityEvent/Sigma/SigmaRuleParser.ts +579 -0
- package/Utils/SecurityEvent/UdmNormalizer.ts +442 -0
- package/Utils/Telemetry/CrossSignalScope.ts +46 -9
- package/Utils/Telemetry/LogErrorPattern.ts +350 -0
- package/build/dist/Models/AnalyticsModels/Index.js +10 -0
- package/build/dist/Models/AnalyticsModels/Index.js.map +1 -1
- package/build/dist/Models/AnalyticsModels/LogCountBaseline.js +159 -0
- package/build/dist/Models/AnalyticsModels/LogCountBaseline.js.map +1 -0
- package/build/dist/Models/AnalyticsModels/SecurityEvent.js +596 -0
- package/build/dist/Models/AnalyticsModels/SecurityEvent.js.map +1 -0
- package/build/dist/Models/AnalyticsModels/Span.js +24 -1
- package/build/dist/Models/AnalyticsModels/Span.js.map +1 -1
- package/build/dist/Models/AnalyticsModels/SpanCountBaseline.js +181 -0
- package/build/dist/Models/AnalyticsModels/SpanCountBaseline.js.map +1 -0
- package/build/dist/Models/DatabaseModels/Alert.js +43 -0
- package/build/dist/Models/DatabaseModels/Alert.js.map +1 -1
- package/build/dist/Models/DatabaseModels/AlertMeasurement.js +1143 -0
- package/build/dist/Models/DatabaseModels/AlertMeasurement.js.map +1 -0
- package/build/dist/Models/DatabaseModels/AlertMeasurementValue.js +523 -0
- package/build/dist/Models/DatabaseModels/AlertMeasurementValue.js.map +1 -0
- package/build/dist/Models/DatabaseModels/AlertStateTimeline.js +9 -2
- package/build/dist/Models/DatabaseModels/AlertStateTimeline.js.map +1 -1
- package/build/dist/Models/DatabaseModels/BillingPaymentMethod.js +10 -11
- package/build/dist/Models/DatabaseModels/BillingPaymentMethod.js.map +1 -1
- package/build/dist/Models/DatabaseModels/DetectionRule.js +598 -0
- package/build/dist/Models/DatabaseModels/DetectionRule.js.map +1 -0
- package/build/dist/Models/DatabaseModels/GoogleSecOpsConnection.js +410 -0
- package/build/dist/Models/DatabaseModels/GoogleSecOpsConnection.js.map +1 -0
- package/build/dist/Models/DatabaseModels/Incident.js +43 -0
- package/build/dist/Models/DatabaseModels/Incident.js.map +1 -1
- package/build/dist/Models/DatabaseModels/IncidentMeasurement.js +1143 -0
- package/build/dist/Models/DatabaseModels/IncidentMeasurement.js.map +1 -0
- package/build/dist/Models/DatabaseModels/IncidentMeasurementValue.js +523 -0
- package/build/dist/Models/DatabaseModels/IncidentMeasurementValue.js.map +1 -0
- package/build/dist/Models/DatabaseModels/IncidentStateTimeline.js +9 -2
- package/build/dist/Models/DatabaseModels/IncidentStateTimeline.js.map +1 -1
- package/build/dist/Models/DatabaseModels/IncomingCallPolicyLabelRule.js +36 -0
- package/build/dist/Models/DatabaseModels/IncomingCallPolicyLabelRule.js.map +1 -1
- package/build/dist/Models/DatabaseModels/IncomingCallPolicyOwnerRule.js +42 -0
- package/build/dist/Models/DatabaseModels/IncomingCallPolicyOwnerRule.js.map +1 -1
- package/build/dist/Models/DatabaseModels/Index.js +20 -0
- package/build/dist/Models/DatabaseModels/Index.js.map +1 -1
- package/build/dist/Models/DatabaseModels/Label.js +11 -0
- package/build/dist/Models/DatabaseModels/Label.js.map +1 -1
- package/build/dist/Models/DatabaseModels/LlmCostBudget.js +681 -0
- package/build/dist/Models/DatabaseModels/LlmCostBudget.js.map +1 -0
- package/build/dist/Models/DatabaseModels/LlmModelPrice.js +503 -0
- package/build/dist/Models/DatabaseModels/LlmModelPrice.js.map +1 -0
- package/build/dist/Models/DatabaseModels/LogSavedView.js +87 -0
- package/build/dist/Models/DatabaseModels/LogSavedView.js.map +1 -1
- package/build/dist/Models/DatabaseModels/MarketingConversion.js +224 -8
- package/build/dist/Models/DatabaseModels/MarketingConversion.js.map +1 -1
- package/build/dist/Models/DatabaseModels/MetricSavedView.js +59 -0
- package/build/dist/Models/DatabaseModels/MetricSavedView.js.map +1 -1
- package/build/dist/Models/DatabaseModels/OnCallDutyPolicyLabelRule.js +36 -0
- package/build/dist/Models/DatabaseModels/OnCallDutyPolicyLabelRule.js.map +1 -1
- package/build/dist/Models/DatabaseModels/OnCallDutyPolicyOwnerRule.js +42 -0
- package/build/dist/Models/DatabaseModels/OnCallDutyPolicyOwnerRule.js.map +1 -1
- package/build/dist/Models/DatabaseModels/OnCallDutyPolicyScheduleLabelRule.js +36 -0
- package/build/dist/Models/DatabaseModels/OnCallDutyPolicyScheduleLabelRule.js.map +1 -1
- package/build/dist/Models/DatabaseModels/OnCallDutyPolicyScheduleOwnerRule.js +42 -0
- package/build/dist/Models/DatabaseModels/OnCallDutyPolicyScheduleOwnerRule.js.map +1 -1
- package/build/dist/Models/DatabaseModels/Probe.js +21 -0
- package/build/dist/Models/DatabaseModels/Probe.js.map +1 -1
- package/build/dist/Models/DatabaseModels/RunbookLabelRule.js +36 -0
- package/build/dist/Models/DatabaseModels/RunbookLabelRule.js.map +1 -1
- package/build/dist/Models/DatabaseModels/RunbookOwnerRule.js +42 -0
- package/build/dist/Models/DatabaseModels/RunbookOwnerRule.js.map +1 -1
- package/build/dist/Models/DatabaseModels/ScheduledMaintenanceMeasurement.js +1133 -0
- package/build/dist/Models/DatabaseModels/ScheduledMaintenanceMeasurement.js.map +1 -0
- package/build/dist/Models/DatabaseModels/ScheduledMaintenanceMeasurementValue.js +515 -0
- package/build/dist/Models/DatabaseModels/ScheduledMaintenanceMeasurementValue.js.map +1 -0
- package/build/dist/Models/DatabaseModels/ScheduledMaintenanceStateTimeline.js +9 -2
- package/build/dist/Models/DatabaseModels/ScheduledMaintenanceStateTimeline.js.map +1 -1
- package/build/dist/Models/DatabaseModels/StatusPageLabelRule.js +36 -0
- package/build/dist/Models/DatabaseModels/StatusPageLabelRule.js.map +1 -1
- package/build/dist/Models/DatabaseModels/StatusPageOwnerRule.js +42 -0
- package/build/dist/Models/DatabaseModels/StatusPageOwnerRule.js.map +1 -1
- package/build/dist/Models/DatabaseModels/TableView.js +15 -0
- package/build/dist/Models/DatabaseModels/TableView.js.map +1 -1
- package/build/dist/Models/DatabaseModels/Team.js +13 -0
- package/build/dist/Models/DatabaseModels/Team.js.map +1 -1
- package/build/dist/Models/DatabaseModels/TeamMember.js +8 -0
- package/build/dist/Models/DatabaseModels/TeamMember.js.map +1 -1
- package/build/dist/Models/DatabaseModels/TraceSavedView.js +52 -0
- package/build/dist/Models/DatabaseModels/TraceSavedView.js.map +1 -1
- package/build/dist/Models/DatabaseModels/WorkflowLabelRule.js +36 -0
- package/build/dist/Models/DatabaseModels/WorkflowLabelRule.js.map +1 -1
- package/build/dist/Models/DatabaseModels/WorkflowOwnerRule.js +42 -0
- package/build/dist/Models/DatabaseModels/WorkflowOwnerRule.js.map +1 -1
- package/build/dist/Server/API/TelemetryAPI.js +136 -0
- package/build/dist/Server/API/TelemetryAPI.js.map +1 -1
- package/build/dist/Server/API/UserAPI.js +75 -2
- package/build/dist/Server/API/UserAPI.js.map +1 -1
- package/build/dist/Server/EnvironmentConfig.js +29 -1
- package/build/dist/Server/EnvironmentConfig.js.map +1 -1
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1788000000000-AddDetectionRuleAndGoogleSecOpsConnection.js +36 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1788000000000-AddDetectionRuleAndGoogleSecOpsConnection.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1788100000000-AddMeasurements.js +162 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1788100000000-AddMeasurements.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1788200000000-AddLlmCostBudget.js +40 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1788200000000-AddLlmCostBudget.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1788300000000-AddLlmModelPrice.js +22 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1788300000000-AddLlmModelPrice.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1788400000000-AddMarketingConversionAttribution.js +52 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1788400000000-AddMarketingConversionAttribution.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1788500000000-RemoveLlmCostBudgetAlertColumns.js +38 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1788500000000-RemoveLlmCostBudgetAlertColumns.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1788600000000-AddDetectionRuleIncidentColumns.js +24 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1788600000000-AddDetectionRuleIncidentColumns.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +14 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
- package/build/dist/Server/Middleware/IdentityRateLimit.js +491 -0
- package/build/dist/Server/Middleware/IdentityRateLimit.js.map +1 -0
- package/build/dist/Server/Services/AccessTokenService.js +13 -2
- package/build/dist/Server/Services/AccessTokenService.js.map +1 -1
- package/build/dist/Server/Services/AlertMeasurementService.js +250 -0
- package/build/dist/Server/Services/AlertMeasurementService.js.map +1 -0
- package/build/dist/Server/Services/AlertMeasurementValueService.js +400 -0
- package/build/dist/Server/Services/AlertMeasurementValueService.js.map +1 -0
- package/build/dist/Server/Services/AlertService.js +37 -0
- package/build/dist/Server/Services/AlertService.js.map +1 -1
- package/build/dist/Server/Services/AlertStateTimelineService.js +48 -0
- package/build/dist/Server/Services/AlertStateTimelineService.js.map +1 -1
- package/build/dist/Server/Services/DetectionRuleService.js +50 -0
- package/build/dist/Server/Services/DetectionRuleService.js.map +1 -0
- package/build/dist/Server/Services/GoogleSecOpsConnectionService.js +55 -0
- package/build/dist/Server/Services/GoogleSecOpsConnectionService.js.map +1 -0
- package/build/dist/Server/Services/IncidentMeasurementService.js +252 -0
- package/build/dist/Server/Services/IncidentMeasurementService.js.map +1 -0
- package/build/dist/Server/Services/IncidentMeasurementValueService.js +413 -0
- package/build/dist/Server/Services/IncidentMeasurementValueService.js.map +1 -0
- package/build/dist/Server/Services/IncidentService.js +41 -0
- package/build/dist/Server/Services/IncidentService.js.map +1 -1
- package/build/dist/Server/Services/IncidentStateTimelineService.js +50 -0
- package/build/dist/Server/Services/IncidentStateTimelineService.js.map +1 -1
- package/build/dist/Server/Services/Index.js +30 -0
- package/build/dist/Server/Services/Index.js.map +1 -1
- package/build/dist/Server/Services/LlmCostBudgetService.js +89 -0
- package/build/dist/Server/Services/LlmCostBudgetService.js.map +1 -0
- package/build/dist/Server/Services/LlmModelPriceService.js +163 -0
- package/build/dist/Server/Services/LlmModelPriceService.js.map +1 -0
- package/build/dist/Server/Services/LogAggregationService.js +709 -0
- package/build/dist/Server/Services/LogAggregationService.js.map +1 -1
- package/build/dist/Server/Services/LogCountBaselineService.js +112 -0
- package/build/dist/Server/Services/LogCountBaselineService.js.map +1 -0
- package/build/dist/Server/Services/MutableMetricService.js +28 -9
- package/build/dist/Server/Services/MutableMetricService.js.map +1 -1
- package/build/dist/Server/Services/ScheduledMaintenanceMeasurementService.js +254 -0
- package/build/dist/Server/Services/ScheduledMaintenanceMeasurementService.js.map +1 -0
- package/build/dist/Server/Services/ScheduledMaintenanceMeasurementValueService.js +434 -0
- package/build/dist/Server/Services/ScheduledMaintenanceMeasurementValueService.js.map +1 -0
- package/build/dist/Server/Services/ScheduledMaintenanceService.js +37 -0
- package/build/dist/Server/Services/ScheduledMaintenanceService.js.map +1 -1
- package/build/dist/Server/Services/ScheduledMaintenanceStateTimelineService.js +134 -0
- package/build/dist/Server/Services/ScheduledMaintenanceStateTimelineService.js.map +1 -1
- package/build/dist/Server/Services/SecurityEventService.js +64 -0
- package/build/dist/Server/Services/SecurityEventService.js.map +1 -0
- package/build/dist/Server/Services/SpanCountBaselineService.js +122 -0
- package/build/dist/Server/Services/SpanCountBaselineService.js.map +1 -0
- package/build/dist/Server/Services/StatusPageService.js +4 -3
- package/build/dist/Server/Services/StatusPageService.js.map +1 -1
- package/build/dist/Server/Services/TelemetryUsageBillingService.js +15 -3
- package/build/dist/Server/Services/TelemetryUsageBillingService.js.map +1 -1
- package/build/dist/Server/Services/UserService.js +262 -42
- package/build/dist/Server/Services/UserService.js.map +1 -1
- package/build/dist/Server/Services/UserTotpAuthService.js +0 -73
- package/build/dist/Server/Services/UserTotpAuthService.js.map +1 -1
- package/build/dist/Server/Services/UserWebAuthnService.js +1 -73
- package/build/dist/Server/Services/UserWebAuthnService.js.map +1 -1
- package/build/dist/Server/Types/Billing/MeteredPlan/AllMeteredPlans.js +13 -0
- package/build/dist/Server/Types/Billing/MeteredPlan/AllMeteredPlans.js.map +1 -1
- package/build/dist/Server/Types/Database/Permissions/TenantPermission.js +10 -0
- package/build/dist/Server/Types/Database/Permissions/TenantPermission.js.map +1 -1
- package/build/dist/Server/Types/Database/QueryHelper.js +1 -1
- package/build/dist/Server/Types/Database/QueryHelper.js.map +1 -1
- package/build/dist/Server/Utils/AI/Chat/ObservabilityChatPrompt.js +1 -1
- package/build/dist/Server/Utils/AI/Toolbox/Index.js +3 -0
- package/build/dist/Server/Utils/AI/Toolbox/Index.js.map +1 -1
- package/build/dist/Server/Utils/AI/Toolbox/SecurityEventTools.js +257 -0
- package/build/dist/Server/Utils/AI/Toolbox/SecurityEventTools.js.map +1 -0
- package/build/dist/Server/Utils/AnalyticsDatabase/StatementGenerator.js +13 -4
- package/build/dist/Server/Utils/AnalyticsDatabase/StatementGenerator.js.map +1 -1
- package/build/dist/Server/Utils/Attribution.js +77 -33
- package/build/dist/Server/Utils/Attribution.js.map +1 -1
- package/build/dist/Server/Utils/Marketing/ConversionUploadProvider.js +46 -9
- package/build/dist/Server/Utils/Marketing/ConversionUploadProvider.js.map +1 -1
- package/build/dist/Server/Utils/Marketing/Providers/GoogleAds.js +52 -8
- package/build/dist/Server/Utils/Marketing/Providers/GoogleAds.js.map +1 -1
- package/build/dist/Server/Utils/Marketing/Providers/LinkedIn.js +41 -17
- package/build/dist/Server/Utils/Marketing/Providers/LinkedIn.js.map +1 -1
- package/build/dist/Server/Utils/Marketing/Providers/Meta.js +42 -18
- package/build/dist/Server/Utils/Marketing/Providers/Meta.js.map +1 -1
- package/build/dist/Server/Utils/Marketing/Providers/MicrosoftAds.js +24 -11
- package/build/dist/Server/Utils/Marketing/Providers/MicrosoftAds.js.map +1 -1
- package/build/dist/Server/Utils/Marketing/Providers/Reddit.js +34 -15
- package/build/dist/Server/Utils/Marketing/Providers/Reddit.js.map +1 -1
- package/build/dist/Server/Utils/Measurement/MeasurementDefinitionValidator.js +109 -0
- package/build/dist/Server/Utils/Measurement/MeasurementDefinitionValidator.js.map +1 -0
- package/build/dist/Server/Utils/Measurement/MeasurementMetricWriter.js +131 -0
- package/build/dist/Server/Utils/Measurement/MeasurementMetricWriter.js.map +1 -0
- package/build/dist/Server/Utils/Monitor/Criteria/CountAnomaly.js +111 -0
- package/build/dist/Server/Utils/Monitor/Criteria/CountAnomaly.js.map +1 -0
- package/build/dist/Server/Utils/Monitor/Criteria/LogMonitorCriteria.js +84 -1
- package/build/dist/Server/Utils/Monitor/Criteria/LogMonitorCriteria.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/Criteria/SecurityEventsMonitorCriteria.js +35 -0
- package/build/dist/Server/Utils/Monitor/Criteria/SecurityEventsMonitorCriteria.js.map +1 -0
- package/build/dist/Server/Utils/Monitor/Criteria/TraceMonitorCriteria.js +84 -1
- package/build/dist/Server/Utils/Monitor/Criteria/TraceMonitorCriteria.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/MonitorCriteriaDataExtractor.js +7 -0
- package/build/dist/Server/Utils/Monitor/MonitorCriteriaDataExtractor.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/MonitorCriteriaEvaluator.js +12 -0
- package/build/dist/Server/Utils/Monitor/MonitorCriteriaEvaluator.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/MonitorCriteriaObservationBuilder.js +9 -0
- package/build/dist/Server/Utils/Monitor/MonitorCriteriaObservationBuilder.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/MonitorResource.js +10 -0
- package/build/dist/Server/Utils/Monitor/MonitorResource.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/MonitorStepResourceIdentity.js +6 -5
- package/build/dist/Server/Utils/Monitor/MonitorStepResourceIdentity.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/MonitorStepsReferenceExtractor.js +1 -0
- package/build/dist/Server/Utils/Monitor/MonitorStepsReferenceExtractor.js.map +1 -1
- package/build/dist/Server/Utils/SecurityEvent/DetectionRuleEvaluator.js +600 -0
- package/build/dist/Server/Utils/SecurityEvent/DetectionRuleEvaluator.js.map +1 -0
- package/build/dist/Server/Utils/SecurityEvent/GoogleSecOps/GoogleSecOpsClient.js +145 -0
- package/build/dist/Server/Utils/SecurityEvent/GoogleSecOps/GoogleSecOpsClient.js.map +1 -0
- package/build/dist/Server/Utils/SecurityEvent/GoogleSecOps/GoogleSecOpsPoller.js +176 -0
- package/build/dist/Server/Utils/SecurityEvent/GoogleSecOps/GoogleSecOpsPoller.js.map +1 -0
- package/build/dist/Server/Utils/SecurityEvent/SecurityEventRow.js +69 -0
- package/build/dist/Server/Utils/SecurityEvent/SecurityEventRow.js.map +1 -0
- package/build/dist/Server/Utils/SecurityEvent/Sigma/SigmaClickhouseCompiler.js +493 -0
- package/build/dist/Server/Utils/SecurityEvent/Sigma/SigmaClickhouseCompiler.js.map +1 -0
- package/build/dist/Server/Utils/Telemetry/LlmCostBudgetEvaluator.js +317 -0
- package/build/dist/Server/Utils/Telemetry/LlmCostBudgetEvaluator.js.map +1 -0
- package/build/dist/Server/Utils/Telemetry/LlmSpan.js +56 -4
- package/build/dist/Server/Utils/Telemetry/LlmSpan.js.map +1 -1
- package/build/dist/Server/Utils/Telemetry/LogErrorPatternSql.js +112 -0
- package/build/dist/Server/Utils/Telemetry/LogErrorPatternSql.js.map +1 -0
- package/build/dist/Server/Utils/UserPermission/UserPermission.js +46 -1
- package/build/dist/Server/Utils/UserPermission/UserPermission.js.map +1 -1
- package/build/dist/Types/AI/AIChatTypes.js +1 -0
- package/build/dist/Types/AI/AIChatTypes.js.map +1 -1
- package/build/dist/Types/Alerts/AlertMeasurementAnchorType.js +15 -0
- package/build/dist/Types/Alerts/AlertMeasurementAnchorType.js.map +1 -0
- package/build/dist/Types/Alerts/AlertStateRole.js +12 -0
- package/build/dist/Types/Alerts/AlertStateRole.js.map +1 -0
- package/build/dist/Types/AnalyticsDatabase/AnalyticsTableName.js +16 -0
- package/build/dist/Types/AnalyticsDatabase/AnalyticsTableName.js.map +1 -1
- package/build/dist/Types/Dashboard/Chart/ChartType.js +1 -0
- package/build/dist/Types/Dashboard/Chart/ChartType.js.map +1 -1
- package/build/dist/Types/Dashboard/DashboardComponentType.js +2 -0
- package/build/dist/Types/Dashboard/DashboardComponentType.js.map +1 -1
- package/build/dist/Types/Dashboard/DashboardComponents/DashboardSecurityEventsFlowComponent.js +2 -0
- package/build/dist/Types/Dashboard/DashboardComponents/DashboardSecurityEventsFlowComponent.js.map +1 -0
- package/build/dist/Types/Dashboard/DashboardComponents/DashboardSecurityEventsListComponent.js +2 -0
- package/build/dist/Types/Dashboard/DashboardComponents/DashboardSecurityEventsListComponent.js.map +1 -0
- package/build/dist/Types/Incident/IncidentMeasurementAnchorType.js +29 -0
- package/build/dist/Types/Incident/IncidentMeasurementAnchorType.js.map +1 -0
- package/build/dist/Types/Incident/IncidentStateRole.js +15 -0
- package/build/dist/Types/Incident/IncidentStateRole.js.map +1 -0
- package/build/dist/Types/Marketing/Attribution.js +68 -0
- package/build/dist/Types/Marketing/Attribution.js.map +1 -0
- package/build/dist/Types/Marketing/MarketingConversion.js +36 -7
- package/build/dist/Types/Marketing/MarketingConversion.js.map +1 -1
- package/build/dist/Types/Measurement/MeasurementAggregationType.js +40 -0
- package/build/dist/Types/Measurement/MeasurementAggregationType.js.map +1 -0
- package/build/dist/Types/Measurement/MeasurementAnchorKind.js +22 -0
- package/build/dist/Types/Measurement/MeasurementAnchorKind.js.map +1 -0
- package/build/dist/Types/Measurement/MeasurementOccurrence.js +15 -0
- package/build/dist/Types/Measurement/MeasurementOccurrence.js.map +1 -0
- package/build/dist/Types/Measurement/MeasurementStatus.js +32 -0
- package/build/dist/Types/Measurement/MeasurementStatus.js.map +1 -0
- package/build/dist/Types/MeteredPlan/ProductType.js +1 -0
- package/build/dist/Types/MeteredPlan/ProductType.js.map +1 -1
- package/build/dist/Types/Monitor/CriteriaFilter.js +2 -0
- package/build/dist/Types/Monitor/CriteriaFilter.js.map +1 -1
- package/build/dist/Types/Monitor/MonitorCriteriaInstance.js +59 -0
- package/build/dist/Types/Monitor/MonitorCriteriaInstance.js.map +1 -1
- package/build/dist/Types/Monitor/MonitorStep.js +17 -0
- package/build/dist/Types/Monitor/MonitorStep.js.map +1 -1
- package/build/dist/Types/Monitor/MonitorStepSecurityEventsMonitor.js +64 -0
- package/build/dist/Types/Monitor/MonitorStepSecurityEventsMonitor.js.map +1 -0
- package/build/dist/Types/Monitor/MonitorType.js +19 -0
- package/build/dist/Types/Monitor/MonitorType.js.map +1 -1
- package/build/dist/Types/Monitor/SecurityEventsMonitor/SecurityEventsMonitorResponse.js +2 -0
- package/build/dist/Types/Monitor/SecurityEventsMonitor/SecurityEventsMonitorResponse.js.map +1 -0
- package/build/dist/Types/Permission.js +347 -2
- package/build/dist/Types/Permission.js.map +1 -1
- package/build/dist/Types/ScheduledMaintenance/ScheduledMaintenanceMeasurementAnchorType.js +19 -0
- package/build/dist/Types/ScheduledMaintenance/ScheduledMaintenanceMeasurementAnchorType.js.map +1 -0
- package/build/dist/Types/ScheduledMaintenance/ScheduledMaintenanceStateRole.js +17 -0
- package/build/dist/Types/ScheduledMaintenance/ScheduledMaintenanceStateRole.js.map +1 -0
- package/build/dist/Types/SecurityEvent/DetectionFindingConstants.js +18 -0
- package/build/dist/Types/SecurityEvent/DetectionFindingConstants.js.map +1 -0
- package/build/dist/Types/SecurityEvent/NormalizedSecurityEvent.js +2 -0
- package/build/dist/Types/SecurityEvent/NormalizedSecurityEvent.js.map +1 -0
- package/build/dist/Types/SecurityEvent/OcsfEventClass.js +181 -0
- package/build/dist/Types/SecurityEvent/OcsfEventClass.js.map +1 -0
- package/build/dist/Types/SecurityEvent/OcsfSeverity.js +81 -0
- package/build/dist/Types/SecurityEvent/OcsfSeverity.js.map +1 -0
- package/build/dist/Types/SecurityEvent/SecurityEventFormat.js +21 -0
- package/build/dist/Types/SecurityEvent/SecurityEventFormat.js.map +1 -0
- package/build/dist/Types/SecurityEvent/SigmaRule.js +16 -0
- package/build/dist/Types/SecurityEvent/SigmaRule.js.map +1 -0
- package/build/dist/Types/Telemetry/LlmConventions.js +19 -1
- package/build/dist/Types/Telemetry/LlmConventions.js.map +1 -1
- package/build/dist/Types/Telemetry/LlmCostCatalog.js +484 -0
- package/build/dist/Types/Telemetry/LlmCostCatalog.js.map +1 -0
- package/build/dist/Types/Telemetry/ServiceType.js +1 -0
- package/build/dist/Types/Telemetry/ServiceType.js.map +1 -1
- package/build/dist/Types/Telemetry/TelemetryRetentionConfig.js.map +1 -1
- package/build/dist/Types/Telemetry/TelemetryType.js +1 -0
- package/build/dist/Types/Telemetry/TelemetryType.js.map +1 -1
- package/build/dist/Types/TwoFactorAuthStatus.js +41 -0
- package/build/dist/Types/TwoFactorAuthStatus.js.map +1 -0
- package/build/dist/UI/Components/BulkUpdate/BulkArchiveActions.js +16 -2
- package/build/dist/UI/Components/BulkUpdate/BulkArchiveActions.js.map +1 -1
- package/build/dist/UI/Components/BulkUpdate/BulkLabelActions.js +15 -1
- package/build/dist/UI/Components/BulkUpdate/BulkLabelActions.js.map +1 -1
- package/build/dist/UI/Components/BulkUpdate/BulkOwnerActions.js +20 -1
- package/build/dist/UI/Components/BulkUpdate/BulkOwnerActions.js.map +1 -1
- package/build/dist/UI/Components/BulkUpdate/BulkUpdateForm.js +1 -1
- package/build/dist/UI/Components/BulkUpdate/BulkUpdateForm.js.map +1 -1
- package/build/dist/UI/Components/Button/Button.js +22 -1
- package/build/dist/UI/Components/Button/Button.js.map +1 -1
- package/build/dist/UI/Components/Card/Card.js +1 -1
- package/build/dist/UI/Components/Card/Card.js.map +1 -1
- package/build/dist/UI/Components/CustomFields/CustomFieldsDetail.js +14 -1
- package/build/dist/UI/Components/CustomFields/CustomFieldsDetail.js.map +1 -1
- package/build/dist/UI/Components/Dictionary/DictionaryFilterOperator.js +47 -0
- package/build/dist/UI/Components/Dictionary/DictionaryFilterOperator.js.map +1 -1
- package/build/dist/UI/Components/DuplicateModel/DuplicateModel.js +8 -0
- package/build/dist/UI/Components/DuplicateModel/DuplicateModel.js.map +1 -1
- package/build/dist/UI/Components/Forms/ModelForm.js +40 -1
- package/build/dist/UI/Components/Forms/ModelForm.js.map +1 -1
- package/build/dist/UI/Components/List/ListRow.js +4 -1
- package/build/dist/UI/Components/List/ListRow.js.map +1 -1
- package/build/dist/UI/Components/LogsViewer/components/ActiveFilterChips.js +32 -7
- package/build/dist/UI/Components/LogsViewer/components/ActiveFilterChips.js.map +1 -1
- package/build/dist/UI/Components/LogsViewer/components/LogsFacetSidebar.js +2 -17
- package/build/dist/UI/Components/LogsViewer/components/LogsFacetSidebar.js.map +1 -1
- package/build/dist/UI/Components/LogsViewer/components/SavedViewsDropdown.js +93 -4
- package/build/dist/UI/Components/LogsViewer/components/SavedViewsDropdown.js.map +1 -1
- package/build/dist/UI/Components/ModelDelete/ModelDelete.js +13 -0
- package/build/dist/UI/Components/ModelDelete/ModelDelete.js.map +1 -1
- package/build/dist/UI/Components/ModelDetail/CardModelDetail.js +27 -11
- package/build/dist/UI/Components/ModelDetail/CardModelDetail.js.map +1 -1
- package/build/dist/UI/Components/ModelList/ModelList.js +13 -2
- package/build/dist/UI/Components/ModelList/ModelList.js.map +1 -1
- package/build/dist/UI/Components/ModelList/StaticModelList.js +4 -1
- package/build/dist/UI/Components/ModelList/StaticModelList.js.map +1 -1
- package/build/dist/UI/Components/ModelTable/BaseModelTable.js +101 -40
- package/build/dist/UI/Components/ModelTable/BaseModelTable.js.map +1 -1
- package/build/dist/UI/Components/ModelTable/ModelTable.js +9 -7
- package/build/dist/UI/Components/ModelTable/ModelTable.js.map +1 -1
- package/build/dist/UI/Components/ModelTable/TableView.js +32 -4
- package/build/dist/UI/Components/ModelTable/TableView.js.map +1 -1
- package/build/dist/UI/Components/MoreMenu/MoreMenuItem.js +19 -2
- package/build/dist/UI/Components/MoreMenu/MoreMenuItem.js.map +1 -1
- package/build/dist/UI/Components/OrderedStatesList/Item.js +4 -1
- package/build/dist/UI/Components/OrderedStatesList/Item.js.map +1 -1
- package/build/dist/UI/Components/OrderedStatesList/OrderedStatesList.js +33 -32
- package/build/dist/UI/Components/OrderedStatesList/OrderedStatesList.js.map +1 -1
- package/build/dist/UI/Components/ResetObjectID/ResetObjectID.js +8 -0
- package/build/dist/UI/Components/ResetObjectID/ResetObjectID.js.map +1 -1
- package/build/dist/UI/Components/SavedViews/SavedViewsFacetSection.js +98 -0
- package/build/dist/UI/Components/SavedViews/SavedViewsFacetSection.js.map +1 -0
- package/build/dist/UI/Components/SavedViews/SavedViewsList.js +99 -0
- package/build/dist/UI/Components/SavedViews/SavedViewsList.js.map +1 -0
- package/build/dist/UI/Components/SavedViews/SavedViewsSearchInput.js +17 -0
- package/build/dist/UI/Components/SavedViews/SavedViewsSearchInput.js.map +1 -0
- package/build/dist/UI/Components/SavedViews/SavedViewsShowMoreButton.js +15 -0
- package/build/dist/UI/Components/SavedViews/SavedViewsShowMoreButton.js.map +1 -0
- package/build/dist/UI/Components/Table/TableRow.js +8 -2
- package/build/dist/UI/Components/Table/TableRow.js.map +1 -1
- package/build/dist/UI/Components/TelemetryViewer/components/SavedViewsDropdown.js +93 -4
- package/build/dist/UI/Components/TelemetryViewer/components/SavedViewsDropdown.js.map +1 -1
- package/build/dist/UI/Types/UseComponentOutsideClick.js +39 -0
- package/build/dist/UI/Types/UseComponentOutsideClick.js.map +1 -1
- package/build/dist/UI/Utils/PermissionGate.js +164 -0
- package/build/dist/UI/Utils/PermissionGate.js.map +1 -0
- package/build/dist/Utils/Dashboard/Components/DashboardSecurityEventsFlowComponent.js +70 -0
- package/build/dist/Utils/Dashboard/Components/DashboardSecurityEventsFlowComponent.js.map +1 -0
- package/build/dist/Utils/Dashboard/Components/DashboardSecurityEventsListComponent.js +92 -0
- package/build/dist/Utils/Dashboard/Components/DashboardSecurityEventsListComponent.js.map +1 -0
- package/build/dist/Utils/Dashboard/Components/Index.js +8 -0
- package/build/dist/Utils/Dashboard/Components/Index.js.map +1 -1
- package/build/dist/Utils/Measurement/MeasurementEvaluator.js +210 -0
- package/build/dist/Utils/Measurement/MeasurementEvaluator.js.map +1 -0
- package/build/dist/Utils/Monitor/MonitorSummarySnapshotUtil.js +1 -0
- package/build/dist/Utils/Monitor/MonitorSummarySnapshotUtil.js.map +1 -1
- package/build/dist/Utils/Monitor/NetworkDeviceLinkRuleUtil.js +51 -10
- package/build/dist/Utils/Monitor/NetworkDeviceLinkRuleUtil.js.map +1 -1
- package/build/dist/Utils/NetworkDevice/DeviceHealthStateUtil.js +108 -0
- package/build/dist/Utils/NetworkDevice/DeviceHealthStateUtil.js.map +1 -0
- package/build/dist/Utils/SecurityEvent/GenericNormalizer.js +176 -0
- package/build/dist/Utils/SecurityEvent/GenericNormalizer.js.map +1 -0
- package/build/dist/Utils/SecurityEvent/GoogleSecOpsAlertNormalizer.js +146 -0
- package/build/dist/Utils/SecurityEvent/GoogleSecOpsAlertNormalizer.js.map +1 -0
- package/build/dist/Utils/SecurityEvent/NormalizerHelpers.js +196 -0
- package/build/dist/Utils/SecurityEvent/NormalizerHelpers.js.map +1 -0
- package/build/dist/Utils/SecurityEvent/OcsfNormalizer.js +136 -0
- package/build/dist/Utils/SecurityEvent/OcsfNormalizer.js.map +1 -0
- package/build/dist/Utils/SecurityEvent/SecurityEventNormalizer.js +64 -0
- package/build/dist/Utils/SecurityEvent/SecurityEventNormalizer.js.map +1 -0
- package/build/dist/Utils/SecurityEvent/Sigma/SigmaRuleParser.js +388 -0
- package/build/dist/Utils/SecurityEvent/Sigma/SigmaRuleParser.js.map +1 -0
- package/build/dist/Utils/SecurityEvent/UdmNormalizer.js +328 -0
- package/build/dist/Utils/SecurityEvent/UdmNormalizer.js.map +1 -0
- package/build/dist/Utils/Telemetry/CrossSignalScope.js +29 -7
- package/build/dist/Utils/Telemetry/CrossSignalScope.js.map +1 -1
- package/build/dist/Utils/Telemetry/LogErrorPattern.js +284 -0
- package/build/dist/Utils/Telemetry/LogErrorPattern.js.map +1 -0
- package/package.json +3 -1
|
@@ -0,0 +1,1190 @@
|
|
|
1
|
+
import DatabaseService from "../../../Server/Services/DatabaseService";
|
|
2
|
+
import UserService from "../../../Server/Services/UserService";
|
|
3
|
+
import UserSessionService from "../../../Server/Services/UserSessionService";
|
|
4
|
+
import UserTotpAuthService from "../../../Server/Services/UserTotpAuthService";
|
|
5
|
+
import UserWebAuthnService from "../../../Server/Services/UserWebAuthnService";
|
|
6
|
+
import logger from "../../../Server/Utils/Logger";
|
|
7
|
+
import User from "../../../Models/DatabaseModels/User";
|
|
8
|
+
import ColumnLength from "../../../Types/Database/ColumnLength";
|
|
9
|
+
import LIMIT_MAX from "../../../Types/Database/LimitMax";
|
|
10
|
+
import Email from "../../../Types/Email";
|
|
11
|
+
import BadDataException from "../../../Types/Exception/BadDataException";
|
|
12
|
+
import NotFoundException from "../../../Types/Exception/NotFoundException";
|
|
13
|
+
import HashedString from "../../../Types/HashedString";
|
|
14
|
+
import ObjectID from "../../../Types/ObjectID";
|
|
15
|
+
import { getJestSpyOn } from "../../Spy";
|
|
16
|
+
import {
|
|
17
|
+
afterEach,
|
|
18
|
+
beforeEach,
|
|
19
|
+
describe,
|
|
20
|
+
expect,
|
|
21
|
+
jest,
|
|
22
|
+
test,
|
|
23
|
+
} from "@jest/globals";
|
|
24
|
+
|
|
25
|
+
/*
|
|
26
|
+
* ---------------------------------------------------------------------------
|
|
27
|
+
* UserService.setTwoFactorAuthRequired / resetTwoFactorAuth, and the three
|
|
28
|
+
* hooks that had to be DELETED for them to work at all.
|
|
29
|
+
*
|
|
30
|
+
* These are the two write paths behind Admin Dashboard > Users > (a user) >
|
|
31
|
+
* Authentication, plus the bulk actions on the user list. Everything here is a
|
|
32
|
+
* property that can regress while the page still renders and the request still
|
|
33
|
+
* returns 200:
|
|
34
|
+
*
|
|
35
|
+
* - "require two factor auth" is a promise about who can sign in. If the
|
|
36
|
+
* session revocation stops happening, or stops being awaited, or its reason
|
|
37
|
+
* string outgrows the ShortText column it is written into, the flag is set
|
|
38
|
+
* and every already-signed-in session sails on refreshing itself
|
|
39
|
+
* indefinitely. The page says "Required". Nothing says otherwise.
|
|
40
|
+
* - "reset two factor auth" is the lost-device fix. If it also cleared
|
|
41
|
+
* `enableTwoFactorAuth`, an operator helping with a lost phone would
|
|
42
|
+
* silently take the account OFF two factor auth, and the request that asked
|
|
43
|
+
* for a fresh QR code would have quietly downgraded the account instead.
|
|
44
|
+
* - three hooks used to refuse exactly the states this feature creates on
|
|
45
|
+
* purpose. They are gone, and "gone" is a thing tests have to hold in place:
|
|
46
|
+
* each of them looked like a safety guard, so each of them is a plausible
|
|
47
|
+
* thing for a future reader to put back.
|
|
48
|
+
*
|
|
49
|
+
* Sibling file: UserAuthenticationService.test.ts already covers
|
|
50
|
+
* getAuthenticationStatus (including the tri-state), setPassword and
|
|
51
|
+
* sendPasswordResetLink. Nothing here duplicates that -- this file is about the
|
|
52
|
+
* two mutating methods and the removed hooks.
|
|
53
|
+
*
|
|
54
|
+
* Everything stubs at this service's own boundary -- findOneBy / updateOneById
|
|
55
|
+
* / findBy on the UserService singleton, plus the two authenticator services
|
|
56
|
+
* and UserSessionService -- so the assertions are about what these methods ASK
|
|
57
|
+
* for. No database is involved.
|
|
58
|
+
* ---------------------------------------------------------------------------
|
|
59
|
+
*/
|
|
60
|
+
|
|
61
|
+
/*
|
|
62
|
+
* A spy plus the name to blame in a violation message, so a failure says WHICH
|
|
63
|
+
* write entry point was used rather than only that one of them was.
|
|
64
|
+
*/
|
|
65
|
+
type NamedSpy = {
|
|
66
|
+
name: string;
|
|
67
|
+
spy: any;
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
type BuildUserFunction = (data: {
|
|
71
|
+
id: ObjectID;
|
|
72
|
+
enableTwoFactorAuth?: boolean | undefined;
|
|
73
|
+
email?: string | undefined;
|
|
74
|
+
}) => User;
|
|
75
|
+
|
|
76
|
+
const buildUser: BuildUserFunction = (data: {
|
|
77
|
+
id: ObjectID;
|
|
78
|
+
enableTwoFactorAuth?: boolean | undefined;
|
|
79
|
+
email?: string | undefined;
|
|
80
|
+
}): User => {
|
|
81
|
+
const user: User = new User();
|
|
82
|
+
user.id = data.id;
|
|
83
|
+
|
|
84
|
+
/*
|
|
85
|
+
* Assigned only when supplied, so an omitted column is genuinely absent --
|
|
86
|
+
* the shape a `select` that did not ask for it produces -- rather than
|
|
87
|
+
* explicitly undefined.
|
|
88
|
+
*/
|
|
89
|
+
if (data.enableTwoFactorAuth !== undefined) {
|
|
90
|
+
user.enableTwoFactorAuth = data.enableTwoFactorAuth;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (data.email !== undefined) {
|
|
94
|
+
user.email = new Email(data.email);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return user;
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
describe("UserService -- admin-controlled two factor auth", () => {
|
|
101
|
+
/*
|
|
102
|
+
* TWO DISTINCT IDS ON PURPOSE, and the distinction is the only thing keeping
|
|
103
|
+
* a whole family of assertions in this file honest.
|
|
104
|
+
*
|
|
105
|
+
* `userId` is what the CALLER asked about -- the id in the admin route's
|
|
106
|
+
* URL. `foundUserId` is the id on the row `findOneBy` actually resolves.
|
|
107
|
+
* Both methods under test look the row up and then key every subsequent
|
|
108
|
+
* operation on `user.id!`, never on `data.userId`, which is what stops a
|
|
109
|
+
* lookup that resolves something else (a soft-delete filter, a tenancy
|
|
110
|
+
* scope, a future `findOneBy` overload) from ending with an update, a
|
|
111
|
+
* delete, or a session revocation aimed at an id nobody confirmed exists.
|
|
112
|
+
*
|
|
113
|
+
* If the fixture made these two the same value -- the obvious way to write
|
|
114
|
+
* it -- then `updateOneById({ id: data.userId })` and `updateOneById({ id:
|
|
115
|
+
* user.id! })` would be indistinguishable, and every "against the right
|
|
116
|
+
* user" assertion below would pass for either. They are generated
|
|
117
|
+
* separately so that they cannot be.
|
|
118
|
+
*/
|
|
119
|
+
let userId: ObjectID;
|
|
120
|
+
let foundUserId: ObjectID;
|
|
121
|
+
let findOneBySpy: any;
|
|
122
|
+
let findBySpy: any;
|
|
123
|
+
let updateOneByIdSpy: any;
|
|
124
|
+
let updateBySpy: any;
|
|
125
|
+
let revokeSessionsSpy: any;
|
|
126
|
+
let totpDeleteBySpy: any;
|
|
127
|
+
let webAuthnDeleteBySpy: any;
|
|
128
|
+
let totpCountBySpy: any;
|
|
129
|
+
let webAuthnCountBySpy: any;
|
|
130
|
+
let totpFindBySpy: any;
|
|
131
|
+
let webAuthnFindBySpy: any;
|
|
132
|
+
let loggerInfoSpy: any;
|
|
133
|
+
|
|
134
|
+
beforeEach(() => {
|
|
135
|
+
jest.restoreAllMocks();
|
|
136
|
+
|
|
137
|
+
userId = ObjectID.generate();
|
|
138
|
+
foundUserId = ObjectID.generate();
|
|
139
|
+
|
|
140
|
+
findOneBySpy = getJestSpyOn(UserService, "findOneBy").mockImplementation(
|
|
141
|
+
async (): Promise<User | null> => {
|
|
142
|
+
return null;
|
|
143
|
+
},
|
|
144
|
+
);
|
|
145
|
+
|
|
146
|
+
/*
|
|
147
|
+
* Returns ONE row, not an empty list, and that is load-bearing rather than
|
|
148
|
+
* incidental.
|
|
149
|
+
*
|
|
150
|
+
* The guard this suite exists to prove is gone read its users through
|
|
151
|
+
* `findBy` and threw from INSIDE `for (const user of users)`. Stub that to
|
|
152
|
+
* `[]` and a verbatim restoration of the guard iterates zero times, throws
|
|
153
|
+
* nothing, and every "the guard stays deleted" test below stays green
|
|
154
|
+
* while the feature is dead. A non-empty list is what makes those tests
|
|
155
|
+
* able to fail.
|
|
156
|
+
*/
|
|
157
|
+
findBySpy = getJestSpyOn(UserService, "findBy").mockImplementation(
|
|
158
|
+
async (): Promise<Array<User>> => {
|
|
159
|
+
return [buildUser({ id: foundUserId })];
|
|
160
|
+
},
|
|
161
|
+
);
|
|
162
|
+
|
|
163
|
+
updateOneByIdSpy = getJestSpyOn(
|
|
164
|
+
UserService,
|
|
165
|
+
"updateOneById",
|
|
166
|
+
).mockImplementation(async (): Promise<void> => {
|
|
167
|
+
return undefined;
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
/*
|
|
171
|
+
* The OTHER write entry point on this service. `updateOneById` is the one
|
|
172
|
+
* these methods use today, but `updateBy` is what it funnels into, so
|
|
173
|
+
* spying on both is what lets "this method writes nothing to the User row"
|
|
174
|
+
* be asserted as a fact about the service rather than about one method
|
|
175
|
+
* name -- see the reset case below.
|
|
176
|
+
*/
|
|
177
|
+
updateBySpy = getJestSpyOn(UserService, "updateBy").mockImplementation(
|
|
178
|
+
async (): Promise<number> => {
|
|
179
|
+
return 0;
|
|
180
|
+
},
|
|
181
|
+
);
|
|
182
|
+
|
|
183
|
+
revokeSessionsSpy = getJestSpyOn(
|
|
184
|
+
UserSessionService,
|
|
185
|
+
"revokeAllSessionsByUserId",
|
|
186
|
+
).mockImplementation(async (): Promise<void> => {
|
|
187
|
+
return undefined;
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
totpDeleteBySpy = getJestSpyOn(
|
|
191
|
+
UserTotpAuthService,
|
|
192
|
+
"deleteBy",
|
|
193
|
+
).mockImplementation(async (): Promise<number> => {
|
|
194
|
+
return 0;
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
webAuthnDeleteBySpy = getJestSpyOn(
|
|
198
|
+
UserWebAuthnService,
|
|
199
|
+
"deleteBy",
|
|
200
|
+
).mockImplementation(async (): Promise<number> => {
|
|
201
|
+
return 0;
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
/*
|
|
205
|
+
* The two probes the DELETED guards used to make. They are stubbed rather
|
|
206
|
+
* than left real so that "was this consulted at all?" is answerable, which
|
|
207
|
+
* is how the removal is held in place: a guard that came back would have to
|
|
208
|
+
* ask one of these two services how many verified factors the user has.
|
|
209
|
+
*/
|
|
210
|
+
totpCountBySpy = getJestSpyOn(UserTotpAuthService, "countBy");
|
|
211
|
+
webAuthnCountBySpy = getJestSpyOn(UserWebAuthnService, "countBy");
|
|
212
|
+
totpFindBySpy = getJestSpyOn(UserTotpAuthService, "findBy");
|
|
213
|
+
webAuthnFindBySpy = getJestSpyOn(UserWebAuthnService, "findBy");
|
|
214
|
+
|
|
215
|
+
for (const spy of [
|
|
216
|
+
totpCountBySpy,
|
|
217
|
+
webAuthnCountBySpy,
|
|
218
|
+
totpFindBySpy,
|
|
219
|
+
webAuthnFindBySpy,
|
|
220
|
+
]) {
|
|
221
|
+
spy.mockImplementation(async (): Promise<any> => {
|
|
222
|
+
return undefined;
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
loggerInfoSpy = getJestSpyOn(logger, "info").mockImplementation(
|
|
227
|
+
(): void => {
|
|
228
|
+
return undefined;
|
|
229
|
+
},
|
|
230
|
+
);
|
|
231
|
+
|
|
232
|
+
/*
|
|
233
|
+
* The deliberate-failure cases below hand a rejection to the CaptureSpan
|
|
234
|
+
* decorator, which records it. That is correct behaviour, and it is also a
|
|
235
|
+
* stack trace on stderr for a test that passed -- silenced so a genuine
|
|
236
|
+
* error in this suite's output is still worth reading.
|
|
237
|
+
*/
|
|
238
|
+
getJestSpyOn(logger, "error").mockImplementation((): void => {
|
|
239
|
+
return undefined;
|
|
240
|
+
});
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
afterEach(() => {
|
|
244
|
+
jest.restoreAllMocks();
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
type ResolveUserFunction = (user: User | null) => void;
|
|
248
|
+
|
|
249
|
+
// Makes the next (and every subsequent) findOneBy return this row.
|
|
250
|
+
const resolveUser: ResolveUserFunction = (user: User | null): void => {
|
|
251
|
+
findOneBySpy.mockImplementation(async (): Promise<User | null> => {
|
|
252
|
+
return user;
|
|
253
|
+
});
|
|
254
|
+
};
|
|
255
|
+
|
|
256
|
+
type FlushMicrotasksFunction = () => Promise<void>;
|
|
257
|
+
|
|
258
|
+
/*
|
|
259
|
+
* Lets every already-scheduled continuation run without advancing anything
|
|
260
|
+
* that is genuinely blocked. Used by the "is it awaited?" cases: after this,
|
|
261
|
+
* a method that did NOT await its dependency has definitely finished.
|
|
262
|
+
*/
|
|
263
|
+
const flushMicrotasks: FlushMicrotasksFunction = async (): Promise<void> => {
|
|
264
|
+
await new Promise<void>((resolve: () => void) => {
|
|
265
|
+
setTimeout(resolve, 0);
|
|
266
|
+
});
|
|
267
|
+
};
|
|
268
|
+
|
|
269
|
+
type AuthenticatorProbeSpiesFunction = () => Array<any>;
|
|
270
|
+
|
|
271
|
+
const authenticatorProbeSpies: AuthenticatorProbeSpiesFunction =
|
|
272
|
+
(): Array<any> => {
|
|
273
|
+
return [
|
|
274
|
+
totpCountBySpy,
|
|
275
|
+
webAuthnCountBySpy,
|
|
276
|
+
totpFindBySpy,
|
|
277
|
+
webAuthnFindBySpy,
|
|
278
|
+
];
|
|
279
|
+
};
|
|
280
|
+
|
|
281
|
+
describe("setTwoFactorAuthRequired", () => {
|
|
282
|
+
test("enabling writes exactly one column: enableTwoFactorAuth true", async () => {
|
|
283
|
+
/*
|
|
284
|
+
* The key set is pinned rather than only the value, so a later edit
|
|
285
|
+
* cannot smuggle another column into an update whose URL says it is
|
|
286
|
+
* about one flag. This method runs as root, which means every column on
|
|
287
|
+
* `User` is writable through it -- isMasterAdmin, email, password -- and
|
|
288
|
+
* a stray key here would be an admin route that changes something its
|
|
289
|
+
* name does not mention.
|
|
290
|
+
*/
|
|
291
|
+
resolveUser(buildUser({ id: foundUserId, enableTwoFactorAuth: false }));
|
|
292
|
+
|
|
293
|
+
await UserService.setTwoFactorAuthRequired({
|
|
294
|
+
userId: userId,
|
|
295
|
+
isRequired: true,
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
expect(updateOneByIdSpy).toHaveBeenCalledTimes(1);
|
|
299
|
+
|
|
300
|
+
const call: any = updateOneByIdSpy.mock.calls[0][0];
|
|
301
|
+
|
|
302
|
+
expect(Object.keys(call.data)).toEqual(["enableTwoFactorAuth"]);
|
|
303
|
+
expect(call.data.enableTwoFactorAuth).toBe(true);
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
test("disabling writes exactly one column: enableTwoFactorAuth false", async () => {
|
|
307
|
+
/*
|
|
308
|
+
* The mirror, so the test above cannot be satisfied by hard-coding true.
|
|
309
|
+
* `false` has to be WRITTEN, not omitted -- an update that simply left the
|
|
310
|
+
* key off would leave the requirement in place while the page reported it
|
|
311
|
+
* lifted.
|
|
312
|
+
*/
|
|
313
|
+
resolveUser(buildUser({ id: foundUserId, enableTwoFactorAuth: true }));
|
|
314
|
+
|
|
315
|
+
await UserService.setTwoFactorAuthRequired({
|
|
316
|
+
userId: userId,
|
|
317
|
+
isRequired: false,
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
expect(updateOneByIdSpy).toHaveBeenCalledTimes(1);
|
|
321
|
+
|
|
322
|
+
const call: any = updateOneByIdSpy.mock.calls[0][0];
|
|
323
|
+
|
|
324
|
+
expect(Object.keys(call.data)).toEqual(["enableTwoFactorAuth"]);
|
|
325
|
+
expect(call.data.enableTwoFactorAuth).toBe(false);
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
test("writes as root, against the id that was actually found", async () => {
|
|
329
|
+
/*
|
|
330
|
+
* Two separate facts. Root, because a master admin acts outside any
|
|
331
|
+
* project and `enableTwoFactorAuth` has an empty update ACL -- without it
|
|
332
|
+
* the write is refused for everyone. And against the FOUND row's id
|
|
333
|
+
* rather than the requested one, so a lookup that ever resolves something
|
|
334
|
+
* else (a soft-delete filter, a tenancy scope) cannot end with this method
|
|
335
|
+
* writing to an id it never confirmed exists.
|
|
336
|
+
*
|
|
337
|
+
* The second fact only exists because the lookup here resolves a row
|
|
338
|
+
* whose id is NOT the requested one. With a fixture that returned the
|
|
339
|
+
* requested id back, `id: data.userId` and `id: user.id!` would produce
|
|
340
|
+
* byte-identical calls and this assertion would be decoration.
|
|
341
|
+
*/
|
|
342
|
+
resolveUser(buildUser({ id: foundUserId, enableTwoFactorAuth: false }));
|
|
343
|
+
|
|
344
|
+
await UserService.setTwoFactorAuthRequired({
|
|
345
|
+
userId: userId,
|
|
346
|
+
isRequired: true,
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
const call: any = updateOneByIdSpy.mock.calls[0][0];
|
|
350
|
+
const violations: Array<string> = [];
|
|
351
|
+
|
|
352
|
+
if (call.id.toString() !== foundUserId.toString()) {
|
|
353
|
+
violations.push(
|
|
354
|
+
`updateOneById targeted ${call.id.toString()}, not the found user ${foundUserId.toString()}`,
|
|
355
|
+
);
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
if (call.props.isRoot !== true) {
|
|
359
|
+
violations.push(
|
|
360
|
+
"updateOneById did not run with props { isRoot: true }",
|
|
361
|
+
);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
expect(violations).toEqual([]);
|
|
365
|
+
|
|
366
|
+
// The lookup itself is a root read of the requested id.
|
|
367
|
+
expect(findOneBySpy.mock.calls[0][0].query).toEqual({ _id: userId });
|
|
368
|
+
expect(findOneBySpy.mock.calls[0][0].props).toEqual({ isRoot: true });
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
test("ENABLING revokes every session, with a reason short enough to store", async () => {
|
|
372
|
+
/*
|
|
373
|
+
* Two failures in one place, and the second is invisible until it fires.
|
|
374
|
+
*
|
|
375
|
+
* `getUserMiddleware` validates an access token statelessly and never
|
|
376
|
+
* looks at `UserSession`, so somebody already signed in keeps working for
|
|
377
|
+
* the rest of that token's life whatever is written here. Revoking is what
|
|
378
|
+
* stops POST /refresh-token -- which DOES check `isRevoked` -- from
|
|
379
|
+
* renewing it forever. Without it, "require two factor auth" is a promise
|
|
380
|
+
* the product does not keep for anybody currently signed in.
|
|
381
|
+
*
|
|
382
|
+
* And `UserSession.revokedReason` is a ShortText column. A reason longer
|
|
383
|
+
* than that fails the very UPDATE that is supposed to be closing the
|
|
384
|
+
* window, so a well-meant, more descriptive string would turn a security
|
|
385
|
+
* action into an exception -- asserted against ColumnLength itself rather
|
|
386
|
+
* than a copied number, so a change to the column moves this with it.
|
|
387
|
+
*/
|
|
388
|
+
resolveUser(buildUser({ id: foundUserId, enableTwoFactorAuth: false }));
|
|
389
|
+
|
|
390
|
+
await UserService.setTwoFactorAuthRequired({
|
|
391
|
+
userId: userId,
|
|
392
|
+
isRequired: true,
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
expect(revokeSessionsSpy).toHaveBeenCalledTimes(1);
|
|
396
|
+
|
|
397
|
+
const revokedUserId: any = revokeSessionsSpy.mock.calls[0][0];
|
|
398
|
+
const options: any = revokeSessionsSpy.mock.calls[0][1];
|
|
399
|
+
|
|
400
|
+
expect(revokedUserId.toString()).toBe(foundUserId.toString());
|
|
401
|
+
|
|
402
|
+
const reason: string = options.reason as string;
|
|
403
|
+
|
|
404
|
+
expect(typeof reason).toBe("string");
|
|
405
|
+
expect(reason.length).toBeGreaterThan(0);
|
|
406
|
+
expect(reason.length).toBeLessThanOrEqual(ColumnLength.ShortText);
|
|
407
|
+
});
|
|
408
|
+
|
|
409
|
+
test("DISABLING does not revoke sessions", async () => {
|
|
410
|
+
/*
|
|
411
|
+
* The asymmetry is deliberate, not an oversight. Relaxing a requirement
|
|
412
|
+
* opens no window -- nobody's access got wider because they were asked for
|
|
413
|
+
* LESS -- and signing the whole company out to give them less to do would
|
|
414
|
+
* be gratuitous. Somebody "tidying up" this method into a symmetric
|
|
415
|
+
* revoke-either-way would log every user of a project out for a change
|
|
416
|
+
* that costs them nothing.
|
|
417
|
+
*/
|
|
418
|
+
resolveUser(buildUser({ id: foundUserId, enableTwoFactorAuth: true }));
|
|
419
|
+
|
|
420
|
+
await UserService.setTwoFactorAuthRequired({
|
|
421
|
+
userId: userId,
|
|
422
|
+
isRequired: false,
|
|
423
|
+
});
|
|
424
|
+
|
|
425
|
+
expect(updateOneByIdSpy).toHaveBeenCalledTimes(1);
|
|
426
|
+
expect(revokeSessionsSpy).not.toHaveBeenCalled();
|
|
427
|
+
});
|
|
428
|
+
|
|
429
|
+
test("does not resolve until the revocation has actually settled", async () => {
|
|
430
|
+
/*
|
|
431
|
+
* A fire-and-forget revocation would still call the spy, so every
|
|
432
|
+
* assertion above would keep passing while the guarantee evaporated: the
|
|
433
|
+
* route would answer 200, the UI would say "required", and the revocation
|
|
434
|
+
* could fail afterwards with nobody listening. Worse, the admin's next
|
|
435
|
+
* action (a reset, say) would race a revocation still in flight.
|
|
436
|
+
*
|
|
437
|
+
* So the await is pinned directly, by blocking the revocation on a gate
|
|
438
|
+
* this test holds and checking the method has not finished.
|
|
439
|
+
*/
|
|
440
|
+
resolveUser(buildUser({ id: foundUserId, enableTwoFactorAuth: false }));
|
|
441
|
+
|
|
442
|
+
let releaseRevocation: () => void = (): void => {
|
|
443
|
+
return undefined;
|
|
444
|
+
};
|
|
445
|
+
|
|
446
|
+
const revocationGate: Promise<void> = new Promise<void>(
|
|
447
|
+
(resolve: () => void) => {
|
|
448
|
+
releaseRevocation = resolve;
|
|
449
|
+
},
|
|
450
|
+
);
|
|
451
|
+
|
|
452
|
+
revokeSessionsSpy.mockImplementation(async (): Promise<void> => {
|
|
453
|
+
await revocationGate;
|
|
454
|
+
});
|
|
455
|
+
|
|
456
|
+
let settled: boolean = false;
|
|
457
|
+
|
|
458
|
+
const pending: Promise<void> = UserService.setTwoFactorAuthRequired({
|
|
459
|
+
userId: userId,
|
|
460
|
+
isRequired: true,
|
|
461
|
+
}).then((): void => {
|
|
462
|
+
settled = true;
|
|
463
|
+
});
|
|
464
|
+
|
|
465
|
+
await flushMicrotasks();
|
|
466
|
+
|
|
467
|
+
expect(revokeSessionsSpy).toHaveBeenCalledTimes(1);
|
|
468
|
+
expect(settled).toBe(false);
|
|
469
|
+
|
|
470
|
+
releaseRevocation();
|
|
471
|
+
await pending;
|
|
472
|
+
|
|
473
|
+
expect(settled).toBe(true);
|
|
474
|
+
});
|
|
475
|
+
|
|
476
|
+
test("throws NotFoundException for a user that does not exist, writing and revoking nothing", async () => {
|
|
477
|
+
/*
|
|
478
|
+
* A missing row must not degrade into a no-op that reports success: an
|
|
479
|
+
* operator acting on a mistyped or stale id would be told two factor auth
|
|
480
|
+
* is now required of somebody, and nobody would be any safer. And nothing
|
|
481
|
+
* may run before the row is confirmed -- an update keyed on an id that
|
|
482
|
+
* does not exist is silent, but a revocation is not, and it would sign out
|
|
483
|
+
* whoever that id belongs to.
|
|
484
|
+
*/
|
|
485
|
+
resolveUser(null);
|
|
486
|
+
|
|
487
|
+
await expect(
|
|
488
|
+
UserService.setTwoFactorAuthRequired({
|
|
489
|
+
userId: userId,
|
|
490
|
+
isRequired: true,
|
|
491
|
+
}),
|
|
492
|
+
).rejects.toThrow(NotFoundException);
|
|
493
|
+
|
|
494
|
+
expect(updateOneByIdSpy).not.toHaveBeenCalled();
|
|
495
|
+
expect(revokeSessionsSpy).not.toHaveBeenCalled();
|
|
496
|
+
});
|
|
497
|
+
|
|
498
|
+
test("logs the change, naming the user it was made against", async () => {
|
|
499
|
+
/*
|
|
500
|
+
* This is one human changing how another human signs in. The user id has
|
|
501
|
+
* to be in the line, or the log records that SOMEBODY's two factor auth
|
|
502
|
+
* requirement changed and an incident review has nothing to go on.
|
|
503
|
+
*/
|
|
504
|
+
resolveUser(buildUser({ id: foundUserId, enableTwoFactorAuth: false }));
|
|
505
|
+
|
|
506
|
+
await UserService.setTwoFactorAuthRequired({
|
|
507
|
+
userId: userId,
|
|
508
|
+
isRequired: true,
|
|
509
|
+
});
|
|
510
|
+
|
|
511
|
+
const lines: Array<string> = loggerInfoSpy.mock.calls.map(
|
|
512
|
+
(call: Array<unknown>): string => {
|
|
513
|
+
return String(call[0]);
|
|
514
|
+
},
|
|
515
|
+
);
|
|
516
|
+
|
|
517
|
+
const namingLines: Array<string> = lines.filter(
|
|
518
|
+
(line: string): boolean => {
|
|
519
|
+
return line.includes(foundUserId.toString());
|
|
520
|
+
},
|
|
521
|
+
);
|
|
522
|
+
|
|
523
|
+
expect(namingLines.length).toBeGreaterThan(0);
|
|
524
|
+
});
|
|
525
|
+
|
|
526
|
+
test("REGRESSION: enabling succeeds for a user with ZERO verified factors", async () => {
|
|
527
|
+
/*
|
|
528
|
+
* THE CASE THE WHOLE FEATURE TURNS ON, and the one that used to be
|
|
529
|
+
* impossible.
|
|
530
|
+
*
|
|
531
|
+
* `UserService.onBeforeUpdate` used to refuse any update setting
|
|
532
|
+
* `enableTwoFactorAuth` on an account with no verified authenticator --
|
|
533
|
+
* "Please verify two factor authentication method before you enable two
|
|
534
|
+
* factor authentication." That guard fired for exactly the users an admin
|
|
535
|
+
* mandate exists for: the ones who have never set two factor auth up. An
|
|
536
|
+
* admin could therefore only require it from people who had already
|
|
537
|
+
* volunteered, which is the opposite of what a mandate is.
|
|
538
|
+
*
|
|
539
|
+
* The guard is gone because its premise dissolved: login now sends an
|
|
540
|
+
* account in this state through ENROLMENT rather than refusing it.
|
|
541
|
+
*
|
|
542
|
+
* The update is routed through the real hook here on purpose. Stubbing
|
|
543
|
+
* `updateOneById` alone would make this pass whether the guard exists or
|
|
544
|
+
* not -- the guard lives inside the write path, so the write path is what
|
|
545
|
+
* has to run.
|
|
546
|
+
*/
|
|
547
|
+
resolveUser(buildUser({ id: foundUserId, enableTwoFactorAuth: false }));
|
|
548
|
+
|
|
549
|
+
updateOneByIdSpy.mockImplementation(
|
|
550
|
+
async (update: any): Promise<void> => {
|
|
551
|
+
await (
|
|
552
|
+
UserService as unknown as {
|
|
553
|
+
onBeforeUpdate: (updateBy: unknown) => Promise<unknown>;
|
|
554
|
+
}
|
|
555
|
+
).onBeforeUpdate({
|
|
556
|
+
query: { _id: update.id },
|
|
557
|
+
data: update.data,
|
|
558
|
+
props: update.props,
|
|
559
|
+
limit: 1,
|
|
560
|
+
skip: 0,
|
|
561
|
+
});
|
|
562
|
+
},
|
|
563
|
+
);
|
|
564
|
+
|
|
565
|
+
await expect(
|
|
566
|
+
UserService.setTwoFactorAuthRequired({
|
|
567
|
+
userId: userId,
|
|
568
|
+
isRequired: true,
|
|
569
|
+
}),
|
|
570
|
+
).resolves.toBeUndefined();
|
|
571
|
+
|
|
572
|
+
expect(updateOneByIdSpy).toHaveBeenCalledTimes(1);
|
|
573
|
+
expect(revokeSessionsSpy).toHaveBeenCalledTimes(1);
|
|
574
|
+
});
|
|
575
|
+
|
|
576
|
+
test("enabling for a user who already has it enabled is idempotent", async () => {
|
|
577
|
+
/*
|
|
578
|
+
* Bulk "require two factor auth" over a selection will always include
|
|
579
|
+
* people who already have it. Short-circuiting on "no change" would look
|
|
580
|
+
* like an optimisation and would quietly drop the session revocation --
|
|
581
|
+
* the half of this method that is doing security work -- for precisely the
|
|
582
|
+
* users an operator was re-asserting the requirement over.
|
|
583
|
+
*/
|
|
584
|
+
resolveUser(buildUser({ id: foundUserId, enableTwoFactorAuth: true }));
|
|
585
|
+
|
|
586
|
+
await UserService.setTwoFactorAuthRequired({
|
|
587
|
+
userId: userId,
|
|
588
|
+
isRequired: true,
|
|
589
|
+
});
|
|
590
|
+
|
|
591
|
+
expect(updateOneByIdSpy).toHaveBeenCalledTimes(1);
|
|
592
|
+
expect(updateOneByIdSpy.mock.calls[0][0].data.enableTwoFactorAuth).toBe(
|
|
593
|
+
true,
|
|
594
|
+
);
|
|
595
|
+
expect(revokeSessionsSpy).toHaveBeenCalledTimes(1);
|
|
596
|
+
});
|
|
597
|
+
});
|
|
598
|
+
|
|
599
|
+
describe("resetTwoFactorAuth", () => {
|
|
600
|
+
type DeleteCallForFunction = (spy: any) => any;
|
|
601
|
+
|
|
602
|
+
const deleteCallFor: DeleteCallForFunction = (spy: any): any => {
|
|
603
|
+
return spy.mock.calls[0][0];
|
|
604
|
+
};
|
|
605
|
+
|
|
606
|
+
test("deletes every authenticator app row for the user, as root, unbounded", async () => {
|
|
607
|
+
/*
|
|
608
|
+
* `LIMIT_MAX` rather than a page: a partial delete leaves the user holding
|
|
609
|
+
* an authenticator that still works, so "reset because the phone was
|
|
610
|
+
* stolen" would leave the thief's factor in place. Root because the
|
|
611
|
+
* operator is not the row's owner and has no project-scoped path to it.
|
|
612
|
+
*/
|
|
613
|
+
resolveUser(buildUser({ id: foundUserId, enableTwoFactorAuth: true }));
|
|
614
|
+
|
|
615
|
+
await UserService.resetTwoFactorAuth({ userId: userId });
|
|
616
|
+
|
|
617
|
+
expect(totpDeleteBySpy).toHaveBeenCalledTimes(1);
|
|
618
|
+
|
|
619
|
+
const call: any = deleteCallFor(totpDeleteBySpy);
|
|
620
|
+
|
|
621
|
+
expect(call.query).toEqual({ userId: foundUserId });
|
|
622
|
+
expect(call.props).toEqual({ isRoot: true });
|
|
623
|
+
expect(call.limit).toBe(LIMIT_MAX);
|
|
624
|
+
expect(call.skip).toBe(0);
|
|
625
|
+
});
|
|
626
|
+
|
|
627
|
+
test("deletes every security key row for the user, in the same shape", async () => {
|
|
628
|
+
/*
|
|
629
|
+
* Both tables or neither. Clearing only the TOTP rows would report a
|
|
630
|
+
* successful reset while the user's security key still satisfies login --
|
|
631
|
+
* which is fine for a lost phone and useless for a lost laptop, and the
|
|
632
|
+
* operator cannot tell the two apart from the button they pressed.
|
|
633
|
+
*/
|
|
634
|
+
resolveUser(buildUser({ id: foundUserId, enableTwoFactorAuth: true }));
|
|
635
|
+
|
|
636
|
+
await UserService.resetTwoFactorAuth({ userId: userId });
|
|
637
|
+
|
|
638
|
+
expect(webAuthnDeleteBySpy).toHaveBeenCalledTimes(1);
|
|
639
|
+
|
|
640
|
+
const call: any = deleteCallFor(webAuthnDeleteBySpy);
|
|
641
|
+
|
|
642
|
+
expect(call.query).toEqual({ userId: foundUserId });
|
|
643
|
+
expect(call.props).toEqual({ isRoot: true });
|
|
644
|
+
expect(call.limit).toBe(LIMIT_MAX);
|
|
645
|
+
expect(call.skip).toBe(0);
|
|
646
|
+
});
|
|
647
|
+
|
|
648
|
+
test("the delete queries carry NO isVerified predicate", async () => {
|
|
649
|
+
/*
|
|
650
|
+
* The tempting-but-wrong filter. `countVerifiedTwoFactorAuthMethods`
|
|
651
|
+
* three methods up DOES filter on `isVerified: true`, so copying that
|
|
652
|
+
* shape down here reads as consistency -- and it is the bug.
|
|
653
|
+
*
|
|
654
|
+
* `UserTotpAuthService.onBeforeCreate` writes `isVerified: false` and only
|
|
655
|
+
* the enrolment step flips it, so every abandoned QR scan leaves a row
|
|
656
|
+
* behind forever. A reset that skipped those would hand the user back the
|
|
657
|
+
* very QR code they could not finish rather than a fresh one, which is
|
|
658
|
+
* exactly the thing they came for help about.
|
|
659
|
+
*
|
|
660
|
+
* Asserted as the exact query object, not "isVerified is not true": a
|
|
661
|
+
* `isVerified: false` predicate would be just as wrong in the other
|
|
662
|
+
* direction, deleting the abandoned rows and keeping the working one.
|
|
663
|
+
*/
|
|
664
|
+
resolveUser(buildUser({ id: foundUserId, enableTwoFactorAuth: true }));
|
|
665
|
+
|
|
666
|
+
await UserService.resetTwoFactorAuth({ userId: userId });
|
|
667
|
+
|
|
668
|
+
const violations: Array<string> = [];
|
|
669
|
+
|
|
670
|
+
for (const spy of [totpDeleteBySpy, webAuthnDeleteBySpy]) {
|
|
671
|
+
const query: any = deleteCallFor(spy).query;
|
|
672
|
+
const keys: Array<string> = Object.keys(query);
|
|
673
|
+
|
|
674
|
+
if (keys.length !== 1 || keys[0] !== "userId") {
|
|
675
|
+
violations.push(
|
|
676
|
+
`a reset delete queried on [${keys.join(", ")}] rather than on userId alone`,
|
|
677
|
+
);
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
expect(violations).toEqual([]);
|
|
682
|
+
});
|
|
683
|
+
|
|
684
|
+
test("does NOT touch enableTwoFactorAuth", async () => {
|
|
685
|
+
/*
|
|
686
|
+
* "Reset" means clear the configuration and KEEP the requirement: the
|
|
687
|
+
* account lands on a new QR code at its next sign-in, which is the whole
|
|
688
|
+
* point. Clearing the flag as well would answer "my phone is gone" by
|
|
689
|
+
* silently taking the account off two factor auth -- a downgrade nobody
|
|
690
|
+
* asked for, on a page whose other button is literally "require two factor
|
|
691
|
+
* auth", and an operator helping with a lost phone would have no reason to
|
|
692
|
+
* notice.
|
|
693
|
+
*
|
|
694
|
+
* Asserted as "no user update at all", which is the stronger and simpler
|
|
695
|
+
* fact: this method has no business writing to the User row.
|
|
696
|
+
*
|
|
697
|
+
* BOTH write entry points are checked, not just the one this method
|
|
698
|
+
* happens to use. Asserting only `updateOneById` would leave the claim
|
|
699
|
+
* resting on a method name: a rewrite reaching for `updateBy` directly --
|
|
700
|
+
* the natural shape if somebody ever wanted to clear the flag for a whole
|
|
701
|
+
* selection at once -- would clear `enableTwoFactorAuth` in exactly the
|
|
702
|
+
* way this test exists to forbid and would not be seen at all.
|
|
703
|
+
*/
|
|
704
|
+
resolveUser(buildUser({ id: foundUserId, enableTwoFactorAuth: true }));
|
|
705
|
+
|
|
706
|
+
await UserService.resetTwoFactorAuth({ userId: userId });
|
|
707
|
+
|
|
708
|
+
const writeEntryPoints: Array<NamedSpy> = [
|
|
709
|
+
{ name: "updateOneById", spy: updateOneByIdSpy },
|
|
710
|
+
{ name: "updateBy", spy: updateBySpy },
|
|
711
|
+
];
|
|
712
|
+
|
|
713
|
+
const violations: Array<string> = [];
|
|
714
|
+
|
|
715
|
+
for (const entryPoint of writeEntryPoints) {
|
|
716
|
+
for (const call of entryPoint.spy.mock.calls) {
|
|
717
|
+
const keys: Array<string> = Object.keys(call[0]?.data || {});
|
|
718
|
+
|
|
719
|
+
violations.push(
|
|
720
|
+
`resetTwoFactorAuth wrote to the User row via ${entryPoint.name} with data keys [${keys.join(", ")}]`,
|
|
721
|
+
);
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
expect(violations).toEqual([]);
|
|
726
|
+
});
|
|
727
|
+
|
|
728
|
+
test("revokes every session, with a reason short enough to store", async () => {
|
|
729
|
+
/*
|
|
730
|
+
* A session that survives a reset is a session that never has to prove the
|
|
731
|
+
* second factor again -- so "reset because the device was stolen" would do
|
|
732
|
+
* nothing at all about the person holding the stolen device, while
|
|
733
|
+
* reporting success.
|
|
734
|
+
*
|
|
735
|
+
* Same ShortText bound as the enable path, for the same reason: an
|
|
736
|
+
* over-long reason fails the write that closes the window.
|
|
737
|
+
*/
|
|
738
|
+
resolveUser(buildUser({ id: foundUserId, enableTwoFactorAuth: true }));
|
|
739
|
+
|
|
740
|
+
await UserService.resetTwoFactorAuth({ userId: userId });
|
|
741
|
+
|
|
742
|
+
expect(revokeSessionsSpy).toHaveBeenCalledTimes(1);
|
|
743
|
+
|
|
744
|
+
const revokedUserId: any = revokeSessionsSpy.mock.calls[0][0];
|
|
745
|
+
const reason: string = revokeSessionsSpy.mock.calls[0][1]
|
|
746
|
+
.reason as string;
|
|
747
|
+
|
|
748
|
+
expect(revokedUserId.toString()).toBe(foundUserId.toString());
|
|
749
|
+
expect(typeof reason).toBe("string");
|
|
750
|
+
expect(reason.length).toBeGreaterThan(0);
|
|
751
|
+
expect(reason.length).toBeLessThanOrEqual(ColumnLength.ShortText);
|
|
752
|
+
});
|
|
753
|
+
|
|
754
|
+
test("both deletes complete BEFORE the revocation", async () => {
|
|
755
|
+
/*
|
|
756
|
+
* Ordering, not just presence. Revoking first would open a window between
|
|
757
|
+
* the revocation and the deletes in which the user (or whoever has their
|
|
758
|
+
* device) can sign in again using the authenticator that is about to be
|
|
759
|
+
* removed -- and come out the other side with a live session and no
|
|
760
|
+
* pending enrolment. The window is small and entirely real: the deletes
|
|
761
|
+
* are two round trips.
|
|
762
|
+
*/
|
|
763
|
+
resolveUser(buildUser({ id: foundUserId, enableTwoFactorAuth: true }));
|
|
764
|
+
|
|
765
|
+
await UserService.resetTwoFactorAuth({ userId: userId });
|
|
766
|
+
|
|
767
|
+
const totpOrder: number = totpDeleteBySpy.mock
|
|
768
|
+
.invocationCallOrder[0] as number;
|
|
769
|
+
const webAuthnOrder: number = webAuthnDeleteBySpy.mock
|
|
770
|
+
.invocationCallOrder[0] as number;
|
|
771
|
+
const revokeOrder: number = revokeSessionsSpy.mock
|
|
772
|
+
.invocationCallOrder[0] as number;
|
|
773
|
+
|
|
774
|
+
const violations: Array<string> = [];
|
|
775
|
+
|
|
776
|
+
if (!(totpOrder < revokeOrder)) {
|
|
777
|
+
violations.push(
|
|
778
|
+
"sessions were revoked before the authenticator app rows were deleted",
|
|
779
|
+
);
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
if (!(webAuthnOrder < revokeOrder)) {
|
|
783
|
+
violations.push(
|
|
784
|
+
"sessions were revoked before the security key rows were deleted",
|
|
785
|
+
);
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
expect(violations).toEqual([]);
|
|
789
|
+
});
|
|
790
|
+
|
|
791
|
+
test("throws NotFoundException for a user that does not exist, deleting nothing", async () => {
|
|
792
|
+
/*
|
|
793
|
+
* The delete queries are keyed on `userId`, so a missing row would make
|
|
794
|
+
* them harmless no-ops -- which is precisely why the check has to be
|
|
795
|
+
* explicit. Without it an operator on a mistyped id is told the reset
|
|
796
|
+
* worked, walks away, and the person who actually needs it is still
|
|
797
|
+
* locked out.
|
|
798
|
+
*/
|
|
799
|
+
resolveUser(null);
|
|
800
|
+
|
|
801
|
+
await expect(
|
|
802
|
+
UserService.resetTwoFactorAuth({ userId: userId }),
|
|
803
|
+
).rejects.toThrow(NotFoundException);
|
|
804
|
+
|
|
805
|
+
expect(totpDeleteBySpy).not.toHaveBeenCalled();
|
|
806
|
+
expect(webAuthnDeleteBySpy).not.toHaveBeenCalled();
|
|
807
|
+
expect(revokeSessionsSpy).not.toHaveBeenCalled();
|
|
808
|
+
});
|
|
809
|
+
|
|
810
|
+
test("is a successful no-op for a user with no factors at all", async () => {
|
|
811
|
+
/*
|
|
812
|
+
* The operator pressing this button does not know what the user has set
|
|
813
|
+
* up -- that is why they are on the page. Refusing "there is nothing to
|
|
814
|
+
* reset" would turn an idempotent repair into an error the operator has to
|
|
815
|
+
* interpret, and it is also the state a half-finished previous reset
|
|
816
|
+
* leaves behind, where pressing it again is the obvious thing to do.
|
|
817
|
+
*/
|
|
818
|
+
resolveUser(buildUser({ id: foundUserId, enableTwoFactorAuth: true }));
|
|
819
|
+
|
|
820
|
+
totpDeleteBySpy.mockImplementation(async (): Promise<number> => {
|
|
821
|
+
return 0;
|
|
822
|
+
});
|
|
823
|
+
|
|
824
|
+
webAuthnDeleteBySpy.mockImplementation(async (): Promise<number> => {
|
|
825
|
+
return 0;
|
|
826
|
+
});
|
|
827
|
+
|
|
828
|
+
await expect(
|
|
829
|
+
UserService.resetTwoFactorAuth({ userId: userId }),
|
|
830
|
+
).resolves.toBeUndefined();
|
|
831
|
+
|
|
832
|
+
expect(totpDeleteBySpy).toHaveBeenCalledTimes(1);
|
|
833
|
+
expect(webAuthnDeleteBySpy).toHaveBeenCalledTimes(1);
|
|
834
|
+
expect(revokeSessionsSpy).toHaveBeenCalledTimes(1);
|
|
835
|
+
});
|
|
836
|
+
|
|
837
|
+
test("resets an Enabled - Pending Setup account with one abandoned enrolment", async () => {
|
|
838
|
+
/*
|
|
839
|
+
* The state an admin mandate creates, plus one QR code the user started
|
|
840
|
+
* and never finished. Nothing is verified, so any implementation that
|
|
841
|
+
* reasoned about verified factors -- a guard refusing to remove the "last"
|
|
842
|
+
* one, or a filtered delete -- behaves differently here than in the
|
|
843
|
+
* ordinary case, and this is the account least able to absorb a mistake:
|
|
844
|
+
* it is already unable to sign in without completing enrolment.
|
|
845
|
+
*/
|
|
846
|
+
resolveUser(buildUser({ id: foundUserId, enableTwoFactorAuth: true }));
|
|
847
|
+
|
|
848
|
+
totpDeleteBySpy.mockImplementation(async (): Promise<number> => {
|
|
849
|
+
// One unverified row cleared.
|
|
850
|
+
return 1;
|
|
851
|
+
});
|
|
852
|
+
|
|
853
|
+
await expect(
|
|
854
|
+
UserService.resetTwoFactorAuth({ userId: userId }),
|
|
855
|
+
).resolves.toBeUndefined();
|
|
856
|
+
|
|
857
|
+
expect(deleteCallFor(totpDeleteBySpy).query).toEqual({
|
|
858
|
+
userId: foundUserId,
|
|
859
|
+
});
|
|
860
|
+
expect(updateOneByIdSpy).not.toHaveBeenCalled();
|
|
861
|
+
expect(revokeSessionsSpy).toHaveBeenCalledTimes(1);
|
|
862
|
+
});
|
|
863
|
+
});
|
|
864
|
+
|
|
865
|
+
describe("the removed hooks", () => {
|
|
866
|
+
type CallOnBeforeUpdateFunction = (updateBy: unknown) => Promise<any>;
|
|
867
|
+
|
|
868
|
+
/*
|
|
869
|
+
* `onBeforeUpdate` is protected, so it is reached through a cast. Driving
|
|
870
|
+
* the hook directly is the point: it is where the deleted guard lived, and
|
|
871
|
+
* a test that went through `updateOneById` would be testing a stub.
|
|
872
|
+
*/
|
|
873
|
+
const callOnBeforeUpdate: CallOnBeforeUpdateFunction = async (
|
|
874
|
+
updateBy: unknown,
|
|
875
|
+
): Promise<any> => {
|
|
876
|
+
return await (
|
|
877
|
+
UserService as unknown as {
|
|
878
|
+
onBeforeUpdate: (u: unknown) => Promise<unknown>;
|
|
879
|
+
}
|
|
880
|
+
).onBeforeUpdate(updateBy);
|
|
881
|
+
};
|
|
882
|
+
|
|
883
|
+
/*
|
|
884
|
+
* ---------------------------------------------------------------------
|
|
885
|
+
* The guard that REPLACED the deleted one, and the reason it had to
|
|
886
|
+
* exist at all.
|
|
887
|
+
*
|
|
888
|
+
* `enableTwoFactorAuth` carries `update: [Permission.CurrentUser]`
|
|
889
|
+
* (Common/Models/DatabaseModels/User.ts) and the User table's row ACL
|
|
890
|
+
* scopes updates to the caller's own id, so every signed-in user can
|
|
891
|
+
* write their own copy of this flag through the ordinary CRUD API -- and
|
|
892
|
+
* the product ships the button that does it, at Dashboard > Profile >
|
|
893
|
+
* Two Factor Authentication.
|
|
894
|
+
*
|
|
895
|
+
* Without the guard the whole feature is self-undoing: the admin requires
|
|
896
|
+
* two factor auth, the user is marched through enrolment at their next
|
|
897
|
+
* sign-in, and then from the session they just earned they flip the
|
|
898
|
+
* toggle back off and delete the factor -- which the removal of the
|
|
899
|
+
* onBeforeDelete guards now also permits. The account is password-only
|
|
900
|
+
* again and the Authentication page reports the mandate as simply absent.
|
|
901
|
+
* ---------------------------------------------------------------------
|
|
902
|
+
*/
|
|
903
|
+
test("onBeforeUpdate refuses to let a non-root caller turn the requirement OFF", async () => {
|
|
904
|
+
await expect(
|
|
905
|
+
callOnBeforeUpdate({
|
|
906
|
+
query: { _id: userId },
|
|
907
|
+
data: { enableTwoFactorAuth: false },
|
|
908
|
+
props: {},
|
|
909
|
+
}),
|
|
910
|
+
).rejects.toThrow(BadDataException);
|
|
911
|
+
});
|
|
912
|
+
|
|
913
|
+
test("the refusal names an administrator, so the user knows who to ask", async () => {
|
|
914
|
+
/*
|
|
915
|
+
* The message is the only thing the user sees when the profile toggle
|
|
916
|
+
* refuses. "Bad data" would send them to support with nothing to say.
|
|
917
|
+
*/
|
|
918
|
+
await expect(
|
|
919
|
+
callOnBeforeUpdate({
|
|
920
|
+
query: { _id: userId },
|
|
921
|
+
data: { enableTwoFactorAuth: false },
|
|
922
|
+
props: {},
|
|
923
|
+
}),
|
|
924
|
+
).rejects.toThrow(/administrator/i);
|
|
925
|
+
});
|
|
926
|
+
|
|
927
|
+
test("a ROOT caller may turn the requirement off -- that is the admin endpoint", async () => {
|
|
928
|
+
/*
|
|
929
|
+
* setTwoFactorAuthRequired({ isRequired: false }) writes with
|
|
930
|
+
* props.isRoot, so the guard must not catch it. If it did, the admin's
|
|
931
|
+
* "Do Not Require" button would fail for every user.
|
|
932
|
+
*/
|
|
933
|
+
await expect(
|
|
934
|
+
callOnBeforeUpdate({
|
|
935
|
+
query: { _id: userId },
|
|
936
|
+
data: { enableTwoFactorAuth: false },
|
|
937
|
+
props: { isRoot: true },
|
|
938
|
+
}),
|
|
939
|
+
).resolves.toBeDefined();
|
|
940
|
+
});
|
|
941
|
+
|
|
942
|
+
test("a non-root caller may still turn the requirement ON for themselves", async () => {
|
|
943
|
+
/*
|
|
944
|
+
* Self-service enrolment stays. The guard is deliberately one-way: it
|
|
945
|
+
* bounds who can WEAKEN an account, not who can strengthen one.
|
|
946
|
+
*/
|
|
947
|
+
await expect(
|
|
948
|
+
callOnBeforeUpdate({
|
|
949
|
+
query: { _id: userId },
|
|
950
|
+
data: { enableTwoFactorAuth: true },
|
|
951
|
+
props: {},
|
|
952
|
+
}),
|
|
953
|
+
).resolves.toBeDefined();
|
|
954
|
+
});
|
|
955
|
+
|
|
956
|
+
test("an unrelated non-root update is untouched by the guard", async () => {
|
|
957
|
+
/*
|
|
958
|
+
* The guard keys on the value being exactly `false`, so an update that
|
|
959
|
+
* does not mention the column at all must sail through -- otherwise
|
|
960
|
+
* every profile edit in the product starts failing.
|
|
961
|
+
*/
|
|
962
|
+
await expect(
|
|
963
|
+
callOnBeforeUpdate({
|
|
964
|
+
query: { _id: userId },
|
|
965
|
+
data: { name: "Someone" },
|
|
966
|
+
props: {},
|
|
967
|
+
}),
|
|
968
|
+
).resolves.toBeDefined();
|
|
969
|
+
});
|
|
970
|
+
|
|
971
|
+
test("onBeforeUpdate allows enableTwoFactorAuth: true for a user with no verified factor", async () => {
|
|
972
|
+
/*
|
|
973
|
+
* The deleted guard, asserted at its own level.
|
|
974
|
+
*
|
|
975
|
+
* It threw "Please verify two factor authentication method before you
|
|
976
|
+
* enable two factor authentication." for exactly this update, which made
|
|
977
|
+
* an admin mandate impossible for the users it was meant for -- and would
|
|
978
|
+
* ALSO have made the Login enrolment flow's own write fail, since that
|
|
979
|
+
* write happens before any factor is verified.
|
|
980
|
+
*/
|
|
981
|
+
await expect(
|
|
982
|
+
callOnBeforeUpdate({
|
|
983
|
+
query: { _id: userId },
|
|
984
|
+
data: { enableTwoFactorAuth: true },
|
|
985
|
+
props: { isRoot: true },
|
|
986
|
+
limit: 1,
|
|
987
|
+
skip: 0,
|
|
988
|
+
}),
|
|
989
|
+
).resolves.toBeDefined();
|
|
990
|
+
});
|
|
991
|
+
|
|
992
|
+
test("that same update issues no probes into the authenticator tables", async () => {
|
|
993
|
+
/*
|
|
994
|
+
* The guard's fingerprint, and a second line of defence behind the test
|
|
995
|
+
* above. A resurrected guard has to ask one of these two services how
|
|
996
|
+
* many factors the user has before it can decide to throw, so "no probe
|
|
997
|
+
* was made" catches a reinstated check even in a form that happens to
|
|
998
|
+
* let this particular update through.
|
|
999
|
+
*
|
|
1000
|
+
* Note the spy list below covers `countBy` and `findBy`, NOT `findOneBy`
|
|
1001
|
+
* -- and the original guard probed with `findOneBy`, which
|
|
1002
|
+
* DatabaseService routes through a private `_findBy` rather than the
|
|
1003
|
+
* public method spied here. So this assertion alone would NOT have
|
|
1004
|
+
* caught the guard in its original form. What catches that is the
|
|
1005
|
+
* non-empty `findBy` stub in beforeEach, which lets the restored guard
|
|
1006
|
+
* actually reach its throw.
|
|
1007
|
+
*
|
|
1008
|
+
* It is also a cost assertion in its own right: `onBeforeUpdate` runs on
|
|
1009
|
+
* EVERY user update in the product -- lastActive being written on every
|
|
1010
|
+
* authenticated request included -- so two extra COUNT queries here would
|
|
1011
|
+
* be two extra queries on one of the hottest paths there is.
|
|
1012
|
+
*/
|
|
1013
|
+
await callOnBeforeUpdate({
|
|
1014
|
+
query: { _id: userId },
|
|
1015
|
+
data: { enableTwoFactorAuth: true },
|
|
1016
|
+
props: { isRoot: true },
|
|
1017
|
+
limit: 1,
|
|
1018
|
+
skip: 0,
|
|
1019
|
+
});
|
|
1020
|
+
|
|
1021
|
+
const violations: Array<string> = [];
|
|
1022
|
+
|
|
1023
|
+
for (const spy of authenticatorProbeSpies()) {
|
|
1024
|
+
if (spy.mock.calls.length > 0) {
|
|
1025
|
+
violations.push(
|
|
1026
|
+
`onBeforeUpdate made ${spy.mock.calls.length} call(s) into an authenticator service`,
|
|
1027
|
+
);
|
|
1028
|
+
}
|
|
1029
|
+
}
|
|
1030
|
+
|
|
1031
|
+
expect(violations).toEqual([]);
|
|
1032
|
+
expect(findBySpy).not.toHaveBeenCalled();
|
|
1033
|
+
});
|
|
1034
|
+
|
|
1035
|
+
test("onBeforeUpdate still carries the affected users forward for a password update", async () => {
|
|
1036
|
+
/*
|
|
1037
|
+
* REGRESSION GUARD FOR THE DELETION ITSELF. The removed block sat directly
|
|
1038
|
+
* beside this one, and this one is load-bearing in a way that is entirely
|
|
1039
|
+
* invisible from `onBeforeUpdate`: `onUpdateSuccess` iterates
|
|
1040
|
+
* `carryForward` to revoke sessions and send the "Password Changed." mail.
|
|
1041
|
+
* An empty carryForward is not an error -- the loop simply runs zero
|
|
1042
|
+
* times, so a password change would silently stop signing out the stolen
|
|
1043
|
+
* session it exists to kill, and stop telling the user anything happened.
|
|
1044
|
+
*
|
|
1045
|
+
* The SELECT is pinned as well as the rows, and that half is not
|
|
1046
|
+
* decoration. `onUpdateSuccess` addresses the mail with `user.email!` --
|
|
1047
|
+
* a non-null assertion over a column it did not fetch itself. Narrowing
|
|
1048
|
+
* this select to `_id` alone still produces a carryForward of the right
|
|
1049
|
+
* length with the right ids, so every assertion about the rows keeps
|
|
1050
|
+
* passing while every "Password Changed." mail goes out to `undefined`.
|
|
1051
|
+
* Because `findBy` is stubbed here, the returned row's own `email` proves
|
|
1052
|
+
* nothing about that; only the select does.
|
|
1053
|
+
*/
|
|
1054
|
+
const affected: User = buildUser({
|
|
1055
|
+
id: foundUserId,
|
|
1056
|
+
email: "user@example.com",
|
|
1057
|
+
});
|
|
1058
|
+
|
|
1059
|
+
findBySpy.mockImplementation(async (): Promise<Array<User>> => {
|
|
1060
|
+
return [affected];
|
|
1061
|
+
});
|
|
1062
|
+
|
|
1063
|
+
const result: any = await callOnBeforeUpdate({
|
|
1064
|
+
query: { _id: userId },
|
|
1065
|
+
data: { password: new HashedString("a-perfectly-fine-password") },
|
|
1066
|
+
props: { isRoot: true },
|
|
1067
|
+
limit: 1,
|
|
1068
|
+
skip: 0,
|
|
1069
|
+
});
|
|
1070
|
+
|
|
1071
|
+
expect(findBySpy).toHaveBeenCalledTimes(1);
|
|
1072
|
+
expect(result.carryForward).toHaveLength(1);
|
|
1073
|
+
expect(result.carryForward[0].id.toString()).toBe(foundUserId.toString());
|
|
1074
|
+
|
|
1075
|
+
const select: any = findBySpy.mock.calls[0][0].select;
|
|
1076
|
+
const violations: Array<string> = [];
|
|
1077
|
+
|
|
1078
|
+
for (const column of ["_id", "email"]) {
|
|
1079
|
+
if (select[column] !== true) {
|
|
1080
|
+
violations.push(
|
|
1081
|
+
`onBeforeUpdate did not carry ${column} forward for a password update; onUpdateSuccess needs it`,
|
|
1082
|
+
);
|
|
1083
|
+
}
|
|
1084
|
+
}
|
|
1085
|
+
|
|
1086
|
+
expect(violations).toEqual([]);
|
|
1087
|
+
});
|
|
1088
|
+
|
|
1089
|
+
test("neither authenticator service overrides onBeforeDelete any more", async () => {
|
|
1090
|
+
/*
|
|
1091
|
+
* Both services used to refuse the deletion of a user's LAST verified
|
|
1092
|
+
* factor while `enableTwoFactorAuth` was on, advising them to "disable two
|
|
1093
|
+
* factor auth before deleting this item".
|
|
1094
|
+
*
|
|
1095
|
+
* Both halves of that stopped being true. The advice is impossible to
|
|
1096
|
+
* follow for a user whose two factor auth was MANDATED -- they cannot turn
|
|
1097
|
+
* the requirement off -- so the guard did not protect them, it stranded
|
|
1098
|
+
* them on an authenticator they may have been trying to replace, and it
|
|
1099
|
+
* would have made `resetTwoFactorAuth` throw on the one account that needs
|
|
1100
|
+
* it most. And the lockout it protected against no longer exists: an
|
|
1101
|
+
* account required to use two factor auth with nothing set up is now sent
|
|
1102
|
+
* through enrolment at its next sign-in, so deleting the last factor costs
|
|
1103
|
+
* a QR code, not an account.
|
|
1104
|
+
*
|
|
1105
|
+
* (It was also wrong on its own terms -- it counted the factors that
|
|
1106
|
+
* existed BEFORE the delete, once per item, inside a loop over the whole
|
|
1107
|
+
* delete set, so removing two verified factors in one call passed the
|
|
1108
|
+
* check twice and landed in the state it existed to prevent.)
|
|
1109
|
+
*
|
|
1110
|
+
* Identity against `DatabaseService.prototype` is the robust form: it does
|
|
1111
|
+
* not care what a reinstated guard would decide, only that the two
|
|
1112
|
+
* services still inherit the placeholder.
|
|
1113
|
+
*/
|
|
1114
|
+
const violations: Array<string> = [];
|
|
1115
|
+
|
|
1116
|
+
const inherited: unknown = (DatabaseService.prototype as any)
|
|
1117
|
+
.onBeforeDelete;
|
|
1118
|
+
|
|
1119
|
+
if ((UserTotpAuthService as any).onBeforeDelete !== inherited) {
|
|
1120
|
+
violations.push("UserTotpAuthService overrides onBeforeDelete again");
|
|
1121
|
+
}
|
|
1122
|
+
|
|
1123
|
+
if ((UserWebAuthnService as any).onBeforeDelete !== inherited) {
|
|
1124
|
+
violations.push("UserWebAuthnService overrides onBeforeDelete again");
|
|
1125
|
+
}
|
|
1126
|
+
|
|
1127
|
+
expect(violations).toEqual([]);
|
|
1128
|
+
});
|
|
1129
|
+
|
|
1130
|
+
test("the override comparison can actually detect an override", async () => {
|
|
1131
|
+
/*
|
|
1132
|
+
* Pairs with the test above so it cannot pass vacuously. If
|
|
1133
|
+
* `DatabaseService.prototype.onBeforeDelete` were ever undefined, or the
|
|
1134
|
+
* decorator started producing a fresh function per access, the identity
|
|
1135
|
+
* check would compare two nothings and report success no matter what the
|
|
1136
|
+
* two services did.
|
|
1137
|
+
*
|
|
1138
|
+
* `UserService` genuinely DOES override `onBeforeDelete` (it refuses to
|
|
1139
|
+
* delete a user who is still a member of a project), so it is the control:
|
|
1140
|
+
* the comparison must come out different for it.
|
|
1141
|
+
*/
|
|
1142
|
+
const inherited: unknown = (DatabaseService.prototype as any)
|
|
1143
|
+
.onBeforeDelete;
|
|
1144
|
+
|
|
1145
|
+
expect(typeof inherited).toBe("function");
|
|
1146
|
+
expect((UserService as any).onBeforeDelete).not.toBe(inherited);
|
|
1147
|
+
});
|
|
1148
|
+
|
|
1149
|
+
test("deleting the last verified factor of a mandated account no longer throws", async () => {
|
|
1150
|
+
/*
|
|
1151
|
+
* The removal expressed as behaviour rather than as identity, because
|
|
1152
|
+
* behaviour is what the product depends on. This is precisely the call
|
|
1153
|
+
* `resetTwoFactorAuth` makes for a user whose two factor auth is REQUIRED
|
|
1154
|
+
* and whose only factor is verified -- the lost-phone case -- and the old
|
|
1155
|
+
* guard turned it into "Please disable two factor auth before deleting
|
|
1156
|
+
* this item", advice that user could not act on.
|
|
1157
|
+
*/
|
|
1158
|
+
const deleteBy: unknown = {
|
|
1159
|
+
query: { userId: userId },
|
|
1160
|
+
limit: LIMIT_MAX,
|
|
1161
|
+
skip: 0,
|
|
1162
|
+
props: { isRoot: true },
|
|
1163
|
+
};
|
|
1164
|
+
|
|
1165
|
+
for (const service of [UserTotpAuthService, UserWebAuthnService]) {
|
|
1166
|
+
await expect(
|
|
1167
|
+
(
|
|
1168
|
+
service as unknown as {
|
|
1169
|
+
onBeforeDelete: (d: unknown) => Promise<unknown>;
|
|
1170
|
+
}
|
|
1171
|
+
).onBeforeDelete(deleteBy),
|
|
1172
|
+
).resolves.toBeDefined();
|
|
1173
|
+
}
|
|
1174
|
+
|
|
1175
|
+
// ...and it reached that answer without consulting anything.
|
|
1176
|
+
const violations: Array<string> = [];
|
|
1177
|
+
|
|
1178
|
+
for (const spy of authenticatorProbeSpies()) {
|
|
1179
|
+
if (spy.mock.calls.length > 0) {
|
|
1180
|
+
violations.push(
|
|
1181
|
+
"onBeforeDelete probed an authenticator service before allowing the delete",
|
|
1182
|
+
);
|
|
1183
|
+
}
|
|
1184
|
+
}
|
|
1185
|
+
|
|
1186
|
+
expect(violations).toEqual([]);
|
|
1187
|
+
expect(findOneBySpy).not.toHaveBeenCalled();
|
|
1188
|
+
});
|
|
1189
|
+
});
|
|
1190
|
+
});
|