feat: completed license server integration

This commit is contained in:
Akhil Mohan
2024-01-27 12:38:23 +05:30
parent fe416556f2
commit c7572a3374
47 changed files with 1818 additions and 121 deletions
+7 -3
View File
@@ -1,3 +1,4 @@
import { registerLicenseRouter } from "./license-router";
import { registerOrgRoleRouter } from "./org-role-router";
import { registerProjectRoleRouter } from "./project-role-router";
import { registerProjectRouter } from "./project-router";
@@ -8,14 +9,17 @@ import { registerSecretRotationProviderRouter } from "./secret-rotation-provider
import { registerSecretRotationRouter } from "./secret-rotation-router";
import { registerSecretScanningRouter } from "./secret-scanning-router";
import { registerSnapshotRouter } from "./snapshot-router";
import { registerTrustedIpRouter } from "./trusted-ip-router";
export const registerV1EERoutes = async (server: FastifyZodProvider) => {
// org role starts with organization
await server.register(registerOrgRoleRouter, { prefix: "/organization" });
await server.register(registerLicenseRouter, { prefix: "/organizations" });
await server.register(
async (projectServer) => {
projectServer.register(registerProjectRoleRouter);
projectServer.register(registerProjectRouter);
async (projectRouter) => {
await projectRouter.register(registerProjectRoleRouter);
await projectRouter.register(registerProjectRouter);
await projectRouter.register(registerTrustedIpRouter);
},
{ prefix: "/workspace" }
);
@@ -0,0 +1,365 @@
import { z } from "zod";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type";
export const registerLicenseRouter = async (server: FastifyZodProvider) => {
server.route({
url: "/:organizationId/plans/table",
method: "GET",
schema: {
querystring: z.object({ billingCycle: z.enum(["monthly", "yearly"]) }),
params: z.object({ organizationId: z.string().trim() }),
response: {
200: z.any()
}
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
const data = await server.services.license.getOrgPlansTableByBillCycle({
actorId: req.permission.id,
actor: req.permission.type,
orgId: req.params.organizationId,
billingCycle: req.query.billingCycle
});
return data;
}
});
server.route({
url: "/:organizationId/plan",
method: "GET",
schema: {
params: z.object({ organizationId: z.string().trim() }),
response: {
200: z.any()
}
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
const data = await server.services.license.getOrgPlan({
actorId: req.permission.id,
actor: req.permission.type,
orgId: req.params.organizationId
});
return data;
}
});
server.route({
url: "/:organizationId/plans",
method: "GET",
schema: {
params: z.object({ organizationId: z.string().trim() }),
querystring: z.object({ workspaceId: z.string().trim().optional() }),
response: {
200: z.any()
}
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
const data = await server.services.license.getOrgPlan({
actorId: req.permission.id,
actor: req.permission.type,
orgId: req.params.organizationId
});
return data;
}
});
server.route({
url: "/:organizationId/session/trial",
method: "POST",
schema: {
params: z.object({ organizationId: z.string().trim() }),
body: z.object({ success_url: z.string().trim() }),
response: {
200: z.any()
}
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
const data = await server.services.license.startOrgTrail({
actorId: req.permission.id,
actor: req.permission.type,
orgId: req.params.organizationId,
success_url: req.body.success_url
});
return data;
}
});
server.route({
url: "/:organizationId/plan/billing",
method: "GET",
schema: {
params: z.object({ organizationId: z.string().trim() }),
response: {
200: z.any()
}
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
const data = await server.services.license.getOrgBillingInfo({
actorId: req.permission.id,
actor: req.permission.type,
orgId: req.params.organizationId
});
return data;
}
});
server.route({
url: "/:organizationId/plan/table",
method: "GET",
schema: {
params: z.object({ organizationId: z.string().trim() }),
response: {
200: z.any()
}
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
const data = await server.services.license.getOrgPlanTable({
actorId: req.permission.id,
actor: req.permission.type,
orgId: req.params.organizationId
});
return data;
}
});
server.route({
url: "/:organizationId/billing-details",
method: "GET",
schema: {
params: z.object({ organizationId: z.string().trim() }),
response: {
200: z.any()
}
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
const data = await server.services.license.getOrgBillingDetails({
actorId: req.permission.id,
actor: req.permission.type,
orgId: req.params.organizationId
});
return data;
}
});
server.route({
url: "/:organizationId/billing-details",
method: "PATCH",
schema: {
params: z.object({ organizationId: z.string().trim() }),
body: z.object({
email: z.string().trim().email().optional(),
name: z.string().trim().optional()
}),
response: {
200: z.any()
}
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
const data = await server.services.license.updateOrgBillingDetails({
actorId: req.permission.id,
actor: req.permission.type,
orgId: req.params.organizationId,
name: req.body.name,
email: req.body.email
});
return data;
}
});
server.route({
url: "/:organizationId/billing-details/payment-methods",
method: "GET",
schema: {
params: z.object({ organizationId: z.string().trim() }),
response: {
200: z.any()
}
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
const data = await server.services.license.getOrgPmtMethods({
actorId: req.permission.id,
actor: req.permission.type,
orgId: req.params.organizationId
});
return data;
}
});
server.route({
url: "/:organizationId/billing-details/payment-methods",
method: "POST",
schema: {
params: z.object({ organizationId: z.string().trim() }),
body: z.object({
success_url: z.string().trim(),
cancel_url: z.string().trim()
}),
response: {
200: z.any()
}
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
const data = await server.services.license.addOrgPmtMethods({
actorId: req.permission.id,
actor: req.permission.type,
orgId: req.params.organizationId,
success_url: req.body.success_url,
cancel_url: req.body.cancel_url
});
return data;
}
});
server.route({
url: "/:organizationId/billing-details/payment-methods/:pmtMethodId",
method: "DELETE",
schema: {
params: z.object({
organizationId: z.string().trim(),
pmtMethodId: z.string().trim()
}),
response: {
200: z.any()
}
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
const data = await server.services.license.delOrgPmtMethods({
actorId: req.permission.id,
actor: req.permission.type,
orgId: req.params.organizationId,
pmtMethodId: req.params.pmtMethodId
});
return data;
}
});
server.route({
url: "/:organizationId/billing-details/tax-ids",
method: "GET",
schema: {
params: z.object({
organizationId: z.string().trim()
}),
response: {
200: z.any()
}
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
const data = await server.services.license.getOrgTaxIds({
actorId: req.permission.id,
actor: req.permission.type,
orgId: req.params.organizationId
});
return data;
}
});
server.route({
url: "/:organizationId/billing-details/tax-ids",
method: "POST",
schema: {
params: z.object({
organizationId: z.string().trim()
}),
body: z.object({
type: z.string().trim(),
value: z.string().trim()
}),
response: {
200: z.any()
}
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
const data = await server.services.license.addOrgTaxId({
actorId: req.permission.id,
actor: req.permission.type,
orgId: req.params.organizationId,
type: req.body.type,
value: req.body.value
});
return data;
}
});
server.route({
url: "/:organizationId/billing-details/tax-ids/:taxId",
method: "DELETE",
schema: {
params: z.object({
organizationId: z.string().trim(),
taxId: z.string().trim()
}),
response: {
200: z.any()
}
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
const data = await server.services.license.delOrgTaxId({
actorId: req.permission.id,
actor: req.permission.type,
orgId: req.params.organizationId,
taxId: req.params.taxId
});
return data;
}
});
server.route({
url: "/:organizationId/invoices",
method: "GET",
schema: {
params: z.object({
organizationId: z.string().trim()
}),
response: {
200: z.any()
}
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
const data = await server.services.license.getOrgTaxInvoices({
actorId: req.permission.id,
actor: req.permission.type,
orgId: req.params.organizationId
});
return data;
}
});
server.route({
url: "/:organizationId/licenses",
method: "GET",
schema: {
params: z.object({
organizationId: z.string().trim()
}),
response: {
200: z.any()
}
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
const data = await server.services.license.getOrgLicenses({
actorId: req.permission.id,
actor: req.permission.type,
orgId: req.params.organizationId
});
return data;
}
});
};
@@ -0,0 +1,158 @@
import { z } from "zod";
import { TrustedIpsSchema } from "@app/db/schemas";
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type";
export const registerTrustedIpRouter = async (server: FastifyZodProvider) => {
server.route({
url: "/:workspaceId/trusted-ips",
method: "GET",
schema: {
params: z.object({
workspaceId: z.string().trim()
}),
response: {
200: z.object({
trustedIps: TrustedIpsSchema.array()
})
}
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
const trustedIps = await server.services.trustedIp.listIpsByProjectId({
projectId: req.params.workspaceId,
actor: req.permission.type,
actorId: req.permission.id
});
return { trustedIps };
}
});
server.route({
url: "/:workspaceId/trusted-ips",
method: "POST",
schema: {
params: z.object({
workspaceId: z.string().trim()
}),
body: z.object({
ipAddress: z.string().trim(),
comment: z.string().trim().default(""),
isActive: z.boolean()
}),
response: {
200: z.object({
trustedIp: TrustedIpsSchema
})
}
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
const { trustedIp, project } = await server.services.trustedIp.addProjectIp({
projectId: req.params.workspaceId,
actor: req.permission.type,
actorId: req.permission.id,
...req.body
});
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
orgId: project.orgId,
projectId: project.id,
event: {
type: EventType.ADD_TRUSTED_IP,
metadata: {
trustedIpId: trustedIp.id.toString(),
ipAddress: trustedIp.ipAddress,
prefix: trustedIp.prefix as number
}
}
});
return { trustedIp };
}
});
server.route({
url: "/:workspaceId/trusted-ips/:trustedIpId",
method: "PATCH",
schema: {
params: z.object({
workspaceId: z.string().trim(),
trustedIpId: z.string().trim()
}),
body: z.object({
ipAddress: z.string().trim(),
comment: z.string().trim().default("")
}),
response: {
200: z.object({
trustedIp: TrustedIpsSchema
})
}
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
const { trustedIp, project } = await server.services.trustedIp.updateProjectIp({
projectId: req.params.workspaceId,
actor: req.permission.type,
actorId: req.permission.id,
trustedIpId: req.params.trustedIpId,
...req.body
});
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
orgId: project.orgId,
projectId: project.id,
event: {
type: EventType.UPDATE_TRUSTED_IP,
metadata: {
trustedIpId: trustedIp.id.toString(),
ipAddress: trustedIp.ipAddress,
prefix: trustedIp.prefix as number
}
}
});
return { trustedIp };
}
});
server.route({
url: "/:workspaceId/trusted-ips/:trustedIpId",
method: "DELETE",
schema: {
params: z.object({
workspaceId: z.string().trim(),
trustedIpId: z.string().trim()
}),
response: {
200: z.object({
trustedIp: TrustedIpsSchema
})
}
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
const { trustedIp, project } = await server.services.trustedIp.deleteProjectIp({
projectId: req.params.workspaceId,
actor: req.permission.type,
actorId: req.permission.id,
trustedIpId: req.params.trustedIpId
});
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
orgId: project.orgId,
projectId: project.id,
event: {
type: EventType.DELETE_TRUSTED_IP,
metadata: {
trustedIpId: trustedIp.id.toString(),
ipAddress: trustedIp.ipAddress,
prefix: trustedIp.prefix as number
}
}
});
return { trustedIp };
}
});
};
@@ -1,18 +1,24 @@
import { QueueJobs, QueueName, TQueueServiceFactory } from "@app/queue";
import { TProjectDalFactory } from "@app/services/project/project-dal";
import { TLicenseServiceFactory } from "../license/license-service";
import { TAuditLogDalFactory } from "./audit-log-dal";
import { TCreateAuditLogDTO } from "./audit-log-types";
type TAuditLogQueueServiceFactoryDep = {
auditLogDal: TAuditLogDalFactory;
queueService: TQueueServiceFactory;
projectDal: Pick<TProjectDalFactory, "findById">;
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
};
export type TAuditLogQueueServiceFactory = ReturnType<typeof auditLogQueueServiceFactory>;
export const auditLogQueueServiceFactory = ({
auditLogDal,
queueService
queueService,
projectDal,
licenseService
}: TAuditLogQueueServiceFactoryDep) => {
const pushToLog = async (data: TCreateAuditLogDTO) => {
await queueService.queue(QueueName.AuditLog, QueueJobs.AuditLog, data, {
@@ -24,8 +30,19 @@ export const auditLogQueueServiceFactory = ({
};
queueService.start(QueueName.AuditLog, async (job) => {
const { actor, orgId, event, ipAddress, projectId, userAgent, userAgentType } = job.data;
const { actor, event, ipAddress, projectId, userAgent, userAgentType } = job.data;
let { orgId } = job.data;
const MS_IN_DAY = 24 * 60 * 60 * 1000;
if (!orgId) {
// it will never be undefined for both org and project id
// TODO(akhilmhdh): use caching here in dal to avoid db calls
const project = await projectDal.findById(projectId as string);
orgId = project.orgId;
}
const plan = await licenseService.getPlan(orgId);
const ttl = plan.auditLogsRetentionDays * MS_IN_DAY;
await auditLogDal.create({
actor: actor.type,
actorMetadata: actor.metadata,
@@ -34,7 +51,7 @@ export const auditLogQueueServiceFactory = ({
ipAddress,
orgId,
eventType: event.type,
expiresAt: new Date(Date.now() + 30 * MS_IN_DAY),
expiresAt: new Date(Date.now() + ttl),
eventMetadata: event.metadata,
userAgentType
});
@@ -0,0 +1,97 @@
import axios, { AxiosError } from "axios";
import { getConfig } from "@app/lib/config/env";
import { request } from "@app/lib/config/request";
import { TFeatureSet } from "./license-types";
export const getDefaultOnPremFeatures = (): TFeatureSet => ({
_id: null,
slug: null,
tier: -1,
workspaceLimit: null,
workspacesUsed: 0,
memberLimit: null,
membersUsed: 0,
environmentLimit: null,
environmentsUsed: 0,
secretVersioning: true,
pitRecovery: false,
ipAllowlisting: false,
rbac: false,
customRateLimits: false,
customAlerts: false,
auditLogs: false,
auditLogsRetentionDays: 0,
samlSSO: false,
status: null,
trial_end: null,
has_used_trial: true,
secretApproval: false,
secretRotation: true
});
export const setupLicenceRequestWithStore = (
baseURL: string,
refreshUrl: string,
licenseKey: string
) => {
let token: string;
const licenceReq = axios.create({
baseURL,
timeout: 15 * 1000,
signal: AbortSignal.timeout(15 * 1000)
});
const refreshLicence = async () => {
const appCfg = getConfig();
const {
data: { token: authToken }
} = await request.post(
refreshUrl,
{},
{
baseURL: appCfg.LICENSE_SERVER_URL,
headers: {
"X-API-KEY": licenseKey
}
}
);
token = authToken;
return token;
};
licenceReq.interceptors.request.use(
(config) => {
if (token && config.headers) {
// eslint-disable-next-line no-param-reassign
config.headers.Authorization = `Bearer ${token}`;
}
return config;
},
(err) => Promise.reject(err)
);
licenceReq.interceptors.response.use(
(response) => response,
async (err) => {
const originalRequest = err.config;
// eslint-disable-next-line
if ((err as AxiosError)?.response?.status === 401 && !originalRequest._retry) {
// eslint-disable-next-line
originalRequest._retry = true;
// refresh
await refreshLicence();
licenceReq.defaults.headers.common.Authorization = `Bearer ${token}`;
return licenceReq(originalRequest);
}
return Promise.reject(err);
}
);
return { request: licenceReq, refreshLicence };
};
@@ -1,6 +1,27 @@
import { Knex } from "knex";
import { TDbClient } from "@app/db";
import { TableName } from "@app/db/schemas";
import { OrgMembershipStatus, TableName } from "@app/db/schemas";
import { DatabaseError } from "@app/lib/errors";
export type TLicenseDalFactory = ReturnType<typeof licenseDalFactory>;
export const licenseDalFactory = (db: TDbClient) => ({ });
export const licenseDalFactory = (db: TDbClient) => {
const countOfOrgMembers = async (orgId: string | null, tx?: Knex) => {
try {
const doc = await (tx || db)(TableName.OrgMembership)
.where({ status: OrgMembershipStatus.Accepted })
.andWhere((bd) => {
if (orgId) {
bd.where({ orgId });
}
})
.count();
return doc?.[0].count;
} catch (error) {
throw new DatabaseError({ error, name: "Count of Org Members" });
}
};
return { countOfOrgMembers };
};
@@ -1,34 +1,523 @@
import axios from "axios";
import { ForbiddenError } from "@casl/ability";
import NodeCache from "node-cache";
import { getConfig } from "@app/lib/config/env";
import { BadRequestError } from "@app/lib/errors";
import { logger } from "@app/lib/logger";
import { TOrgDalFactory } from "@app/services/org/org-dal";
import { OrgPermissionActions, OrgPermissionSubjects } from "../permission/org-permission";
import { TPermissionServiceFactory } from "../permission/permission-service";
import { getDefaultOnPremFeatures, setupLicenceRequestWithStore } from "./licence-fns";
import { TLicenseDalFactory } from "./license-dal";
import {
InstanceType,
TAddOrgPmtMethodDTO,
TAddOrgTaxIdDTO,
TDelOrgPmtMethodDTO,
TDelOrgTaxIdDTO,
TFeatureSet,
TGetOrgBillInfoDTO,
TGetOrgTaxIdDTO,
TOrgInvoiceDTO,
TOrgLicensesDTO,
TOrgPlanDTO,
TOrgPlansTableDTO,
TOrgPmtMethodsDTO,
TStartOrgTrailDTO,
TUpdateOrgBillingDetailsDTO
} from "./license-types";
type TLicenseServiceFactoryDep = {
orgDal: Pick<TOrgDalFactory, "findOrgById">;
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">;
licenseDal: TLicenseDalFactory;
};
export type TLicenseServiceFactory = ReturnType<typeof licenseServiceFactory>;
export const licenseServiceFactory = ({ licenseDal }: TLicenseServiceFactoryDep) => {
const LICENSE_SERVER_CLOUD_LOGIN = "/api/auth/v1/license-server-login";
const LICENSE_SERVER_ON_PREM_LOGIN = "/api/auth/v1/licence-login";
const FEATURE_CACHE_KEY = (orgId: string, projectId?: string) => `${orgId}-${projectId || ""}`;
export const licenseServiceFactory = ({
orgDal,
permissionService,
licenseDal
}: TLicenseServiceFactoryDep) => {
let isValidLicense = false;
let instanceType = InstanceType.OnPrem;
let onPremFeatures: TFeatureSet = getDefaultOnPremFeatures();
const featureStore = new NodeCache({ stdTTL: 60 });
const appCfg = getConfig();
const licenceApi = axios.create({
baseURL: appCfg.LICENCE_SERVER_URL
});
const licenseServerCloudApi = setupLicenceRequestWithStore(
appCfg.LICENSE_SERVER_URL || "",
LICENSE_SERVER_CLOUD_LOGIN,
appCfg.LICENSE_SERVER_KEY || ""
);
const licenseServerOnPremApi = setupLicenceRequestWithStore(
appCfg.LICENSE_SERVER_URL || "",
LICENSE_SERVER_ON_PREM_LOGIN,
appCfg.LICENSE_KEY || ""
);
const init = async () => {
try {
if (appCfg.LICENSE_SERVER_KEY) {
const token = await licenseServerCloudApi.refreshLicence();
if (token) instanceType = InstanceType.Cloud;
logger.info(`Instance type: ${InstanceType.Cloud}`);
isValidLicense = true;
return;
}
if (appCfg.LICENSE_KEY) {
const token = await licenseServerOnPremApi.refreshLicence();
if (token) {
const {
data: { currentPlan }
} = await licenseServerOnPremApi.request.get<{ currentPlan: TFeatureSet }>(
"/api/license/v1/plan"
);
onPremFeatures = currentPlan;
instanceType = InstanceType.EnterpriseOnPrem;
logger.info(`Instance type: ${InstanceType.EnterpriseOnPrem}`);
isValidLicense = true;
}
}
} catch (error) {
logger.error(error);
}
};
const getPlan = async (orgId: string, projectId?: string) => {
try {
if (instanceType === InstanceType.Cloud) {
const cachedPlan = featureStore.get<TFeatureSet>(FEATURE_CACHE_KEY(orgId, projectId));
if (cachedPlan) return cachedPlan;
const org = await orgDal.findOrgById(orgId);
if (!org) throw new BadRequestError({ message: "Org not found" });
const {
data: { currentPlan }
} = await licenseServerCloudApi.request.get<{ currentPlan: TFeatureSet }>(
`/api/license-server/v1/customers/${org.customerId}/cloud-plan`,
{
params: {
workspaceId: projectId
}
}
);
featureStore.set(FEATURE_CACHE_KEY(org.id, projectId), currentPlan);
return currentPlan;
}
} catch (error) {
logger.error(error);
return onPremFeatures;
}
return onPremFeatures;
};
const refreshPlan = async (orgId: string, projectId?: string) => {
if (instanceType === InstanceType.Cloud) {
featureStore.del(FEATURE_CACHE_KEY(orgId, projectId));
await getPlan(orgId, projectId);
}
};
const generateOrgCustomerId = async (orgName: string, email: string) => {
const {
data: { customerId }
} = await licenceApi.post("/api/license-server/v1/customers", { email, name: orgName });
return customerId;
if (instanceType === InstanceType.Cloud) {
const {
data: { customerId }
} = await licenseServerCloudApi.request.post(
"/api/license-server/v1/customers",
{
email,
name: orgName
},
{ timeout: 5000, signal: AbortSignal.timeout(5000) }
);
return customerId;
}
};
const removeOrgCustomer = async (customerId: string) => {
await licenceApi.delete(`/api/license-server/v1/customers/${customerId}`);
await licenseServerCloudApi.request.delete(`/api/license-server/v1/customers/${customerId}`);
};
const updateSubscriptionOrgMemberCount = async (orgId: string) => {
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);
if (org?.customerId) {
await licenseServerCloudApi.request.patch(
`/api/license-server/v1/customers/${org.customerId}/cloud-plan`,
{
quantity: count
}
);
}
featureStore.del(orgId);
} else if (instanceType === InstanceType.EnterpriseOnPrem) {
const usedSeats = await licenseDal.countOfOrgMembers(null);
await licenseServerOnPremApi.request.patch(`/api/license/v1/license`, { usedSeats });
}
await refreshPlan(orgId);
};
// below all are api calls
const getOrgPlansTableByBillCycle = async ({
orgId,
actor,
actorId,
billingCycle
}: TOrgPlansTableDTO) => {
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId);
ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionActions.Read,
OrgPermissionSubjects.Billing
);
const { data } = await licenseServerCloudApi.request.get(
`/api/license-server/v1/cloud-products?billing-cycle=${billingCycle}`
);
return data;
};
const getOrgPlan = async ({ orgId, actor, actorId, projectId }: TOrgPlanDTO) => {
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId);
ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionActions.Read,
OrgPermissionSubjects.Billing
);
const plan = await getPlan(orgId, projectId);
return plan;
};
const startOrgTrail = async ({ orgId, actorId, actor, success_url }: TStartOrgTrailDTO) => {
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId);
ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionActions.Create,
OrgPermissionSubjects.Billing
);
ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionActions.Edit,
OrgPermissionSubjects.Billing
);
const organization = await orgDal.findOrgById(orgId);
if (!organization) {
throw new BadRequestError({
message: "Failed to find organization"
});
}
const {
data: { url }
} = await licenseServerCloudApi.request.post(
`/api/license-server/v1/customers/${organization.customerId}/session/trail`,
{ success_url }
);
featureStore.del(FEATURE_CACHE_KEY(orgId));
return { url };
};
const getOrgBillingInfo = async ({ orgId, actor, actorId }: TGetOrgBillInfoDTO) => {
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId);
ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionActions.Read,
OrgPermissionSubjects.Billing
);
const organization = await orgDal.findOrgById(orgId);
if (!organization) {
throw new BadRequestError({
message: "Failed to find organization"
});
}
const { data } = await licenseServerCloudApi.request.get(
`/api/license-server/v1/customers/${organization.customerId}/cloud-plan/billing`
);
return data;
};
// returns org current plan feature table
const getOrgPlanTable = async ({ orgId, actor, actorId }: TGetOrgBillInfoDTO) => {
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId);
ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionActions.Read,
OrgPermissionSubjects.Billing
);
const organization = await orgDal.findOrgById(orgId);
if (!organization) {
throw new BadRequestError({
message: "Failed to find organization"
});
}
const { data } = await licenseServerCloudApi.request.get(
`/api/license-server/v1/customers/${organization.customerId}/cloud-plan/table`
);
return data;
};
const getOrgBillingDetails = async ({ orgId, actor, actorId }: TGetOrgBillInfoDTO) => {
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId);
ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionActions.Read,
OrgPermissionSubjects.Billing
);
const organization = await orgDal.findOrgById(orgId);
if (!organization) {
throw new BadRequestError({
message: "Failed to find organization"
});
}
const { data } = await licenseServerCloudApi.request.get(
`/api/license-server/v1/customers/${organization.customerId}/billing-details`
);
return data;
};
const updateOrgBillingDetails = async ({
actorId,
actor,
orgId,
name,
email
}: TUpdateOrgBillingDetailsDTO) => {
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId);
ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionActions.Read,
OrgPermissionSubjects.Billing
);
const organization = await orgDal.findOrgById(orgId);
if (!organization) {
throw new BadRequestError({
message: "Failed to find organization"
});
}
const { data } = await licenseServerCloudApi.request.patch(
`/api/license-server/v1/customers/${organization.customerId}/billing-details`,
{
name,
email
}
);
return data;
};
const getOrgPmtMethods = async ({ orgId, actor, actorId }: TOrgPmtMethodsDTO) => {
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId);
ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionActions.Read,
OrgPermissionSubjects.Billing
);
const organization = await orgDal.findOrgById(orgId);
if (!organization) {
throw new BadRequestError({
message: "Failed to find organization"
});
}
const {
data: { pmtMethods }
} = await licenseServerCloudApi.request.get(
`/api/license-server/v1/customers/${organization.customerId}/billing-details/payment-methods`
);
return pmtMethods;
};
const addOrgPmtMethods = async ({
orgId,
actor,
actorId,
success_url,
cancel_url
}: TAddOrgPmtMethodDTO) => {
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId);
ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionActions.Read,
OrgPermissionSubjects.Billing
);
const organization = await orgDal.findOrgById(orgId);
if (!organization) {
throw new BadRequestError({
message: "Failed to find organization"
});
}
const {
data: { url }
} = await licenseServerCloudApi.request.post(
`/api/license-server/v1/customers/${organization.customerId}/billing-details/payment-methods`,
{
success_url,
cancel_url
}
);
return { url };
};
const delOrgPmtMethods = async ({ actorId, actor, orgId, pmtMethodId }: TDelOrgPmtMethodDTO) => {
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId);
ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionActions.Read,
OrgPermissionSubjects.Billing
);
const organization = await orgDal.findOrgById(orgId);
if (!organization) {
throw new BadRequestError({
message: "Failed to find organization"
});
}
const { data } = await licenseServerCloudApi.request.delete(
`/api/license-server/v1/customers/${organization.customerId}/billing-details/payment-methods/${pmtMethodId}`
);
return data;
};
const getOrgTaxIds = async ({ orgId, actor, actorId }: TGetOrgTaxIdDTO) => {
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId);
ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionActions.Read,
OrgPermissionSubjects.Billing
);
const organization = await orgDal.findOrgById(orgId);
if (!organization) {
throw new BadRequestError({
message: "Failed to find organization"
});
}
const {
data: { tax_ids: taxIds }
} = await licenseServerCloudApi.request.get(
`/api/license-server/v1/customers/${organization.customerId}/billing-details/tax-ids`
);
return taxIds;
};
const addOrgTaxId = async ({ actorId, actor, orgId, type, value }: TAddOrgTaxIdDTO) => {
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId);
ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionActions.Read,
OrgPermissionSubjects.Billing
);
const organization = await orgDal.findOrgById(orgId);
if (!organization) {
throw new BadRequestError({
message: "Failed to find organization"
});
}
const { data } = await licenseServerCloudApi.request.post(
`/api/license-server/v1/customers/${organization.customerId}/billing-details/tax-ids`,
{
type,
value
}
);
return data;
};
const delOrgTaxId = async ({ orgId, actor, actorId, taxId }: TDelOrgTaxIdDTO) => {
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId);
ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionActions.Read,
OrgPermissionSubjects.Billing
);
const organization = await orgDal.findOrgById(orgId);
if (!organization) {
throw new BadRequestError({
message: "Failed to find organization"
});
}
const { data } = await licenseServerCloudApi.request.delete(
`/api/license-server/v1/customers/${organization.customerId}/billing-details/tax-ids/${taxId}`
);
return data;
};
const getOrgTaxInvoices = async ({ actorId, actor, orgId }: TOrgInvoiceDTO) => {
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId);
ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionActions.Read,
OrgPermissionSubjects.Billing
);
const organization = await orgDal.findOrgById(orgId);
if (!organization) {
throw new BadRequestError({
message: "Failed to find organization"
});
}
const {
data: { invoices }
} = await licenseServerCloudApi.request.get(
`/api/license-server/v1/customers/${organization.customerId}/invoices`
);
return invoices;
};
const getOrgLicenses = async ({ orgId, actor, actorId }: TOrgLicensesDTO) => {
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId);
ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionActions.Read,
OrgPermissionSubjects.Billing
);
const organization = await orgDal.findOrgById(orgId);
if (!organization) {
throw new BadRequestError({
message: "Failed to find organization"
});
}
const {
data: { licenses }
} = await licenseServerCloudApi.request.get(
`/api/license-server/v1/customers/${organization.customerId}/licenses`
);
return licenses;
};
return {
generateOrgCustomerId,
removeOrgCustomer
removeOrgCustomer,
init,
get isValidLicense() {
return isValidLicense;
},
getPlan,
updateSubscriptionOrgMemberCount,
refreshPlan,
getOrgPlan,
getOrgPlansTableByBillCycle,
startOrgTrail,
getOrgBillingInfo,
getOrgPlanTable,
getOrgBillingDetails,
updateOrgBillingDetails,
addOrgPmtMethods,
delOrgPmtMethods,
getOrgPmtMethods,
getOrgLicenses,
getOrgTaxInvoices,
getOrgTaxIds,
addOrgTaxId,
delOrgTaxId
};
};
@@ -0,0 +1,72 @@
import { TOrgPermission } from "@app/lib/types";
export enum InstanceType {
OnPrem = "self-hosted",
EnterpriseOnPrem = "enterprise-self-hosted",
Cloud = "cloud"
}
export type TFeatureSet = {
_id: null;
slug: null;
tier: -1;
workspaceLimit: null;
workspacesUsed: 0;
memberLimit: null;
membersUsed: 0;
environmentLimit: null;
environmentsUsed: 0;
secretVersioning: true;
pitRecovery: false;
ipAllowlisting: false;
rbac: false;
customRateLimits: false;
customAlerts: false;
auditLogs: false;
auditLogsRetentionDays: 0;
samlSSO: false;
status: null;
trial_end: null;
has_used_trial: true;
secretApproval: false;
secretRotation: true;
};
export type TOrgPlansTableDTO = {
billingCycle: string;
} & TOrgPermission;
export type TOrgPlanDTO = {
projectId?: string;
} & TOrgPermission;
export type TStartOrgTrailDTO = {
success_url: string;
} & TOrgPermission;
export type TGetOrgBillInfoDTO = TOrgPermission;
export type TOrgPlanTableDTO = TOrgPermission;
export type TOrgBillingDetailsDTO = TOrgPermission;
export type TUpdateOrgBillingDetailsDTO = TOrgPermission & {
name?: string;
email?: string;
};
export type TOrgPmtMethodsDTO = TOrgPermission;
export type TAddOrgPmtMethodDTO = TOrgPermission & { success_url: string; cancel_url: string };
export type TDelOrgPmtMethodDTO = TOrgPermission & { pmtMethodId: string };
export type TGetOrgTaxIdDTO = TOrgPermission;
export type TAddOrgTaxIdDTO = TOrgPermission & { type: string; value: string };
export type TDelOrgTaxIdDTO = TOrgPermission & { taxId: string };
export type TOrgInvoiceDTO = TOrgPermission;
export type TOrgLicensesDTO = TOrgPermission;
@@ -20,6 +20,7 @@ import { TOrgBotDalFactory } from "@app/services/org/org-bot-dal";
import { TOrgDalFactory } from "@app/services/org/org-dal";
import { TUserDalFactory } from "@app/services/user/user-dal";
import { TLicenseServiceFactory } from "../license/license-service";
import { OrgPermissionActions, OrgPermissionSubjects } from "../permission/org-permission";
import { TPermissionServiceFactory } from "../permission/permission-service";
import { TSamlConfigDalFactory } from "./saml-config-dal";
@@ -40,6 +41,7 @@ type TSamlConfigServiceFactoryDep = {
>;
orgBotDal: Pick<TOrgBotDalFactory, "findOne">;
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">;
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
};
export type TSamlConfigServiceFactory = ReturnType<typeof samlConfigServiceFactory>;
@@ -49,7 +51,8 @@ export const samlConfigServiceFactory = ({
orgBotDal,
orgDal,
userDal,
permissionService
permissionService,
licenseService
}: TSamlConfigServiceFactoryDep) => {
const createSamlCfg = async ({
cert,
@@ -67,7 +70,12 @@ export const samlConfigServiceFactory = ({
OrgPermissionSubjects.Sso
);
// TODO(akhilmhdh-pg): licence check
const plan = await licenseService.getPlan(orgId);
if (!plan.samlSSO)
throw new BadRequestError({
message:
"Failed to update SAML SSO configuration due to plan restriction. Upgrade plan to update SSO configuration."
});
const orgBot = await orgBotDal.findOne({ orgId });
if (!orgBot)
@@ -123,6 +131,13 @@ export const samlConfigServiceFactory = ({
OrgPermissionActions.Edit,
OrgPermissionSubjects.Sso
);
const plan = await licenseService.getPlan(orgId);
if (!plan.samlSSO)
throw new BadRequestError({
message:
"Failed to update SAML SSO configuration due to plan restriction. Upgrade plan to update SSO configuration."
});
const updateQuery: TSamlConfigsUpdate = { authProvider, isActive };
const orgBot = await orgBotDal.findOne({ orgId });
if (!orgBot)
@@ -4,8 +4,10 @@ import Ajv from "ajv";
import { infisicalSymmetricEncypt } from "@app/lib/crypto/encryption";
import { BadRequestError } from "@app/lib/errors";
import { TProjectPermission } from "@app/lib/types";
import { TProjectDalFactory } from "@app/services/project/project-dal";
import { TProjectEnvDalFactory } from "@app/services/project-env/project-env-dal";
import { TLicenseServiceFactory } from "../license/license-service";
import { TPermissionServiceFactory } from "../permission/permission-service";
import { ProjectPermissionActions, ProjectPermissionSub } from "../permission/project-permission";
import { TSecretRotationDalFactory } from "./secret-rotation-dal";
@@ -22,6 +24,8 @@ import { rotationTemplates } from "./templates";
type TSecretRotationServiceFactoryDep = {
secretRotationDal: TSecretRotationDalFactory;
projectDal: Pick<TProjectDalFactory, "findById">;
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
projectEnvDal: Pick<TProjectEnvDalFactory, "findOne">;
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
secretRotationQueue: TSecretRotationQueueFactory;
@@ -34,7 +38,9 @@ export const secretRotationServiceFactory = ({
secretRotationDal,
permissionService,
projectEnvDal,
secretRotationQueue
secretRotationQueue,
licenseService,
projectDal
}: TSecretRotationServiceFactoryDep) => {
const getProviderTemplates = async ({ actor, actorId, projectId }: TProjectPermission) => {
const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId);
@@ -68,6 +74,14 @@ export const secretRotationServiceFactory = ({
const env = await projectEnvDal.findOne({ slug: environment });
if (!env) throw new BadRequestError({ message: "Environment not found" });
const project = await projectDal.findById(projectId);
const plan = await licenseService.getPlan(project.orgId);
if (!plan.secretRotation)
throw new BadRequestError({
message:
"Failed to add secret rotation due to plan restriction. Upgrade plan to add secret rotation."
});
const selectedTemplate = rotationTemplates.find(({ name }) => name === provider);
if (!selectedTemplate) throw new BadRequestError({ message: "Provider not found" });
const formattedInputs: Record<string, unknown> = {};
@@ -149,6 +163,14 @@ export const secretRotationServiceFactory = ({
const doc = await secretRotationDal.findById(rotationId);
if (!doc) throw new BadRequestError({ message: "Rotation not found" });
const project = await projectDal.findById(doc.projectId);
const plan = await licenseService.getPlan(project.orgId);
if (!plan.secretRotation)
throw new BadRequestError({
message:
"Failed to add secret rotation due to plan restriction. Upgrade plan to add secret rotation."
});
const { permission } = await permissionService.getProjectPermission(
actor,
actorId,
@@ -1,12 +1,13 @@
import { ForbiddenError } from "@casl/ability";
import { BadRequestError } from "@app/lib/errors";
import { BadRequestError, InternalServerError } from "@app/lib/errors";
import { groupBy } from "@app/lib/fn";
import { TSecretDalFactory } from "@app/services/secret/secret-dal";
import { TSecretVersionDalFactory } from "@app/services/secret/secret-version-dal";
import { TSecretFolderDalFactory } from "@app/services/secret-folder/secret-folder-dal";
import { TSecretFolderVersionDalFactory } from "@app/services/secret-folder/secret-folder-version-dal";
import { TLicenseServiceFactory } from "../license/license-service";
import { TPermissionServiceFactory } from "../permission/permission-service";
import { ProjectPermissionActions, ProjectPermissionSub } from "../permission/project-permission";
import {
@@ -34,6 +35,7 @@ type TSecretSnapshotServiceFactoryDep = {
"findById" | "findBySecretPath" | "delete" | "insertMany"
>;
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
licenseService: Pick<TLicenseServiceFactory, "isValidLicense">;
};
export type TSecretSnapshotServiceFactory = ReturnType<typeof secretSnapshotServiceFactory>;
@@ -46,7 +48,8 @@ export const secretSnapshotServiceFactory = ({
snapshotFolderDal,
folderDal,
secretDal,
permissionService
permissionService,
licenseService
}: TSecretSnapshotServiceFactoryDep) => {
const projectSecretSnapshotCount = async ({
environment,
@@ -109,6 +112,9 @@ export const secretSnapshotServiceFactory = ({
};
const performSnapshot = async (folderId: string) => {
if (!licenseService.isValidLicense)
throw new InternalServerError({ message: "Invalid license" });
const snapshot = await snapshotDal.transaction(async (tx) => {
const folder = await folderDal.findById(folderId, tx);
if (!folder) throw new BadRequestError({ message: "Folder not found" });
@@ -0,0 +1,10 @@
import { TDbClient } from "@app/db";
import { TableName } from "@app/db/schemas";
import { ormify } from "@app/lib/knex";
export type TTrustedIpDalFactory = ReturnType<typeof trustedIpDalFactory>;
export const trustedIpDalFactory = (db: TDbClient) => {
const trustedIpOrm = ormify(db, TableName.TrustedIps);
return trustedIpOrm;
};
@@ -0,0 +1,150 @@
import { ForbiddenError } from "@casl/ability";
import { BadRequestError } from "@app/lib/errors";
import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip";
import { TProjectPermission } from "@app/lib/types";
import { TProjectDalFactory } from "@app/services/project/project-dal";
import { TLicenseServiceFactory } from "../license/license-service";
import { TPermissionServiceFactory } from "../permission/permission-service";
import { ProjectPermissionActions, ProjectPermissionSub } from "../permission/project-permission";
import { TTrustedIpDalFactory } from "./trusted-ip-dal";
import { TCreateIpDTO, TDeleteIpDTO, TUpdateIpDTO } from "./trusted-ip-types";
type TTrustedIpServiceFactoryDep = {
trustedIpDal: TTrustedIpDalFactory;
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
projectDal: Pick<TProjectDalFactory, "findById">;
};
export type TTrustedIpServiceFactory = ReturnType<typeof trustedIpServiceFactory>;
export const trustedIpServiceFactory = ({
trustedIpDal,
permissionService,
licenseService,
projectDal
}: TTrustedIpServiceFactoryDep) => {
const listIpsByProjectId = async ({ projectId, actor, actorId }: TProjectPermission) => {
const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read,
ProjectPermissionSub.IpAllowList
);
const trustedIps = await trustedIpDal.find({
projectId
});
return trustedIps;
};
const addProjectIp = async ({
projectId,
actorId,
actor,
ipAddress: ip,
comment,
isActive
}: TCreateIpDTO) => {
const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Create,
ProjectPermissionSub.IpAllowList
);
const project = await projectDal.findById(projectId);
const plan = await licenseService.getPlan(project.orgId);
if (!plan.ipAllowlisting)
throw new BadRequestError({
message:
"Failed to add IP access range due to plan restriction. Upgrade plan to add IP access range."
});
const isValidIp = isValidIpOrCidr(ip);
if (!isValidIp)
throw new BadRequestError({
message: "The IP is not a valid IPv4, IPv6, or CIDR block"
});
const { ipAddress, type, prefix } = extractIPDetails(ip);
const trustedIp = await trustedIpDal.create({
projectId,
ipAddress,
type,
prefix,
isActive,
comment
});
return { trustedIp, project }; // for audit log
};
const updateProjectIp = async ({
projectId,
actorId,
actor,
ipAddress: ip,
comment,
trustedIpId
}: TUpdateIpDTO) => {
const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Create,
ProjectPermissionSub.IpAllowList
);
const project = await projectDal.findById(projectId);
const plan = await licenseService.getPlan(project.orgId);
if (!plan.ipAllowlisting)
throw new BadRequestError({
message:
"Failed to add IP access range due to plan restriction. Upgrade plan to add IP access range."
});
const isValidIp = isValidIpOrCidr(ip);
if (!isValidIp)
throw new BadRequestError({
message: "The IP is not a valid IPv4, IPv6, or CIDR block"
});
const { ipAddress, type, prefix } = extractIPDetails(ip);
const [trustedIp] = await trustedIpDal.update(
{ projectId, id: trustedIpId },
{
projectId,
ipAddress,
type,
prefix: prefix === undefined ? null : prefix,
comment
}
);
return { trustedIp, project }; // for audit log
};
const deleteProjectIp = async ({ projectId, actorId, actor, trustedIpId }: TDeleteIpDTO) => {
const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Create,
ProjectPermissionSub.IpAllowList
);
const project = await projectDal.findById(projectId);
const plan = await licenseService.getPlan(project.orgId);
if (!plan.ipAllowlisting)
throw new BadRequestError({
message:
"Failed to add IP access range due to plan restriction. Upgrade plan to add IP access range."
});
const [trustedIp] = await trustedIpDal.delete({ projectId, id: trustedIpId });
return { trustedIp, project }; // for audit log
};
return {
listIpsByProjectId,
addProjectIp,
updateProjectIp,
deleteProjectIp
};
};
@@ -0,0 +1,17 @@
import { TProjectPermission } from "@app/lib/types";
export type TCreateIpDTO = TProjectPermission & {
comment: string;
isActive?: boolean;
ipAddress: string;
};
export type TUpdateIpDTO = TProjectPermission & {
trustedIpId: string;
ipAddress: string;
comment: string;
};
export type TDeleteIpDTO = TProjectPermission & {
trustedIpId: string;
};