From 6541749f9b3b9d2d3de316437b4c0c4a34c938e7 Mon Sep 17 00:00:00 2001 From: Darcy Ye Date: Wed, 15 Jan 2025 10:00:16 +0800 Subject: [PATCH] refactor(core): add SAML app quota guard (#6941) --- packages/console/package.json | 2 +- .../console/src/consts/quota-item-phrases.ts | 4 ++++ packages/console/src/consts/tenants.ts | 2 ++ packages/core/package.json | 2 +- .../core/src/__mocks__/cloud-connection.ts | 1 + .../src/saml-applications/routes/index.ts | 4 ++++ packages/core/src/utils/subscription/types.ts | 1 + .../admin-console/subscription/quota-item.ts | 7 +++++++ pnpm-lock.yaml | 19 +++++++++++++++---- 9 files changed, 36 insertions(+), 6 deletions(-) diff --git a/packages/console/package.json b/packages/console/package.json index f72068d0d..03a126fa4 100644 --- a/packages/console/package.json +++ b/packages/console/package.json @@ -27,7 +27,7 @@ "devDependencies": { "@fontsource/roboto-mono": "^5.0.0", "@jest/types": "^29.5.0", - "@logto/cloud": "0.2.5-aac51e9", + "@logto/cloud": "0.2.5-c98a257", "@logto/connector-kit": "workspace:^4.1.0", "@logto/core-kit": "workspace:^2.5.2", "@logto/language-kit": "workspace:^1.1.0", diff --git a/packages/console/src/consts/quota-item-phrases.ts b/packages/console/src/consts/quota-item-phrases.ts index f36407447..f0ce97ac0 100644 --- a/packages/console/src/consts/quota-item-phrases.ts +++ b/packages/console/src/consts/quota-item-phrases.ts @@ -29,6 +29,7 @@ export const skuQuotaItemPhrasesMap: Record< subjectTokenEnabled: 'impersonation_enabled.name', bringYourUiEnabled: 'bring_your_ui_enabled.name', idpInitiatedSsoEnabled: 'idp_initiated_sso_enabled.name', + samlApplicationsLimit: 'saml_applications_limit.name', }; export const skuQuotaItemUnlimitedPhrasesMap: Record< @@ -57,6 +58,7 @@ export const skuQuotaItemUnlimitedPhrasesMap: Record< subjectTokenEnabled: 'impersonation_enabled.unlimited', bringYourUiEnabled: 'bring_your_ui_enabled.unlimited', idpInitiatedSsoEnabled: 'idp_initiated_sso_enabled.unlimited', + samlApplicationsLimit: 'saml_applications_limit.unlimited', }; export const skuQuotaItemLimitedPhrasesMap: Record< @@ -85,6 +87,7 @@ export const skuQuotaItemLimitedPhrasesMap: Record< subjectTokenEnabled: 'impersonation_enabled.limited', bringYourUiEnabled: 'bring_your_ui_enabled.limited', idpInitiatedSsoEnabled: 'idp_initiated_sso_enabled.limited', + samlApplicationsLimit: 'saml_applications_limit.limited', }; export const skuQuotaItemNotEligiblePhrasesMap: Record< @@ -113,5 +116,6 @@ export const skuQuotaItemNotEligiblePhrasesMap: Record< subjectTokenEnabled: 'impersonation_enabled.not_eligible', bringYourUiEnabled: 'bring_your_ui_enabled.not_eligible', idpInitiatedSsoEnabled: 'idp_initiated_sso_enabled.not_eligible', + samlApplicationsLimit: 'saml_applications_limit.not_eligible', }; /* === for new pricing model === */ diff --git a/packages/console/src/consts/tenants.ts b/packages/console/src/consts/tenants.ts index 0dd6f3b29..20c5bfc5d 100644 --- a/packages/console/src/consts/tenants.ts +++ b/packages/console/src/consts/tenants.ts @@ -109,6 +109,7 @@ export const defaultSubscriptionQuota: NewSubscriptionQuota = { subjectTokenEnabled: false, bringYourUiEnabled: false, idpInitiatedSsoEnabled: false, + samlApplicationsLimit: 0, }; export const defaultSubscriptionUsage: NewSubscriptionCountBasedUsage = { @@ -130,6 +131,7 @@ export const defaultSubscriptionUsage: NewSubscriptionCountBasedUsage = { subjectTokenEnabled: false, bringYourUiEnabled: false, idpInitiatedSsoEnabled: false, + samlApplicationsLimit: 0, }; const getAdminTenantEndpoint = () => { diff --git a/packages/core/package.json b/packages/core/package.json index 43137fc97..6cbb96b7d 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -99,7 +99,7 @@ "zod": "^3.23.8" }, "devDependencies": { - "@logto/cloud": "0.2.5-aac51e9", + "@logto/cloud": "0.2.5-c98a257", "@silverhand/eslint-config": "6.0.1", "@silverhand/ts-config": "6.0.0", "@types/adm-zip": "^0.5.5", diff --git a/packages/core/src/__mocks__/cloud-connection.ts b/packages/core/src/__mocks__/cloud-connection.ts index 4d012b084..34ce4f8f2 100644 --- a/packages/core/src/__mocks__/cloud-connection.ts +++ b/packages/core/src/__mocks__/cloud-connection.ts @@ -36,6 +36,7 @@ export const mockQuota = { subjectTokenEnabled: false, bringYourUiEnabled: false, idpInitiatedSsoEnabled: false, + samlApplicationsLimit: 0, }; export const mockSubscriptionData: Subscription = { diff --git a/packages/core/src/saml-applications/routes/index.ts b/packages/core/src/saml-applications/routes/index.ts index f523583a3..6bca1c6da 100644 --- a/packages/core/src/saml-applications/routes/index.ts +++ b/packages/core/src/saml-applications/routes/index.ts @@ -13,6 +13,7 @@ import { z } from 'zod'; import { EnvSet } from '#src/env-set/index.js'; import RequestError from '#src/errors/RequestError/index.js'; import koaGuard from '#src/middleware/koa-guard.js'; +import { koaQuotaGuard } from '#src/middleware/koa-quota-guard.js'; import { buildOidcClientMetadata } from '#src/oidc/utils.js'; import { generateInternalSecret } from '#src/routes/applications/application-secret.js'; import type { ManagementApiRouter, RouterInitArgs } from '#src/routes/types.js'; @@ -45,10 +46,12 @@ export default function samlApplicationRoutes( findSamlApplicationById, updateSamlApplicationById, }, + quota, } = libraries; router.post( '/saml-applications', + koaQuotaGuard({ key: 'samlApplicationsLimit', quota }), koaGuard({ body: samlApplicationCreateGuard, response: samlApplicationResponseGuard, @@ -178,6 +181,7 @@ export default function samlApplicationRoutes( router.post( '/saml-applications/:id/secrets', + koaQuotaGuard({ key: 'samlApplicationsLimit', quota }), koaGuard({ params: z.object({ id: z.string() }), // The life span of the SAML app secret is in years (at least 1 year), and for security concern, secrets which never expire are not recommended. diff --git a/packages/core/src/utils/subscription/types.ts b/packages/core/src/utils/subscription/types.ts index 236f082b4..c38490f4b 100644 --- a/packages/core/src/utils/subscription/types.ts +++ b/packages/core/src/utils/subscription/types.ts @@ -99,6 +99,7 @@ const logtoSkuQuotaGuard = z.object({ organizationsEnabled: z.boolean(), organizationsLimit: z.number().nullable(), idpInitiatedSsoEnabled: z.boolean(), + samlApplicationsLimit: z.number().nullable(), }) satisfies ToZodObject; /** diff --git a/packages/phrases/src/locales/en/translation/admin-console/subscription/quota-item.ts b/packages/phrases/src/locales/en/translation/admin-console/subscription/quota-item.ts index ba561515e..c2b07d9e6 100644 --- a/packages/phrases/src/locales/en/translation/admin-console/subscription/quota-item.ts +++ b/packages/phrases/src/locales/en/translation/admin-console/subscription/quota-item.ts @@ -177,6 +177,13 @@ const quota_item = { unlimited: 'IDP-initiated SSO', not_eligible: 'IDP-initiated SSO not allowed', }, + saml_applications_limit: { + name: 'SAML applications', + limited: '{{count, number}} SAML application', + limited_other: '{{count, number}} SAML applications', + unlimited: 'Unlimited SAML applications', + not_eligible: 'Remove your SAML applications', + }, }; export default Object.freeze(quota_item); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7f4619020..69aba8008 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -2858,8 +2858,8 @@ importers: specifier: ^29.5.0 version: 29.5.0 '@logto/cloud': - specifier: 0.2.5-aac51e9 - version: 0.2.5-aac51e9(zod@3.23.8) + specifier: 0.2.5-c98a257 + version: 0.2.5-c98a257(zod@3.23.8) '@logto/connector-kit': specifier: workspace:^4.1.0 version: link:../toolkit/connector-kit @@ -3354,8 +3354,8 @@ importers: version: 3.23.8 devDependencies: '@logto/cloud': - specifier: 0.2.5-aac51e9 - version: 0.2.5-aac51e9(zod@3.23.8) + specifier: 0.2.5-c98a257 + version: 0.2.5-c98a257(zod@3.23.8) '@silverhand/eslint-config': specifier: 6.0.1 version: 6.0.1(eslint@8.57.0)(prettier@3.0.0)(typescript@5.5.3) @@ -6206,6 +6206,10 @@ packages: resolution: {integrity: sha512-p/9u33xBFIuh6NRFO5D/G2rsvmZJAdnjjB6n8RxlMp34XHBuRRmiuB/KadKFQmUkPVJhDOo5hs9dgdd28ClQIA==} engines: {node: ^20.9.0} + '@logto/cloud@0.2.5-c98a257': + resolution: {integrity: sha512-/xXcTtYq3aCJUEADIRlbKZJKL345cHOvimSjq/DExYJXm7PkB3iaJAwlXRcmESkPxLom1o10WDQtuV9dPNVmzA==} + engines: {node: ^20.9.0} + '@logto/js@4.1.4': resolution: {integrity: sha512-6twud1nFBQmj89/aflzej6yD1QwXfPiYmRtyYuN4a7O9OaaW3X/kJBVwjKUn5NC9IUt+rd+jXsI3QJXENfaLAw==} @@ -16503,6 +16507,13 @@ snapshots: transitivePeerDependencies: - zod + '@logto/cloud@0.2.5-c98a257(zod@3.23.8)': + dependencies: + '@silverhand/essentials': 2.9.2 + '@withtyped/server': 0.14.0(zod@3.23.8) + transitivePeerDependencies: + - zod + '@logto/js@4.1.4': dependencies: '@silverhand/essentials': 2.9.2