@oneuptime/common 12.0.27 → 12.0.29
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/RumSessionChunk.ts +52 -0
- package/Models/DatabaseModels/CloudResource.ts +17 -1
- package/Models/DatabaseModels/DashboardDomain.ts +43 -0
- package/Models/DatabaseModels/MonitorTemplate.ts +15 -3
- package/Models/DatabaseModels/RumApplication.ts +35 -2
- package/Models/DatabaseModels/StatusPageDomain.ts +43 -0
- package/Server/API/DashboardDomainAPI.ts +74 -0
- package/Server/API/MicrosoftTeamsAPI.ts +81 -180
- package/Server/API/StatusPageDomainAPI.ts +74 -0
- package/Server/API/TelemetryAPI.ts +104 -15
- package/Server/Infrastructure/Postgres/SchemaMigrations/1790200000000-AllowNullMonitorNameOnMonitorTemplate.ts +43 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1790300000000-AddCertificateReissueRequestedAtToDomains.ts +40 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +4 -0
- package/Server/Middleware/ProjectAuthorization.ts +6 -1
- package/Server/Services/DashboardDomainService.ts +131 -0
- package/Server/Services/LogPipelineProcessorService.ts +88 -0
- package/Server/Services/MonitorTemplateService.ts +70 -11
- package/Server/Services/StatusPageDomainService.ts +163 -3
- package/Server/Services/StatusPagePrivateUserSessionService.ts +197 -56
- package/Server/Types/Domain.ts +17 -0
- package/Server/Utils/APIKey/AccessPermission.ts +24 -0
- package/Server/Utils/Greenlock/Greenlock.ts +156 -58
- package/Server/Utils/LogPipelineProcessorValidation.ts +112 -0
- package/Server/Utils/SecurityEvent/GoogleSecOps/GoogleSecOpsClient.ts +1094 -45
- package/Server/Utils/SecurityEvent/GoogleSecOps/GoogleSecOpsPoller.ts +153 -17
- package/Server/Utils/SessionReplay/SessionReplayGateCache.ts +12 -5
- package/Server/Utils/SessionReplay/SessionReplayIdentity.ts +102 -0
- package/Server/Utils/SessionReplay/SessionReplayReadService.ts +11 -2
- package/Server/Utils/Workspace/MicrosoftTeams/Actions/Alert.ts +23 -11
- package/Server/Utils/Workspace/MicrosoftTeams/Actions/Auth.ts +0 -170
- package/Server/Utils/Workspace/MicrosoftTeams/Actions/Authorization.ts +158 -0
- package/Server/Utils/Workspace/MicrosoftTeams/Actions/Incident.ts +23 -11
- package/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.ts +35 -13
- package/Tests/App/Dashboard/MonitorTemplateColumnAndFacet.test.tsx +505 -0
- package/Tests/App/StatusPage/StatusPageLoginCodeBootstrap.test.ts +106 -0
- package/Tests/App/StatusPage/StatusPageLoginCodeUtil.test.ts +84 -0
- package/Tests/App/StatusPage/StatusPageRedirectSafety.test.ts +119 -0
- package/Tests/Models/DatabaseModels/SessionReplayModels.test.ts +81 -0
- package/Tests/Models/MonitorTemplateMonitorNameColumn.test.ts +258 -0
- package/Tests/Server/API/CustomDomainReissueSslAPI.test.ts +469 -0
- package/Tests/Server/API/MicrosoftTeamsBotMessagesEndpoint.test.ts +729 -0
- package/Tests/Server/API/MicrosoftTeamsBotTestEndpoint.test.ts +26 -0
- package/Tests/Server/API/MicrosoftTeamsLegacyWebhookRemoval.test.ts +402 -0
- package/Tests/Server/API/SessionReplayAPI.test.ts +232 -0
- package/Tests/Server/Middleware/ProjectAuthorizationApiKeyMiddleware.test.ts +4 -0
- package/Tests/Server/Services/AddCertificateReissueRequestedAtToDomainsMigration.test.ts +287 -0
- package/Tests/Server/Services/CustomDomainCertificateReissue.test.ts +477 -0
- package/Tests/Server/Services/LogPipelineProcessorSaveValidation.test.ts +249 -0
- package/Tests/Server/Services/MonitorTemplateServiceBulkSync.test.ts +549 -0
- package/Tests/Server/Services/MonitorTemplateServiceNetworkDeviceSync.test.ts +16 -3
- package/Tests/Server/Services/NetworkDeviceAutoImportRuleEngineService.test.ts +57 -0
- package/Tests/Server/Services/SecurityEventLastErrorColumnWidth.test.ts +151 -15
- package/Tests/Server/Services/StatusPagePrivateUserSessionService.test.ts +587 -0
- package/Tests/Server/Types/Database/Permissions/ApiKeyFileAccess.test.ts +286 -0
- package/Tests/Server/Types/Database/Permissions/MonitorTemplateColumnAccess.test.ts +328 -0
- package/Tests/Server/Types/Database/QueryHelperFindWithSameTextAndIsNull.test.ts +83 -0
- package/Tests/Server/Utils/AI/SRE/ConfidenceSignal.test.ts +377 -0
- package/Tests/Server/Utils/APIKey/AccessPermission.test.ts +16 -1
- package/Tests/Server/Utils/Greenlock/CertificateRenewalSchedule.test.ts +250 -0
- package/Tests/Server/Utils/LogPipelineProcessorValidation.test.ts +198 -0
- package/Tests/Server/Utils/SecurityEvent/ConnectorErrorMessage.test.ts +48 -9
- package/Tests/Server/Utils/SecurityEvent/GoogleSecOpsAuth.test.ts +726 -0
- package/Tests/Server/Utils/SecurityEvent/GoogleSecOpsClient.test.ts +74 -11
- package/Tests/Server/Utils/SecurityEvent/GoogleSecOpsErrorHandling.test.ts +1107 -0
- package/Tests/Server/Utils/SecurityEvent/GoogleSecOpsPoller.test.ts +20 -3
- package/Tests/Server/Utils/SecurityEvent/GoogleSecOpsPollerFailureTaxonomy.test.ts +51 -11
- package/Tests/Server/Utils/SecurityEvent/GoogleSecOpsPollerHardening.test.ts +1246 -0
- package/Tests/Server/Utils/SecurityEvent/GoogleSecOpsRequestContract.test.ts +961 -0
- package/Tests/Server/Utils/SecurityEvent/GoogleSecOpsResponseParsing.test.ts +653 -0
- package/Tests/Server/Utils/Workspace/MicrosoftTeamsActionAuthorization.test.ts +880 -0
- package/Tests/Types/API/HTTPResponse.test.ts +175 -0
- package/Tests/Types/API/Route.test.ts +36 -4
- package/Tests/Types/Database/DatabaseProperty.test.ts +171 -0
- package/Tests/Types/DateLocalShortDateTimeString.test.ts +518 -0
- package/Tests/Types/DateLocalTimeString.test.ts +259 -0
- package/Tests/Types/DateUserPrefers12HourFormat.test.ts +316 -0
- package/Tests/UI/Components/LogsHistogram.test.tsx +41 -2
- package/Tests/UI/Components/LogsViewerEmptyStateTimeRange.test.tsx +212 -0
- package/Tests/UI/Components/TelemetryChartClockFormat.test.tsx +359 -0
- package/Tests/UI/Components/TimeRangePickerClockFormat.test.tsx +422 -0
- package/Tests/UI/Components/TimeRangePickerDropdown.test.tsx +146 -8
- package/Tests/UI/MicrosoftTeams/MicrosoftTeamsMessagingEndpointGuidance.test.tsx +520 -0
- package/Tests/UI/Utils/Navigation.test.ts +82 -0
- package/Tests/Utils/Array.test.ts +100 -0
- package/Tests/Utils/CertificateReissue.test.ts +225 -0
- package/Tests/Utils/Grok/Grok.test.ts +492 -0
- package/Tests/Utils/Monitor/NetworkDeviceMonitorTemplateUtil.test.ts +181 -13
- package/Types/API/Route.ts +20 -5
- package/Types/Date.ts +143 -13
- package/Types/Rum/SessionReplay.ts +76 -1
- package/UI/Components/Date/TimeRangePickerDropdown.tsx +9 -5
- package/UI/Components/LogsViewer/LogsViewer.tsx +12 -7
- package/UI/Components/LogsViewer/components/HistogramTooltip.tsx +18 -17
- package/UI/Components/LogsViewer/components/LogsAnalyticsView.tsx +21 -28
- package/UI/Components/LogsViewer/components/LogsHistogram.tsx +2 -4
- package/UI/Components/TelemetryViewer/components/TelemetryHistogram.tsx +2 -4
- package/UI/Components/TelemetryViewer/components/TelemetryHistogramTooltip.tsx +17 -16
- package/UI/Utils/Navigation.ts +42 -0
- package/Utils/Array.ts +37 -0
- package/Utils/CertificateReissue.ts +128 -0
- package/Utils/Grok/Grok.ts +461 -0
- package/Utils/Grok/GrokPatterns.ts +118 -0
- package/Utils/Monitor/NetworkDeviceMonitorTemplateUtil.ts +58 -6
- package/build/dist/Models/AnalyticsModels/RumSessionChunk.js +48 -0
- package/build/dist/Models/AnalyticsModels/RumSessionChunk.js.map +1 -1
- package/build/dist/Models/DatabaseModels/CloudResource.js +19 -2
- package/build/dist/Models/DatabaseModels/CloudResource.js.map +1 -1
- package/build/dist/Models/DatabaseModels/DashboardDomain.js +44 -0
- package/build/dist/Models/DatabaseModels/DashboardDomain.js.map +1 -1
- package/build/dist/Models/DatabaseModels/MonitorTemplate.js +17 -4
- package/build/dist/Models/DatabaseModels/MonitorTemplate.js.map +1 -1
- package/build/dist/Models/DatabaseModels/RumApplication.js +37 -3
- package/build/dist/Models/DatabaseModels/RumApplication.js.map +1 -1
- package/build/dist/Models/DatabaseModels/StatusPageDomain.js +44 -0
- package/build/dist/Models/DatabaseModels/StatusPageDomain.js.map +1 -1
- package/build/dist/Server/API/DashboardDomainAPI.js +43 -1
- package/build/dist/Server/API/DashboardDomainAPI.js.map +1 -1
- package/build/dist/Server/API/MicrosoftTeamsAPI.js +74 -118
- package/build/dist/Server/API/MicrosoftTeamsAPI.js.map +1 -1
- package/build/dist/Server/API/StatusPageDomainAPI.js +43 -1
- package/build/dist/Server/API/StatusPageDomainAPI.js.map +1 -1
- package/build/dist/Server/API/TelemetryAPI.js +67 -14
- package/build/dist/Server/API/TelemetryAPI.js.map +1 -1
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1790200000000-AllowNullMonitorNameOnMonitorTemplate.js +34 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1790200000000-AllowNullMonitorNameOnMonitorTemplate.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1790300000000-AddCertificateReissueRequestedAtToDomains.js +28 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1790300000000-AddCertificateReissueRequestedAtToDomains.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +4 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
- package/build/dist/Server/Middleware/ProjectAuthorization.js +6 -1
- package/build/dist/Server/Middleware/ProjectAuthorization.js.map +1 -1
- package/build/dist/Server/Services/DashboardDomainService.js +104 -0
- package/build/dist/Server/Services/DashboardDomainService.js.map +1 -1
- package/build/dist/Server/Services/LogPipelineProcessorService.js +65 -0
- package/build/dist/Server/Services/LogPipelineProcessorService.js.map +1 -1
- package/build/dist/Server/Services/MonitorTemplateService.js +57 -11
- package/build/dist/Server/Services/MonitorTemplateService.js.map +1 -1
- package/build/dist/Server/Services/StatusPageDomainService.js +132 -3
- package/build/dist/Server/Services/StatusPageDomainService.js.map +1 -1
- package/build/dist/Server/Services/StatusPagePrivateUserSessionService.js +112 -32
- package/build/dist/Server/Services/StatusPagePrivateUserSessionService.js.map +1 -1
- package/build/dist/Server/Types/Domain.js +16 -0
- package/build/dist/Server/Types/Domain.js.map +1 -1
- package/build/dist/Server/Utils/APIKey/AccessPermission.js +24 -0
- package/build/dist/Server/Utils/APIKey/AccessPermission.js.map +1 -1
- package/build/dist/Server/Utils/Greenlock/Greenlock.js +110 -35
- package/build/dist/Server/Utils/Greenlock/Greenlock.js.map +1 -1
- package/build/dist/Server/Utils/LogPipelineProcessorValidation.js +77 -0
- package/build/dist/Server/Utils/LogPipelineProcessorValidation.js.map +1 -0
- package/build/dist/Server/Utils/SecurityEvent/GoogleSecOps/GoogleSecOpsClient.js +777 -36
- package/build/dist/Server/Utils/SecurityEvent/GoogleSecOps/GoogleSecOpsClient.js.map +1 -1
- package/build/dist/Server/Utils/SecurityEvent/GoogleSecOps/GoogleSecOpsPoller.js +108 -17
- package/build/dist/Server/Utils/SecurityEvent/GoogleSecOps/GoogleSecOpsPoller.js.map +1 -1
- package/build/dist/Server/Utils/SessionReplay/SessionReplayGateCache.js.map +1 -1
- package/build/dist/Server/Utils/SessionReplay/SessionReplayIdentity.js +59 -0
- package/build/dist/Server/Utils/SessionReplay/SessionReplayIdentity.js.map +1 -0
- package/build/dist/Server/Utils/SessionReplay/SessionReplayReadService.js +9 -2
- package/build/dist/Server/Utils/SessionReplay/SessionReplayReadService.js.map +1 -1
- package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/Alert.js +17 -6
- package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/Alert.js.map +1 -1
- package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/Auth.js +0 -159
- package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/Auth.js.map +1 -1
- package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/Authorization.js +128 -0
- package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/Authorization.js.map +1 -0
- package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/Incident.js +17 -6
- package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/Incident.js.map +1 -1
- package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.js +25 -13
- package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.js.map +1 -1
- package/build/dist/Types/API/Route.js +16 -5
- package/build/dist/Types/API/Route.js.map +1 -1
- package/build/dist/Types/Date.js +115 -13
- package/build/dist/Types/Date.js.map +1 -1
- package/build/dist/Types/Rum/SessionReplay.js +38 -0
- package/build/dist/Types/Rum/SessionReplay.js.map +1 -1
- package/build/dist/UI/Components/Date/TimeRangePickerDropdown.js +9 -5
- package/build/dist/UI/Components/Date/TimeRangePickerDropdown.js.map +1 -1
- package/build/dist/UI/Components/LogsViewer/LogsViewer.js +12 -7
- package/build/dist/UI/Components/LogsViewer/LogsViewer.js.map +1 -1
- package/build/dist/UI/Components/LogsViewer/components/HistogramTooltip.js +15 -14
- package/build/dist/UI/Components/LogsViewer/components/HistogramTooltip.js.map +1 -1
- package/build/dist/UI/Components/LogsViewer/components/LogsAnalyticsView.js +17 -22
- package/build/dist/UI/Components/LogsViewer/components/LogsAnalyticsView.js.map +1 -1
- package/build/dist/UI/Components/LogsViewer/components/LogsHistogram.js +2 -4
- package/build/dist/UI/Components/LogsViewer/components/LogsHistogram.js.map +1 -1
- package/build/dist/UI/Components/TelemetryViewer/components/TelemetryHistogram.js +2 -4
- package/build/dist/UI/Components/TelemetryViewer/components/TelemetryHistogram.js.map +1 -1
- package/build/dist/UI/Components/TelemetryViewer/components/TelemetryHistogramTooltip.js +15 -14
- package/build/dist/UI/Components/TelemetryViewer/components/TelemetryHistogramTooltip.js.map +1 -1
- package/build/dist/UI/Utils/Navigation.js +30 -0
- package/build/dist/UI/Utils/Navigation.js.map +1 -1
- package/build/dist/Utils/Array.js +25 -0
- package/build/dist/Utils/Array.js.map +1 -1
- package/build/dist/Utils/CertificateReissue.js +97 -0
- package/build/dist/Utils/CertificateReissue.js.map +1 -0
- package/build/dist/Utils/Grok/Grok.js +324 -0
- package/build/dist/Utils/Grok/Grok.js.map +1 -0
- package/build/dist/Utils/Grok/GrokPatterns.js +112 -0
- package/build/dist/Utils/Grok/GrokPatterns.js.map +1 -0
- package/build/dist/Utils/Monitor/NetworkDeviceMonitorTemplateUtil.js +63 -3
- package/build/dist/Utils/Monitor/NetworkDeviceMonitorTemplateUtil.js.map +1 -1
- package/package.json +1 -1
- package/UI/Utils/JsonWebToken.ts +0 -14
- package/build/dist/UI/Utils/JsonWebToken.js +0 -10
- package/build/dist/UI/Utils/JsonWebToken.js.map +0 -1
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
import { describe, expect, jest, test } from "@jest/globals";
|
|
2
|
+
|
|
3
|
+
/*
|
|
4
|
+
* PasswordHash has a known, pre-existing TS5.9 compile failure under ts-jest
|
|
5
|
+
* (Buffer vs BinaryLike) that breaks every suite whose import graph reaches
|
|
6
|
+
* it. APIKeyAccessPermission pulls in ApiKeyPermissionService, which reaches
|
|
7
|
+
* it; nothing here touches password hashing, so stub it before that graph is
|
|
8
|
+
* compiled. Same reason and same shape as the stub in
|
|
9
|
+
* Tests/Server/Utils/APIKey/AccessPermission.test.ts.
|
|
10
|
+
*/
|
|
11
|
+
jest.mock("../../../../../Server/Utils/PasswordHash", () => {
|
|
12
|
+
return {
|
|
13
|
+
__esModule: true,
|
|
14
|
+
default: class PasswordHashStub {},
|
|
15
|
+
};
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
import DatabaseRequestType from "../../../../../Server/Types/BaseDatabase/DatabaseRequestType";
|
|
19
|
+
import CreatePermission from "../../../../../Server/Types/Database/Permissions/CreatePermission";
|
|
20
|
+
import TenantPermission from "../../../../../Server/Types/Database/Permissions/TenantPermission";
|
|
21
|
+
import APIKeyAccessPermission from "../../../../../Server/Utils/APIKey/AccessPermission";
|
|
22
|
+
import AllModelTypes from "../../../../../Models/DatabaseModels/Index";
|
|
23
|
+
import BaseModel from "../../../../../Models/DatabaseModels/DatabaseBaseModel/DatabaseBaseModel";
|
|
24
|
+
import File from "../../../../../Models/DatabaseModels/File";
|
|
25
|
+
import DatabaseCommonInteractionProps from "../../../../../Types/BaseDatabase/DatabaseCommonInteractionProps";
|
|
26
|
+
import MimeType from "../../../../../Types/File/MimeType";
|
|
27
|
+
import ObjectID from "../../../../../Types/ObjectID";
|
|
28
|
+
import Permission, {
|
|
29
|
+
UserGlobalAccessPermission,
|
|
30
|
+
UserPermission,
|
|
31
|
+
UserTenantAccessPermission,
|
|
32
|
+
} from "../../../../../Types/Permission";
|
|
33
|
+
import UserType from "../../../../../Types/UserType";
|
|
34
|
+
|
|
35
|
+
/*
|
|
36
|
+
* Contract under test: an API key is not a logged-in user, and File's gate must
|
|
37
|
+
* not treat it as one.
|
|
38
|
+
*
|
|
39
|
+
* File and FileModel gate on [CurrentUser, AuthenticatedRequest]. CurrentUser is
|
|
40
|
+
* auto-granted to every authenticated caller, so a key that holds nothing else
|
|
41
|
+
* from that list intersects down to CurrentUser alone — and that is exactly the
|
|
42
|
+
* condition TenantPermission.isAccessGrantedOnlyByCurrentUser reports as "this
|
|
43
|
+
* caller is here as nothing more than some logged-in user". A caller in that
|
|
44
|
+
* state is confined to rows they own, which a key can never be: it carries no
|
|
45
|
+
* userId, and both the query-side scope and the create-side ownership check
|
|
46
|
+
* reject it outright rather than run unscoped.
|
|
47
|
+
*
|
|
48
|
+
* Granting Permission.AuthenticatedRequest to API keys is what makes that
|
|
49
|
+
* predicate answer the truth. The permission exists for precisely this - it is a
|
|
50
|
+
* marker for "some authenticated principal made this call", not a capability an
|
|
51
|
+
* administrator grants, and File/FileModel are the only models that name it.
|
|
52
|
+
*
|
|
53
|
+
* Worth being exact about what is and is not broken today, because it decides
|
|
54
|
+
* what this test is for. File declares no user column, so
|
|
55
|
+
* CreatePermission.checkCreateOwnership returns before the userId guard and file
|
|
56
|
+
* creation by API key does NOT currently fail - the third test below pins that
|
|
57
|
+
* it passes both before and after. The predicate is still answering wrongly, and
|
|
58
|
+
* the moment File gains a user column, or a second model adopts the same
|
|
59
|
+
* [CurrentUser, AuthenticatedRequest] pair, the wrong answer stops being
|
|
60
|
+
* harmless. This suite pins the predicate itself rather than the symptom, so
|
|
61
|
+
* that the grant cannot be quietly dropped as unused.
|
|
62
|
+
*/
|
|
63
|
+
|
|
64
|
+
const projectId: ObjectID = ObjectID.generate();
|
|
65
|
+
const userId: ObjectID = ObjectID.generate();
|
|
66
|
+
|
|
67
|
+
/*
|
|
68
|
+
* The tenant half of an API key's permissions. getApiTenantAccessPermission
|
|
69
|
+
* returns the default baseline plus whatever rows the key was configured with;
|
|
70
|
+
* built here rather than called so the suite stays free of the service and its
|
|
71
|
+
* cache. A key with no configured rows is the interesting case - it is the one
|
|
72
|
+
* that has nothing but the auto-granted permissions to intersect with.
|
|
73
|
+
*/
|
|
74
|
+
function tenantPermissions(): UserTenantAccessPermission {
|
|
75
|
+
const permissions: Array<UserPermission> = [
|
|
76
|
+
Permission.CurrentUser,
|
|
77
|
+
Permission.UnAuthorizedSsoUser,
|
|
78
|
+
].map((permission: Permission) => {
|
|
79
|
+
return {
|
|
80
|
+
permission: permission,
|
|
81
|
+
labelIds: [],
|
|
82
|
+
isBlockPermission: false,
|
|
83
|
+
_type: "UserPermission",
|
|
84
|
+
};
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
return {
|
|
88
|
+
projectId,
|
|
89
|
+
permissions,
|
|
90
|
+
isBlockPermission: false,
|
|
91
|
+
_type: "UserTenantAccessPermission",
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function apiKeyProps(
|
|
96
|
+
globalPermissions: Array<Permission>,
|
|
97
|
+
): DatabaseCommonInteractionProps {
|
|
98
|
+
return {
|
|
99
|
+
tenantId: projectId,
|
|
100
|
+
userType: UserType.API,
|
|
101
|
+
userGlobalAccessPermission: {
|
|
102
|
+
projectIds: [projectId],
|
|
103
|
+
globalPermissions: globalPermissions,
|
|
104
|
+
_type: "UserGlobalAccessPermission",
|
|
105
|
+
},
|
|
106
|
+
userTenantAccessPermission: {
|
|
107
|
+
[projectId.toString()]: tenantPermissions(),
|
|
108
|
+
},
|
|
109
|
+
} as unknown as DatabaseCommonInteractionProps;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/*
|
|
113
|
+
* The list as it stood before AuthenticatedRequest was granted. Kept literal
|
|
114
|
+
* rather than derived: it is the negative control, and a control that tracks the
|
|
115
|
+
* production list would silently stop controlling for anything.
|
|
116
|
+
*/
|
|
117
|
+
const GLOBAL_PERMISSIONS_WITHOUT_MARKER: Array<Permission> = [
|
|
118
|
+
Permission.Public,
|
|
119
|
+
Permission.User,
|
|
120
|
+
Permission.CurrentUser,
|
|
121
|
+
];
|
|
122
|
+
|
|
123
|
+
function newFile(): File {
|
|
124
|
+
const file: File = new File();
|
|
125
|
+
file.name = "screenshot.png";
|
|
126
|
+
file.fileType = MimeType.png;
|
|
127
|
+
file.file = Buffer.from("not-really-a-png");
|
|
128
|
+
file.isPublic = false;
|
|
129
|
+
return file;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/*
|
|
133
|
+
* Every model that names AuthenticatedRequest anywhere in its table or column
|
|
134
|
+
* access control, by model name.
|
|
135
|
+
*/
|
|
136
|
+
function modelsNamingTheMarker(): Array<string> {
|
|
137
|
+
const naming: Array<string> = [];
|
|
138
|
+
|
|
139
|
+
for (const modelType of AllModelTypes as Array<{ new (): BaseModel }>) {
|
|
140
|
+
const model: BaseModel = new modelType();
|
|
141
|
+
|
|
142
|
+
const permissions: Array<Permission> = [
|
|
143
|
+
...(model.createRecordPermissions || []),
|
|
144
|
+
...(model.readRecordPermissions || []),
|
|
145
|
+
...(model.updateRecordPermissions || []),
|
|
146
|
+
...(model.deleteRecordPermissions || []),
|
|
147
|
+
];
|
|
148
|
+
|
|
149
|
+
for (const column of model.getTableColumns().columns) {
|
|
150
|
+
const accessControl: ReturnType<BaseModel["getColumnAccessControlFor"]> =
|
|
151
|
+
model.getColumnAccessControlFor(column);
|
|
152
|
+
|
|
153
|
+
if (!accessControl) {
|
|
154
|
+
continue;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
permissions.push(
|
|
158
|
+
...(accessControl.create || []),
|
|
159
|
+
...(accessControl.read || []),
|
|
160
|
+
...(accessControl.update || []),
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (permissions.includes(Permission.AuthenticatedRequest)) {
|
|
165
|
+
naming.push(modelType.name);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return naming.sort();
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
describe("API key access to File", () => {
|
|
173
|
+
test("no model other than File gates on the marker", () => {
|
|
174
|
+
/*
|
|
175
|
+
* The grant is unconditional - every API key in every project holds
|
|
176
|
+
* AuthenticatedRequest, and unlike a role permission no administrator can
|
|
177
|
+
* revoke it. What keeps that safe is not the grant but this list: the
|
|
178
|
+
* marker is deliberately absent from
|
|
179
|
+
* TenantPermission.AUTO_GRANTED_TENANT_PERMISSIONS, so any model that
|
|
180
|
+
* starts naming it hands every API key a role-grade way through its gate.
|
|
181
|
+
*
|
|
182
|
+
* Sweeping the models rather than trusting a grep is the point. A new
|
|
183
|
+
* model quietly adopting the [CurrentUser, AuthenticatedRequest] pair is
|
|
184
|
+
* the one change that turns this from bounded to unbounded, and it should
|
|
185
|
+
* fail here and be argued for rather than land unnoticed.
|
|
186
|
+
*
|
|
187
|
+
* This list must only ever shrink. A new entry needs its own review.
|
|
188
|
+
*/
|
|
189
|
+
expect(modelsNamingTheMarker()).toEqual(["File"]);
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
test("the production global permission list carries the AuthenticatedRequest marker", async () => {
|
|
193
|
+
const permission: UserGlobalAccessPermission =
|
|
194
|
+
await APIKeyAccessPermission.getDefaultApiGlobalPermission(projectId);
|
|
195
|
+
|
|
196
|
+
expect(permission.globalPermissions).toContain(
|
|
197
|
+
Permission.AuthenticatedRequest,
|
|
198
|
+
);
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
test("a key holding the marker is not mistaken for a bare logged-in user", async () => {
|
|
202
|
+
const permission: UserGlobalAccessPermission =
|
|
203
|
+
await APIKeyAccessPermission.getDefaultApiGlobalPermission(projectId);
|
|
204
|
+
|
|
205
|
+
/*
|
|
206
|
+
* Driven off the real list rather than a copy of it: this is the assertion
|
|
207
|
+
* that fails if the grant is ever removed.
|
|
208
|
+
*/
|
|
209
|
+
expect(
|
|
210
|
+
TenantPermission.isAccessGrantedOnlyByCurrentUser(
|
|
211
|
+
File,
|
|
212
|
+
apiKeyProps(permission.globalPermissions),
|
|
213
|
+
DatabaseRequestType.Create,
|
|
214
|
+
),
|
|
215
|
+
).toBe(false);
|
|
216
|
+
|
|
217
|
+
// The negative control: without the marker the predicate answers wrongly.
|
|
218
|
+
expect(
|
|
219
|
+
TenantPermission.isAccessGrantedOnlyByCurrentUser(
|
|
220
|
+
File,
|
|
221
|
+
apiKeyProps(GLOBAL_PERMISSIONS_WITHOUT_MARKER),
|
|
222
|
+
DatabaseRequestType.Create,
|
|
223
|
+
),
|
|
224
|
+
).toBe(true);
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
test("file creation by API key is allowed either way, because File has no user column", async () => {
|
|
228
|
+
/*
|
|
229
|
+
* Pinned deliberately. The PR that added the grant described this path as
|
|
230
|
+
* broken; it is not, and recording that here keeps the next reader from
|
|
231
|
+
* re-diagnosing a failure that was never happening. What holds it up is
|
|
232
|
+
* File declaring no user column - assert that too, so if File ever gains
|
|
233
|
+
* one this test says which assumption moved.
|
|
234
|
+
*/
|
|
235
|
+
expect(new File().getUserColumn()).toBeFalsy();
|
|
236
|
+
|
|
237
|
+
const permission: UserGlobalAccessPermission =
|
|
238
|
+
await APIKeyAccessPermission.getDefaultApiGlobalPermission(projectId);
|
|
239
|
+
|
|
240
|
+
expect(() => {
|
|
241
|
+
return CreatePermission.checkCreatePermissions(
|
|
242
|
+
File,
|
|
243
|
+
newFile(),
|
|
244
|
+
apiKeyProps(permission.globalPermissions),
|
|
245
|
+
);
|
|
246
|
+
}).not.toThrow();
|
|
247
|
+
|
|
248
|
+
expect(() => {
|
|
249
|
+
return CreatePermission.checkCreatePermissions(
|
|
250
|
+
File,
|
|
251
|
+
newFile(),
|
|
252
|
+
apiKeyProps(GLOBAL_PERMISSIONS_WITHOUT_MARKER),
|
|
253
|
+
);
|
|
254
|
+
}).not.toThrow();
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
test("a logged-in user still reaches File without the marker", () => {
|
|
258
|
+
/*
|
|
259
|
+
* AccessTokenService hands a user session [Public, User, CurrentUser] and no
|
|
260
|
+
* AuthenticatedRequest, so users continue to arrive through CurrentUser and
|
|
261
|
+
* are unaffected by the grant. Pinned because the asymmetry is deliberate
|
|
262
|
+
* and reads like an oversight otherwise.
|
|
263
|
+
*/
|
|
264
|
+
const userProps: DatabaseCommonInteractionProps = {
|
|
265
|
+
tenantId: projectId,
|
|
266
|
+
userId: userId,
|
|
267
|
+
userType: UserType.User,
|
|
268
|
+
userGlobalAccessPermission: {
|
|
269
|
+
projectIds: [projectId],
|
|
270
|
+
globalPermissions: GLOBAL_PERMISSIONS_WITHOUT_MARKER,
|
|
271
|
+
_type: "UserGlobalAccessPermission",
|
|
272
|
+
},
|
|
273
|
+
userTenantAccessPermission: {
|
|
274
|
+
[projectId.toString()]: tenantPermissions(),
|
|
275
|
+
},
|
|
276
|
+
} as unknown as DatabaseCommonInteractionProps;
|
|
277
|
+
|
|
278
|
+
expect(() => {
|
|
279
|
+
return CreatePermission.checkCreatePermissions(
|
|
280
|
+
File,
|
|
281
|
+
newFile(),
|
|
282
|
+
userProps,
|
|
283
|
+
);
|
|
284
|
+
}).not.toThrow();
|
|
285
|
+
});
|
|
286
|
+
});
|
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
import QueryPermission from "../../../../../Server/Types/Database/Permissions/QueryPermission";
|
|
2
|
+
import Query from "../../../../../Server/Types/Database/Query";
|
|
3
|
+
import Select from "../../../../../Server/Types/Database/Select";
|
|
4
|
+
import Monitor from "../../../../../Models/DatabaseModels/Monitor";
|
|
5
|
+
import MonitorTemplate from "../../../../../Models/DatabaseModels/MonitorTemplate";
|
|
6
|
+
import BaseModel from "../../../../../Models/DatabaseModels/DatabaseBaseModel/DatabaseBaseModel";
|
|
7
|
+
import DatabaseCommonInteractionProps from "../../../../../Types/BaseDatabase/DatabaseCommonInteractionProps";
|
|
8
|
+
import Includes from "../../../../../Types/BaseDatabase/Includes";
|
|
9
|
+
import IsNull from "../../../../../Types/BaseDatabase/IsNull";
|
|
10
|
+
import NotNull from "../../../../../Types/BaseDatabase/NotNull";
|
|
11
|
+
import { TableColumnMetadata } from "../../../../../Types/Database/TableColumn";
|
|
12
|
+
import TableColumnType from "../../../../../Types/Database/TableColumnType";
|
|
13
|
+
import ObjectID from "../../../../../Types/ObjectID";
|
|
14
|
+
import Permission from "../../../../../Types/Permission";
|
|
15
|
+
import { describe, expect, it } from "@jest/globals";
|
|
16
|
+
|
|
17
|
+
/*
|
|
18
|
+
* The server half of the monitor list's Template column and Template chip
|
|
19
|
+
* (issue #3491).
|
|
20
|
+
*
|
|
21
|
+
* Both of them are one line of UI and one permission rule, and the permission
|
|
22
|
+
* rule is the half that fails loudly for everybody at once:
|
|
23
|
+
*
|
|
24
|
+
* - the column selects `monitorTemplate: { _id, templateName }`, and
|
|
25
|
+
* QueryPermission.checkRelationQueryPermission THROWS on the first inner key
|
|
26
|
+
* that is not marked `canReadOnRelationQuery` rather than dropping it — so a
|
|
27
|
+
* missing flag does not hide the column, it fails the whole monitors list
|
|
28
|
+
* request, for every caller up to ProjectOwner.
|
|
29
|
+
*
|
|
30
|
+
* - the chip filters `monitorTemplateId`, and a WHERE clause is held to the
|
|
31
|
+
* same standard as a SELECT: the column has to be one the caller may read,
|
|
32
|
+
* or the list 403s the moment the chip is set.
|
|
33
|
+
*
|
|
34
|
+
* The selects and queries below are written out as literals rather than
|
|
35
|
+
* imported from the dashboard, so that changing the UI without changing the
|
|
36
|
+
* model surfaces here.
|
|
37
|
+
*/
|
|
38
|
+
|
|
39
|
+
const projectId: ObjectID = ObjectID.generate();
|
|
40
|
+
const userId: ObjectID = ObjectID.generate();
|
|
41
|
+
const templateId: ObjectID = ObjectID.generate();
|
|
42
|
+
|
|
43
|
+
/*
|
|
44
|
+
* Fresh props per assertion: DatabaseCommonInteractionPropsUtil MUTATES what it
|
|
45
|
+
* is handed, pushing the auto-granted Public and CurrentUser onto the list, so
|
|
46
|
+
* a shared object carries state between tests.
|
|
47
|
+
*/
|
|
48
|
+
function makeProps(
|
|
49
|
+
permissions: Array<Permission>,
|
|
50
|
+
): DatabaseCommonInteractionProps {
|
|
51
|
+
return {
|
|
52
|
+
userId: userId,
|
|
53
|
+
tenantId: projectId,
|
|
54
|
+
userTenantAccessPermission: {
|
|
55
|
+
[projectId.toString()]: {
|
|
56
|
+
projectId: projectId,
|
|
57
|
+
permissions: permissions.map((permission: Permission) => {
|
|
58
|
+
return {
|
|
59
|
+
permission: permission,
|
|
60
|
+
labelIds: [],
|
|
61
|
+
isBlockPermission: false,
|
|
62
|
+
_type: "UserPermission" as const,
|
|
63
|
+
};
|
|
64
|
+
}),
|
|
65
|
+
_type: "UserTenantAccessPermission",
|
|
66
|
+
},
|
|
67
|
+
},
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Everyone the monitor list is readable by. The failures this file guards
|
|
73
|
+
* against are in the permission layer and are not role-specific, so every
|
|
74
|
+
* assertion that matters is made across the whole read list rather than for one
|
|
75
|
+
* role — the on-call schedule regression this mirrors hit ProjectOwner too.
|
|
76
|
+
*/
|
|
77
|
+
const MONITOR_READERS: Array<Permission> = [
|
|
78
|
+
Permission.ProjectOwner,
|
|
79
|
+
Permission.ProjectAdmin,
|
|
80
|
+
Permission.ProjectMember,
|
|
81
|
+
Permission.Viewer,
|
|
82
|
+
Permission.MonitorAdmin,
|
|
83
|
+
Permission.MonitorMember,
|
|
84
|
+
Permission.MonitorViewer,
|
|
85
|
+
Permission.ReadProjectMonitor,
|
|
86
|
+
];
|
|
87
|
+
|
|
88
|
+
/*
|
|
89
|
+
* The select the Template column declares in
|
|
90
|
+
* App/FeatureSet/Dashboard/src/Components/Monitor/MonitorTable.tsx, alongside
|
|
91
|
+
* the fields the rest of that table asks for.
|
|
92
|
+
*/
|
|
93
|
+
const MONITOR_TABLE_SELECT: Select<Monitor> = {
|
|
94
|
+
_id: true,
|
|
95
|
+
name: true,
|
|
96
|
+
monitorType: true,
|
|
97
|
+
monitorTemplate: {
|
|
98
|
+
_id: true,
|
|
99
|
+
templateName: true,
|
|
100
|
+
},
|
|
101
|
+
} as Select<Monitor>;
|
|
102
|
+
|
|
103
|
+
function checkRelationSelect(
|
|
104
|
+
select: Select<Monitor>,
|
|
105
|
+
props: DatabaseCommonInteractionProps,
|
|
106
|
+
): void {
|
|
107
|
+
QueryPermission.checkRelationQueryPermission(
|
|
108
|
+
Monitor,
|
|
109
|
+
{} as Query<Monitor>,
|
|
110
|
+
select,
|
|
111
|
+
props,
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function checkQuery(
|
|
116
|
+
query: Query<Monitor>,
|
|
117
|
+
props: DatabaseCommonInteractionProps,
|
|
118
|
+
): void {
|
|
119
|
+
QueryPermission.checkQueryPermission(Monitor, query, props);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
describe("selecting a monitor's template through the relation", () => {
|
|
123
|
+
it("accepts the monitor table's select", () => {
|
|
124
|
+
expect(() => {
|
|
125
|
+
return checkRelationSelect(
|
|
126
|
+
MONITOR_TABLE_SELECT,
|
|
127
|
+
makeProps([Permission.ProjectOwner]),
|
|
128
|
+
);
|
|
129
|
+
}).not.toThrow();
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it("accepts it for every reader of the monitor list", () => {
|
|
133
|
+
for (const permission of MONITOR_READERS) {
|
|
134
|
+
expect(() => {
|
|
135
|
+
return checkRelationSelect(
|
|
136
|
+
MONITOR_TABLE_SELECT,
|
|
137
|
+
makeProps([permission]),
|
|
138
|
+
);
|
|
139
|
+
}).not.toThrow();
|
|
140
|
+
}
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it("accepts the template name selected on its own", () => {
|
|
144
|
+
// Without `_id` alongside it, in case the cell stops linking to the template.
|
|
145
|
+
expect(() => {
|
|
146
|
+
return checkRelationSelect(
|
|
147
|
+
{
|
|
148
|
+
monitorTemplate: {
|
|
149
|
+
templateName: true,
|
|
150
|
+
},
|
|
151
|
+
} as Select<Monitor>,
|
|
152
|
+
makeProps([Permission.ProjectMember]),
|
|
153
|
+
);
|
|
154
|
+
}).not.toThrow();
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
/*
|
|
158
|
+
* The flag is what makes the select legal, and it is on the related model
|
|
159
|
+
* rather than on Monitor — so nothing in the dashboard or in Monitor.ts
|
|
160
|
+
* mentions it, and dropping it is an easy, silent edit.
|
|
161
|
+
*/
|
|
162
|
+
it("is legal because templateName is readable through a relation", () => {
|
|
163
|
+
const template: BaseModel = new MonitorTemplate();
|
|
164
|
+
const metadata: TableColumnMetadata =
|
|
165
|
+
template.getTableColumnMetadata("templateName");
|
|
166
|
+
|
|
167
|
+
expect(metadata.canReadOnRelationQuery).toBe(true);
|
|
168
|
+
expect(metadata.type).toBe(TableColumnType.ShortText);
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
/*
|
|
172
|
+
* The relation traversal only recurses into Entity columns. If Monitor's
|
|
173
|
+
* `monitorTemplate` stopped being one, `monitorTemplate: { ... }` would no
|
|
174
|
+
* longer be a relation select at all and this whole gate would go vacuous.
|
|
175
|
+
*/
|
|
176
|
+
it("travels a real Entity relation on Monitor", () => {
|
|
177
|
+
const monitor: BaseModel = new Monitor();
|
|
178
|
+
const metadata: TableColumnMetadata =
|
|
179
|
+
monitor.getTableColumnMetadata("monitorTemplate");
|
|
180
|
+
|
|
181
|
+
expect(metadata.type).toBe(TableColumnType.Entity);
|
|
182
|
+
expect(metadata.modelType).toBe(MonitorTemplate);
|
|
183
|
+
expect(metadata.manyToOneRelationColumn).toBe("monitorTemplateId");
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
/*
|
|
187
|
+
* The flag is not a blanket opening of the template table. A column that
|
|
188
|
+
* does not carry it still refuses, which is what keeps the surface to the
|
|
189
|
+
* one label the column renders.
|
|
190
|
+
*/
|
|
191
|
+
it("still refuses a template column that is not marked for relation reads", () => {
|
|
192
|
+
expect(() => {
|
|
193
|
+
return checkRelationSelect(
|
|
194
|
+
{
|
|
195
|
+
monitorTemplate: {
|
|
196
|
+
monitorSteps: true,
|
|
197
|
+
},
|
|
198
|
+
} as Select<Monitor>,
|
|
199
|
+
makeProps([Permission.ProjectOwner]),
|
|
200
|
+
);
|
|
201
|
+
}).toThrow();
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
it("still refuses a column that does not exist on the template", () => {
|
|
205
|
+
expect(() => {
|
|
206
|
+
return checkRelationSelect(
|
|
207
|
+
{
|
|
208
|
+
monitorTemplate: {
|
|
209
|
+
noSuchColumn: true,
|
|
210
|
+
},
|
|
211
|
+
} as unknown as Select<Monitor>,
|
|
212
|
+
makeProps([Permission.ProjectOwner]),
|
|
213
|
+
);
|
|
214
|
+
}).toThrow();
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
it("still refuses a deep relation through the template", () => {
|
|
218
|
+
expect(() => {
|
|
219
|
+
return checkRelationSelect(
|
|
220
|
+
{
|
|
221
|
+
monitorTemplate: {
|
|
222
|
+
project: {
|
|
223
|
+
name: true,
|
|
224
|
+
},
|
|
225
|
+
},
|
|
226
|
+
} as unknown as Select<Monitor>,
|
|
227
|
+
makeProps([Permission.ProjectOwner]),
|
|
228
|
+
);
|
|
229
|
+
}).toThrow("You cannot query deep relations");
|
|
230
|
+
});
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
describe("filtering monitors by their template", () => {
|
|
234
|
+
/*
|
|
235
|
+
* The chip writes the foreign key. A WHERE clause reads a value as surely as
|
|
236
|
+
* a SELECT does, so this has to be a column the caller may read — otherwise
|
|
237
|
+
* setting the chip 403s a list that rendered fine a moment earlier.
|
|
238
|
+
*/
|
|
239
|
+
it("accepts the chip's monitorTemplateId filter for every reader", () => {
|
|
240
|
+
for (const permission of MONITOR_READERS) {
|
|
241
|
+
expect(() => {
|
|
242
|
+
return checkQuery(
|
|
243
|
+
{
|
|
244
|
+
projectId: projectId,
|
|
245
|
+
monitorTemplateId: templateId,
|
|
246
|
+
} as Query<Monitor>,
|
|
247
|
+
makeProps([permission]),
|
|
248
|
+
);
|
|
249
|
+
}).not.toThrow();
|
|
250
|
+
}
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
it("accepts the multi-select form the chip actually sends", () => {
|
|
254
|
+
expect(() => {
|
|
255
|
+
return checkQuery(
|
|
256
|
+
{
|
|
257
|
+
projectId: projectId,
|
|
258
|
+
monitorTemplateId: new Includes([templateId, ObjectID.generate()]),
|
|
259
|
+
} as unknown as Query<Monitor>,
|
|
260
|
+
makeProps([Permission.ReadProjectMonitor]),
|
|
261
|
+
);
|
|
262
|
+
}).not.toThrow();
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
/*
|
|
266
|
+
* "Which monitors came from no template" — the rows the Template column
|
|
267
|
+
* renders as "—", and the ones a template rollout has not reached. Only the
|
|
268
|
+
* foreign key can be asked this: there is no template row to join against.
|
|
269
|
+
*/
|
|
270
|
+
it("accepts the 'is empty' and 'is not empty' forms", () => {
|
|
271
|
+
expect(() => {
|
|
272
|
+
return checkQuery(
|
|
273
|
+
{
|
|
274
|
+
projectId: projectId,
|
|
275
|
+
monitorTemplateId: new IsNull(),
|
|
276
|
+
} as unknown as Query<Monitor>,
|
|
277
|
+
makeProps([Permission.ReadProjectMonitor]),
|
|
278
|
+
);
|
|
279
|
+
}).not.toThrow();
|
|
280
|
+
|
|
281
|
+
expect(() => {
|
|
282
|
+
return checkQuery(
|
|
283
|
+
{
|
|
284
|
+
projectId: projectId,
|
|
285
|
+
monitorTemplateId: new NotNull(),
|
|
286
|
+
} as unknown as Query<Monitor>,
|
|
287
|
+
makeProps([Permission.ReadProjectMonitor]),
|
|
288
|
+
);
|
|
289
|
+
}).not.toThrow();
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
it("filters a real, indexed column rather than a computed one", () => {
|
|
293
|
+
const monitor: BaseModel = new Monitor();
|
|
294
|
+
const metadata: TableColumnMetadata =
|
|
295
|
+
monitor.getTableColumnMetadata("monitorTemplateId");
|
|
296
|
+
|
|
297
|
+
expect(metadata.type).toBe(TableColumnType.ObjectID);
|
|
298
|
+
expect(monitor.getTableColumns().columns).toContain("monitorTemplateId");
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
/*
|
|
302
|
+
* The counterpart to the accept cases: the gate is really running, so the
|
|
303
|
+
* tests above pass because the column is readable rather than because
|
|
304
|
+
* nothing was checked.
|
|
305
|
+
*/
|
|
306
|
+
it("still refuses a column that does not exist on Monitor", () => {
|
|
307
|
+
expect(() => {
|
|
308
|
+
return checkQuery(
|
|
309
|
+
{
|
|
310
|
+
projectId: projectId,
|
|
311
|
+
monitorTemplateName: "Production API Health",
|
|
312
|
+
} as unknown as Query<Monitor>,
|
|
313
|
+
makeProps([Permission.ProjectOwner]),
|
|
314
|
+
);
|
|
315
|
+
}).toThrow("Column does not exist");
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
it("refuses the filter for a caller with no read on monitors at all", () => {
|
|
319
|
+
expect(() => {
|
|
320
|
+
return checkQuery(
|
|
321
|
+
{
|
|
322
|
+
monitorTemplateId: templateId,
|
|
323
|
+
} as Query<Monitor>,
|
|
324
|
+
makeProps([Permission.ReadProjectIncident]),
|
|
325
|
+
);
|
|
326
|
+
}).toThrow();
|
|
327
|
+
});
|
|
328
|
+
});
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import QueryHelper from "../../../../Server/Types/Database/QueryHelper";
|
|
2
|
+
import { describe, expect, it } from "@jest/globals";
|
|
3
|
+
|
|
4
|
+
/*
|
|
5
|
+
* Fills the last two gaps in QueryHelper coverage that the sibling
|
|
6
|
+
* QueryHelper*.test.ts files leave untouched: the case-insensitive single-value
|
|
7
|
+
* `findWithSameText` and the parameterless `isNull` predicate.
|
|
8
|
+
*
|
|
9
|
+
* As with every QueryHelper operator, the SQL is produced by a function on the
|
|
10
|
+
* FindOperator's `.value`, given a column alias, and the comparand must live in
|
|
11
|
+
* the bound-parameter bag rather than the SQL text — these operators filter on
|
|
12
|
+
* user-typed / ingest-stamped identifiers, so an interpolated value would be an
|
|
13
|
+
* injection vector.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
interface RawOperator {
|
|
17
|
+
type: string;
|
|
18
|
+
objectLiteralParameters: Record<string, unknown>;
|
|
19
|
+
getSql: (aliasPath: string) => string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function asRaw(operator: unknown): RawOperator {
|
|
23
|
+
return operator as unknown as RawOperator;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const ALIAS: string = '"Monitor"."name"';
|
|
27
|
+
|
|
28
|
+
function paramValues(operator: RawOperator): Array<unknown> {
|
|
29
|
+
return Object.values(operator.objectLiteralParameters ?? {});
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
describe("QueryHelper.findWithSameText", () => {
|
|
33
|
+
it("wraps the column in LOWER() and lower-cases a string comparand", () => {
|
|
34
|
+
const op: RawOperator = asRaw(QueryHelper.findWithSameText("Production"));
|
|
35
|
+
const sql: string = op.getSql(ALIAS);
|
|
36
|
+
|
|
37
|
+
// A string comparison must be case-insensitive on BOTH sides.
|
|
38
|
+
expect(sql.toUpperCase()).toContain("LOWER(");
|
|
39
|
+
expect(sql).toContain(`LOWER(${ALIAS}) =`);
|
|
40
|
+
|
|
41
|
+
// Value is bound, lower-cased, never interpolated.
|
|
42
|
+
expect(sql).not.toContain("Production");
|
|
43
|
+
expect(sql).not.toContain("production");
|
|
44
|
+
expect(paramValues(op)).toEqual(["production"]);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it("trims surrounding whitespace off a string comparand", () => {
|
|
48
|
+
const op: RawOperator = asRaw(QueryHelper.findWithSameText(" Prod "));
|
|
49
|
+
expect(paramValues(op)).toEqual(["prod"]);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it("compares a number without LOWER() and binds it as a string", () => {
|
|
53
|
+
const op: RawOperator = asRaw(QueryHelper.findWithSameText(42));
|
|
54
|
+
const sql: string = op.getSql(ALIAS);
|
|
55
|
+
|
|
56
|
+
// Numbers can't be lower-cased; the column must be compared directly.
|
|
57
|
+
expect(sql.toUpperCase()).not.toContain("LOWER(");
|
|
58
|
+
expect(sql).toContain(`(${ALIAS} =`);
|
|
59
|
+
|
|
60
|
+
// The value is still routed through the parameter bag, stringified.
|
|
61
|
+
expect(sql).not.toContain("42");
|
|
62
|
+
expect(paramValues(op)).toEqual(["42"]);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it("binds exactly one parameter", () => {
|
|
66
|
+
const op: RawOperator = asRaw(QueryHelper.findWithSameText("anything"));
|
|
67
|
+
expect(Object.keys(op.objectLiteralParameters)).toHaveLength(1);
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
describe("QueryHelper.isNull", () => {
|
|
72
|
+
it("emits IS NULL and binds nothing", () => {
|
|
73
|
+
const op: RawOperator = asRaw(QueryHelper.isNull());
|
|
74
|
+
const sql: string = op.getSql(ALIAS);
|
|
75
|
+
|
|
76
|
+
expect(sql.toUpperCase()).toContain("IS NULL");
|
|
77
|
+
expect(sql.toUpperCase()).not.toContain("IS NOT NULL");
|
|
78
|
+
expect(sql).toContain(ALIAS);
|
|
79
|
+
|
|
80
|
+
// A pure IS NULL predicate has no comparand to bind.
|
|
81
|
+
expect(Object.keys(op.objectLiteralParameters ?? {})).toHaveLength(0);
|
|
82
|
+
});
|
|
83
|
+
});
|