feat: completed license server integration

This commit is contained in:
Akhil Mohan
2024-01-13 22:55:43 +05:30
parent fe416556f2
commit c7572a3374
47 changed files with 1818 additions and 121 deletions

View File

@@ -22,6 +22,7 @@
"@fastify/swagger-ui": "^1.10.1",
"@node-saml/passport-saml": "^4.0.4",
"@octokit/rest": "^20.0.2",
"@octokit/webhooks-types": "^7.3.1",
"@ucast/mongo2js": "^1.3.4",
"ajv": "^8.12.0",
"argon2": "^0.31.2",
@@ -44,6 +45,7 @@
"lodash.isequal": "^4.5.0",
"mysql2": "^3.6.5",
"nanoid": "^5.0.4",
"node-cache": "^5.1.2",
"nodemailer": "^6.9.7",
"ora": "^7.0.1",
"passport-github": "^1.1.0",
@@ -60,7 +62,6 @@
"zod-to-json-schema": "^3.22.0"
},
"devDependencies": {
"@octokit/webhooks-types": "^7.3.1",
"@types/bcrypt": "^5.0.2",
"@types/jmespath": "^0.15.2",
"@types/jsonwebtoken": "^9.0.5",
@@ -2313,8 +2314,7 @@
"node_modules/@octokit/webhooks-types": {
"version": "7.3.1",
"resolved": "https://registry.npmjs.org/@octokit/webhooks-types/-/webhooks-types-7.3.1.tgz",
"integrity": "sha512-u6355ZsZnHwmxen30SrqnYb1pXieBFkYgkNzt+Ed4Ao5tupN1OErHfzwiV6hq6duGkDAYASbq7/uVJQ69PjLEg==",
"dev": true
"integrity": "sha512-u6355ZsZnHwmxen30SrqnYb1pXieBFkYgkNzt+Ed4Ao5tupN1OErHfzwiV6hq6duGkDAYASbq7/uVJQ69PjLEg=="
},
"node_modules/@octokit/webhooks/node_modules/@octokit/openapi-types": {
"version": "12.11.0",
@@ -5342,6 +5342,14 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/clone": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz",
"integrity": "sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==",
"engines": {
"node": ">=0.8"
}
},
"node_modules/cluster-key-slot": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz",
@@ -8929,6 +8937,17 @@
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-5.1.0.tgz",
"integrity": "sha512-eh0GgfEkpnoWDq+VY8OyvYhFEzBk6jIYbRKdIlyTiAXIVJ8PyBaKb0rp7oDtoddbdoHWhq8wwr+XZ81F1rpNdA=="
},
"node_modules/node-cache": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/node-cache/-/node-cache-5.1.2.tgz",
"integrity": "sha512-t1QzWwnk4sjLWaQAS8CHgOJ+RAfmHpxFWmc36IWTiWHQfs0w5JDMBS1b1ZxQteo0vVVuWJvIUKHDkkeK7vIGCg==",
"dependencies": {
"clone": "2.x"
},
"engines": {
"node": ">= 8.0.0"
}
},
"node_modules/node-fetch": {
"version": "2.7.0",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz",

View File

@@ -78,8 +78,8 @@
"@fastify/swagger-ui": "^1.10.1",
"@node-saml/passport-saml": "^4.0.4",
"@octokit/rest": "^20.0.2",
"@ucast/mongo2js": "^1.3.4",
"@octokit/webhooks-types": "^7.3.1",
"@ucast/mongo2js": "^1.3.4",
"ajv": "^8.12.0",
"argon2": "^0.31.2",
"aws-sdk": "^2.1532.0",
@@ -101,6 +101,7 @@
"lodash.isequal": "^4.5.0",
"mysql2": "^3.6.5",
"nanoid": "^5.0.4",
"node-cache": "^5.1.2",
"nodemailer": "^6.9.7",
"ora": "^7.0.1",
"passport-github": "^1.1.0",

View File

@@ -3,6 +3,7 @@ import "fastify";
import { TUsers } from "@app/db/schemas";
import { TAuditLogServiceFactory } from "@app/ee/services/audit-log/audit-log-service";
import { TCreateAuditLogDTO } from "@app/ee/services/audit-log/audit-log-types";
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import { TSamlConfigServiceFactory } from "@app/ee/services/saml-config/saml-config-service";
import { TSecretApprovalPolicyServiceFactory } from "@app/ee/services/secret-approval-policy/secret-approval-policy-service";
@@ -10,6 +11,7 @@ import { TSecretApprovalRequestServiceFactory } from "@app/ee/services/secret-ap
import { TSecretRotationServiceFactory } from "@app/ee/services/secret-rotation/secret-rotation-service";
import { TSecretScanningServiceFactory } from "@app/ee/services/secret-scanning/secret-scanning-service";
import { TSecretSnapshotServiceFactory } from "@app/ee/services/secret-snapshot/secret-snapshot-service";
import { TTrustedIpServiceFactory } from "@app/ee/services/trusted-ip/trusted-ip-service";
import { TAuthMode } from "@app/server/plugins/auth/inject-identity";
import { TApiKeyServiceFactory } from "@app/services/api-key/api-key-service";
import { TAuthLoginFactory } from "@app/services/auth/auth-login-service";
@@ -101,6 +103,8 @@ declare module "fastify" {
saml: TSamlConfigServiceFactory;
auditLog: TAuditLogServiceFactory;
secretScanning: TSecretScanningServiceFactory;
license: TLicenseServiceFactory;
trustedIp: TTrustedIpServiceFactory;
};
// this is exclusive use for middlewares in which we need to inject data

View File

@@ -148,6 +148,9 @@ import {
TSuperAdmin,
TSuperAdminInsert,
TSuperAdminUpdate,
TTrustedIps,
TTrustedIpsInsert,
TTrustedIpsUpdate,
TUserActions,
TUserActionsInsert,
TUserActionsUpdate,
@@ -393,6 +396,11 @@ declare module "knex/types/tables" {
TSecretScanningGitRisksInsert,
TSecretScanningGitRisksUpdate
>;
[TableName.TrustedIps]: Knex.CompositeTableType<
TTrustedIps,
TTrustedIpsInsert,
TTrustedIpsUpdate
>;
// Junction tables
[TableName.JnSecretTag]: Knex.CompositeTableType<
TSecretTagJunction,

View File

@@ -6,7 +6,7 @@ import { createOnUpdateTrigger, dropOnUpdateTrigger } from "../utils";
export async function up(knex: Knex): Promise<void> {
if (!(await knex.schema.hasTable(TableName.Project))) {
await knex.schema.createTable(TableName.Project, (t) => {
t.string("id").primary().defaultTo(knex.fn.uuid());
t.string("id", 36).primary().defaultTo(knex.fn.uuid());
t.string("name").notNullable();
t.boolean("autoCapitalization").defaultTo(true);
t.uuid("orgId").notNullable();

View File

@@ -6,7 +6,7 @@ import { createOnUpdateTrigger, dropOnUpdateTrigger } from "../utils";
export async function up(knex: Knex): Promise<void> {
if (!(await knex.schema.hasTable(TableName.ServiceToken))) {
await knex.schema.createTable(TableName.ServiceToken, (t) => {
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
t.string("id", 36).primary().defaultTo(knex.fn.uuid());
t.string("name").notNullable();
t.jsonb("scopes").notNullable();
t.specificType("permissions", "text[]").notNullable();

View File

@@ -0,0 +1,26 @@
import { Knex } from "knex";
import { TableName } from "../schemas";
import { createOnUpdateTrigger, dropOnUpdateTrigger } from "../utils";
export async function up(knex: Knex): Promise<void> {
if (!(await knex.schema.hasTable(TableName.TrustedIps))) {
await knex.schema.createTable(TableName.TrustedIps, (t) => {
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
t.string("ipAddress").notNullable();
t.string("type").notNullable();
t.integer("prefix");
t.boolean("isActive").defaultTo(true);
t.string("comment");
t.string("projectId").notNullable();
t.foreign("projectId").references("id").inTable(TableName.Project);
t.timestamps(true, true, true);
});
}
await createOnUpdateTrigger(knex, TableName.TrustedIps);
}
export async function down(knex: Knex): Promise<void> {
await knex.schema.dropTableIfExists(TableName.TrustedIps);
await dropOnUpdateTrigger(knex, TableName.TrustedIps);
}

View File

@@ -48,6 +48,7 @@ export * from "./secret-versions";
export * from "./secrets";
export * from "./service-tokens";
export * from "./super-admin";
export * from "./trusted-ips";
export * from "./user-actions";
export * from "./user-encryption-keys";
export * from "./users";

View File

@@ -53,6 +53,7 @@ export enum TableName {
GitAppInstallSession = "git_app_install_sessions",
GitAppOrg = "git_app_org",
SecretScanningGitRisk = "secret_scanning_git_risks",
TrustedIps = "trusted_ips",
// junction tables
JnSecretTag = "secret_tag_junction",
JnSecretVersionTag = "secret_version_tag_junction"

View File

@@ -0,0 +1,24 @@
// Code generated by automation script, DO NOT EDIT.
// Automated by pulling database and generating zod schema
// To update. Just run npm run generate:schema
// Written by akhilmhdh.
import { z } from "zod";
import { TImmutableDBKeys } from "./models";
export const TrustedIpsSchema = z.object({
id: z.string().uuid(),
ipAddress: z.string(),
type: z.string(),
prefix: z.number().nullable().optional(),
isActive: z.boolean().default(true).nullable().optional(),
comment: z.string().nullable().optional(),
projectId: z.string(),
createdAt: z.date(),
updatedAt: z.date(),
});
export type TTrustedIps = z.infer<typeof TrustedIpsSchema>;
export type TTrustedIpsInsert = Omit<TTrustedIps, TImmutableDBKeys>;
export type TTrustedIpsUpdate = Partial<Omit<TTrustedIps, TImmutableDBKeys>>;

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" }
);

View File

@@ -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;
}
});
};

View File

@@ -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 };
}
});
};

View File

@@ -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
});

View File

@@ -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 };
};

View File

@@ -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 };
};

View File

@@ -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
};
};

View File

@@ -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;

View File

@@ -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)

View File

@@ -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,

View File

@@ -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" });

View File

@@ -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;
};

View File

@@ -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
};
};

View File

@@ -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;
};

View File

@@ -83,8 +83,9 @@ const envSchema = z
SECRET_SCANNING_GIT_APP_ID: zpStr(z.string().optional()),
SECRET_SCANNING_PRIVATE_KEY: zpStr(z.string().optional()),
// LICENCE
LICENCE_SERVER_URL: zpStr(z.string().optional()),
LICENCE_SERVER_KEY: zpStr(z.string().optional())
LICENSE_SERVER_URL: zpStr(z.string().optional()),
LICENSE_SERVER_KEY: zpStr(z.string().optional()),
LICENSE_KEY: zpStr(z.string().optional())
})
.transform((data) => ({
...data,

View File

@@ -11,6 +11,18 @@ export class DatabaseError extends Error {
}
}
export class InternalServerError extends Error {
name: string;
error: unknown;
constructor({ name, error, message }: { message?: string; name?: string; error?: unknown }) {
super(message || "Something went wrong");
this.name = name || "InternalServerError";
this.error = error;
}
}
export class UnauthorizedError extends Error {
name: string;

View File

@@ -4,7 +4,6 @@ import fp from "fastify-plugin";
import { jsonSchemaTransform } from "./fastify-zod";
// TODO(akhilmhdh-pg): change the localhost port later
export const fastifySwagger = fp(async (fastify) => {
await fastify.register(swagger, {
transform: jsonSchemaTransform,
@@ -16,7 +15,7 @@ export const fastifySwagger = fp(async (fastify) => {
},
servers: [
{
url: "http://localhost:4000",
url: "http://localhost:8080",
description: "Local server"
},
{

View File

@@ -5,6 +5,8 @@ import { registerV1EERoutes } from "@app/ee/routes/v1";
import { auditLogDalFactory } from "@app/ee/services/audit-log/audit-log-dal";
import { auditLogQueueServiceFactory } from "@app/ee/services/audit-log/audit-log-queue";
import { auditLogServiceFactory } from "@app/ee/services/audit-log/audit-log-service";
import { licenseDalFactory } from "@app/ee/services/license/license-dal";
import { licenseServiceFactory } from "@app/ee/services/license/license-service";
import { permissionDalFactory } from "@app/ee/services/permission/permission-dal";
import { permissionServiceFactory } from "@app/ee/services/permission/permission-service";
import { samlConfigDalFactory } from "@app/ee/services/saml-config/saml-config-dal";
@@ -28,6 +30,8 @@ import { secretSnapshotServiceFactory } from "@app/ee/services/secret-snapshot/s
import { snapshotDalFactory } from "@app/ee/services/secret-snapshot/snapshot-dal";
import { snapshotFolderDalFactory } from "@app/ee/services/secret-snapshot/snapshot-folder-dal";
import { snapshotSecretDalFactory } from "@app/ee/services/secret-snapshot/snapshot-secret-dal";
import { trustedIpDalFactory } from "@app/ee/services/trusted-ip/trusted-ip-dal";
import { trustedIpServiceFactory } from "@app/ee/services/trusted-ip/trusted-ip-service";
import { getConfig } from "@app/lib/config/env";
import { TQueueServiceFactory } from "@app/queue";
import { apiKeyDalFactory } from "@app/services/api-key/api-key-dal";
@@ -150,6 +154,7 @@ export const registerRoutes = async (
const identityUaClientSecretDal = identityUaClientSecretDalFactory(db);
const auditLogDal = auditLogDalFactory(db);
const trustedIpDal = trustedIpDalFactory(db);
// ee db layer ops
const permissionDal = permissionDalFactory(db);
@@ -168,6 +173,7 @@ export const registerRoutes = async (
const gitAppInstallSessionDal = gitAppInstallSessionDalFactory(db);
const gitAppOrgDal = gitAppDalFactory(db);
const secretScanningDal = secretScanningDalFactory(db);
const licenseDal = licenseDalFactory(db);
const permissionService = permissionServiceFactory({
permissionDal,
@@ -175,7 +181,19 @@ export const registerRoutes = async (
projectRoleDal,
serviceTokenDal
});
const auditLogQueue = auditLogQueueServiceFactory({ auditLogDal, queueService });
const licenseService = licenseServiceFactory({ permissionService, orgDal, licenseDal });
const trustedIpService = trustedIpServiceFactory({
licenseService,
projectDal,
trustedIpDal,
permissionService
});
const auditLogQueue = auditLogQueueServiceFactory({
auditLogDal,
queueService,
projectDal,
licenseService
});
const auditLogService = auditLogServiceFactory({ auditLogDal, permissionService, auditLogQueue });
const sapService = secretApprovalPolicyServiceFactory({
projectMembershipDal,
@@ -189,7 +207,8 @@ export const registerRoutes = async (
orgBotDal,
orgDal,
userDal,
samlConfigDal
samlConfigDal,
licenseService
});
const tokenService = tokenServiceFactory({ tokenDal: authTokenDal, userDal });
@@ -202,6 +221,8 @@ export const registerRoutes = async (
userDal
});
const orgService = orgServiceFactory({
licenseService,
samlConfigDal,
orgRoleDal,
permissionService,
orgDal,
@@ -217,7 +238,8 @@ export const registerRoutes = async (
authDal,
userDal,
orgDal,
orgService
orgService,
licenseService
});
const orgRoleService = orgRoleServiceFactory({ permissionService, orgRoleDal });
const superAdminService = superAdminServiceFactory({
@@ -247,7 +269,8 @@ export const registerRoutes = async (
secretBlindIndexDal,
projectEnvDal,
projectMembershipDal,
folderDal
folderDal,
licenseService
});
const projectMembershipService = projectMembershipServiceFactory({
projectMembershipDal,
@@ -257,9 +280,15 @@ export const registerRoutes = async (
userDal,
smtpService,
projectKeyDal,
projectRoleDal
projectRoleDal,
licenseService
});
const projectEnvService = projectEnvServiceFactory({
permissionService,
projectEnvDal,
licenseService,
projectDal
});
const projectEnvService = projectEnvServiceFactory({ permissionService, projectEnvDal });
const projectKeyService = projectKeyServiceFactory({
permissionService,
projectKeyDal,
@@ -275,7 +304,8 @@ export const registerRoutes = async (
snapshotSecretDal,
secretVersionDal,
folderVersionDal,
permissionService
permissionService,
licenseService
});
const webhookService = webhookServiceFactory({
permissionService,
@@ -350,7 +380,9 @@ export const registerRoutes = async (
permissionService,
projectEnvDal,
secretRotationDal,
secretRotationQueue
secretRotationQueue,
projectDal,
licenseService
});
const integrationService = integrationServiceFactory({
@@ -383,10 +415,13 @@ export const registerRoutes = async (
identityDal,
identityAccessTokenDal,
identityUaClientSecretDal,
identityUaDal
identityUaDal,
licenseService
});
await superAdminService.initServerCfg();
// setup the communication with license key server
await licenseService.init();
// inject all services
server.decorate<FastifyZodProvider["services"]>("services", {
login: loginService,
@@ -423,7 +458,9 @@ export const registerRoutes = async (
snapshot: snapshotService,
saml: samlService,
auditLog: auditLogService,
secretScanning: secretScanningService
secretScanning: secretScanningService,
license: licenseService,
trustedIp: trustedIpService
});
server.decorate<FastifyZodProvider["store"]>("store", {

View File

@@ -39,7 +39,11 @@ export const registerIdentityUaRouter = async (server: FastifyZodProvider) => {
},
handler: async (req) => {
const { identityUa, accessToken, identityAccessToken, validClientSecretInfo } =
await server.services.identityUa.login(req.body.clientId, req.body.clientSecret);
await server.services.identityUa.login(
req.body.clientId,
req.body.clientSecret,
req.realIp
);
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,

View File

@@ -24,10 +24,10 @@ import { registerWebhookRouter } from "./webhook-router";
export const registerV1Routes = async (server: FastifyZodProvider) => {
await server.register(registerSsoRouter, { prefix: "/sso" });
await server.register(
async (authServer) => {
await authServer.register(registerAuthRoutes);
await authServer.register(registerIdentityUaRouter);
await authServer.register(registerIdentityAccessTokenRouter);
async (authRouter) => {
await authRouter.register(registerAuthRoutes);
await authRouter.register(registerIdentityUaRouter);
await authRouter.register(registerIdentityAccessTokenRouter);
},
{ prefix: "/auth" }
);
@@ -41,12 +41,12 @@ export const registerV1Routes = async (server: FastifyZodProvider) => {
await server.register(registerSecretFolderRouter, { prefix: "/folders" });
await server.register(
async (projectServer) => {
await projectServer.register(registerProjectRouter);
await projectServer.register(registerProjectEnvRouter);
await projectServer.register(registerProjectKeyRouter);
await projectServer.register(registerProjectMembershipRouter);
await projectServer.register(registerSecretTagRouter);
async (projectRouter) => {
await projectRouter.register(registerProjectRouter);
await projectRouter.register(registerProjectEnvRouter);
await projectRouter.register(registerProjectKeyRouter);
await projectRouter.register(registerProjectMembershipRouter);
await projectRouter.register(registerSecretTagRouter);
},
{ prefix: "/workspace" }
);

View File

@@ -84,8 +84,6 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => {
}
});
// TODO(akhilmhdh-pg): missing my-workspace list
server.route({
method: "PATCH",
url: "/:organizationId/name",
@@ -161,7 +159,6 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => {
method: "DELETE",
url: "/:organizationId/incidentContactOrg/:incidentContactId",
schema: {
// TODO(akhilmhdh-pg): change accept id instead of email
params: z.object({ organizationId: z.string().trim(), incidentContactId: z.string().trim() }),
response: {
200: z.object({

View File

@@ -88,7 +88,7 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => {
onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
if (req.auth.actor !== ActorType.USER) return;
const membership = await server.services.org.deleteOrgMembership({
userId: req.permission.id,
orgId: req.params.organizationId,
@@ -117,6 +117,7 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => {
const organization = await server.services.org.createOrganization(
req.permission.id,
req.auth.user.email,
req.body.name
);
return { organization };

View File

@@ -11,27 +11,45 @@ export type TTokenDalConfig = {};
export type TTokenDalFactory = ReturnType<typeof tokenDalFactory>;
// TODO(akhilmhdh-pg): wrap all with database error
export const tokenDalFactory = (db: TDbClient) => {
const authOrm = ormify(db, TableName.AuthTokens);
const findOneTokenSession = async (
filter: Partial<TAuthTokenSessions>
): Promise<TAuthTokenSessions | undefined> =>
db(TableName.AuthTokenSession).where(filter).first();
): Promise<TAuthTokenSessions | undefined> => {
try {
const doc = await db(TableName.AuthTokenSession).where(filter).first();
return doc;
} catch (error) {
throw new DatabaseError({ error, name: "FindOneTokenSession" });
}
};
const deleteTokenForUser = async ({
userId,
type,
orgId
}: TDeleteTokenForUserDalDTO): Promise<TAuthTokens[] | undefined> =>
db(TableName.AuthTokens).where({ userId, type, orgId }).delete().returning("*");
}: TDeleteTokenForUserDalDTO): Promise<TAuthTokens[] | undefined> => {
try {
const doc = await db(TableName.AuthTokens)
.where({ userId, type, orgId })
.delete()
.returning("*");
return doc;
} catch (error) {
throw new DatabaseError({ error, name: "DeleteTokenForUser" });
}
};
const decrementTriesField = async ({
userId,
type
}: TDeleteTokenForUserDalDTO): Promise<void> => {
await db(TableName.AuthTokens).where({ userId, type }).decrement("triesLeft", 1);
try {
await db(TableName.AuthTokens).where({ userId, type }).decrement("triesLeft", 1);
} catch (error) {
throw new DatabaseError({ error, name: "DecrementTriesField" });
}
};
const findTokenSessions = async (filter: Partial<TAuthTokenSessions>, tx?: Knex) => {
@@ -48,29 +66,37 @@ export const tokenDalFactory = (db: TDbClient) => {
ip: string,
userAgent: string
): Promise<TAuthTokenSessions | undefined> => {
const [session] = await db(TableName.AuthTokenSession)
.insert({
userId,
ip,
userAgent,
accessVersion: 1,
refreshVersion: 1,
lastUsed: new Date()
})
.returning("*");
return session;
try {
const [session] = await db(TableName.AuthTokenSession)
.insert({
userId,
ip,
userAgent,
accessVersion: 1,
refreshVersion: 1,
lastUsed: new Date()
})
.returning("*");
return session;
} catch (error) {
throw new DatabaseError({ error, name: "InsertTokenSession" });
}
};
const incrementTokenSessionVersion = async (
userId: string,
sessionId: string
): Promise<TAuthTokenSessions | undefined> => {
const [session] = await db(TableName.AuthTokenSession)
.where({ userId, id: sessionId })
.increment("accessVersion", 1)
.increment("refreshVersion", 1)
.returning("*");
return session;
try {
const [session] = await db(TableName.AuthTokenSession)
.where({ userId, id: sessionId })
.increment("accessVersion", 1)
.increment("refreshVersion", 1)
.returning("*");
return session;
} catch (error) {
throw new DatabaseError({ error, name: "IncrementTokenSessionVersion" });
}
};
const deleteTokenSession = async (filter: Partial<TAuthTokenSessions>, tx?: Knex) => {

View File

@@ -1,6 +1,7 @@
import jwt from "jsonwebtoken";
import { OrgMembershipStatus } from "@app/db/schemas";
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
import { getConfig } from "@app/lib/config/env";
import { BadRequestError } from "@app/lib/errors";
import { isDisposableEmail } from "@app/lib/validator";
@@ -22,6 +23,7 @@ type TAuthSignupDep = {
orgDal: TOrgDalFactory;
tokenService: TAuthTokenServiceFactory;
smtpService: TSmtpService;
licenseService: Pick<TLicenseServiceFactory, "updateSubscriptionOrgMemberCount">;
};
export type TAuthSignupFactory = ReturnType<typeof authSignupServiceFactory>;
@@ -31,7 +33,8 @@ export const authSignupServiceFactory = ({
tokenService,
smtpService,
orgService,
orgDal
orgDal,
licenseService
}: TAuthSignupDep) => {
// first step of signup. create user and send email
const beginEmailSignupProcess = async (email: string) => {
@@ -143,13 +146,17 @@ export const authSignupServiceFactory = ({
);
if (!hasSamlEnabled) {
await orgService.createOrganization(user.id, organizationName);
await orgService.createOrganization(user.id, user.email, organizationName);
}
await orgDal.updateMembership(
const updatedMembersips = await orgDal.updateMembership(
{ inviteEmail: email, status: OrgMembershipStatus.Invited },
{ userId: user.id, status: OrgMembershipStatus.Accepted }
);
const uniqueOrgId = [...new Set(updatedMembersips.map(({ orgId }) => orgId))];
await Promise.allSettled(
uniqueOrgId.map((orgId) => licenseService.updateSubscriptionOrgMemberCount(orgId))
);
const tokenSession = await tokenService.getUserTokenSession({
userAgent,
@@ -238,11 +245,16 @@ export const authSignupServiceFactory = ({
tx
);
await orgDal.updateMembership(
const updatedMembersips = await orgDal.updateMembership(
{ inviteEmail: email, status: OrgMembershipStatus.Invited },
{ userId: us.id, status: OrgMembershipStatus.Accepted },
tx
);
const uniqueOrgId = [...new Set(updatedMembersips.map(({ orgId }) => orgId))];
await Promise.allSettled(
uniqueOrgId.map((orgId) => licenseService.updateSubscriptionOrgMemberCount(orgId))
);
return { info: us, key: userEncKey };
});

View File

@@ -5,6 +5,7 @@ import bcrypt from "bcrypt";
import jwt from "jsonwebtoken";
import { IdentityAuthMethod } from "@app/db/schemas";
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
import {
OrgPermissionActions,
OrgPermissionSubjects
@@ -13,7 +14,7 @@ import { TPermissionServiceFactory } from "@app/ee/services/permission/permissio
import { isAtLeastAsPrivileged } from "@app/lib/casl";
import { getConfig } from "@app/lib/config/env";
import { BadRequestError, ForbiddenRequestError, UnauthorizedError } from "@app/lib/errors";
import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip";
import { checkIPAgainstBlocklist, extractIPDetails, isValidIpOrCidr,TIp } from "@app/lib/ip";
import { ActorType, AuthTokenType } from "../auth/auth-type";
import { TIdentityDalFactory } from "../identity/identity-dal";
@@ -38,6 +39,7 @@ type TIdentityUaServiceFactoryDep = {
identityOrgMembershipDal: TIdentityOrgDalFactory;
identityDal: Pick<TIdentityDalFactory, "updateById">;
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">;
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
};
export type TIdentityUaServiceFactory = ReturnType<typeof identityUaServiceFactory>;
@@ -48,13 +50,17 @@ export const identityUaServiceFactory = ({
identityAccessTokenDal,
identityOrgMembershipDal,
identityDal,
permissionService
permissionService,
licenseService
}: TIdentityUaServiceFactoryDep) => {
const login = async (clientId: string, clientSecret: string) => {
const login = async (clientId: string, clientSecret: string, ip: string) => {
const identityUa = await identityUaDal.findOne({ clientId });
if (!identityUa) throw new UnauthorizedError();
// TODO(akhilmhdh-pg): add ip checking
checkIPAgainstBlocklist({
ipAddress: ip,
trustedIps: identityUa.clientSecretTrustedIps as TIp[]
});
const clientSecrtInfo = await identityUaClientSecretDal.find({
identityUAId: identityUa.id,
isClientSecretRevoked: false
@@ -166,10 +172,11 @@ export const identityUaServiceFactory = ({
OrgPermissionActions.Create,
OrgPermissionSubjects.Identity
);
const plan = await licenseService.getPlan(identityMembershipOrg.orgId);
const reformattedClientSecretTrustedIps = clientSecretTrustedIps.map(
(clientSecretTrustedIp) => {
// TODO(akhilmhdh-pg): add licence server here
if (/* !plan.ipAllowlisting && */ clientSecretTrustedIp.ipAddress !== "0.0.0.0/0")
if (!plan.ipAllowlisting && clientSecretTrustedIp.ipAddress !== "0.0.0.0/0")
throw new BadRequestError({
message:
"Failed to add IP access range to service token due to plan restriction. Upgrade plan to add IP access range."
@@ -182,8 +189,7 @@ export const identityUaServiceFactory = ({
}
);
const reformattedAccessTokenTrustedIps = accessTokenTrustedIps.map((accessTokenTrustedIp) => {
// TODO(akhilmhdh-pg): add licence server here
if (/* !plan.ipAllowlisting && */ accessTokenTrustedIp.ipAddress !== "0.0.0.0/0")
if (!plan.ipAllowlisting && accessTokenTrustedIp.ipAddress !== "0.0.0.0/0")
throw new BadRequestError({
message:
"Failed to add IP access range to service token due to plan restriction. Upgrade plan to add IP access range."
@@ -256,10 +262,11 @@ export const identityUaServiceFactory = ({
OrgPermissionActions.Edit,
OrgPermissionSubjects.Identity
);
const plan = await licenseService.getPlan(identityMembershipOrg.orgId);
const reformattedClientSecretTrustedIps = clientSecretTrustedIps?.map(
(clientSecretTrustedIp) => {
// TODO(akhilmhdh-pg): add licence server here
if (/* !plan.ipAllowlisting && */ clientSecretTrustedIp.ipAddress !== "0.0.0.0/0")
if (!plan.ipAllowlisting && clientSecretTrustedIp.ipAddress !== "0.0.0.0/0")
throw new BadRequestError({
message:
"Failed to add IP access range to service token due to plan restriction. Upgrade plan to add IP access range."
@@ -272,8 +279,7 @@ export const identityUaServiceFactory = ({
}
);
const reformattedAccessTokenTrustedIps = accessTokenTrustedIps?.map((accessTokenTrustedIp) => {
// TODO(akhilmhdh-pg): add licence server here
if (/* !plan.ipAllowlisting && */ accessTokenTrustedIp.ipAddress !== "0.0.0.0/0")
if (!plan.ipAllowlisting && accessTokenTrustedIp.ipAddress !== "0.0.0.0/0")
throw new BadRequestError({
message:
"Failed to add IP access range to service token due to plan restriction. Upgrade plan to add IP access range."

View File

@@ -56,7 +56,7 @@ export const identityServiceFactory = ({
return newIdentity;
});
// TODO(akhilmhdh-pg): add audit log here
return identity;
};

View File

@@ -994,7 +994,6 @@ export const integrationAuthServiceFactory = ({
});
return delIntegrationAuth;
// TODO(akhilmhdh-pg): add audit log
};
return {

View File

@@ -90,7 +90,7 @@ export const integrationServiceFactory = ({
integration: integrationAuth.integration
});
// TODO(akhilmhdh-pg): audit log
return { integration, integrationAuth };
};

View File

@@ -4,6 +4,7 @@ import { TDbClient } from "@app/db";
import {
TableName,
TOrganizations,
TOrganizationsInsert,
TOrgMemberships,
TOrgMembershipsInsert,
TOrgMembershipsUpdate
@@ -73,11 +74,9 @@ export const orgDalFactory = (db: TDbClient) => {
}
};
const create = async ({ name }: { name: string }, tx?: Knex) => {
const create = async (dto: TOrganizationsInsert, tx?: Knex) => {
try {
const [organization] = await (tx || db)(TableName.Organization)
.insert({ name })
.returning("*");
const [organization] = await (tx || db)(TableName.Organization).insert(dto).returning("*");
return organization;
} catch (error) {
throw new DatabaseError({ error, name: "Create organization" });

View File

@@ -2,11 +2,13 @@ import { ForbiddenError } from "@casl/ability";
import jwt from "jsonwebtoken";
import { OrgMembershipRole, OrgMembershipStatus } 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 { TSamlConfigDalFactory } from "@app/ee/services/saml-config/saml-config-dal";
import { getConfig } from "@app/lib/config/env";
import { generateAsymmetricKeyPair } from "@app/lib/crypto";
import { generateSymmetricKey, infisicalSymmetricEncypt } from "@app/lib/crypto/encryption";
@@ -35,9 +37,14 @@ type TOrgServiceFactoryDep = {
orgRoleDal: TOrgRoleDalFactory;
userDal: TUserDalFactory;
incidentContactDal: TIncidentContactsDalFactory;
samlConfigDal: Pick<TSamlConfigDalFactory, "findOne">;
smtpService: TSmtpService;
tokenService: TAuthTokenServiceFactory;
permissionService: TPermissionServiceFactory;
licenseService: Pick<
TLicenseServiceFactory,
"getPlan" | "updateSubscriptionOrgMemberCount" | "generateOrgCustomerId" | "removeOrgCustomer"
>;
};
export type TOrgServiceFactory = ReturnType<typeof orgServiceFactory>;
@@ -50,7 +57,9 @@ export const orgServiceFactory = ({
permissionService,
smtpService,
tokenService,
orgBotDal
orgBotDal,
licenseService,
samlConfigDal
}: TOrgServiceFactoryDep) => {
/*
* Get organization details by the organization id
@@ -99,7 +108,7 @@ export const orgServiceFactory = ({
/*
* Create organization
* */
const createOrganization = async (userId: string, orgName: string) => {
const createOrganization = async (userId: string, userEmail: string, orgName: string) => {
const { privateKey, publicKey } = generateAsymmetricKeyPair();
const key = generateSymmetricKey();
const {
@@ -117,8 +126,9 @@ export const orgServiceFactory = ({
algorithm: symmetricKeyAlgorithm
} = infisicalSymmetricEncypt(key);
const customerId = await licenseService.generateOrgCustomerId(orgName, userEmail);
const organization = await orgDal.transaction(async (tx) => {
const org = await orgDal.create({ name: orgName }, tx);
const org = await orgDal.create({ name: orgName, customerId }, tx);
await orgDal.createMembership(
{
userId,
@@ -161,6 +171,9 @@ export const orgServiceFactory = ({
throw new UnauthorizedError({ name: "Delete org by id", message: "Not an admin" });
const organization = await orgDal.deleteById(orgId);
if (organization.customerId) {
await licenseService.removeOrgCustomer(organization.customerId);
}
return organization;
};
/*
@@ -184,6 +197,14 @@ export const orgServiceFactory = ({
const customRole = await orgRoleDal.findOne({ slug: role, orgId });
if (!customRole)
throw new BadRequestError({ name: "Update membership", message: "Role not found" });
const plan = await licenseService.getPlan(orgId);
if (!plan?.rbac)
throw new BadRequestError({
message:
"Failed to assign custom role due to RBAC restriction. Upgrade plan to assign custom role to member."
});
const [membership] = await orgDal.updateMembership(
{ id: membershipId, orgId },
{
@@ -210,7 +231,21 @@ export const orgServiceFactory = ({
OrgPermissionSubjects.Member
);
// TODO(akhilmhdh-pg): SAML SSO check and licence check limit org members
const samlCfg = await samlConfigDal.findOne({ orgId });
if (samlCfg && samlCfg.isActive) {
throw new BadRequestError({
message: "Failed to invite member due to SAML SSO configured for organization"
});
}
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
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.findUserByEmail(inviteeEmail, tx);
if (inviteeUser) {
@@ -284,6 +319,7 @@ export const orgServiceFactory = ({
}
});
await licenseService.updateSubscriptionOrgMemberCount(orgId);
if (!appCfg.isSmtpConfigured) {
return `${appCfg.SITE_URL}/signupinvite?token=${token}&to=${inviteeEmail}&organization_id=${org?.id}`;
}
@@ -323,7 +359,7 @@ export const orgServiceFactory = ({
orgId,
status: OrgMembershipStatus.Accepted
});
// TODO(akhilmhdh-pg): update org licence subscription
await licenseService.updateSubscriptionOrgMemberCount(orgId);
return { user };
}
@@ -350,6 +386,8 @@ export const orgServiceFactory = ({
);
const membership = await orgDal.deleteMembershipById(membershipId, orgId);
await licenseService.updateSubscriptionOrgMemberCount(orgId);
return membership;
};

View File

@@ -1,5 +1,6 @@
import { ForbiddenError } from "@casl/ability";
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import {
ProjectPermissionActions,
@@ -7,19 +8,24 @@ import {
} from "@app/ee/services/permission/project-permission";
import { BadRequestError } from "@app/lib/errors";
import { TProjectDalFactory } from "../project/project-dal";
import { TProjectEnvDalFactory } from "./project-env-dal";
import { TCreateEnvDTO, TDeleteEnvDTO, TUpdateEnvDTO } from "./project-env-types";
type TProjectEnvServiceFactoryDep = {
projectEnvDal: TProjectEnvDalFactory;
projectDal: Pick<TProjectDalFactory, "findById">;
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
};
export type TProjectEnvServiceFactory = ReturnType<typeof projectEnvServiceFactory>;
export const projectEnvServiceFactory = ({
projectEnvDal,
permissionService
permissionService,
licenseService,
projectDal
}: TProjectEnvServiceFactoryDep) => {
const createEnvironment = async ({ projectId, actorId, actor, name, slug }: TCreateEnvDTO) => {
const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId);
@@ -28,14 +34,25 @@ export const projectEnvServiceFactory = ({
ProjectPermissionSub.Environments
);
// TODO(akhilmhdh-pg): add licence service here
const existingEnv = await projectEnvDal.findOne({ slug });
const envs = await projectEnvDal.find({ projectId });
const existingEnv = envs.find(({ slug: envSlug }) => envSlug === slug);
if (existingEnv)
throw new BadRequestError({
message: "Environment with slug already exist",
name: "Create envv"
});
const project = await projectDal.findById(projectId);
const plan = await licenseService.getPlan(project.orgId);
if (plan.environmentLimit !== null && envs.length >= plan.environmentLimit) {
// case: limit imposed on number of environments allowed
// case: number of environments used exceeds the number of environments allowed
throw new BadRequestError({
message:
"Failed to create environment due to environment limit reached. Upgrade plan to create more environments."
});
}
const env = await projectEnvDal.transaction(async (tx) => {
const lastPos = await projectEnvDal.findLastEnvPosition(projectId, tx);
const doc = await projectEnvDal.create({ slug, name, projectId, position: lastPos + 1 }, tx);

View File

@@ -1,6 +1,7 @@
import { ForbiddenError } from "@casl/ability";
import { OrgMembershipStatus, ProjectMembershipRole } from "@app/db/schemas";
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import {
ProjectPermissionActions,
@@ -34,6 +35,7 @@ type TProjectMembershipServiceFactoryDep = {
orgDal: Pick<TOrgDalFactory, "findMembership">;
projectDal: Pick<TProjectDalFactory, "findById">;
projectKeyDal: Pick<TProjectKeyDalFactory, "findLatestProjectKey" | "delete" | "insertMany">;
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
};
export type TProjectMembershipServiceFactory = ReturnType<typeof projectMembershipServiceFactory>;
@@ -46,7 +48,8 @@ export const projectMembershipServiceFactory = ({
orgDal,
userDal,
projectDal,
projectKeyDal
projectKeyDal,
licenseService
}: TProjectMembershipServiceFactoryDep) => {
const getProjectMemberships = async ({ actorId, actor, projectId }: TGetProjectMembershipDTO) => {
const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId);
@@ -120,7 +123,6 @@ export const projectMembershipServiceFactory = ({
}
});
// TODO(akhilmhdh-pg): Audit log
return { invitee, latestKey };
};
@@ -209,6 +211,14 @@ export const projectMembershipServiceFactory = ({
const customRole = await projectRoleDal.findOne({ slug: role, projectId });
if (!customRole)
throw new BadRequestError({ name: "Update project membership", message: "Role not found" });
const project = await projectDal.findById(customRole.projectId);
const plan = await licenseService.getPlan(project.orgId);
if (!plan?.rbac)
throw new BadRequestError({
message:
"Failed to assign custom role due to RBAC restriction. Upgrade plan to assign custom role to member."
});
const [membership] = await projectMembershipDal.update(
{ id: membershipId, projectId },
{

View File

@@ -1,6 +1,7 @@
import { ForbiddenError } from "@casl/ability";
import { ProjectMembershipRole } from "@app/db/schemas";
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
import {
OrgPermissionActions,
OrgPermissionSubjects
@@ -12,6 +13,7 @@ import {
} from "@app/ee/services/permission/project-permission";
import { getConfig } from "@app/lib/config/env";
import { createSecretBlindIndex } from "@app/lib/crypto";
import { BadRequestError } from "@app/lib/errors";
import { TProjectEnvDalFactory } from "../project-env/project-env-dal";
import { TProjectMembershipDalFactory } from "../project-membership/project-membership-dal";
@@ -33,6 +35,7 @@ type TProjectServiceFactoryDep = {
projectMembershipDal: Pick<TProjectMembershipDalFactory, "create">;
secretBlindIndexDal: Pick<TSecretBlindIndexDalFactory, "create">;
permissionService: TPermissionServiceFactory;
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
};
export type TProjectServiceFactory = ReturnType<typeof projectServiceFactory>;
@@ -43,7 +46,8 @@ export const projectServiceFactory = ({
folderDal,
secretBlindIndexDal,
projectMembershipDal,
projectEnvDal
projectEnvDal,
licenseService
}: TProjectServiceFactoryDep) => {
/*
* Create workspace. Make user the admin
@@ -57,7 +61,17 @@ export const projectServiceFactory = ({
const appCfg = getConfig();
const blindIndex = createSecretBlindIndex(appCfg.ROOT_ENCRYPTION_KEY, appCfg.ENCRYPTION_KEY);
// TODO(backend-pg): licence server
const plan = await licenseService.getPlan(orgId);
if (plan.workspaceLimit !== null && plan.workspacesUsed >= plan.workspaceLimit) {
// case: limit imposed on number of workspaces allowed
// case: number of workspaces used exceeds the number of workspaces allowed
throw new BadRequestError({
message:
"Failed to create workspace due to plan limit reached. Upgrade plan to add more workspaces."
});
}
const newProject = projectDal.transaction(async (tx) => {
const project = await projectDal.create({ name: workspaceName, orgId }, tx);
// set user as admin member for proeject

View File

@@ -82,7 +82,6 @@ export const serviceTokenServiceFactory = ({
});
const token = `st.${serviceToken.id.toString()}.${secret}`;
// TODO(akhilmhdh-pg): audit log
return { token, serviceToken };
};

View File

@@ -84,7 +84,6 @@ export const webhookServiceFactory = ({
}
const webhook = await webhookDal.create(insertDoc);
// TODO(akhilmhdh-pg): add audit log
return { ...webhook, projectId, environment: env };
};

View File

@@ -1,6 +1,6 @@
export type TrustedIp = {
id: string;
workspace: string;
projectId: string;
ipAddress: string;
type: "ipv4" | "ipv6";
isActive: boolean;

View File

@@ -172,7 +172,7 @@ export const OrgMembersTable = ({ handlePopUpOpen, setCompleteInviteLink }: Prop
{!isLoading &&
filterdUser?.map(
({ user: u, inviteEmail, role, roleId, id: orgMembershipId, status }) => {
const name = u ? `${u.firstName} ${u.lastName}` : "-";
const name = u && u.firstName ? `${u.firstName} ${u.lastName}` : "-";
const email = u?.email || inviteEmail;
return (
<Tr key={`org-membership-${orgMembershipId}`} className="w-full">