Skip to content

Commit

Permalink
Merge pull request #2024 from Infisical/revert-2023-revert-1995-ident…
Browse files Browse the repository at this point in the history
…ity-based-pricing

Add support for Identity-Based Pricing"
  • Loading branch information
dangtony98 committed Jun 27, 2024
2 parents fc27ad4 + 1c2698f commit 15b4c39
Show file tree
Hide file tree
Showing 14 changed files with 149 additions and 21 deletions.
1 change: 1 addition & 0 deletions .infisicalignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@ frontend/src/views/Project/MembersPage/components/MemberListTab/MemberRoleForm/M
frontend/src/views/Project/MembersPage/components/MemberListTab/MemberRoleForm/SpecificPrivilegeSection.tsx:generic-api-key:292
docs/self-hosting/configuration/envars.mdx:generic-api-key:106
frontend/src/views/Project/MembersPage/components/MemberListTab/MemberRoleForm/SpecificPrivilegeSection.tsx:generic-api-key:451
docs/mint.json:generic-api-key:651
15 changes: 15 additions & 0 deletions backend/src/ee/services/ldap-config/ldap-config-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -456,6 +456,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) {
Expand Down
2 changes: 2 additions & 0 deletions backend/src/ee/services/license/__mocks__/licence-fns.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ export const getDefaultOnPremFeatures = () => {
workspacesUsed: 0,
memberLimit: null,
membersUsed: 0,
identityLimit: null,
identitiesUsed: 0,
environmentLimit: null,
environmentsUsed: 0,
secretVersioning: true,
Expand Down
2 changes: 2 additions & 0 deletions backend/src/ee/services/license/licence-fns.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ export const getDefaultOnPremFeatures = (): TFeatureSet => ({
membersUsed: 0,
environmentLimit: null,
environmentsUsed: 0,
identityLimit: null,
identitiesUsed: 0,
dynamicSecret: false,
secretVersioning: true,
pitRecovery: false,
Expand Down
37 changes: 35 additions & 2 deletions backend/src/ee/services/license/license-dal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 Users + Identities" });
}
};

return { countOfOrgMembers, countOrgUsersAndIdentities };
};
13 changes: 10 additions & 3 deletions backend/src/ee/services/license/license-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,7 @@ export const licenseServiceFactory = ({
LICENSE_SERVER_CLOUD_PLAN_TTL,
JSON.stringify(currentPlan)
);

return currentPlan;
}
} catch (error) {
Expand Down Expand Up @@ -204,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);
};
Expand Down
2 changes: 2 additions & 0 deletions backend/src/ee/services/license/license-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ export type TFeatureSet = {
dynamicSecret: false;
memberLimit: null;
membersUsed: 0;
identityLimit: null;
identitiesUsed: 0;
environmentLimit: null;
environmentsUsed: 0;
secretVersioning: true;
Expand Down
15 changes: 15 additions & 0 deletions backend/src/ee/services/saml-config/saml-config-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -380,6 +380,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) {
Expand Down
3 changes: 2 additions & 1 deletion backend/src/server/routes/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -806,7 +806,8 @@ export const registerRoutes = async (
const identityService = identityServiceFactory({
permissionService,
identityDAL,
identityOrgMembershipDAL
identityOrgMembershipDAL,
licenseService
});
const identityAccessTokenService = identityAccessTokenServiceFactory({
identityAccessTokenDAL,
Expand Down
17 changes: 16 additions & 1 deletion backend/src/services/identity/identity-service.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { ForbiddenError } from "@casl/ability";

import { OrgMembershipRole, TableName, TOrgRoles } from "@app/db/schemas";
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import { isAtLeastAsPrivileged } from "@app/lib/casl";
Expand All @@ -16,14 +17,16 @@ type TIdentityServiceFactoryDep = {
identityDAL: TIdentityDALFactory;
identityOrgMembershipDAL: TIdentityOrgDALFactory;
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission" | "getOrgPermissionByRole">;
licenseService: Pick<TLicenseServiceFactory, "getPlan" | "updateSubscriptionOrgMemberCount">;
};

export type TIdentityServiceFactory = ReturnType<typeof identityServiceFactory>;

export const identityServiceFactory = ({
identityDAL,
identityOrgMembershipDAL,
permissionService
permissionService,
licenseService
}: TIdentityServiceFactoryDep) => {
const createIdentity = async ({
name,
Expand All @@ -45,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(
Expand All @@ -58,6 +69,7 @@ export const identityServiceFactory = ({
);
return newIdentity;
});
await licenseService.updateSubscriptionOrgMemberCount(orgId);

return identity;
};
Expand Down Expand Up @@ -168,6 +180,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 };
};

Expand Down
13 changes: 10 additions & 3 deletions backend/src/services/org/org-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -420,13 +420,20 @@ export const orgServiceFactory = ({
}

const plan = await licenseService.getPlan(orgId);
if (plan.memberLimit !== null && plan.membersUsed >= plan.memberLimit) {
// case: limit imposed on number of members allowed
// case: number of members used exceeds the number of members allowed
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 invite member due to member limit reached. Upgrade plan to invite 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 invite member due to member limit reached. Upgrade plan to invite more members."
});
}

const invitee = await orgDAL.transaction(async (tx) => {
const inviteeUser = await userDAL.findUserByUsername(inviteeEmail, tx);
if (inviteeUser) {
Expand Down
2 changes: 2 additions & 0 deletions frontend/src/hooks/api/subscriptions/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ export type SubscriptionPlan = {
id: string;
membersUsed: number;
memberLimit: number;
identitiesUsed: number;
identityLimit: number;
auditLogs: boolean;
dynamicSecret: boolean;
auditLogsRetentionDays: number;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,13 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";

import { createNotification } from "@app/components/notifications";
import { OrgPermissionCan } from "@app/components/permissions";
import { Button, DeleteActionModal } from "@app/components/v2";
import { OrgPermissionActions, OrgPermissionSubjects, useOrganization } from "@app/context";
import { Button, DeleteActionModal, UpgradePlanModal } from "@app/components/v2";
import {
OrgPermissionActions,
OrgPermissionSubjects,
useOrganization,
useSubscription
} from "@app/context";
import { withPermission } from "@app/hoc";
import { useDeleteIdentity } from "@app/hooks/api";
import { usePopUp } from "@app/hooks/usePopUp";
Expand All @@ -17,10 +22,10 @@ import { IdentityUniversalAuthClientSecretModal } from "./IdentityUniversalAuthC

export const IdentitySection = withPermission(
() => {
const { subscription } = useSubscription();
const { currentOrg } = useOrganization();
const orgId = currentOrg?.id || "";


const { mutateAsync: deleteMutateAsync } = useDeleteIdentity();
const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([
"identity",
Expand All @@ -31,6 +36,10 @@ export const IdentitySection = withPermission(
"upgradePlan"
] as const);

const isMoreIdentitiesAllowed = subscription?.identityLimit
? subscription.identitiesUsed < subscription.identityLimit
: true;

const onDeleteIdentitySubmit = async (identityId: string) => {
try {
await deleteMutateAsync({
Expand Down Expand Up @@ -81,7 +90,15 @@ export const IdentitySection = withPermission(
colorSchema="primary"
type="submit"
leftIcon={<FontAwesomeIcon icon={faPlus} />}
onClick={() => handlePopUpOpen("identity")}
onClick={() => {
if (!isMoreIdentitiesAllowed) {
handlePopUpOpen("upgradePlan", {
description: "You can add more identities if you upgrade your Infisical plan."
});
return;
}
handlePopUpOpen("identity");
}}
isDisabled={!isAllowed}
>
Create identity
Expand Down Expand Up @@ -118,6 +135,11 @@ export const IdentitySection = withPermission(
)
}
/>
<UpgradePlanModal
isOpen={popUp.upgradePlan.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)}
text={(popUp.upgradePlan?.data as { description: string })?.description}
/>
</div>
);
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@ import { AddOrgMemberModal } from "./AddOrgMemberModal";
import { OrgMembersTable } from "./OrgMembersTable";

export const OrgMembersSection = () => {

const { subscription } = useSubscription();
const { currentOrg } = useOrganization();
const orgId = currentOrg?.id ?? "";
Expand All @@ -39,9 +38,13 @@ export const OrgMembersSection = () => {

const { mutateAsync: deleteMutateAsync } = useDeleteOrgMembership();

const isMoreUsersNotAllowed = subscription?.memberLimit
? subscription.membersUsed >= subscription.memberLimit
: false;
const isMoreUsersAllowed = subscription?.memberLimit
? subscription.membersUsed < subscription.memberLimit
: true;

const isMoreIdentitiesAllowed = subscription?.identityLimit
? subscription.identitiesUsed < subscription.identityLimit
: true;

const handleAddMemberModal = () => {
if (currentOrg?.authEnforced) {
Expand All @@ -52,13 +55,14 @@ export const OrgMembersSection = () => {
return;
}

if (isMoreUsersNotAllowed) {
if (!isMoreUsersAllowed || !isMoreIdentitiesAllowed) {
handlePopUpOpen("upgradePlan", {
description: "You can add more members if you upgrade your Infisical plan."
});
} else {
handlePopUpOpen("addMember");
return;
}

handlePopUpOpen("addMember");
};

const onRemoveMemberSubmit = async (orgMembershipId: string) => {
Expand Down

0 comments on commit 15b4c39

Please sign in to comment.