@oneuptime/common 11.0.3 → 11.0.4
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/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/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/Index.ts +8 -0
- package/Server/Services/ProjectService.ts +44 -1
- package/Server/Utils/Cookie.ts +39 -5
- package/Server/Utils/JsonWebToken.ts +7 -0
- package/Server/Utils/ValidateGlobalProviderProjectTeams.ts +119 -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/CodeEditor/CodeEditor.tsx +2 -0
- package/UI/Components/CollapsibleSection/CollapsibleSection.tsx +8 -1
- package/UI/Components/Dropdown/Dropdown.tsx +2 -0
- 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/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/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/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/Index.js +8 -0
- package/build/dist/Server/Services/Index.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/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/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/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/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
|
@@ -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
|
+
});
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import CookieUtil from "../../../Server/Utils/Cookie";
|
|
2
|
+
import JSONWebToken from "../../../Server/Utils/JsonWebToken";
|
|
3
|
+
import { JSONObject } from "../../../Types/JSON";
|
|
4
|
+
import JSONWebTokenData from "../../../Types/JsonWebTokenData";
|
|
5
|
+
import Email from "../../../Types/Email";
|
|
6
|
+
import Name from "../../../Types/Name";
|
|
7
|
+
import ObjectID from "../../../Types/ObjectID";
|
|
8
|
+
import SsoProviderType from "../../../Types/SSO/SsoProviderType";
|
|
9
|
+
import User from "../../../Models/DatabaseModels/User";
|
|
10
|
+
import { describe, expect, test } from "@jest/globals";
|
|
11
|
+
|
|
12
|
+
/*
|
|
13
|
+
* Tests for the SSO-provider discriminator that CookieUtil.getSSOToken stamps
|
|
14
|
+
* onto a per-project SSO token. These run real JWT signing (no mocks): the JWT
|
|
15
|
+
* secret falls back to EncryptionSecret = "secret" when ENCRYPTION_SECRET is
|
|
16
|
+
* unset (see Common/Server/EnvironmentConfig.ts), so no env setup is required.
|
|
17
|
+
*/
|
|
18
|
+
describe("CookieUtil.getSSOToken - SSO provider discriminator", () => {
|
|
19
|
+
const buildUser: () => User = (): User => {
|
|
20
|
+
const user: User = new User();
|
|
21
|
+
user.id = ObjectID.generate();
|
|
22
|
+
user.name = new Name("Test User");
|
|
23
|
+
user.email = new Email("test@oneuptime.com");
|
|
24
|
+
return user;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
test("token carries userId and projectId (round-trips through JSONWebToken.decode)", () => {
|
|
28
|
+
const user: User = buildUser();
|
|
29
|
+
const projectId: ObjectID = ObjectID.generate();
|
|
30
|
+
|
|
31
|
+
const token: string = CookieUtil.getSSOToken({
|
|
32
|
+
user,
|
|
33
|
+
projectId,
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
expect(typeof token).toBe("string");
|
|
37
|
+
expect(token.length).toBeGreaterThan(0);
|
|
38
|
+
|
|
39
|
+
const decoded: JSONWebTokenData = JSONWebToken.decode(token);
|
|
40
|
+
|
|
41
|
+
expect(decoded.userId?.toString()).toBe(user.id!.toString());
|
|
42
|
+
expect(decoded.projectId?.toString()).toBe(projectId.toString());
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
test("token carries the ssoProviderId + ssoProviderType discriminator when provided", () => {
|
|
46
|
+
const user: User = buildUser();
|
|
47
|
+
const projectId: ObjectID = ObjectID.generate();
|
|
48
|
+
const ssoProviderId: ObjectID = ObjectID.generate();
|
|
49
|
+
|
|
50
|
+
const token: string = CookieUtil.getSSOToken({
|
|
51
|
+
user,
|
|
52
|
+
projectId,
|
|
53
|
+
ssoProviderId,
|
|
54
|
+
ssoProviderType: SsoProviderType.GlobalSSO,
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
/*
|
|
58
|
+
* The discriminator lives on the raw JWT payload. JSONWebToken.decode()
|
|
59
|
+
* intentionally re-shapes the payload into JSONWebTokenData and does not
|
|
60
|
+
* surface ssoProviderId / ssoProviderType, so we assert the round-trip on
|
|
61
|
+
* the raw payload (decodeJsonPayload), which is the actual transport layer.
|
|
62
|
+
*/
|
|
63
|
+
const payload: JSONObject = JSONWebToken.decodeJsonPayload(token);
|
|
64
|
+
|
|
65
|
+
expect(payload["userId"]).toBe(user.id!.toString());
|
|
66
|
+
expect(payload["projectId"]).toBe(projectId.toString());
|
|
67
|
+
expect(payload["ssoProviderId"]).toBe(ssoProviderId.toString());
|
|
68
|
+
expect(payload["ssoProviderType"]).toBe(SsoProviderType.GlobalSSO);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test("each SsoProviderType enum value round-trips on the token", () => {
|
|
72
|
+
const user: User = buildUser();
|
|
73
|
+
const projectId: ObjectID = ObjectID.generate();
|
|
74
|
+
const ssoProviderId: ObjectID = ObjectID.generate();
|
|
75
|
+
|
|
76
|
+
const providerTypes: Array<SsoProviderType> = [
|
|
77
|
+
SsoProviderType.ProjectSSO,
|
|
78
|
+
SsoProviderType.ProjectOIDC,
|
|
79
|
+
SsoProviderType.GlobalSSO,
|
|
80
|
+
SsoProviderType.GlobalOIDC,
|
|
81
|
+
];
|
|
82
|
+
|
|
83
|
+
for (const providerType of providerTypes) {
|
|
84
|
+
const token: string = CookieUtil.getSSOToken({
|
|
85
|
+
user,
|
|
86
|
+
projectId,
|
|
87
|
+
ssoProviderId,
|
|
88
|
+
ssoProviderType: providerType,
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
const payload: JSONObject = JSONWebToken.decodeJsonPayload(token);
|
|
92
|
+
|
|
93
|
+
expect(payload["ssoProviderId"]).toBe(ssoProviderId.toString());
|
|
94
|
+
expect(payload["ssoProviderType"]).toBe(providerType);
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
test("ssoProviderId / ssoProviderType are absent (undefined) when not provided (legacy token shape)", () => {
|
|
99
|
+
const user: User = buildUser();
|
|
100
|
+
const projectId: ObjectID = ObjectID.generate();
|
|
101
|
+
|
|
102
|
+
const token: string = CookieUtil.getSSOToken({
|
|
103
|
+
user,
|
|
104
|
+
projectId,
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
const payload: JSONObject = JSONWebToken.decodeJsonPayload(token);
|
|
108
|
+
|
|
109
|
+
// No discriminator was provided, so neither field should carry a value.
|
|
110
|
+
expect(payload["ssoProviderId"]).toBeUndefined();
|
|
111
|
+
expect(payload["ssoProviderType"]).toBeUndefined();
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
test("ssoProviderId without ssoProviderType still records the provider id", () => {
|
|
115
|
+
const user: User = buildUser();
|
|
116
|
+
const projectId: ObjectID = ObjectID.generate();
|
|
117
|
+
const ssoProviderId: ObjectID = ObjectID.generate();
|
|
118
|
+
|
|
119
|
+
const token: string = CookieUtil.getSSOToken({
|
|
120
|
+
user,
|
|
121
|
+
projectId,
|
|
122
|
+
ssoProviderId,
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
const payload: JSONObject = JSONWebToken.decodeJsonPayload(token);
|
|
126
|
+
|
|
127
|
+
expect(payload["ssoProviderId"]).toBe(ssoProviderId.toString());
|
|
128
|
+
expect(payload["ssoProviderType"]).toBeUndefined();
|
|
129
|
+
});
|
|
130
|
+
});
|
|
@@ -2,6 +2,7 @@ import Email from "./Email";
|
|
|
2
2
|
import { JSONObject } from "./JSON";
|
|
3
3
|
import Name from "./Name";
|
|
4
4
|
import ObjectID from "./ObjectID";
|
|
5
|
+
import SsoProviderType from "./SSO/SsoProviderType";
|
|
5
6
|
|
|
6
7
|
export default interface JSONWebTokenData extends JSONObject {
|
|
7
8
|
userId: ObjectID;
|
|
@@ -12,4 +13,6 @@ export default interface JSONWebTokenData extends JSONObject {
|
|
|
12
13
|
projectId?: ObjectID | undefined; // for SSO logins.
|
|
13
14
|
isGlobalLogin: boolean; // If this is OneUptime username and password login. This is true, if this is SSO login. Then, this is false.
|
|
14
15
|
sessionId?: ObjectID | undefined;
|
|
16
|
+
ssoProviderId?: ObjectID | undefined; // which SSO provider (Project or Global SSO/OIDC) issued this per-project SSO token.
|
|
17
|
+
ssoProviderType?: SsoProviderType | undefined;
|
|
15
18
|
}
|
|
@@ -60,7 +60,9 @@ const Accordion: FunctionComponent<ComponentProps> = (
|
|
|
60
60
|
className = "-ml-5 -mr-5 p-5 mt-1";
|
|
61
61
|
}
|
|
62
62
|
|
|
63
|
-
const
|
|
63
|
+
const generatedId: string = React.useId();
|
|
64
|
+
const accordionId: string = `accordion-content-${generatedId}`;
|
|
65
|
+
const accordionTitleId: string = `accordion-title-${generatedId}`;
|
|
64
66
|
|
|
65
67
|
const handleKeyDown: (event: React.KeyboardEvent) => void = (
|
|
66
68
|
event: React.KeyboardEvent,
|
|
@@ -84,6 +86,7 @@ const Accordion: FunctionComponent<ComponentProps> = (
|
|
|
84
86
|
tabIndex={0}
|
|
85
87
|
aria-expanded={isOpen}
|
|
86
88
|
aria-controls={accordionId}
|
|
89
|
+
aria-labelledby={props.title ? accordionTitleId : undefined}
|
|
87
90
|
onClick={() => {
|
|
88
91
|
setIsOpen(!isOpen);
|
|
89
92
|
}}
|
|
@@ -121,6 +124,7 @@ const Accordion: FunctionComponent<ComponentProps> = (
|
|
|
121
124
|
}`}
|
|
122
125
|
>
|
|
123
126
|
<div
|
|
127
|
+
id={accordionTitleId}
|
|
124
128
|
className={`text-gray-900 leading-snug ${props.titleClassName || ""}`}
|
|
125
129
|
>
|
|
126
130
|
{props.title}
|
|
@@ -30,6 +30,7 @@ export interface ComponentProps {
|
|
|
30
30
|
error?: string | undefined;
|
|
31
31
|
tabIndex?: number | undefined;
|
|
32
32
|
dataTestId?: string | undefined;
|
|
33
|
+
ariaLabelledby?: string | undefined;
|
|
33
34
|
// Force single-column (1 item per row). Default: responsive 1/2/3 grid.
|
|
34
35
|
singleColumn?: boolean | undefined;
|
|
35
36
|
}
|
|
@@ -67,7 +68,11 @@ const CardSelect: FunctionComponent<ComponentProps> = (
|
|
|
67
68
|
|
|
68
69
|
return (
|
|
69
70
|
<div data-testid={props.dataTestId}>
|
|
70
|
-
<div
|
|
71
|
+
<div
|
|
72
|
+
role="radiogroup"
|
|
73
|
+
aria-label="Select an option"
|
|
74
|
+
aria-labelledby={props.ariaLabelledby}
|
|
75
|
+
>
|
|
71
76
|
{groups.map((group: RenderGroup, groupIndex: number) => {
|
|
72
77
|
return (
|
|
73
78
|
<div key={groupIndex} className={groupIndex > 0 ? "mt-8" : ""}>
|
|
@@ -19,6 +19,7 @@ export interface CategoryCheckboxProps
|
|
|
19
19
|
initialValue?: undefined | Array<CategoryCheckboxValue | BaseModel>;
|
|
20
20
|
error?: string | undefined;
|
|
21
21
|
dataTestId?: string | undefined;
|
|
22
|
+
ariaLabelledby?: string | undefined;
|
|
22
23
|
}
|
|
23
24
|
|
|
24
25
|
const CategoryCheckbox: FunctionComponent<CategoryCheckboxProps> = (
|
|
@@ -152,7 +153,7 @@ const CategoryCheckbox: FunctionComponent<CategoryCheckboxProps> = (
|
|
|
152
153
|
};
|
|
153
154
|
|
|
154
155
|
return (
|
|
155
|
-
<div>
|
|
156
|
+
<div role="group" aria-labelledby={props.ariaLabelledby}>
|
|
156
157
|
{getCategory(undefined, categories.length === 0)}
|
|
157
158
|
{categories.map((category: CheckboxCategory, i: number) => {
|
|
158
159
|
return (
|
|
@@ -25,6 +25,7 @@ export interface ComponentProps {
|
|
|
25
25
|
value?: string | undefined;
|
|
26
26
|
showLineNumbers?: boolean | undefined;
|
|
27
27
|
disableSpellCheck?: boolean | undefined;
|
|
28
|
+
ariaLabelledby?: string | undefined;
|
|
28
29
|
}
|
|
29
30
|
|
|
30
31
|
const CodeEditor: FunctionComponent<ComponentProps> = (
|
|
@@ -119,6 +120,7 @@ const CodeEditor: FunctionComponent<ComponentProps> = (
|
|
|
119
120
|
return (
|
|
120
121
|
<div
|
|
121
122
|
data-testid={props.dataTestId}
|
|
123
|
+
aria-labelledby={props.ariaLabelledby}
|
|
122
124
|
onClick={() => {
|
|
123
125
|
if (props.onClick) {
|
|
124
126
|
props.onClick();
|
|
@@ -46,6 +46,9 @@ const CollapsibleSection: FunctionComponent<ComponentProps> = (
|
|
|
46
46
|
|
|
47
47
|
const variant: CollapsibleSectionVariant = props.variant || "default";
|
|
48
48
|
|
|
49
|
+
// Associate the role="button" header with its visible title text (WCAG 4.1.2).
|
|
50
|
+
const collapsibleTitleId: string = `collapsible-title-${React.useId()}`;
|
|
51
|
+
|
|
49
52
|
const getContainerClassName: () => string = (): string => {
|
|
50
53
|
const baseClassName: string = props.className || "";
|
|
51
54
|
|
|
@@ -97,6 +100,7 @@ const CollapsibleSection: FunctionComponent<ComponentProps> = (
|
|
|
97
100
|
}
|
|
98
101
|
}}
|
|
99
102
|
aria-expanded={!isCollapsed}
|
|
103
|
+
aria-labelledby={collapsibleTitleId}
|
|
100
104
|
>
|
|
101
105
|
<div className="flex items-center flex-1 min-w-0">
|
|
102
106
|
<Icon
|
|
@@ -105,7 +109,10 @@ const CollapsibleSection: FunctionComponent<ComponentProps> = (
|
|
|
105
109
|
/>
|
|
106
110
|
<div className="flex-1 min-w-0">
|
|
107
111
|
<div className="flex items-center">
|
|
108
|
-
<span
|
|
112
|
+
<span
|
|
113
|
+
id={collapsibleTitleId}
|
|
114
|
+
className="text-sm font-medium text-gray-900 truncate"
|
|
115
|
+
>
|
|
109
116
|
{props.title}
|
|
110
117
|
</span>
|
|
111
118
|
{isCollapsed && props.badge && (
|
|
@@ -59,6 +59,7 @@ export interface ComponentProps {
|
|
|
59
59
|
id?: string | undefined;
|
|
60
60
|
dataTestId?: string | undefined;
|
|
61
61
|
ariaLabel?: string | undefined;
|
|
62
|
+
ariaLabelledby?: string | undefined;
|
|
62
63
|
}
|
|
63
64
|
|
|
64
65
|
const Dropdown: FunctionComponent<ComponentProps> = (
|
|
@@ -537,6 +538,7 @@ const Dropdown: FunctionComponent<ComponentProps> = (
|
|
|
537
538
|
props.onFocus?.();
|
|
538
539
|
}}
|
|
539
540
|
aria-label={props.ariaLabel}
|
|
541
|
+
aria-labelledby={props.ariaLabelledby}
|
|
540
542
|
aria-invalid={props.error ? true : undefined}
|
|
541
543
|
aria-describedby={props.error ? errorId : undefined}
|
|
542
544
|
classNames={{
|
|
@@ -86,6 +86,7 @@ export interface EntityDropdownProps {
|
|
|
86
86
|
id?: string | undefined;
|
|
87
87
|
dataTestId?: string | undefined;
|
|
88
88
|
ariaLabel?: string | undefined;
|
|
89
|
+
ariaLabelledby?: string | undefined;
|
|
89
90
|
disabled?: boolean | undefined;
|
|
90
91
|
|
|
91
92
|
/*
|
|
@@ -1115,6 +1116,7 @@ const EntityDropdown: FunctionComponent<EntityDropdownProps> = (
|
|
|
1115
1116
|
<button
|
|
1116
1117
|
type="button"
|
|
1117
1118
|
disabled={props.disabled}
|
|
1119
|
+
aria-labelledby={props.ariaLabelledby}
|
|
1118
1120
|
onClick={(): void => {
|
|
1119
1121
|
if (props.disabled) {
|
|
1120
1122
|
return;
|
|
@@ -1204,6 +1206,7 @@ const EntityDropdown: FunctionComponent<EntityDropdownProps> = (
|
|
|
1204
1206
|
aria-autocomplete="list"
|
|
1205
1207
|
aria-expanded={isOpen}
|
|
1206
1208
|
aria-label={props.ariaLabel}
|
|
1209
|
+
aria-labelledby={props.ariaLabelledby}
|
|
1207
1210
|
aria-invalid={props.error ? true : undefined}
|
|
1208
1211
|
data-testid={props.dataTestId}
|
|
1209
1212
|
role="combobox"
|
|
@@ -32,6 +32,7 @@ export interface ComponentProps {
|
|
|
32
32
|
isMultiFilePicker?: boolean | undefined;
|
|
33
33
|
tabIndex?: number | undefined;
|
|
34
34
|
error?: string | undefined;
|
|
35
|
+
ariaLabelledby?: string | undefined;
|
|
35
36
|
}
|
|
36
37
|
|
|
37
38
|
type UploadStatus = {
|
|
@@ -449,6 +450,7 @@ const FilePicker: FunctionComponent<ComponentProps> = (
|
|
|
449
450
|
id="file-upload"
|
|
450
451
|
name="file-upload"
|
|
451
452
|
type="file"
|
|
453
|
+
aria-labelledby={props.ariaLabelledby}
|
|
452
454
|
className="sr-only"
|
|
453
455
|
/>
|
|
454
456
|
</label>
|
|
@@ -24,6 +24,7 @@ export interface ComponentProps {
|
|
|
24
24
|
dataTestId?: string | undefined;
|
|
25
25
|
onEnterPress?: (() => void) | undefined;
|
|
26
26
|
error?: string | undefined;
|
|
27
|
+
ariaLabelledby?: string | undefined;
|
|
27
28
|
}
|
|
28
29
|
|
|
29
30
|
const ColorPicker: FunctionComponent<ComponentProps> = (
|
|
@@ -83,6 +84,7 @@ const ColorPicker: FunctionComponent<ComponentProps> = (
|
|
|
83
84
|
readOnly={true}
|
|
84
85
|
type={InputType.TEXT}
|
|
85
86
|
tabIndex={props.tabIndex}
|
|
87
|
+
ariaLabelledby={props.ariaLabelledby}
|
|
86
88
|
onChange={(value: string) => {
|
|
87
89
|
if (!value) {
|
|
88
90
|
return handleChange("");
|
|
@@ -5,6 +5,8 @@ import React, { FunctionComponent, ReactElement } from "react";
|
|
|
5
5
|
|
|
6
6
|
export interface ComponentProps {
|
|
7
7
|
title: string;
|
|
8
|
+
id?: string | undefined;
|
|
9
|
+
htmlFor?: string | undefined;
|
|
8
10
|
required?: boolean | undefined;
|
|
9
11
|
sideLink?: FormFieldSideLink | undefined;
|
|
10
12
|
description?: string | ReactElement | undefined;
|
|
@@ -26,6 +28,8 @@ const FieldLabelElement: FunctionComponent<ComponentProps> = (
|
|
|
26
28
|
return (
|
|
27
29
|
<>
|
|
28
30
|
<label
|
|
31
|
+
id={props.id}
|
|
32
|
+
htmlFor={props.htmlFor}
|
|
29
33
|
className={
|
|
30
34
|
props.className ||
|
|
31
35
|
`block ${
|