mirror of
https://github.com/Infisical/infisical.git
synced 2025-03-19 06:52:36 +00:00
Compare commits
4 Commits
infisical-
...
shubham/en
Author | SHA1 | Date | |
---|---|---|---|
fc3d0e429a | |||
174fd9df23 | |||
c603a41f39 | |||
235fc65b6e |
backend/src
db
ee/services
server/routes
services
frontend/src/views/Org/MembersPage/components/OrgIdentityTab/components/IdentitySection
@ -0,0 +1,15 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { TableName } from "../schemas";
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
await knex.schema.alterTable(TableName.Organization, (t) => {
|
||||
t.string("billingVersion").defaultTo("v1");
|
||||
});
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
await knex.schema.alterTable(TableName.Organization, (t) => {
|
||||
t.dropColumn("billingVersion");
|
||||
});
|
||||
}
|
@ -15,7 +15,8 @@ export const OrganizationsSchema = z.object({
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date(),
|
||||
authEnforced: z.boolean().default(false).nullable().optional(),
|
||||
scimEnabled: z.boolean().default(false).nullable().optional()
|
||||
scimEnabled: z.boolean().default(false).nullable().optional(),
|
||||
billingVersion: z.string().default("v1").nullable().optional() // v0 -> User-only billing, v1 -> Universal Identity billing
|
||||
});
|
||||
|
||||
export type TOrganizations = z.infer<typeof OrganizationsSchema>;
|
||||
|
@ -7,9 +7,9 @@ import { DatabaseError } from "@app/lib/errors";
|
||||
export type TLicenseDALFactory = ReturnType<typeof licenseDALFactory>;
|
||||
|
||||
export const licenseDALFactory = (db: TDbClient) => {
|
||||
const countOfOrgMembers = async (orgId: string | null, tx?: Knex) => {
|
||||
const countOfOrgIdentities = async (orgId: string | null, orgBillingVersion?: string | null, tx?: Knex) => {
|
||||
try {
|
||||
const doc = await (tx || db)(TableName.OrgMembership)
|
||||
const userIdentities = await (tx || db)(TableName.OrgMembership)
|
||||
.where({ status: OrgMembershipStatus.Accepted })
|
||||
.andWhere((bd) => {
|
||||
if (orgId) {
|
||||
@ -19,11 +19,34 @@ export const licenseDALFactory = (db: TDbClient) => {
|
||||
.join(TableName.Users, `${TableName.OrgMembership}.userId`, `${TableName.Users}.id`)
|
||||
.where(`${TableName.Users}.isGhost`, false)
|
||||
.count();
|
||||
return doc?.[0].count;
|
||||
|
||||
const userIdentitiesCount =
|
||||
typeof userIdentities?.[0].count === "string"
|
||||
? parseInt(userIdentities?.[0].count, 10)
|
||||
: userIdentities?.[0].count;
|
||||
if (orgBillingVersion === "v0") {
|
||||
return userIdentitiesCount;
|
||||
}
|
||||
|
||||
const machineIdentities = await (tx || db)(TableName.IdentityOrgMembership)
|
||||
.where((bd) => {
|
||||
if (orgId) {
|
||||
void bd.where(`${TableName.IdentityOrgMembership}.orgId`, orgId);
|
||||
}
|
||||
})
|
||||
.join(TableName.Identity, `${TableName.IdentityOrgMembership}.identityId`, `${TableName.Identity}.id`)
|
||||
.count();
|
||||
|
||||
const machineIdentitiesCount =
|
||||
typeof machineIdentities?.[0].count === "string"
|
||||
? parseInt(machineIdentities?.[0].count, 10)
|
||||
: machineIdentities?.[0].count;
|
||||
|
||||
return userIdentitiesCount + machineIdentitiesCount;
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error, name: "Count of Org Members" });
|
||||
}
|
||||
};
|
||||
|
||||
return { countOfOrgMembers };
|
||||
return { countOfOrgIdentities };
|
||||
};
|
||||
|
@ -199,12 +199,12 @@ export const licenseServiceFactory = ({
|
||||
await licenseServerCloudApi.request.delete(`/api/license-server/v1/customers/${customerId}`);
|
||||
};
|
||||
|
||||
const updateSubscriptionOrgMemberCount = async (orgId: string) => {
|
||||
const updateSubscriptionOrgIdentitiesCount = async (orgId: string): Promise<void> => {
|
||||
if (instanceType === InstanceType.Cloud) {
|
||||
const org = await orgDAL.findOrgById(orgId);
|
||||
if (!org) throw new BadRequestError({ message: "Org not found" });
|
||||
|
||||
const count = await licenseDAL.countOfOrgMembers(orgId);
|
||||
const count = await licenseDAL.countOfOrgIdentities(orgId, org.billingVersion);
|
||||
if (org?.customerId) {
|
||||
await licenseServerCloudApi.request.patch(`/api/license-server/v1/customers/${org.customerId}/cloud-plan`, {
|
||||
quantity: count
|
||||
@ -212,7 +212,7 @@ export const licenseServiceFactory = ({
|
||||
}
|
||||
await keyStore.deleteItem(FEATURE_CACHE_KEY(orgId));
|
||||
} else if (instanceType === InstanceType.EnterpriseOnPrem) {
|
||||
const usedSeats = await licenseDAL.countOfOrgMembers(null);
|
||||
const usedSeats = await licenseDAL.countOfOrgIdentities(null, null);
|
||||
await licenseServerOnPremApi.request.patch(`/api/license/v1/license`, { usedSeats });
|
||||
}
|
||||
await refreshPlan(orgId);
|
||||
@ -579,7 +579,7 @@ export const licenseServiceFactory = ({
|
||||
return onPremFeatures;
|
||||
},
|
||||
getPlan,
|
||||
updateSubscriptionOrgMemberCount,
|
||||
updateSubscriptionOrgIdentitiesCount,
|
||||
refreshPlan,
|
||||
getOrgPlan,
|
||||
getOrgPlansTableByBillCycle,
|
||||
|
@ -75,7 +75,7 @@ type TScimServiceFactoryDep = {
|
||||
>;
|
||||
projectKeyDAL: Pick<TProjectKeyDALFactory, "find" | "findLatestProjectKey" | "insertMany" | "delete">;
|
||||
projectBotDAL: Pick<TProjectBotDALFactory, "findOne">;
|
||||
licenseService: Pick<TLicenseServiceFactory, "getPlan" | "updateSubscriptionOrgMemberCount">;
|
||||
licenseService: Pick<TLicenseServiceFactory, "getPlan" | "updateSubscriptionOrgIdentitiesCount">;
|
||||
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">;
|
||||
smtpService: Pick<TSmtpService, "sendMail">;
|
||||
};
|
||||
|
@ -801,7 +801,8 @@ export const registerRoutes = async (
|
||||
const identityService = identityServiceFactory({
|
||||
permissionService,
|
||||
identityDAL,
|
||||
identityOrgMembershipDAL
|
||||
identityOrgMembershipDAL,
|
||||
licenseService
|
||||
});
|
||||
const identityAccessTokenService = identityAccessTokenServiceFactory({
|
||||
identityAccessTokenDAL,
|
||||
|
@ -39,7 +39,7 @@ type TAuthSignupDep = {
|
||||
orgDAL: TOrgDALFactory;
|
||||
tokenService: TAuthTokenServiceFactory;
|
||||
smtpService: TSmtpService;
|
||||
licenseService: Pick<TLicenseServiceFactory, "updateSubscriptionOrgMemberCount">;
|
||||
licenseService: Pick<TLicenseServiceFactory, "updateSubscriptionOrgIdentitiesCount">;
|
||||
};
|
||||
|
||||
export type TAuthSignupFactory = ReturnType<typeof authSignupServiceFactory>;
|
||||
@ -209,7 +209,7 @@ export const authSignupServiceFactory = ({
|
||||
{ userId: user.id, status: OrgMembershipStatus.Accepted }
|
||||
);
|
||||
const uniqueOrgId = [...new Set(updatedMembersips.map(({ orgId }) => orgId))];
|
||||
await Promise.allSettled(uniqueOrgId.map((orgId) => licenseService.updateSubscriptionOrgMemberCount(orgId)));
|
||||
await Promise.allSettled(uniqueOrgId.map((orgId) => licenseService.updateSubscriptionOrgIdentitiesCount(orgId)));
|
||||
|
||||
await convertPendingGroupAdditionsToGroupMemberships({
|
||||
userIds: [user.id],
|
||||
@ -321,7 +321,7 @@ export const authSignupServiceFactory = ({
|
||||
tx
|
||||
);
|
||||
const uniqueOrgId = [...new Set(updatedMembersips.map(({ orgId }) => orgId))];
|
||||
await Promise.allSettled(uniqueOrgId.map((orgId) => licenseService.updateSubscriptionOrgMemberCount(orgId)));
|
||||
await Promise.allSettled(uniqueOrgId.map((orgId) => licenseService.updateSubscriptionOrgIdentitiesCount(orgId)));
|
||||
|
||||
await convertPendingGroupAdditionsToGroupMemberships({
|
||||
userIds: [user.id],
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { ForbiddenError } from "@casl/ability";
|
||||
|
||||
import { OrgMembershipRole, 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";
|
||||
@ -16,6 +17,7 @@ type TIdentityServiceFactoryDep = {
|
||||
identityDAL: TIdentityDALFactory;
|
||||
identityOrgMembershipDAL: TIdentityOrgDALFactory;
|
||||
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission" | "getOrgPermissionByRole">;
|
||||
licenseService: Pick<TLicenseServiceFactory, "updateSubscriptionOrgIdentitiesCount" | "getPlan">;
|
||||
};
|
||||
|
||||
export type TIdentityServiceFactory = ReturnType<typeof identityServiceFactory>;
|
||||
@ -23,7 +25,8 @@ export type TIdentityServiceFactory = ReturnType<typeof identityServiceFactory>;
|
||||
export const identityServiceFactory = ({
|
||||
identityDAL,
|
||||
identityOrgMembershipDAL,
|
||||
permissionService
|
||||
permissionService,
|
||||
licenseService
|
||||
}: TIdentityServiceFactoryDep) => {
|
||||
const createIdentity = async ({
|
||||
name,
|
||||
@ -45,6 +48,15 @@ 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.memberLimit !== null && plan.membersUsed >= plan.memberLimit) {
|
||||
// case: limit imposed on number of identities allowed
|
||||
// case: number of identities used exceeds the number of identities allowed
|
||||
throw new BadRequestError({
|
||||
message: "Failed to add identity due to identiy limit reached. Upgrade plan to add more identities."
|
||||
});
|
||||
}
|
||||
|
||||
const identity = await identityDAL.transaction(async (tx) => {
|
||||
const newIdentity = await identityDAL.create({ name }, tx);
|
||||
await identityOrgMembershipDAL.create(
|
||||
@ -58,7 +70,7 @@ export const identityServiceFactory = ({
|
||||
);
|
||||
return newIdentity;
|
||||
});
|
||||
|
||||
await licenseService.updateSubscriptionOrgIdentitiesCount(orgId);
|
||||
return identity;
|
||||
};
|
||||
|
||||
@ -150,6 +162,7 @@ export const identityServiceFactory = ({
|
||||
throw new ForbiddenRequestError({ message: "Failed to delete more privileged identity" });
|
||||
|
||||
const deletedIdentity = await identityDAL.deleteById(id);
|
||||
await licenseService.updateSubscriptionOrgIdentitiesCount(identityOrgMembership.orgId);
|
||||
return { ...deletedIdentity, orgId: identityOrgMembership.orgId };
|
||||
};
|
||||
|
||||
|
@ -11,7 +11,7 @@ type TDeleteOrgMembership = {
|
||||
projectMembershipDAL: Pick<TProjectMembershipDALFactory, "delete" | "findProjectMembershipsByUserId">;
|
||||
projectKeyDAL: Pick<TProjectKeyDALFactory, "find" | "delete">;
|
||||
userAliasDAL: Pick<TUserAliasDALFactory, "delete">;
|
||||
licenseService: Pick<TLicenseServiceFactory, "updateSubscriptionOrgMemberCount">;
|
||||
licenseService: Pick<TLicenseServiceFactory, "updateSubscriptionOrgIdentitiesCount">;
|
||||
};
|
||||
|
||||
export const deleteOrgMembershipFn = async ({
|
||||
@ -27,7 +27,7 @@ export const deleteOrgMembershipFn = async ({
|
||||
const orgMembership = await orgDAL.deleteMembershipById(orgMembershipId, orgId, tx);
|
||||
|
||||
if (!orgMembership.userId) {
|
||||
await licenseService.updateSubscriptionOrgMemberCount(orgId);
|
||||
await licenseService.updateSubscriptionOrgIdentitiesCount(orgId);
|
||||
return orgMembership;
|
||||
}
|
||||
|
||||
@ -70,7 +70,7 @@ export const deleteOrgMembershipFn = async ({
|
||||
tx
|
||||
);
|
||||
|
||||
await licenseService.updateSubscriptionOrgMemberCount(orgId);
|
||||
await licenseService.updateSubscriptionOrgIdentitiesCount(orgId);
|
||||
return orgMembership;
|
||||
});
|
||||
|
||||
|
@ -61,7 +61,7 @@ type TOrgServiceFactoryDep = {
|
||||
permissionService: TPermissionServiceFactory;
|
||||
licenseService: Pick<
|
||||
TLicenseServiceFactory,
|
||||
"getPlan" | "updateSubscriptionOrgMemberCount" | "generateOrgCustomerId" | "removeOrgCustomer"
|
||||
"getPlan" | "updateSubscriptionOrgIdentitiesCount" | "generateOrgCustomerId" | "removeOrgCustomer"
|
||||
>;
|
||||
};
|
||||
|
||||
@ -514,7 +514,7 @@ export const orgServiceFactory = ({
|
||||
}
|
||||
});
|
||||
|
||||
await licenseService.updateSubscriptionOrgMemberCount(orgId);
|
||||
await licenseService.updateSubscriptionOrgIdentitiesCount(orgId);
|
||||
if (!appCfg.isSmtpConfigured) {
|
||||
return `${appCfg.SITE_URL}/signupinvite?token=${token}&to=${inviteeEmail}&organization_id=${org?.id}`;
|
||||
}
|
||||
@ -558,7 +558,7 @@ export const orgServiceFactory = ({
|
||||
orgId,
|
||||
status: OrgMembershipStatus.Accepted
|
||||
});
|
||||
await licenseService.updateSubscriptionOrgMemberCount(orgId);
|
||||
await licenseService.updateSubscriptionOrgIdentitiesCount(orgId);
|
||||
return { user };
|
||||
}
|
||||
|
||||
|
@ -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";
|
||||
@ -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",
|
||||
@ -56,6 +61,20 @@ export const IdentitySection = withPermission(
|
||||
}
|
||||
};
|
||||
|
||||
const isMoreUsersNotAllowed = subscription?.memberLimit
|
||||
? subscription.membersUsed >= subscription.memberLimit
|
||||
: false;
|
||||
|
||||
const handleAddMachineIdentityModal = () => {
|
||||
if (isMoreUsersNotAllowed) {
|
||||
handlePopUpOpen("upgradePlan", {
|
||||
description: "You can add more identities if you upgrade your Infisical plan."
|
||||
});
|
||||
} else {
|
||||
handlePopUpOpen("identity");
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
||||
<div className="mb-4 flex justify-between">
|
||||
@ -81,7 +100,7 @@ export const IdentitySection = withPermission(
|
||||
colorSchema="primary"
|
||||
type="submit"
|
||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||
onClick={() => handlePopUpOpen("identity")}
|
||||
onClick={() => handleAddMachineIdentityModal()}
|
||||
isDisabled={!isAllowed}
|
||||
>
|
||||
Create identity
|
||||
@ -105,6 +124,11 @@ export const IdentitySection = withPermission(
|
||||
handlePopUpOpen={handlePopUpOpen}
|
||||
handlePopUpToggle={handlePopUpToggle}
|
||||
/>
|
||||
<UpgradePlanModal
|
||||
isOpen={popUp.upgradePlan.isOpen}
|
||||
onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)}
|
||||
text={(popUp.upgradePlan?.data as { description: string })?.description}
|
||||
/>
|
||||
<DeleteActionModal
|
||||
isOpen={popUp.deleteIdentity.isOpen}
|
||||
title={`Are you sure want to delete ${
|
||||
|
Reference in New Issue
Block a user