Merge pull request #4701 from Infisical/feat/sub-org

feat: sub organization
This commit is contained in:
Scott Wilson
2025-10-21 14:18:48 -07:00
committed by GitHub
200 changed files with 5621 additions and 2275 deletions
+28 -5
View File
@@ -10,6 +10,7 @@ export const licenseDALFactory = (db: TDbClient) => {
const countOfOrgMembers = async (orgId: string | null, tx?: Knex) => {
try {
const doc = await (tx || db.replicaNode())(TableName.Membership)
.join(TableName.Organization, `${TableName.Organization}.id`, `${TableName.Membership}.scopeOrgId`)
.where({ status: OrgMembershipStatus.Accepted, scope: AccessScope.Organization })
.andWhere((bd) => {
if (orgId) {
@@ -18,6 +19,7 @@ export const licenseDALFactory = (db: TDbClient) => {
})
.join(TableName.Users, `${TableName.Membership}.actorUserId`, `${TableName.Users}.id`)
.where(`${TableName.Users}.isGhost`, false)
.whereNull(`${TableName.Organization}.rootOrgId`)
.count();
return Number(doc?.[0]?.count ?? 0);
} catch (error) {
@@ -25,10 +27,31 @@ export const licenseDALFactory = (db: TDbClient) => {
}
};
const countOfOrgIdentities = async (orgId: string | null, tx?: Knex) => {
try {
// count org identities
const identityDoc = await (tx || db.replicaNode())(TableName.Identity)
.join(TableName.Organization, `${TableName.Identity}.orgId`, `${TableName.Organization}.id`)
.where((bd) => {
if (orgId) {
void bd.where(`${TableName.Organization}.rootOrgId`, orgId).orWhere(`${TableName.Organization}.id`, orgId);
}
})
.count();
const identityCount = Number(identityDoc?.[0].count);
return identityCount;
} catch (error) {
throw new DatabaseError({ error, name: "Count of Org Users + Identities" });
}
};
const countOrgUsersAndIdentities = async (orgId: string | null, tx?: Knex) => {
try {
// count org users
const userDoc = await (tx || db.replicaNode())(TableName.Membership)
.join(TableName.Organization, `${TableName.Organization}.id`, `${TableName.Membership}.scopeOrgId`)
.where({ status: OrgMembershipStatus.Accepted, scope: AccessScope.Organization })
.whereNotNull(`${TableName.Membership}.actorUserId`)
.andWhere((bd) => {
@@ -38,17 +61,17 @@ export const licenseDALFactory = (db: TDbClient) => {
})
.join(TableName.Users, `${TableName.Membership}.actorUserId`, `${TableName.Users}.id`)
.where(`${TableName.Users}.isGhost`, false)
.whereNull(`${TableName.Organization}.rootOrgId`)
.count();
const userCount = Number(userDoc?.[0].count);
// count org identities
const identityDoc = await (tx || db.replicaNode())(TableName.Membership)
.where({ scope: AccessScope.Organization })
.whereNotNull(`${TableName.Membership}.actorIdentityId`)
const identityDoc = await (tx || db.replicaNode())(TableName.Identity)
.join(TableName.Organization, `${TableName.Identity}.orgId`, `${TableName.Organization}.id`)
.where((bd) => {
if (orgId) {
void bd.where(`${TableName.Membership}.scopeOrgId`, orgId);
void bd.where(`${TableName.Organization}.rootOrgId`, orgId).orWhere(`${TableName.Organization}.id`, orgId);
}
})
.count();
@@ -61,5 +84,5 @@ export const licenseDALFactory = (db: TDbClient) => {
}
};
return { countOfOrgMembers, countOrgUsersAndIdentities };
return { countOfOrgMembers, countOrgUsersAndIdentities, countOfOrgIdentities };
};
@@ -28,6 +28,7 @@ export const getDefaultOnPremFeatures = (): TFeatureSet => ({
rbac: false,
githubOrgSync: false,
customRateLimits: false,
subOrganization: false,
customAlerts: false,
secretAccessInsights: false,
auditLogs: false,
@@ -9,12 +9,12 @@ import { AxiosError } from "axios";
import { CronJob } from "cron";
import { Knex } from "knex";
import { OrganizationActionScope } from "@app/db/schemas";
import { TKeyStoreFactory } from "@app/keystore/keystore";
import { getConfig } from "@app/lib/config/env";
import { verifyOfflineLicense } from "@app/lib/crypto";
import { BadRequestError, NotFoundError } from "@app/lib/errors";
import { logger } from "@app/lib/logger";
import { TIdentityOrgDALFactory } from "@app/services/identity/identity-org-dal";
import { TOrgDALFactory } from "@app/services/org/org-dal";
import { TProjectDALFactory } from "@app/services/project/project-dal";
@@ -45,11 +45,10 @@ import {
} from "./license-types";
type TLicenseServiceFactoryDep = {
orgDAL: Pick<TOrgDALFactory, "findOrgById" | "countAllOrgMembers">;
orgDAL: Pick<TOrgDALFactory, "findRootOrgDetails" | "countAllOrgMembers" | "findById">;
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">;
licenseDAL: TLicenseDALFactory;
keyStore: Pick<TKeyStoreFactory, "setItemWithExpiry" | "getItem" | "deleteItem">;
identityOrgMembershipDAL: TIdentityOrgDALFactory;
projectDAL: TProjectDALFactory;
};
@@ -66,7 +65,6 @@ export const licenseServiceFactory = ({
permissionService,
licenseDAL,
keyStore,
identityOrgMembershipDAL,
projectDAL
}: TLicenseServiceFactoryDep) => {
let isValidLicense = false;
@@ -199,19 +197,21 @@ export const licenseServiceFactory = ({
return JSON.parse(cachedPlan) as TFeatureSet;
}
const org = await orgDAL.findOrgById(orgId);
const org = await orgDAL.findRootOrgDetails(orgId);
if (!org) throw new NotFoundError({ message: `Organization with ID '${orgId}' not found` });
const rootOrgId = org.id;
const {
data: { currentPlan }
} = await licenseServerCloudApi.request.get<{ currentPlan: TFeatureSet }>(
`/api/license-server/v1/customers/${org.customerId}/cloud-plan`
);
const workspacesUsed = await projectDAL.countOfOrgProjects(orgId);
const workspacesUsed = await projectDAL.countOfOrgProjects(rootOrgId);
currentPlan.workspacesUsed = workspacesUsed;
const membersUsed = await licenseDAL.countOfOrgMembers(orgId);
const membersUsed = await licenseDAL.countOfOrgMembers(rootOrgId);
currentPlan.membersUsed = membersUsed;
const identityUsed = await licenseDAL.countOrgUsersAndIdentities(orgId);
const identityUsed = await licenseDAL.countOrgUsersAndIdentities(rootOrgId);
currentPlan.identitiesUsed = identityUsed;
if (currentPlan.identityLimit && currentPlan.identityLimit !== identityUsed) {
@@ -284,19 +284,20 @@ export const licenseServiceFactory = ({
};
const updateSubscriptionOrgMemberCount = async (orgId: string, tx?: Knex) => {
if (instanceType === InstanceType.Cloud) {
const org = await orgDAL.findOrgById(orgId);
if (!org) throw new NotFoundError({ message: `Organization with ID '${orgId}' not found` });
const org = await orgDAL.findRootOrgDetails(orgId, tx);
if (!org) throw new NotFoundError({ message: `Organization with ID '${orgId}' not found` });
const quantity = await licenseDAL.countOfOrgMembers(orgId, tx);
const quantityIdentities = await licenseDAL.countOrgUsersAndIdentities(orgId, tx);
const rootOrgId = org.id;
if (instanceType === InstanceType.Cloud) {
const quantity = await licenseDAL.countOfOrgMembers(rootOrgId, tx);
const quantityIdentities = await licenseDAL.countOrgUsersAndIdentities(rootOrgId, tx);
if (org?.customerId) {
await licenseServerCloudApi.request.patch(`/api/license-server/v1/customers/${org.customerId}/cloud-plan`, {
quantity,
quantityIdentities
});
}
await keyStore.deleteItem(FEATURE_CACHE_KEY(orgId));
await keyStore.deleteItem(FEATURE_CACHE_KEY(rootOrgId));
} else if (instanceType === InstanceType.EnterpriseOnPrem) {
const usedSeats = await licenseDAL.countOfOrgMembers(null, tx);
const usedIdentitySeats = await licenseDAL.countOrgUsersAndIdentities(null, tx);
@@ -307,7 +308,7 @@ export const licenseServiceFactory = ({
usedIdentitySeats
});
}
await refreshPlan(orgId);
await refreshPlan(rootOrgId);
};
// below all are api calls
@@ -319,7 +320,14 @@ export const licenseServiceFactory = ({
actorAuthMethod,
billingCycle
}: TOrgPlansTableDTO) => {
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);
const { permission } = await permissionService.getOrgPermission({
actorId,
actor,
orgId,
actorOrgId,
actorAuthMethod,
scope: OrganizationActionScope.ParentOrganization
});
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionBillingActions.Read, OrgPermissionSubjects.Billing);
const { data } = await licenseServerCloudApi.request.get(
`/api/license-server/v1/cloud-products?billing-cycle=${billingCycle}`
@@ -336,7 +344,14 @@ export const licenseServiceFactory = ({
projectId,
refreshCache
}: TOrgPlanDTO) => {
await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);
await permissionService.getOrgPermission({
actorId,
actor,
orgId,
actorOrgId,
actorAuthMethod,
scope: OrganizationActionScope.ParentOrganization
});
if (refreshCache) {
await refreshPlan(orgId);
}
@@ -352,13 +367,20 @@ export const licenseServiceFactory = ({
actorAuthMethod,
success_url
}: TStartOrgTrialDTO) => {
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);
const { permission } = await permissionService.getOrgPermission({
actorId,
actor,
orgId,
actorOrgId,
actorAuthMethod,
scope: OrganizationActionScope.ParentOrganization
});
ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionBillingActions.ManageBilling,
OrgPermissionSubjects.Billing
);
const organization = await orgDAL.findOrgById(orgId);
const organization = await orgDAL.findById(orgId);
if (!organization) {
throw new NotFoundError({
message: `Organization with ID '${orgId}' not found`
@@ -384,13 +406,20 @@ export const licenseServiceFactory = ({
actorAuthMethod,
actorOrgId
}: TCreateOrgPortalSession) => {
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);
const { permission } = await permissionService.getOrgPermission({
actorId,
actor,
orgId,
actorOrgId,
actorAuthMethod,
scope: OrganizationActionScope.ParentOrganization
});
ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionBillingActions.ManageBilling,
OrgPermissionSubjects.Billing
);
const organization = await orgDAL.findOrgById(orgId);
const organization = await orgDAL.findById(orgId);
if (!organization) {
throw new NotFoundError({
message: "Organization not found"
@@ -433,10 +462,17 @@ export const licenseServiceFactory = ({
};
const getOrgBillingInfo = async ({ orgId, actor, actorId, actorAuthMethod, actorOrgId }: TGetOrgBillInfoDTO) => {
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);
const { permission } = await permissionService.getOrgPermission({
actorId,
actor,
orgId,
actorOrgId,
actorAuthMethod,
scope: OrganizationActionScope.ParentOrganization
});
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionBillingActions.Read, OrgPermissionSubjects.Billing);
const organization = await orgDAL.findOrgById(orgId);
const organization = await orgDAL.findById(orgId);
if (!organization) {
throw new NotFoundError({
message: `Organization with ID '${orgId}' not found`
@@ -502,7 +538,7 @@ export const licenseServiceFactory = ({
const getUsageMetrics = async (orgId: string) => {
const [orgMembersUsed, identityUsed, projectCount] = await Promise.all([
orgDAL.countAllOrgMembers(orgId),
identityOrgMembershipDAL.countAllOrgIdentities({ scopeOrgId: orgId }),
licenseDAL.countOfOrgIdentities(orgId),
projectDAL.countOfOrgProjects(orgId)
]);
@@ -516,10 +552,17 @@ export const licenseServiceFactory = ({
// returns org current plan feature table
const getOrgPlanTable = async ({ orgId, actor, actorId, actorAuthMethod, actorOrgId }: TGetOrgBillInfoDTO) => {
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);
const { permission } = await permissionService.getOrgPermission({
actorId,
actor,
orgId,
actorOrgId,
actorAuthMethod,
scope: OrganizationActionScope.ParentOrganization
});
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionBillingActions.Read, OrgPermissionSubjects.Billing);
const organization = await orgDAL.findOrgById(orgId);
const organization = await orgDAL.findById(orgId);
if (!organization) {
throw new NotFoundError({
message: `Organization with ID '${orgId}' not found`
@@ -553,10 +596,17 @@ export const licenseServiceFactory = ({
};
const getOrgBillingDetails = async ({ orgId, actor, actorId, actorAuthMethod, actorOrgId }: TGetOrgBillInfoDTO) => {
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);
const { permission } = await permissionService.getOrgPermission({
actorId,
actor,
orgId,
actorOrgId,
actorAuthMethod,
scope: OrganizationActionScope.ParentOrganization
});
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionBillingActions.Read, OrgPermissionSubjects.Billing);
const organization = await orgDAL.findOrgById(orgId);
const organization = await orgDAL.findById(orgId);
if (!organization) {
throw new NotFoundError({
message: `Organization with ID '${orgId}' not found`
@@ -578,13 +628,20 @@ export const licenseServiceFactory = ({
name,
email
}: TUpdateOrgBillingDetailsDTO) => {
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);
const { permission } = await permissionService.getOrgPermission({
actorId,
actor,
orgId,
actorOrgId,
actorAuthMethod,
scope: OrganizationActionScope.ParentOrganization
});
ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionBillingActions.ManageBilling,
OrgPermissionSubjects.Billing
);
const organization = await orgDAL.findOrgById(orgId);
const organization = await orgDAL.findById(orgId);
if (!organization) {
throw new NotFoundError({
message: `Organization with ID '${orgId}' not found`
@@ -601,10 +658,17 @@ export const licenseServiceFactory = ({
};
const getOrgPmtMethods = async ({ orgId, actor, actorId, actorAuthMethod, actorOrgId }: TOrgPmtMethodsDTO) => {
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);
const { permission } = await permissionService.getOrgPermission({
actorId,
actor,
orgId,
actorOrgId,
actorAuthMethod,
scope: OrganizationActionScope.ParentOrganization
});
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionBillingActions.Read, OrgPermissionSubjects.Billing);
const organization = await orgDAL.findOrgById(orgId);
const organization = await orgDAL.findById(orgId);
if (!organization) {
throw new NotFoundError({
message: `Organization with ID '${orgId}' not found`
@@ -628,13 +692,20 @@ export const licenseServiceFactory = ({
success_url,
cancel_url
}: TAddOrgPmtMethodDTO) => {
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);
const { permission } = await permissionService.getOrgPermission({
actorId,
actor,
orgId,
actorOrgId,
actorAuthMethod,
scope: OrganizationActionScope.ParentOrganization
});
ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionBillingActions.ManageBilling,
OrgPermissionSubjects.Billing
);
const organization = await orgDAL.findOrgById(orgId);
const organization = await orgDAL.findById(orgId);
if (!organization) {
throw new NotFoundError({
message: `Organization with ID '${orgId}' not found`
@@ -660,13 +731,20 @@ export const licenseServiceFactory = ({
orgId,
pmtMethodId
}: TDelOrgPmtMethodDTO) => {
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);
const { permission } = await permissionService.getOrgPermission({
actorId,
actor,
orgId,
actorOrgId,
actorAuthMethod,
scope: OrganizationActionScope.ParentOrganization
});
ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionBillingActions.ManageBilling,
OrgPermissionSubjects.Billing
);
const organization = await orgDAL.findOrgById(orgId);
const organization = await orgDAL.findById(orgId);
if (!organization) {
throw new NotFoundError({
message: `Organization with ID '${orgId}' not found`
@@ -692,10 +770,17 @@ export const licenseServiceFactory = ({
};
const getOrgTaxIds = async ({ orgId, actor, actorId, actorAuthMethod, actorOrgId }: TGetOrgTaxIdDTO) => {
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);
const { permission } = await permissionService.getOrgPermission({
actorId,
actor,
orgId,
actorOrgId,
actorAuthMethod,
scope: OrganizationActionScope.ParentOrganization
});
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionBillingActions.Read, OrgPermissionSubjects.Billing);
const organization = await orgDAL.findOrgById(orgId);
const organization = await orgDAL.findById(orgId);
if (!organization) {
throw new NotFoundError({
message: `Organization with ID '${orgId}' not found`
@@ -710,13 +795,20 @@ export const licenseServiceFactory = ({
};
const addOrgTaxId = async ({ actorId, actor, actorAuthMethod, actorOrgId, orgId, type, value }: TAddOrgTaxIdDTO) => {
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);
const { permission } = await permissionService.getOrgPermission({
actorId,
actor,
orgId,
actorOrgId,
actorAuthMethod,
scope: OrganizationActionScope.ParentOrganization
});
ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionBillingActions.ManageBilling,
OrgPermissionSubjects.Billing
);
const organization = await orgDAL.findOrgById(orgId);
const organization = await orgDAL.findById(orgId);
if (!organization) {
throw new NotFoundError({
message: `Organization with ID '${orgId}' not found`
@@ -734,13 +826,20 @@ export const licenseServiceFactory = ({
};
const delOrgTaxId = async ({ orgId, actor, actorId, actorAuthMethod, actorOrgId, taxId }: TDelOrgTaxIdDTO) => {
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);
const { permission } = await permissionService.getOrgPermission({
actorId,
actor,
orgId,
actorOrgId,
actorAuthMethod,
scope: OrganizationActionScope.ParentOrganization
});
ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionBillingActions.ManageBilling,
OrgPermissionSubjects.Billing
);
const organization = await orgDAL.findOrgById(orgId);
const organization = await orgDAL.findById(orgId);
if (!organization) {
throw new NotFoundError({
message: `Organization with ID '${orgId}' not found`
@@ -754,10 +853,17 @@ export const licenseServiceFactory = ({
};
const getOrgTaxInvoices = async ({ actorId, actor, actorOrgId, actorAuthMethod, orgId }: TOrgInvoiceDTO) => {
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);
const { permission } = await permissionService.getOrgPermission({
actorId,
actor,
orgId,
actorOrgId,
actorAuthMethod,
scope: OrganizationActionScope.ParentOrganization
});
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionBillingActions.Read, OrgPermissionSubjects.Billing);
const organization = await orgDAL.findOrgById(orgId);
const organization = await orgDAL.findById(orgId);
if (!organization) {
throw new NotFoundError({
message: `Organization with ID '${orgId}' not found`
@@ -771,10 +877,17 @@ export const licenseServiceFactory = ({
};
const getOrgLicenses = async ({ orgId, actor, actorId, actorAuthMethod, actorOrgId }: TOrgLicensesDTO) => {
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);
const { permission } = await permissionService.getOrgPermission({
actorId,
actor,
orgId,
actorOrgId,
actorAuthMethod,
scope: OrganizationActionScope.ParentOrganization
});
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionBillingActions.Read, OrgPermissionSubjects.Billing);
const organization = await orgDAL.findOrgById(orgId);
const organization = await orgDAL.findById(orgId);
if (!organization) {
throw new NotFoundError({
message: `Organization with ID '${orgId}' not found`
@@ -819,7 +932,6 @@ export const licenseServiceFactory = ({
getLicenseId,
invalidateGetPlan,
updateSubscriptionOrgMemberCount,
refreshPlan,
getOrgPlan,
getOrgPlansTableByBillCycle,
startOrgTrial,
@@ -33,6 +33,7 @@ export type TFeatureSet = {
membersUsed: number;
identityLimit: null;
identitiesUsed: number;
subOrganization: false;
environmentLimit: null;
environmentsUsed: 0;
secretVersioning: true;