@oneuptime/common 12.0.14 → 12.0.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Models/DatabaseModels/GlobalOidc.ts +20 -0
- package/Models/DatabaseModels/GlobalSso.ts +20 -0
- package/Models/DatabaseModels/MarketingConversion.ts +19 -9
- package/Models/DatabaseModels/NetworkDeviceLinkRule.ts +46 -0
- package/Models/DatabaseModels/StatusPage.ts +7 -4
- package/Server/API/StatusPageAPI.ts +29 -4
- package/Server/EnvironmentConfig.ts +7 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1787800000000-AddScopeToNetworkDeviceLinkRule.ts +19 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1787900000000-AddRestrictToAttachedProjectsToGlobalSso.ts +40 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +4 -0
- package/Server/Middleware/SlackAuthorization.ts +43 -6
- package/Server/Middleware/UserAuthorization.ts +236 -26
- package/Server/Middleware/WhatsAppAuthorization.ts +43 -3
- package/Server/Services/GlobalOidcProjectService.ts +126 -1
- package/Server/Services/GlobalOidcService.ts +132 -0
- package/Server/Services/GlobalSsoProjectService.ts +126 -1
- package/Server/Services/GlobalSsoService.ts +132 -0
- package/Server/Services/UserEmailService.ts +4 -4
- package/Server/Utils/CronTab.ts +73 -0
- package/Server/Utils/GlobalSsoAuthorization.ts +175 -0
- package/Server/Utils/Marketing/ConversionUploadProvider.ts +35 -5
- package/Server/Utils/Marketing/Providers/GoogleAds.ts +1 -1
- package/Server/Utils/Marketing/Providers/LinkedIn.ts +1 -1
- package/Server/Utils/Marketing/Providers/Meta.ts +1 -1
- package/Server/Utils/Marketing/Providers/MicrosoftAds.ts +1 -1
- package/Server/Utils/Marketing/Providers/Reddit.ts +1 -1
- package/Server/Utils/Monitor/Criteria/APIRequestCriteria.ts +48 -33
- package/Server/Utils/Monitor/Criteria/DnsMonitorCriteria.ts +35 -30
- package/Server/Utils/Monitor/Criteria/DomainMonitorCriteria.ts +33 -28
- package/Server/Utils/Monitor/Criteria/EvaluateOverTime.ts +475 -47
- package/Server/Utils/Monitor/Criteria/ExternalStatusPageMonitorCriteria.ts +35 -30
- package/Server/Utils/Monitor/Criteria/IncomingRequestCriteria.ts +34 -28
- package/Server/Utils/Monitor/Criteria/SSLMonitorCriteria.ts +33 -28
- package/Server/Utils/Monitor/Criteria/ServerMonitorCriteria.ts +69 -40
- package/Server/Utils/Monitor/Criteria/SnmpMonitorCriteria.ts +35 -29
- package/Server/Utils/Monitor/MonitorCriteriaEvaluator.ts +8 -0
- package/Server/Utils/Monitor/MonitorResource.ts +161 -21
- package/Server/Utils/StatusPageContentSecurityPolicy.ts +56 -0
- package/Server/Utils/StatusPageCustomizationAccess.ts +117 -0
- package/Tests/Server/API/StatusPageCustomizationOrigin.test.ts +348 -0
- package/Tests/Server/Infrastructure/Postgres/FixTotpOtpUrlAlgorithmMigration.test.ts +9 -59
- package/Tests/Server/Infrastructure/Postgres/SchemaMigrationsOrdering.test.ts +227 -0
- package/Tests/Server/Middleware/SlackAuthorization.test.ts +149 -0
- package/Tests/Server/Middleware/UserAuthorization.test.ts +27 -13
- package/Tests/Server/Middleware/UserAuthorizationGlobalSsoGovernance.test.ts +1455 -0
- package/Tests/Server/Middleware/UserAuthorizationSSOProvider.test.ts +91 -2
- package/Tests/Server/Middleware/WhatsAppAuthorization.test.ts +233 -0
- package/Tests/Server/Services/DiscoveryScanClaimHookFreeSafety.test.ts +21 -8
- package/Tests/Server/Services/GlobalSsoProviderAuthorization.test.ts +1242 -0
- package/Tests/Server/Services/MonitorResourceProbeAgreementHydration.test.ts +459 -0
- package/Tests/Server/Services/MonitorResourceProbeAgreementReuse.test.ts +10 -0
- package/Tests/Server/Types/AnalyticsDatabase/AggregateUtilBucketTimestamp.test.ts +416 -0
- package/Tests/Server/Types/Database/QueryHelperManyToManyAndEmptyValues.test.ts +551 -0
- package/Tests/Server/Types/Database/SelectUtil.test.ts +348 -0
- package/Tests/Server/Utils/CronTab.test.ts +116 -0
- package/Tests/Server/Utils/GlobalSSOToken.test.ts +581 -0
- package/Tests/Server/Utils/GlobalSsoAuthorization.test.ts +1401 -0
- package/Tests/Server/Utils/Marketing/AdUploadableConversionTypes.test.ts +254 -0
- package/Tests/Server/Utils/Marketing/ConversionUploadProvider.test.ts +1 -1
- package/Tests/Server/Utils/Monitor/Criteria/APIRequestCriteriaEvaluateOverTime.test.ts +508 -0
- package/Tests/Server/Utils/Monitor/Criteria/APIRequestCriteriaPortTimings.test.ts +30 -9
- package/Tests/Server/Utils/Monitor/Criteria/EvaluateOverTime.test.ts +987 -0
- package/Tests/Server/Utils/Monitor/Criteria/ExternalStatusPageMonitorCriteria.test.ts +916 -0
- package/Tests/Server/Utils/Monitor/Criteria/ProfileMonitorCriteria.test.ts +382 -0
- package/Tests/Server/Utils/Monitor/Criteria/ServerMonitorCriteria.test.ts +1219 -0
- package/Tests/Server/Utils/Monitor/MonitorResourceIngestedStepSelection.test.ts +86 -0
- package/Tests/Server/Utils/StatusPageContentSecurityPolicy.test.ts +136 -0
- package/Tests/Server/Utils/StatusPageCustomizationAccess.test.ts +223 -0
- package/Tests/Types/Monitor/MonitorStepConfigHelpers.test.ts +165 -0
- package/Tests/Types/NetworkDevice/NetworkDeviceLinkRuleScope.test.ts +215 -0
- package/Tests/UI/Components/Forms/FormSubmitAnalytics.test.tsx +206 -0
- package/Tests/UI/Utils/AIChatExport/ConversationMarkdown.test.ts +774 -0
- package/Tests/UI/Utils/AIChatExport/MarkdownBlocks.test.ts +791 -0
- package/Tests/UI/Utils/AIChatExport/MarkdownSafety.test.ts +544 -0
- package/Tests/Utils/Analytics.test.ts +187 -0
- package/Tests/Utils/Monitor/MonitorMetricType.test.ts +80 -0
- package/Tests/Utils/Monitor/NetworkDeviceLinkRuleUtil.test.ts +1291 -0
- package/Tests/Utils/NetworkDiscovery/UnclaimedScanDiagnosis.test.ts +214 -0
- package/Types/Marketing/MarketingConversion.ts +12 -0
- package/Types/Monitor/CriteriaFilter.ts +9 -0
- package/Types/NetworkDevice/NetworkDeviceLinkRuleScope.ts +51 -0
- package/UI/Components/FormModal/BasicFormModal.tsx +12 -0
- package/UI/Components/Forms/BasicForm.tsx +13 -1
- package/UI/Components/Forms/BasicModelForm.tsx +2 -1
- package/UI/Components/Forms/ModelForm.tsx +10 -1
- package/UI/Components/Forms/Utils/FormAnalyticsName.ts +39 -0
- package/UI/Components/ModelFormModal/ModelFormModal.tsx +6 -1
- package/Utils/Analytics.ts +55 -2
- package/Utils/Monitor/MonitorMetricType.ts +40 -0
- package/Utils/Monitor/NetworkDeviceLinkRuleUtil.ts +587 -37
- package/Utils/NetworkDiscovery/UnclaimedScanDiagnosis.ts +115 -0
- package/build/dist/Models/DatabaseModels/GlobalOidc.js +21 -0
- package/build/dist/Models/DatabaseModels/GlobalOidc.js.map +1 -1
- package/build/dist/Models/DatabaseModels/GlobalSso.js +21 -0
- package/build/dist/Models/DatabaseModels/GlobalSso.js.map +1 -1
- package/build/dist/Models/DatabaseModels/MarketingConversion.js +20 -10
- package/build/dist/Models/DatabaseModels/MarketingConversion.js.map +1 -1
- package/build/dist/Models/DatabaseModels/NetworkDeviceLinkRule.js +47 -0
- package/build/dist/Models/DatabaseModels/NetworkDeviceLinkRule.js.map +1 -1
- package/build/dist/Models/DatabaseModels/StatusPage.js +4 -4
- package/build/dist/Models/DatabaseModels/StatusPage.js.map +1 -1
- package/build/dist/Server/API/StatusPageAPI.js +25 -4
- package/build/dist/Server/API/StatusPageAPI.js.map +1 -1
- package/build/dist/Server/EnvironmentConfig.js +6 -0
- package/build/dist/Server/EnvironmentConfig.js.map +1 -1
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1787800000000-AddScopeToNetworkDeviceLinkRule.js +12 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1787800000000-AddScopeToNetworkDeviceLinkRule.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1787900000000-AddRestrictToAttachedProjectsToGlobalSso.js +29 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1787900000000-AddRestrictToAttachedProjectsToGlobalSso.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +4 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
- package/build/dist/Server/Middleware/SlackAuthorization.js +28 -4
- package/build/dist/Server/Middleware/SlackAuthorization.js.map +1 -1
- package/build/dist/Server/Middleware/UserAuthorization.js +197 -22
- package/build/dist/Server/Middleware/UserAuthorization.js.map +1 -1
- package/build/dist/Server/Middleware/WhatsAppAuthorization.js +32 -2
- package/build/dist/Server/Middleware/WhatsAppAuthorization.js.map +1 -1
- package/build/dist/Server/Services/GlobalOidcProjectService.js +105 -0
- package/build/dist/Server/Services/GlobalOidcProjectService.js.map +1 -1
- package/build/dist/Server/Services/GlobalOidcService.js +127 -0
- package/build/dist/Server/Services/GlobalOidcService.js.map +1 -1
- package/build/dist/Server/Services/GlobalSsoProjectService.js +105 -0
- package/build/dist/Server/Services/GlobalSsoProjectService.js.map +1 -1
- package/build/dist/Server/Services/GlobalSsoService.js +127 -0
- package/build/dist/Server/Services/GlobalSsoService.js.map +1 -1
- package/build/dist/Server/Services/UserEmailService.js +4 -4
- package/build/dist/Server/Utils/CronTab.js +66 -0
- package/build/dist/Server/Utils/CronTab.js.map +1 -1
- package/build/dist/Server/Utils/GlobalSsoAuthorization.js +112 -0
- package/build/dist/Server/Utils/GlobalSsoAuthorization.js.map +1 -0
- package/build/dist/Server/Utils/Marketing/ConversionUploadProvider.js +17 -1
- package/build/dist/Server/Utils/Marketing/ConversionUploadProvider.js.map +1 -1
- package/build/dist/Server/Utils/Marketing/Providers/GoogleAds.js +1 -1
- package/build/dist/Server/Utils/Marketing/Providers/GoogleAds.js.map +1 -1
- package/build/dist/Server/Utils/Marketing/Providers/LinkedIn.js +1 -1
- package/build/dist/Server/Utils/Marketing/Providers/LinkedIn.js.map +1 -1
- package/build/dist/Server/Utils/Marketing/Providers/Meta.js +1 -1
- package/build/dist/Server/Utils/Marketing/Providers/Meta.js.map +1 -1
- package/build/dist/Server/Utils/Marketing/Providers/MicrosoftAds.js +1 -1
- package/build/dist/Server/Utils/Marketing/Providers/MicrosoftAds.js.map +1 -1
- package/build/dist/Server/Utils/Marketing/Providers/Reddit.js +1 -1
- package/build/dist/Server/Utils/Marketing/Providers/Reddit.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/Criteria/APIRequestCriteria.js +37 -35
- package/build/dist/Server/Utils/Monitor/Criteria/APIRequestCriteria.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/Criteria/DnsMonitorCriteria.js +21 -25
- package/build/dist/Server/Utils/Monitor/Criteria/DnsMonitorCriteria.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/Criteria/DomainMonitorCriteria.js +20 -22
- package/build/dist/Server/Utils/Monitor/Criteria/DomainMonitorCriteria.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/Criteria/EvaluateOverTime.js +370 -42
- package/build/dist/Server/Utils/Monitor/Criteria/EvaluateOverTime.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/Criteria/ExternalStatusPageMonitorCriteria.js +21 -25
- package/build/dist/Server/Utils/Monitor/Criteria/ExternalStatusPageMonitorCriteria.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/Criteria/IncomingRequestCriteria.js +21 -25
- package/build/dist/Server/Utils/Monitor/Criteria/IncomingRequestCriteria.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/Criteria/SSLMonitorCriteria.js +21 -25
- package/build/dist/Server/Utils/Monitor/Criteria/SSLMonitorCriteria.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/Criteria/ServerMonitorCriteria.js +64 -52
- package/build/dist/Server/Utils/Monitor/Criteria/ServerMonitorCriteria.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/Criteria/SnmpMonitorCriteria.js +25 -29
- package/build/dist/Server/Utils/Monitor/Criteria/SnmpMonitorCriteria.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/MonitorCriteriaEvaluator.js +8 -0
- package/build/dist/Server/Utils/Monitor/MonitorCriteriaEvaluator.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/MonitorResource.js +123 -23
- package/build/dist/Server/Utils/Monitor/MonitorResource.js.map +1 -1
- package/build/dist/Server/Utils/StatusPageContentSecurityPolicy.js +39 -0
- package/build/dist/Server/Utils/StatusPageContentSecurityPolicy.js.map +1 -0
- package/build/dist/Server/Utils/StatusPageCustomizationAccess.js +85 -0
- package/build/dist/Server/Utils/StatusPageCustomizationAccess.js.map +1 -0
- package/build/dist/Types/Marketing/MarketingConversion.js +10 -0
- package/build/dist/Types/Marketing/MarketingConversion.js.map +1 -1
- package/build/dist/Types/Monitor/CriteriaFilter.js +1 -0
- package/build/dist/Types/Monitor/CriteriaFilter.js.map +1 -1
- package/build/dist/Types/NetworkDevice/NetworkDeviceLinkRuleScope.js +46 -0
- package/build/dist/Types/NetworkDevice/NetworkDeviceLinkRuleScope.js.map +1 -0
- package/build/dist/UI/Components/FormModal/BasicFormModal.js +2 -1
- package/build/dist/UI/Components/FormModal/BasicFormModal.js.map +1 -1
- package/build/dist/UI/Components/Forms/BasicForm.js +10 -1
- package/build/dist/UI/Components/Forms/BasicForm.js.map +1 -1
- package/build/dist/UI/Components/Forms/BasicModelForm.js +2 -1
- package/build/dist/UI/Components/Forms/BasicModelForm.js.map +1 -1
- package/build/dist/UI/Components/Forms/ModelForm.js +4 -1
- package/build/dist/UI/Components/Forms/ModelForm.js.map +1 -1
- package/build/dist/UI/Components/Forms/Utils/FormAnalyticsName.js +33 -0
- package/build/dist/UI/Components/Forms/Utils/FormAnalyticsName.js.map +1 -0
- package/build/dist/UI/Components/ModelFormModal/ModelFormModal.js +2 -1
- package/build/dist/UI/Components/ModelFormModal/ModelFormModal.js.map +1 -1
- package/build/dist/Utils/Analytics.js +50 -2
- package/build/dist/Utils/Analytics.js.map +1 -1
- package/build/dist/Utils/Monitor/MonitorMetricType.js +37 -0
- package/build/dist/Utils/Monitor/MonitorMetricType.js.map +1 -1
- package/build/dist/Utils/Monitor/NetworkDeviceLinkRuleUtil.js +318 -37
- package/build/dist/Utils/Monitor/NetworkDeviceLinkRuleUtil.js.map +1 -1
- package/build/dist/Utils/NetworkDiscovery/UnclaimedScanDiagnosis.js +87 -0
- package/build/dist/Utils/NetworkDiscovery/UnclaimedScanDiagnosis.js.map +1 -0
- package/package.json +1 -1
|
@@ -27,6 +27,11 @@ import JSONFunctions from "../../Types/JSONFunctions";
|
|
|
27
27
|
import JSONWebTokenData from "../../Types/JsonWebTokenData";
|
|
28
28
|
import ObjectID from "../../Types/ObjectID";
|
|
29
29
|
import SsoProviderType from "../../Types/SSO/SsoProviderType";
|
|
30
|
+
import GlobalSsoService from "../Services/GlobalSsoService";
|
|
31
|
+
import { GlobalProviderTrust } from "../Utils/GlobalSsoAuthorization";
|
|
32
|
+
import GlobalOidcService from "../Services/GlobalOidcService";
|
|
33
|
+
import GlobalSsoProjectService from "../Services/GlobalSsoProjectService";
|
|
34
|
+
import GlobalOidcProjectService from "../Services/GlobalOidcProjectService";
|
|
30
35
|
import NotAuthorizedException from "../../Types/Exception/NotAuthorizedException";
|
|
31
36
|
import Permission, {
|
|
32
37
|
PermissionHelper,
|
|
@@ -234,8 +239,13 @@ export default class UserMiddleware {
|
|
|
234
239
|
}
|
|
235
240
|
}
|
|
236
241
|
|
|
242
|
+
/**
|
|
243
|
+
* Per-project SSO token check (Project SSO/OIDC login). Bound to one project,
|
|
244
|
+
* entirely stateless: signature, expiry, project, user, and the optional
|
|
245
|
+
* pinned-provider discriminator.
|
|
246
|
+
*/
|
|
237
247
|
@CaptureSpan()
|
|
238
|
-
public static
|
|
248
|
+
public static doesProjectScopedSsoTokenExist(
|
|
239
249
|
req: ExpressRequest,
|
|
240
250
|
projectId: ObjectID,
|
|
241
251
|
userId: ObjectID,
|
|
@@ -243,41 +253,241 @@ export default class UserMiddleware {
|
|
|
243
253
|
): boolean {
|
|
244
254
|
const ssoTokens: Dictionary<string> = this.getSsoTokens(req);
|
|
245
255
|
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
256
|
+
if (!ssoTokens || !ssoTokens[projectId.toString()]) {
|
|
257
|
+
return false;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
try {
|
|
250
261
|
const decodedData: JSONWebTokenData = JSONWebToken.decode(
|
|
251
262
|
ssoTokens[projectId.toString()] as string,
|
|
252
263
|
);
|
|
264
|
+
|
|
265
|
+
/*
|
|
266
|
+
* A Global-typed credential is NEVER accepted here, whichever slot it
|
|
267
|
+
* arrived in. Before the single-token redesign the Global SSO router
|
|
268
|
+
* minted one per-project token per project, typed GlobalSSO, signed for
|
|
269
|
+
* 30 days - and those are still sitting in browsers as `sso-<projectId>`
|
|
270
|
+
* cookies and in the mobile app's AsyncStorage, replayed on every
|
|
271
|
+
* request. Accepting them here would let a Global credential short-
|
|
272
|
+
* circuit past the provider-trust check, so disabling a provider would
|
|
273
|
+
* still not revoke them. Route every Global token through the stateful
|
|
274
|
+
* path instead.
|
|
275
|
+
*/
|
|
253
276
|
if (
|
|
277
|
+
decodedData.ssoProviderType === SsoProviderType.GlobalSSO ||
|
|
278
|
+
decodedData.ssoProviderType === SsoProviderType.GlobalOIDC
|
|
279
|
+
) {
|
|
280
|
+
return false;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
return (
|
|
254
284
|
decodedData.projectId?.toString() === projectId.toString() &&
|
|
255
285
|
decodedData.userId.toString() === userId.toString() &&
|
|
256
286
|
this.isSsoProviderSatisfied(decodedData, requiredSsoProviderId)
|
|
257
|
-
)
|
|
258
|
-
|
|
259
|
-
|
|
287
|
+
);
|
|
288
|
+
} catch {
|
|
289
|
+
/*
|
|
290
|
+
* A token that expires between `getSsoTokens` decoding it and this call
|
|
291
|
+
* throws out of `decode`. Swallowing it here means the request falls
|
|
292
|
+
* through to the Global SSO token instead of 500-ing, which is what a
|
|
293
|
+
* user with both kinds would expect.
|
|
294
|
+
*/
|
|
295
|
+
return false;
|
|
260
296
|
}
|
|
297
|
+
}
|
|
261
298
|
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
299
|
+
/**
|
|
300
|
+
* The Global SSO token, if one is present and stateless-valid for this user.
|
|
301
|
+
*
|
|
302
|
+
* "Stateless-valid" means signature, expiry, Global provider type, matching
|
|
303
|
+
* user, and the project's pinned-provider discriminator. It deliberately
|
|
304
|
+
* says nothing about whether the provider still exists, is still enabled, or
|
|
305
|
+
* governs the project in question - those need the database, and live in
|
|
306
|
+
* `isGlobalSsoTokenAuthorizedForProject`.
|
|
307
|
+
*/
|
|
308
|
+
@CaptureSpan()
|
|
309
|
+
public static getStatelessValidGlobalSsoTokenData(
|
|
310
|
+
req: ExpressRequest,
|
|
311
|
+
userId: ObjectID,
|
|
312
|
+
requiredSsoProviderId?: ObjectID | undefined,
|
|
313
|
+
): JSONWebTokenData | null {
|
|
269
314
|
const globalSsoTokenData: JSONWebTokenData | null =
|
|
270
315
|
this.getGlobalSsoTokenData(req);
|
|
271
316
|
|
|
317
|
+
if (!globalSsoTokenData) {
|
|
318
|
+
return null;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
if (globalSsoTokenData.userId.toString() !== userId.toString()) {
|
|
322
|
+
return null;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
if (
|
|
326
|
+
!this.isSsoProviderSatisfied(globalSsoTokenData, requiredSsoProviderId)
|
|
327
|
+
) {
|
|
328
|
+
return null;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
return globalSsoTokenData;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* The stateless half of the whole decision, kept as one call because it is
|
|
336
|
+
* the part that can be reasoned about without a database.
|
|
337
|
+
*
|
|
338
|
+
* NOTE: this is NOT the enforcement entry point. A Global SSO token that
|
|
339
|
+
* passes here can still be refused by `isSsoSatisfiedForProject`, which also
|
|
340
|
+
* asks whether the provider is still trusted and whether it governs this
|
|
341
|
+
* project. Enforcement must call that.
|
|
342
|
+
*/
|
|
343
|
+
@CaptureSpan()
|
|
344
|
+
public static doesSsoTokenForProjectExist(
|
|
345
|
+
req: ExpressRequest,
|
|
346
|
+
projectId: ObjectID,
|
|
347
|
+
userId: ObjectID,
|
|
348
|
+
requiredSsoProviderId?: ObjectID | undefined,
|
|
349
|
+
): boolean {
|
|
350
|
+
if (
|
|
351
|
+
this.doesProjectScopedSsoTokenExist(
|
|
352
|
+
req,
|
|
353
|
+
projectId,
|
|
354
|
+
userId,
|
|
355
|
+
requiredSsoProviderId,
|
|
356
|
+
)
|
|
357
|
+
) {
|
|
358
|
+
return true;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
return Boolean(
|
|
362
|
+
this.getStatelessValidGlobalSsoTokenData(
|
|
363
|
+
req,
|
|
364
|
+
userId,
|
|
365
|
+
requiredSsoProviderId,
|
|
366
|
+
),
|
|
367
|
+
);
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
/**
|
|
371
|
+
* Whether a stateless-valid Global SSO token is STILL authorized for this
|
|
372
|
+
* project right now.
|
|
373
|
+
*
|
|
374
|
+
* Two questions the token itself cannot answer:
|
|
375
|
+
*
|
|
376
|
+
* 1. IS THE PROVIDER STILL TRUSTED? Always checked. A Global SSO token
|
|
377
|
+
* lives for 30 days and carries no revocation, so without this an admin
|
|
378
|
+
* turning a provider off - or deleting it outright - changes nothing
|
|
379
|
+
* for anyone already signed in, for up to a month.
|
|
380
|
+
*
|
|
381
|
+
* 2. DOES THE PROVIDER GOVERN THIS PROJECT? Only checked when the admin
|
|
382
|
+
* turned on `restrictToAttachedProjects` for that provider. The
|
|
383
|
+
* attachment rows are the PROVISIONING allow-list by default, and the
|
|
384
|
+
* login routers grant a session on membership of ANY project - so
|
|
385
|
+
* treating attachments as an access boundary unasked would deny users
|
|
386
|
+
* projects they legitimately reach today, with nothing in the product
|
|
387
|
+
* to recover with.
|
|
388
|
+
*
|
|
389
|
+
* Answers are cached in-process for 60s and concurrent misses share one
|
|
390
|
+
* query (Common/Server/Utils/GlobalSsoAuthorization.ts).
|
|
391
|
+
*
|
|
392
|
+
* THROWS rather than denying when the lookup itself fails. "This provider is
|
|
393
|
+
* not allowed here" and "we could not find out" are different answers, and
|
|
394
|
+
* conflating them lets a database blip look like a permission decision - on
|
|
395
|
+
* the multi-tenant path that would silently hand back a 200 with no
|
|
396
|
+
* permissions rather than an error anyone would notice.
|
|
397
|
+
*/
|
|
398
|
+
@CaptureSpan()
|
|
399
|
+
public static async isGlobalSsoTokenAuthorizedForProject(data: {
|
|
400
|
+
globalSsoTokenData: JSONWebTokenData;
|
|
401
|
+
projectId: ObjectID;
|
|
402
|
+
}): Promise<boolean> {
|
|
403
|
+
const { globalSsoTokenData, projectId } = data;
|
|
404
|
+
|
|
405
|
+
const providerIdValue: string | undefined =
|
|
406
|
+
globalSsoTokenData.ssoProviderId?.toString();
|
|
407
|
+
|
|
408
|
+
if (!providerIdValue) {
|
|
409
|
+
/*
|
|
410
|
+
* A Global-typed token with no provider id cannot be checked against
|
|
411
|
+
* either question, so it cannot be trusted for a project. This is a
|
|
412
|
+
* decision, not a failure - do not throw.
|
|
413
|
+
*/
|
|
414
|
+
return false;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
const providerId: ObjectID = new ObjectID(providerIdValue);
|
|
418
|
+
|
|
419
|
+
const isOidc: boolean =
|
|
420
|
+
globalSsoTokenData.ssoProviderType === SsoProviderType.GlobalOIDC;
|
|
421
|
+
|
|
422
|
+
const trust: GlobalProviderTrust = isOidc
|
|
423
|
+
? await GlobalOidcService.getProviderTrust(providerId)
|
|
424
|
+
: await GlobalSsoService.getProviderTrust(providerId);
|
|
425
|
+
|
|
426
|
+
if (!trust.isUsable) {
|
|
427
|
+
return false;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
if (!trust.restrictToAttachedProjects) {
|
|
431
|
+
/*
|
|
432
|
+
* The default, and what every existing installation gets: a global login
|
|
433
|
+
* satisfies enforcement for every project the user belongs to.
|
|
434
|
+
*/
|
|
435
|
+
return true;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
return isOidc
|
|
439
|
+
? GlobalOidcProjectService.doesProviderGovernProject({
|
|
440
|
+
globalOidcId: providerId,
|
|
441
|
+
projectId,
|
|
442
|
+
})
|
|
443
|
+
: GlobalSsoProjectService.doesProviderGovernProject({
|
|
444
|
+
globalSsoId: providerId,
|
|
445
|
+
projectId,
|
|
446
|
+
});
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
/**
|
|
450
|
+
* THE enforcement entry point: is this request's SSO requirement satisfied
|
|
451
|
+
* for this project?
|
|
452
|
+
*
|
|
453
|
+
* A per-project token is decided statelessly. A Global SSO token additionally
|
|
454
|
+
* has to survive the provider-trust and project-governance checks above.
|
|
455
|
+
*/
|
|
456
|
+
@CaptureSpan()
|
|
457
|
+
public static async isSsoSatisfiedForProject(data: {
|
|
458
|
+
req: ExpressRequest;
|
|
459
|
+
projectId: ObjectID;
|
|
460
|
+
userId: ObjectID;
|
|
461
|
+
requiredSsoProviderId?: ObjectID | undefined;
|
|
462
|
+
}): Promise<boolean> {
|
|
463
|
+
const { req, projectId, userId, requiredSsoProviderId } = data;
|
|
464
|
+
|
|
272
465
|
if (
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
466
|
+
this.doesProjectScopedSsoTokenExist(
|
|
467
|
+
req,
|
|
468
|
+
projectId,
|
|
469
|
+
userId,
|
|
470
|
+
requiredSsoProviderId,
|
|
471
|
+
)
|
|
276
472
|
) {
|
|
277
473
|
return true;
|
|
278
474
|
}
|
|
279
475
|
|
|
280
|
-
|
|
476
|
+
const globalSsoTokenData: JSONWebTokenData | null =
|
|
477
|
+
this.getStatelessValidGlobalSsoTokenData(
|
|
478
|
+
req,
|
|
479
|
+
userId,
|
|
480
|
+
requiredSsoProviderId,
|
|
481
|
+
);
|
|
482
|
+
|
|
483
|
+
if (!globalSsoTokenData) {
|
|
484
|
+
return false;
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
return this.isGlobalSsoTokenAuthorizedForProject({
|
|
488
|
+
globalSsoTokenData,
|
|
489
|
+
projectId,
|
|
490
|
+
});
|
|
281
491
|
}
|
|
282
492
|
|
|
283
493
|
@CaptureSpan()
|
|
@@ -646,12 +856,12 @@ export default class UserMiddleware {
|
|
|
646
856
|
);
|
|
647
857
|
|
|
648
858
|
if (
|
|
649
|
-
!UserMiddleware.
|
|
859
|
+
!(await UserMiddleware.isSsoSatisfiedForProject({
|
|
650
860
|
req,
|
|
651
|
-
tenantId,
|
|
861
|
+
projectId: tenantId,
|
|
652
862
|
userId,
|
|
653
|
-
requiredSsoProviderId ?? undefined,
|
|
654
|
-
)
|
|
863
|
+
requiredSsoProviderId: requiredSsoProviderId ?? undefined,
|
|
864
|
+
}))
|
|
655
865
|
) {
|
|
656
866
|
throw new SsoAuthorizationException();
|
|
657
867
|
}
|
|
@@ -714,12 +924,12 @@ export default class UserMiddleware {
|
|
|
714
924
|
});
|
|
715
925
|
|
|
716
926
|
if (
|
|
717
|
-
!UserMiddleware.
|
|
927
|
+
!(await UserMiddleware.isSsoSatisfiedForProject({
|
|
718
928
|
req,
|
|
719
929
|
projectId,
|
|
720
930
|
userId,
|
|
721
|
-
requiredSsoProviderId ?? undefined,
|
|
722
|
-
)
|
|
931
|
+
requiredSsoProviderId: requiredSsoProviderId ?? undefined,
|
|
932
|
+
}))
|
|
723
933
|
) {
|
|
724
934
|
return {
|
|
725
935
|
projectId,
|
|
@@ -12,6 +12,19 @@ import crypto from "crypto";
|
|
|
12
12
|
import logger, { getLogAttributesFromRequest } from "../Utils/Logger";
|
|
13
13
|
import CaptureSpan from "../Utils/Telemetry/CaptureSpan";
|
|
14
14
|
|
|
15
|
+
/*
|
|
16
|
+
* Meta sends the digest as "sha256=" followed by the 64 hex characters of a
|
|
17
|
+
* SHA-256 HMAC. The header is attacker-controlled and the endpoint is
|
|
18
|
+
* unauthenticated by design - the signature IS the authentication - so the
|
|
19
|
+
* shape has to be checked before the value reaches crypto.timingSafeEqual,
|
|
20
|
+
* which throws RangeError on a length mismatch rather than returning false.
|
|
21
|
+
* Without the guard, any unauthenticated request carrying a signature header
|
|
22
|
+
* of some other length threw out of the middleware instead of getting the
|
|
23
|
+
* intended 400.
|
|
24
|
+
*/
|
|
25
|
+
const SIGNATURE_PREFIX: string = "sha256=";
|
|
26
|
+
const HEX_DIGEST_REGEX: RegExp = /^[a-f0-9]{64}$/i;
|
|
27
|
+
|
|
15
28
|
export default class WhatsAppAuthorization {
|
|
16
29
|
@CaptureSpan()
|
|
17
30
|
public static async isAuthorizedWhatsAppRequest(
|
|
@@ -40,6 +53,26 @@ export default class WhatsAppAuthorization {
|
|
|
40
53
|
);
|
|
41
54
|
}
|
|
42
55
|
|
|
56
|
+
/*
|
|
57
|
+
* Checked up front, before the config lookup: a request whose signature
|
|
58
|
+
* cannot possibly verify should not cost a database round trip.
|
|
59
|
+
*/
|
|
60
|
+
const providedDigest: string = signature.startsWith(SIGNATURE_PREFIX)
|
|
61
|
+
? signature.slice(SIGNATURE_PREFIX.length)
|
|
62
|
+
: "";
|
|
63
|
+
|
|
64
|
+
if (!HEX_DIGEST_REGEX.test(providedDigest)) {
|
|
65
|
+
logger.error(
|
|
66
|
+
"WhatsApp webhook request has a malformed X-Hub-Signature-256 header.",
|
|
67
|
+
getLogAttributesFromRequest(req),
|
|
68
|
+
);
|
|
69
|
+
return Response.sendErrorResponse(
|
|
70
|
+
req,
|
|
71
|
+
res,
|
|
72
|
+
new BadDataException("WhatsApp webhook signature verification failed."),
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
|
|
43
76
|
const globalConfig: GlobalConfig | null =
|
|
44
77
|
await GlobalConfigService.findOneBy({
|
|
45
78
|
query: {
|
|
@@ -70,12 +103,19 @@ export default class WhatsAppAuthorization {
|
|
|
70
103
|
|
|
71
104
|
const rawBody: string = req.rawBody || "";
|
|
72
105
|
|
|
73
|
-
const
|
|
106
|
+
const expectedDigest: string = crypto
|
|
107
|
+
.createHmac("sha256", appSecret)
|
|
108
|
+
.update(rawBody)
|
|
109
|
+
.digest("hex");
|
|
74
110
|
|
|
111
|
+
/*
|
|
112
|
+
* Both sides are decoded from 64 validated hex characters, so both
|
|
113
|
+
* buffers are 32 bytes and timingSafeEqual cannot throw here.
|
|
114
|
+
*/
|
|
75
115
|
if (
|
|
76
116
|
!crypto.timingSafeEqual(
|
|
77
|
-
Buffer.from(
|
|
78
|
-
Buffer.from(
|
|
117
|
+
Buffer.from(expectedDigest, "hex") as Uint8Array,
|
|
118
|
+
Buffer.from(providedDigest, "hex") as Uint8Array,
|
|
79
119
|
)
|
|
80
120
|
) {
|
|
81
121
|
logger.error(
|
|
@@ -4,18 +4,138 @@ import Team from "../../Models/DatabaseModels/Team";
|
|
|
4
4
|
import { LIMIT_PER_PROJECT } from "../../Types/Database/LimitMax";
|
|
5
5
|
import ObjectID from "../../Types/ObjectID";
|
|
6
6
|
import CreateBy from "../Types/Database/CreateBy";
|
|
7
|
-
import { OnCreate, OnUpdate } from "../Types/Database/Hooks";
|
|
7
|
+
import { OnCreate, OnDelete, OnUpdate } from "../Types/Database/Hooks";
|
|
8
8
|
import UpdateBy from "../Types/Database/UpdateBy";
|
|
9
9
|
import CaptureSpan from "../Utils/Telemetry/CaptureSpan";
|
|
10
10
|
import validateGlobalProviderProjectTeams, {
|
|
11
11
|
resolveAttachmentProjectId,
|
|
12
12
|
} from "../Utils/ValidateGlobalProviderProjectTeams";
|
|
13
|
+
import {
|
|
14
|
+
GLOBAL_SSO_AUTHORIZATION_CACHE_TTL_MS,
|
|
15
|
+
GlobalProviderAttachments,
|
|
16
|
+
clearGlobalSsoAuthorizationCaches,
|
|
17
|
+
doAttachmentsGovernProject,
|
|
18
|
+
globalProviderCacheKey,
|
|
19
|
+
globalSsoAttachmentsCache,
|
|
20
|
+
loadAttachmentsOnce,
|
|
21
|
+
} from "../Utils/GlobalSsoAuthorization";
|
|
22
|
+
import DeleteBy from "../Types/Database/DeleteBy";
|
|
13
23
|
|
|
14
24
|
export class Service extends DatabaseService<Model> {
|
|
15
25
|
public constructor() {
|
|
16
26
|
super(Model);
|
|
17
27
|
}
|
|
18
28
|
|
|
29
|
+
/**
|
|
30
|
+
* Whether this provider's attachments cover `projectId`.
|
|
31
|
+
*
|
|
32
|
+
* Only consulted when the provider has `restrictToAttachedProjects` on -
|
|
33
|
+
* attachments are the PROVISIONING allow-list by default, not an access
|
|
34
|
+
* boundary, and reading them as one without the admin asking would lock
|
|
35
|
+
* existing users out of projects they legitimately reach.
|
|
36
|
+
*
|
|
37
|
+
* "No attachment rows at all" means instance-wide, matching the login
|
|
38
|
+
* router's default-all mode. "Rows exist but none are enabled" is a
|
|
39
|
+
* different answer and denies - otherwise an admin disabling the last
|
|
40
|
+
* attachment would WIDEN the provider to every project.
|
|
41
|
+
*
|
|
42
|
+
* Cached for 60s, with concurrent misses sharing one query.
|
|
43
|
+
*/
|
|
44
|
+
@CaptureSpan()
|
|
45
|
+
public async doesProviderGovernProject(data: {
|
|
46
|
+
globalOidcId: ObjectID;
|
|
47
|
+
projectId: ObjectID;
|
|
48
|
+
}): Promise<boolean> {
|
|
49
|
+
const key: string = globalProviderCacheKey("oidc", data.globalOidcId);
|
|
50
|
+
|
|
51
|
+
const cached: GlobalProviderAttachments | undefined =
|
|
52
|
+
globalSsoAttachmentsCache.get(key);
|
|
53
|
+
|
|
54
|
+
if (cached !== undefined) {
|
|
55
|
+
return doAttachmentsGovernProject(cached, data.projectId);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const attachments: GlobalProviderAttachments = await loadAttachmentsOnce(
|
|
59
|
+
key,
|
|
60
|
+
async (): Promise<GlobalProviderAttachments> => {
|
|
61
|
+
/*
|
|
62
|
+
* Fetched WITHOUT the isEnabled filter so a disabled row still counts
|
|
63
|
+
* as "this provider has attachments"; the enabled ones are selected
|
|
64
|
+
* out below.
|
|
65
|
+
*/
|
|
66
|
+
const rows: Array<Model> = await this.findBy({
|
|
67
|
+
query: { globalOidcId: data.globalOidcId },
|
|
68
|
+
select: { _id: true, projectId: true, isEnabled: true },
|
|
69
|
+
limit: LIMIT_PER_PROJECT,
|
|
70
|
+
skip: 0,
|
|
71
|
+
props: { isRoot: true },
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
const loaded: GlobalProviderAttachments = {
|
|
75
|
+
hasAnyAttachmentRows: rows.length > 0,
|
|
76
|
+
enabledProjectIds: rows
|
|
77
|
+
.filter((row: Model) => {
|
|
78
|
+
return Boolean(row.isEnabled) && Boolean(row.projectId);
|
|
79
|
+
})
|
|
80
|
+
.map((row: Model) => {
|
|
81
|
+
return row.projectId!.toString();
|
|
82
|
+
}),
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
globalSsoAttachmentsCache.set(
|
|
86
|
+
key,
|
|
87
|
+
loaded,
|
|
88
|
+
GLOBAL_SSO_AUTHORIZATION_CACHE_TTL_MS,
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
return loaded;
|
|
92
|
+
},
|
|
93
|
+
);
|
|
94
|
+
|
|
95
|
+
return doAttachmentsGovernProject(attachments, data.projectId);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
@CaptureSpan()
|
|
99
|
+
protected override async onBeforeDelete(
|
|
100
|
+
deleteBy: DeleteBy<Model>,
|
|
101
|
+
): Promise<OnDelete<Model>> {
|
|
102
|
+
// Detaching a project has to take effect now, not in 60s, on this node.
|
|
103
|
+
clearGlobalSsoAuthorizationCaches();
|
|
104
|
+
return { deleteBy, carryForward: null };
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/*
|
|
108
|
+
* Cleared again AFTER the write commits. A clear that runs before the row
|
|
109
|
+
* lands can be immediately re-filled with the pre-change answer by a
|
|
110
|
+
* concurrent request, handing the old attachment set another full TTL.
|
|
111
|
+
*/
|
|
112
|
+
@CaptureSpan()
|
|
113
|
+
protected override async onDeleteSuccess(
|
|
114
|
+
onDelete: OnDelete<Model>,
|
|
115
|
+
_itemIdsBeforeDelete: Array<ObjectID>,
|
|
116
|
+
): Promise<OnDelete<Model>> {
|
|
117
|
+
clearGlobalSsoAuthorizationCaches();
|
|
118
|
+
return onDelete;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
@CaptureSpan()
|
|
122
|
+
protected override async onCreateSuccess(
|
|
123
|
+
_onCreate: OnCreate<Model>,
|
|
124
|
+
createdItem: Model,
|
|
125
|
+
): Promise<Model> {
|
|
126
|
+
clearGlobalSsoAuthorizationCaches();
|
|
127
|
+
return createdItem;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
@CaptureSpan()
|
|
131
|
+
protected override async onUpdateSuccess(
|
|
132
|
+
onUpdate: OnUpdate<Model>,
|
|
133
|
+
_updatedItemIds: Array<ObjectID>,
|
|
134
|
+
): Promise<OnUpdate<Model>> {
|
|
135
|
+
clearGlobalSsoAuthorizationCaches();
|
|
136
|
+
return onUpdate;
|
|
137
|
+
}
|
|
138
|
+
|
|
19
139
|
@CaptureSpan()
|
|
20
140
|
protected override async onBeforeCreate(
|
|
21
141
|
createBy: CreateBy<Model>,
|
|
@@ -38,6 +158,9 @@ export class Service extends DatabaseService<Model> {
|
|
|
38
158
|
projectId,
|
|
39
159
|
});
|
|
40
160
|
|
|
161
|
+
// A new attachment narrows (or widens) which projects the provider governs.
|
|
162
|
+
clearGlobalSsoAuthorizationCaches();
|
|
163
|
+
|
|
41
164
|
return { createBy, carryForward: null };
|
|
42
165
|
}
|
|
43
166
|
|
|
@@ -78,6 +201,8 @@ export class Service extends DatabaseService<Model> {
|
|
|
78
201
|
}
|
|
79
202
|
}
|
|
80
203
|
|
|
204
|
+
clearGlobalSsoAuthorizationCaches();
|
|
205
|
+
|
|
81
206
|
return { updateBy, carryForward: null };
|
|
82
207
|
}
|
|
83
208
|
}
|
|
@@ -1,10 +1,142 @@
|
|
|
1
1
|
import DatabaseService from "./DatabaseService";
|
|
2
2
|
import Model from "../../Models/DatabaseModels/GlobalOidc";
|
|
3
|
+
import ObjectID from "../../Types/ObjectID";
|
|
4
|
+
import CaptureSpan from "../Utils/Telemetry/CaptureSpan";
|
|
5
|
+
import CreateBy from "../Types/Database/CreateBy";
|
|
6
|
+
import DeleteBy from "../Types/Database/DeleteBy";
|
|
7
|
+
import UpdateBy from "../Types/Database/UpdateBy";
|
|
8
|
+
import { OnCreate, OnDelete, OnUpdate } from "../Types/Database/Hooks";
|
|
9
|
+
import {
|
|
10
|
+
GLOBAL_SSO_AUTHORIZATION_CACHE_TTL_MS,
|
|
11
|
+
GlobalProviderTrust,
|
|
12
|
+
clearGlobalSsoAuthorizationCaches,
|
|
13
|
+
globalProviderCacheKey,
|
|
14
|
+
globalSsoProviderTrustCache,
|
|
15
|
+
loadTrustOnce,
|
|
16
|
+
} from "../Utils/GlobalSsoAuthorization";
|
|
3
17
|
|
|
4
18
|
export class Service extends DatabaseService<Model> {
|
|
5
19
|
public constructor() {
|
|
6
20
|
super(Model);
|
|
7
21
|
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* What the SSO-enforcement middleware needs to know about this provider:
|
|
25
|
+
* whether it is still usable at all, and whether the admin opted it into
|
|
26
|
+
* attachment-scoped access.
|
|
27
|
+
*
|
|
28
|
+
* Called on every authenticated request against an SSO-enforced project, so
|
|
29
|
+
* the answer is cached for 60s and concurrent misses share one query.
|
|
30
|
+
* Without this lookup, turning a provider off leaves every token it ever
|
|
31
|
+
* issued working until each one expires - up to thirty days.
|
|
32
|
+
*/
|
|
33
|
+
@CaptureSpan()
|
|
34
|
+
public async getProviderTrust(
|
|
35
|
+
providerId: ObjectID,
|
|
36
|
+
): Promise<GlobalProviderTrust> {
|
|
37
|
+
const key: string = globalProviderCacheKey("oidc", providerId);
|
|
38
|
+
|
|
39
|
+
const cached: GlobalProviderTrust | undefined =
|
|
40
|
+
globalSsoProviderTrustCache.get(key);
|
|
41
|
+
|
|
42
|
+
if (cached !== undefined) {
|
|
43
|
+
return cached;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return loadTrustOnce(key, async (): Promise<GlobalProviderTrust> => {
|
|
47
|
+
const provider: Model | null = await this.findOneBy({
|
|
48
|
+
query: { _id: providerId.toString() },
|
|
49
|
+
select: {
|
|
50
|
+
_id: true,
|
|
51
|
+
isEnabled: true,
|
|
52
|
+
restrictToAttachedProjects: true,
|
|
53
|
+
},
|
|
54
|
+
props: { isRoot: true },
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
/*
|
|
58
|
+
* A deleted provider and a disabled one are the same answer: no. Both
|
|
59
|
+
* are cached, so a revoked provider does not cost a query per request.
|
|
60
|
+
*/
|
|
61
|
+
const trust: GlobalProviderTrust = {
|
|
62
|
+
isUsable: Boolean(provider && provider.isEnabled),
|
|
63
|
+
restrictToAttachedProjects: Boolean(
|
|
64
|
+
provider && provider.restrictToAttachedProjects,
|
|
65
|
+
),
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
globalSsoProviderTrustCache.set(
|
|
69
|
+
key,
|
|
70
|
+
trust,
|
|
71
|
+
GLOBAL_SSO_AUTHORIZATION_CACHE_TTL_MS,
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
return trust;
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/*
|
|
79
|
+
* Cache invalidation runs in the SUCCESS hooks, not the before-hooks: a
|
|
80
|
+
* clear that happens before the row is written can be immediately re-filled
|
|
81
|
+
* with the pre-change answer by a concurrent request, which would hand a
|
|
82
|
+
* disabled provider another full TTL of life on the very node that served
|
|
83
|
+
* the disable. Clearing in both is deliberate - the before-hook narrows the
|
|
84
|
+
* window, the success hook closes it.
|
|
85
|
+
*
|
|
86
|
+
* These caches are per-process, so the hooks make a change immediate on the
|
|
87
|
+
* node that served it and the TTL bounds every other node.
|
|
88
|
+
*/
|
|
89
|
+
|
|
90
|
+
@CaptureSpan()
|
|
91
|
+
protected override async onBeforeUpdate(
|
|
92
|
+
updateBy: UpdateBy<Model>,
|
|
93
|
+
): Promise<OnUpdate<Model>> {
|
|
94
|
+
clearGlobalSsoAuthorizationCaches();
|
|
95
|
+
return { updateBy, carryForward: null };
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
@CaptureSpan()
|
|
99
|
+
protected override async onUpdateSuccess(
|
|
100
|
+
onUpdate: OnUpdate<Model>,
|
|
101
|
+
_updatedItemIds: Array<ObjectID>,
|
|
102
|
+
): Promise<OnUpdate<Model>> {
|
|
103
|
+
clearGlobalSsoAuthorizationCaches();
|
|
104
|
+
return onUpdate;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
@CaptureSpan()
|
|
108
|
+
protected override async onBeforeDelete(
|
|
109
|
+
deleteBy: DeleteBy<Model>,
|
|
110
|
+
): Promise<OnDelete<Model>> {
|
|
111
|
+
clearGlobalSsoAuthorizationCaches();
|
|
112
|
+
return { deleteBy, carryForward: null };
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
@CaptureSpan()
|
|
116
|
+
protected override async onDeleteSuccess(
|
|
117
|
+
onDelete: OnDelete<Model>,
|
|
118
|
+
_itemIdsBeforeDelete: Array<ObjectID>,
|
|
119
|
+
): Promise<OnDelete<Model>> {
|
|
120
|
+
clearGlobalSsoAuthorizationCaches();
|
|
121
|
+
return onDelete;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
@CaptureSpan()
|
|
125
|
+
protected override async onBeforeCreate(
|
|
126
|
+
createBy: CreateBy<Model>,
|
|
127
|
+
): Promise<OnCreate<Model>> {
|
|
128
|
+
clearGlobalSsoAuthorizationCaches();
|
|
129
|
+
return { createBy, carryForward: null };
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
@CaptureSpan()
|
|
133
|
+
protected override async onCreateSuccess(
|
|
134
|
+
_onCreate: OnCreate<Model>,
|
|
135
|
+
createdItem: Model,
|
|
136
|
+
): Promise<Model> {
|
|
137
|
+
clearGlobalSsoAuthorizationCaches();
|
|
138
|
+
return createdItem;
|
|
139
|
+
}
|
|
8
140
|
}
|
|
9
141
|
|
|
10
142
|
export default new Service();
|