@oneuptime/common 13.0.7 → 13.0.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Models/DatabaseModels/OnCallDutyPolicyScheduleCalendarFeed.ts +1 -1
- package/Models/DatabaseModels/ProjectOnCallCalendarFeed.ts +1 -1
- package/Models/DatabaseModels/UserOnCallCalendarFeed.ts +1 -1
- package/Server/API/AIBillingAPI.ts +1 -0
- package/Server/API/AIInsightAPI.ts +1 -4
- package/Server/API/AIInvestigationAPI.ts +1 -4
- package/Server/API/AIReadinessAPI.ts +4 -5
- package/Server/API/AlertAPI.ts +1 -0
- package/Server/API/AutoRemediationAPI.ts +1 -4
- package/Server/API/BaseAPI.ts +13 -0
- package/Server/API/BaseAnalyticsAPI.ts +80 -9
- package/Server/API/BillingAPI.ts +2 -0
- package/Server/API/BillingInvoiceAPI.ts +1 -0
- package/Server/API/BillingPaymentMethodAPI.ts +2 -0
- package/Server/API/CodeFixRunAPI.ts +1 -3
- package/Server/API/CommonAPI.ts +68 -4
- package/Server/API/DashboardAPI.ts +12 -12
- package/Server/API/GlobalConfigAPI.ts +17 -0
- package/Server/API/IncidentAPI.ts +2 -0
- package/Server/API/IncidentEpisodeAPI.ts +1 -0
- package/Server/API/NotificationAPI.ts +1 -0
- package/Server/API/OnCallCalendarAPI.ts +4 -5
- package/Server/API/OnCallDutyPolicyAPI.ts +7 -0
- package/Server/API/OnCallReadinessAPI.ts +4 -1
- package/Server/API/OnCallScheduleTimelineAPI.ts +731 -0
- package/Server/API/ProjectAPI.ts +1 -0
- package/Server/API/ScheduledMaintenanceAPI.ts +1 -0
- package/Server/API/StatusPageAPI.ts +161 -32
- package/Server/API/TeamMemberAPI.ts +3 -0
- package/Server/API/UserCallAPI.ts +2 -0
- package/Server/API/UserEmailAPI.ts +2 -0
- package/Server/API/UserIncomingCallNumberAPI.ts +2 -0
- package/Server/API/UserMicrosoftTeamsAPI.ts +1 -0
- package/Server/API/UserSlackAPI.ts +1 -0
- package/Server/API/UserSmsAPI.ts +2 -0
- package/Server/API/UserTotpAuthAPI.ts +1 -0
- package/Server/API/UserWebAuthnAPI.ts +2 -0
- package/Server/API/UserWebhookAPI.ts +1 -0
- package/Server/API/UserWhatsAppAPI.ts +2 -0
- package/Server/Middleware/MasterAdminAuthorization.ts +13 -2
- package/Server/Middleware/UserAuthorization.ts +79 -5
- package/Server/Services/ApiKeyPermissionService.ts +101 -4
- package/Server/Services/DatabaseService.ts +245 -0
- package/Server/Services/DetectionRuleService.ts +5 -4
- package/Server/Services/OnCallDutyPolicyChildService.ts +261 -0
- package/Server/Services/OnCallDutyPolicyEscalationRuleScheduleService.ts +2 -2
- package/Server/Services/OnCallDutyPolicyEscalationRuleService.ts +2 -2
- package/Server/Services/OnCallDutyPolicyEscalationRuleTeamService.ts +2 -2
- package/Server/Services/OnCallDutyPolicyEscalationRuleUserService.ts +2 -2
- package/Server/Services/PayAsYouGoBillingService.ts +23 -28
- package/Server/Services/ProjectService.ts +18 -0
- package/Server/Services/RumSessionPinService.ts +274 -22
- package/Server/Services/TeamPermissionService.ts +120 -26
- package/Server/Services/ThreatIntelFeedService.ts +6 -3
- package/Server/Types/Database/Permissions/BasePermission.ts +9 -7
- package/Server/Types/Database/Permissions/Index.ts +75 -11
- package/Server/Types/Database/Permissions/TablePermission.ts +24 -28
- package/Server/Utils/AI/AIChatPrivacyFilter.ts +8 -0
- package/Server/Utils/AI/AIRunPrivacyFilter.ts +8 -0
- package/Server/Utils/MasterAdminPageAccess.ts +61 -0
- package/Server/Utils/Monitor/MonitorAlert.ts +3 -1
- package/Server/Utils/Monitor/MonitorIncident.ts +3 -1
- package/Server/Utils/OnCall/OnCallCalendarFeedRenderer.ts +1 -1
- package/Server/Utils/Realtime.ts +163 -45
- package/Server/Utils/SecurityEvent/DetectionRuleEvaluator.ts +11 -28
- package/Server/Utils/SecurityEvent/GoogleSecOpsSeverityRepair.ts +170 -0
- package/Server/Utils/SecurityEvent/Sigma/SigmaClickhouseCompiler.ts +1 -1
- package/Server/Utils/SecurityEvent/ThreatIntel/ThreatIntelFeedPoller.ts +6 -2
- package/Server/Utils/SecurityEvent/ThreatIntel/ThreatIntelMatcher.ts +10 -9
- package/Tests/App/Dashboard/AffectedResourcesPicker.test.tsx +1059 -0
- package/Tests/App/Dashboard/AffectedResourcesPickerEditForm.test.tsx +376 -0
- package/Tests/App/Dashboard/AffectedResourcesPickerHiddenTypes.test.tsx +757 -0
- package/Tests/App/Dashboard/AffectedResourcesSummaryStep.test.tsx +307 -0
- package/Tests/App/Dashboard/ArgumentsFormGroupByUnlistedAttributes.test.tsx +131 -0
- package/Tests/App/Dashboard/DashboardHeaderRequests.test.tsx +1 -1
- package/Tests/App/Dashboard/DashboardMonitorLabelVariable.test.tsx +9 -1
- package/Tests/App/Dashboard/DashboardVariableAllContract.test.tsx +24 -0
- package/Tests/App/Dashboard/DashboardVariableSelector.test.tsx +162 -8
- package/Tests/App/Dashboard/EntityFilterDropdownArchivedSlos.test.tsx +105 -6
- package/Tests/App/Dashboard/EntityFilterDropdownUnlistedSelection.test.tsx +434 -0
- package/Tests/App/Dashboard/EscalationRuleReadiness.test.tsx +276 -1
- package/Tests/App/Dashboard/OnCallCalendarFeedSideMenus.test.tsx +9 -1
- package/Tests/App/Dashboard/PayAsYouGoConsentGate.test.tsx +58 -42
- package/Tests/App/Dashboard/PayAsYouGoNotices.test.tsx +42 -5
- package/Tests/App/Dashboard/ResourceAuditLogsSideMenus.test.tsx +34 -23
- package/Tests/App/Dashboard/ScheduleTimeline.test.tsx +1580 -0
- package/Tests/App/Dashboard/ScheduleTimelineAPI.test.ts +280 -0
- package/Tests/App/Dashboard/ScheduleTimelineModel.test.ts +673 -0
- package/Tests/App/Dashboard/SecurityEventConnectionDiagnostics.test.tsx +95 -68
- package/Tests/App/Dashboard/SecurityEventsDetectionRulesPage.test.tsx +86 -3
- package/Tests/App/Dashboard/SecurityEventsEmptyState.test.tsx +28 -3
- package/Tests/App/Dashboard/SecurityEventsExplorer.test.tsx +761 -0
- package/Tests/App/Dashboard/SecurityEventsGuideContent.test.ts +1313 -0
- package/Tests/App/Dashboard/SecurityEventsHowItWorksCard.test.tsx +536 -0
- package/Tests/App/Dashboard/SecurityEventsNoResults.test.tsx +222 -0
- package/Tests/App/Dashboard/SecurityEventsThreatIntelPage.test.tsx +199 -0
- package/Tests/App/Dashboard/SecurityEventsVolumeChart.test.tsx +321 -0
- package/Tests/App/Dashboard/SloOverviewHero.test.tsx +171 -36
- package/Tests/App/Dashboard/SloViewSideMenu.test.tsx +215 -0
- package/Tests/App/Dashboard/SloViewSideMenuCounts.test.tsx +40 -3
- package/Tests/App/PublicDashboard/DashboardVariableSelector.test.tsx +12 -4
- package/Tests/App/PublicDashboard/PublicDashboardWidgetListAuth.test.tsx +623 -0
- package/Tests/Models/OnCallCalendarFeedModels.test.ts +23 -1
- package/Tests/Server/API/AIChatRouteAuthorization.test.ts +138 -5
- package/Tests/Server/API/AIGenerationProjectAIToggle.test.ts +187 -0
- package/Tests/Server/API/AIInvestigationAvailabilityAuthorization.test.ts +51 -3
- package/Tests/Server/API/AIInvestigationEvidenceAPI.test.ts +55 -2
- package/Tests/Server/API/BaseAnalyticsAPIAggregateCache.test.ts +729 -0
- package/Tests/Server/API/BillingPayAsYouGoStatus.test.ts +93 -7
- package/Tests/Server/API/BillingPaymentMethodAPI.test.ts +36 -3
- package/Tests/Server/API/CommonAPIAuthGuard.test.ts +1003 -24
- package/Tests/Server/API/DashboardPublicRateLimit.test.ts +6 -3
- package/Tests/Server/API/ExpiredSessionAIRoutes.test.ts +708 -0
- package/Tests/Server/API/ExpiredSessionCustomRoutes.test.ts +928 -0
- package/Tests/Server/API/ExpiredSessionMonitorTemplate.test.ts +319 -0
- package/Tests/Server/API/ExpiredSessionOnCallDutyPolicy.test.ts +336 -0
- package/Tests/Server/API/ExpiredSessionRouteGuards.test.ts +820 -0
- package/Tests/Server/API/GlobalConfigLicense.test.ts +140 -0
- package/Tests/Server/API/NotificationChannelVerificationAPI.test.ts +115 -12
- package/Tests/Server/API/OnCallCalendarAPI.test.ts +375 -8
- package/Tests/Server/API/OnCallReadinessAPI.test.ts +175 -17
- package/Tests/Server/API/OnCallScheduleTimelineAPI.test.ts +1513 -0
- package/Tests/Server/API/OnCallSetupReminderAPI.test.ts +119 -7
- package/Tests/Server/API/ProjectScopedCustomRouteAuth.test.ts +111 -4
- package/Tests/Server/API/PublicRouteInvalidAccessToken.test.ts +615 -0
- package/Tests/Server/API/SecurityEventAttributesAPI.test.ts +89 -2
- package/Tests/Server/API/SessionReplayAPI.test.ts +121 -0
- package/Tests/Server/API/StatusPageMasterPasswordRateLimit.test.ts +4 -2
- package/Tests/Server/API/StatusPagePostmortemVisibility.test.ts +441 -0
- package/Tests/Server/API/StatusPageTestEmailReportAuth.test.ts +664 -0
- package/Tests/Server/API/TeamMemberRemoveUserFromProjectAPI.test.ts +7 -0
- package/Tests/Server/API/UserIncomingCallNumberApi.test.ts +6 -0
- package/Tests/Server/API/UserNotificationSettingAPI.test.ts +65 -4
- package/Tests/Server/API/UserProjectsAPI.test.ts +63 -3
- package/Tests/Server/API/UserTotpAuthAPI.test.ts +48 -2
- package/Tests/Server/API/UserWebAuthnAuthenticationOptionsRoute.test.ts +11 -2
- package/Tests/Server/API/UserWebAuthnBackupCodeMinting.test.ts +46 -3
- package/Tests/Server/API/UserWebAuthnPasskeyAPI.test.ts +18 -2
- package/Tests/Server/API/ZZVerifyChatsAuth.test.ts +24 -7
- package/Tests/Server/Middleware/ExpiredSessionUserMiddleware.test.ts +563 -0
- package/Tests/Server/Middleware/MasterAdminAuthorization.test.ts +299 -19
- package/Tests/Server/Middleware/PublicRouteUserMiddleware.test.ts +453 -0
- package/Tests/Server/Services/AIChatPrivacyPin.test.ts +24 -0
- package/Tests/Server/Services/ApiKeyPermissionSecurity.test.ts +2 -0
- package/Tests/Server/Services/ApiKeyPermissionService.test.ts +3 -3
- package/Tests/Server/Services/DatabaseServiceCreateSuppliedId.test.ts +147 -0
- package/Tests/Server/Services/DatabaseServiceLoginBeforeHooks.test.ts +1078 -0
- package/Tests/Server/Services/DatabaseServiceTenantRelationGuard.test.ts +259 -0
- package/Tests/Server/Services/DatabaseServiceWriteOperatorValues.test.ts +147 -0
- package/Tests/Server/Services/MonitorPayAsYouGoBilling.test.ts +115 -1
- package/Tests/Server/Services/OnCallChildCreateAuthorizationPostgres.test.ts +711 -0
- package/Tests/Server/Services/OnCallDutyPolicyChildService.test.ts +733 -0
- package/Tests/Server/Services/PayAsYouGoBillingService.test.ts +190 -140
- package/Tests/Server/Services/PayAsYouGoStripeRateLimitBudget.test.ts +4 -9
- package/Tests/Server/Services/PermissionBlockDeletion.test.ts +780 -0
- package/Tests/Server/Services/PermissionBlockDeletionPostgres.test.ts +628 -0
- package/Tests/Server/Services/ProjectServiceCreateSubscriptionStarted.test.ts +77 -1
- package/Tests/Server/Services/RumSessionPinCreatePermission.test.ts +715 -0
- package/Tests/Server/Services/TeamMemberMultiTenantReadPostgres.test.ts +394 -0
- package/Tests/Server/Services/TeamPrivilegeEscalation.test.ts +6 -0
- package/Tests/Server/Services/TelemetryPayAsYouGoBilling.test.ts +131 -0
- package/Tests/Server/Services/UserNotificationRuleAdminGuards.test.ts +13 -2
- package/Tests/Server/Types/Database/Permissions/ModelPermissionAnonymousRefusal.test.ts +518 -0
- package/Tests/Server/Types/Database/Permissions/MultiTenantPermissions.test.ts +350 -0
- package/Tests/Server/Types/Database/Permissions/OnCallCalendarFeedPermissions.test.ts +282 -25
- package/Tests/Server/Types/Database/Permissions/TableLevelBlockPermission.test.ts +459 -0
- package/Tests/Server/Utils/AI/AIRunPrivacyFilter.test.ts +101 -9
- package/Tests/Server/Utils/AI/ChatAgentHelpers.test.ts +88 -4
- package/Tests/Server/Utils/AI/Toolbox/ExceptionTools.test.ts +459 -0
- package/Tests/Server/Utils/AI/Toolbox/LogTools.test.ts +794 -0
- package/Tests/Server/Utils/AI/Toolbox/RecentChangesTools.test.ts +574 -0
- package/Tests/Server/Utils/AI/Toolbox/SecurityEventTools.test.ts +596 -0
- package/Tests/Server/Utils/CaptchaRequestAndLogging.test.ts +395 -0
- package/Tests/Server/Utils/MasterAdminPageAccess.test.ts +251 -0
- package/Tests/Server/Utils/Monitor/MonitorActionTimestamps.test.ts +616 -0
- package/Tests/Server/Utils/Realtime.test.ts +502 -0
- package/Tests/Server/Utils/SecurityEvent/Connectors/GoogleSecOps/GoogleSecOpsConnectorSeverity.test.ts +216 -0
- package/Tests/Server/Utils/SecurityEvent/DetectionRuleEvaluator.test.ts +142 -0
- package/Tests/Server/Utils/SecurityEvent/GoogleSecOpsSeverityRepair.test.ts +140 -0
- package/Tests/Server/Utils/SecurityEvent/GoogleSecOpsSeverityRepairClickhouseIntegration.test.ts +542 -0
- package/Tests/Server/Utils/Telemetry/ChangeEventRow.test.ts +743 -0
- package/Tests/Server/Utils/Telemetry/RuntimeMetrics.test.ts +465 -0
- package/Tests/Server/Utils/Workspace/MicrosoftTeams/Messages/Alert.test.ts +202 -0
- package/Tests/Server/Utils/Workspace/MicrosoftTeams/Messages/Incident.test.ts +194 -0
- package/Tests/Server/Utils/Workspace/MicrosoftTeams/Messages/ScheduledMaintenance.test.ts +231 -0
- package/Tests/Server/Utils/Workspace/Slack/Messages/Alert.test.ts +194 -0
- package/Tests/Server/Utils/Workspace/Slack/Messages/AlertEpisode.test.ts +194 -0
- package/Tests/Server/Utils/Workspace/Slack/Messages/Incident.test.ts +194 -0
- package/Tests/Server/Utils/Workspace/Slack/Messages/IncidentEpisode.test.ts +194 -0
- package/Tests/Server/Utils/Workspace/Slack/Messages/ScheduledMaintenance.test.ts +247 -0
- package/Tests/Types/AI/AIInsightEvidence.test.ts +206 -0
- package/Tests/Types/BaseDatabase/DatabaseCommonInteractionPropsUtilAnonymous.test.ts +144 -0
- package/Tests/Types/Call/CallProvider.test.ts +528 -0
- package/Tests/Types/Kubernetes/KubernetesCostIngest.test.ts +236 -0
- package/Tests/Types/OnCallDutyPolicy/ScheduleTimeline.test.ts +736 -0
- package/Tests/Types/OnCallDutyPolicy/ScheduleTimelineLayout.test.ts +996 -0
- package/Tests/Types/SecurityEvent/SigmaRule.test.ts +142 -0
- package/Tests/Types/Workflow/Components/JSON.test.ts +182 -0
- package/Tests/UI/Components/BasicFormDropdownNormalization.test.tsx +116 -1
- package/Tests/UI/Components/Dropdown/DropdownAsyncOptions.test.tsx +81 -0
- package/Tests/UI/Components/EditionLabelLicenseRefresh.test.tsx +89 -1
- package/Tests/UI/Components/EventAttachmentList.test.tsx +174 -0
- package/Tests/UI/Components/Forms/BasicFormBurnRateTemplateDefaults.test.tsx +395 -0
- package/Tests/UI/Components/Forms/BasicFormCollapsibleSections.test.tsx +286 -0
- package/Tests/UI/Components/Forms/FormSummary.test.tsx +128 -0
- package/Tests/UI/Components/ModelTable/BaseModelTableQueryChange.test.tsx +365 -0
- package/Tests/UI/Components/OpenAuthenticatedUrl.test.ts +358 -0
- package/Tests/UI/Components/SessionAwareImage.test.tsx +496 -0
- package/Tests/UI/Components/TelemetryHistogramZeroBuckets.test.tsx +190 -0
- package/Tests/UI/Components/TooltipInteractive.test.tsx +97 -0
- package/Tests/UI/MicrosoftTeams/MicrosoftTeamsChatsCardInstallGuidance.test.tsx +1 -1
- package/Tests/UI/Monitor/EvaluationLogListTimestamps.test.tsx +367 -0
- package/Tests/UI/Utils/API/BaseAPIRefreshSession.test.ts +1087 -0
- package/Tests/UI/Utils/API/DashboardHost.ts +12 -0
- package/Tests/UI/Utils/API/FakeAxiosServer.ts +183 -0
- package/Tests/UI/Utils/Breadcrumb/fixtures/RealBreadcrumbTrails.ts +10 -0
- package/Tests/UI/Utils/Breadcrumb/fixtures/RealRoutePatterns.ts +2 -0
- package/Tests/UI/Utils/Cookie.test.ts +440 -0
- package/Tests/UI/Utils/ModelAPIRequestClient.test.ts +278 -0
- package/Tests/UI/Utils/Navigation.test.ts +33 -0
- package/Tests/UI/Utils/PricingPlan.test.ts +295 -0
- package/Tests/UI/Utils/Realtime.test.ts +759 -0
- package/Tests/UI/Utils/Telemetry/Telemetry.test.ts +500 -0
- package/Tests/Utils/APIInstanceDispatch.test.ts +311 -0
- package/Tests/Utils/SecurityEvent/GoogleSecOpsAlertNormalizerSeverity.test.ts +565 -0
- package/Types/BaseDatabase/DatabaseCommonInteractionPropsUtil.ts +40 -0
- package/Types/Monitor/MonitorEvaluationSummary.ts +3 -0
- package/Types/OnCallDutyPolicy/ScheduleTimeline.ts +507 -0
- package/Types/OnCallDutyPolicy/ScheduleTimelineLayout.ts +589 -0
- package/Types/Realtime/EventName.ts +11 -0
- package/Types/SecurityEvent/DetectionFindingConstants.ts +14 -0
- package/Types/SecurityEvent/OcsfSeverity.ts +31 -26
- package/Types/SecurityEvent/SigmaRule.ts +51 -0
- package/Types/SecurityEvent/ThreatIntelConstants.ts +18 -0
- package/UI/Components/AttachmentList/EventAttachmentList.tsx +30 -2
- package/UI/Components/Dashboard/DashboardVariableControl.tsx +12 -2
- package/UI/Components/Dropdown/Dropdown.tsx +6 -1
- package/UI/Components/EditionLabel/EditionLabel.tsx +12 -0
- package/UI/Components/EventItem/EventItem.tsx +12 -1
- package/UI/Components/Forms/BasicForm.tsx +151 -126
- package/UI/Components/Forms/CollapsibleFormSection.tsx +63 -0
- package/UI/Components/Forms/Fields/FormField.tsx +10 -4
- package/UI/Components/Forms/FormSummary.tsx +7 -0
- package/UI/Components/Forms/Types/Field.ts +10 -0
- package/UI/Components/Markdown.tsx/MarkdownViewer.tsx +2 -1
- package/UI/Components/Markdown.tsx/SessionAwareImage.tsx +122 -0
- package/UI/Components/ModelTable/BaseModelTable.tsx +34 -5
- package/UI/Components/ModelTable/TableView.tsx +1 -1
- package/UI/Components/TelemetryViewer/components/TelemetryHistogram.tsx +8 -1
- package/UI/Components/Tooltip/Tooltip.tsx +8 -1
- package/UI/Utils/API/API.ts +173 -12
- package/UI/Utils/API/RequestOptions.ts +11 -0
- package/UI/Utils/AnalyticsModelAPI/AnalyticsModelAPI.ts +31 -7
- package/UI/Utils/ModelAPI/ModelAPI.ts +30 -9
- package/UI/Utils/Navigation.ts +9 -0
- package/UI/Utils/OpenAuthenticatedUrl.ts +154 -0
- package/UI/Utils/Realtime.ts +176 -30
- package/Utils/API.ts +14 -5
- package/Utils/SecurityEvent/GoogleSecOpsAlertNormalizer.ts +157 -7
- package/Utils/SecurityEvent/Sigma/SigmaRuleParser.ts +9 -17
- package/Utils/Slo/SloBurnRateTemplate.ts +2 -2
- package/build/dist/Models/DatabaseModels/OnCallDutyPolicyScheduleCalendarFeed.js +1 -1
- package/build/dist/Models/DatabaseModels/OnCallDutyPolicyScheduleCalendarFeed.js.map +1 -1
- package/build/dist/Models/DatabaseModels/ProjectOnCallCalendarFeed.js +1 -1
- package/build/dist/Models/DatabaseModels/ProjectOnCallCalendarFeed.js.map +1 -1
- package/build/dist/Models/DatabaseModels/UserOnCallCalendarFeed.js +1 -1
- package/build/dist/Models/DatabaseModels/UserOnCallCalendarFeed.js.map +1 -1
- package/build/dist/Server/API/AIBillingAPI.js +1 -1
- package/build/dist/Server/API/AIBillingAPI.js.map +1 -1
- package/build/dist/Server/API/AIInsightAPI.js +1 -4
- package/build/dist/Server/API/AIInsightAPI.js.map +1 -1
- package/build/dist/Server/API/AIInvestigationAPI.js +1 -4
- package/build/dist/Server/API/AIInvestigationAPI.js.map +1 -1
- package/build/dist/Server/API/AIReadinessAPI.js +1 -3
- package/build/dist/Server/API/AIReadinessAPI.js.map +1 -1
- package/build/dist/Server/API/AlertAPI.js +1 -1
- package/build/dist/Server/API/AlertAPI.js.map +1 -1
- package/build/dist/Server/API/AutoRemediationAPI.js +1 -4
- package/build/dist/Server/API/AutoRemediationAPI.js.map +1 -1
- package/build/dist/Server/API/BaseAPI.js +12 -0
- package/build/dist/Server/API/BaseAPI.js.map +1 -1
- package/build/dist/Server/API/BaseAnalyticsAPI.js +55 -10
- package/build/dist/Server/API/BaseAnalyticsAPI.js.map +1 -1
- package/build/dist/Server/API/BillingAPI.js +2 -2
- package/build/dist/Server/API/BillingAPI.js.map +1 -1
- package/build/dist/Server/API/BillingInvoiceAPI.js +1 -1
- package/build/dist/Server/API/BillingInvoiceAPI.js.map +1 -1
- package/build/dist/Server/API/BillingPaymentMethodAPI.js +2 -2
- package/build/dist/Server/API/BillingPaymentMethodAPI.js.map +1 -1
- package/build/dist/Server/API/CodeFixRunAPI.js +1 -3
- package/build/dist/Server/API/CodeFixRunAPI.js.map +1 -1
- package/build/dist/Server/API/CommonAPI.js +50 -5
- package/build/dist/Server/API/CommonAPI.js.map +1 -1
- package/build/dist/Server/API/DashboardAPI.js +12 -12
- package/build/dist/Server/API/DashboardAPI.js.map +1 -1
- package/build/dist/Server/API/GlobalConfigAPI.js +12 -0
- package/build/dist/Server/API/GlobalConfigAPI.js.map +1 -1
- package/build/dist/Server/API/IncidentAPI.js +2 -2
- package/build/dist/Server/API/IncidentAPI.js.map +1 -1
- package/build/dist/Server/API/IncidentEpisodeAPI.js +1 -1
- package/build/dist/Server/API/IncidentEpisodeAPI.js.map +1 -1
- package/build/dist/Server/API/NotificationAPI.js +1 -1
- package/build/dist/Server/API/NotificationAPI.js.map +1 -1
- package/build/dist/Server/API/OnCallCalendarAPI.js +1 -4
- package/build/dist/Server/API/OnCallCalendarAPI.js.map +1 -1
- package/build/dist/Server/API/OnCallDutyPolicyAPI.js +6 -0
- package/build/dist/Server/API/OnCallDutyPolicyAPI.js.map +1 -1
- package/build/dist/Server/API/OnCallReadinessAPI.js +4 -1
- package/build/dist/Server/API/OnCallReadinessAPI.js.map +1 -1
- package/build/dist/Server/API/OnCallScheduleTimelineAPI.js +494 -0
- package/build/dist/Server/API/OnCallScheduleTimelineAPI.js.map +1 -0
- package/build/dist/Server/API/ProjectAPI.js +1 -1
- package/build/dist/Server/API/ProjectAPI.js.map +1 -1
- package/build/dist/Server/API/ScheduledMaintenanceAPI.js +1 -1
- package/build/dist/Server/API/ScheduledMaintenanceAPI.js.map +1 -1
- package/build/dist/Server/API/StatusPageAPI.js +125 -33
- package/build/dist/Server/API/StatusPageAPI.js.map +1 -1
- package/build/dist/Server/API/TeamMemberAPI.js +3 -3
- package/build/dist/Server/API/TeamMemberAPI.js.map +1 -1
- package/build/dist/Server/API/UserCallAPI.js +2 -2
- package/build/dist/Server/API/UserCallAPI.js.map +1 -1
- package/build/dist/Server/API/UserEmailAPI.js +2 -2
- package/build/dist/Server/API/UserEmailAPI.js.map +1 -1
- package/build/dist/Server/API/UserIncomingCallNumberAPI.js +2 -2
- package/build/dist/Server/API/UserIncomingCallNumberAPI.js.map +1 -1
- package/build/dist/Server/API/UserMicrosoftTeamsAPI.js +1 -1
- package/build/dist/Server/API/UserMicrosoftTeamsAPI.js.map +1 -1
- package/build/dist/Server/API/UserSlackAPI.js +1 -1
- package/build/dist/Server/API/UserSlackAPI.js.map +1 -1
- package/build/dist/Server/API/UserSmsAPI.js +2 -2
- package/build/dist/Server/API/UserSmsAPI.js.map +1 -1
- package/build/dist/Server/API/UserTotpAuthAPI.js +1 -1
- package/build/dist/Server/API/UserTotpAuthAPI.js.map +1 -1
- package/build/dist/Server/API/UserWebAuthnAPI.js +2 -2
- package/build/dist/Server/API/UserWebAuthnAPI.js.map +1 -1
- package/build/dist/Server/API/UserWebhookAPI.js +1 -1
- package/build/dist/Server/API/UserWebhookAPI.js.map +1 -1
- package/build/dist/Server/API/UserWhatsAppAPI.js +2 -2
- package/build/dist/Server/API/UserWhatsAppAPI.js.map +1 -1
- package/build/dist/Server/Middleware/MasterAdminAuthorization.js +11 -2
- package/build/dist/Server/Middleware/MasterAdminAuthorization.js.map +1 -1
- package/build/dist/Server/Middleware/UserAuthorization.js +64 -4
- package/build/dist/Server/Middleware/UserAuthorization.js.map +1 -1
- package/build/dist/Server/Services/ApiKeyPermissionService.js +80 -4
- package/build/dist/Server/Services/ApiKeyPermissionService.js.map +1 -1
- package/build/dist/Server/Services/DatabaseService.js +171 -0
- package/build/dist/Server/Services/DatabaseService.js.map +1 -1
- package/build/dist/Server/Services/DetectionRuleService.js +4 -4
- package/build/dist/Server/Services/DetectionRuleService.js.map +1 -1
- package/build/dist/Server/Services/OnCallDutyPolicyChildService.js +170 -0
- package/build/dist/Server/Services/OnCallDutyPolicyChildService.js.map +1 -0
- package/build/dist/Server/Services/OnCallDutyPolicyEscalationRuleScheduleService.js +2 -2
- package/build/dist/Server/Services/OnCallDutyPolicyEscalationRuleScheduleService.js.map +1 -1
- package/build/dist/Server/Services/OnCallDutyPolicyEscalationRuleService.js +2 -2
- package/build/dist/Server/Services/OnCallDutyPolicyEscalationRuleService.js.map +1 -1
- package/build/dist/Server/Services/OnCallDutyPolicyEscalationRuleTeamService.js +2 -2
- package/build/dist/Server/Services/OnCallDutyPolicyEscalationRuleTeamService.js.map +1 -1
- package/build/dist/Server/Services/OnCallDutyPolicyEscalationRuleUserService.js +2 -2
- package/build/dist/Server/Services/OnCallDutyPolicyEscalationRuleUserService.js.map +1 -1
- package/build/dist/Server/Services/PayAsYouGoBillingService.js +20 -27
- package/build/dist/Server/Services/PayAsYouGoBillingService.js.map +1 -1
- package/build/dist/Server/Services/ProjectService.js +17 -0
- package/build/dist/Server/Services/ProjectService.js.map +1 -1
- package/build/dist/Server/Services/RumSessionPinService.js +180 -16
- package/build/dist/Server/Services/RumSessionPinService.js.map +1 -1
- package/build/dist/Server/Services/TeamPermissionService.js +89 -27
- package/build/dist/Server/Services/TeamPermissionService.js.map +1 -1
- package/build/dist/Server/Services/ThreatIntelFeedService.js +5 -4
- package/build/dist/Server/Services/ThreatIntelFeedService.js.map +1 -1
- package/build/dist/Server/Types/Database/Permissions/BasePermission.js +9 -6
- package/build/dist/Server/Types/Database/Permissions/BasePermission.js.map +1 -1
- package/build/dist/Server/Types/Database/Permissions/Index.js +60 -6
- package/build/dist/Server/Types/Database/Permissions/Index.js.map +1 -1
- package/build/dist/Server/Types/Database/Permissions/TablePermission.js +14 -13
- package/build/dist/Server/Types/Database/Permissions/TablePermission.js.map +1 -1
- package/build/dist/Server/Utils/AI/AIChatPrivacyFilter.js +7 -0
- package/build/dist/Server/Utils/AI/AIChatPrivacyFilter.js.map +1 -1
- package/build/dist/Server/Utils/AI/AIRunPrivacyFilter.js +7 -0
- package/build/dist/Server/Utils/AI/AIRunPrivacyFilter.js.map +1 -1
- package/build/dist/Server/Utils/MasterAdminPageAccess.js +39 -0
- package/build/dist/Server/Utils/MasterAdminPageAccess.js.map +1 -0
- package/build/dist/Server/Utils/Monitor/MonitorAlert.js +3 -1
- package/build/dist/Server/Utils/Monitor/MonitorAlert.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/MonitorIncident.js +3 -1
- package/build/dist/Server/Utils/Monitor/MonitorIncident.js.map +1 -1
- package/build/dist/Server/Utils/Realtime.js +126 -37
- package/build/dist/Server/Utils/Realtime.js.map +1 -1
- package/build/dist/Server/Utils/SecurityEvent/DetectionRuleEvaluator.js +7 -25
- package/build/dist/Server/Utils/SecurityEvent/DetectionRuleEvaluator.js.map +1 -1
- package/build/dist/Server/Utils/SecurityEvent/GoogleSecOpsSeverityRepair.js +127 -0
- package/build/dist/Server/Utils/SecurityEvent/GoogleSecOpsSeverityRepair.js.map +1 -0
- package/build/dist/Server/Utils/SecurityEvent/Sigma/SigmaClickhouseCompiler.js +1 -1
- package/build/dist/Server/Utils/SecurityEvent/Sigma/SigmaClickhouseCompiler.js.map +1 -1
- package/build/dist/Server/Utils/SecurityEvent/ThreatIntel/ThreatIntelFeedPoller.js +3 -2
- package/build/dist/Server/Utils/SecurityEvent/ThreatIntel/ThreatIntelFeedPoller.js.map +1 -1
- package/build/dist/Server/Utils/SecurityEvent/ThreatIntel/ThreatIntelMatcher.js +4 -8
- package/build/dist/Server/Utils/SecurityEvent/ThreatIntel/ThreatIntelMatcher.js.map +1 -1
- package/build/dist/Types/BaseDatabase/DatabaseCommonInteractionPropsUtil.js +31 -1
- package/build/dist/Types/BaseDatabase/DatabaseCommonInteractionPropsUtil.js.map +1 -1
- package/build/dist/Types/OnCallDutyPolicy/ScheduleTimeline.js +321 -0
- package/build/dist/Types/OnCallDutyPolicy/ScheduleTimeline.js.map +1 -0
- package/build/dist/Types/OnCallDutyPolicy/ScheduleTimelineLayout.js +372 -0
- package/build/dist/Types/OnCallDutyPolicy/ScheduleTimelineLayout.js.map +1 -0
- package/build/dist/Types/Realtime/EventName.js +10 -0
- package/build/dist/Types/Realtime/EventName.js.map +1 -1
- package/build/dist/Types/SecurityEvent/DetectionFindingConstants.js +11 -0
- package/build/dist/Types/SecurityEvent/DetectionFindingConstants.js.map +1 -1
- package/build/dist/Types/SecurityEvent/OcsfSeverity.js +30 -25
- package/build/dist/Types/SecurityEvent/OcsfSeverity.js.map +1 -1
- package/build/dist/Types/SecurityEvent/SigmaRule.js +47 -0
- package/build/dist/Types/SecurityEvent/SigmaRule.js.map +1 -1
- package/build/dist/Types/SecurityEvent/ThreatIntelConstants.js +14 -0
- package/build/dist/Types/SecurityEvent/ThreatIntelConstants.js.map +1 -1
- package/build/dist/UI/Components/AttachmentList/EventAttachmentList.js +9 -4
- package/build/dist/UI/Components/AttachmentList/EventAttachmentList.js.map +1 -1
- package/build/dist/UI/Components/Dashboard/DashboardVariableControl.js +10 -2
- package/build/dist/UI/Components/Dashboard/DashboardVariableControl.js.map +1 -1
- package/build/dist/UI/Components/Dropdown/Dropdown.js +5 -1
- package/build/dist/UI/Components/Dropdown/Dropdown.js.map +1 -1
- package/build/dist/UI/Components/EditionLabel/EditionLabel.js +11 -0
- package/build/dist/UI/Components/EditionLabel/EditionLabel.js.map +1 -1
- package/build/dist/UI/Components/EventItem/EventItem.js +2 -2
- package/build/dist/UI/Components/EventItem/EventItem.js.map +1 -1
- package/build/dist/UI/Components/Forms/BasicForm.js +70 -64
- package/build/dist/UI/Components/Forms/BasicForm.js.map +1 -1
- package/build/dist/UI/Components/Forms/CollapsibleFormSection.js +27 -0
- package/build/dist/UI/Components/Forms/CollapsibleFormSection.js.map +1 -0
- package/build/dist/UI/Components/Forms/Fields/FormField.js +10 -7
- package/build/dist/UI/Components/Forms/Fields/FormField.js.map +1 -1
- package/build/dist/UI/Components/Forms/FormSummary.js +7 -0
- package/build/dist/UI/Components/Forms/FormSummary.js.map +1 -1
- package/build/dist/UI/Components/Markdown.tsx/MarkdownViewer.js +2 -1
- package/build/dist/UI/Components/Markdown.tsx/MarkdownViewer.js.map +1 -1
- package/build/dist/UI/Components/Markdown.tsx/SessionAwareImage.js +84 -0
- package/build/dist/UI/Components/Markdown.tsx/SessionAwareImage.js.map +1 -0
- package/build/dist/UI/Components/ModelTable/BaseModelTable.js +14 -1
- package/build/dist/UI/Components/ModelTable/BaseModelTable.js.map +1 -1
- package/build/dist/UI/Components/ModelTable/TableView.js +1 -1
- package/build/dist/UI/Components/ModelTable/TableView.js.map +1 -1
- package/build/dist/UI/Components/TelemetryViewer/components/TelemetryHistogram.js +8 -1
- package/build/dist/UI/Components/TelemetryViewer/components/TelemetryHistogram.js.map +1 -1
- package/build/dist/UI/Components/Tooltip/Tooltip.js +2 -1
- package/build/dist/UI/Components/Tooltip/Tooltip.js.map +1 -1
- package/build/dist/UI/Utils/API/API.js +143 -19
- package/build/dist/UI/Utils/API/API.js.map +1 -1
- package/build/dist/UI/Utils/AnalyticsModelAPI/AnalyticsModelAPI.js +21 -7
- package/build/dist/UI/Utils/AnalyticsModelAPI/AnalyticsModelAPI.js.map +1 -1
- package/build/dist/UI/Utils/ModelAPI/ModelAPI.js +26 -9
- package/build/dist/UI/Utils/ModelAPI/ModelAPI.js.map +1 -1
- package/build/dist/UI/Utils/Navigation.js +8 -0
- package/build/dist/UI/Utils/Navigation.js.map +1 -1
- package/build/dist/UI/Utils/OpenAuthenticatedUrl.js +92 -0
- package/build/dist/UI/Utils/OpenAuthenticatedUrl.js.map +1 -0
- package/build/dist/UI/Utils/Realtime.js +120 -15
- package/build/dist/UI/Utils/Realtime.js.map +1 -1
- package/build/dist/Utils/API.js +13 -5
- package/build/dist/Utils/API.js.map +1 -1
- package/build/dist/Utils/SecurityEvent/GoogleSecOpsAlertNormalizer.js +123 -6
- package/build/dist/Utils/SecurityEvent/GoogleSecOpsAlertNormalizer.js.map +1 -1
- package/build/dist/Utils/SecurityEvent/Sigma/SigmaRuleParser.js +4 -18
- package/build/dist/Utils/SecurityEvent/Sigma/SigmaRuleParser.js.map +1 -1
- package/build/dist/Utils/Slo/SloBurnRateTemplate.js +2 -2
- package/package.json +1 -1
|
@@ -0,0 +1,928 @@
|
|
|
1
|
+
import { mockRouter } from "./Helpers";
|
|
2
|
+
import CommonAPI from "../../../Server/API/CommonAPI";
|
|
3
|
+
import OnCallReadinessAPI from "../../../Server/API/OnCallReadinessAPI";
|
|
4
|
+
import UserMiddleware from "../../../Server/Middleware/UserAuthorization";
|
|
5
|
+
import OnCallDutyPolicyService from "../../../Server/Services/OnCallDutyPolicyService";
|
|
6
|
+
import OnCallReadinessService from "../../../Server/Services/OnCallReadinessService";
|
|
7
|
+
import OnCallSetupReminderService, {
|
|
8
|
+
SetupReminderOutcome,
|
|
9
|
+
SetupReminderResult,
|
|
10
|
+
SetupReminderUserResult,
|
|
11
|
+
} from "../../../Server/Services/OnCallSetupReminderService";
|
|
12
|
+
import ProjectService from "../../../Server/Services/ProjectService";
|
|
13
|
+
import TeamMemberService from "../../../Server/Services/TeamMemberService";
|
|
14
|
+
import CookieUtil from "../../../Server/Utils/Cookie";
|
|
15
|
+
import {
|
|
16
|
+
ExpressRequest,
|
|
17
|
+
ExpressResponse,
|
|
18
|
+
NextFunction,
|
|
19
|
+
OneUptimeRequest,
|
|
20
|
+
} from "../../../Server/Utils/Express";
|
|
21
|
+
import logger from "../../../Server/Utils/Logger";
|
|
22
|
+
import Response from "../../../Server/Utils/Response";
|
|
23
|
+
import DatabaseCommonInteractionProps from "../../../Types/BaseDatabase/DatabaseCommonInteractionProps";
|
|
24
|
+
import Dictionary from "../../../Types/Dictionary";
|
|
25
|
+
import BadDataException from "../../../Types/Exception/BadDataException";
|
|
26
|
+
import Exception from "../../../Types/Exception/Exception";
|
|
27
|
+
import ExceptionCode from "../../../Types/Exception/ExceptionCode";
|
|
28
|
+
import NotAuthenticatedException from "../../../Types/Exception/NotAuthenticatedException";
|
|
29
|
+
import NotAuthorizedException from "../../../Types/Exception/NotAuthorizedException";
|
|
30
|
+
import { JSONObject } from "../../../Types/JSON";
|
|
31
|
+
import ObjectID from "../../../Types/ObjectID";
|
|
32
|
+
import Permission, {
|
|
33
|
+
UserPermission,
|
|
34
|
+
UserTenantAccessPermission,
|
|
35
|
+
} from "../../../Types/Permission";
|
|
36
|
+
import UserType from "../../../Types/UserType";
|
|
37
|
+
import { afterEach, beforeEach, describe, expect, test } from "@jest/globals";
|
|
38
|
+
|
|
39
|
+
/*
|
|
40
|
+
* The customer report, replayed at the server.
|
|
41
|
+
*
|
|
42
|
+
* An admin opened On-Call > Readiness, left the tab for more than fifteen
|
|
43
|
+
* minutes, came back and pressed "Send setup reminder". The dashboard answered
|
|
44
|
+
* "You are not authorized to access this project's data." They were still
|
|
45
|
+
* signed in - the refresh token was good for days - and they were an owner of
|
|
46
|
+
* the project.
|
|
47
|
+
*
|
|
48
|
+
* What happened on the wire: the access-token cookie's maxAge is the JWT's own
|
|
49
|
+
* fifteen-minute lifetime, so the browser had simply stopped sending it. The
|
|
50
|
+
* POST arrived with the `tenantid` header the dashboard always adds, the
|
|
51
|
+
* refresh-token cookie, and no access token. getUserMiddleware treats that as a
|
|
52
|
+
* Public request and calls next(); the route's member guard then saw no userId
|
|
53
|
+
* and refused with NotAuthorizedException - a 422. The browser client
|
|
54
|
+
* (Common/UI/Utils/API/API.ts) refreshes the session and replays the request on
|
|
55
|
+
* a 401 and on nothing else, so the 422 went straight to the screen.
|
|
56
|
+
*
|
|
57
|
+
* The fix is that "no credentials at all" is a 401 everywhere, decided before
|
|
58
|
+
* anything about the project is looked at. This file drives the readiness
|
|
59
|
+
* router - the customer's POST and its three GET siblings - the way that
|
|
60
|
+
* request actually arrives:
|
|
61
|
+
*
|
|
62
|
+
* - the REAL UserMiddleware.getUserMiddleware turns the cookie-less request
|
|
63
|
+
* into the Public request, instead of a fixture guessing at its shape;
|
|
64
|
+
* - the REAL CommonAPI.getDatabaseCommonInteractionProps turns that into
|
|
65
|
+
* props (only ProjectService's plan lookup is stubbed, because CI runs with
|
|
66
|
+
* billing enabled and the project does not exist);
|
|
67
|
+
* - the error the handler hands to next() is given to the REAL
|
|
68
|
+
* Response.sendErrorResponse, which applies the same "exception code is
|
|
69
|
+
* the HTTP status" rule as the app's error handler, so the assertions are
|
|
70
|
+
* on the status the browser would actually receive.
|
|
71
|
+
*
|
|
72
|
+
* Authenticated callers are built as getUserMiddleware leaves them after a
|
|
73
|
+
* successful token decode (userType, userAuthorization, tenant permissions),
|
|
74
|
+
* because running a real decode needs a signing secret and a database. What
|
|
75
|
+
* they pin is that the change did NOT move anyone else: a logged-in member
|
|
76
|
+
* without the reminder permission still gets the 422, and a privileged member
|
|
77
|
+
* still sends.
|
|
78
|
+
*/
|
|
79
|
+
|
|
80
|
+
jest.mock("../../../Server/Utils/Express", () => {
|
|
81
|
+
return {
|
|
82
|
+
getRouter: () => {
|
|
83
|
+
return mockRouter;
|
|
84
|
+
},
|
|
85
|
+
};
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
const POLICY_ROUTE: string = "/on-call-readiness/policy/:policyId";
|
|
89
|
+
const PROJECT_ROUTE: string = "/on-call-readiness/project";
|
|
90
|
+
const USER_ROUTE: string = "/on-call-readiness/user/:userId";
|
|
91
|
+
const REMINDER_ROUTE: string = "/on-call-readiness/send-setup-reminder";
|
|
92
|
+
|
|
93
|
+
// The member / permission refusal: what the customer saw, and must not again.
|
|
94
|
+
const REFUSAL: string = "You are not authorized to access this project's data.";
|
|
95
|
+
|
|
96
|
+
type RouterFunction = (
|
|
97
|
+
req: ExpressRequest,
|
|
98
|
+
res: ExpressResponse,
|
|
99
|
+
next: NextFunction,
|
|
100
|
+
) => void | Promise<void>;
|
|
101
|
+
|
|
102
|
+
// The slice of Helpers.ts's private Route type this file relies on.
|
|
103
|
+
interface RegisteredRoute {
|
|
104
|
+
method: string;
|
|
105
|
+
uri: string;
|
|
106
|
+
middlewares: Array<RouterFunction>;
|
|
107
|
+
handlerFunction: RouterFunction;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
interface RouteCallResult {
|
|
111
|
+
// The exception the handler passed to next(), if any.
|
|
112
|
+
thrownToNext: unknown;
|
|
113
|
+
// Whether every middleware called next() and the handler itself ran.
|
|
114
|
+
reachedHandler: boolean;
|
|
115
|
+
// The HTTP status the client receives for a refusal, or undefined on success.
|
|
116
|
+
status: number | undefined;
|
|
117
|
+
// The body the client receives for a refusal.
|
|
118
|
+
errorBody: JSONObject | undefined;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
let projectId: ObjectID;
|
|
122
|
+
let otherProjectId: ObjectID;
|
|
123
|
+
let callerUserId: ObjectID;
|
|
124
|
+
let subjectUserId: ObjectID;
|
|
125
|
+
let policyId: ObjectID;
|
|
126
|
+
|
|
127
|
+
let sendRemindersSpy: jest.SpyInstance;
|
|
128
|
+
let policyReadinessSpy: jest.SpyInstance;
|
|
129
|
+
let projectReadinessSpy: jest.SpyInstance;
|
|
130
|
+
let userReadinessSpy: jest.SpyInstance;
|
|
131
|
+
let clearCacheSpy: jest.SpyInstance;
|
|
132
|
+
let policyFindOneByIdSpy: jest.SpyInstance;
|
|
133
|
+
let teamMemberFindBySpy: jest.SpyInstance;
|
|
134
|
+
let sendJsonSpy: jest.SpyInstance;
|
|
135
|
+
|
|
136
|
+
function findRoute(method: string, uri: string): RegisteredRoute {
|
|
137
|
+
const route: RegisteredRoute | undefined = (
|
|
138
|
+
mockRouter.routes as unknown as Array<RegisteredRoute>
|
|
139
|
+
).find((candidate: RegisteredRoute): boolean => {
|
|
140
|
+
return candidate.method === method && candidate.uri === uri;
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
if (!route) {
|
|
144
|
+
throw new Error(`No ${method} route registered for ${uri}`);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return route;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/*
|
|
151
|
+
* The request a browser sends once the access-token cookie has expired: the
|
|
152
|
+
* dashboard's `tenantid` header (ModelAPI.getCommonHeaders adds it to every
|
|
153
|
+
* call) and the refresh-token cookie, which is long-lived and so is still
|
|
154
|
+
* there. No access token and no Authorization header - which is the whole
|
|
155
|
+
* point.
|
|
156
|
+
*/
|
|
157
|
+
function buildExpiredSessionRequest(data: {
|
|
158
|
+
tenantId?: ObjectID | undefined;
|
|
159
|
+
params?: Dictionary<string> | undefined;
|
|
160
|
+
query?: Dictionary<string> | undefined;
|
|
161
|
+
body?: JSONObject | undefined;
|
|
162
|
+
}): ExpressRequest {
|
|
163
|
+
const headers: Dictionary<string> = {};
|
|
164
|
+
|
|
165
|
+
if (data.tenantId) {
|
|
166
|
+
headers["tenantid"] = data.tenantId.toString();
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const cookies: Dictionary<string> = {};
|
|
170
|
+
cookies[CookieUtil.getRefreshTokenKey()] = "still-valid-refresh-token";
|
|
171
|
+
|
|
172
|
+
return {
|
|
173
|
+
params: data.params || {},
|
|
174
|
+
query: data.query || {},
|
|
175
|
+
body: data.body || {},
|
|
176
|
+
headers: headers,
|
|
177
|
+
cookies: cookies,
|
|
178
|
+
} as unknown as ExpressRequest;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/*
|
|
182
|
+
* A logged-in caller as getUserMiddleware leaves the request after decoding a
|
|
183
|
+
* valid access token and loading the caller's permissions for the tenant.
|
|
184
|
+
*
|
|
185
|
+
* isBlockPermission is an explicit false on every entry: the permission reads
|
|
186
|
+
* filter on it strictly, so an undefined would silently drop the grant and turn
|
|
187
|
+
* the "privileged member" below into an unprivileged one.
|
|
188
|
+
*/
|
|
189
|
+
function buildSignedInRequest(data: {
|
|
190
|
+
tenantId: ObjectID;
|
|
191
|
+
userId: ObjectID | undefined;
|
|
192
|
+
userType: UserType;
|
|
193
|
+
permissions: Array<Permission>;
|
|
194
|
+
body?: JSONObject | undefined;
|
|
195
|
+
}): ExpressRequest {
|
|
196
|
+
const userPermissions: Array<UserPermission> = data.permissions.map(
|
|
197
|
+
(permission: Permission): UserPermission => {
|
|
198
|
+
return {
|
|
199
|
+
_type: "UserPermission",
|
|
200
|
+
permission: permission,
|
|
201
|
+
labelIds: [],
|
|
202
|
+
isBlockPermission: false,
|
|
203
|
+
};
|
|
204
|
+
},
|
|
205
|
+
);
|
|
206
|
+
|
|
207
|
+
const tenantPermission: UserTenantAccessPermission = {
|
|
208
|
+
_type: "UserTenantAccessPermission",
|
|
209
|
+
projectId: data.tenantId,
|
|
210
|
+
permissions: userPermissions,
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
const permissionMap: Dictionary<UserTenantAccessPermission> = {};
|
|
214
|
+
permissionMap[data.tenantId.toString()] = tenantPermission;
|
|
215
|
+
|
|
216
|
+
return {
|
|
217
|
+
params: {},
|
|
218
|
+
query: {},
|
|
219
|
+
body: data.body || {},
|
|
220
|
+
headers: { tenantid: data.tenantId.toString() },
|
|
221
|
+
cookies: {},
|
|
222
|
+
tenantId: data.tenantId,
|
|
223
|
+
userType: data.userType,
|
|
224
|
+
userAuthorization: data.userId ? { userId: data.userId } : undefined,
|
|
225
|
+
userTenantAccessPermission: permissionMap,
|
|
226
|
+
} as unknown as ExpressRequest;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/*
|
|
230
|
+
* Runs a registered route's middleware chain and handler for real, then hands
|
|
231
|
+
* any error the handler raised to Response.sendErrorResponse - the app's error
|
|
232
|
+
* path, reduced to the one rule these assertions need: the exception's code is
|
|
233
|
+
* the HTTP status.
|
|
234
|
+
*
|
|
235
|
+
* `signedIn` requests skip the middleware: buildSignedInRequest already IS the
|
|
236
|
+
* middleware's output for a valid token, and re-running it without that token
|
|
237
|
+
* would overwrite the caller back to Public.
|
|
238
|
+
*/
|
|
239
|
+
async function callRoute(data: {
|
|
240
|
+
method: string;
|
|
241
|
+
uri: string;
|
|
242
|
+
req: ExpressRequest;
|
|
243
|
+
signedIn?: boolean | undefined;
|
|
244
|
+
}): Promise<RouteCallResult> {
|
|
245
|
+
const route: RegisteredRoute = findRoute(data.method, data.uri);
|
|
246
|
+
|
|
247
|
+
const sent: { status: number | undefined; body: JSONObject | undefined } = {
|
|
248
|
+
status: undefined,
|
|
249
|
+
body: undefined,
|
|
250
|
+
};
|
|
251
|
+
|
|
252
|
+
const res: ExpressResponse = {
|
|
253
|
+
status: (code: number): ExpressResponse => {
|
|
254
|
+
sent.status = code;
|
|
255
|
+
return res;
|
|
256
|
+
},
|
|
257
|
+
send: (body: JSONObject): ExpressResponse => {
|
|
258
|
+
sent.body = body;
|
|
259
|
+
return res;
|
|
260
|
+
},
|
|
261
|
+
json: (body: JSONObject): ExpressResponse => {
|
|
262
|
+
sent.body = body;
|
|
263
|
+
return res;
|
|
264
|
+
},
|
|
265
|
+
setHeader: (): void => {
|
|
266
|
+
// not needed by the paths under test
|
|
267
|
+
},
|
|
268
|
+
} as unknown as ExpressResponse;
|
|
269
|
+
|
|
270
|
+
const middlewares: Array<RouterFunction> = data.signedIn
|
|
271
|
+
? []
|
|
272
|
+
: route.middlewares;
|
|
273
|
+
|
|
274
|
+
for (const middleware of middlewares) {
|
|
275
|
+
const step: { calledNext: boolean; error: unknown } = {
|
|
276
|
+
calledNext: false,
|
|
277
|
+
error: undefined,
|
|
278
|
+
};
|
|
279
|
+
|
|
280
|
+
await middleware(data.req, res, ((error?: unknown): void => {
|
|
281
|
+
step.calledNext = true;
|
|
282
|
+
step.error = error;
|
|
283
|
+
}) as NextFunction);
|
|
284
|
+
|
|
285
|
+
if (!step.calledNext || step.error) {
|
|
286
|
+
// The middleware answered the request itself.
|
|
287
|
+
return {
|
|
288
|
+
thrownToNext: step.error,
|
|
289
|
+
reachedHandler: false,
|
|
290
|
+
status: sent.status,
|
|
291
|
+
errorBody: sent.body,
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
const outcome: { error: unknown } = { error: undefined };
|
|
297
|
+
|
|
298
|
+
await route.handlerFunction(data.req, res, ((error?: unknown): void => {
|
|
299
|
+
outcome.error = error;
|
|
300
|
+
}) as NextFunction);
|
|
301
|
+
|
|
302
|
+
if (outcome.error) {
|
|
303
|
+
Response.sendErrorResponse(data.req, res, outcome.error as Exception);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
return {
|
|
307
|
+
thrownToNext: outcome.error,
|
|
308
|
+
reachedHandler: true,
|
|
309
|
+
status: sent.status,
|
|
310
|
+
errorBody: sent.body,
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/*
|
|
315
|
+
* The refusal an expired session must get, checked at both ends: the exception
|
|
316
|
+
* the handler raised, and the status and message the browser receives.
|
|
317
|
+
*/
|
|
318
|
+
function expectAskedToAuthenticate(result: RouteCallResult): void {
|
|
319
|
+
expect(result.thrownToNext).toBeInstanceOf(NotAuthenticatedException);
|
|
320
|
+
expect(result.thrownToNext).not.toBeInstanceOf(NotAuthorizedException);
|
|
321
|
+
expect((result.thrownToNext as Exception).code).toBe(
|
|
322
|
+
ExceptionCode.NotAuthenticatedException,
|
|
323
|
+
);
|
|
324
|
+
expect(result.status).toBe(401);
|
|
325
|
+
expect(result.errorBody?.["message"]).toBe(
|
|
326
|
+
CommonAPI.AUTHENTICATION_REQUIRED_MESSAGE,
|
|
327
|
+
);
|
|
328
|
+
expect(result.errorBody?.["message"]).not.toBe(REFUSAL);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
function expectNoReadinessWork(): void {
|
|
332
|
+
expect(policyFindOneByIdSpy).not.toHaveBeenCalled();
|
|
333
|
+
expect(teamMemberFindBySpy).not.toHaveBeenCalled();
|
|
334
|
+
expect(policyReadinessSpy).not.toHaveBeenCalled();
|
|
335
|
+
expect(projectReadinessSpy).not.toHaveBeenCalled();
|
|
336
|
+
expect(userReadinessSpy).not.toHaveBeenCalled();
|
|
337
|
+
expect(clearCacheSpy).not.toHaveBeenCalled();
|
|
338
|
+
expect(sendRemindersSpy).not.toHaveBeenCalled();
|
|
339
|
+
expect(sendJsonSpy).not.toHaveBeenCalled();
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
function emptySummary(forProjectId: ObjectID): JSONObject {
|
|
343
|
+
return {
|
|
344
|
+
projectId: forProjectId,
|
|
345
|
+
readyCount: 0,
|
|
346
|
+
partiallyReadyCount: 0,
|
|
347
|
+
notReachableCount: 0,
|
|
348
|
+
isFallbackEnabled: true,
|
|
349
|
+
isTruncated: false,
|
|
350
|
+
users: [],
|
|
351
|
+
} as unknown as JSONObject;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
beforeEach(() => {
|
|
355
|
+
jest.clearAllMocks();
|
|
356
|
+
|
|
357
|
+
projectId = ObjectID.generate();
|
|
358
|
+
otherProjectId = ObjectID.generate();
|
|
359
|
+
callerUserId = ObjectID.generate();
|
|
360
|
+
subjectUserId = ObjectID.generate();
|
|
361
|
+
policyId = ObjectID.generate();
|
|
362
|
+
|
|
363
|
+
// Both sources of 401 in this file log the refusal; keep the output clean.
|
|
364
|
+
jest.spyOn(logger, "error").mockImplementation((): void => {});
|
|
365
|
+
|
|
366
|
+
/*
|
|
367
|
+
* getUserMiddleware fires a lastActive write for any tenant header, and
|
|
368
|
+
* getDatabaseCommonInteractionProps resolves the plan when billing is on
|
|
369
|
+
* (it is in CI). Neither is under test, and both would reach the database.
|
|
370
|
+
*/
|
|
371
|
+
jest.spyOn(ProjectService, "updateLastActive").mockResolvedValue(undefined);
|
|
372
|
+
jest
|
|
373
|
+
.spyOn(ProjectService, "getCurrentPlan")
|
|
374
|
+
.mockResolvedValue({ plan: null, isSubscriptionUnpaid: false });
|
|
375
|
+
|
|
376
|
+
sendJsonSpy = jest
|
|
377
|
+
.spyOn(Response, "sendJsonObjectResponse")
|
|
378
|
+
.mockImplementation((): void => {});
|
|
379
|
+
|
|
380
|
+
policyFindOneByIdSpy = jest
|
|
381
|
+
.spyOn(OnCallDutyPolicyService, "findOneById")
|
|
382
|
+
.mockResolvedValue({ id: policyId, projectId: projectId } as never);
|
|
383
|
+
|
|
384
|
+
teamMemberFindBySpy = jest
|
|
385
|
+
.spyOn(TeamMemberService, "findBy")
|
|
386
|
+
.mockResolvedValue([{ _id: ObjectID.generate().toString() }] as never);
|
|
387
|
+
|
|
388
|
+
policyReadinessSpy = jest
|
|
389
|
+
.spyOn(OnCallReadinessService, "getReadinessForPolicy")
|
|
390
|
+
.mockResolvedValue(emptySummary(projectId) as never);
|
|
391
|
+
|
|
392
|
+
projectReadinessSpy = jest
|
|
393
|
+
.spyOn(OnCallReadinessService, "getReadinessForProject")
|
|
394
|
+
.mockResolvedValue(emptySummary(projectId) as never);
|
|
395
|
+
|
|
396
|
+
userReadinessSpy = jest
|
|
397
|
+
.spyOn(OnCallReadinessService, "getReadinessForUser")
|
|
398
|
+
.mockRejectedValue(
|
|
399
|
+
new Error("not stubbed: no test here reads a single user") as never,
|
|
400
|
+
);
|
|
401
|
+
|
|
402
|
+
clearCacheSpy = jest.spyOn(OnCallReadinessService, "clearCache");
|
|
403
|
+
|
|
404
|
+
sendRemindersSpy = jest
|
|
405
|
+
.spyOn(OnCallSetupReminderService, "sendSetupReminders")
|
|
406
|
+
.mockImplementation((async (data: {
|
|
407
|
+
projectId: ObjectID;
|
|
408
|
+
userIds: Array<ObjectID>;
|
|
409
|
+
}): Promise<SetupReminderResult> => {
|
|
410
|
+
return {
|
|
411
|
+
projectId: data.projectId,
|
|
412
|
+
requestedCount: data.userIds.length,
|
|
413
|
+
sentCount: data.userIds.length,
|
|
414
|
+
skippedCount: 0,
|
|
415
|
+
failedCount: 0,
|
|
416
|
+
results: data.userIds.map(
|
|
417
|
+
(userId: ObjectID): SetupReminderUserResult => {
|
|
418
|
+
return {
|
|
419
|
+
userId: userId,
|
|
420
|
+
outcome: SetupReminderOutcome.Sent,
|
|
421
|
+
message: "Reminder sent to their account email address.",
|
|
422
|
+
};
|
|
423
|
+
},
|
|
424
|
+
),
|
|
425
|
+
};
|
|
426
|
+
}) as never);
|
|
427
|
+
});
|
|
428
|
+
|
|
429
|
+
afterEach(() => {
|
|
430
|
+
jest.restoreAllMocks();
|
|
431
|
+
});
|
|
432
|
+
|
|
433
|
+
describe("the request an expired session actually sends", () => {
|
|
434
|
+
test("getUserMiddleware turns it into an anonymous Public request that still carries the tenant", async () => {
|
|
435
|
+
/*
|
|
436
|
+
* Pinned first because every other test in this file depends on it: the
|
|
437
|
+
* cookie-less request is let THROUGH the user middleware, not refused
|
|
438
|
+
* there, so the route's own guard is what answers it.
|
|
439
|
+
*/
|
|
440
|
+
const req: ExpressRequest = buildExpiredSessionRequest({
|
|
441
|
+
tenantId: projectId,
|
|
442
|
+
body: { userIds: [subjectUserId.toString()] },
|
|
443
|
+
});
|
|
444
|
+
|
|
445
|
+
const next: jest.Mock = jest.fn();
|
|
446
|
+
|
|
447
|
+
await UserMiddleware.getUserMiddleware(
|
|
448
|
+
req,
|
|
449
|
+
{} as ExpressResponse,
|
|
450
|
+
next as unknown as NextFunction,
|
|
451
|
+
);
|
|
452
|
+
|
|
453
|
+
expect(next).toHaveBeenCalledTimes(1);
|
|
454
|
+
expect(next).toHaveBeenCalledWith();
|
|
455
|
+
|
|
456
|
+
const oneUptimeRequest: OneUptimeRequest = req as OneUptimeRequest;
|
|
457
|
+
|
|
458
|
+
expect(oneUptimeRequest.userType).toBe(UserType.Public);
|
|
459
|
+
expect(oneUptimeRequest.tenantId?.toString()).toBe(projectId.toString());
|
|
460
|
+
expect(oneUptimeRequest.userAuthorization).toBeUndefined();
|
|
461
|
+
expect(UserMiddleware.isAnonymousRequest(oneUptimeRequest)).toBe(true);
|
|
462
|
+
|
|
463
|
+
const props: DatabaseCommonInteractionProps =
|
|
464
|
+
await CommonAPI.getDatabaseCommonInteractionProps(req);
|
|
465
|
+
|
|
466
|
+
expect(props.userId).toBeUndefined();
|
|
467
|
+
expect(props.tenantId?.toString()).toBe(projectId.toString());
|
|
468
|
+
expect(CommonAPI.isAnonymous(props)).toBe(true);
|
|
469
|
+
});
|
|
470
|
+
|
|
471
|
+
test("the middleware's 401 and the guards' 401 read the same to the user", () => {
|
|
472
|
+
/*
|
|
473
|
+
* Both are "you need to log in again". If the refresh itself fails, this
|
|
474
|
+
* is the sentence the dashboard ends up showing, whichever layer refused.
|
|
475
|
+
*/
|
|
476
|
+
expect(CommonAPI.AUTHENTICATION_REQUIRED_MESSAGE).toBe(
|
|
477
|
+
UserMiddleware.AUTHENTICATION_REQUIRED_MESSAGE,
|
|
478
|
+
);
|
|
479
|
+
expect(CommonAPI.AUTHENTICATION_REQUIRED_MESSAGE).toBe(
|
|
480
|
+
"Authentication required. Please log in to access this resource.",
|
|
481
|
+
);
|
|
482
|
+
});
|
|
483
|
+
|
|
484
|
+
test("the readiness routes are behind the user middleware only, so the handler's guard is what answers", () => {
|
|
485
|
+
for (const [method, uri] of [
|
|
486
|
+
["GET", POLICY_ROUTE],
|
|
487
|
+
["GET", PROJECT_ROUTE],
|
|
488
|
+
["GET", USER_ROUTE],
|
|
489
|
+
["POST", REMINDER_ROUTE],
|
|
490
|
+
] as Array<[string, string]>) {
|
|
491
|
+
const route: RegisteredRoute = findRoute(method, uri);
|
|
492
|
+
|
|
493
|
+
expect(route.middlewares).toEqual([UserMiddleware.getUserMiddleware]);
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
// The module exports the router the routes were registered on.
|
|
497
|
+
expect(OnCallReadinessAPI).toBe(mockRouter);
|
|
498
|
+
});
|
|
499
|
+
});
|
|
500
|
+
|
|
501
|
+
describe("POST /on-call-readiness/send-setup-reminder with an expired session", () => {
|
|
502
|
+
test("is answered 401 - not the 422 'not authorized' the customer saw - and nothing is sent", async () => {
|
|
503
|
+
const result: RouteCallResult = await callRoute({
|
|
504
|
+
method: "POST",
|
|
505
|
+
uri: REMINDER_ROUTE,
|
|
506
|
+
req: buildExpiredSessionRequest({
|
|
507
|
+
tenantId: projectId,
|
|
508
|
+
body: { userIds: [subjectUserId.toString()] },
|
|
509
|
+
}),
|
|
510
|
+
});
|
|
511
|
+
|
|
512
|
+
expect(result.reachedHandler).toBe(true);
|
|
513
|
+
expectAskedToAuthenticate(result);
|
|
514
|
+
expect(result.status).not.toBe(ExceptionCode.NotAuthorizedException);
|
|
515
|
+
expectNoReadinessWork();
|
|
516
|
+
});
|
|
517
|
+
|
|
518
|
+
test("is 401 even without the tenant header - who you are is asked before which project", async () => {
|
|
519
|
+
const result: RouteCallResult = await callRoute({
|
|
520
|
+
method: "POST",
|
|
521
|
+
uri: REMINDER_ROUTE,
|
|
522
|
+
req: buildExpiredSessionRequest({
|
|
523
|
+
tenantId: undefined,
|
|
524
|
+
body: { userIds: [subjectUserId.toString()] },
|
|
525
|
+
}),
|
|
526
|
+
});
|
|
527
|
+
|
|
528
|
+
expectAskedToAuthenticate(result);
|
|
529
|
+
expect(result.status).not.toBe(ExceptionCode.BadDataException);
|
|
530
|
+
expectNoReadinessWork();
|
|
531
|
+
});
|
|
532
|
+
|
|
533
|
+
test("is 401 even when the body is malformed - the body is never read for a caller with no session", async () => {
|
|
534
|
+
const result: RouteCallResult = await callRoute({
|
|
535
|
+
method: "POST",
|
|
536
|
+
uri: REMINDER_ROUTE,
|
|
537
|
+
req: buildExpiredSessionRequest({
|
|
538
|
+
tenantId: projectId,
|
|
539
|
+
body: { userIds: "not-even-an-array" } as unknown as JSONObject,
|
|
540
|
+
}),
|
|
541
|
+
});
|
|
542
|
+
|
|
543
|
+
expectAskedToAuthenticate(result);
|
|
544
|
+
expectNoReadinessWork();
|
|
545
|
+
});
|
|
546
|
+
|
|
547
|
+
test("is 401 when the body names a project of its own - nothing in the request can stand in for a session", async () => {
|
|
548
|
+
const result: RouteCallResult = await callRoute({
|
|
549
|
+
method: "POST",
|
|
550
|
+
uri: REMINDER_ROUTE,
|
|
551
|
+
req: buildExpiredSessionRequest({
|
|
552
|
+
tenantId: undefined,
|
|
553
|
+
body: {
|
|
554
|
+
projectId: projectId.toString(),
|
|
555
|
+
userIds: [subjectUserId.toString()],
|
|
556
|
+
},
|
|
557
|
+
}),
|
|
558
|
+
});
|
|
559
|
+
|
|
560
|
+
expectAskedToAuthenticate(result);
|
|
561
|
+
expectNoReadinessWork();
|
|
562
|
+
});
|
|
563
|
+
|
|
564
|
+
test("a stale access token that IS still sent is refused 401 by the middleware, before the handler", async () => {
|
|
565
|
+
/*
|
|
566
|
+
* The other way an expired session arrives: a client that keeps the token
|
|
567
|
+
* past its lifetime (the mobile app, or a cookie that outlived its JWT).
|
|
568
|
+
* getUserMiddleware cannot decode it and answers 401 itself.
|
|
569
|
+
*/
|
|
570
|
+
const req: ExpressRequest = buildExpiredSessionRequest({
|
|
571
|
+
tenantId: projectId,
|
|
572
|
+
body: { userIds: [subjectUserId.toString()] },
|
|
573
|
+
});
|
|
574
|
+
|
|
575
|
+
(req as unknown as { cookies: Dictionary<string> }).cookies[
|
|
576
|
+
CookieUtil.getUserTokenKey()
|
|
577
|
+
] = "an.expired.jwt";
|
|
578
|
+
|
|
579
|
+
const result: RouteCallResult = await callRoute({
|
|
580
|
+
method: "POST",
|
|
581
|
+
uri: REMINDER_ROUTE,
|
|
582
|
+
req: req,
|
|
583
|
+
});
|
|
584
|
+
|
|
585
|
+
expect(result.reachedHandler).toBe(false);
|
|
586
|
+
expect(result.status).toBe(401);
|
|
587
|
+
expect(result.errorBody?.["message"]).toBe(
|
|
588
|
+
"AccessToken is invalid or expired. Please refresh your token.",
|
|
589
|
+
);
|
|
590
|
+
expectNoReadinessWork();
|
|
591
|
+
});
|
|
592
|
+
|
|
593
|
+
test("the refreshed replay of the same POST succeeds, and the reminder goes out exactly once", async () => {
|
|
594
|
+
/*
|
|
595
|
+
* The whole point of answering 401: the client refreshes the session and
|
|
596
|
+
* resends the identical request. First attempt refused without side
|
|
597
|
+
* effects, second attempt (same body, now with a session) sends.
|
|
598
|
+
*/
|
|
599
|
+
const body: JSONObject = { userIds: [subjectUserId.toString()] };
|
|
600
|
+
|
|
601
|
+
const expired: RouteCallResult = await callRoute({
|
|
602
|
+
method: "POST",
|
|
603
|
+
uri: REMINDER_ROUTE,
|
|
604
|
+
req: buildExpiredSessionRequest({ tenantId: projectId, body: body }),
|
|
605
|
+
});
|
|
606
|
+
|
|
607
|
+
expectAskedToAuthenticate(expired);
|
|
608
|
+
expect(sendRemindersSpy).not.toHaveBeenCalled();
|
|
609
|
+
|
|
610
|
+
const replayed: RouteCallResult = await callRoute({
|
|
611
|
+
method: "POST",
|
|
612
|
+
uri: REMINDER_ROUTE,
|
|
613
|
+
req: buildSignedInRequest({
|
|
614
|
+
tenantId: projectId,
|
|
615
|
+
userId: callerUserId,
|
|
616
|
+
userType: UserType.User,
|
|
617
|
+
permissions: [Permission.ProjectOwner],
|
|
618
|
+
body: body,
|
|
619
|
+
}),
|
|
620
|
+
signedIn: true,
|
|
621
|
+
});
|
|
622
|
+
|
|
623
|
+
expect(replayed.thrownToNext).toBeUndefined();
|
|
624
|
+
expect(replayed.status).toBeUndefined();
|
|
625
|
+
expect(sendRemindersSpy).toHaveBeenCalledTimes(1);
|
|
626
|
+
expect(sendJsonSpy).toHaveBeenCalledTimes(1);
|
|
627
|
+
});
|
|
628
|
+
});
|
|
629
|
+
|
|
630
|
+
describe("POST /on-call-readiness/send-setup-reminder for signed-in callers is unchanged", () => {
|
|
631
|
+
test("a privileged member sends, to the project from their tenant", async () => {
|
|
632
|
+
const result: RouteCallResult = await callRoute({
|
|
633
|
+
method: "POST",
|
|
634
|
+
uri: REMINDER_ROUTE,
|
|
635
|
+
req: buildSignedInRequest({
|
|
636
|
+
tenantId: projectId,
|
|
637
|
+
userId: callerUserId,
|
|
638
|
+
userType: UserType.User,
|
|
639
|
+
permissions: [Permission.ProjectMember, Permission.OnCallAdmin],
|
|
640
|
+
body: { userIds: [subjectUserId.toString()] },
|
|
641
|
+
}),
|
|
642
|
+
signedIn: true,
|
|
643
|
+
});
|
|
644
|
+
|
|
645
|
+
expect(result.thrownToNext).toBeUndefined();
|
|
646
|
+
expect(sendRemindersSpy).toHaveBeenCalledTimes(1);
|
|
647
|
+
|
|
648
|
+
const call: { projectId: ObjectID; userIds: Array<ObjectID> } =
|
|
649
|
+
sendRemindersSpy.mock.calls[0]![0] as {
|
|
650
|
+
projectId: ObjectID;
|
|
651
|
+
userIds: Array<ObjectID>;
|
|
652
|
+
};
|
|
653
|
+
|
|
654
|
+
expect(call.projectId.toString()).toBe(projectId.toString());
|
|
655
|
+
expect(
|
|
656
|
+
call.userIds.map((id: ObjectID): string => {
|
|
657
|
+
return id.toString();
|
|
658
|
+
}),
|
|
659
|
+
).toEqual([subjectUserId.toString()]);
|
|
660
|
+
|
|
661
|
+
const payload: JSONObject = sendJsonSpy.mock.calls[0]![2] as JSONObject;
|
|
662
|
+
|
|
663
|
+
expect(payload["sentCount"]).toBe(1);
|
|
664
|
+
});
|
|
665
|
+
|
|
666
|
+
test("a member without the reminder permission still gets the 422 refusal, and nothing is sent", async () => {
|
|
667
|
+
/*
|
|
668
|
+
* A real, current session - so NOT a 401. Answering 401 here would make
|
|
669
|
+
* the dashboard refresh a perfectly good session and replay the POST into
|
|
670
|
+
* the same refusal.
|
|
671
|
+
*/
|
|
672
|
+
const result: RouteCallResult = await callRoute({
|
|
673
|
+
method: "POST",
|
|
674
|
+
uri: REMINDER_ROUTE,
|
|
675
|
+
req: buildSignedInRequest({
|
|
676
|
+
tenantId: projectId,
|
|
677
|
+
userId: callerUserId,
|
|
678
|
+
userType: UserType.User,
|
|
679
|
+
permissions: [Permission.ProjectMember],
|
|
680
|
+
body: { userIds: [subjectUserId.toString()] },
|
|
681
|
+
}),
|
|
682
|
+
signedIn: true,
|
|
683
|
+
});
|
|
684
|
+
|
|
685
|
+
expect(result.thrownToNext).toBeInstanceOf(NotAuthorizedException);
|
|
686
|
+
expect(result.thrownToNext).not.toBeInstanceOf(NotAuthenticatedException);
|
|
687
|
+
expect((result.thrownToNext as Exception).code).toBe(
|
|
688
|
+
ExceptionCode.NotAuthorizedException,
|
|
689
|
+
);
|
|
690
|
+
expect(result.status).toBe(422);
|
|
691
|
+
expect(result.errorBody?.["message"]).toBe(REFUSAL);
|
|
692
|
+
expectNoReadinessWork();
|
|
693
|
+
});
|
|
694
|
+
|
|
695
|
+
test("an owner of ANOTHER project naming this one in the header still gets the 422", async () => {
|
|
696
|
+
const req: ExpressRequest = buildSignedInRequest({
|
|
697
|
+
tenantId: otherProjectId,
|
|
698
|
+
userId: callerUserId,
|
|
699
|
+
userType: UserType.User,
|
|
700
|
+
permissions: [Permission.ProjectOwner],
|
|
701
|
+
body: { userIds: [subjectUserId.toString()] },
|
|
702
|
+
});
|
|
703
|
+
|
|
704
|
+
// The header says this project; the grants are for the other one.
|
|
705
|
+
(req as OneUptimeRequest).tenantId = projectId;
|
|
706
|
+
req.headers["tenantid"] = projectId.toString();
|
|
707
|
+
|
|
708
|
+
const result: RouteCallResult = await callRoute({
|
|
709
|
+
method: "POST",
|
|
710
|
+
uri: REMINDER_ROUTE,
|
|
711
|
+
req: req,
|
|
712
|
+
signedIn: true,
|
|
713
|
+
});
|
|
714
|
+
|
|
715
|
+
expect(result.thrownToNext).toBeInstanceOf(NotAuthorizedException);
|
|
716
|
+
expect(result.status).toBe(422);
|
|
717
|
+
expectNoReadinessWork();
|
|
718
|
+
});
|
|
719
|
+
|
|
720
|
+
test("a signed-in caller with no tenant header still gets the 400", async () => {
|
|
721
|
+
const req: ExpressRequest = buildSignedInRequest({
|
|
722
|
+
tenantId: projectId,
|
|
723
|
+
userId: callerUserId,
|
|
724
|
+
userType: UserType.User,
|
|
725
|
+
permissions: [Permission.ProjectOwner],
|
|
726
|
+
body: { userIds: [subjectUserId.toString()] },
|
|
727
|
+
});
|
|
728
|
+
|
|
729
|
+
// Signed in, but the call carried no tenant header.
|
|
730
|
+
(req as OneUptimeRequest).tenantId = undefined as unknown as ObjectID;
|
|
731
|
+
req.headers = {};
|
|
732
|
+
|
|
733
|
+
const result: RouteCallResult = await callRoute({
|
|
734
|
+
method: "POST",
|
|
735
|
+
uri: REMINDER_ROUTE,
|
|
736
|
+
req: req,
|
|
737
|
+
signedIn: true,
|
|
738
|
+
});
|
|
739
|
+
|
|
740
|
+
expect(result.thrownToNext).toBeInstanceOf(BadDataException);
|
|
741
|
+
expect(result.status).toBe(400);
|
|
742
|
+
expectNoReadinessWork();
|
|
743
|
+
});
|
|
744
|
+
|
|
745
|
+
test("a project API key - authenticated, but not a person - gets the 422, not a 401", async () => {
|
|
746
|
+
/*
|
|
747
|
+
* An API client has no session to refresh, so a 401 would send it round a
|
|
748
|
+
* refresh it can never complete. The route is for members; the key is
|
|
749
|
+
* refused as not authorised, exactly as before.
|
|
750
|
+
*/
|
|
751
|
+
const result: RouteCallResult = await callRoute({
|
|
752
|
+
method: "POST",
|
|
753
|
+
uri: REMINDER_ROUTE,
|
|
754
|
+
req: buildSignedInRequest({
|
|
755
|
+
tenantId: projectId,
|
|
756
|
+
userId: undefined,
|
|
757
|
+
userType: UserType.API,
|
|
758
|
+
permissions: [Permission.ProjectOwner],
|
|
759
|
+
body: { userIds: [subjectUserId.toString()] },
|
|
760
|
+
}),
|
|
761
|
+
signedIn: true,
|
|
762
|
+
});
|
|
763
|
+
|
|
764
|
+
expect(result.thrownToNext).toBeInstanceOf(NotAuthorizedException);
|
|
765
|
+
expect(result.status).toBe(422);
|
|
766
|
+
expect(result.errorBody?.["message"]).toBe(REFUSAL);
|
|
767
|
+
expectNoReadinessWork();
|
|
768
|
+
});
|
|
769
|
+
});
|
|
770
|
+
|
|
771
|
+
describe("the readiness GET routes with an expired session", () => {
|
|
772
|
+
type ReadCase = {
|
|
773
|
+
name: string;
|
|
774
|
+
uri: string;
|
|
775
|
+
};
|
|
776
|
+
|
|
777
|
+
const reads: Array<ReadCase> = [
|
|
778
|
+
{ name: "GET /on-call-readiness/policy/:policyId", uri: POLICY_ROUTE },
|
|
779
|
+
{ name: "GET /on-call-readiness/project", uri: PROJECT_ROUTE },
|
|
780
|
+
{ name: "GET /on-call-readiness/user/:userId", uri: USER_ROUTE },
|
|
781
|
+
];
|
|
782
|
+
|
|
783
|
+
// Ids are generated per test, so the path params are filled in here.
|
|
784
|
+
function paramsFor(uri: string): Dictionary<string> {
|
|
785
|
+
if (uri === POLICY_ROUTE) {
|
|
786
|
+
return { policyId: policyId.toString() };
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
if (uri === USER_ROUTE) {
|
|
790
|
+
return { userId: subjectUserId.toString() };
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
return {};
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
test.each(reads)(
|
|
797
|
+
"$name answers 401 and does no readiness work",
|
|
798
|
+
async (read: ReadCase) => {
|
|
799
|
+
const result: RouteCallResult = await callRoute({
|
|
800
|
+
method: "GET",
|
|
801
|
+
uri: read.uri,
|
|
802
|
+
req: buildExpiredSessionRequest({
|
|
803
|
+
tenantId: projectId,
|
|
804
|
+
params: paramsFor(read.uri),
|
|
805
|
+
}),
|
|
806
|
+
});
|
|
807
|
+
|
|
808
|
+
expect(result.reachedHandler).toBe(true);
|
|
809
|
+
expectAskedToAuthenticate(result);
|
|
810
|
+
expectNoReadinessWork();
|
|
811
|
+
},
|
|
812
|
+
);
|
|
813
|
+
|
|
814
|
+
test.each(reads)(
|
|
815
|
+
"$name with ?refresh=true does not clear the shared readiness cache for an anonymous caller",
|
|
816
|
+
async (read: ReadCase) => {
|
|
817
|
+
const result: RouteCallResult = await callRoute({
|
|
818
|
+
method: "GET",
|
|
819
|
+
uri: read.uri,
|
|
820
|
+
req: buildExpiredSessionRequest({
|
|
821
|
+
tenantId: projectId,
|
|
822
|
+
params: paramsFor(read.uri),
|
|
823
|
+
query: { refresh: "true" },
|
|
824
|
+
}),
|
|
825
|
+
});
|
|
826
|
+
|
|
827
|
+
expectAskedToAuthenticate(result);
|
|
828
|
+
expect(clearCacheSpy).not.toHaveBeenCalled();
|
|
829
|
+
},
|
|
830
|
+
);
|
|
831
|
+
|
|
832
|
+
test.each(reads)(
|
|
833
|
+
"$name answers 401 without a tenant header too",
|
|
834
|
+
async (read: ReadCase) => {
|
|
835
|
+
const result: RouteCallResult = await callRoute({
|
|
836
|
+
method: "GET",
|
|
837
|
+
uri: read.uri,
|
|
838
|
+
req: buildExpiredSessionRequest({
|
|
839
|
+
tenantId: undefined,
|
|
840
|
+
params: paramsFor(read.uri),
|
|
841
|
+
}),
|
|
842
|
+
});
|
|
843
|
+
|
|
844
|
+
expectAskedToAuthenticate(result);
|
|
845
|
+
expectNoReadinessWork();
|
|
846
|
+
},
|
|
847
|
+
);
|
|
848
|
+
|
|
849
|
+
test("a malformed path id from an anonymous caller is still the 401, not the 400", async () => {
|
|
850
|
+
const result: RouteCallResult = await callRoute({
|
|
851
|
+
method: "GET",
|
|
852
|
+
uri: POLICY_ROUTE,
|
|
853
|
+
req: buildExpiredSessionRequest({
|
|
854
|
+
tenantId: projectId,
|
|
855
|
+
params: { policyId: "not-a-uuid" },
|
|
856
|
+
}),
|
|
857
|
+
});
|
|
858
|
+
|
|
859
|
+
expectAskedToAuthenticate(result);
|
|
860
|
+
expectNoReadinessWork();
|
|
861
|
+
});
|
|
862
|
+
|
|
863
|
+
test("a signed-in member still reads the project summary (the reads keep the membership bar)", async () => {
|
|
864
|
+
const result: RouteCallResult = await callRoute({
|
|
865
|
+
method: "GET",
|
|
866
|
+
uri: PROJECT_ROUTE,
|
|
867
|
+
req: buildSignedInRequest({
|
|
868
|
+
tenantId: projectId,
|
|
869
|
+
userId: callerUserId,
|
|
870
|
+
userType: UserType.User,
|
|
871
|
+
permissions: [Permission.ProjectMember],
|
|
872
|
+
}),
|
|
873
|
+
signedIn: true,
|
|
874
|
+
});
|
|
875
|
+
|
|
876
|
+
expect(result.thrownToNext).toBeUndefined();
|
|
877
|
+
expect(result.status).toBeUndefined();
|
|
878
|
+
expect(projectReadinessSpy).toHaveBeenCalledTimes(1);
|
|
879
|
+
expect(sendJsonSpy).toHaveBeenCalledTimes(1);
|
|
880
|
+
});
|
|
881
|
+
|
|
882
|
+
test("a signed-in member reads a policy of their own project", async () => {
|
|
883
|
+
const req: ExpressRequest = buildSignedInRequest({
|
|
884
|
+
tenantId: projectId,
|
|
885
|
+
userId: callerUserId,
|
|
886
|
+
userType: UserType.User,
|
|
887
|
+
permissions: [Permission.ProjectMember],
|
|
888
|
+
});
|
|
889
|
+
|
|
890
|
+
(req as unknown as { params: Dictionary<string> }).params = {
|
|
891
|
+
policyId: policyId.toString(),
|
|
892
|
+
};
|
|
893
|
+
|
|
894
|
+
const result: RouteCallResult = await callRoute({
|
|
895
|
+
method: "GET",
|
|
896
|
+
uri: POLICY_ROUTE,
|
|
897
|
+
req: req,
|
|
898
|
+
signedIn: true,
|
|
899
|
+
});
|
|
900
|
+
|
|
901
|
+
expect(result.thrownToNext).toBeUndefined();
|
|
902
|
+
expect(policyReadinessSpy).toHaveBeenCalledTimes(1);
|
|
903
|
+
});
|
|
904
|
+
|
|
905
|
+
test("a signed-in caller who is not a member of the named project still gets the 422", async () => {
|
|
906
|
+
const req: ExpressRequest = buildSignedInRequest({
|
|
907
|
+
tenantId: otherProjectId,
|
|
908
|
+
userId: callerUserId,
|
|
909
|
+
userType: UserType.User,
|
|
910
|
+
permissions: [Permission.ProjectMember],
|
|
911
|
+
});
|
|
912
|
+
|
|
913
|
+
(req as OneUptimeRequest).tenantId = projectId;
|
|
914
|
+
req.headers["tenantid"] = projectId.toString();
|
|
915
|
+
|
|
916
|
+
const result: RouteCallResult = await callRoute({
|
|
917
|
+
method: "GET",
|
|
918
|
+
uri: PROJECT_ROUTE,
|
|
919
|
+
req: req,
|
|
920
|
+
signedIn: true,
|
|
921
|
+
});
|
|
922
|
+
|
|
923
|
+
expect(result.thrownToNext).toBeInstanceOf(NotAuthorizedException);
|
|
924
|
+
expect(result.status).toBe(422);
|
|
925
|
+
expect(result.errorBody?.["message"]).toBe(REFUSAL);
|
|
926
|
+
expectNoReadinessWork();
|
|
927
|
+
});
|
|
928
|
+
});
|