@oneuptime/common 12.0.14 → 12.0.15
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/GlobalOidc.ts +20 -0
- package/Models/DatabaseModels/GlobalSso.ts +20 -0
- package/Models/DatabaseModels/MarketingConversion.ts +19 -9
- package/Models/DatabaseModels/NetworkDeviceLinkRule.ts +46 -0
- package/Models/DatabaseModels/StatusPage.ts +7 -4
- package/Server/API/StatusPageAPI.ts +29 -4
- package/Server/EnvironmentConfig.ts +7 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1787800000000-AddScopeToNetworkDeviceLinkRule.ts +19 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1787900000000-AddRestrictToAttachedProjectsToGlobalSso.ts +40 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +4 -0
- package/Server/Middleware/SlackAuthorization.ts +43 -6
- package/Server/Middleware/UserAuthorization.ts +236 -26
- package/Server/Middleware/WhatsAppAuthorization.ts +43 -3
- package/Server/Services/GlobalOidcProjectService.ts +126 -1
- package/Server/Services/GlobalOidcService.ts +132 -0
- package/Server/Services/GlobalSsoProjectService.ts +126 -1
- package/Server/Services/GlobalSsoService.ts +132 -0
- package/Server/Services/UserEmailService.ts +4 -4
- package/Server/Utils/CronTab.ts +73 -0
- package/Server/Utils/GlobalSsoAuthorization.ts +175 -0
- package/Server/Utils/Marketing/ConversionUploadProvider.ts +35 -5
- package/Server/Utils/Marketing/Providers/GoogleAds.ts +1 -1
- package/Server/Utils/Marketing/Providers/LinkedIn.ts +1 -1
- package/Server/Utils/Marketing/Providers/Meta.ts +1 -1
- package/Server/Utils/Marketing/Providers/MicrosoftAds.ts +1 -1
- package/Server/Utils/Marketing/Providers/Reddit.ts +1 -1
- package/Server/Utils/Monitor/Criteria/APIRequestCriteria.ts +48 -33
- package/Server/Utils/Monitor/Criteria/DnsMonitorCriteria.ts +35 -30
- package/Server/Utils/Monitor/Criteria/DomainMonitorCriteria.ts +33 -28
- package/Server/Utils/Monitor/Criteria/EvaluateOverTime.ts +475 -47
- package/Server/Utils/Monitor/Criteria/ExternalStatusPageMonitorCriteria.ts +35 -30
- package/Server/Utils/Monitor/Criteria/IncomingRequestCriteria.ts +34 -28
- package/Server/Utils/Monitor/Criteria/SSLMonitorCriteria.ts +33 -28
- package/Server/Utils/Monitor/Criteria/ServerMonitorCriteria.ts +69 -40
- package/Server/Utils/Monitor/Criteria/SnmpMonitorCriteria.ts +35 -29
- package/Server/Utils/Monitor/MonitorCriteriaEvaluator.ts +8 -0
- package/Server/Utils/Monitor/MonitorResource.ts +161 -21
- package/Server/Utils/StatusPageContentSecurityPolicy.ts +56 -0
- package/Server/Utils/StatusPageCustomizationAccess.ts +117 -0
- package/Tests/Server/API/StatusPageCustomizationOrigin.test.ts +348 -0
- package/Tests/Server/Infrastructure/Postgres/FixTotpOtpUrlAlgorithmMigration.test.ts +9 -59
- package/Tests/Server/Infrastructure/Postgres/SchemaMigrationsOrdering.test.ts +227 -0
- package/Tests/Server/Middleware/SlackAuthorization.test.ts +149 -0
- package/Tests/Server/Middleware/UserAuthorization.test.ts +27 -13
- package/Tests/Server/Middleware/UserAuthorizationGlobalSsoGovernance.test.ts +1455 -0
- package/Tests/Server/Middleware/UserAuthorizationSSOProvider.test.ts +91 -2
- package/Tests/Server/Middleware/WhatsAppAuthorization.test.ts +233 -0
- package/Tests/Server/Services/DiscoveryScanClaimHookFreeSafety.test.ts +21 -8
- package/Tests/Server/Services/GlobalSsoProviderAuthorization.test.ts +1242 -0
- package/Tests/Server/Services/MonitorResourceProbeAgreementHydration.test.ts +459 -0
- package/Tests/Server/Services/MonitorResourceProbeAgreementReuse.test.ts +10 -0
- package/Tests/Server/Types/AnalyticsDatabase/AggregateUtilBucketTimestamp.test.ts +416 -0
- package/Tests/Server/Types/Database/QueryHelperManyToManyAndEmptyValues.test.ts +551 -0
- package/Tests/Server/Types/Database/SelectUtil.test.ts +348 -0
- package/Tests/Server/Utils/CronTab.test.ts +116 -0
- package/Tests/Server/Utils/GlobalSSOToken.test.ts +581 -0
- package/Tests/Server/Utils/GlobalSsoAuthorization.test.ts +1401 -0
- package/Tests/Server/Utils/Marketing/AdUploadableConversionTypes.test.ts +254 -0
- package/Tests/Server/Utils/Marketing/ConversionUploadProvider.test.ts +1 -1
- package/Tests/Server/Utils/Monitor/Criteria/APIRequestCriteriaEvaluateOverTime.test.ts +508 -0
- package/Tests/Server/Utils/Monitor/Criteria/APIRequestCriteriaPortTimings.test.ts +30 -9
- package/Tests/Server/Utils/Monitor/Criteria/EvaluateOverTime.test.ts +987 -0
- package/Tests/Server/Utils/Monitor/Criteria/ExternalStatusPageMonitorCriteria.test.ts +916 -0
- package/Tests/Server/Utils/Monitor/Criteria/ProfileMonitorCriteria.test.ts +382 -0
- package/Tests/Server/Utils/Monitor/Criteria/ServerMonitorCriteria.test.ts +1219 -0
- package/Tests/Server/Utils/Monitor/MonitorResourceIngestedStepSelection.test.ts +86 -0
- package/Tests/Server/Utils/StatusPageContentSecurityPolicy.test.ts +136 -0
- package/Tests/Server/Utils/StatusPageCustomizationAccess.test.ts +223 -0
- package/Tests/Types/Monitor/MonitorStepConfigHelpers.test.ts +165 -0
- package/Tests/Types/NetworkDevice/NetworkDeviceLinkRuleScope.test.ts +215 -0
- package/Tests/UI/Components/Forms/FormSubmitAnalytics.test.tsx +206 -0
- package/Tests/UI/Utils/AIChatExport/ConversationMarkdown.test.ts +774 -0
- package/Tests/UI/Utils/AIChatExport/MarkdownBlocks.test.ts +791 -0
- package/Tests/UI/Utils/AIChatExport/MarkdownSafety.test.ts +544 -0
- package/Tests/Utils/Analytics.test.ts +187 -0
- package/Tests/Utils/Monitor/MonitorMetricType.test.ts +80 -0
- package/Tests/Utils/Monitor/NetworkDeviceLinkRuleUtil.test.ts +1291 -0
- package/Tests/Utils/NetworkDiscovery/UnclaimedScanDiagnosis.test.ts +214 -0
- package/Types/Marketing/MarketingConversion.ts +12 -0
- package/Types/Monitor/CriteriaFilter.ts +9 -0
- package/Types/NetworkDevice/NetworkDeviceLinkRuleScope.ts +51 -0
- package/UI/Components/FormModal/BasicFormModal.tsx +12 -0
- package/UI/Components/Forms/BasicForm.tsx +13 -1
- package/UI/Components/Forms/BasicModelForm.tsx +2 -1
- package/UI/Components/Forms/ModelForm.tsx +10 -1
- package/UI/Components/Forms/Utils/FormAnalyticsName.ts +39 -0
- package/UI/Components/ModelFormModal/ModelFormModal.tsx +6 -1
- package/Utils/Analytics.ts +55 -2
- package/Utils/Monitor/MonitorMetricType.ts +40 -0
- package/Utils/Monitor/NetworkDeviceLinkRuleUtil.ts +587 -37
- package/Utils/NetworkDiscovery/UnclaimedScanDiagnosis.ts +115 -0
- package/build/dist/Models/DatabaseModels/GlobalOidc.js +21 -0
- package/build/dist/Models/DatabaseModels/GlobalOidc.js.map +1 -1
- package/build/dist/Models/DatabaseModels/GlobalSso.js +21 -0
- package/build/dist/Models/DatabaseModels/GlobalSso.js.map +1 -1
- package/build/dist/Models/DatabaseModels/MarketingConversion.js +20 -10
- package/build/dist/Models/DatabaseModels/MarketingConversion.js.map +1 -1
- package/build/dist/Models/DatabaseModels/NetworkDeviceLinkRule.js +47 -0
- package/build/dist/Models/DatabaseModels/NetworkDeviceLinkRule.js.map +1 -1
- package/build/dist/Models/DatabaseModels/StatusPage.js +4 -4
- package/build/dist/Models/DatabaseModels/StatusPage.js.map +1 -1
- package/build/dist/Server/API/StatusPageAPI.js +25 -4
- package/build/dist/Server/API/StatusPageAPI.js.map +1 -1
- package/build/dist/Server/EnvironmentConfig.js +6 -0
- package/build/dist/Server/EnvironmentConfig.js.map +1 -1
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1787800000000-AddScopeToNetworkDeviceLinkRule.js +12 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1787800000000-AddScopeToNetworkDeviceLinkRule.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1787900000000-AddRestrictToAttachedProjectsToGlobalSso.js +29 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1787900000000-AddRestrictToAttachedProjectsToGlobalSso.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/SlackAuthorization.js +28 -4
- package/build/dist/Server/Middleware/SlackAuthorization.js.map +1 -1
- package/build/dist/Server/Middleware/UserAuthorization.js +197 -22
- package/build/dist/Server/Middleware/UserAuthorization.js.map +1 -1
- package/build/dist/Server/Middleware/WhatsAppAuthorization.js +32 -2
- package/build/dist/Server/Middleware/WhatsAppAuthorization.js.map +1 -1
- package/build/dist/Server/Services/GlobalOidcProjectService.js +105 -0
- package/build/dist/Server/Services/GlobalOidcProjectService.js.map +1 -1
- package/build/dist/Server/Services/GlobalOidcService.js +127 -0
- package/build/dist/Server/Services/GlobalOidcService.js.map +1 -1
- package/build/dist/Server/Services/GlobalSsoProjectService.js +105 -0
- package/build/dist/Server/Services/GlobalSsoProjectService.js.map +1 -1
- package/build/dist/Server/Services/GlobalSsoService.js +127 -0
- package/build/dist/Server/Services/GlobalSsoService.js.map +1 -1
- package/build/dist/Server/Services/UserEmailService.js +4 -4
- package/build/dist/Server/Utils/CronTab.js +66 -0
- package/build/dist/Server/Utils/CronTab.js.map +1 -1
- package/build/dist/Server/Utils/GlobalSsoAuthorization.js +112 -0
- package/build/dist/Server/Utils/GlobalSsoAuthorization.js.map +1 -0
- package/build/dist/Server/Utils/Marketing/ConversionUploadProvider.js +17 -1
- package/build/dist/Server/Utils/Marketing/ConversionUploadProvider.js.map +1 -1
- package/build/dist/Server/Utils/Marketing/Providers/GoogleAds.js +1 -1
- package/build/dist/Server/Utils/Marketing/Providers/GoogleAds.js.map +1 -1
- package/build/dist/Server/Utils/Marketing/Providers/LinkedIn.js +1 -1
- package/build/dist/Server/Utils/Marketing/Providers/LinkedIn.js.map +1 -1
- package/build/dist/Server/Utils/Marketing/Providers/Meta.js +1 -1
- package/build/dist/Server/Utils/Marketing/Providers/Meta.js.map +1 -1
- package/build/dist/Server/Utils/Marketing/Providers/MicrosoftAds.js +1 -1
- package/build/dist/Server/Utils/Marketing/Providers/MicrosoftAds.js.map +1 -1
- package/build/dist/Server/Utils/Marketing/Providers/Reddit.js +1 -1
- package/build/dist/Server/Utils/Marketing/Providers/Reddit.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/Criteria/APIRequestCriteria.js +37 -35
- package/build/dist/Server/Utils/Monitor/Criteria/APIRequestCriteria.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/Criteria/DnsMonitorCriteria.js +21 -25
- package/build/dist/Server/Utils/Monitor/Criteria/DnsMonitorCriteria.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/Criteria/DomainMonitorCriteria.js +20 -22
- package/build/dist/Server/Utils/Monitor/Criteria/DomainMonitorCriteria.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/Criteria/EvaluateOverTime.js +370 -42
- package/build/dist/Server/Utils/Monitor/Criteria/EvaluateOverTime.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/Criteria/ExternalStatusPageMonitorCriteria.js +21 -25
- package/build/dist/Server/Utils/Monitor/Criteria/ExternalStatusPageMonitorCriteria.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/Criteria/IncomingRequestCriteria.js +21 -25
- package/build/dist/Server/Utils/Monitor/Criteria/IncomingRequestCriteria.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/Criteria/SSLMonitorCriteria.js +21 -25
- package/build/dist/Server/Utils/Monitor/Criteria/SSLMonitorCriteria.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/Criteria/ServerMonitorCriteria.js +64 -52
- package/build/dist/Server/Utils/Monitor/Criteria/ServerMonitorCriteria.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/Criteria/SnmpMonitorCriteria.js +25 -29
- package/build/dist/Server/Utils/Monitor/Criteria/SnmpMonitorCriteria.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/MonitorCriteriaEvaluator.js +8 -0
- package/build/dist/Server/Utils/Monitor/MonitorCriteriaEvaluator.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/MonitorResource.js +123 -23
- package/build/dist/Server/Utils/Monitor/MonitorResource.js.map +1 -1
- package/build/dist/Server/Utils/StatusPageContentSecurityPolicy.js +39 -0
- package/build/dist/Server/Utils/StatusPageContentSecurityPolicy.js.map +1 -0
- package/build/dist/Server/Utils/StatusPageCustomizationAccess.js +85 -0
- package/build/dist/Server/Utils/StatusPageCustomizationAccess.js.map +1 -0
- package/build/dist/Types/Marketing/MarketingConversion.js +10 -0
- package/build/dist/Types/Marketing/MarketingConversion.js.map +1 -1
- package/build/dist/Types/Monitor/CriteriaFilter.js +1 -0
- package/build/dist/Types/Monitor/CriteriaFilter.js.map +1 -1
- package/build/dist/Types/NetworkDevice/NetworkDeviceLinkRuleScope.js +46 -0
- package/build/dist/Types/NetworkDevice/NetworkDeviceLinkRuleScope.js.map +1 -0
- package/build/dist/UI/Components/FormModal/BasicFormModal.js +2 -1
- package/build/dist/UI/Components/FormModal/BasicFormModal.js.map +1 -1
- package/build/dist/UI/Components/Forms/BasicForm.js +10 -1
- package/build/dist/UI/Components/Forms/BasicForm.js.map +1 -1
- package/build/dist/UI/Components/Forms/BasicModelForm.js +2 -1
- package/build/dist/UI/Components/Forms/BasicModelForm.js.map +1 -1
- package/build/dist/UI/Components/Forms/ModelForm.js +4 -1
- package/build/dist/UI/Components/Forms/ModelForm.js.map +1 -1
- package/build/dist/UI/Components/Forms/Utils/FormAnalyticsName.js +33 -0
- package/build/dist/UI/Components/Forms/Utils/FormAnalyticsName.js.map +1 -0
- package/build/dist/UI/Components/ModelFormModal/ModelFormModal.js +2 -1
- package/build/dist/UI/Components/ModelFormModal/ModelFormModal.js.map +1 -1
- package/build/dist/Utils/Analytics.js +50 -2
- package/build/dist/Utils/Analytics.js.map +1 -1
- package/build/dist/Utils/Monitor/MonitorMetricType.js +37 -0
- package/build/dist/Utils/Monitor/MonitorMetricType.js.map +1 -1
- package/build/dist/Utils/Monitor/NetworkDeviceLinkRuleUtil.js +318 -37
- package/build/dist/Utils/Monitor/NetworkDeviceLinkRuleUtil.js.map +1 -1
- package/build/dist/Utils/NetworkDiscovery/UnclaimedScanDiagnosis.js +87 -0
- package/build/dist/Utils/NetworkDiscovery/UnclaimedScanDiagnosis.js.map +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1,1455 @@
|
|
|
1
|
+
import UserMiddleware from "../../../Server/Middleware/UserAuthorization";
|
|
2
|
+
import GlobalOidcProjectService from "../../../Server/Services/GlobalOidcProjectService";
|
|
3
|
+
import GlobalOidcService from "../../../Server/Services/GlobalOidcService";
|
|
4
|
+
import GlobalSsoProjectService from "../../../Server/Services/GlobalSsoProjectService";
|
|
5
|
+
import GlobalSsoService from "../../../Server/Services/GlobalSsoService";
|
|
6
|
+
import CookieUtil from "../../../Server/Utils/Cookie";
|
|
7
|
+
import { ExpressRequest } from "../../../Server/Utils/Express";
|
|
8
|
+
import { GlobalProviderTrust } from "../../../Server/Utils/GlobalSsoAuthorization";
|
|
9
|
+
import JSONWebToken from "../../../Server/Utils/JsonWebToken";
|
|
10
|
+
import Email from "../../../Types/Email";
|
|
11
|
+
import JSONWebTokenData from "../../../Types/JsonWebTokenData";
|
|
12
|
+
import Name from "../../../Types/Name";
|
|
13
|
+
import ObjectID from "../../../Types/ObjectID";
|
|
14
|
+
import SsoProviderType from "../../../Types/SSO/SsoProviderType";
|
|
15
|
+
import User from "../../../Models/DatabaseModels/User";
|
|
16
|
+
import {
|
|
17
|
+
afterEach,
|
|
18
|
+
beforeEach,
|
|
19
|
+
describe,
|
|
20
|
+
expect,
|
|
21
|
+
jest,
|
|
22
|
+
test,
|
|
23
|
+
} from "@jest/globals";
|
|
24
|
+
import type { SpyInstance } from "jest-mock";
|
|
25
|
+
|
|
26
|
+
jest.mock("../../../Server/Utils/Logger");
|
|
27
|
+
|
|
28
|
+
/*
|
|
29
|
+
* A Global SSO/OIDC token is a 30-day JWT with NO project binding, so by
|
|
30
|
+
* itself it only proves "this person authenticated against SOME instance-wide
|
|
31
|
+
* identity provider at some point in the last month". Enforcement therefore
|
|
32
|
+
* has two halves:
|
|
33
|
+
*
|
|
34
|
+
* STATELESS - signature, expiry, Global provider type, matching user, and
|
|
35
|
+
* the project's pinned-provider discriminator.
|
|
36
|
+
* STATEFUL - is the provider still trusted (exists AND enabled), and -
|
|
37
|
+
* only when the admin opted in - does it govern this project.
|
|
38
|
+
*
|
|
39
|
+
* `UserMiddleware.isSsoSatisfiedForProject` is THE enforcement entry point and
|
|
40
|
+
* runs both. `UserMiddleware.doesSsoTokenForProjectExist` is the stateless
|
|
41
|
+
* half only, and is deliberately NOT enforcement (see the final describe).
|
|
42
|
+
*
|
|
43
|
+
* These tests mint REAL tokens through CookieUtil (real signing, real
|
|
44
|
+
* verification) and stub ONLY the four service methods that reach Postgres,
|
|
45
|
+
* so the middleware's own routing, ordering and AND-ing logic runs for real.
|
|
46
|
+
*
|
|
47
|
+
* Deliberately NOT repeated here, because it is already covered:
|
|
48
|
+
* - Common/Tests/Server/Middleware/UserAuthorizationSSOProvider.test.ts -
|
|
49
|
+
* the full `requiredSsoProviderId` discriminator matrix, the cookie vs
|
|
50
|
+
* `x-global-sso-token` header transports, and the sync-level refusal of
|
|
51
|
+
* Global-typed tokens in the per-project slot.
|
|
52
|
+
* - Common/Tests/Server/Utils/GlobalSSOToken.test.ts - the global token's
|
|
53
|
+
* payload/TTL contract, wrong-secret and syntactic tampering, and
|
|
54
|
+
* project-typed tokens sitting in the global slot.
|
|
55
|
+
* - Common/Tests/Server/Utils/GlobalSsoAuthorization.test.ts - the
|
|
56
|
+
* attachment/cache primitives underneath the services.
|
|
57
|
+
* What this file adds is the DATABASE-backed half of the decision, and proof
|
|
58
|
+
* that every stateless rejection happens BEFORE the database is consulted.
|
|
59
|
+
*
|
|
60
|
+
* Every "refused" assertion below is paired with an "allowed" assertion over
|
|
61
|
+
* the SAME setup with only the single thing under test corrected. An
|
|
62
|
+
* implementation that simply returned false everywhere would fail this file.
|
|
63
|
+
*/
|
|
64
|
+
|
|
65
|
+
const THIRTY_DAYS_IN_SECONDS: number = 30 * 24 * 60 * 60;
|
|
66
|
+
|
|
67
|
+
const TRUSTED_AND_UNRESTRICTED: GlobalProviderTrust = {
|
|
68
|
+
isUsable: true,
|
|
69
|
+
restrictToAttachedProjects: false,
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
const TRUSTED_AND_RESTRICTED: GlobalProviderTrust = {
|
|
73
|
+
isUsable: true,
|
|
74
|
+
restrictToAttachedProjects: true,
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
const REVOKED: GlobalProviderTrust = {
|
|
78
|
+
isUsable: false,
|
|
79
|
+
restrictToAttachedProjects: false,
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
type TrustSpy = SpyInstance<
|
|
83
|
+
(providerId: ObjectID) => Promise<GlobalProviderTrust>
|
|
84
|
+
>;
|
|
85
|
+
|
|
86
|
+
type SsoGovernSpy = SpyInstance<
|
|
87
|
+
(data: { globalSsoId: ObjectID; projectId: ObjectID }) => Promise<boolean>
|
|
88
|
+
>;
|
|
89
|
+
|
|
90
|
+
type OidcGovernSpy = SpyInstance<
|
|
91
|
+
(data: { globalOidcId: ObjectID; projectId: ObjectID }) => Promise<boolean>
|
|
92
|
+
>;
|
|
93
|
+
|
|
94
|
+
let ssoTrustSpy: TrustSpy;
|
|
95
|
+
let oidcTrustSpy: TrustSpy;
|
|
96
|
+
let ssoGovernsSpy: SsoGovernSpy;
|
|
97
|
+
let oidcGovernsSpy: OidcGovernSpy;
|
|
98
|
+
|
|
99
|
+
const buildUser: (userId: ObjectID) => User = (userId: ObjectID): User => {
|
|
100
|
+
const user: User = new User();
|
|
101
|
+
user.id = userId;
|
|
102
|
+
user.name = new Name("Global SSO User");
|
|
103
|
+
user.email = new Email("global-sso@oneuptime.com");
|
|
104
|
+
return user;
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
/** A real Global SSO/OIDC token, exactly as a global login mints it. */
|
|
108
|
+
const mintGlobalToken: (data: {
|
|
109
|
+
userId: ObjectID;
|
|
110
|
+
providerId: ObjectID;
|
|
111
|
+
providerType: SsoProviderType;
|
|
112
|
+
}) => string = (data: {
|
|
113
|
+
userId: ObjectID;
|
|
114
|
+
providerId: ObjectID;
|
|
115
|
+
providerType: SsoProviderType;
|
|
116
|
+
}): string => {
|
|
117
|
+
return CookieUtil.getGlobalSSOToken({
|
|
118
|
+
user: buildUser(data.userId),
|
|
119
|
+
ssoProviderId: data.providerId,
|
|
120
|
+
ssoProviderType: data.providerType,
|
|
121
|
+
});
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
/** A real per-project `sso-<projectId>` token. */
|
|
125
|
+
const mintProjectToken: (data: {
|
|
126
|
+
userId: ObjectID;
|
|
127
|
+
projectId: ObjectID;
|
|
128
|
+
providerId: ObjectID;
|
|
129
|
+
providerType: SsoProviderType;
|
|
130
|
+
}) => string = (data: {
|
|
131
|
+
userId: ObjectID;
|
|
132
|
+
projectId: ObjectID;
|
|
133
|
+
providerId: ObjectID;
|
|
134
|
+
providerType: SsoProviderType;
|
|
135
|
+
}): string => {
|
|
136
|
+
return CookieUtil.getSSOToken({
|
|
137
|
+
user: buildUser(data.userId),
|
|
138
|
+
projectId: data.projectId,
|
|
139
|
+
ssoProviderId: data.providerId,
|
|
140
|
+
ssoProviderType: data.providerType,
|
|
141
|
+
});
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
const decodeToken: (token: string) => JSONWebTokenData = (
|
|
145
|
+
token: string,
|
|
146
|
+
): JSONWebTokenData => {
|
|
147
|
+
return JSONWebToken.decode(token);
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
interface RequestParts {
|
|
151
|
+
globalToken?: string | undefined;
|
|
152
|
+
projectTokens?: Array<{ projectId: ObjectID; token: string }> | undefined;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const buildRequest: (parts: RequestParts) => ExpressRequest = (
|
|
156
|
+
parts: RequestParts,
|
|
157
|
+
): ExpressRequest => {
|
|
158
|
+
const cookies: Record<string, string> = {};
|
|
159
|
+
|
|
160
|
+
if (parts.globalToken) {
|
|
161
|
+
cookies[CookieUtil.getGlobalSSOKey()] = parts.globalToken;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
for (const entry of parts.projectTokens || []) {
|
|
165
|
+
cookies[CookieUtil.getUserSSOKey(entry.projectId)] = entry.token;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return {
|
|
169
|
+
cookies: cookies,
|
|
170
|
+
headers: {},
|
|
171
|
+
} as unknown as ExpressRequest;
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
/*
|
|
175
|
+
* "The database was never asked." Used by every test that claims a decision
|
|
176
|
+
* was reached statelessly - a stateless gate that silently started issuing
|
|
177
|
+
* queries would be a performance regression on the hottest path in the
|
|
178
|
+
* product, and a gate that ran AFTER the lookup would leak provider existence.
|
|
179
|
+
*/
|
|
180
|
+
const expectNoDatabaseLookups: () => void = (): void => {
|
|
181
|
+
expect(ssoTrustSpy).not.toHaveBeenCalled();
|
|
182
|
+
expect(oidcTrustSpy).not.toHaveBeenCalled();
|
|
183
|
+
expect(ssoGovernsSpy).not.toHaveBeenCalled();
|
|
184
|
+
expect(oidcGovernsSpy).not.toHaveBeenCalled();
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
beforeEach(() => {
|
|
188
|
+
ssoTrustSpy = jest.spyOn(GlobalSsoService, "getProviderTrust");
|
|
189
|
+
oidcTrustSpy = jest.spyOn(GlobalOidcService, "getProviderTrust");
|
|
190
|
+
ssoGovernsSpy = jest.spyOn(
|
|
191
|
+
GlobalSsoProjectService,
|
|
192
|
+
"doesProviderGovernProject",
|
|
193
|
+
);
|
|
194
|
+
oidcGovernsSpy = jest.spyOn(
|
|
195
|
+
GlobalOidcProjectService,
|
|
196
|
+
"doesProviderGovernProject",
|
|
197
|
+
);
|
|
198
|
+
|
|
199
|
+
/*
|
|
200
|
+
* Default: a healthy, enabled, unrestricted provider that governs
|
|
201
|
+
* everything - i.e. what an existing installation looks like on upgrade.
|
|
202
|
+
* Each test narrows only the answer it is about.
|
|
203
|
+
*/
|
|
204
|
+
ssoTrustSpy.mockResolvedValue(TRUSTED_AND_UNRESTRICTED);
|
|
205
|
+
oidcTrustSpy.mockResolvedValue(TRUSTED_AND_UNRESTRICTED);
|
|
206
|
+
ssoGovernsSpy.mockResolvedValue(true);
|
|
207
|
+
oidcGovernsSpy.mockResolvedValue(true);
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
afterEach(() => {
|
|
211
|
+
jest.restoreAllMocks();
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
/*
|
|
215
|
+
* The provider-trust check is what makes the admin "Enabled" toggle mean
|
|
216
|
+
* something. A Global SSO token carries no revocation and lives for 30 days,
|
|
217
|
+
* so if this check were conditional - skipped for unrestricted providers, or
|
|
218
|
+
* short-circuited when governance already said yes - turning a provider off
|
|
219
|
+
* (or deleting it) would change nothing for anyone already signed in, for up
|
|
220
|
+
* to a month.
|
|
221
|
+
*/
|
|
222
|
+
describe("isGlobalSsoTokenAuthorizedForProject - provider trust is unconditional", () => {
|
|
223
|
+
const projectId: ObjectID = ObjectID.generate();
|
|
224
|
+
const userId: ObjectID = ObjectID.generate();
|
|
225
|
+
const providerId: ObjectID = ObjectID.generate();
|
|
226
|
+
|
|
227
|
+
const samlTokenData: () => JSONWebTokenData = (): JSONWebTokenData => {
|
|
228
|
+
return decodeToken(
|
|
229
|
+
mintGlobalToken({
|
|
230
|
+
userId,
|
|
231
|
+
providerId,
|
|
232
|
+
providerType: SsoProviderType.GlobalSSO,
|
|
233
|
+
}),
|
|
234
|
+
);
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
const oidcTokenData: () => JSONWebTokenData = (): JSONWebTokenData => {
|
|
238
|
+
return decodeToken(
|
|
239
|
+
mintGlobalToken({
|
|
240
|
+
userId,
|
|
241
|
+
providerId,
|
|
242
|
+
providerType: SsoProviderType.GlobalOIDC,
|
|
243
|
+
}),
|
|
244
|
+
);
|
|
245
|
+
};
|
|
246
|
+
|
|
247
|
+
test("SAML: a disabled or deleted provider refuses an otherwise perfect token", async () => {
|
|
248
|
+
ssoTrustSpy.mockResolvedValue(REVOKED);
|
|
249
|
+
|
|
250
|
+
await expect(
|
|
251
|
+
UserMiddleware.isGlobalSsoTokenAuthorizedForProject({
|
|
252
|
+
globalSsoTokenData: samlTokenData(),
|
|
253
|
+
projectId,
|
|
254
|
+
}),
|
|
255
|
+
).resolves.toBe(false);
|
|
256
|
+
|
|
257
|
+
// Trust said no, so there is nothing left to ask.
|
|
258
|
+
expect(ssoGovernsSpy).not.toHaveBeenCalled();
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
test("SAML: the SAME token is allowed the moment the provider is enabled again", async () => {
|
|
262
|
+
ssoTrustSpy.mockResolvedValue(TRUSTED_AND_UNRESTRICTED);
|
|
263
|
+
|
|
264
|
+
await expect(
|
|
265
|
+
UserMiddleware.isGlobalSsoTokenAuthorizedForProject({
|
|
266
|
+
globalSsoTokenData: samlTokenData(),
|
|
267
|
+
projectId,
|
|
268
|
+
}),
|
|
269
|
+
).resolves.toBe(true);
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
test("OIDC: a disabled or deleted provider refuses an otherwise perfect token", async () => {
|
|
273
|
+
oidcTrustSpy.mockResolvedValue(REVOKED);
|
|
274
|
+
|
|
275
|
+
await expect(
|
|
276
|
+
UserMiddleware.isGlobalSsoTokenAuthorizedForProject({
|
|
277
|
+
globalSsoTokenData: oidcTokenData(),
|
|
278
|
+
projectId,
|
|
279
|
+
}),
|
|
280
|
+
).resolves.toBe(false);
|
|
281
|
+
|
|
282
|
+
expect(oidcGovernsSpy).not.toHaveBeenCalled();
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
test("OIDC: the SAME token is allowed the moment the provider is enabled again", async () => {
|
|
286
|
+
oidcTrustSpy.mockResolvedValue(TRUSTED_AND_UNRESTRICTED);
|
|
287
|
+
|
|
288
|
+
await expect(
|
|
289
|
+
UserMiddleware.isGlobalSsoTokenAuthorizedForProject({
|
|
290
|
+
globalSsoTokenData: oidcTokenData(),
|
|
291
|
+
projectId,
|
|
292
|
+
}),
|
|
293
|
+
).resolves.toBe(true);
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
test("a disabled provider is refused EVEN when it governs the project - trust is not skippable", async () => {
|
|
297
|
+
// Restriction on, attachment present: governance would happily say yes.
|
|
298
|
+
ssoTrustSpy.mockResolvedValue({
|
|
299
|
+
isUsable: false,
|
|
300
|
+
restrictToAttachedProjects: true,
|
|
301
|
+
});
|
|
302
|
+
ssoGovernsSpy.mockResolvedValue(true);
|
|
303
|
+
|
|
304
|
+
await expect(
|
|
305
|
+
UserMiddleware.isGlobalSsoTokenAuthorizedForProject({
|
|
306
|
+
globalSsoTokenData: samlTokenData(),
|
|
307
|
+
projectId,
|
|
308
|
+
}),
|
|
309
|
+
).resolves.toBe(false);
|
|
310
|
+
|
|
311
|
+
// Same setup, only `isUsable` flipped -> allowed.
|
|
312
|
+
ssoTrustSpy.mockResolvedValue(TRUSTED_AND_RESTRICTED);
|
|
313
|
+
|
|
314
|
+
await expect(
|
|
315
|
+
UserMiddleware.isGlobalSsoTokenAuthorizedForProject({
|
|
316
|
+
globalSsoTokenData: samlTokenData(),
|
|
317
|
+
projectId,
|
|
318
|
+
}),
|
|
319
|
+
).resolves.toBe(true);
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
test("the whole enforcement entry point refuses a request once the provider is disabled", async () => {
|
|
323
|
+
ssoTrustSpy.mockResolvedValue(REVOKED);
|
|
324
|
+
|
|
325
|
+
const req: ExpressRequest = buildRequest({
|
|
326
|
+
globalToken: mintGlobalToken({
|
|
327
|
+
userId,
|
|
328
|
+
providerId,
|
|
329
|
+
providerType: SsoProviderType.GlobalSSO,
|
|
330
|
+
}),
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
await expect(
|
|
334
|
+
UserMiddleware.isSsoSatisfiedForProject({ req, projectId, userId }),
|
|
335
|
+
).resolves.toBe(false);
|
|
336
|
+
|
|
337
|
+
ssoTrustSpy.mockResolvedValue(TRUSTED_AND_UNRESTRICTED);
|
|
338
|
+
|
|
339
|
+
await expect(
|
|
340
|
+
UserMiddleware.isSsoSatisfiedForProject({ req, projectId, userId }),
|
|
341
|
+
).resolves.toBe(true);
|
|
342
|
+
});
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
/*
|
|
346
|
+
* THE HEADLINE BEHAVIOUR.
|
|
347
|
+
*
|
|
348
|
+
* The attachment rows (GlobalSsoProject / GlobalOidcProject) were introduced
|
|
349
|
+
* as the PROVISIONING allow-list, and the login routers grant a session on
|
|
350
|
+
* membership of ANY project. Reading them as an access boundary by default
|
|
351
|
+
* would deny existing users the projects they legitimately reach today, the
|
|
352
|
+
* instant they upgraded, with nothing inside the product to recover with. So
|
|
353
|
+
* the boundary is OPT-IN per provider, via `restrictToAttachedProjects`, and
|
|
354
|
+
* with it off the attachment table is not even consulted.
|
|
355
|
+
*/
|
|
356
|
+
describe("isGlobalSsoTokenAuthorizedForProject - project governance is opt-in", () => {
|
|
357
|
+
const projectId: ObjectID = ObjectID.generate();
|
|
358
|
+
const userId: ObjectID = ObjectID.generate();
|
|
359
|
+
const providerId: ObjectID = ObjectID.generate();
|
|
360
|
+
|
|
361
|
+
const samlTokenData: () => JSONWebTokenData = (): JSONWebTokenData => {
|
|
362
|
+
return decodeToken(
|
|
363
|
+
mintGlobalToken({
|
|
364
|
+
userId,
|
|
365
|
+
providerId,
|
|
366
|
+
providerType: SsoProviderType.GlobalSSO,
|
|
367
|
+
}),
|
|
368
|
+
);
|
|
369
|
+
};
|
|
370
|
+
|
|
371
|
+
test("restrictToAttachedProjects FALSE -> allowed WITHOUT ever asking the attachment table", async () => {
|
|
372
|
+
ssoTrustSpy.mockResolvedValue(TRUSTED_AND_UNRESTRICTED);
|
|
373
|
+
/*
|
|
374
|
+
* Rigged to deny. If the implementation consulted it anyway, the result
|
|
375
|
+
* would be false and this test would fail - which is exactly the
|
|
376
|
+
* upgrade-day lockout being guarded against.
|
|
377
|
+
*/
|
|
378
|
+
ssoGovernsSpy.mockResolvedValue(false);
|
|
379
|
+
|
|
380
|
+
await expect(
|
|
381
|
+
UserMiddleware.isGlobalSsoTokenAuthorizedForProject({
|
|
382
|
+
globalSsoTokenData: samlTokenData(),
|
|
383
|
+
projectId,
|
|
384
|
+
}),
|
|
385
|
+
).resolves.toBe(true);
|
|
386
|
+
|
|
387
|
+
expect(ssoGovernsSpy).not.toHaveBeenCalled();
|
|
388
|
+
expect(oidcGovernsSpy).not.toHaveBeenCalled();
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
test("restrictToAttachedProjects FALSE -> every project the user reaches is satisfied, not just one", async () => {
|
|
392
|
+
ssoTrustSpy.mockResolvedValue(TRUSTED_AND_UNRESTRICTED);
|
|
393
|
+
ssoGovernsSpy.mockResolvedValue(false);
|
|
394
|
+
|
|
395
|
+
const otherProjectId: ObjectID = ObjectID.generate();
|
|
396
|
+
const thirdProjectId: ObjectID = ObjectID.generate();
|
|
397
|
+
|
|
398
|
+
for (const candidateProjectId of [
|
|
399
|
+
projectId,
|
|
400
|
+
otherProjectId,
|
|
401
|
+
thirdProjectId,
|
|
402
|
+
]) {
|
|
403
|
+
await expect(
|
|
404
|
+
UserMiddleware.isGlobalSsoTokenAuthorizedForProject({
|
|
405
|
+
globalSsoTokenData: samlTokenData(),
|
|
406
|
+
projectId: candidateProjectId,
|
|
407
|
+
}),
|
|
408
|
+
).resolves.toBe(true);
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
expect(ssoGovernsSpy).not.toHaveBeenCalled();
|
|
412
|
+
});
|
|
413
|
+
|
|
414
|
+
test("restrictToAttachedProjects TRUE + provider governs the project -> allowed", async () => {
|
|
415
|
+
ssoTrustSpy.mockResolvedValue(TRUSTED_AND_RESTRICTED);
|
|
416
|
+
ssoGovernsSpy.mockResolvedValue(true);
|
|
417
|
+
|
|
418
|
+
await expect(
|
|
419
|
+
UserMiddleware.isGlobalSsoTokenAuthorizedForProject({
|
|
420
|
+
globalSsoTokenData: samlTokenData(),
|
|
421
|
+
projectId,
|
|
422
|
+
}),
|
|
423
|
+
).resolves.toBe(true);
|
|
424
|
+
|
|
425
|
+
expect(ssoGovernsSpy).toHaveBeenCalledTimes(1);
|
|
426
|
+
});
|
|
427
|
+
|
|
428
|
+
test("restrictToAttachedProjects TRUE + provider does NOT govern the project -> refused", async () => {
|
|
429
|
+
ssoTrustSpy.mockResolvedValue(TRUSTED_AND_RESTRICTED);
|
|
430
|
+
ssoGovernsSpy.mockResolvedValue(false);
|
|
431
|
+
|
|
432
|
+
await expect(
|
|
433
|
+
UserMiddleware.isGlobalSsoTokenAuthorizedForProject({
|
|
434
|
+
globalSsoTokenData: samlTokenData(),
|
|
435
|
+
projectId,
|
|
436
|
+
}),
|
|
437
|
+
).resolves.toBe(false);
|
|
438
|
+
|
|
439
|
+
expect(ssoGovernsSpy).toHaveBeenCalledTimes(1);
|
|
440
|
+
});
|
|
441
|
+
|
|
442
|
+
test("flipping ONLY restrictToAttachedProjects turns a non-governed project from allowed to refused", async () => {
|
|
443
|
+
ssoGovernsSpy.mockResolvedValue(false);
|
|
444
|
+
|
|
445
|
+
ssoTrustSpy.mockResolvedValue(TRUSTED_AND_UNRESTRICTED);
|
|
446
|
+
await expect(
|
|
447
|
+
UserMiddleware.isGlobalSsoTokenAuthorizedForProject({
|
|
448
|
+
globalSsoTokenData: samlTokenData(),
|
|
449
|
+
projectId,
|
|
450
|
+
}),
|
|
451
|
+
).resolves.toBe(true);
|
|
452
|
+
|
|
453
|
+
ssoTrustSpy.mockResolvedValue(TRUSTED_AND_RESTRICTED);
|
|
454
|
+
await expect(
|
|
455
|
+
UserMiddleware.isGlobalSsoTokenAuthorizedForProject({
|
|
456
|
+
globalSsoTokenData: samlTokenData(),
|
|
457
|
+
projectId,
|
|
458
|
+
}),
|
|
459
|
+
).resolves.toBe(false);
|
|
460
|
+
});
|
|
461
|
+
|
|
462
|
+
test("governance flows through the enforcement entry point too, both ways", async () => {
|
|
463
|
+
const req: ExpressRequest = buildRequest({
|
|
464
|
+
globalToken: mintGlobalToken({
|
|
465
|
+
userId,
|
|
466
|
+
providerId,
|
|
467
|
+
providerType: SsoProviderType.GlobalSSO,
|
|
468
|
+
}),
|
|
469
|
+
});
|
|
470
|
+
|
|
471
|
+
ssoTrustSpy.mockResolvedValue(TRUSTED_AND_RESTRICTED);
|
|
472
|
+
|
|
473
|
+
ssoGovernsSpy.mockResolvedValue(false);
|
|
474
|
+
await expect(
|
|
475
|
+
UserMiddleware.isSsoSatisfiedForProject({ req, projectId, userId }),
|
|
476
|
+
).resolves.toBe(false);
|
|
477
|
+
|
|
478
|
+
ssoGovernsSpy.mockResolvedValue(true);
|
|
479
|
+
await expect(
|
|
480
|
+
UserMiddleware.isSsoSatisfiedForProject({ req, projectId, userId }),
|
|
481
|
+
).resolves.toBe(true);
|
|
482
|
+
});
|
|
483
|
+
});
|
|
484
|
+
|
|
485
|
+
/*
|
|
486
|
+
* Global SAML and Global OIDC are different tables, different services, and
|
|
487
|
+
* different provider-id namespaces - an id from one must never be resolved
|
|
488
|
+
* against the other, or a disabled SAML provider could be vouched for by an
|
|
489
|
+
* unrelated OIDC row that happens to share its id.
|
|
490
|
+
*/
|
|
491
|
+
describe("isGlobalSsoTokenAuthorizedForProject - SAML vs OIDC routing", () => {
|
|
492
|
+
const projectId: ObjectID = ObjectID.generate();
|
|
493
|
+
const userId: ObjectID = ObjectID.generate();
|
|
494
|
+
const providerId: ObjectID = ObjectID.generate();
|
|
495
|
+
|
|
496
|
+
test("a GlobalSSO token consults the SSO service and NOT the OIDC one", async () => {
|
|
497
|
+
ssoTrustSpy.mockResolvedValue(TRUSTED_AND_RESTRICTED);
|
|
498
|
+
ssoGovernsSpy.mockResolvedValue(true);
|
|
499
|
+
// Rigged the other way, so any cross-talk changes the answer.
|
|
500
|
+
oidcTrustSpy.mockResolvedValue(REVOKED);
|
|
501
|
+
oidcGovernsSpy.mockResolvedValue(false);
|
|
502
|
+
|
|
503
|
+
await expect(
|
|
504
|
+
UserMiddleware.isGlobalSsoTokenAuthorizedForProject({
|
|
505
|
+
globalSsoTokenData: decodeToken(
|
|
506
|
+
mintGlobalToken({
|
|
507
|
+
userId,
|
|
508
|
+
providerId,
|
|
509
|
+
providerType: SsoProviderType.GlobalSSO,
|
|
510
|
+
}),
|
|
511
|
+
),
|
|
512
|
+
projectId,
|
|
513
|
+
}),
|
|
514
|
+
).resolves.toBe(true);
|
|
515
|
+
|
|
516
|
+
expect(ssoTrustSpy).toHaveBeenCalledTimes(1);
|
|
517
|
+
expect(ssoGovernsSpy).toHaveBeenCalledTimes(1);
|
|
518
|
+
expect(oidcTrustSpy).not.toHaveBeenCalled();
|
|
519
|
+
expect(oidcGovernsSpy).not.toHaveBeenCalled();
|
|
520
|
+
});
|
|
521
|
+
|
|
522
|
+
test("a GlobalOIDC token consults the OIDC service and NOT the SSO one", async () => {
|
|
523
|
+
oidcTrustSpy.mockResolvedValue(TRUSTED_AND_RESTRICTED);
|
|
524
|
+
oidcGovernsSpy.mockResolvedValue(true);
|
|
525
|
+
ssoTrustSpy.mockResolvedValue(REVOKED);
|
|
526
|
+
ssoGovernsSpy.mockResolvedValue(false);
|
|
527
|
+
|
|
528
|
+
await expect(
|
|
529
|
+
UserMiddleware.isGlobalSsoTokenAuthorizedForProject({
|
|
530
|
+
globalSsoTokenData: decodeToken(
|
|
531
|
+
mintGlobalToken({
|
|
532
|
+
userId,
|
|
533
|
+
providerId,
|
|
534
|
+
providerType: SsoProviderType.GlobalOIDC,
|
|
535
|
+
}),
|
|
536
|
+
),
|
|
537
|
+
projectId,
|
|
538
|
+
}),
|
|
539
|
+
).resolves.toBe(true);
|
|
540
|
+
|
|
541
|
+
expect(oidcTrustSpy).toHaveBeenCalledTimes(1);
|
|
542
|
+
expect(oidcGovernsSpy).toHaveBeenCalledTimes(1);
|
|
543
|
+
expect(ssoTrustSpy).not.toHaveBeenCalled();
|
|
544
|
+
expect(ssoGovernsSpy).not.toHaveBeenCalled();
|
|
545
|
+
});
|
|
546
|
+
|
|
547
|
+
test("a disabled SAML provider is NOT rescued by an enabled OIDC row of the same id", async () => {
|
|
548
|
+
ssoTrustSpy.mockResolvedValue(REVOKED);
|
|
549
|
+
oidcTrustSpy.mockResolvedValue(TRUSTED_AND_UNRESTRICTED);
|
|
550
|
+
|
|
551
|
+
await expect(
|
|
552
|
+
UserMiddleware.isGlobalSsoTokenAuthorizedForProject({
|
|
553
|
+
globalSsoTokenData: decodeToken(
|
|
554
|
+
mintGlobalToken({
|
|
555
|
+
userId,
|
|
556
|
+
providerId,
|
|
557
|
+
providerType: SsoProviderType.GlobalSSO,
|
|
558
|
+
}),
|
|
559
|
+
),
|
|
560
|
+
projectId,
|
|
561
|
+
}),
|
|
562
|
+
).resolves.toBe(false);
|
|
563
|
+
|
|
564
|
+
expect(oidcTrustSpy).not.toHaveBeenCalled();
|
|
565
|
+
});
|
|
566
|
+
|
|
567
|
+
test("a disabled OIDC provider is NOT rescued by an enabled SAML row of the same id", async () => {
|
|
568
|
+
oidcTrustSpy.mockResolvedValue(REVOKED);
|
|
569
|
+
ssoTrustSpy.mockResolvedValue(TRUSTED_AND_UNRESTRICTED);
|
|
570
|
+
|
|
571
|
+
await expect(
|
|
572
|
+
UserMiddleware.isGlobalSsoTokenAuthorizedForProject({
|
|
573
|
+
globalSsoTokenData: decodeToken(
|
|
574
|
+
mintGlobalToken({
|
|
575
|
+
userId,
|
|
576
|
+
providerId,
|
|
577
|
+
providerType: SsoProviderType.GlobalOIDC,
|
|
578
|
+
}),
|
|
579
|
+
),
|
|
580
|
+
projectId,
|
|
581
|
+
}),
|
|
582
|
+
).resolves.toBe(false);
|
|
583
|
+
|
|
584
|
+
expect(ssoTrustSpy).not.toHaveBeenCalled();
|
|
585
|
+
});
|
|
586
|
+
});
|
|
587
|
+
|
|
588
|
+
/*
|
|
589
|
+
* The decision has to be made about the provider named by THIS token and the
|
|
590
|
+
* project named by THIS request. Passing the wrong id - a hard-coded one, the
|
|
591
|
+
* required-provider id, the user id - would produce answers that look right in
|
|
592
|
+
* a single-provider install and are wrong everywhere else.
|
|
593
|
+
*/
|
|
594
|
+
describe("isGlobalSsoTokenAuthorizedForProject - the ids handed to the services", () => {
|
|
595
|
+
const projectId: ObjectID = ObjectID.generate();
|
|
596
|
+
const userId: ObjectID = ObjectID.generate();
|
|
597
|
+
const providerId: ObjectID = ObjectID.generate();
|
|
598
|
+
|
|
599
|
+
test("SAML: the token's provider id and the request's project id are the ones queried", async () => {
|
|
600
|
+
ssoTrustSpy.mockResolvedValue(TRUSTED_AND_RESTRICTED);
|
|
601
|
+
ssoGovernsSpy.mockResolvedValue(true);
|
|
602
|
+
|
|
603
|
+
await UserMiddleware.isGlobalSsoTokenAuthorizedForProject({
|
|
604
|
+
globalSsoTokenData: decodeToken(
|
|
605
|
+
mintGlobalToken({
|
|
606
|
+
userId,
|
|
607
|
+
providerId,
|
|
608
|
+
providerType: SsoProviderType.GlobalSSO,
|
|
609
|
+
}),
|
|
610
|
+
),
|
|
611
|
+
projectId,
|
|
612
|
+
});
|
|
613
|
+
|
|
614
|
+
expect(ssoTrustSpy).toHaveBeenCalledTimes(1);
|
|
615
|
+
expect(ssoTrustSpy.mock.calls[0]?.[0]?.toString()).toBe(
|
|
616
|
+
providerId.toString(),
|
|
617
|
+
);
|
|
618
|
+
|
|
619
|
+
expect(ssoGovernsSpy).toHaveBeenCalledTimes(1);
|
|
620
|
+
expect(ssoGovernsSpy.mock.calls[0]?.[0]?.globalSsoId.toString()).toBe(
|
|
621
|
+
providerId.toString(),
|
|
622
|
+
);
|
|
623
|
+
expect(ssoGovernsSpy.mock.calls[0]?.[0]?.projectId.toString()).toBe(
|
|
624
|
+
projectId.toString(),
|
|
625
|
+
);
|
|
626
|
+
});
|
|
627
|
+
|
|
628
|
+
test("OIDC: the token's provider id and the request's project id are the ones queried", async () => {
|
|
629
|
+
oidcTrustSpy.mockResolvedValue(TRUSTED_AND_RESTRICTED);
|
|
630
|
+
oidcGovernsSpy.mockResolvedValue(true);
|
|
631
|
+
|
|
632
|
+
await UserMiddleware.isGlobalSsoTokenAuthorizedForProject({
|
|
633
|
+
globalSsoTokenData: decodeToken(
|
|
634
|
+
mintGlobalToken({
|
|
635
|
+
userId,
|
|
636
|
+
providerId,
|
|
637
|
+
providerType: SsoProviderType.GlobalOIDC,
|
|
638
|
+
}),
|
|
639
|
+
),
|
|
640
|
+
projectId,
|
|
641
|
+
});
|
|
642
|
+
|
|
643
|
+
expect(oidcTrustSpy.mock.calls[0]?.[0]?.toString()).toBe(
|
|
644
|
+
providerId.toString(),
|
|
645
|
+
);
|
|
646
|
+
expect(oidcGovernsSpy.mock.calls[0]?.[0]?.globalOidcId.toString()).toBe(
|
|
647
|
+
providerId.toString(),
|
|
648
|
+
);
|
|
649
|
+
expect(oidcGovernsSpy.mock.calls[0]?.[0]?.projectId.toString()).toBe(
|
|
650
|
+
projectId.toString(),
|
|
651
|
+
);
|
|
652
|
+
});
|
|
653
|
+
|
|
654
|
+
test("each project in a multi-project fan-out is queried with its OWN id", async () => {
|
|
655
|
+
ssoTrustSpy.mockResolvedValue(TRUSTED_AND_RESTRICTED);
|
|
656
|
+
ssoGovernsSpy.mockResolvedValue(true);
|
|
657
|
+
|
|
658
|
+
const projectA: ObjectID = ObjectID.generate();
|
|
659
|
+
const projectB: ObjectID = ObjectID.generate();
|
|
660
|
+
|
|
661
|
+
const tokenData: JSONWebTokenData = decodeToken(
|
|
662
|
+
mintGlobalToken({
|
|
663
|
+
userId,
|
|
664
|
+
providerId,
|
|
665
|
+
providerType: SsoProviderType.GlobalSSO,
|
|
666
|
+
}),
|
|
667
|
+
);
|
|
668
|
+
|
|
669
|
+
await UserMiddleware.isGlobalSsoTokenAuthorizedForProject({
|
|
670
|
+
globalSsoTokenData: tokenData,
|
|
671
|
+
projectId: projectA,
|
|
672
|
+
});
|
|
673
|
+
await UserMiddleware.isGlobalSsoTokenAuthorizedForProject({
|
|
674
|
+
globalSsoTokenData: tokenData,
|
|
675
|
+
projectId: projectB,
|
|
676
|
+
});
|
|
677
|
+
|
|
678
|
+
expect(ssoGovernsSpy.mock.calls[0]?.[0]?.projectId.toString()).toBe(
|
|
679
|
+
projectA.toString(),
|
|
680
|
+
);
|
|
681
|
+
expect(ssoGovernsSpy.mock.calls[1]?.[0]?.projectId.toString()).toBe(
|
|
682
|
+
projectB.toString(),
|
|
683
|
+
);
|
|
684
|
+
});
|
|
685
|
+
});
|
|
686
|
+
|
|
687
|
+
/*
|
|
688
|
+
* "This provider is not allowed here" and "we could not find out" are
|
|
689
|
+
* different answers, and the code must not conflate them.
|
|
690
|
+
*
|
|
691
|
+
* If a failed lookup were caught and turned into `false`, a database blip - a
|
|
692
|
+
* dropped connection, a statement timeout, a failover - would look exactly
|
|
693
|
+
* like a deliberate permission decision. On the multi-tenant permission path
|
|
694
|
+
* that means the request still succeeds, with 200 and an empty permission
|
|
695
|
+
* set: the UI silently loses every button instead of showing an error, and
|
|
696
|
+
* nothing in the logs distinguishes it from a correctly-revoked user. Letting
|
|
697
|
+
* it throw turns an infrastructure fault into an infrastructure-shaped
|
|
698
|
+
* failure that alerts, retries, and gets fixed.
|
|
699
|
+
*/
|
|
700
|
+
describe("isGlobalSsoTokenAuthorizedForProject - a failed lookup THROWS, it does not deny", () => {
|
|
701
|
+
const projectId: ObjectID = ObjectID.generate();
|
|
702
|
+
const userId: ObjectID = ObjectID.generate();
|
|
703
|
+
const providerId: ObjectID = ObjectID.generate();
|
|
704
|
+
|
|
705
|
+
const samlTokenData: () => JSONWebTokenData = (): JSONWebTokenData => {
|
|
706
|
+
return decodeToken(
|
|
707
|
+
mintGlobalToken({
|
|
708
|
+
userId,
|
|
709
|
+
providerId,
|
|
710
|
+
providerType: SsoProviderType.GlobalSSO,
|
|
711
|
+
}),
|
|
712
|
+
);
|
|
713
|
+
};
|
|
714
|
+
|
|
715
|
+
const buildGlobalRequest: (
|
|
716
|
+
providerType: SsoProviderType,
|
|
717
|
+
) => ExpressRequest = (providerType: SsoProviderType): ExpressRequest => {
|
|
718
|
+
return buildRequest({
|
|
719
|
+
globalToken: mintGlobalToken({ userId, providerId, providerType }),
|
|
720
|
+
});
|
|
721
|
+
};
|
|
722
|
+
|
|
723
|
+
test("SAML trust lookup rejects -> the decision rejects rather than resolving false", async () => {
|
|
724
|
+
ssoTrustSpy.mockRejectedValue(new Error("connection terminated"));
|
|
725
|
+
|
|
726
|
+
await expect(
|
|
727
|
+
UserMiddleware.isGlobalSsoTokenAuthorizedForProject({
|
|
728
|
+
globalSsoTokenData: samlTokenData(),
|
|
729
|
+
projectId,
|
|
730
|
+
}),
|
|
731
|
+
).rejects.toThrow("connection terminated");
|
|
732
|
+
|
|
733
|
+
/*
|
|
734
|
+
* And the same lookup succeeding is a plain "allowed" - so the rejection
|
|
735
|
+
* above is about the failure and nothing else.
|
|
736
|
+
*/
|
|
737
|
+
ssoTrustSpy.mockResolvedValue(TRUSTED_AND_UNRESTRICTED);
|
|
738
|
+
|
|
739
|
+
await expect(
|
|
740
|
+
UserMiddleware.isGlobalSsoTokenAuthorizedForProject({
|
|
741
|
+
globalSsoTokenData: samlTokenData(),
|
|
742
|
+
projectId,
|
|
743
|
+
}),
|
|
744
|
+
).resolves.toBe(true);
|
|
745
|
+
});
|
|
746
|
+
|
|
747
|
+
test("OIDC trust lookup rejects -> the decision rejects rather than resolving false", async () => {
|
|
748
|
+
oidcTrustSpy.mockRejectedValue(new Error("statement timeout"));
|
|
749
|
+
|
|
750
|
+
await expect(
|
|
751
|
+
UserMiddleware.isGlobalSsoTokenAuthorizedForProject({
|
|
752
|
+
globalSsoTokenData: decodeToken(
|
|
753
|
+
mintGlobalToken({
|
|
754
|
+
userId,
|
|
755
|
+
providerId,
|
|
756
|
+
providerType: SsoProviderType.GlobalOIDC,
|
|
757
|
+
}),
|
|
758
|
+
),
|
|
759
|
+
projectId,
|
|
760
|
+
}),
|
|
761
|
+
).rejects.toThrow("statement timeout");
|
|
762
|
+
});
|
|
763
|
+
|
|
764
|
+
test("governance lookup rejects under restrict-on -> the decision rejects", async () => {
|
|
765
|
+
ssoTrustSpy.mockResolvedValue(TRUSTED_AND_RESTRICTED);
|
|
766
|
+
ssoGovernsSpy.mockRejectedValue(new Error("attachment read failed"));
|
|
767
|
+
|
|
768
|
+
await expect(
|
|
769
|
+
UserMiddleware.isGlobalSsoTokenAuthorizedForProject({
|
|
770
|
+
globalSsoTokenData: samlTokenData(),
|
|
771
|
+
projectId,
|
|
772
|
+
}),
|
|
773
|
+
).rejects.toThrow("attachment read failed");
|
|
774
|
+
|
|
775
|
+
// Same restrict-on setup, lookup healthy -> allowed.
|
|
776
|
+
ssoGovernsSpy.mockResolvedValue(true);
|
|
777
|
+
|
|
778
|
+
await expect(
|
|
779
|
+
UserMiddleware.isGlobalSsoTokenAuthorizedForProject({
|
|
780
|
+
globalSsoTokenData: samlTokenData(),
|
|
781
|
+
projectId,
|
|
782
|
+
}),
|
|
783
|
+
).resolves.toBe(true);
|
|
784
|
+
});
|
|
785
|
+
|
|
786
|
+
test("OIDC governance lookup rejects under restrict-on -> the decision rejects", async () => {
|
|
787
|
+
oidcTrustSpy.mockResolvedValue(TRUSTED_AND_RESTRICTED);
|
|
788
|
+
oidcGovernsSpy.mockRejectedValue(new Error("oidc attachment read failed"));
|
|
789
|
+
|
|
790
|
+
await expect(
|
|
791
|
+
UserMiddleware.isGlobalSsoTokenAuthorizedForProject({
|
|
792
|
+
globalSsoTokenData: decodeToken(
|
|
793
|
+
mintGlobalToken({
|
|
794
|
+
userId,
|
|
795
|
+
providerId,
|
|
796
|
+
providerType: SsoProviderType.GlobalOIDC,
|
|
797
|
+
}),
|
|
798
|
+
),
|
|
799
|
+
projectId,
|
|
800
|
+
}),
|
|
801
|
+
).rejects.toThrow("oidc attachment read failed");
|
|
802
|
+
});
|
|
803
|
+
|
|
804
|
+
test("the enforcement entry point propagates the failure instead of denying", async () => {
|
|
805
|
+
const req: ExpressRequest = buildGlobalRequest(SsoProviderType.GlobalSSO);
|
|
806
|
+
|
|
807
|
+
ssoTrustSpy.mockRejectedValue(new Error("connection terminated"));
|
|
808
|
+
|
|
809
|
+
await expect(
|
|
810
|
+
UserMiddleware.isSsoSatisfiedForProject({ req, projectId, userId }),
|
|
811
|
+
).rejects.toThrow("connection terminated");
|
|
812
|
+
|
|
813
|
+
ssoTrustSpy.mockResolvedValue(TRUSTED_AND_UNRESTRICTED);
|
|
814
|
+
|
|
815
|
+
await expect(
|
|
816
|
+
UserMiddleware.isSsoSatisfiedForProject({ req, projectId, userId }),
|
|
817
|
+
).resolves.toBe(true);
|
|
818
|
+
});
|
|
819
|
+
|
|
820
|
+
test("the enforcement entry point propagates a failed governance lookup too", async () => {
|
|
821
|
+
const req: ExpressRequest = buildGlobalRequest(SsoProviderType.GlobalSSO);
|
|
822
|
+
|
|
823
|
+
ssoTrustSpy.mockResolvedValue(TRUSTED_AND_RESTRICTED);
|
|
824
|
+
ssoGovernsSpy.mockRejectedValue(new Error("attachment read failed"));
|
|
825
|
+
|
|
826
|
+
await expect(
|
|
827
|
+
UserMiddleware.isSsoSatisfiedForProject({ req, projectId, userId }),
|
|
828
|
+
).rejects.toThrow("attachment read failed");
|
|
829
|
+
});
|
|
830
|
+
});
|
|
831
|
+
|
|
832
|
+
/*
|
|
833
|
+
* A Global-typed token with no `ssoProviderId` cannot be checked against
|
|
834
|
+
* either question, so it cannot be trusted for a project. That is a DECISION -
|
|
835
|
+
* `false` - not a failure. It must not throw, and it must not spend a query
|
|
836
|
+
* finding out, because the answer is fully determined by the token.
|
|
837
|
+
*/
|
|
838
|
+
describe("isGlobalSsoTokenAuthorizedForProject - a token with no provider id", () => {
|
|
839
|
+
const projectId: ObjectID = ObjectID.generate();
|
|
840
|
+
const userId: ObjectID = ObjectID.generate();
|
|
841
|
+
const providerId: ObjectID = ObjectID.generate();
|
|
842
|
+
|
|
843
|
+
const mintProviderlessGlobalToken: () => string = (): string => {
|
|
844
|
+
return JSONWebToken.sign({
|
|
845
|
+
data: {
|
|
846
|
+
userId: userId,
|
|
847
|
+
name: new Name("Global SSO User"),
|
|
848
|
+
email: new Email("global-sso@oneuptime.com"),
|
|
849
|
+
isMasterAdmin: false,
|
|
850
|
+
isGeneralLogin: false,
|
|
851
|
+
// No ssoProviderId at all - the shape a pre-discriminator token had.
|
|
852
|
+
ssoProviderType: SsoProviderType.GlobalSSO.toString(),
|
|
853
|
+
},
|
|
854
|
+
expiresInSeconds: THIRTY_DAYS_IN_SECONDS,
|
|
855
|
+
});
|
|
856
|
+
};
|
|
857
|
+
|
|
858
|
+
test("the fixture really is a Global token with no provider id", () => {
|
|
859
|
+
const decoded: JSONWebTokenData = decodeToken(
|
|
860
|
+
mintProviderlessGlobalToken(),
|
|
861
|
+
);
|
|
862
|
+
|
|
863
|
+
expect(decoded.ssoProviderType).toBe(SsoProviderType.GlobalSSO);
|
|
864
|
+
expect(decoded.ssoProviderId).toBeUndefined();
|
|
865
|
+
});
|
|
866
|
+
|
|
867
|
+
test("resolves false, and never touches the database", async () => {
|
|
868
|
+
await expect(
|
|
869
|
+
UserMiddleware.isGlobalSsoTokenAuthorizedForProject({
|
|
870
|
+
globalSsoTokenData: decodeToken(mintProviderlessGlobalToken()),
|
|
871
|
+
projectId,
|
|
872
|
+
}),
|
|
873
|
+
).resolves.toBe(false);
|
|
874
|
+
|
|
875
|
+
expectNoDatabaseLookups();
|
|
876
|
+
});
|
|
877
|
+
|
|
878
|
+
test("the same token WITH a provider id is allowed - the refusal is about the missing id", async () => {
|
|
879
|
+
ssoTrustSpy.mockResolvedValue(TRUSTED_AND_UNRESTRICTED);
|
|
880
|
+
|
|
881
|
+
await expect(
|
|
882
|
+
UserMiddleware.isGlobalSsoTokenAuthorizedForProject({
|
|
883
|
+
globalSsoTokenData: decodeToken(
|
|
884
|
+
mintGlobalToken({
|
|
885
|
+
userId,
|
|
886
|
+
providerId,
|
|
887
|
+
providerType: SsoProviderType.GlobalSSO,
|
|
888
|
+
}),
|
|
889
|
+
),
|
|
890
|
+
projectId,
|
|
891
|
+
}),
|
|
892
|
+
).resolves.toBe(true);
|
|
893
|
+
|
|
894
|
+
expect(ssoTrustSpy).toHaveBeenCalledTimes(1);
|
|
895
|
+
});
|
|
896
|
+
|
|
897
|
+
test("through the enforcement entry point: refused, with no database lookups", async () => {
|
|
898
|
+
const req: ExpressRequest = buildRequest({
|
|
899
|
+
globalToken: mintProviderlessGlobalToken(),
|
|
900
|
+
});
|
|
901
|
+
|
|
902
|
+
await expect(
|
|
903
|
+
UserMiddleware.isSsoSatisfiedForProject({ req, projectId, userId }),
|
|
904
|
+
).resolves.toBe(false);
|
|
905
|
+
|
|
906
|
+
expectNoDatabaseLookups();
|
|
907
|
+
});
|
|
908
|
+
});
|
|
909
|
+
|
|
910
|
+
/*
|
|
911
|
+
* A per-project SSO token is bound to one project by a login that already
|
|
912
|
+
* proved the project's own provider trusts this user. It is decided
|
|
913
|
+
* statelessly and short-circuits the whole stateful path - which is what
|
|
914
|
+
* keeps a project-SSO-only installation from paying for Global SSO queries it
|
|
915
|
+
* has no use for.
|
|
916
|
+
*/
|
|
917
|
+
describe("isSsoSatisfiedForProject - the per-project token short-circuits", () => {
|
|
918
|
+
const projectId: ObjectID = ObjectID.generate();
|
|
919
|
+
const otherProjectId: ObjectID = ObjectID.generate();
|
|
920
|
+
const userId: ObjectID = ObjectID.generate();
|
|
921
|
+
const providerId: ObjectID = ObjectID.generate();
|
|
922
|
+
|
|
923
|
+
test("a valid ProjectSSO token for THIS project -> true, with no global lookups", async () => {
|
|
924
|
+
// Rigged so any fall-through to the global path would refuse.
|
|
925
|
+
ssoTrustSpy.mockResolvedValue(REVOKED);
|
|
926
|
+
oidcTrustSpy.mockResolvedValue(REVOKED);
|
|
927
|
+
|
|
928
|
+
const req: ExpressRequest = buildRequest({
|
|
929
|
+
projectTokens: [
|
|
930
|
+
{
|
|
931
|
+
projectId,
|
|
932
|
+
token: mintProjectToken({
|
|
933
|
+
userId,
|
|
934
|
+
projectId,
|
|
935
|
+
providerId,
|
|
936
|
+
providerType: SsoProviderType.ProjectSSO,
|
|
937
|
+
}),
|
|
938
|
+
},
|
|
939
|
+
],
|
|
940
|
+
});
|
|
941
|
+
|
|
942
|
+
await expect(
|
|
943
|
+
UserMiddleware.isSsoSatisfiedForProject({ req, projectId, userId }),
|
|
944
|
+
).resolves.toBe(true);
|
|
945
|
+
|
|
946
|
+
expectNoDatabaseLookups();
|
|
947
|
+
});
|
|
948
|
+
|
|
949
|
+
test("a valid ProjectOIDC token for THIS project -> true, with no global lookups", async () => {
|
|
950
|
+
ssoTrustSpy.mockResolvedValue(REVOKED);
|
|
951
|
+
oidcTrustSpy.mockResolvedValue(REVOKED);
|
|
952
|
+
|
|
953
|
+
const req: ExpressRequest = buildRequest({
|
|
954
|
+
projectTokens: [
|
|
955
|
+
{
|
|
956
|
+
projectId,
|
|
957
|
+
token: mintProjectToken({
|
|
958
|
+
userId,
|
|
959
|
+
projectId,
|
|
960
|
+
providerId,
|
|
961
|
+
providerType: SsoProviderType.ProjectOIDC,
|
|
962
|
+
}),
|
|
963
|
+
},
|
|
964
|
+
],
|
|
965
|
+
});
|
|
966
|
+
|
|
967
|
+
await expect(
|
|
968
|
+
UserMiddleware.isSsoSatisfiedForProject({ req, projectId, userId }),
|
|
969
|
+
).resolves.toBe(true);
|
|
970
|
+
|
|
971
|
+
expectNoDatabaseLookups();
|
|
972
|
+
});
|
|
973
|
+
|
|
974
|
+
test("a per-project token for a DIFFERENT project does not short-circuit - the global path decides", async () => {
|
|
975
|
+
const req: ExpressRequest = buildRequest({
|
|
976
|
+
globalToken: mintGlobalToken({
|
|
977
|
+
userId,
|
|
978
|
+
providerId,
|
|
979
|
+
providerType: SsoProviderType.GlobalSSO,
|
|
980
|
+
}),
|
|
981
|
+
projectTokens: [
|
|
982
|
+
{
|
|
983
|
+
projectId: otherProjectId,
|
|
984
|
+
token: mintProjectToken({
|
|
985
|
+
userId,
|
|
986
|
+
projectId: otherProjectId,
|
|
987
|
+
providerId,
|
|
988
|
+
providerType: SsoProviderType.ProjectSSO,
|
|
989
|
+
}),
|
|
990
|
+
},
|
|
991
|
+
],
|
|
992
|
+
});
|
|
993
|
+
|
|
994
|
+
/*
|
|
995
|
+
* The global provider is disabled, so the unrelated project token must
|
|
996
|
+
* not carry this request.
|
|
997
|
+
*/
|
|
998
|
+
ssoTrustSpy.mockResolvedValue(REVOKED);
|
|
999
|
+
|
|
1000
|
+
await expect(
|
|
1001
|
+
UserMiddleware.isSsoSatisfiedForProject({ req, projectId, userId }),
|
|
1002
|
+
).resolves.toBe(false);
|
|
1003
|
+
|
|
1004
|
+
expect(ssoTrustSpy).toHaveBeenCalledTimes(1);
|
|
1005
|
+
|
|
1006
|
+
// The project the token IS for still short-circuits, disabled or not.
|
|
1007
|
+
ssoTrustSpy.mockClear();
|
|
1008
|
+
|
|
1009
|
+
await expect(
|
|
1010
|
+
UserMiddleware.isSsoSatisfiedForProject({
|
|
1011
|
+
req,
|
|
1012
|
+
projectId: otherProjectId,
|
|
1013
|
+
userId,
|
|
1014
|
+
}),
|
|
1015
|
+
).resolves.toBe(true);
|
|
1016
|
+
|
|
1017
|
+
expectNoDatabaseLookups();
|
|
1018
|
+
});
|
|
1019
|
+
|
|
1020
|
+
test("with the global provider healthy, the other project is satisfied by the global token", async () => {
|
|
1021
|
+
const req: ExpressRequest = buildRequest({
|
|
1022
|
+
globalToken: mintGlobalToken({
|
|
1023
|
+
userId,
|
|
1024
|
+
providerId,
|
|
1025
|
+
providerType: SsoProviderType.GlobalSSO,
|
|
1026
|
+
}),
|
|
1027
|
+
projectTokens: [
|
|
1028
|
+
{
|
|
1029
|
+
projectId: otherProjectId,
|
|
1030
|
+
token: mintProjectToken({
|
|
1031
|
+
userId,
|
|
1032
|
+
projectId: otherProjectId,
|
|
1033
|
+
providerId,
|
|
1034
|
+
providerType: SsoProviderType.ProjectSSO,
|
|
1035
|
+
}),
|
|
1036
|
+
},
|
|
1037
|
+
],
|
|
1038
|
+
});
|
|
1039
|
+
|
|
1040
|
+
ssoTrustSpy.mockResolvedValue(TRUSTED_AND_UNRESTRICTED);
|
|
1041
|
+
|
|
1042
|
+
await expect(
|
|
1043
|
+
UserMiddleware.isSsoSatisfiedForProject({ req, projectId, userId }),
|
|
1044
|
+
).resolves.toBe(true);
|
|
1045
|
+
|
|
1046
|
+
expect(ssoTrustSpy).toHaveBeenCalledTimes(1);
|
|
1047
|
+
});
|
|
1048
|
+
});
|
|
1049
|
+
|
|
1050
|
+
/*
|
|
1051
|
+
* THE UPGRADE HAZARD.
|
|
1052
|
+
*
|
|
1053
|
+
* Before the single-token redesign the Global SSO router minted one
|
|
1054
|
+
* per-project token PER PROJECT, typed GlobalSSO/GlobalOIDC and signed for 30
|
|
1055
|
+
* days. Those are still sitting in browsers as `sso-<projectId>` cookies and
|
|
1056
|
+
* in the mobile app's AsyncStorage, replayed on every request. If the
|
|
1057
|
+
* per-project slot accepted them they would short-circuit past the
|
|
1058
|
+
* provider-trust check - so an admin disabling a provider would still not
|
|
1059
|
+
* revoke them, for up to a month. The slot refuses them by TYPE, which sends
|
|
1060
|
+
* the user through a fresh global login that mints a properly-typed token.
|
|
1061
|
+
*/
|
|
1062
|
+
describe("isSsoSatisfiedForProject - a legacy Global-typed token in the per-project slot", () => {
|
|
1063
|
+
const projectId: ObjectID = ObjectID.generate();
|
|
1064
|
+
const userId: ObjectID = ObjectID.generate();
|
|
1065
|
+
const providerId: ObjectID = ObjectID.generate();
|
|
1066
|
+
|
|
1067
|
+
const buildLegacyRequest: (data: {
|
|
1068
|
+
projectSlotType: SsoProviderType;
|
|
1069
|
+
alsoInGlobalSlot: boolean;
|
|
1070
|
+
}) => ExpressRequest = (data: {
|
|
1071
|
+
projectSlotType: SsoProviderType;
|
|
1072
|
+
alsoInGlobalSlot: boolean;
|
|
1073
|
+
}): ExpressRequest => {
|
|
1074
|
+
return buildRequest({
|
|
1075
|
+
globalToken: data.alsoInGlobalSlot
|
|
1076
|
+
? mintGlobalToken({
|
|
1077
|
+
userId,
|
|
1078
|
+
providerId,
|
|
1079
|
+
providerType: SsoProviderType.GlobalSSO,
|
|
1080
|
+
})
|
|
1081
|
+
: undefined,
|
|
1082
|
+
projectTokens: [
|
|
1083
|
+
{
|
|
1084
|
+
projectId,
|
|
1085
|
+
token: mintProjectToken({
|
|
1086
|
+
userId,
|
|
1087
|
+
projectId,
|
|
1088
|
+
providerId,
|
|
1089
|
+
providerType: data.projectSlotType,
|
|
1090
|
+
}),
|
|
1091
|
+
},
|
|
1092
|
+
],
|
|
1093
|
+
});
|
|
1094
|
+
};
|
|
1095
|
+
|
|
1096
|
+
test("with the provider disabled, the legacy GlobalSSO-typed token no longer buys 30 days of access", async () => {
|
|
1097
|
+
ssoTrustSpy.mockResolvedValue(REVOKED);
|
|
1098
|
+
|
|
1099
|
+
await expect(
|
|
1100
|
+
UserMiddleware.isSsoSatisfiedForProject({
|
|
1101
|
+
req: buildLegacyRequest({
|
|
1102
|
+
projectSlotType: SsoProviderType.GlobalSSO,
|
|
1103
|
+
alsoInGlobalSlot: true,
|
|
1104
|
+
}),
|
|
1105
|
+
projectId,
|
|
1106
|
+
userId,
|
|
1107
|
+
}),
|
|
1108
|
+
).resolves.toBe(false);
|
|
1109
|
+
|
|
1110
|
+
// It really did go through the stateful path rather than being dropped.
|
|
1111
|
+
expect(ssoTrustSpy).toHaveBeenCalledTimes(1);
|
|
1112
|
+
});
|
|
1113
|
+
|
|
1114
|
+
test("the SAME request with the project-slot token typed ProjectSSO short-circuits and is allowed", async () => {
|
|
1115
|
+
/*
|
|
1116
|
+
* The pairing that keeps the test above honest: a blanket-deny
|
|
1117
|
+
* implementation would fail here. A properly project-typed token is a
|
|
1118
|
+
* project login, decided statelessly, so the disabled GLOBAL provider is
|
|
1119
|
+
* irrelevant to it.
|
|
1120
|
+
*/
|
|
1121
|
+
ssoTrustSpy.mockResolvedValue(REVOKED);
|
|
1122
|
+
|
|
1123
|
+
await expect(
|
|
1124
|
+
UserMiddleware.isSsoSatisfiedForProject({
|
|
1125
|
+
req: buildLegacyRequest({
|
|
1126
|
+
projectSlotType: SsoProviderType.ProjectSSO,
|
|
1127
|
+
alsoInGlobalSlot: true,
|
|
1128
|
+
}),
|
|
1129
|
+
projectId,
|
|
1130
|
+
userId,
|
|
1131
|
+
}),
|
|
1132
|
+
).resolves.toBe(true);
|
|
1133
|
+
|
|
1134
|
+
expectNoDatabaseLookups();
|
|
1135
|
+
});
|
|
1136
|
+
|
|
1137
|
+
test("a legacy GlobalOIDC-typed token in the project slot is refused the same way", async () => {
|
|
1138
|
+
oidcTrustSpy.mockResolvedValue(REVOKED);
|
|
1139
|
+
|
|
1140
|
+
const req: ExpressRequest = buildRequest({
|
|
1141
|
+
globalToken: mintGlobalToken({
|
|
1142
|
+
userId,
|
|
1143
|
+
providerId,
|
|
1144
|
+
providerType: SsoProviderType.GlobalOIDC,
|
|
1145
|
+
}),
|
|
1146
|
+
projectTokens: [
|
|
1147
|
+
{
|
|
1148
|
+
projectId,
|
|
1149
|
+
token: mintProjectToken({
|
|
1150
|
+
userId,
|
|
1151
|
+
projectId,
|
|
1152
|
+
providerId,
|
|
1153
|
+
providerType: SsoProviderType.GlobalOIDC,
|
|
1154
|
+
}),
|
|
1155
|
+
},
|
|
1156
|
+
],
|
|
1157
|
+
});
|
|
1158
|
+
|
|
1159
|
+
await expect(
|
|
1160
|
+
UserMiddleware.isSsoSatisfiedForProject({ req, projectId, userId }),
|
|
1161
|
+
).resolves.toBe(false);
|
|
1162
|
+
|
|
1163
|
+
expect(oidcTrustSpy).toHaveBeenCalledTimes(1);
|
|
1164
|
+
});
|
|
1165
|
+
|
|
1166
|
+
test("a legacy Global-typed token ALONE in the project slot is not a credential at all", async () => {
|
|
1167
|
+
// Provider perfectly healthy - the refusal is about the slot and type.
|
|
1168
|
+
ssoTrustSpy.mockResolvedValue(TRUSTED_AND_UNRESTRICTED);
|
|
1169
|
+
|
|
1170
|
+
await expect(
|
|
1171
|
+
UserMiddleware.isSsoSatisfiedForProject({
|
|
1172
|
+
req: buildLegacyRequest({
|
|
1173
|
+
projectSlotType: SsoProviderType.GlobalSSO,
|
|
1174
|
+
alsoInGlobalSlot: false,
|
|
1175
|
+
}),
|
|
1176
|
+
projectId,
|
|
1177
|
+
userId,
|
|
1178
|
+
}),
|
|
1179
|
+
).resolves.toBe(false);
|
|
1180
|
+
|
|
1181
|
+
// There was no global token to check, so nothing was looked up.
|
|
1182
|
+
expectNoDatabaseLookups();
|
|
1183
|
+
});
|
|
1184
|
+
|
|
1185
|
+
test("moving that same credential into the global slot makes it work again", async () => {
|
|
1186
|
+
ssoTrustSpy.mockResolvedValue(TRUSTED_AND_UNRESTRICTED);
|
|
1187
|
+
|
|
1188
|
+
await expect(
|
|
1189
|
+
UserMiddleware.isSsoSatisfiedForProject({
|
|
1190
|
+
req: buildRequest({
|
|
1191
|
+
globalToken: mintGlobalToken({
|
|
1192
|
+
userId,
|
|
1193
|
+
providerId,
|
|
1194
|
+
providerType: SsoProviderType.GlobalSSO,
|
|
1195
|
+
}),
|
|
1196
|
+
}),
|
|
1197
|
+
projectId,
|
|
1198
|
+
userId,
|
|
1199
|
+
}),
|
|
1200
|
+
).resolves.toBe(true);
|
|
1201
|
+
|
|
1202
|
+
expect(ssoTrustSpy).toHaveBeenCalledTimes(1);
|
|
1203
|
+
});
|
|
1204
|
+
});
|
|
1205
|
+
|
|
1206
|
+
/*
|
|
1207
|
+
* Every stateless gate must be decided BEFORE the database is consulted. Two
|
|
1208
|
+
* reasons: a forged or expired token should never cost a query (this path runs
|
|
1209
|
+
* on every authenticated request), and a lookup that ran first would make the
|
|
1210
|
+
* request's fate depend on database health for tokens that were never valid
|
|
1211
|
+
* in the first place.
|
|
1212
|
+
*/
|
|
1213
|
+
describe("isSsoSatisfiedForProject - stateless gates run before the database", () => {
|
|
1214
|
+
const projectId: ObjectID = ObjectID.generate();
|
|
1215
|
+
const userId: ObjectID = ObjectID.generate();
|
|
1216
|
+
const providerId: ObjectID = ObjectID.generate();
|
|
1217
|
+
|
|
1218
|
+
const validGlobalToken: () => string = (): string => {
|
|
1219
|
+
return mintGlobalToken({
|
|
1220
|
+
userId,
|
|
1221
|
+
providerId,
|
|
1222
|
+
providerType: SsoProviderType.GlobalSSO,
|
|
1223
|
+
});
|
|
1224
|
+
};
|
|
1225
|
+
|
|
1226
|
+
test("no token at all -> false, no lookups", async () => {
|
|
1227
|
+
await expect(
|
|
1228
|
+
UserMiddleware.isSsoSatisfiedForProject({
|
|
1229
|
+
req: buildRequest({}),
|
|
1230
|
+
projectId,
|
|
1231
|
+
userId,
|
|
1232
|
+
}),
|
|
1233
|
+
).resolves.toBe(false);
|
|
1234
|
+
|
|
1235
|
+
expectNoDatabaseLookups();
|
|
1236
|
+
});
|
|
1237
|
+
|
|
1238
|
+
test("a token belonging to a DIFFERENT user -> false, no lookups", async () => {
|
|
1239
|
+
const req: ExpressRequest = buildRequest({
|
|
1240
|
+
globalToken: validGlobalToken(),
|
|
1241
|
+
});
|
|
1242
|
+
const otherUserId: ObjectID = ObjectID.generate();
|
|
1243
|
+
|
|
1244
|
+
await expect(
|
|
1245
|
+
UserMiddleware.isSsoSatisfiedForProject({
|
|
1246
|
+
req,
|
|
1247
|
+
projectId,
|
|
1248
|
+
userId: otherUserId,
|
|
1249
|
+
}),
|
|
1250
|
+
).resolves.toBe(false);
|
|
1251
|
+
|
|
1252
|
+
expectNoDatabaseLookups();
|
|
1253
|
+
|
|
1254
|
+
// The user it was minted for still gets in, via the stateful path.
|
|
1255
|
+
await expect(
|
|
1256
|
+
UserMiddleware.isSsoSatisfiedForProject({ req, projectId, userId }),
|
|
1257
|
+
).resolves.toBe(true);
|
|
1258
|
+
|
|
1259
|
+
expect(ssoTrustSpy).toHaveBeenCalledTimes(1);
|
|
1260
|
+
});
|
|
1261
|
+
|
|
1262
|
+
test("an expired token -> false, no lookups", async () => {
|
|
1263
|
+
const expiredToken: string = JSONWebToken.sign({
|
|
1264
|
+
data: {
|
|
1265
|
+
userId: userId,
|
|
1266
|
+
name: new Name("Global SSO User"),
|
|
1267
|
+
email: new Email("global-sso@oneuptime.com"),
|
|
1268
|
+
isMasterAdmin: false,
|
|
1269
|
+
isGeneralLogin: false,
|
|
1270
|
+
ssoProviderId: providerId.toString(),
|
|
1271
|
+
ssoProviderType: SsoProviderType.GlobalSSO.toString(),
|
|
1272
|
+
},
|
|
1273
|
+
expiresInSeconds: -60,
|
|
1274
|
+
});
|
|
1275
|
+
|
|
1276
|
+
await expect(
|
|
1277
|
+
UserMiddleware.isSsoSatisfiedForProject({
|
|
1278
|
+
req: buildRequest({ globalToken: expiredToken }),
|
|
1279
|
+
projectId,
|
|
1280
|
+
userId,
|
|
1281
|
+
}),
|
|
1282
|
+
).resolves.toBe(false);
|
|
1283
|
+
|
|
1284
|
+
expectNoDatabaseLookups();
|
|
1285
|
+
|
|
1286
|
+
// The identical payload, unexpired, is allowed.
|
|
1287
|
+
await expect(
|
|
1288
|
+
UserMiddleware.isSsoSatisfiedForProject({
|
|
1289
|
+
req: buildRequest({ globalToken: validGlobalToken() }),
|
|
1290
|
+
projectId,
|
|
1291
|
+
userId,
|
|
1292
|
+
}),
|
|
1293
|
+
).resolves.toBe(true);
|
|
1294
|
+
});
|
|
1295
|
+
|
|
1296
|
+
test("a token whose payload was edited after signing -> false, no lookups", async () => {
|
|
1297
|
+
const genuine: string = validGlobalToken();
|
|
1298
|
+
const parts: Array<string> = genuine.split(".");
|
|
1299
|
+
|
|
1300
|
+
const payload: Record<string, unknown> = JSON.parse(
|
|
1301
|
+
Buffer.from(parts[1] as string, "base64url").toString("utf8"),
|
|
1302
|
+
) as Record<string, unknown>;
|
|
1303
|
+
|
|
1304
|
+
// Re-point the token at a provider the attacker would rather use.
|
|
1305
|
+
payload["ssoProviderId"] = ObjectID.generate().toString();
|
|
1306
|
+
|
|
1307
|
+
const forged: string = `${parts[0]}.${Buffer.from(
|
|
1308
|
+
JSON.stringify(payload),
|
|
1309
|
+
"utf8",
|
|
1310
|
+
).toString("base64url")}.${parts[2]}`;
|
|
1311
|
+
|
|
1312
|
+
await expect(
|
|
1313
|
+
UserMiddleware.isSsoSatisfiedForProject({
|
|
1314
|
+
req: buildRequest({ globalToken: forged }),
|
|
1315
|
+
projectId,
|
|
1316
|
+
userId,
|
|
1317
|
+
}),
|
|
1318
|
+
).resolves.toBe(false);
|
|
1319
|
+
|
|
1320
|
+
expectNoDatabaseLookups();
|
|
1321
|
+
|
|
1322
|
+
// The untampered token is allowed, so the refusal is about the edit.
|
|
1323
|
+
await expect(
|
|
1324
|
+
UserMiddleware.isSsoSatisfiedForProject({
|
|
1325
|
+
req: buildRequest({ globalToken: genuine }),
|
|
1326
|
+
projectId,
|
|
1327
|
+
userId,
|
|
1328
|
+
}),
|
|
1329
|
+
).resolves.toBe(true);
|
|
1330
|
+
});
|
|
1331
|
+
|
|
1332
|
+
test("a project pinned to a DIFFERENT provider -> false, no lookups", async () => {
|
|
1333
|
+
const req: ExpressRequest = buildRequest({
|
|
1334
|
+
globalToken: validGlobalToken(),
|
|
1335
|
+
});
|
|
1336
|
+
const requiredProviderId: ObjectID = ObjectID.generate();
|
|
1337
|
+
|
|
1338
|
+
await expect(
|
|
1339
|
+
UserMiddleware.isSsoSatisfiedForProject({
|
|
1340
|
+
req,
|
|
1341
|
+
projectId,
|
|
1342
|
+
userId,
|
|
1343
|
+
requiredSsoProviderId: requiredProviderId,
|
|
1344
|
+
}),
|
|
1345
|
+
).resolves.toBe(false);
|
|
1346
|
+
|
|
1347
|
+
expectNoDatabaseLookups();
|
|
1348
|
+
|
|
1349
|
+
// Pinned to the provider the token actually carries -> allowed.
|
|
1350
|
+
await expect(
|
|
1351
|
+
UserMiddleware.isSsoSatisfiedForProject({
|
|
1352
|
+
req,
|
|
1353
|
+
projectId,
|
|
1354
|
+
userId,
|
|
1355
|
+
requiredSsoProviderId: providerId,
|
|
1356
|
+
}),
|
|
1357
|
+
).resolves.toBe(true);
|
|
1358
|
+
|
|
1359
|
+
expect(ssoTrustSpy).toHaveBeenCalledTimes(1);
|
|
1360
|
+
});
|
|
1361
|
+
|
|
1362
|
+
test("a project-typed token sitting in the global slot -> false, no lookups", async () => {
|
|
1363
|
+
const req: ExpressRequest = buildRequest({
|
|
1364
|
+
globalToken: mintProjectToken({
|
|
1365
|
+
userId,
|
|
1366
|
+
projectId,
|
|
1367
|
+
providerId,
|
|
1368
|
+
providerType: SsoProviderType.ProjectSSO,
|
|
1369
|
+
}),
|
|
1370
|
+
});
|
|
1371
|
+
|
|
1372
|
+
await expect(
|
|
1373
|
+
UserMiddleware.isSsoSatisfiedForProject({ req, projectId, userId }),
|
|
1374
|
+
).resolves.toBe(false);
|
|
1375
|
+
|
|
1376
|
+
expectNoDatabaseLookups();
|
|
1377
|
+
});
|
|
1378
|
+
});
|
|
1379
|
+
|
|
1380
|
+
/*
|
|
1381
|
+
* `doesSsoTokenForProjectExist` is the STATELESS half only. It is kept for the
|
|
1382
|
+
* existing suites and for callers that just want to know whether a credential
|
|
1383
|
+
* is present and well-formed - it is NOT enforcement, and this test exists so
|
|
1384
|
+
* that anyone tempted to wire a middleware back to it sees the divergence
|
|
1385
|
+
* spelled out: it answers "yes" for a token whose provider an admin revoked
|
|
1386
|
+
* an hour ago.
|
|
1387
|
+
*/
|
|
1388
|
+
describe("doesSsoTokenForProjectExist is NOT the enforcement entry point", () => {
|
|
1389
|
+
const projectId: ObjectID = ObjectID.generate();
|
|
1390
|
+
const userId: ObjectID = ObjectID.generate();
|
|
1391
|
+
const providerId: ObjectID = ObjectID.generate();
|
|
1392
|
+
|
|
1393
|
+
test("the sync check says yes where the enforcement check refuses (disabled provider)", async () => {
|
|
1394
|
+
const req: ExpressRequest = buildRequest({
|
|
1395
|
+
globalToken: mintGlobalToken({
|
|
1396
|
+
userId,
|
|
1397
|
+
providerId,
|
|
1398
|
+
providerType: SsoProviderType.GlobalSSO,
|
|
1399
|
+
}),
|
|
1400
|
+
});
|
|
1401
|
+
|
|
1402
|
+
ssoTrustSpy.mockResolvedValue(REVOKED);
|
|
1403
|
+
|
|
1404
|
+
// Stateless: the token is genuine, unexpired, and for this user.
|
|
1405
|
+
expect(
|
|
1406
|
+
UserMiddleware.doesSsoTokenForProjectExist(req, projectId, userId),
|
|
1407
|
+
).toBe(true);
|
|
1408
|
+
|
|
1409
|
+
// Stateful: the provider behind it is gone, so the request is refused.
|
|
1410
|
+
await expect(
|
|
1411
|
+
UserMiddleware.isSsoSatisfiedForProject({ req, projectId, userId }),
|
|
1412
|
+
).resolves.toBe(false);
|
|
1413
|
+
});
|
|
1414
|
+
|
|
1415
|
+
test("the sync check says yes where the enforcement check refuses (restrict-on, unattached project)", async () => {
|
|
1416
|
+
const req: ExpressRequest = buildRequest({
|
|
1417
|
+
globalToken: mintGlobalToken({
|
|
1418
|
+
userId,
|
|
1419
|
+
providerId,
|
|
1420
|
+
providerType: SsoProviderType.GlobalSSO,
|
|
1421
|
+
}),
|
|
1422
|
+
});
|
|
1423
|
+
|
|
1424
|
+
ssoTrustSpy.mockResolvedValue(TRUSTED_AND_RESTRICTED);
|
|
1425
|
+
ssoGovernsSpy.mockResolvedValue(false);
|
|
1426
|
+
|
|
1427
|
+
expect(
|
|
1428
|
+
UserMiddleware.doesSsoTokenForProjectExist(req, projectId, userId),
|
|
1429
|
+
).toBe(true);
|
|
1430
|
+
|
|
1431
|
+
await expect(
|
|
1432
|
+
UserMiddleware.isSsoSatisfiedForProject({ req, projectId, userId }),
|
|
1433
|
+
).resolves.toBe(false);
|
|
1434
|
+
});
|
|
1435
|
+
|
|
1436
|
+
test("the two agree whenever the provider is healthy - the divergence is only about revocation", async () => {
|
|
1437
|
+
const req: ExpressRequest = buildRequest({
|
|
1438
|
+
globalToken: mintGlobalToken({
|
|
1439
|
+
userId,
|
|
1440
|
+
providerId,
|
|
1441
|
+
providerType: SsoProviderType.GlobalSSO,
|
|
1442
|
+
}),
|
|
1443
|
+
});
|
|
1444
|
+
|
|
1445
|
+
ssoTrustSpy.mockResolvedValue(TRUSTED_AND_UNRESTRICTED);
|
|
1446
|
+
|
|
1447
|
+
expect(
|
|
1448
|
+
UserMiddleware.doesSsoTokenForProjectExist(req, projectId, userId),
|
|
1449
|
+
).toBe(true);
|
|
1450
|
+
|
|
1451
|
+
await expect(
|
|
1452
|
+
UserMiddleware.isSsoSatisfiedForProject({ req, projectId, userId }),
|
|
1453
|
+
).resolves.toBe(true);
|
|
1454
|
+
});
|
|
1455
|
+
});
|