@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.
Files changed (228) 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/GlobalCache.ts +11 -0
  11. package/Server/Infrastructure/Postgres/SchemaMigrations/{1781587937032-MigrationName.ts → 1781750000000-MigrationName.ts} +2 -2
  12. package/Server/Infrastructure/Postgres/SchemaMigrations/1782000000000-AddGlobalSsoAndOidc.ts +176 -0
  13. package/Server/Infrastructure/Postgres/SchemaMigrations/1782100000000-AddStatusPageImageAltText.ts +25 -0
  14. package/Server/Infrastructure/Postgres/SchemaMigrations/1782200000000-AddRequireSsoForLoginToGlobalProviders.ts +25 -0
  15. package/Server/Infrastructure/Postgres/SchemaMigrations/1782300000000-MoveRequireSsoForLoginToGlobalConfig.ts +38 -0
  16. package/Server/Infrastructure/Postgres/SchemaMigrations/1782310000000-MigrationName.ts +299 -0
  17. package/Server/Infrastructure/Postgres/SchemaMigrations/1782400000000-RemoveIsTestedFromGlobalSsoAndOidc.ts +21 -0
  18. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +14 -2
  19. package/Server/Middleware/UserAuthorization.ts +113 -42
  20. package/Server/Services/CephClusterService.ts +22 -10
  21. package/Server/Services/CloudResourceService.ts +22 -10
  22. package/Server/Services/DockerHostService.ts +22 -10
  23. package/Server/Services/DockerSwarmClusterService.ts +22 -10
  24. package/Server/Services/GlobalConfigService.ts +50 -0
  25. package/Server/Services/GlobalOidcProjectService.ts +85 -0
  26. package/Server/Services/GlobalOidcService.ts +10 -0
  27. package/Server/Services/GlobalSsoProjectService.ts +85 -0
  28. package/Server/Services/GlobalSsoService.ts +10 -0
  29. package/Server/Services/HostService.ts +22 -10
  30. package/Server/Services/Index.ts +8 -0
  31. package/Server/Services/KubernetesClusterService.ts +22 -10
  32. package/Server/Services/PodmanHostService.ts +22 -10
  33. package/Server/Services/ProjectService.ts +44 -1
  34. package/Server/Services/ProxmoxClusterService.ts +22 -10
  35. package/Server/Services/RumApplicationService.ts +22 -10
  36. package/Server/Services/ServerlessFunctionService.ts +22 -10
  37. package/Server/Utils/Cookie.ts +39 -5
  38. package/Server/Utils/JsonWebToken.ts +7 -0
  39. package/Server/Utils/ValidateGlobalProviderProjectTeams.ts +119 -0
  40. package/Tests/Server/Infrastructure/GlobalCache.test.ts +43 -0
  41. package/Tests/Server/Middleware/UserAuthorization.test.ts +51 -13
  42. package/Tests/Server/Middleware/UserAuthorizationSSOProvider.test.ts +163 -0
  43. package/Tests/Server/Utils/CookieSSOToken.test.ts +130 -0
  44. package/Types/JsonWebTokenData.ts +3 -0
  45. package/Types/SSO/SsoProviderType.ts +8 -0
  46. package/UI/Components/Accordion/Accordion.tsx +5 -1
  47. package/UI/Components/CardSelect/CardSelect.tsx +6 -1
  48. package/UI/Components/CategoryCheckbox/Index.tsx +2 -1
  49. package/UI/Components/Charts/Area/AreaChart.tsx +4 -8
  50. package/UI/Components/Charts/Bar/BarChart.tsx +4 -8
  51. package/UI/Components/Charts/ChartGroup/ChartGroup.tsx +4 -8
  52. package/UI/Components/Charts/ChartLibrary/AreaChart/AreaChart.tsx +1 -1
  53. package/UI/Components/Charts/ChartLibrary/BarChart/BarChart.tsx +1 -1
  54. package/UI/Components/Charts/ChartLibrary/LineChart/LineChart.tsx +1 -1
  55. package/UI/Components/Charts/Line/LineChart.tsx +4 -8
  56. package/UI/Components/CodeEditor/CodeEditor.tsx +2 -0
  57. package/UI/Components/CollapsibleSection/CollapsibleSection.tsx +8 -1
  58. package/UI/Components/Dropdown/Dropdown.tsx +2 -0
  59. package/UI/Components/EditionLabel/EditionLabel.tsx +68 -16
  60. package/UI/Components/EntityDropdown/EntityDropdown.tsx +3 -0
  61. package/UI/Components/FilePicker/FilePicker.tsx +2 -0
  62. package/UI/Components/Forms/Fields/ColorPicker.tsx +2 -0
  63. package/UI/Components/Forms/Fields/FieldLabel.tsx +4 -0
  64. package/UI/Components/Forms/Fields/FormField.tsx +72 -15
  65. package/UI/Components/Forms/Fields/IconPicker.tsx +2 -0
  66. package/UI/Components/Forms/Validation.ts +107 -23
  67. package/UI/Components/Input/Input.tsx +4 -0
  68. package/UI/Components/Link/Link.tsx +23 -0
  69. package/UI/Components/Markdown.tsx/MarkdownConverters.ts +0 -0
  70. package/UI/Components/Markdown.tsx/MarkdownEditor.tsx +3 -0
  71. package/UI/Components/Markdown.tsx/MarkdownViewer.tsx +63 -2
  72. package/UI/Components/Modal/Modal.tsx +4 -1
  73. package/UI/Components/Radio/Radio.tsx +2 -0
  74. package/UI/Components/RadioButtons/GroupRadioButtons.tsx +6 -1
  75. package/UI/Components/Tabs/Tabs.tsx +63 -0
  76. package/UI/Components/TextArea/TextArea.tsx +2 -0
  77. package/UI/Components/TimePicker/TimePicker.tsx +2 -0
  78. package/UI/Components/Toggle/Toggle.tsx +2 -1
  79. package/UI/Components/Tooltip/Tooltip.tsx +6 -1
  80. package/build/dist/Models/DatabaseModels/GlobalConfig.js +20 -0
  81. package/build/dist/Models/DatabaseModels/GlobalConfig.js.map +1 -1
  82. package/build/dist/Models/DatabaseModels/GlobalOidc.js +379 -0
  83. package/build/dist/Models/DatabaseModels/GlobalOidc.js.map +1 -0
  84. package/build/dist/Models/DatabaseModels/GlobalOidcProject.js +276 -0
  85. package/build/dist/Models/DatabaseModels/GlobalOidcProject.js.map +1 -0
  86. package/build/dist/Models/DatabaseModels/GlobalSso.js +341 -0
  87. package/build/dist/Models/DatabaseModels/GlobalSso.js.map +1 -0
  88. package/build/dist/Models/DatabaseModels/GlobalSsoProject.js +279 -0
  89. package/build/dist/Models/DatabaseModels/GlobalSsoProject.js.map +1 -0
  90. package/build/dist/Models/DatabaseModels/Index.js +8 -0
  91. package/build/dist/Models/DatabaseModels/Index.js.map +1 -1
  92. package/build/dist/Models/DatabaseModels/Project.js +32 -0
  93. package/build/dist/Models/DatabaseModels/Project.js.map +1 -1
  94. package/build/dist/Models/DatabaseModels/StatusPage.js +84 -0
  95. package/build/dist/Models/DatabaseModels/StatusPage.js.map +1 -1
  96. package/build/dist/Server/API/StatusPageAPI.js +2 -0
  97. package/build/dist/Server/API/StatusPageAPI.js.map +1 -1
  98. package/build/dist/Server/Infrastructure/GlobalCache.js +13 -0
  99. package/build/dist/Server/Infrastructure/GlobalCache.js.map +1 -1
  100. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/{1781587937032-MigrationName.js → 1781750000000-MigrationName.js} +3 -3
  101. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/{1781587937032-MigrationName.js.map → 1781750000000-MigrationName.js.map} +1 -1
  102. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1782000000000-AddGlobalSsoAndOidc.js +73 -0
  103. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1782000000000-AddGlobalSsoAndOidc.js.map +1 -0
  104. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1782100000000-AddStatusPageImageAltText.js +14 -0
  105. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1782100000000-AddStatusPageImageAltText.js.map +1 -0
  106. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1782200000000-AddRequireSsoForLoginToGlobalProviders.js +14 -0
  107. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1782200000000-AddRequireSsoForLoginToGlobalProviders.js.map +1 -0
  108. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1782300000000-MoveRequireSsoForLoginToGlobalConfig.js +23 -0
  109. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1782300000000-MoveRequireSsoForLoginToGlobalConfig.js.map +1 -0
  110. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1782310000000-MigrationName.js +106 -0
  111. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1782310000000-MigrationName.js.map +1 -0
  112. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1782400000000-RemoveIsTestedFromGlobalSsoAndOidc.js +14 -0
  113. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1782400000000-RemoveIsTestedFromGlobalSsoAndOidc.js.map +1 -0
  114. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +14 -2
  115. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  116. package/build/dist/Server/Middleware/UserAuthorization.js +77 -34
  117. package/build/dist/Server/Middleware/UserAuthorization.js.map +1 -1
  118. package/build/dist/Server/Services/CephClusterService.js +19 -2
  119. package/build/dist/Server/Services/CephClusterService.js.map +1 -1
  120. package/build/dist/Server/Services/CloudResourceService.js +19 -2
  121. package/build/dist/Server/Services/CloudResourceService.js.map +1 -1
  122. package/build/dist/Server/Services/DockerHostService.js +19 -2
  123. package/build/dist/Server/Services/DockerHostService.js.map +1 -1
  124. package/build/dist/Server/Services/DockerSwarmClusterService.js +19 -2
  125. package/build/dist/Server/Services/DockerSwarmClusterService.js.map +1 -1
  126. package/build/dist/Server/Services/GlobalConfigService.js +55 -0
  127. package/build/dist/Server/Services/GlobalConfigService.js.map +1 -1
  128. package/build/dist/Server/Services/GlobalOidcProjectService.js +80 -0
  129. package/build/dist/Server/Services/GlobalOidcProjectService.js.map +1 -0
  130. package/build/dist/Server/Services/GlobalOidcService.js +9 -0
  131. package/build/dist/Server/Services/GlobalOidcService.js.map +1 -0
  132. package/build/dist/Server/Services/GlobalSsoProjectService.js +80 -0
  133. package/build/dist/Server/Services/GlobalSsoProjectService.js.map +1 -0
  134. package/build/dist/Server/Services/GlobalSsoService.js +9 -0
  135. package/build/dist/Server/Services/GlobalSsoService.js.map +1 -0
  136. package/build/dist/Server/Services/HostService.js +19 -2
  137. package/build/dist/Server/Services/HostService.js.map +1 -1
  138. package/build/dist/Server/Services/Index.js +8 -0
  139. package/build/dist/Server/Services/Index.js.map +1 -1
  140. package/build/dist/Server/Services/KubernetesClusterService.js +19 -2
  141. package/build/dist/Server/Services/KubernetesClusterService.js.map +1 -1
  142. package/build/dist/Server/Services/PodmanHostService.js +19 -2
  143. package/build/dist/Server/Services/PodmanHostService.js.map +1 -1
  144. package/build/dist/Server/Services/ProjectService.js +36 -1
  145. package/build/dist/Server/Services/ProjectService.js.map +1 -1
  146. package/build/dist/Server/Services/ProxmoxClusterService.js +19 -2
  147. package/build/dist/Server/Services/ProxmoxClusterService.js.map +1 -1
  148. package/build/dist/Server/Services/RumApplicationService.js +19 -2
  149. package/build/dist/Server/Services/RumApplicationService.js.map +1 -1
  150. package/build/dist/Server/Services/ServerlessFunctionService.js +19 -2
  151. package/build/dist/Server/Services/ServerlessFunctionService.js.map +1 -1
  152. package/build/dist/Server/Utils/Cookie.js +32 -3
  153. package/build/dist/Server/Utils/Cookie.js.map +1 -1
  154. package/build/dist/Server/Utils/JsonWebToken.js +6 -0
  155. package/build/dist/Server/Utils/JsonWebToken.js.map +1 -1
  156. package/build/dist/Server/Utils/ValidateGlobalProviderProjectTeams.js +66 -0
  157. package/build/dist/Server/Utils/ValidateGlobalProviderProjectTeams.js.map +1 -0
  158. package/build/dist/Types/SSO/SsoProviderType.js +9 -0
  159. package/build/dist/Types/SSO/SsoProviderType.js.map +1 -0
  160. package/build/dist/UI/Components/Accordion/Accordion.js +5 -3
  161. package/build/dist/UI/Components/Accordion/Accordion.js.map +1 -1
  162. package/build/dist/UI/Components/CardSelect/CardSelect.js +1 -1
  163. package/build/dist/UI/Components/CardSelect/CardSelect.js.map +1 -1
  164. package/build/dist/UI/Components/CategoryCheckbox/Index.js +1 -1
  165. package/build/dist/UI/Components/CategoryCheckbox/Index.js.map +1 -1
  166. package/build/dist/UI/Components/Charts/Area/AreaChart.js +2 -6
  167. package/build/dist/UI/Components/Charts/Area/AreaChart.js.map +1 -1
  168. package/build/dist/UI/Components/Charts/Bar/BarChart.js +2 -6
  169. package/build/dist/UI/Components/Charts/Bar/BarChart.js.map +1 -1
  170. package/build/dist/UI/Components/Charts/ChartGroup/ChartGroup.js +8 -8
  171. package/build/dist/UI/Components/Charts/ChartGroup/ChartGroup.js.map +1 -1
  172. package/build/dist/UI/Components/Charts/ChartLibrary/AreaChart/AreaChart.js +1 -1
  173. package/build/dist/UI/Components/Charts/ChartLibrary/AreaChart/AreaChart.js.map +1 -1
  174. package/build/dist/UI/Components/Charts/ChartLibrary/BarChart/BarChart.js +1 -1
  175. package/build/dist/UI/Components/Charts/ChartLibrary/BarChart/BarChart.js.map +1 -1
  176. package/build/dist/UI/Components/Charts/ChartLibrary/LineChart/LineChart.js +1 -1
  177. package/build/dist/UI/Components/Charts/ChartLibrary/LineChart/LineChart.js.map +1 -1
  178. package/build/dist/UI/Components/Charts/Line/LineChart.js +2 -6
  179. package/build/dist/UI/Components/Charts/Line/LineChart.js.map +1 -1
  180. package/build/dist/UI/Components/CodeEditor/CodeEditor.js +1 -1
  181. package/build/dist/UI/Components/CodeEditor/CodeEditor.js.map +1 -1
  182. package/build/dist/UI/Components/CollapsibleSection/CollapsibleSection.js +4 -2
  183. package/build/dist/UI/Components/CollapsibleSection/CollapsibleSection.js.map +1 -1
  184. package/build/dist/UI/Components/Dropdown/Dropdown.js +1 -1
  185. package/build/dist/UI/Components/Dropdown/Dropdown.js.map +1 -1
  186. package/build/dist/UI/Components/EditionLabel/EditionLabel.js +31 -5
  187. package/build/dist/UI/Components/EditionLabel/EditionLabel.js.map +1 -1
  188. package/build/dist/UI/Components/EntityDropdown/EntityDropdown.js +2 -2
  189. package/build/dist/UI/Components/EntityDropdown/EntityDropdown.js.map +1 -1
  190. package/build/dist/UI/Components/FilePicker/FilePicker.js +1 -1
  191. package/build/dist/UI/Components/FilePicker/FilePicker.js.map +1 -1
  192. package/build/dist/UI/Components/Forms/Fields/ColorPicker.js +1 -1
  193. package/build/dist/UI/Components/Forms/Fields/ColorPicker.js.map +1 -1
  194. package/build/dist/UI/Components/Forms/Fields/FieldLabel.js +1 -1
  195. package/build/dist/UI/Components/Forms/Fields/FieldLabel.js.map +1 -1
  196. package/build/dist/UI/Components/Forms/Fields/FormField.js +58 -22
  197. package/build/dist/UI/Components/Forms/Fields/FormField.js.map +1 -1
  198. package/build/dist/UI/Components/Forms/Fields/IconPicker.js +1 -1
  199. package/build/dist/UI/Components/Forms/Fields/IconPicker.js.map +1 -1
  200. package/build/dist/UI/Components/Forms/Validation.js +64 -15
  201. package/build/dist/UI/Components/Forms/Validation.js.map +1 -1
  202. package/build/dist/UI/Components/Input/Input.js +1 -1
  203. package/build/dist/UI/Components/Input/Input.js.map +1 -1
  204. package/build/dist/UI/Components/Link/Link.js +22 -1
  205. package/build/dist/UI/Components/Link/Link.js.map +1 -1
  206. package/build/dist/UI/Components/Markdown.tsx/MarkdownConverters.js +0 -0
  207. package/build/dist/UI/Components/Markdown.tsx/MarkdownConverters.js.map +1 -1
  208. package/build/dist/UI/Components/Markdown.tsx/MarkdownEditor.js +2 -2
  209. package/build/dist/UI/Components/Markdown.tsx/MarkdownEditor.js.map +1 -1
  210. package/build/dist/UI/Components/Markdown.tsx/MarkdownViewer.js +46 -2
  211. package/build/dist/UI/Components/Markdown.tsx/MarkdownViewer.js.map +1 -1
  212. package/build/dist/UI/Components/Modal/Modal.js +1 -1
  213. package/build/dist/UI/Components/Modal/Modal.js.map +1 -1
  214. package/build/dist/UI/Components/Radio/Radio.js +1 -1
  215. package/build/dist/UI/Components/Radio/Radio.js.map +1 -1
  216. package/build/dist/UI/Components/RadioButtons/GroupRadioButtons.js +1 -1
  217. package/build/dist/UI/Components/RadioButtons/GroupRadioButtons.js.map +1 -1
  218. package/build/dist/UI/Components/Tabs/Tabs.js +50 -1
  219. package/build/dist/UI/Components/Tabs/Tabs.js.map +1 -1
  220. package/build/dist/UI/Components/TextArea/TextArea.js +1 -1
  221. package/build/dist/UI/Components/TextArea/TextArea.js.map +1 -1
  222. package/build/dist/UI/Components/TimePicker/TimePicker.js +1 -1
  223. package/build/dist/UI/Components/TimePicker/TimePicker.js.map +1 -1
  224. package/build/dist/UI/Components/Toggle/Toggle.js +1 -1
  225. package/build/dist/UI/Components/Toggle/Toggle.js.map +1 -1
  226. package/build/dist/UI/Components/Tooltip/Tooltip.js +6 -1
  227. package/build/dist/UI/Components/Tooltip/Tooltip.js.map +1 -1
  228. package/package.json +1 -1
@@ -192,21 +192,33 @@ export class Service extends DatabaseService<Model> {
192
192
  )
193
193
  .digest("hex");
194
194
 
195
- const cached: string | null = await GlobalCache.getString(
196
- LAST_SEEN_CACHE_NAMESPACE,
197
- cacheKey,
198
- );
195
+ let cached: string | null = null;
196
+ try {
197
+ cached = await GlobalCache.getString(LAST_SEEN_CACHE_NAMESPACE, cacheKey);
198
+ } catch {
199
+ /*
200
+ * Cache unavailable — fail open and refresh lastSeenAt anyway. A
201
+ * cache error must never skip the DB write below, otherwise the
202
+ * resource is wrongly marked "disconnected" while telemetry is
203
+ * still flowing. Mirrors shouldRunMaintenance's fail-open stance.
204
+ */
205
+ cached = null;
206
+ }
199
207
 
200
208
  if (cached === extrasFingerprint) {
201
209
  return; // same data was written recently
202
210
  }
203
211
 
204
- await GlobalCache.setString(
205
- LAST_SEEN_CACHE_NAMESPACE,
206
- cacheKey,
207
- extrasFingerprint,
208
- { expiresInSeconds: LAST_SEEN_THROTTLE_SECONDS },
209
- );
212
+ try {
213
+ await GlobalCache.setString(
214
+ LAST_SEEN_CACHE_NAMESPACE,
215
+ cacheKey,
216
+ extrasFingerprint,
217
+ { expiresInSeconds: LAST_SEEN_THROTTLE_SECONDS },
218
+ );
219
+ } catch {
220
+ // Best-effort throttle write; proceed with the DB update regardless.
221
+ }
210
222
 
211
223
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
212
224
  const data: any = {
@@ -202,21 +202,33 @@ export class Service extends DatabaseService<Model> {
202
202
  )
203
203
  .digest("hex");
204
204
 
205
- const cached: string | null = await GlobalCache.getString(
206
- LAST_SEEN_CACHE_NAMESPACE,
207
- cacheKey,
208
- );
205
+ let cached: string | null = null;
206
+ try {
207
+ cached = await GlobalCache.getString(LAST_SEEN_CACHE_NAMESPACE, cacheKey);
208
+ } catch {
209
+ /*
210
+ * Cache unavailable — fail open and refresh lastSeenAt anyway. A
211
+ * cache error must never skip the DB write below, otherwise the
212
+ * resource is wrongly marked "disconnected" while telemetry is
213
+ * still flowing. Mirrors shouldRunMaintenance's fail-open stance.
214
+ */
215
+ cached = null;
216
+ }
209
217
 
210
218
  if (cached === extrasFingerprint) {
211
219
  return; // same data was written recently
212
220
  }
213
221
 
214
- await GlobalCache.setString(
215
- LAST_SEEN_CACHE_NAMESPACE,
216
- cacheKey,
217
- extrasFingerprint,
218
- { expiresInSeconds: LAST_SEEN_THROTTLE_SECONDS },
219
- );
222
+ try {
223
+ await GlobalCache.setString(
224
+ LAST_SEEN_CACHE_NAMESPACE,
225
+ cacheKey,
226
+ extrasFingerprint,
227
+ { expiresInSeconds: LAST_SEEN_THROTTLE_SECONDS },
228
+ );
229
+ } catch {
230
+ // Best-effort throttle write; proceed with the DB update regardless.
231
+ }
220
232
 
221
233
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
222
234
  const data: any = {
@@ -1,10 +1,60 @@
1
1
  import DatabaseService from "./DatabaseService";
2
2
  import Model from "../../Models/DatabaseModels/GlobalConfig";
3
+ import InMemoryTTLCache from "../Infrastructure/InMemoryTTLCache";
4
+ import ObjectID from "../../Types/ObjectID";
5
+ import { OnUpdate } from "../Types/Database/Hooks";
6
+ import CaptureSpan from "../Utils/Telemetry/CaptureSpan";
3
7
 
4
8
  export class Service extends DatabaseService<Model> {
9
+ /*
10
+ * Caches the instance-wide "Require SSO for Login" flag. This is read on the
11
+ * authenticated request path (per project), so it must not hit Postgres every
12
+ * time. Refreshed at most once per 60s and invalidated on update below.
13
+ */
14
+ private requireSsoForLoginCache: InMemoryTTLCache<boolean> =
15
+ new InMemoryTTLCache(10_000);
16
+
5
17
  public constructor() {
6
18
  super(Model);
7
19
  }
20
+
21
+ /*
22
+ * Instance-wide: must every user sign in with SSO to access projects?
23
+ * (Master admins are exempted by the enforcement layer, not here.)
24
+ */
25
+ @CaptureSpan()
26
+ public async getRequireSsoForLogin(): Promise<boolean> {
27
+ const key: string = "global";
28
+ const cached: boolean | undefined = this.requireSsoForLoginCache.get(key);
29
+ if (cached !== undefined) {
30
+ return cached;
31
+ }
32
+
33
+ const config: Model | null = await this.findOneBy({
34
+ query: {},
35
+ select: { requireSsoForLogin: true },
36
+ props: { isRoot: true },
37
+ });
38
+
39
+ const value: boolean = Boolean(config?.requireSsoForLogin);
40
+ this.requireSsoForLoginCache.set(key, value, 60_000);
41
+ return value;
42
+ }
43
+
44
+ @CaptureSpan()
45
+ protected override async onUpdateSuccess(
46
+ onUpdate: OnUpdate<Model>,
47
+ _updatedItemIds: Array<ObjectID>,
48
+ ): Promise<OnUpdate<Model>> {
49
+ if (
50
+ (onUpdate.updateBy.data as { requireSsoForLogin?: unknown })
51
+ .requireSsoForLogin !== undefined
52
+ ) {
53
+ this.requireSsoForLoginCache.clear();
54
+ }
55
+
56
+ return onUpdate;
57
+ }
8
58
  }
9
59
 
10
60
  export default new Service();
@@ -0,0 +1,85 @@
1
+ import DatabaseService from "./DatabaseService";
2
+ import Model from "../../Models/DatabaseModels/GlobalOidcProject";
3
+ import Team from "../../Models/DatabaseModels/Team";
4
+ import { LIMIT_PER_PROJECT } from "../../Types/Database/LimitMax";
5
+ import ObjectID from "../../Types/ObjectID";
6
+ import CreateBy from "../Types/Database/CreateBy";
7
+ import { OnCreate, OnUpdate } from "../Types/Database/Hooks";
8
+ import UpdateBy from "../Types/Database/UpdateBy";
9
+ import CaptureSpan from "../Utils/Telemetry/CaptureSpan";
10
+ import validateGlobalProviderProjectTeams, {
11
+ resolveAttachmentProjectId,
12
+ } from "../Utils/ValidateGlobalProviderProjectTeams";
13
+
14
+ export class Service extends DatabaseService<Model> {
15
+ public constructor() {
16
+ super(Model);
17
+ }
18
+
19
+ @CaptureSpan()
20
+ protected override async onBeforeCreate(
21
+ createBy: CreateBy<Model>,
22
+ ): Promise<OnCreate<Model>> {
23
+ /*
24
+ * The attach form submits the project via the `project` relation, so the
25
+ * `projectId` FK is not set yet. Resolve it and persist it (the column is
26
+ * required / NOT NULL) before validating the default teams against it.
27
+ */
28
+ const projectId: ObjectID | undefined = resolveAttachmentProjectId(
29
+ createBy.data,
30
+ );
31
+
32
+ if (projectId) {
33
+ createBy.data.projectId = projectId;
34
+ }
35
+
36
+ await validateGlobalProviderProjectTeams({
37
+ teams: createBy.data.teams,
38
+ projectId,
39
+ });
40
+
41
+ return { createBy, carryForward: null };
42
+ }
43
+
44
+ @CaptureSpan()
45
+ protected override async onBeforeUpdate(
46
+ updateBy: UpdateBy<Model>,
47
+ ): Promise<OnUpdate<Model>> {
48
+ // `updateBy.data` is a partial-entity shape; narrow the relation/id here.
49
+ const teams: Array<Team> | undefined = updateBy.data.teams as unknown as
50
+ | Array<Team>
51
+ | undefined;
52
+
53
+ if (teams && teams.length > 0) {
54
+ const explicitProjectId: ObjectID | undefined = updateBy.data
55
+ .projectId as unknown as ObjectID | undefined;
56
+
57
+ if (explicitProjectId) {
58
+ await validateGlobalProviderProjectTeams({
59
+ teams,
60
+ projectId: explicitProjectId,
61
+ });
62
+ } else {
63
+ // projectId is immutable here; resolve it from the row(s) being updated.
64
+ const rows: Array<Model> = await this.findBy({
65
+ query: updateBy.query,
66
+ select: { _id: true, projectId: true },
67
+ limit: LIMIT_PER_PROJECT,
68
+ skip: 0,
69
+ props: { isRoot: true },
70
+ });
71
+
72
+ for (const row of rows) {
73
+ await validateGlobalProviderProjectTeams({
74
+ teams,
75
+ projectId: row.projectId,
76
+ });
77
+ }
78
+ }
79
+ }
80
+
81
+ return { updateBy, carryForward: null };
82
+ }
83
+ }
84
+
85
+ export default new Service();
@@ -0,0 +1,10 @@
1
+ import DatabaseService from "./DatabaseService";
2
+ import Model from "../../Models/DatabaseModels/GlobalOidc";
3
+
4
+ export class Service extends DatabaseService<Model> {
5
+ public constructor() {
6
+ super(Model);
7
+ }
8
+ }
9
+
10
+ export default new Service();
@@ -0,0 +1,85 @@
1
+ import DatabaseService from "./DatabaseService";
2
+ import Model from "../../Models/DatabaseModels/GlobalSsoProject";
3
+ import Team from "../../Models/DatabaseModels/Team";
4
+ import { LIMIT_PER_PROJECT } from "../../Types/Database/LimitMax";
5
+ import ObjectID from "../../Types/ObjectID";
6
+ import CreateBy from "../Types/Database/CreateBy";
7
+ import { OnCreate, OnUpdate } from "../Types/Database/Hooks";
8
+ import UpdateBy from "../Types/Database/UpdateBy";
9
+ import CaptureSpan from "../Utils/Telemetry/CaptureSpan";
10
+ import validateGlobalProviderProjectTeams, {
11
+ resolveAttachmentProjectId,
12
+ } from "../Utils/ValidateGlobalProviderProjectTeams";
13
+
14
+ export class Service extends DatabaseService<Model> {
15
+ public constructor() {
16
+ super(Model);
17
+ }
18
+
19
+ @CaptureSpan()
20
+ protected override async onBeforeCreate(
21
+ createBy: CreateBy<Model>,
22
+ ): Promise<OnCreate<Model>> {
23
+ /*
24
+ * The attach form submits the project via the `project` relation, so the
25
+ * `projectId` FK is not set yet. Resolve it and persist it (the column is
26
+ * required / NOT NULL) before validating the default teams against it.
27
+ */
28
+ const projectId: ObjectID | undefined = resolveAttachmentProjectId(
29
+ createBy.data,
30
+ );
31
+
32
+ if (projectId) {
33
+ createBy.data.projectId = projectId;
34
+ }
35
+
36
+ await validateGlobalProviderProjectTeams({
37
+ teams: createBy.data.teams,
38
+ projectId,
39
+ });
40
+
41
+ return { createBy, carryForward: null };
42
+ }
43
+
44
+ @CaptureSpan()
45
+ protected override async onBeforeUpdate(
46
+ updateBy: UpdateBy<Model>,
47
+ ): Promise<OnUpdate<Model>> {
48
+ // `updateBy.data` is a partial-entity shape; narrow the relation/id here.
49
+ const teams: Array<Team> | undefined = updateBy.data.teams as unknown as
50
+ | Array<Team>
51
+ | undefined;
52
+
53
+ if (teams && teams.length > 0) {
54
+ const explicitProjectId: ObjectID | undefined = updateBy.data
55
+ .projectId as unknown as ObjectID | undefined;
56
+
57
+ if (explicitProjectId) {
58
+ await validateGlobalProviderProjectTeams({
59
+ teams,
60
+ projectId: explicitProjectId,
61
+ });
62
+ } else {
63
+ // projectId is immutable here; resolve it from the row(s) being updated.
64
+ const rows: Array<Model> = await this.findBy({
65
+ query: updateBy.query,
66
+ select: { _id: true, projectId: true },
67
+ limit: LIMIT_PER_PROJECT,
68
+ skip: 0,
69
+ props: { isRoot: true },
70
+ });
71
+
72
+ for (const row of rows) {
73
+ await validateGlobalProviderProjectTeams({
74
+ teams,
75
+ projectId: row.projectId,
76
+ });
77
+ }
78
+ }
79
+ }
80
+
81
+ return { updateBy, carryForward: null };
82
+ }
83
+ }
84
+
85
+ export default new Service();
@@ -0,0 +1,10 @@
1
+ import DatabaseService from "./DatabaseService";
2
+ import Model from "../../Models/DatabaseModels/GlobalSso";
3
+
4
+ export class Service extends DatabaseService<Model> {
5
+ public constructor() {
6
+ super(Model);
7
+ }
8
+ }
9
+
10
+ export default new Service();
@@ -214,21 +214,33 @@ export class Service extends DatabaseService<Model> {
214
214
  */
215
215
  const cacheKey: string = hostId.toString();
216
216
  const extrasFingerprint: string = this.fingerprintExtras(extra);
217
- const cached: string | null = await GlobalCache.getString(
218
- LAST_SEEN_CACHE_NAMESPACE,
219
- cacheKey,
220
- );
217
+ let cached: string | null = null;
218
+ try {
219
+ cached = await GlobalCache.getString(LAST_SEEN_CACHE_NAMESPACE, cacheKey);
220
+ } catch {
221
+ /*
222
+ * Cache unavailable — fail open and refresh lastSeenAt anyway. A
223
+ * cache error must never skip the DB write below, otherwise the
224
+ * resource is wrongly marked "disconnected" while telemetry is
225
+ * still flowing. Mirrors shouldRunMaintenance's fail-open stance.
226
+ */
227
+ cached = null;
228
+ }
221
229
 
222
230
  if (cached === extrasFingerprint) {
223
231
  return; // same data was written recently
224
232
  }
225
233
 
226
- await GlobalCache.setString(
227
- LAST_SEEN_CACHE_NAMESPACE,
228
- cacheKey,
229
- extrasFingerprint,
230
- { expiresInSeconds: LAST_SEEN_THROTTLE_SECONDS },
231
- );
234
+ try {
235
+ await GlobalCache.setString(
236
+ LAST_SEEN_CACHE_NAMESPACE,
237
+ cacheKey,
238
+ extrasFingerprint,
239
+ { expiresInSeconds: LAST_SEEN_THROTTLE_SECONDS },
240
+ );
241
+ } catch {
242
+ // Best-effort throttle write; proceed with the DB update regardless.
243
+ }
232
244
 
233
245
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
234
246
  const data: any = {
@@ -106,6 +106,10 @@ import ProfileSampleService from "./ProfileSampleService";
106
106
  import ProjectSmtpConfigService from "./ProjectSmtpConfigService";
107
107
  import ProjectSsoService from "./ProjectSsoService";
108
108
  import ProjectOidcService from "./ProjectOidcService";
109
+ import GlobalSsoService from "./GlobalSsoService";
110
+ import GlobalOidcService from "./GlobalOidcService";
111
+ import GlobalSsoProjectService from "./GlobalSsoProjectService";
112
+ import GlobalOidcProjectService from "./GlobalOidcProjectService";
109
113
  import PromoCodeService from "./PromoCodeService";
110
114
  import EnterpriseLicenseService from "./EnterpriseLicenseService";
111
115
  import OpenSourceDeploymentService from "./OpenSourceDeploymentService";
@@ -343,6 +347,10 @@ const services: Array<BaseService> = [
343
347
  AIAgentTaskPullRequestService,
344
348
  ProjectSsoService,
345
349
  ProjectOidcService,
350
+ GlobalSsoService,
351
+ GlobalOidcService,
352
+ GlobalSsoProjectService,
353
+ GlobalOidcProjectService,
346
354
 
347
355
  ScheduledMaintenanceCustomFieldService,
348
356
  ScheduledMaintenanceInternalNoteService,
@@ -155,21 +155,33 @@ export class Service extends DatabaseService<Model> {
155
155
  )
156
156
  .digest("hex");
157
157
 
158
- const cached: string | null = await GlobalCache.getString(
159
- LAST_SEEN_CACHE_NAMESPACE,
160
- cacheKey,
161
- );
158
+ let cached: string | null = null;
159
+ try {
160
+ cached = await GlobalCache.getString(LAST_SEEN_CACHE_NAMESPACE, cacheKey);
161
+ } catch {
162
+ /*
163
+ * Cache unavailable — fail open and refresh lastSeenAt anyway. A
164
+ * cache error must never skip the DB write below, otherwise the
165
+ * resource is wrongly marked "disconnected" while telemetry is
166
+ * still flowing. Mirrors shouldRunMaintenance's fail-open stance.
167
+ */
168
+ cached = null;
169
+ }
162
170
 
163
171
  if (cached === extrasFingerprint) {
164
172
  return; // same data was written recently
165
173
  }
166
174
 
167
- await GlobalCache.setString(
168
- LAST_SEEN_CACHE_NAMESPACE,
169
- cacheKey,
170
- extrasFingerprint,
171
- { expiresInSeconds: LAST_SEEN_THROTTLE_SECONDS },
172
- );
175
+ try {
176
+ await GlobalCache.setString(
177
+ LAST_SEEN_CACHE_NAMESPACE,
178
+ cacheKey,
179
+ extrasFingerprint,
180
+ { expiresInSeconds: LAST_SEEN_THROTTLE_SECONDS },
181
+ );
182
+ } catch {
183
+ // Best-effort throttle write; proceed with the DB update regardless.
184
+ }
173
185
 
174
186
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
175
187
  const data: any = {
@@ -192,21 +192,33 @@ export class Service extends DatabaseService<Model> {
192
192
  )
193
193
  .digest("hex");
194
194
 
195
- const cached: string | null = await GlobalCache.getString(
196
- LAST_SEEN_CACHE_NAMESPACE,
197
- cacheKey,
198
- );
195
+ let cached: string | null = null;
196
+ try {
197
+ cached = await GlobalCache.getString(LAST_SEEN_CACHE_NAMESPACE, cacheKey);
198
+ } catch {
199
+ /*
200
+ * Cache unavailable — fail open and refresh lastSeenAt anyway. A
201
+ * cache error must never skip the DB write below, otherwise the
202
+ * resource is wrongly marked "disconnected" while telemetry is
203
+ * still flowing. Mirrors shouldRunMaintenance's fail-open stance.
204
+ */
205
+ cached = null;
206
+ }
199
207
 
200
208
  if (cached === extrasFingerprint) {
201
209
  return; // same data was written recently
202
210
  }
203
211
 
204
- await GlobalCache.setString(
205
- LAST_SEEN_CACHE_NAMESPACE,
206
- cacheKey,
207
- extrasFingerprint,
208
- { expiresInSeconds: LAST_SEEN_THROTTLE_SECONDS },
209
- );
212
+ try {
213
+ await GlobalCache.setString(
214
+ LAST_SEEN_CACHE_NAMESPACE,
215
+ cacheKey,
216
+ extrasFingerprint,
217
+ { expiresInSeconds: LAST_SEEN_THROTTLE_SECONDS },
218
+ );
219
+ } catch {
220
+ // Best-effort throttle write; proceed with the DB update regardless.
221
+ }
210
222
 
211
223
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
212
224
  const data: any = {
@@ -103,6 +103,14 @@ export class ProjectService extends DatabaseService<Model> {
103
103
  */
104
104
  private requireSsoForLoginCache: InMemoryTTLCache<boolean> =
105
105
  new InMemoryTTLCache(10_000);
106
+ /*
107
+ * Caches the `requireSsoWithSsoProviderId` discriminator per project so the
108
+ * enforce-SSO middleware can require that the SSO token was issued by a
109
+ * specific provider. Stored as a string id (or null when unset). Populated
110
+ * alongside `getRequireSsoForLogin` so the common path stays a single query.
111
+ */
112
+ private requireSsoWithSsoProviderIdCache: InMemoryTTLCache<string | null> =
113
+ new InMemoryTTLCache(10_000);
106
114
  /*
107
115
  * Caches the current billing plan per project. `getCurrentPlan` is hit
108
116
  * by `CommonAPI.getDatabaseCommonInteractionProps` on every
@@ -351,6 +359,10 @@ export class ProjectService extends DatabaseService<Model> {
351
359
  this.requireSsoForLoginCache.clear();
352
360
  }
353
361
 
362
+ if (updateBy.data.requireSsoWithSsoProviderId !== undefined) {
363
+ this.requireSsoWithSsoProviderIdCache.clear();
364
+ }
365
+
354
366
  if (IsBillingEnabled) {
355
367
  if (
356
368
  updateBy.data.businessDetails ||
@@ -1338,7 +1350,7 @@ export class ProjectService extends DatabaseService<Model> {
1338
1350
 
1339
1351
  const project: Model | null = await this.findOneById({
1340
1352
  id: projectId,
1341
- select: { requireSsoForLogin: true },
1353
+ select: { requireSsoForLogin: true, requireSsoWithSsoProviderId: true },
1342
1354
  props: { isRoot: true },
1343
1355
  });
1344
1356
 
@@ -1349,9 +1361,40 @@ export class ProjectService extends DatabaseService<Model> {
1349
1361
 
1350
1362
  const value: boolean = Boolean(project.requireSsoForLogin);
1351
1363
  this.requireSsoForLoginCache.set(key, value, 60_000);
1364
+ this.requireSsoWithSsoProviderIdCache.set(
1365
+ key,
1366
+ project.requireSsoWithSsoProviderId
1367
+ ? project.requireSsoWithSsoProviderId.toString()
1368
+ : null,
1369
+ 60_000,
1370
+ );
1352
1371
  return value;
1353
1372
  }
1354
1373
 
1374
+ /**
1375
+ * Returns the specific SSO provider id a project requires for SSO-enforced
1376
+ * login, or null when any trusted provider is acceptable. Cached for 60s and
1377
+ * populated by `getRequireSsoForLogin`, so the enforce path stays one query.
1378
+ */
1379
+ @CaptureSpan()
1380
+ public async getRequireSsoWithSsoProviderId(
1381
+ projectId: ObjectID,
1382
+ ): Promise<ObjectID | null> {
1383
+ const key: string = projectId.toString();
1384
+ const cached: string | null | undefined =
1385
+ this.requireSsoWithSsoProviderIdCache.get(key);
1386
+ if (cached !== undefined) {
1387
+ return cached ? new ObjectID(cached) : null;
1388
+ }
1389
+
1390
+ // Populate both caches via the existing single-query path.
1391
+ await this.getRequireSsoForLogin(projectId);
1392
+
1393
+ const populated: string | null | undefined =
1394
+ this.requireSsoWithSsoProviderIdCache.get(key);
1395
+ return populated ? new ObjectID(populated) : null;
1396
+ }
1397
+
1355
1398
  @CaptureSpan()
1356
1399
  public async getOwners(projectId: ObjectID): Promise<Array<User>> {
1357
1400
  if (!projectId) {
@@ -193,21 +193,33 @@ export class Service extends DatabaseService<Model> {
193
193
  )
194
194
  .digest("hex");
195
195
 
196
- const cached: string | null = await GlobalCache.getString(
197
- LAST_SEEN_CACHE_NAMESPACE,
198
- cacheKey,
199
- );
196
+ let cached: string | null = null;
197
+ try {
198
+ cached = await GlobalCache.getString(LAST_SEEN_CACHE_NAMESPACE, cacheKey);
199
+ } catch {
200
+ /*
201
+ * Cache unavailable — fail open and refresh lastSeenAt anyway. A
202
+ * cache error must never skip the DB write below, otherwise the
203
+ * resource is wrongly marked "disconnected" while telemetry is
204
+ * still flowing. Mirrors shouldRunMaintenance's fail-open stance.
205
+ */
206
+ cached = null;
207
+ }
200
208
 
201
209
  if (cached === extrasFingerprint) {
202
210
  return; // same data was written recently
203
211
  }
204
212
 
205
- await GlobalCache.setString(
206
- LAST_SEEN_CACHE_NAMESPACE,
207
- cacheKey,
208
- extrasFingerprint,
209
- { expiresInSeconds: LAST_SEEN_THROTTLE_SECONDS },
210
- );
213
+ try {
214
+ await GlobalCache.setString(
215
+ LAST_SEEN_CACHE_NAMESPACE,
216
+ cacheKey,
217
+ extrasFingerprint,
218
+ { expiresInSeconds: LAST_SEEN_THROTTLE_SECONDS },
219
+ );
220
+ } catch {
221
+ // Best-effort throttle write; proceed with the DB update regardless.
222
+ }
211
223
 
212
224
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
213
225
  const data: any = {
@@ -141,21 +141,33 @@ export class Service extends DatabaseService<Model> {
141
141
  )
142
142
  .digest("hex");
143
143
 
144
- const cached: string | null = await GlobalCache.getString(
145
- LAST_SEEN_CACHE_NAMESPACE,
146
- cacheKey,
147
- );
144
+ let cached: string | null = null;
145
+ try {
146
+ cached = await GlobalCache.getString(LAST_SEEN_CACHE_NAMESPACE, cacheKey);
147
+ } catch {
148
+ /*
149
+ * Cache unavailable — fail open and refresh lastSeenAt anyway. A
150
+ * cache error must never skip the DB write below, otherwise the
151
+ * resource is wrongly marked "disconnected" while telemetry is
152
+ * still flowing. Mirrors shouldRunMaintenance's fail-open stance.
153
+ */
154
+ cached = null;
155
+ }
148
156
 
149
157
  if (cached === extrasFingerprint) {
150
158
  return; // same data was written recently
151
159
  }
152
160
 
153
- await GlobalCache.setString(
154
- LAST_SEEN_CACHE_NAMESPACE,
155
- cacheKey,
156
- extrasFingerprint,
157
- { expiresInSeconds: LAST_SEEN_THROTTLE_SECONDS },
158
- );
161
+ try {
162
+ await GlobalCache.setString(
163
+ LAST_SEEN_CACHE_NAMESPACE,
164
+ cacheKey,
165
+ extrasFingerprint,
166
+ { expiresInSeconds: LAST_SEEN_THROTTLE_SECONDS },
167
+ );
168
+ } catch {
169
+ // Best-effort throttle write; proceed with the DB update regardless.
170
+ }
159
171
 
160
172
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
161
173
  const data: any = {