@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.
Files changed (167) hide show
  1. package/Models/DatabaseModels/GlobalConfig.ts +19 -0
  2. package/Models/DatabaseModels/GlobalOidc.ts +351 -0
  3. package/Models/DatabaseModels/GlobalOidcProject.ts +265 -0
  4. package/Models/DatabaseModels/GlobalSso.ts +312 -0
  5. package/Models/DatabaseModels/GlobalSsoProject.ts +268 -0
  6. package/Models/DatabaseModels/Index.ts +8 -0
  7. package/Models/DatabaseModels/Project.ts +31 -0
  8. package/Models/DatabaseModels/StatusPage.ts +82 -0
  9. package/Server/API/StatusPageAPI.ts +2 -0
  10. package/Server/Infrastructure/Postgres/SchemaMigrations/{1781587937032-MigrationName.ts → 1781750000000-MigrationName.ts} +2 -2
  11. package/Server/Infrastructure/Postgres/SchemaMigrations/1782000000000-AddGlobalSsoAndOidc.ts +176 -0
  12. package/Server/Infrastructure/Postgres/SchemaMigrations/1782100000000-AddStatusPageImageAltText.ts +25 -0
  13. package/Server/Infrastructure/Postgres/SchemaMigrations/1782200000000-AddRequireSsoForLoginToGlobalProviders.ts +25 -0
  14. package/Server/Infrastructure/Postgres/SchemaMigrations/1782300000000-MoveRequireSsoForLoginToGlobalConfig.ts +38 -0
  15. package/Server/Infrastructure/Postgres/SchemaMigrations/1782310000000-MigrationName.ts +299 -0
  16. package/Server/Infrastructure/Postgres/SchemaMigrations/1782400000000-RemoveIsTestedFromGlobalSsoAndOidc.ts +21 -0
  17. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +14 -2
  18. package/Server/Middleware/UserAuthorization.ts +113 -42
  19. package/Server/Services/GlobalConfigService.ts +50 -0
  20. package/Server/Services/GlobalOidcProjectService.ts +85 -0
  21. package/Server/Services/GlobalOidcService.ts +10 -0
  22. package/Server/Services/GlobalSsoProjectService.ts +85 -0
  23. package/Server/Services/GlobalSsoService.ts +10 -0
  24. package/Server/Services/Index.ts +8 -0
  25. package/Server/Services/ProjectService.ts +44 -1
  26. package/Server/Utils/Cookie.ts +39 -5
  27. package/Server/Utils/JsonWebToken.ts +7 -0
  28. package/Server/Utils/ValidateGlobalProviderProjectTeams.ts +119 -0
  29. package/Tests/Server/Middleware/UserAuthorization.test.ts +51 -13
  30. package/Tests/Server/Middleware/UserAuthorizationSSOProvider.test.ts +163 -0
  31. package/Tests/Server/Utils/CookieSSOToken.test.ts +130 -0
  32. package/Types/JsonWebTokenData.ts +3 -0
  33. package/Types/SSO/SsoProviderType.ts +8 -0
  34. package/UI/Components/Accordion/Accordion.tsx +5 -1
  35. package/UI/Components/CardSelect/CardSelect.tsx +6 -1
  36. package/UI/Components/CategoryCheckbox/Index.tsx +2 -1
  37. package/UI/Components/CodeEditor/CodeEditor.tsx +2 -0
  38. package/UI/Components/CollapsibleSection/CollapsibleSection.tsx +8 -1
  39. package/UI/Components/Dropdown/Dropdown.tsx +2 -0
  40. package/UI/Components/EntityDropdown/EntityDropdown.tsx +3 -0
  41. package/UI/Components/FilePicker/FilePicker.tsx +2 -0
  42. package/UI/Components/Forms/Fields/ColorPicker.tsx +2 -0
  43. package/UI/Components/Forms/Fields/FieldLabel.tsx +4 -0
  44. package/UI/Components/Forms/Fields/FormField.tsx +72 -15
  45. package/UI/Components/Forms/Fields/IconPicker.tsx +2 -0
  46. package/UI/Components/Forms/Validation.ts +107 -23
  47. package/UI/Components/Input/Input.tsx +4 -0
  48. package/UI/Components/Link/Link.tsx +23 -0
  49. package/UI/Components/Markdown.tsx/MarkdownConverters.ts +0 -0
  50. package/UI/Components/Markdown.tsx/MarkdownEditor.tsx +3 -0
  51. package/UI/Components/Markdown.tsx/MarkdownViewer.tsx +63 -2
  52. package/UI/Components/Radio/Radio.tsx +2 -0
  53. package/UI/Components/RadioButtons/GroupRadioButtons.tsx +6 -1
  54. package/UI/Components/Tabs/Tabs.tsx +63 -0
  55. package/UI/Components/TextArea/TextArea.tsx +2 -0
  56. package/UI/Components/TimePicker/TimePicker.tsx +2 -0
  57. package/UI/Components/Toggle/Toggle.tsx +2 -1
  58. package/UI/Components/Tooltip/Tooltip.tsx +6 -1
  59. package/build/dist/Models/DatabaseModels/GlobalConfig.js +20 -0
  60. package/build/dist/Models/DatabaseModels/GlobalConfig.js.map +1 -1
  61. package/build/dist/Models/DatabaseModels/GlobalOidc.js +379 -0
  62. package/build/dist/Models/DatabaseModels/GlobalOidc.js.map +1 -0
  63. package/build/dist/Models/DatabaseModels/GlobalOidcProject.js +276 -0
  64. package/build/dist/Models/DatabaseModels/GlobalOidcProject.js.map +1 -0
  65. package/build/dist/Models/DatabaseModels/GlobalSso.js +341 -0
  66. package/build/dist/Models/DatabaseModels/GlobalSso.js.map +1 -0
  67. package/build/dist/Models/DatabaseModels/GlobalSsoProject.js +279 -0
  68. package/build/dist/Models/DatabaseModels/GlobalSsoProject.js.map +1 -0
  69. package/build/dist/Models/DatabaseModels/Index.js +8 -0
  70. package/build/dist/Models/DatabaseModels/Index.js.map +1 -1
  71. package/build/dist/Models/DatabaseModels/Project.js +32 -0
  72. package/build/dist/Models/DatabaseModels/Project.js.map +1 -1
  73. package/build/dist/Models/DatabaseModels/StatusPage.js +84 -0
  74. package/build/dist/Models/DatabaseModels/StatusPage.js.map +1 -1
  75. package/build/dist/Server/API/StatusPageAPI.js +2 -0
  76. package/build/dist/Server/API/StatusPageAPI.js.map +1 -1
  77. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/{1781587937032-MigrationName.js → 1781750000000-MigrationName.js} +3 -3
  78. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/{1781587937032-MigrationName.js.map → 1781750000000-MigrationName.js.map} +1 -1
  79. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1782000000000-AddGlobalSsoAndOidc.js +73 -0
  80. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1782000000000-AddGlobalSsoAndOidc.js.map +1 -0
  81. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1782100000000-AddStatusPageImageAltText.js +14 -0
  82. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1782100000000-AddStatusPageImageAltText.js.map +1 -0
  83. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1782200000000-AddRequireSsoForLoginToGlobalProviders.js +14 -0
  84. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1782200000000-AddRequireSsoForLoginToGlobalProviders.js.map +1 -0
  85. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1782300000000-MoveRequireSsoForLoginToGlobalConfig.js +23 -0
  86. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1782300000000-MoveRequireSsoForLoginToGlobalConfig.js.map +1 -0
  87. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1782310000000-MigrationName.js +106 -0
  88. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1782310000000-MigrationName.js.map +1 -0
  89. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1782400000000-RemoveIsTestedFromGlobalSsoAndOidc.js +14 -0
  90. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1782400000000-RemoveIsTestedFromGlobalSsoAndOidc.js.map +1 -0
  91. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +14 -2
  92. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  93. package/build/dist/Server/Middleware/UserAuthorization.js +77 -34
  94. package/build/dist/Server/Middleware/UserAuthorization.js.map +1 -1
  95. package/build/dist/Server/Services/GlobalConfigService.js +55 -0
  96. package/build/dist/Server/Services/GlobalConfigService.js.map +1 -1
  97. package/build/dist/Server/Services/GlobalOidcProjectService.js +80 -0
  98. package/build/dist/Server/Services/GlobalOidcProjectService.js.map +1 -0
  99. package/build/dist/Server/Services/GlobalOidcService.js +9 -0
  100. package/build/dist/Server/Services/GlobalOidcService.js.map +1 -0
  101. package/build/dist/Server/Services/GlobalSsoProjectService.js +80 -0
  102. package/build/dist/Server/Services/GlobalSsoProjectService.js.map +1 -0
  103. package/build/dist/Server/Services/GlobalSsoService.js +9 -0
  104. package/build/dist/Server/Services/GlobalSsoService.js.map +1 -0
  105. package/build/dist/Server/Services/Index.js +8 -0
  106. package/build/dist/Server/Services/Index.js.map +1 -1
  107. package/build/dist/Server/Services/ProjectService.js +36 -1
  108. package/build/dist/Server/Services/ProjectService.js.map +1 -1
  109. package/build/dist/Server/Utils/Cookie.js +32 -3
  110. package/build/dist/Server/Utils/Cookie.js.map +1 -1
  111. package/build/dist/Server/Utils/JsonWebToken.js +6 -0
  112. package/build/dist/Server/Utils/JsonWebToken.js.map +1 -1
  113. package/build/dist/Server/Utils/ValidateGlobalProviderProjectTeams.js +66 -0
  114. package/build/dist/Server/Utils/ValidateGlobalProviderProjectTeams.js.map +1 -0
  115. package/build/dist/Types/SSO/SsoProviderType.js +9 -0
  116. package/build/dist/Types/SSO/SsoProviderType.js.map +1 -0
  117. package/build/dist/UI/Components/Accordion/Accordion.js +5 -3
  118. package/build/dist/UI/Components/Accordion/Accordion.js.map +1 -1
  119. package/build/dist/UI/Components/CardSelect/CardSelect.js +1 -1
  120. package/build/dist/UI/Components/CardSelect/CardSelect.js.map +1 -1
  121. package/build/dist/UI/Components/CategoryCheckbox/Index.js +1 -1
  122. package/build/dist/UI/Components/CategoryCheckbox/Index.js.map +1 -1
  123. package/build/dist/UI/Components/CodeEditor/CodeEditor.js +1 -1
  124. package/build/dist/UI/Components/CodeEditor/CodeEditor.js.map +1 -1
  125. package/build/dist/UI/Components/CollapsibleSection/CollapsibleSection.js +4 -2
  126. package/build/dist/UI/Components/CollapsibleSection/CollapsibleSection.js.map +1 -1
  127. package/build/dist/UI/Components/Dropdown/Dropdown.js +1 -1
  128. package/build/dist/UI/Components/Dropdown/Dropdown.js.map +1 -1
  129. package/build/dist/UI/Components/EntityDropdown/EntityDropdown.js +2 -2
  130. package/build/dist/UI/Components/EntityDropdown/EntityDropdown.js.map +1 -1
  131. package/build/dist/UI/Components/FilePicker/FilePicker.js +1 -1
  132. package/build/dist/UI/Components/FilePicker/FilePicker.js.map +1 -1
  133. package/build/dist/UI/Components/Forms/Fields/ColorPicker.js +1 -1
  134. package/build/dist/UI/Components/Forms/Fields/ColorPicker.js.map +1 -1
  135. package/build/dist/UI/Components/Forms/Fields/FieldLabel.js +1 -1
  136. package/build/dist/UI/Components/Forms/Fields/FieldLabel.js.map +1 -1
  137. package/build/dist/UI/Components/Forms/Fields/FormField.js +58 -22
  138. package/build/dist/UI/Components/Forms/Fields/FormField.js.map +1 -1
  139. package/build/dist/UI/Components/Forms/Fields/IconPicker.js +1 -1
  140. package/build/dist/UI/Components/Forms/Fields/IconPicker.js.map +1 -1
  141. package/build/dist/UI/Components/Forms/Validation.js +64 -15
  142. package/build/dist/UI/Components/Forms/Validation.js.map +1 -1
  143. package/build/dist/UI/Components/Input/Input.js +1 -1
  144. package/build/dist/UI/Components/Input/Input.js.map +1 -1
  145. package/build/dist/UI/Components/Link/Link.js +22 -1
  146. package/build/dist/UI/Components/Link/Link.js.map +1 -1
  147. package/build/dist/UI/Components/Markdown.tsx/MarkdownConverters.js +0 -0
  148. package/build/dist/UI/Components/Markdown.tsx/MarkdownConverters.js.map +1 -1
  149. package/build/dist/UI/Components/Markdown.tsx/MarkdownEditor.js +2 -2
  150. package/build/dist/UI/Components/Markdown.tsx/MarkdownEditor.js.map +1 -1
  151. package/build/dist/UI/Components/Markdown.tsx/MarkdownViewer.js +46 -2
  152. package/build/dist/UI/Components/Markdown.tsx/MarkdownViewer.js.map +1 -1
  153. package/build/dist/UI/Components/Radio/Radio.js +1 -1
  154. package/build/dist/UI/Components/Radio/Radio.js.map +1 -1
  155. package/build/dist/UI/Components/RadioButtons/GroupRadioButtons.js +1 -1
  156. package/build/dist/UI/Components/RadioButtons/GroupRadioButtons.js.map +1 -1
  157. package/build/dist/UI/Components/Tabs/Tabs.js +50 -1
  158. package/build/dist/UI/Components/Tabs/Tabs.js.map +1 -1
  159. package/build/dist/UI/Components/TextArea/TextArea.js +1 -1
  160. package/build/dist/UI/Components/TextArea/TextArea.js.map +1 -1
  161. package/build/dist/UI/Components/TimePicker/TimePicker.js +1 -1
  162. package/build/dist/UI/Components/TimePicker/TimePicker.js.map +1 -1
  163. package/build/dist/UI/Components/Toggle/Toggle.js +1 -1
  164. package/build/dist/UI/Components/Toggle/Toggle.js.map +1 -1
  165. package/build/dist/UI/Components/Tooltip/Tooltip.js +6 -1
  166. package/build/dist/UI/Components/Tooltip/Tooltip.js.map +1 -1
  167. 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
  }
@@ -0,0 +1,8 @@
1
+ enum SsoProviderType {
2
+ ProjectSSO = "ProjectSSO",
3
+ ProjectOIDC = "ProjectOIDC",
4
+ GlobalSSO = "GlobalSSO",
5
+ GlobalOIDC = "GlobalOIDC",
6
+ }
7
+
8
+ export default SsoProviderType;
@@ -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 accordionId: string = `accordion-content-${React.useId()}`;
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 role="radiogroup" aria-label="Select an option">
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 className="text-sm font-medium text-gray-900 truncate">
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 ${