@oneuptime/common 11.0.3 → 11.0.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Models/DatabaseModels/GlobalConfig.ts +19 -0
- package/Models/DatabaseModels/GlobalOidc.ts +351 -0
- package/Models/DatabaseModels/GlobalOidcProject.ts +265 -0
- package/Models/DatabaseModels/GlobalSso.ts +312 -0
- package/Models/DatabaseModels/GlobalSsoProject.ts +268 -0
- package/Models/DatabaseModels/Index.ts +8 -0
- package/Models/DatabaseModels/Project.ts +31 -0
- package/Models/DatabaseModels/StatusPage.ts +82 -0
- package/Server/API/StatusPageAPI.ts +2 -0
- package/Server/Infrastructure/GlobalCache.ts +11 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/{1781587937032-MigrationName.ts → 1781750000000-MigrationName.ts} +2 -2
- package/Server/Infrastructure/Postgres/SchemaMigrations/1782000000000-AddGlobalSsoAndOidc.ts +176 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1782100000000-AddStatusPageImageAltText.ts +25 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1782200000000-AddRequireSsoForLoginToGlobalProviders.ts +25 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1782300000000-MoveRequireSsoForLoginToGlobalConfig.ts +38 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1782310000000-MigrationName.ts +299 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1782400000000-RemoveIsTestedFromGlobalSsoAndOidc.ts +21 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +14 -2
- package/Server/Middleware/UserAuthorization.ts +113 -42
- package/Server/Services/CephClusterService.ts +22 -10
- package/Server/Services/CloudResourceService.ts +22 -10
- package/Server/Services/DockerHostService.ts +22 -10
- package/Server/Services/DockerSwarmClusterService.ts +22 -10
- package/Server/Services/GlobalConfigService.ts +50 -0
- package/Server/Services/GlobalOidcProjectService.ts +85 -0
- package/Server/Services/GlobalOidcService.ts +10 -0
- package/Server/Services/GlobalSsoProjectService.ts +85 -0
- package/Server/Services/GlobalSsoService.ts +10 -0
- package/Server/Services/HostService.ts +22 -10
- package/Server/Services/Index.ts +8 -0
- package/Server/Services/KubernetesClusterService.ts +22 -10
- package/Server/Services/PodmanHostService.ts +22 -10
- package/Server/Services/ProjectService.ts +44 -1
- package/Server/Services/ProxmoxClusterService.ts +22 -10
- package/Server/Services/RumApplicationService.ts +22 -10
- package/Server/Services/ServerlessFunctionService.ts +22 -10
- package/Server/Utils/Cookie.ts +39 -5
- package/Server/Utils/JsonWebToken.ts +7 -0
- package/Server/Utils/ValidateGlobalProviderProjectTeams.ts +119 -0
- package/Tests/Server/Infrastructure/GlobalCache.test.ts +43 -0
- package/Tests/Server/Middleware/UserAuthorization.test.ts +51 -13
- package/Tests/Server/Middleware/UserAuthorizationSSOProvider.test.ts +163 -0
- package/Tests/Server/Utils/CookieSSOToken.test.ts +130 -0
- package/Types/JsonWebTokenData.ts +3 -0
- package/Types/SSO/SsoProviderType.ts +8 -0
- package/UI/Components/Accordion/Accordion.tsx +5 -1
- package/UI/Components/CardSelect/CardSelect.tsx +6 -1
- package/UI/Components/CategoryCheckbox/Index.tsx +2 -1
- package/UI/Components/Charts/Area/AreaChart.tsx +4 -8
- package/UI/Components/Charts/Bar/BarChart.tsx +4 -8
- package/UI/Components/Charts/ChartGroup/ChartGroup.tsx +4 -8
- package/UI/Components/Charts/ChartLibrary/AreaChart/AreaChart.tsx +1 -1
- package/UI/Components/Charts/ChartLibrary/BarChart/BarChart.tsx +1 -1
- package/UI/Components/Charts/ChartLibrary/LineChart/LineChart.tsx +1 -1
- package/UI/Components/Charts/Line/LineChart.tsx +4 -8
- package/UI/Components/CodeEditor/CodeEditor.tsx +2 -0
- package/UI/Components/CollapsibleSection/CollapsibleSection.tsx +8 -1
- package/UI/Components/Dropdown/Dropdown.tsx +2 -0
- package/UI/Components/EditionLabel/EditionLabel.tsx +68 -16
- package/UI/Components/EntityDropdown/EntityDropdown.tsx +3 -0
- package/UI/Components/FilePicker/FilePicker.tsx +2 -0
- package/UI/Components/Forms/Fields/ColorPicker.tsx +2 -0
- package/UI/Components/Forms/Fields/FieldLabel.tsx +4 -0
- package/UI/Components/Forms/Fields/FormField.tsx +72 -15
- package/UI/Components/Forms/Fields/IconPicker.tsx +2 -0
- package/UI/Components/Forms/Validation.ts +107 -23
- package/UI/Components/Input/Input.tsx +4 -0
- package/UI/Components/Link/Link.tsx +23 -0
- package/UI/Components/Markdown.tsx/MarkdownConverters.ts +0 -0
- package/UI/Components/Markdown.tsx/MarkdownEditor.tsx +3 -0
- package/UI/Components/Markdown.tsx/MarkdownViewer.tsx +63 -2
- package/UI/Components/Modal/Modal.tsx +4 -1
- package/UI/Components/Radio/Radio.tsx +2 -0
- package/UI/Components/RadioButtons/GroupRadioButtons.tsx +6 -1
- package/UI/Components/Tabs/Tabs.tsx +63 -0
- package/UI/Components/TextArea/TextArea.tsx +2 -0
- package/UI/Components/TimePicker/TimePicker.tsx +2 -0
- package/UI/Components/Toggle/Toggle.tsx +2 -1
- package/UI/Components/Tooltip/Tooltip.tsx +6 -1
- package/build/dist/Models/DatabaseModels/GlobalConfig.js +20 -0
- package/build/dist/Models/DatabaseModels/GlobalConfig.js.map +1 -1
- package/build/dist/Models/DatabaseModels/GlobalOidc.js +379 -0
- package/build/dist/Models/DatabaseModels/GlobalOidc.js.map +1 -0
- package/build/dist/Models/DatabaseModels/GlobalOidcProject.js +276 -0
- package/build/dist/Models/DatabaseModels/GlobalOidcProject.js.map +1 -0
- package/build/dist/Models/DatabaseModels/GlobalSso.js +341 -0
- package/build/dist/Models/DatabaseModels/GlobalSso.js.map +1 -0
- package/build/dist/Models/DatabaseModels/GlobalSsoProject.js +279 -0
- package/build/dist/Models/DatabaseModels/GlobalSsoProject.js.map +1 -0
- package/build/dist/Models/DatabaseModels/Index.js +8 -0
- package/build/dist/Models/DatabaseModels/Index.js.map +1 -1
- package/build/dist/Models/DatabaseModels/Project.js +32 -0
- package/build/dist/Models/DatabaseModels/Project.js.map +1 -1
- package/build/dist/Models/DatabaseModels/StatusPage.js +84 -0
- package/build/dist/Models/DatabaseModels/StatusPage.js.map +1 -1
- package/build/dist/Server/API/StatusPageAPI.js +2 -0
- package/build/dist/Server/API/StatusPageAPI.js.map +1 -1
- package/build/dist/Server/Infrastructure/GlobalCache.js +13 -0
- package/build/dist/Server/Infrastructure/GlobalCache.js.map +1 -1
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/{1781587937032-MigrationName.js → 1781750000000-MigrationName.js} +3 -3
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/{1781587937032-MigrationName.js.map → 1781750000000-MigrationName.js.map} +1 -1
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1782000000000-AddGlobalSsoAndOidc.js +73 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1782000000000-AddGlobalSsoAndOidc.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1782100000000-AddStatusPageImageAltText.js +14 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1782100000000-AddStatusPageImageAltText.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1782200000000-AddRequireSsoForLoginToGlobalProviders.js +14 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1782200000000-AddRequireSsoForLoginToGlobalProviders.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1782300000000-MoveRequireSsoForLoginToGlobalConfig.js +23 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1782300000000-MoveRequireSsoForLoginToGlobalConfig.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1782310000000-MigrationName.js +106 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1782310000000-MigrationName.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1782400000000-RemoveIsTestedFromGlobalSsoAndOidc.js +14 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1782400000000-RemoveIsTestedFromGlobalSsoAndOidc.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +14 -2
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
- package/build/dist/Server/Middleware/UserAuthorization.js +77 -34
- package/build/dist/Server/Middleware/UserAuthorization.js.map +1 -1
- package/build/dist/Server/Services/CephClusterService.js +19 -2
- package/build/dist/Server/Services/CephClusterService.js.map +1 -1
- package/build/dist/Server/Services/CloudResourceService.js +19 -2
- package/build/dist/Server/Services/CloudResourceService.js.map +1 -1
- package/build/dist/Server/Services/DockerHostService.js +19 -2
- package/build/dist/Server/Services/DockerHostService.js.map +1 -1
- package/build/dist/Server/Services/DockerSwarmClusterService.js +19 -2
- package/build/dist/Server/Services/DockerSwarmClusterService.js.map +1 -1
- package/build/dist/Server/Services/GlobalConfigService.js +55 -0
- package/build/dist/Server/Services/GlobalConfigService.js.map +1 -1
- package/build/dist/Server/Services/GlobalOidcProjectService.js +80 -0
- package/build/dist/Server/Services/GlobalOidcProjectService.js.map +1 -0
- package/build/dist/Server/Services/GlobalOidcService.js +9 -0
- package/build/dist/Server/Services/GlobalOidcService.js.map +1 -0
- package/build/dist/Server/Services/GlobalSsoProjectService.js +80 -0
- package/build/dist/Server/Services/GlobalSsoProjectService.js.map +1 -0
- package/build/dist/Server/Services/GlobalSsoService.js +9 -0
- package/build/dist/Server/Services/GlobalSsoService.js.map +1 -0
- package/build/dist/Server/Services/HostService.js +19 -2
- package/build/dist/Server/Services/HostService.js.map +1 -1
- package/build/dist/Server/Services/Index.js +8 -0
- package/build/dist/Server/Services/Index.js.map +1 -1
- package/build/dist/Server/Services/KubernetesClusterService.js +19 -2
- package/build/dist/Server/Services/KubernetesClusterService.js.map +1 -1
- package/build/dist/Server/Services/PodmanHostService.js +19 -2
- package/build/dist/Server/Services/PodmanHostService.js.map +1 -1
- package/build/dist/Server/Services/ProjectService.js +36 -1
- package/build/dist/Server/Services/ProjectService.js.map +1 -1
- package/build/dist/Server/Services/ProxmoxClusterService.js +19 -2
- package/build/dist/Server/Services/ProxmoxClusterService.js.map +1 -1
- package/build/dist/Server/Services/RumApplicationService.js +19 -2
- package/build/dist/Server/Services/RumApplicationService.js.map +1 -1
- package/build/dist/Server/Services/ServerlessFunctionService.js +19 -2
- package/build/dist/Server/Services/ServerlessFunctionService.js.map +1 -1
- package/build/dist/Server/Utils/Cookie.js +32 -3
- package/build/dist/Server/Utils/Cookie.js.map +1 -1
- package/build/dist/Server/Utils/JsonWebToken.js +6 -0
- package/build/dist/Server/Utils/JsonWebToken.js.map +1 -1
- package/build/dist/Server/Utils/ValidateGlobalProviderProjectTeams.js +66 -0
- package/build/dist/Server/Utils/ValidateGlobalProviderProjectTeams.js.map +1 -0
- package/build/dist/Types/SSO/SsoProviderType.js +9 -0
- package/build/dist/Types/SSO/SsoProviderType.js.map +1 -0
- package/build/dist/UI/Components/Accordion/Accordion.js +5 -3
- package/build/dist/UI/Components/Accordion/Accordion.js.map +1 -1
- package/build/dist/UI/Components/CardSelect/CardSelect.js +1 -1
- package/build/dist/UI/Components/CardSelect/CardSelect.js.map +1 -1
- package/build/dist/UI/Components/CategoryCheckbox/Index.js +1 -1
- package/build/dist/UI/Components/CategoryCheckbox/Index.js.map +1 -1
- package/build/dist/UI/Components/Charts/Area/AreaChart.js +2 -6
- package/build/dist/UI/Components/Charts/Area/AreaChart.js.map +1 -1
- package/build/dist/UI/Components/Charts/Bar/BarChart.js +2 -6
- package/build/dist/UI/Components/Charts/Bar/BarChart.js.map +1 -1
- package/build/dist/UI/Components/Charts/ChartGroup/ChartGroup.js +8 -8
- package/build/dist/UI/Components/Charts/ChartGroup/ChartGroup.js.map +1 -1
- package/build/dist/UI/Components/Charts/ChartLibrary/AreaChart/AreaChart.js +1 -1
- package/build/dist/UI/Components/Charts/ChartLibrary/AreaChart/AreaChart.js.map +1 -1
- package/build/dist/UI/Components/Charts/ChartLibrary/BarChart/BarChart.js +1 -1
- package/build/dist/UI/Components/Charts/ChartLibrary/BarChart/BarChart.js.map +1 -1
- package/build/dist/UI/Components/Charts/ChartLibrary/LineChart/LineChart.js +1 -1
- package/build/dist/UI/Components/Charts/ChartLibrary/LineChart/LineChart.js.map +1 -1
- package/build/dist/UI/Components/Charts/Line/LineChart.js +2 -6
- package/build/dist/UI/Components/Charts/Line/LineChart.js.map +1 -1
- package/build/dist/UI/Components/CodeEditor/CodeEditor.js +1 -1
- package/build/dist/UI/Components/CodeEditor/CodeEditor.js.map +1 -1
- package/build/dist/UI/Components/CollapsibleSection/CollapsibleSection.js +4 -2
- package/build/dist/UI/Components/CollapsibleSection/CollapsibleSection.js.map +1 -1
- package/build/dist/UI/Components/Dropdown/Dropdown.js +1 -1
- package/build/dist/UI/Components/Dropdown/Dropdown.js.map +1 -1
- package/build/dist/UI/Components/EditionLabel/EditionLabel.js +31 -5
- package/build/dist/UI/Components/EditionLabel/EditionLabel.js.map +1 -1
- package/build/dist/UI/Components/EntityDropdown/EntityDropdown.js +2 -2
- package/build/dist/UI/Components/EntityDropdown/EntityDropdown.js.map +1 -1
- package/build/dist/UI/Components/FilePicker/FilePicker.js +1 -1
- package/build/dist/UI/Components/FilePicker/FilePicker.js.map +1 -1
- package/build/dist/UI/Components/Forms/Fields/ColorPicker.js +1 -1
- package/build/dist/UI/Components/Forms/Fields/ColorPicker.js.map +1 -1
- package/build/dist/UI/Components/Forms/Fields/FieldLabel.js +1 -1
- package/build/dist/UI/Components/Forms/Fields/FieldLabel.js.map +1 -1
- package/build/dist/UI/Components/Forms/Fields/FormField.js +58 -22
- package/build/dist/UI/Components/Forms/Fields/FormField.js.map +1 -1
- package/build/dist/UI/Components/Forms/Fields/IconPicker.js +1 -1
- package/build/dist/UI/Components/Forms/Fields/IconPicker.js.map +1 -1
- package/build/dist/UI/Components/Forms/Validation.js +64 -15
- package/build/dist/UI/Components/Forms/Validation.js.map +1 -1
- package/build/dist/UI/Components/Input/Input.js +1 -1
- package/build/dist/UI/Components/Input/Input.js.map +1 -1
- package/build/dist/UI/Components/Link/Link.js +22 -1
- package/build/dist/UI/Components/Link/Link.js.map +1 -1
- package/build/dist/UI/Components/Markdown.tsx/MarkdownConverters.js +0 -0
- package/build/dist/UI/Components/Markdown.tsx/MarkdownConverters.js.map +1 -1
- package/build/dist/UI/Components/Markdown.tsx/MarkdownEditor.js +2 -2
- package/build/dist/UI/Components/Markdown.tsx/MarkdownEditor.js.map +1 -1
- package/build/dist/UI/Components/Markdown.tsx/MarkdownViewer.js +46 -2
- package/build/dist/UI/Components/Markdown.tsx/MarkdownViewer.js.map +1 -1
- package/build/dist/UI/Components/Modal/Modal.js +1 -1
- package/build/dist/UI/Components/Modal/Modal.js.map +1 -1
- package/build/dist/UI/Components/Radio/Radio.js +1 -1
- package/build/dist/UI/Components/Radio/Radio.js.map +1 -1
- package/build/dist/UI/Components/RadioButtons/GroupRadioButtons.js +1 -1
- package/build/dist/UI/Components/RadioButtons/GroupRadioButtons.js.map +1 -1
- package/build/dist/UI/Components/Tabs/Tabs.js +50 -1
- package/build/dist/UI/Components/Tabs/Tabs.js.map +1 -1
- package/build/dist/UI/Components/TextArea/TextArea.js +1 -1
- package/build/dist/UI/Components/TextArea/TextArea.js.map +1 -1
- package/build/dist/UI/Components/TimePicker/TimePicker.js +1 -1
- package/build/dist/UI/Components/TimePicker/TimePicker.js.map +1 -1
- package/build/dist/UI/Components/Toggle/Toggle.js +1 -1
- package/build/dist/UI/Components/Toggle/Toggle.js.map +1 -1
- package/build/dist/UI/Components/Tooltip/Tooltip.js +6 -1
- package/build/dist/UI/Components/Tooltip/Tooltip.js.map +1 -1
- package/package.json +1 -1
|
@@ -170,21 +170,33 @@ export class Service extends DatabaseService<Model> {
|
|
|
170
170
|
)
|
|
171
171
|
.digest("hex");
|
|
172
172
|
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
cacheKey
|
|
176
|
-
|
|
173
|
+
let cached: string | null = null;
|
|
174
|
+
try {
|
|
175
|
+
cached = await GlobalCache.getString(LAST_SEEN_CACHE_NAMESPACE, cacheKey);
|
|
176
|
+
} catch {
|
|
177
|
+
/*
|
|
178
|
+
* Cache unavailable — fail open and refresh lastSeenAt anyway. A
|
|
179
|
+
* cache error must never skip the DB write below, otherwise the
|
|
180
|
+
* resource is wrongly marked "disconnected" while telemetry is
|
|
181
|
+
* still flowing. Mirrors shouldRunMaintenance's fail-open stance.
|
|
182
|
+
*/
|
|
183
|
+
cached = null;
|
|
184
|
+
}
|
|
177
185
|
|
|
178
186
|
if (cached === extrasFingerprint) {
|
|
179
187
|
return; // same data was written recently
|
|
180
188
|
}
|
|
181
189
|
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
190
|
+
try {
|
|
191
|
+
await GlobalCache.setString(
|
|
192
|
+
LAST_SEEN_CACHE_NAMESPACE,
|
|
193
|
+
cacheKey,
|
|
194
|
+
extrasFingerprint,
|
|
195
|
+
{ expiresInSeconds: LAST_SEEN_THROTTLE_SECONDS },
|
|
196
|
+
);
|
|
197
|
+
} catch {
|
|
198
|
+
// Best-effort throttle write; proceed with the DB update regardless.
|
|
199
|
+
}
|
|
188
200
|
|
|
189
201
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
190
202
|
const data: any = {
|
package/Server/Utils/Cookie.ts
CHANGED
|
@@ -8,6 +8,7 @@ import StatusPagePrivateUser from "../../Models/DatabaseModels/StatusPagePrivate
|
|
|
8
8
|
import OneUptimeDate from "../../Types/Date";
|
|
9
9
|
import PositiveNumber from "../../Types/PositiveNumber";
|
|
10
10
|
import CookieName from "../../Types/CookieName";
|
|
11
|
+
import SsoProviderType from "../../Types/SSO/SsoProviderType";
|
|
11
12
|
import {
|
|
12
13
|
MASTER_PASSWORD_COOKIE_IDENTIFIER,
|
|
13
14
|
MASTER_PASSWORD_COOKIE_MAX_AGE_IN_DAYS,
|
|
@@ -38,15 +39,24 @@ export default class CookieUtil {
|
|
|
38
39
|
return cookies;
|
|
39
40
|
}
|
|
40
41
|
|
|
42
|
+
/*
|
|
43
|
+
* Builds a per-project SSO token. The optional `ssoProviderId` /
|
|
44
|
+
* `ssoProviderType` discriminator records WHICH identity provider issued the
|
|
45
|
+
* token, so a project that enforces SSO can require a specific provider
|
|
46
|
+
* (e.g. its own Project SSO, or an instance-wide Global SSO). Used by both
|
|
47
|
+
* the cookie flow (web) and the deep-link flow (mobile) so the token shape
|
|
48
|
+
* stays identical.
|
|
49
|
+
*/
|
|
41
50
|
@CaptureSpan()
|
|
42
|
-
public static
|
|
51
|
+
public static getSSOToken(data: {
|
|
43
52
|
user: User;
|
|
44
53
|
projectId: ObjectID;
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
54
|
+
ssoProviderId?: ObjectID | undefined;
|
|
55
|
+
ssoProviderType?: SsoProviderType | undefined;
|
|
56
|
+
}): string {
|
|
57
|
+
const { user, projectId } = data;
|
|
48
58
|
|
|
49
|
-
|
|
59
|
+
return JSONWebToken.sign({
|
|
50
60
|
data: {
|
|
51
61
|
userId: user.id!,
|
|
52
62
|
projectId: projectId,
|
|
@@ -54,9 +64,33 @@ export default class CookieUtil {
|
|
|
54
64
|
email: user.email,
|
|
55
65
|
isMasterAdmin: false,
|
|
56
66
|
isGeneralLogin: false,
|
|
67
|
+
ssoProviderId: data.ssoProviderId
|
|
68
|
+
? data.ssoProviderId.toString()
|
|
69
|
+
: undefined,
|
|
70
|
+
ssoProviderType: data.ssoProviderType
|
|
71
|
+
? data.ssoProviderType.toString()
|
|
72
|
+
: undefined,
|
|
57
73
|
},
|
|
58
74
|
expiresInSeconds: OneUptimeDate.getSecondsInDays(new PositiveNumber(30)),
|
|
59
75
|
});
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
@CaptureSpan()
|
|
79
|
+
public static setSSOCookie(data: {
|
|
80
|
+
user: User;
|
|
81
|
+
projectId: ObjectID;
|
|
82
|
+
expressResponse: ExpressResponse;
|
|
83
|
+
ssoProviderId?: ObjectID | undefined;
|
|
84
|
+
ssoProviderType?: SsoProviderType | undefined;
|
|
85
|
+
}): void {
|
|
86
|
+
const { projectId, expressResponse: res } = data;
|
|
87
|
+
|
|
88
|
+
const ssoToken: string = CookieUtil.getSSOToken({
|
|
89
|
+
user: data.user,
|
|
90
|
+
projectId: projectId,
|
|
91
|
+
ssoProviderId: data.ssoProviderId,
|
|
92
|
+
ssoProviderType: data.ssoProviderType,
|
|
93
|
+
});
|
|
60
94
|
|
|
61
95
|
CookieUtil.setCookie(res, CookieUtil.getUserSSOKey(projectId), ssoToken, {
|
|
62
96
|
maxAge: OneUptimeDate.getMillisecondsInDays(new PositiveNumber(30)),
|
|
@@ -6,6 +6,7 @@ import JSONFunctions from "../../Types/JSONFunctions";
|
|
|
6
6
|
import JSONWebTokenData from "../../Types/JsonWebTokenData";
|
|
7
7
|
import Name from "../../Types/Name";
|
|
8
8
|
import ObjectID from "../../Types/ObjectID";
|
|
9
|
+
import SsoProviderType from "../../Types/SSO/SsoProviderType";
|
|
9
10
|
import Timezone from "../../Types/Timezone";
|
|
10
11
|
import StatusPagePrivateUser from "../../Models/DatabaseModels/StatusPagePrivateUser";
|
|
11
12
|
import User from "../../Models/DatabaseModels/User";
|
|
@@ -139,6 +140,12 @@ class JSONWebToken {
|
|
|
139
140
|
sessionId: decoded["sessionId"]
|
|
140
141
|
? new ObjectID(decoded["sessionId"] as string)
|
|
141
142
|
: undefined,
|
|
143
|
+
ssoProviderId: decoded["ssoProviderId"]
|
|
144
|
+
? new ObjectID(decoded["ssoProviderId"] as string)
|
|
145
|
+
: undefined,
|
|
146
|
+
ssoProviderType: decoded["ssoProviderType"]
|
|
147
|
+
? (decoded["ssoProviderType"] as SsoProviderType)
|
|
148
|
+
: undefined,
|
|
142
149
|
};
|
|
143
150
|
} catch (e) {
|
|
144
151
|
logger.error(e);
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { LIMIT_PER_PROJECT } from "../../Types/Database/LimitMax";
|
|
2
|
+
import BadDataException from "../../Types/Exception/BadDataException";
|
|
3
|
+
import ObjectID from "../../Types/ObjectID";
|
|
4
|
+
import Project from "../../Models/DatabaseModels/Project";
|
|
5
|
+
import Team from "../../Models/DatabaseModels/Team";
|
|
6
|
+
import QueryHelper from "../Types/Database/QueryHelper";
|
|
7
|
+
import TeamService from "../Services/TeamService";
|
|
8
|
+
|
|
9
|
+
/*
|
|
10
|
+
* The Admin Dashboard attach form submits the chosen project via the `project`
|
|
11
|
+
* relation, not the `projectId` FK: the entity dropdown sets the related
|
|
12
|
+
* Project (as a model with only `_id`), so `createBy.data.projectId` is
|
|
13
|
+
* undefined at create-hook time. Resolve the FK from either shape so the hook
|
|
14
|
+
* can populate the NOT NULL `projectId` column and validate teams against it.
|
|
15
|
+
*/
|
|
16
|
+
export const resolveAttachmentProjectId: (data: {
|
|
17
|
+
projectId?: ObjectID | undefined;
|
|
18
|
+
project?: Project | undefined;
|
|
19
|
+
}) => ObjectID | undefined = (data: {
|
|
20
|
+
projectId?: ObjectID | undefined;
|
|
21
|
+
project?: Project | undefined;
|
|
22
|
+
}): ObjectID | undefined => {
|
|
23
|
+
if (data.projectId) {
|
|
24
|
+
return data.projectId;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const project: (Project & { _id?: string }) | undefined = data.project as
|
|
28
|
+
| (Project & { _id?: string })
|
|
29
|
+
| undefined;
|
|
30
|
+
|
|
31
|
+
if (!project) {
|
|
32
|
+
return undefined;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const idString: string | undefined =
|
|
36
|
+
project._id?.toString() || (project.id ? project.id.toString() : undefined);
|
|
37
|
+
|
|
38
|
+
return idString ? new ObjectID(idString) : undefined;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
/*
|
|
42
|
+
* Guards a Global SSO / Global OIDC project-attachment: every default team
|
|
43
|
+
* selected for the attachment MUST belong to the same project the attachment
|
|
44
|
+
* targets.
|
|
45
|
+
*
|
|
46
|
+
* Without this guard, an admin (or a direct API call) could attach a team that
|
|
47
|
+
* lives in project B to an attachment that targets project A. The SSO/OIDC
|
|
48
|
+
* login fan-out (see App/FeatureSet/Identity/API/GlobalSSO.ts) provisions a
|
|
49
|
+
* TeamMember with `projectId = attachment.projectId` but `teamId = team.id`,
|
|
50
|
+
* and because that path runs with `ignoreHooks: true` no service-level
|
|
51
|
+
* validation would catch the mismatch — producing a corrupt, cross-project
|
|
52
|
+
* membership row. This is the server-side backstop for the project-scoped team
|
|
53
|
+
* picker in the Admin Dashboard.
|
|
54
|
+
*/
|
|
55
|
+
type ValidateGlobalProviderProjectTeamsFunction = (data: {
|
|
56
|
+
teams: Array<Team> | undefined;
|
|
57
|
+
projectId: ObjectID | undefined;
|
|
58
|
+
}) => Promise<void>;
|
|
59
|
+
|
|
60
|
+
const validateGlobalProviderProjectTeams: ValidateGlobalProviderProjectTeamsFunction =
|
|
61
|
+
async (data: {
|
|
62
|
+
teams: Array<Team> | undefined;
|
|
63
|
+
projectId: ObjectID | undefined;
|
|
64
|
+
}): Promise<void> => {
|
|
65
|
+
const teams: Array<Team> | undefined = data.teams;
|
|
66
|
+
|
|
67
|
+
if (!teams || teams.length === 0) {
|
|
68
|
+
// No default teams selected: nothing to validate.
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (!data.projectId) {
|
|
73
|
+
throw new BadDataException(
|
|
74
|
+
"A project must be selected before choosing default teams.",
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const projectId: string = data.projectId.toString();
|
|
79
|
+
|
|
80
|
+
const teamIds: Array<string> = teams
|
|
81
|
+
.map((team: Team) => {
|
|
82
|
+
return (
|
|
83
|
+
team.id?.toString() ||
|
|
84
|
+
(team as { _id?: string })._id?.toString() ||
|
|
85
|
+
undefined
|
|
86
|
+
);
|
|
87
|
+
})
|
|
88
|
+
.filter((id: string | undefined): id is string => {
|
|
89
|
+
return Boolean(id);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
if (teamIds.length === 0) {
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const foundTeams: Array<Team> = await TeamService.findBy({
|
|
97
|
+
query: { _id: QueryHelper.any(teamIds) },
|
|
98
|
+
select: { _id: true, projectId: true },
|
|
99
|
+
limit: LIMIT_PER_PROJECT,
|
|
100
|
+
skip: 0,
|
|
101
|
+
props: { isRoot: true },
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
if (foundTeams.length !== teamIds.length) {
|
|
105
|
+
throw new BadDataException(
|
|
106
|
+
"One or more selected teams could not be found.",
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
for (const team of foundTeams) {
|
|
111
|
+
if (team.projectId?.toString() !== projectId) {
|
|
112
|
+
throw new BadDataException(
|
|
113
|
+
"All selected teams must belong to the project this provider is attached to.",
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
export default validateGlobalProviderProjectTeams;
|
|
@@ -17,6 +17,7 @@ type MockClient = {
|
|
|
17
17
|
set: jest.Mock;
|
|
18
18
|
expire: jest.Mock;
|
|
19
19
|
get: jest.Mock;
|
|
20
|
+
del: jest.Mock;
|
|
20
21
|
};
|
|
21
22
|
|
|
22
23
|
describe("GlobalCache.setString", () => {
|
|
@@ -27,6 +28,7 @@ describe("GlobalCache.setString", () => {
|
|
|
27
28
|
set: jest.fn().mockResolvedValue("OK"),
|
|
28
29
|
expire: jest.fn().mockResolvedValue(1),
|
|
29
30
|
get: jest.fn(),
|
|
31
|
+
del: jest.fn().mockResolvedValue(1),
|
|
30
32
|
};
|
|
31
33
|
(Redis.getClient as jest.Mock).mockReturnValue(client);
|
|
32
34
|
(Redis.isConnected as jest.Mock).mockReturnValue(true);
|
|
@@ -98,3 +100,44 @@ describe("GlobalCache.setString", () => {
|
|
|
98
100
|
expect(client.expire).not.toHaveBeenCalled();
|
|
99
101
|
});
|
|
100
102
|
});
|
|
103
|
+
|
|
104
|
+
describe("GlobalCache.deleteKey", () => {
|
|
105
|
+
let client: MockClient;
|
|
106
|
+
|
|
107
|
+
beforeEach(() => {
|
|
108
|
+
client = {
|
|
109
|
+
set: jest.fn().mockResolvedValue("OK"),
|
|
110
|
+
expire: jest.fn().mockResolvedValue(1),
|
|
111
|
+
get: jest.fn(),
|
|
112
|
+
del: jest.fn().mockResolvedValue(1),
|
|
113
|
+
};
|
|
114
|
+
(Redis.getClient as jest.Mock).mockReturnValue(client);
|
|
115
|
+
(Redis.isConnected as jest.Mock).mockReturnValue(true);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
afterEach(() => {
|
|
119
|
+
jest.clearAllMocks();
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
/*
|
|
123
|
+
* deleteKey backs clearMaintenanceFence (OtelIngestBaseService): when
|
|
124
|
+
* the fenced maintenance work (updateLastSeen) fails, the fence is
|
|
125
|
+
* released so the next ingest batch retries instead of leaving the
|
|
126
|
+
* resource stranded as "disconnected" for the whole TTL window.
|
|
127
|
+
*/
|
|
128
|
+
test("deletes the namespaced key", async () => {
|
|
129
|
+
await GlobalCache.deleteKey("ns", "key");
|
|
130
|
+
|
|
131
|
+
expect(client.del).toHaveBeenCalledTimes(1);
|
|
132
|
+
expect(client.del).toHaveBeenCalledWith("ns-key");
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
test("throws when the cache is not connected", async () => {
|
|
136
|
+
(Redis.isConnected as jest.Mock).mockReturnValue(false);
|
|
137
|
+
|
|
138
|
+
await expect(GlobalCache.deleteKey("ns", "key")).rejects.toThrow(
|
|
139
|
+
DatabaseNotConnectedException,
|
|
140
|
+
);
|
|
141
|
+
expect(client.del).not.toHaveBeenCalled();
|
|
142
|
+
});
|
|
143
|
+
});
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import ProjectMiddleware from "../../../Server/Middleware/ProjectAuthorization";
|
|
2
2
|
import UserMiddleware from "../../../Server/Middleware/UserAuthorization";
|
|
3
3
|
import AccessTokenService from "../../../Server/Services/AccessTokenService";
|
|
4
|
+
import GlobalConfigService from "../../../Server/Services/GlobalConfigService";
|
|
4
5
|
import ProjectService from "../../../Server/Services/ProjectService";
|
|
5
6
|
import TeamMemberService from "../../../Server/Services/TeamMemberService";
|
|
6
7
|
import UserService from "../../../Server/Services/UserService";
|
|
@@ -26,7 +27,6 @@ import {
|
|
|
26
27
|
UserGlobalAccessPermission,
|
|
27
28
|
UserTenantAccessPermission,
|
|
28
29
|
} from "../../../Types/Permission";
|
|
29
|
-
import Project from "../../../Models/DatabaseModels/Project";
|
|
30
30
|
import {
|
|
31
31
|
describe,
|
|
32
32
|
expect,
|
|
@@ -44,6 +44,7 @@ jest.mock("../../../Server/Middleware/ProjectAuthorization");
|
|
|
44
44
|
jest.mock("../../../Server/Utils/JsonWebToken");
|
|
45
45
|
jest.mock("../../../Server/Services/UserService");
|
|
46
46
|
jest.mock("../../../Server/Services/AccessTokenService");
|
|
47
|
+
jest.mock("../../../Server/Services/GlobalConfigService");
|
|
47
48
|
jest.mock("../../../Server/Utils/Response");
|
|
48
49
|
jest.mock("../../../Server/Services/ProjectService");
|
|
49
50
|
jest.mock("../../../Server/Services/TeamMemberService");
|
|
@@ -56,7 +57,6 @@ describe("UserMiddleware", () => {
|
|
|
56
57
|
const mockedAccessToken: string = ObjectID.generate().toString();
|
|
57
58
|
const projectId: ObjectID = ObjectID.generate();
|
|
58
59
|
const userId: ObjectID = ObjectID.generate();
|
|
59
|
-
const mockedProject: Project = { _id: projectId.toString() } as Project;
|
|
60
60
|
|
|
61
61
|
beforeEach(() => {
|
|
62
62
|
jest.clearAllMocks();
|
|
@@ -590,15 +590,32 @@ describe("UserMiddleware", () => {
|
|
|
590
590
|
ProjectService,
|
|
591
591
|
"getRequireSsoForLogin",
|
|
592
592
|
);
|
|
593
|
+
const spyGetRequireSsoWithSsoProviderId: jest.SpyInstance = getJestSpyOn(
|
|
594
|
+
ProjectService,
|
|
595
|
+
"getRequireSsoWithSsoProviderId",
|
|
596
|
+
);
|
|
593
597
|
const spyDoesSsoTokenForProjectExist: jest.SpyInstance = getJestSpyOn(
|
|
594
598
|
UserMiddleware,
|
|
595
599
|
"doesSsoTokenForProjectExist",
|
|
596
600
|
);
|
|
601
|
+
const spyGetGlobalRequireSsoForLogin: jest.SpyInstance = getJestSpyOn(
|
|
602
|
+
GlobalConfigService,
|
|
603
|
+
"getRequireSsoForLogin",
|
|
604
|
+
);
|
|
597
605
|
|
|
598
606
|
afterEach(() => {
|
|
599
607
|
jest.clearAllMocks();
|
|
600
608
|
});
|
|
601
609
|
|
|
610
|
+
/*
|
|
611
|
+
* By default no project requires a specific SSO provider (discriminator),
|
|
612
|
+
* and the instance-wide "Require SSO for Login" flag is off.
|
|
613
|
+
*/
|
|
614
|
+
beforeEach(() => {
|
|
615
|
+
spyGetRequireSsoWithSsoProviderId.mockResolvedValue(null);
|
|
616
|
+
spyGetGlobalRequireSsoForLogin.mockResolvedValue(false);
|
|
617
|
+
});
|
|
618
|
+
|
|
602
619
|
test("should throw 'Invalid tenantId' error, when project is not found for the tenantId", async () => {
|
|
603
620
|
spyGetRequireSsoForLogin.mockRejectedValueOnce(
|
|
604
621
|
new BadDataException("Project not found"),
|
|
@@ -630,6 +647,7 @@ describe("UserMiddleware", () => {
|
|
|
630
647
|
req,
|
|
631
648
|
projectId,
|
|
632
649
|
userId,
|
|
650
|
+
undefined,
|
|
633
651
|
);
|
|
634
652
|
});
|
|
635
653
|
|
|
@@ -688,16 +706,37 @@ describe("UserMiddleware", () => {
|
|
|
688
706
|
projectId,
|
|
689
707
|
} as UserTenantAccessPermission;
|
|
690
708
|
|
|
691
|
-
const
|
|
709
|
+
const spyGetProjectRequireSsoForLogin: jest.SpyInstance = getJestSpyOn(
|
|
710
|
+
ProjectService,
|
|
711
|
+
"getRequireSsoForLogin",
|
|
712
|
+
);
|
|
713
|
+
const spyGetRequireSsoWithSsoProviderId: jest.SpyInstance = getJestSpyOn(
|
|
714
|
+
ProjectService,
|
|
715
|
+
"getRequireSsoWithSsoProviderId",
|
|
716
|
+
);
|
|
692
717
|
const spyDoesSsoTokenForProjectExist: jest.SpyInstance = getJestSpyOn(
|
|
693
718
|
UserMiddleware,
|
|
694
719
|
"doesSsoTokenForProjectExist",
|
|
695
720
|
);
|
|
721
|
+
const spyGetGlobalRequireSsoForLogin: jest.SpyInstance = getJestSpyOn(
|
|
722
|
+
GlobalConfigService,
|
|
723
|
+
"getRequireSsoForLogin",
|
|
724
|
+
);
|
|
696
725
|
|
|
697
726
|
afterEach(() => {
|
|
698
727
|
jest.clearAllMocks();
|
|
699
728
|
});
|
|
700
729
|
|
|
730
|
+
/*
|
|
731
|
+
* By default neither a project's own nor the instance-wide "Require SSO for
|
|
732
|
+
* Login" flag is on, and no project requires a specific SSO provider.
|
|
733
|
+
*/
|
|
734
|
+
beforeEach(() => {
|
|
735
|
+
spyGetProjectRequireSsoForLogin.mockResolvedValue(false);
|
|
736
|
+
spyGetRequireSsoWithSsoProviderId.mockResolvedValue(null);
|
|
737
|
+
spyGetGlobalRequireSsoForLogin.mockResolvedValue(false);
|
|
738
|
+
});
|
|
739
|
+
|
|
701
740
|
test("should return null, when projectIds length is zero", async () => {
|
|
702
741
|
const result: Dictionary<UserTenantAccessPermission> | null =
|
|
703
742
|
await UserMiddleware.getUserTenantAccessPermissionForMultiTenant(
|
|
@@ -707,14 +746,12 @@ describe("UserMiddleware", () => {
|
|
|
707
746
|
);
|
|
708
747
|
|
|
709
748
|
expect(result).toBeNull();
|
|
710
|
-
expect(
|
|
749
|
+
expect(spyGetProjectRequireSsoForLogin).not.toBeCalled();
|
|
711
750
|
});
|
|
712
751
|
|
|
713
752
|
test("should return default tenant access permission, when project for a projectId is found, sso is required for login, but sso token does not exist for that projectId", async () => {
|
|
714
753
|
spyDoesSsoTokenForProjectExist.mockReturnValueOnce(false);
|
|
715
|
-
|
|
716
|
-
{ ...mockedProject, requireSsoForLogin: true },
|
|
717
|
-
]);
|
|
754
|
+
spyGetProjectRequireSsoForLogin.mockResolvedValueOnce(true);
|
|
718
755
|
|
|
719
756
|
const spyGetDefaultUserTenantAccessPermission: jest.SpyInstance =
|
|
720
757
|
getJestSpyOn(
|
|
@@ -736,6 +773,7 @@ describe("UserMiddleware", () => {
|
|
|
736
773
|
req,
|
|
737
774
|
projectId,
|
|
738
775
|
userId,
|
|
776
|
+
undefined,
|
|
739
777
|
);
|
|
740
778
|
expect(spyGetDefaultUserTenantAccessPermission).toHaveBeenCalledWith(
|
|
741
779
|
projectId,
|
|
@@ -743,8 +781,6 @@ describe("UserMiddleware", () => {
|
|
|
743
781
|
});
|
|
744
782
|
|
|
745
783
|
test("should return user tenant access permission, when project for a projectId is found, sso is not required for login and project level permission exist for the projectId", async () => {
|
|
746
|
-
spyFindBy.mockResolvedValueOnce([mockedProject]);
|
|
747
|
-
|
|
748
784
|
const spyGetUserTenantAccessPermission: jest.SpyInstance = getJestSpyOn(
|
|
749
785
|
AccessTokenService,
|
|
750
786
|
"getUserTenantAccessPermission",
|
|
@@ -768,8 +804,6 @@ describe("UserMiddleware", () => {
|
|
|
768
804
|
});
|
|
769
805
|
|
|
770
806
|
test("should return null, when project for a projectId is found, sso is not required for login but project level permission does not exist for the projectId", async () => {
|
|
771
|
-
spyFindBy.mockResolvedValueOnce([mockedProject]);
|
|
772
|
-
|
|
773
807
|
const spyGetUserTenantAccessPermission: jest.SpyInstance = getJestSpyOn(
|
|
774
808
|
AccessTokenService,
|
|
775
809
|
"getUserTenantAccessPermission",
|
|
@@ -790,7 +824,9 @@ describe("UserMiddleware", () => {
|
|
|
790
824
|
});
|
|
791
825
|
|
|
792
826
|
test("should return user tenant access permission, when project for a projectId is not found, but project level permission exist for the projectId", async () => {
|
|
793
|
-
|
|
827
|
+
spyGetProjectRequireSsoForLogin.mockRejectedValueOnce(
|
|
828
|
+
new BadDataException("Project not found"),
|
|
829
|
+
);
|
|
794
830
|
|
|
795
831
|
getJestSpyOn(
|
|
796
832
|
AccessTokenService,
|
|
@@ -810,7 +846,9 @@ describe("UserMiddleware", () => {
|
|
|
810
846
|
});
|
|
811
847
|
|
|
812
848
|
test("should return null, when project for a projectId is not found, and project level permission does not exist for the projectId", async () => {
|
|
813
|
-
|
|
849
|
+
spyGetProjectRequireSsoForLogin.mockRejectedValueOnce(
|
|
850
|
+
new BadDataException("Project not found"),
|
|
851
|
+
);
|
|
814
852
|
|
|
815
853
|
const spyGetUserTenantAccessPermission: jest.SpyInstance = getJestSpyOn(
|
|
816
854
|
AccessTokenService,
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import UserMiddleware from "../../../Server/Middleware/UserAuthorization";
|
|
2
|
+
import CookieUtil from "../../../Server/Utils/Cookie";
|
|
3
|
+
import { ExpressRequest } from "../../../Server/Utils/Express";
|
|
4
|
+
import Email from "../../../Types/Email";
|
|
5
|
+
import ObjectID from "../../../Types/ObjectID";
|
|
6
|
+
import SsoProviderType from "../../../Types/SSO/SsoProviderType";
|
|
7
|
+
import User from "../../../Models/DatabaseModels/User";
|
|
8
|
+
import { describe, expect, test, jest } from "@jest/globals";
|
|
9
|
+
|
|
10
|
+
jest.mock("../../../Server/Utils/Logger");
|
|
11
|
+
|
|
12
|
+
/*
|
|
13
|
+
* Tests for UserMiddleware.doesSsoTokenForProjectExist's `requiredSsoProviderId`
|
|
14
|
+
* param. When provided, an SSO token only satisfies the project if it ALSO
|
|
15
|
+
* carries a matching `ssoProviderId` discriminator. Legacy tokens (no
|
|
16
|
+
* discriminator) must pass when no provider is required but fail a
|
|
17
|
+
* specific-provider requirement.
|
|
18
|
+
*
|
|
19
|
+
* These use REAL tokens minted by CookieUtil.getSSOToken and the REAL
|
|
20
|
+
* JSONWebToken.decode() path (no mocking) so they verify the full production
|
|
21
|
+
* wiring end to end — including that decode() surfaces ssoProviderId.
|
|
22
|
+
*/
|
|
23
|
+
describe("UserMiddleware.doesSsoTokenForProjectExist - requiredSsoProviderId", () => {
|
|
24
|
+
const projectId: ObjectID = ObjectID.generate();
|
|
25
|
+
const userId: ObjectID = ObjectID.generate();
|
|
26
|
+
const ssoProviderId: ObjectID = ObjectID.generate();
|
|
27
|
+
const otherProviderId: ObjectID = ObjectID.generate();
|
|
28
|
+
|
|
29
|
+
const buildUser: () => User = (): User => {
|
|
30
|
+
const u: User = new User();
|
|
31
|
+
u.id = userId;
|
|
32
|
+
u.email = new Email("sso-user@oneuptime.com");
|
|
33
|
+
return u;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
// Build a request whose cookies carry a real `sso-<projectId>` token.
|
|
37
|
+
const buildRequestWithSsoToken: (data: {
|
|
38
|
+
tokenProjectId: ObjectID;
|
|
39
|
+
discriminatorProviderId?: ObjectID | undefined;
|
|
40
|
+
}) => ExpressRequest = (data: {
|
|
41
|
+
tokenProjectId: ObjectID;
|
|
42
|
+
discriminatorProviderId?: ObjectID | undefined;
|
|
43
|
+
}): ExpressRequest => {
|
|
44
|
+
const token: string = CookieUtil.getSSOToken({
|
|
45
|
+
user: buildUser(),
|
|
46
|
+
projectId: data.tokenProjectId,
|
|
47
|
+
ssoProviderId: data.discriminatorProviderId,
|
|
48
|
+
ssoProviderType: data.discriminatorProviderId
|
|
49
|
+
? SsoProviderType.GlobalSSO
|
|
50
|
+
: undefined,
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
return {
|
|
54
|
+
cookies: {
|
|
55
|
+
[CookieUtil.getUserSSOKey(data.tokenProjectId)]: token,
|
|
56
|
+
},
|
|
57
|
+
headers: {},
|
|
58
|
+
} as unknown as ExpressRequest;
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
test("matching project+user, NO requiredProviderId -> true", () => {
|
|
62
|
+
const req: ExpressRequest = buildRequestWithSsoToken({
|
|
63
|
+
tokenProjectId: projectId,
|
|
64
|
+
discriminatorProviderId: ssoProviderId,
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
expect(
|
|
68
|
+
UserMiddleware.doesSsoTokenForProjectExist(req, projectId, userId),
|
|
69
|
+
).toBe(true);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test("matching project+user, requiredProviderId EQUALS token's ssoProviderId -> true", () => {
|
|
73
|
+
const req: ExpressRequest = buildRequestWithSsoToken({
|
|
74
|
+
tokenProjectId: projectId,
|
|
75
|
+
discriminatorProviderId: ssoProviderId,
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
expect(
|
|
79
|
+
UserMiddleware.doesSsoTokenForProjectExist(
|
|
80
|
+
req,
|
|
81
|
+
projectId,
|
|
82
|
+
userId,
|
|
83
|
+
ssoProviderId,
|
|
84
|
+
),
|
|
85
|
+
).toBe(true);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
test("matching project+user, requiredProviderId DIFFERENT from token's ssoProviderId -> false", () => {
|
|
89
|
+
const req: ExpressRequest = buildRequestWithSsoToken({
|
|
90
|
+
tokenProjectId: projectId,
|
|
91
|
+
discriminatorProviderId: ssoProviderId,
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
expect(
|
|
95
|
+
UserMiddleware.doesSsoTokenForProjectExist(
|
|
96
|
+
req,
|
|
97
|
+
projectId,
|
|
98
|
+
userId,
|
|
99
|
+
otherProviderId,
|
|
100
|
+
),
|
|
101
|
+
).toBe(false);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
test("legacy token (NO discriminator) + requiredProviderId given -> false", () => {
|
|
105
|
+
const req: ExpressRequest = buildRequestWithSsoToken({
|
|
106
|
+
tokenProjectId: projectId,
|
|
107
|
+
// no discriminatorProviderId -> legacy token shape
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
expect(
|
|
111
|
+
UserMiddleware.doesSsoTokenForProjectExist(
|
|
112
|
+
req,
|
|
113
|
+
projectId,
|
|
114
|
+
userId,
|
|
115
|
+
ssoProviderId,
|
|
116
|
+
),
|
|
117
|
+
).toBe(false);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
test("legacy token (NO discriminator) + NO requiredProviderId -> true (backwards compatible)", () => {
|
|
121
|
+
const req: ExpressRequest = buildRequestWithSsoToken({
|
|
122
|
+
tokenProjectId: projectId,
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
expect(
|
|
126
|
+
UserMiddleware.doesSsoTokenForProjectExist(req, projectId, userId),
|
|
127
|
+
).toBe(true);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
test("wrong userId -> false (even with matching provider)", () => {
|
|
131
|
+
const req: ExpressRequest = buildRequestWithSsoToken({
|
|
132
|
+
tokenProjectId: projectId,
|
|
133
|
+
discriminatorProviderId: ssoProviderId,
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
const differentUserId: ObjectID = ObjectID.generate();
|
|
137
|
+
|
|
138
|
+
expect(
|
|
139
|
+
UserMiddleware.doesSsoTokenForProjectExist(
|
|
140
|
+
req,
|
|
141
|
+
projectId,
|
|
142
|
+
differentUserId,
|
|
143
|
+
ssoProviderId,
|
|
144
|
+
),
|
|
145
|
+
).toBe(false);
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
test("no sso cookie for the project -> false", () => {
|
|
149
|
+
const req: ExpressRequest = {
|
|
150
|
+
cookies: {},
|
|
151
|
+
headers: {},
|
|
152
|
+
} as unknown as ExpressRequest;
|
|
153
|
+
|
|
154
|
+
expect(
|
|
155
|
+
UserMiddleware.doesSsoTokenForProjectExist(
|
|
156
|
+
req,
|
|
157
|
+
projectId,
|
|
158
|
+
userId,
|
|
159
|
+
ssoProviderId,
|
|
160
|
+
),
|
|
161
|
+
).toBe(false);
|
|
162
|
+
});
|
|
163
|
+
});
|