From d0f1cad98c701fb2100bbff620fd541ec09e2345 Mon Sep 17 00:00:00 2001 From: Tuan Dang Date: Wed, 19 Jun 2024 13:59:47 -0700 Subject: [PATCH] Add support for identity-based pricing --- .../ldap-config/ldap-config-service.ts | 15 ++++++++ .../src/ee/services/license/license-dal.ts | 37 ++++++++++++++++++- .../ee/services/license/license-service.ts | 12 ++++-- .../saml-config/saml-config-service.ts | 15 ++++++++ .../src/services/identity/identity-service.ts | 17 ++++++++- 5 files changed, 89 insertions(+), 7 deletions(-) diff --git a/backend/src/ee/services/ldap-config/ldap-config-service.ts b/backend/src/ee/services/ldap-config/ldap-config-service.ts index dd49bd0aeb..546063132d 100644 --- a/backend/src/ee/services/ldap-config/ldap-config-service.ts +++ b/backend/src/ee/services/ldap-config/ldap-config-service.ts @@ -437,6 +437,21 @@ export const ldapConfigServiceFactory = ({ } }); } else { + const plan = await licenseService.getPlan(orgId); + if (plan?.memberLimit && plan.membersUsed >= plan.memberLimit) { + // limit imposed on number of members allowed / number of members used exceeds the number of members allowed + throw new BadRequestError({ + message: "Failed to create new member via LDAP due to member limit reached. Upgrade plan to add more members." + }); + } + + if (plan?.identityLimit && plan.identitiesUsed >= plan.identityLimit) { + // limit imposed on number of identities allowed / number of identities used exceeds the number of identities allowed + throw new BadRequestError({ + message: "Failed to create new member via LDAP due to member limit reached. Upgrade plan to add more members." + }); + } + userAlias = await userDAL.transaction(async (tx) => { let newUser: TUsers | undefined; if (serverCfg.trustSamlEmails) { diff --git a/backend/src/ee/services/license/license-dal.ts b/backend/src/ee/services/license/license-dal.ts index cf70488019..4f37a4f93f 100644 --- a/backend/src/ee/services/license/license-dal.ts +++ b/backend/src/ee/services/license/license-dal.ts @@ -19,11 +19,44 @@ export const licenseDALFactory = (db: TDbClient) => { .join(TableName.Users, `${TableName.OrgMembership}.userId`, `${TableName.Users}.id`) .where(`${TableName.Users}.isGhost`, false) .count(); - return doc?.[0].count; + return Number(doc?.[0].count); } catch (error) { throw new DatabaseError({ error, name: "Count of Org Members" }); } }; - return { countOfOrgMembers }; + const countOrgUsersAndIdentities = async (orgId: string | null, tx?: Knex) => { + try { + // count org users + const userDoc = await (tx || db)(TableName.OrgMembership) + .where({ status: OrgMembershipStatus.Accepted }) + .andWhere((bd) => { + if (orgId) { + void bd.where({ orgId }); + } + }) + .join(TableName.Users, `${TableName.OrgMembership}.userId`, `${TableName.Users}.id`) + .where(`${TableName.Users}.isGhost`, false) + .count(); + + const userCount = Number(userDoc?.[0].count); + + // count org identities + const identityDoc = await (tx || db)(TableName.IdentityOrgMembership) + .where((bd) => { + if (orgId) { + void bd.where({ orgId }); + } + }) + .count(); + + const identityCount = Number(identityDoc?.[0].count); + + return userCount + identityCount; + } catch (error) { + throw new DatabaseError({ error, name: "Count of Org Identities" }); + } + }; + + return { countOfOrgMembers, countOrgUsersAndIdentities }; }; diff --git a/backend/src/ee/services/license/license-service.ts b/backend/src/ee/services/license/license-service.ts index 0819c461b1..0b0fec53ab 100644 --- a/backend/src/ee/services/license/license-service.ts +++ b/backend/src/ee/services/license/license-service.ts @@ -205,16 +205,22 @@ export const licenseServiceFactory = ({ const org = await orgDAL.findOrgById(orgId); if (!org) throw new BadRequestError({ message: "Org not found" }); - const count = await licenseDAL.countOfOrgMembers(orgId); + const quantity = await licenseDAL.countOfOrgMembers(orgId); + const quantityIdentities = await licenseDAL.countOrgUsersAndIdentities(orgId); if (org?.customerId) { await licenseServerCloudApi.request.patch(`/api/license-server/v1/customers/${org.customerId}/cloud-plan`, { - quantity: count + quantity, + quantityIdentities }); } await keyStore.deleteItem(FEATURE_CACHE_KEY(orgId)); } else if (instanceType === InstanceType.EnterpriseOnPrem) { const usedSeats = await licenseDAL.countOfOrgMembers(null); - await licenseServerOnPremApi.request.patch(`/api/license/v1/license`, { usedSeats }); + const usedIdentitySeats = await licenseDAL.countOrgUsersAndIdentities(null); + await licenseServerOnPremApi.request.patch(`/api/license/v1/license`, { + usedSeats, + usedIdentitySeats + }); } await refreshPlan(orgId); }; diff --git a/backend/src/ee/services/saml-config/saml-config-service.ts b/backend/src/ee/services/saml-config/saml-config-service.ts index 5d7b7ec3b9..c4e6dc2bd5 100644 --- a/backend/src/ee/services/saml-config/saml-config-service.ts +++ b/backend/src/ee/services/saml-config/saml-config-service.ts @@ -377,6 +377,21 @@ export const samlConfigServiceFactory = ({ return foundUser; }); } else { + const plan = await licenseService.getPlan(orgId); + if (plan?.memberLimit && plan.membersUsed >= plan.memberLimit) { + // limit imposed on number of members allowed / number of members used exceeds the number of members allowed + throw new BadRequestError({ + message: "Failed to create new member via SAML due to member limit reached. Upgrade plan to add more members." + }); + } + + if (plan?.identityLimit && plan.identitiesUsed >= plan.identityLimit) { + // limit imposed on number of identities allowed / number of identities used exceeds the number of identities allowed + throw new BadRequestError({ + message: "Failed to create new member via SAML due to member limit reached. Upgrade plan to add more members." + }); + } + user = await userDAL.transaction(async (tx) => { let newUser: TUsers | undefined; if (serverCfg.trustSamlEmails) { diff --git a/backend/src/services/identity/identity-service.ts b/backend/src/services/identity/identity-service.ts index f5db1e9013..4659919fa8 100644 --- a/backend/src/services/identity/identity-service.ts +++ b/backend/src/services/identity/identity-service.ts @@ -17,7 +17,7 @@ type TIdentityServiceFactoryDep = { identityDAL: TIdentityDALFactory; identityOrgMembershipDAL: TIdentityOrgDALFactory; permissionService: Pick; - licenseService: Pick; + licenseService: Pick; }; export type TIdentityServiceFactory = ReturnType; @@ -25,7 +25,8 @@ export type TIdentityServiceFactory = ReturnType; export const identityServiceFactory = ({ identityDAL, identityOrgMembershipDAL, - permissionService + permissionService, + licenseService }: TIdentityServiceFactoryDep) => { const createIdentity = async ({ name, @@ -47,6 +48,14 @@ export const identityServiceFactory = ({ const hasRequiredPriviledges = isAtLeastAsPrivileged(permission, rolePermission); if (!hasRequiredPriviledges) throw new BadRequestError({ message: "Failed to create a more privileged identity" }); + const plan = await licenseService.getPlan(orgId); + if (plan?.identityLimit && plan.identitiesUsed >= plan.identityLimit) { + // limit imposed on number of identities allowed / number of identities used exceeds the number of identities allowed + throw new BadRequestError({ + message: "Failed to create identity due to identity limit reached. Upgrade plan to create more identities." + }); + } + const identity = await identityDAL.transaction(async (tx) => { const newIdentity = await identityDAL.create({ name }, tx); await identityOrgMembershipDAL.create( @@ -60,6 +69,7 @@ export const identityServiceFactory = ({ ); return newIdentity; }); + await licenseService.updateSubscriptionOrgMemberCount(orgId); return identity; }; @@ -152,6 +162,9 @@ export const identityServiceFactory = ({ throw new ForbiddenRequestError({ message: "Failed to delete more privileged identity" }); const deletedIdentity = await identityDAL.deleteById(id); + + await licenseService.updateSubscriptionOrgMemberCount(identityOrgMembership.orgId); + return { ...deletedIdentity, orgId: identityOrgMembership.orgId }; };