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
+22 -3
View File
@@ -22,6 +22,7 @@
"@fastify/swagger-ui": "^1.10.1", "@fastify/swagger-ui": "^1.10.1",
"@node-saml/passport-saml": "^4.0.4", "@node-saml/passport-saml": "^4.0.4",
"@octokit/rest": "^20.0.2", "@octokit/rest": "^20.0.2",
"@octokit/webhooks-types": "^7.3.1",
"@ucast/mongo2js": "^1.3.4", "@ucast/mongo2js": "^1.3.4",
"ajv": "^8.12.0", "ajv": "^8.12.0",
"argon2": "^0.31.2", "argon2": "^0.31.2",
@@ -44,6 +45,7 @@
"lodash.isequal": "^4.5.0", "lodash.isequal": "^4.5.0",
"mysql2": "^3.6.5", "mysql2": "^3.6.5",
"nanoid": "^5.0.4", "nanoid": "^5.0.4",
"node-cache": "^5.1.2",
"nodemailer": "^6.9.7", "nodemailer": "^6.9.7",
"ora": "^7.0.1", "ora": "^7.0.1",
"passport-github": "^1.1.0", "passport-github": "^1.1.0",
@@ -60,7 +62,6 @@
"zod-to-json-schema": "^3.22.0" "zod-to-json-schema": "^3.22.0"
}, },
"devDependencies": { "devDependencies": {
"@octokit/webhooks-types": "^7.3.1",
"@types/bcrypt": "^5.0.2", "@types/bcrypt": "^5.0.2",
"@types/jmespath": "^0.15.2", "@types/jmespath": "^0.15.2",
"@types/jsonwebtoken": "^9.0.5", "@types/jsonwebtoken": "^9.0.5",
@@ -2313,8 +2314,7 @@
"node_modules/@octokit/webhooks-types": { "node_modules/@octokit/webhooks-types": {
"version": "7.3.1", "version": "7.3.1",
"resolved": "https://registry.npmjs.org/@octokit/webhooks-types/-/webhooks-types-7.3.1.tgz", "resolved": "https://registry.npmjs.org/@octokit/webhooks-types/-/webhooks-types-7.3.1.tgz",
"integrity": "sha512-u6355ZsZnHwmxen30SrqnYb1pXieBFkYgkNzt+Ed4Ao5tupN1OErHfzwiV6hq6duGkDAYASbq7/uVJQ69PjLEg==", "integrity": "sha512-u6355ZsZnHwmxen30SrqnYb1pXieBFkYgkNzt+Ed4Ao5tupN1OErHfzwiV6hq6duGkDAYASbq7/uVJQ69PjLEg=="
"dev": true
}, },
"node_modules/@octokit/webhooks/node_modules/@octokit/openapi-types": { "node_modules/@octokit/webhooks/node_modules/@octokit/openapi-types": {
"version": "12.11.0", "version": "12.11.0",
@@ -5342,6 +5342,14 @@
"url": "https://github.com/sponsors/sindresorhus" "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": { "node_modules/cluster-key-slot": {
"version": "1.1.2", "version": "1.1.2",
"resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz", "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", "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-5.1.0.tgz",
"integrity": "sha512-eh0GgfEkpnoWDq+VY8OyvYhFEzBk6jIYbRKdIlyTiAXIVJ8PyBaKb0rp7oDtoddbdoHWhq8wwr+XZ81F1rpNdA==" "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": { "node_modules/node-fetch": {
"version": "2.7.0", "version": "2.7.0",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz",
+2 -1
View File
@@ -78,8 +78,8 @@
"@fastify/swagger-ui": "^1.10.1", "@fastify/swagger-ui": "^1.10.1",
"@node-saml/passport-saml": "^4.0.4", "@node-saml/passport-saml": "^4.0.4",
"@octokit/rest": "^20.0.2", "@octokit/rest": "^20.0.2",
"@ucast/mongo2js": "^1.3.4",
"@octokit/webhooks-types": "^7.3.1", "@octokit/webhooks-types": "^7.3.1",
"@ucast/mongo2js": "^1.3.4",
"ajv": "^8.12.0", "ajv": "^8.12.0",
"argon2": "^0.31.2", "argon2": "^0.31.2",
"aws-sdk": "^2.1532.0", "aws-sdk": "^2.1532.0",
@@ -101,6 +101,7 @@
"lodash.isequal": "^4.5.0", "lodash.isequal": "^4.5.0",
"mysql2": "^3.6.5", "mysql2": "^3.6.5",
"nanoid": "^5.0.4", "nanoid": "^5.0.4",
"node-cache": "^5.1.2",
"nodemailer": "^6.9.7", "nodemailer": "^6.9.7",
"ora": "^7.0.1", "ora": "^7.0.1",
"passport-github": "^1.1.0", "passport-github": "^1.1.0",
+4
View File
@@ -3,6 +3,7 @@ import "fastify";
import { TUsers } from "@app/db/schemas"; import { TUsers } from "@app/db/schemas";
import { TAuditLogServiceFactory } from "@app/ee/services/audit-log/audit-log-service"; import { TAuditLogServiceFactory } from "@app/ee/services/audit-log/audit-log-service";
import { TCreateAuditLogDTO } from "@app/ee/services/audit-log/audit-log-types"; 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 { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import { TSamlConfigServiceFactory } from "@app/ee/services/saml-config/saml-config-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"; 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 { TSecretRotationServiceFactory } from "@app/ee/services/secret-rotation/secret-rotation-service";
import { TSecretScanningServiceFactory } from "@app/ee/services/secret-scanning/secret-scanning-service"; import { TSecretScanningServiceFactory } from "@app/ee/services/secret-scanning/secret-scanning-service";
import { TSecretSnapshotServiceFactory } from "@app/ee/services/secret-snapshot/secret-snapshot-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 { TAuthMode } from "@app/server/plugins/auth/inject-identity";
import { TApiKeyServiceFactory } from "@app/services/api-key/api-key-service"; import { TApiKeyServiceFactory } from "@app/services/api-key/api-key-service";
import { TAuthLoginFactory } from "@app/services/auth/auth-login-service"; import { TAuthLoginFactory } from "@app/services/auth/auth-login-service";
@@ -101,6 +103,8 @@ declare module "fastify" {
saml: TSamlConfigServiceFactory; saml: TSamlConfigServiceFactory;
auditLog: TAuditLogServiceFactory; auditLog: TAuditLogServiceFactory;
secretScanning: TSecretScanningServiceFactory; secretScanning: TSecretScanningServiceFactory;
license: TLicenseServiceFactory;
trustedIp: TTrustedIpServiceFactory;
}; };
// this is exclusive use for middlewares in which we need to inject data // this is exclusive use for middlewares in which we need to inject data
+8
View File
@@ -148,6 +148,9 @@ import {
TSuperAdmin, TSuperAdmin,
TSuperAdminInsert, TSuperAdminInsert,
TSuperAdminUpdate, TSuperAdminUpdate,
TTrustedIps,
TTrustedIpsInsert,
TTrustedIpsUpdate,
TUserActions, TUserActions,
TUserActionsInsert, TUserActionsInsert,
TUserActionsUpdate, TUserActionsUpdate,
@@ -393,6 +396,11 @@ declare module "knex/types/tables" {
TSecretScanningGitRisksInsert, TSecretScanningGitRisksInsert,
TSecretScanningGitRisksUpdate TSecretScanningGitRisksUpdate
>; >;
[TableName.TrustedIps]: Knex.CompositeTableType<
TTrustedIps,
TTrustedIpsInsert,
TTrustedIpsUpdate
>;
// Junction tables // Junction tables
[TableName.JnSecretTag]: Knex.CompositeTableType< [TableName.JnSecretTag]: Knex.CompositeTableType<
TSecretTagJunction, TSecretTagJunction,
@@ -6,7 +6,7 @@ import { createOnUpdateTrigger, dropOnUpdateTrigger } from "../utils";
export async function up(knex: Knex): Promise<void> { export async function up(knex: Knex): Promise<void> {
if (!(await knex.schema.hasTable(TableName.Project))) { if (!(await knex.schema.hasTable(TableName.Project))) {
await knex.schema.createTable(TableName.Project, (t) => { 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.string("name").notNullable();
t.boolean("autoCapitalization").defaultTo(true); t.boolean("autoCapitalization").defaultTo(true);
t.uuid("orgId").notNullable(); t.uuid("orgId").notNullable();
@@ -6,7 +6,7 @@ import { createOnUpdateTrigger, dropOnUpdateTrigger } from "../utils";
export async function up(knex: Knex): Promise<void> { export async function up(knex: Knex): Promise<void> {
if (!(await knex.schema.hasTable(TableName.ServiceToken))) { if (!(await knex.schema.hasTable(TableName.ServiceToken))) {
await knex.schema.createTable(TableName.ServiceToken, (t) => { 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.string("name").notNullable();
t.jsonb("scopes").notNullable(); t.jsonb("scopes").notNullable();
t.specificType("permissions", "text[]").notNullable(); t.specificType("permissions", "text[]").notNullable();
@@ -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);
}
+1
View File
@@ -48,6 +48,7 @@ export * from "./secret-versions";
export * from "./secrets"; export * from "./secrets";
export * from "./service-tokens"; export * from "./service-tokens";
export * from "./super-admin"; export * from "./super-admin";
export * from "./trusted-ips";
export * from "./user-actions"; export * from "./user-actions";
export * from "./user-encryption-keys"; export * from "./user-encryption-keys";
export * from "./users"; export * from "./users";
+1
View File
@@ -53,6 +53,7 @@ export enum TableName {
GitAppInstallSession = "git_app_install_sessions", GitAppInstallSession = "git_app_install_sessions",
GitAppOrg = "git_app_org", GitAppOrg = "git_app_org",
SecretScanningGitRisk = "secret_scanning_git_risks", SecretScanningGitRisk = "secret_scanning_git_risks",
TrustedIps = "trusted_ips",
// junction tables // junction tables
JnSecretTag = "secret_tag_junction", JnSecretTag = "secret_tag_junction",
JnSecretVersionTag = "secret_version_tag_junction" JnSecretVersionTag = "secret_version_tag_junction"
+24
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>>;
+7 -3
View File
@@ -1,3 +1,4 @@
import { registerLicenseRouter } from "./license-router";
import { registerOrgRoleRouter } from "./org-role-router"; import { registerOrgRoleRouter } from "./org-role-router";
import { registerProjectRoleRouter } from "./project-role-router"; import { registerProjectRoleRouter } from "./project-role-router";
import { registerProjectRouter } from "./project-router"; import { registerProjectRouter } from "./project-router";
@@ -8,14 +9,17 @@ import { registerSecretRotationProviderRouter } from "./secret-rotation-provider
import { registerSecretRotationRouter } from "./secret-rotation-router"; import { registerSecretRotationRouter } from "./secret-rotation-router";
import { registerSecretScanningRouter } from "./secret-scanning-router"; import { registerSecretScanningRouter } from "./secret-scanning-router";
import { registerSnapshotRouter } from "./snapshot-router"; import { registerSnapshotRouter } from "./snapshot-router";
import { registerTrustedIpRouter } from "./trusted-ip-router";
export const registerV1EERoutes = async (server: FastifyZodProvider) => { export const registerV1EERoutes = async (server: FastifyZodProvider) => {
// org role starts with organization // org role starts with organization
await server.register(registerOrgRoleRouter, { prefix: "/organization" }); await server.register(registerOrgRoleRouter, { prefix: "/organization" });
await server.register(registerLicenseRouter, { prefix: "/organizations" });
await server.register( await server.register(
async (projectServer) => { async (projectRouter) => {
projectServer.register(registerProjectRoleRouter); await projectRouter.register(registerProjectRoleRouter);
projectServer.register(registerProjectRouter); await projectRouter.register(registerProjectRouter);
await projectRouter.register(registerTrustedIpRouter);
}, },
{ prefix: "/workspace" } { 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 { 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 { TAuditLogDalFactory } from "./audit-log-dal";
import { TCreateAuditLogDTO } from "./audit-log-types"; import { TCreateAuditLogDTO } from "./audit-log-types";
type TAuditLogQueueServiceFactoryDep = { type TAuditLogQueueServiceFactoryDep = {
auditLogDal: TAuditLogDalFactory; auditLogDal: TAuditLogDalFactory;
queueService: TQueueServiceFactory; queueService: TQueueServiceFactory;
projectDal: Pick<TProjectDalFactory, "findById">;
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
}; };
export type TAuditLogQueueServiceFactory = ReturnType<typeof auditLogQueueServiceFactory>; export type TAuditLogQueueServiceFactory = ReturnType<typeof auditLogQueueServiceFactory>;
export const auditLogQueueServiceFactory = ({ export const auditLogQueueServiceFactory = ({
auditLogDal, auditLogDal,
queueService queueService,
projectDal,
licenseService
}: TAuditLogQueueServiceFactoryDep) => { }: TAuditLogQueueServiceFactoryDep) => {
const pushToLog = async (data: TCreateAuditLogDTO) => { const pushToLog = async (data: TCreateAuditLogDTO) => {
await queueService.queue(QueueName.AuditLog, QueueJobs.AuditLog, data, { await queueService.queue(QueueName.AuditLog, QueueJobs.AuditLog, data, {
@@ -24,8 +30,19 @@ export const auditLogQueueServiceFactory = ({
}; };
queueService.start(QueueName.AuditLog, async (job) => { 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; 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({ await auditLogDal.create({
actor: actor.type, actor: actor.type,
actorMetadata: actor.metadata, actorMetadata: actor.metadata,
@@ -34,7 +51,7 @@ export const auditLogQueueServiceFactory = ({
ipAddress, ipAddress,
orgId, orgId,
eventType: event.type, eventType: event.type,
expiresAt: new Date(Date.now() + 30 * MS_IN_DAY), expiresAt: new Date(Date.now() + ttl),
eventMetadata: event.metadata, eventMetadata: event.metadata,
userAgentType 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 { 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 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 { 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 { 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 = { type TLicenseServiceFactoryDep = {
orgDal: Pick<TOrgDalFactory, "findOrgById">;
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">;
licenseDal: TLicenseDalFactory; licenseDal: TLicenseDalFactory;
}; };
export type TLicenseServiceFactory = ReturnType<typeof licenseServiceFactory>; 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 appCfg = getConfig();
const licenceApi = axios.create({ const licenseServerCloudApi = setupLicenceRequestWithStore(
baseURL: appCfg.LICENCE_SERVER_URL 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 generateOrgCustomerId = async (orgName: string, email: string) => {
const { if (instanceType === InstanceType.Cloud) {
data: { customerId } const {
} = await licenceApi.post("/api/license-server/v1/customers", { email, name: orgName }); data: { customerId }
return 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) => { 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 { return {
generateOrgCustomerId, 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 { TOrgDalFactory } from "@app/services/org/org-dal";
import { TUserDalFactory } from "@app/services/user/user-dal"; import { TUserDalFactory } from "@app/services/user/user-dal";
import { TLicenseServiceFactory } from "../license/license-service";
import { OrgPermissionActions, OrgPermissionSubjects } from "../permission/org-permission"; import { OrgPermissionActions, OrgPermissionSubjects } from "../permission/org-permission";
import { TPermissionServiceFactory } from "../permission/permission-service"; import { TPermissionServiceFactory } from "../permission/permission-service";
import { TSamlConfigDalFactory } from "./saml-config-dal"; import { TSamlConfigDalFactory } from "./saml-config-dal";
@@ -40,6 +41,7 @@ type TSamlConfigServiceFactoryDep = {
>; >;
orgBotDal: Pick<TOrgBotDalFactory, "findOne">; orgBotDal: Pick<TOrgBotDalFactory, "findOne">;
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">; permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">;
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
}; };
export type TSamlConfigServiceFactory = ReturnType<typeof samlConfigServiceFactory>; export type TSamlConfigServiceFactory = ReturnType<typeof samlConfigServiceFactory>;
@@ -49,7 +51,8 @@ export const samlConfigServiceFactory = ({
orgBotDal, orgBotDal,
orgDal, orgDal,
userDal, userDal,
permissionService permissionService,
licenseService
}: TSamlConfigServiceFactoryDep) => { }: TSamlConfigServiceFactoryDep) => {
const createSamlCfg = async ({ const createSamlCfg = async ({
cert, cert,
@@ -67,7 +70,12 @@ export const samlConfigServiceFactory = ({
OrgPermissionSubjects.Sso 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 }); const orgBot = await orgBotDal.findOne({ orgId });
if (!orgBot) if (!orgBot)
@@ -123,6 +131,13 @@ export const samlConfigServiceFactory = ({
OrgPermissionActions.Edit, OrgPermissionActions.Edit,
OrgPermissionSubjects.Sso 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 updateQuery: TSamlConfigsUpdate = { authProvider, isActive };
const orgBot = await orgBotDal.findOne({ orgId }); const orgBot = await orgBotDal.findOne({ orgId });
if (!orgBot) if (!orgBot)
@@ -4,8 +4,10 @@ import Ajv from "ajv";
import { infisicalSymmetricEncypt } from "@app/lib/crypto/encryption"; import { infisicalSymmetricEncypt } from "@app/lib/crypto/encryption";
import { BadRequestError } from "@app/lib/errors"; import { BadRequestError } from "@app/lib/errors";
import { TProjectPermission } from "@app/lib/types"; 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 { TProjectEnvDalFactory } from "@app/services/project-env/project-env-dal";
import { TLicenseServiceFactory } from "../license/license-service";
import { TPermissionServiceFactory } from "../permission/permission-service"; import { TPermissionServiceFactory } from "../permission/permission-service";
import { ProjectPermissionActions, ProjectPermissionSub } from "../permission/project-permission"; import { ProjectPermissionActions, ProjectPermissionSub } from "../permission/project-permission";
import { TSecretRotationDalFactory } from "./secret-rotation-dal"; import { TSecretRotationDalFactory } from "./secret-rotation-dal";
@@ -22,6 +24,8 @@ import { rotationTemplates } from "./templates";
type TSecretRotationServiceFactoryDep = { type TSecretRotationServiceFactoryDep = {
secretRotationDal: TSecretRotationDalFactory; secretRotationDal: TSecretRotationDalFactory;
projectDal: Pick<TProjectDalFactory, "findById">;
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
projectEnvDal: Pick<TProjectEnvDalFactory, "findOne">; projectEnvDal: Pick<TProjectEnvDalFactory, "findOne">;
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">; permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
secretRotationQueue: TSecretRotationQueueFactory; secretRotationQueue: TSecretRotationQueueFactory;
@@ -34,7 +38,9 @@ export const secretRotationServiceFactory = ({
secretRotationDal, secretRotationDal,
permissionService, permissionService,
projectEnvDal, projectEnvDal,
secretRotationQueue secretRotationQueue,
licenseService,
projectDal
}: TSecretRotationServiceFactoryDep) => { }: TSecretRotationServiceFactoryDep) => {
const getProviderTemplates = async ({ actor, actorId, projectId }: TProjectPermission) => { const getProviderTemplates = async ({ actor, actorId, projectId }: TProjectPermission) => {
const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId); const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId);
@@ -68,6 +74,14 @@ export const secretRotationServiceFactory = ({
const env = await projectEnvDal.findOne({ slug: environment }); const env = await projectEnvDal.findOne({ slug: environment });
if (!env) throw new BadRequestError({ message: "Environment not found" }); 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); const selectedTemplate = rotationTemplates.find(({ name }) => name === provider);
if (!selectedTemplate) throw new BadRequestError({ message: "Provider not found" }); if (!selectedTemplate) throw new BadRequestError({ message: "Provider not found" });
const formattedInputs: Record<string, unknown> = {}; const formattedInputs: Record<string, unknown> = {};
@@ -149,6 +163,14 @@ export const secretRotationServiceFactory = ({
const doc = await secretRotationDal.findById(rotationId); const doc = await secretRotationDal.findById(rotationId);
if (!doc) throw new BadRequestError({ message: "Rotation not found" }); 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( const { permission } = await permissionService.getProjectPermission(
actor, actor,
actorId, actorId,
@@ -1,12 +1,13 @@
import { ForbiddenError } from "@casl/ability"; 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 { groupBy } from "@app/lib/fn";
import { TSecretDalFactory } from "@app/services/secret/secret-dal"; import { TSecretDalFactory } from "@app/services/secret/secret-dal";
import { TSecretVersionDalFactory } from "@app/services/secret/secret-version-dal"; import { TSecretVersionDalFactory } from "@app/services/secret/secret-version-dal";
import { TSecretFolderDalFactory } from "@app/services/secret-folder/secret-folder-dal"; import { TSecretFolderDalFactory } from "@app/services/secret-folder/secret-folder-dal";
import { TSecretFolderVersionDalFactory } from "@app/services/secret-folder/secret-folder-version-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 { TPermissionServiceFactory } from "../permission/permission-service";
import { ProjectPermissionActions, ProjectPermissionSub } from "../permission/project-permission"; import { ProjectPermissionActions, ProjectPermissionSub } from "../permission/project-permission";
import { import {
@@ -34,6 +35,7 @@ type TSecretSnapshotServiceFactoryDep = {
"findById" | "findBySecretPath" | "delete" | "insertMany" "findById" | "findBySecretPath" | "delete" | "insertMany"
>; >;
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">; permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
licenseService: Pick<TLicenseServiceFactory, "isValidLicense">;
}; };
export type TSecretSnapshotServiceFactory = ReturnType<typeof secretSnapshotServiceFactory>; export type TSecretSnapshotServiceFactory = ReturnType<typeof secretSnapshotServiceFactory>;
@@ -46,7 +48,8 @@ export const secretSnapshotServiceFactory = ({
snapshotFolderDal, snapshotFolderDal,
folderDal, folderDal,
secretDal, secretDal,
permissionService permissionService,
licenseService
}: TSecretSnapshotServiceFactoryDep) => { }: TSecretSnapshotServiceFactoryDep) => {
const projectSecretSnapshotCount = async ({ const projectSecretSnapshotCount = async ({
environment, environment,
@@ -109,6 +112,9 @@ export const secretSnapshotServiceFactory = ({
}; };
const performSnapshot = async (folderId: string) => { const performSnapshot = async (folderId: string) => {
if (!licenseService.isValidLicense)
throw new InternalServerError({ message: "Invalid license" });
const snapshot = await snapshotDal.transaction(async (tx) => { const snapshot = await snapshotDal.transaction(async (tx) => {
const folder = await folderDal.findById(folderId, tx); const folder = await folderDal.findById(folderId, tx);
if (!folder) throw new BadRequestError({ message: "Folder not found" }); 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;
};
+3 -2
View File
@@ -83,8 +83,9 @@ const envSchema = z
SECRET_SCANNING_GIT_APP_ID: zpStr(z.string().optional()), SECRET_SCANNING_GIT_APP_ID: zpStr(z.string().optional()),
SECRET_SCANNING_PRIVATE_KEY: zpStr(z.string().optional()), SECRET_SCANNING_PRIVATE_KEY: zpStr(z.string().optional()),
// LICENCE // LICENCE
LICENCE_SERVER_URL: zpStr(z.string().optional()), LICENSE_SERVER_URL: zpStr(z.string().optional()),
LICENCE_SERVER_KEY: zpStr(z.string().optional()) LICENSE_SERVER_KEY: zpStr(z.string().optional()),
LICENSE_KEY: zpStr(z.string().optional())
}) })
.transform((data) => ({ .transform((data) => ({
...data, ...data,
+12
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 { export class UnauthorizedError extends Error {
name: string; name: string;
+1 -2
View File
@@ -4,7 +4,6 @@ import fp from "fastify-plugin";
import { jsonSchemaTransform } from "./fastify-zod"; import { jsonSchemaTransform } from "./fastify-zod";
// TODO(akhilmhdh-pg): change the localhost port later
export const fastifySwagger = fp(async (fastify) => { export const fastifySwagger = fp(async (fastify) => {
await fastify.register(swagger, { await fastify.register(swagger, {
transform: jsonSchemaTransform, transform: jsonSchemaTransform,
@@ -16,7 +15,7 @@ export const fastifySwagger = fp(async (fastify) => {
}, },
servers: [ servers: [
{ {
url: "http://localhost:4000", url: "http://localhost:8080",
description: "Local server" description: "Local server"
}, },
{ {
+47 -10
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 { auditLogDalFactory } from "@app/ee/services/audit-log/audit-log-dal";
import { auditLogQueueServiceFactory } from "@app/ee/services/audit-log/audit-log-queue"; import { auditLogQueueServiceFactory } from "@app/ee/services/audit-log/audit-log-queue";
import { auditLogServiceFactory } from "@app/ee/services/audit-log/audit-log-service"; 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 { permissionDalFactory } from "@app/ee/services/permission/permission-dal";
import { permissionServiceFactory } from "@app/ee/services/permission/permission-service"; import { permissionServiceFactory } from "@app/ee/services/permission/permission-service";
import { samlConfigDalFactory } from "@app/ee/services/saml-config/saml-config-dal"; 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 { snapshotDalFactory } from "@app/ee/services/secret-snapshot/snapshot-dal";
import { snapshotFolderDalFactory } from "@app/ee/services/secret-snapshot/snapshot-folder-dal"; import { snapshotFolderDalFactory } from "@app/ee/services/secret-snapshot/snapshot-folder-dal";
import { snapshotSecretDalFactory } from "@app/ee/services/secret-snapshot/snapshot-secret-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 { getConfig } from "@app/lib/config/env";
import { TQueueServiceFactory } from "@app/queue"; import { TQueueServiceFactory } from "@app/queue";
import { apiKeyDalFactory } from "@app/services/api-key/api-key-dal"; import { apiKeyDalFactory } from "@app/services/api-key/api-key-dal";
@@ -150,6 +154,7 @@ export const registerRoutes = async (
const identityUaClientSecretDal = identityUaClientSecretDalFactory(db); const identityUaClientSecretDal = identityUaClientSecretDalFactory(db);
const auditLogDal = auditLogDalFactory(db); const auditLogDal = auditLogDalFactory(db);
const trustedIpDal = trustedIpDalFactory(db);
// ee db layer ops // ee db layer ops
const permissionDal = permissionDalFactory(db); const permissionDal = permissionDalFactory(db);
@@ -168,6 +173,7 @@ export const registerRoutes = async (
const gitAppInstallSessionDal = gitAppInstallSessionDalFactory(db); const gitAppInstallSessionDal = gitAppInstallSessionDalFactory(db);
const gitAppOrgDal = gitAppDalFactory(db); const gitAppOrgDal = gitAppDalFactory(db);
const secretScanningDal = secretScanningDalFactory(db); const secretScanningDal = secretScanningDalFactory(db);
const licenseDal = licenseDalFactory(db);
const permissionService = permissionServiceFactory({ const permissionService = permissionServiceFactory({
permissionDal, permissionDal,
@@ -175,7 +181,19 @@ export const registerRoutes = async (
projectRoleDal, projectRoleDal,
serviceTokenDal 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 auditLogService = auditLogServiceFactory({ auditLogDal, permissionService, auditLogQueue });
const sapService = secretApprovalPolicyServiceFactory({ const sapService = secretApprovalPolicyServiceFactory({
projectMembershipDal, projectMembershipDal,
@@ -189,7 +207,8 @@ export const registerRoutes = async (
orgBotDal, orgBotDal,
orgDal, orgDal,
userDal, userDal,
samlConfigDal samlConfigDal,
licenseService
}); });
const tokenService = tokenServiceFactory({ tokenDal: authTokenDal, userDal }); const tokenService = tokenServiceFactory({ tokenDal: authTokenDal, userDal });
@@ -202,6 +221,8 @@ export const registerRoutes = async (
userDal userDal
}); });
const orgService = orgServiceFactory({ const orgService = orgServiceFactory({
licenseService,
samlConfigDal,
orgRoleDal, orgRoleDal,
permissionService, permissionService,
orgDal, orgDal,
@@ -217,7 +238,8 @@ export const registerRoutes = async (
authDal, authDal,
userDal, userDal,
orgDal, orgDal,
orgService orgService,
licenseService
}); });
const orgRoleService = orgRoleServiceFactory({ permissionService, orgRoleDal }); const orgRoleService = orgRoleServiceFactory({ permissionService, orgRoleDal });
const superAdminService = superAdminServiceFactory({ const superAdminService = superAdminServiceFactory({
@@ -247,7 +269,8 @@ export const registerRoutes = async (
secretBlindIndexDal, secretBlindIndexDal,
projectEnvDal, projectEnvDal,
projectMembershipDal, projectMembershipDal,
folderDal folderDal,
licenseService
}); });
const projectMembershipService = projectMembershipServiceFactory({ const projectMembershipService = projectMembershipServiceFactory({
projectMembershipDal, projectMembershipDal,
@@ -257,9 +280,15 @@ export const registerRoutes = async (
userDal, userDal,
smtpService, smtpService,
projectKeyDal, projectKeyDal,
projectRoleDal projectRoleDal,
licenseService
});
const projectEnvService = projectEnvServiceFactory({
permissionService,
projectEnvDal,
licenseService,
projectDal
}); });
const projectEnvService = projectEnvServiceFactory({ permissionService, projectEnvDal });
const projectKeyService = projectKeyServiceFactory({ const projectKeyService = projectKeyServiceFactory({
permissionService, permissionService,
projectKeyDal, projectKeyDal,
@@ -275,7 +304,8 @@ export const registerRoutes = async (
snapshotSecretDal, snapshotSecretDal,
secretVersionDal, secretVersionDal,
folderVersionDal, folderVersionDal,
permissionService permissionService,
licenseService
}); });
const webhookService = webhookServiceFactory({ const webhookService = webhookServiceFactory({
permissionService, permissionService,
@@ -350,7 +380,9 @@ export const registerRoutes = async (
permissionService, permissionService,
projectEnvDal, projectEnvDal,
secretRotationDal, secretRotationDal,
secretRotationQueue secretRotationQueue,
projectDal,
licenseService
}); });
const integrationService = integrationServiceFactory({ const integrationService = integrationServiceFactory({
@@ -383,10 +415,13 @@ export const registerRoutes = async (
identityDal, identityDal,
identityAccessTokenDal, identityAccessTokenDal,
identityUaClientSecretDal, identityUaClientSecretDal,
identityUaDal identityUaDal,
licenseService
}); });
await superAdminService.initServerCfg(); await superAdminService.initServerCfg();
// setup the communication with license key server
await licenseService.init();
// inject all services // inject all services
server.decorate<FastifyZodProvider["services"]>("services", { server.decorate<FastifyZodProvider["services"]>("services", {
login: loginService, login: loginService,
@@ -423,7 +458,9 @@ export const registerRoutes = async (
snapshot: snapshotService, snapshot: snapshotService,
saml: samlService, saml: samlService,
auditLog: auditLogService, auditLog: auditLogService,
secretScanning: secretScanningService secretScanning: secretScanningService,
license: licenseService,
trustedIp: trustedIpService
}); });
server.decorate<FastifyZodProvider["store"]>("store", { server.decorate<FastifyZodProvider["store"]>("store", {
@@ -39,7 +39,11 @@ export const registerIdentityUaRouter = async (server: FastifyZodProvider) => {
}, },
handler: async (req) => { handler: async (req) => {
const { identityUa, accessToken, identityAccessToken, validClientSecretInfo } = 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({ await server.services.auditLog.createAuditLog({
...req.auditLogInfo, ...req.auditLogInfo,
+10 -10
View File
@@ -24,10 +24,10 @@ import { registerWebhookRouter } from "./webhook-router";
export const registerV1Routes = async (server: FastifyZodProvider) => { export const registerV1Routes = async (server: FastifyZodProvider) => {
await server.register(registerSsoRouter, { prefix: "/sso" }); await server.register(registerSsoRouter, { prefix: "/sso" });
await server.register( await server.register(
async (authServer) => { async (authRouter) => {
await authServer.register(registerAuthRoutes); await authRouter.register(registerAuthRoutes);
await authServer.register(registerIdentityUaRouter); await authRouter.register(registerIdentityUaRouter);
await authServer.register(registerIdentityAccessTokenRouter); await authRouter.register(registerIdentityAccessTokenRouter);
}, },
{ prefix: "/auth" } { prefix: "/auth" }
); );
@@ -41,12 +41,12 @@ export const registerV1Routes = async (server: FastifyZodProvider) => {
await server.register(registerSecretFolderRouter, { prefix: "/folders" }); await server.register(registerSecretFolderRouter, { prefix: "/folders" });
await server.register( await server.register(
async (projectServer) => { async (projectRouter) => {
await projectServer.register(registerProjectRouter); await projectRouter.register(registerProjectRouter);
await projectServer.register(registerProjectEnvRouter); await projectRouter.register(registerProjectEnvRouter);
await projectServer.register(registerProjectKeyRouter); await projectRouter.register(registerProjectKeyRouter);
await projectServer.register(registerProjectMembershipRouter); await projectRouter.register(registerProjectMembershipRouter);
await projectServer.register(registerSecretTagRouter); await projectRouter.register(registerSecretTagRouter);
}, },
{ prefix: "/workspace" } { prefix: "/workspace" }
); );
@@ -84,8 +84,6 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => {
} }
}); });
// TODO(akhilmhdh-pg): missing my-workspace list
server.route({ server.route({
method: "PATCH", method: "PATCH",
url: "/:organizationId/name", url: "/:organizationId/name",
@@ -161,7 +159,6 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => {
method: "DELETE", method: "DELETE",
url: "/:organizationId/incidentContactOrg/:incidentContactId", url: "/:organizationId/incidentContactOrg/:incidentContactId",
schema: { schema: {
// TODO(akhilmhdh-pg): change accept id instead of email
params: z.object({ organizationId: z.string().trim(), incidentContactId: z.string().trim() }), params: z.object({ organizationId: z.string().trim(), incidentContactId: z.string().trim() }),
response: { response: {
200: z.object({ 200: z.object({
@@ -88,7 +88,7 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => {
onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY, AuthMode.IDENTITY_ACCESS_TOKEN]), onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => { handler: async (req) => {
if (req.auth.actor !== ActorType.USER) return; if (req.auth.actor !== ActorType.USER) return;
const membership = await server.services.org.deleteOrgMembership({ const membership = await server.services.org.deleteOrgMembership({
userId: req.permission.id, userId: req.permission.id,
orgId: req.params.organizationId, orgId: req.params.organizationId,
@@ -117,6 +117,7 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => {
const organization = await server.services.org.createOrganization( const organization = await server.services.org.createOrganization(
req.permission.id, req.permission.id,
req.auth.user.email,
req.body.name req.body.name
); );
return { organization }; return { organization };
@@ -11,27 +11,45 @@ export type TTokenDalConfig = {};
export type TTokenDalFactory = ReturnType<typeof tokenDalFactory>; export type TTokenDalFactory = ReturnType<typeof tokenDalFactory>;
// TODO(akhilmhdh-pg): wrap all with database error
export const tokenDalFactory = (db: TDbClient) => { export const tokenDalFactory = (db: TDbClient) => {
const authOrm = ormify(db, TableName.AuthTokens); const authOrm = ormify(db, TableName.AuthTokens);
const findOneTokenSession = async ( const findOneTokenSession = async (
filter: Partial<TAuthTokenSessions> filter: Partial<TAuthTokenSessions>
): Promise<TAuthTokenSessions | undefined> => ): Promise<TAuthTokenSessions | undefined> => {
db(TableName.AuthTokenSession).where(filter).first(); try {
const doc = await db(TableName.AuthTokenSession).where(filter).first();
return doc;
} catch (error) {
throw new DatabaseError({ error, name: "FindOneTokenSession" });
}
};
const deleteTokenForUser = async ({ const deleteTokenForUser = async ({
userId, userId,
type, type,
orgId orgId
}: TDeleteTokenForUserDalDTO): Promise<TAuthTokens[] | undefined> => }: TDeleteTokenForUserDalDTO): Promise<TAuthTokens[] | undefined> => {
db(TableName.AuthTokens).where({ userId, type, orgId }).delete().returning("*"); 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 ({ const decrementTriesField = async ({
userId, userId,
type type
}: TDeleteTokenForUserDalDTO): Promise<void> => { }: 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) => { const findTokenSessions = async (filter: Partial<TAuthTokenSessions>, tx?: Knex) => {
@@ -48,29 +66,37 @@ export const tokenDalFactory = (db: TDbClient) => {
ip: string, ip: string,
userAgent: string userAgent: string
): Promise<TAuthTokenSessions | undefined> => { ): Promise<TAuthTokenSessions | undefined> => {
const [session] = await db(TableName.AuthTokenSession) try {
.insert({ const [session] = await db(TableName.AuthTokenSession)
userId, .insert({
ip, userId,
userAgent, ip,
accessVersion: 1, userAgent,
refreshVersion: 1, accessVersion: 1,
lastUsed: new Date() refreshVersion: 1,
}) lastUsed: new Date()
.returning("*"); })
return session; .returning("*");
return session;
} catch (error) {
throw new DatabaseError({ error, name: "InsertTokenSession" });
}
}; };
const incrementTokenSessionVersion = async ( const incrementTokenSessionVersion = async (
userId: string, userId: string,
sessionId: string sessionId: string
): Promise<TAuthTokenSessions | undefined> => { ): Promise<TAuthTokenSessions | undefined> => {
const [session] = await db(TableName.AuthTokenSession) try {
.where({ userId, id: sessionId }) const [session] = await db(TableName.AuthTokenSession)
.increment("accessVersion", 1) .where({ userId, id: sessionId })
.increment("refreshVersion", 1) .increment("accessVersion", 1)
.returning("*"); .increment("refreshVersion", 1)
return session; .returning("*");
return session;
} catch (error) {
throw new DatabaseError({ error, name: "IncrementTokenSessionVersion" });
}
}; };
const deleteTokenSession = async (filter: Partial<TAuthTokenSessions>, tx?: Knex) => { const deleteTokenSession = async (filter: Partial<TAuthTokenSessions>, tx?: Knex) => {
@@ -1,6 +1,7 @@
import jwt from "jsonwebtoken"; import jwt from "jsonwebtoken";
import { OrgMembershipStatus } from "@app/db/schemas"; import { OrgMembershipStatus } from "@app/db/schemas";
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
import { getConfig } from "@app/lib/config/env"; import { getConfig } from "@app/lib/config/env";
import { BadRequestError } from "@app/lib/errors"; import { BadRequestError } from "@app/lib/errors";
import { isDisposableEmail } from "@app/lib/validator"; import { isDisposableEmail } from "@app/lib/validator";
@@ -22,6 +23,7 @@ type TAuthSignupDep = {
orgDal: TOrgDalFactory; orgDal: TOrgDalFactory;
tokenService: TAuthTokenServiceFactory; tokenService: TAuthTokenServiceFactory;
smtpService: TSmtpService; smtpService: TSmtpService;
licenseService: Pick<TLicenseServiceFactory, "updateSubscriptionOrgMemberCount">;
}; };
export type TAuthSignupFactory = ReturnType<typeof authSignupServiceFactory>; export type TAuthSignupFactory = ReturnType<typeof authSignupServiceFactory>;
@@ -31,7 +33,8 @@ export const authSignupServiceFactory = ({
tokenService, tokenService,
smtpService, smtpService,
orgService, orgService,
orgDal orgDal,
licenseService
}: TAuthSignupDep) => { }: TAuthSignupDep) => {
// first step of signup. create user and send email // first step of signup. create user and send email
const beginEmailSignupProcess = async (email: string) => { const beginEmailSignupProcess = async (email: string) => {
@@ -143,13 +146,17 @@ export const authSignupServiceFactory = ({
); );
if (!hasSamlEnabled) { 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 }, { inviteEmail: email, status: OrgMembershipStatus.Invited },
{ userId: user.id, status: OrgMembershipStatus.Accepted } { 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({ const tokenSession = await tokenService.getUserTokenSession({
userAgent, userAgent,
@@ -238,11 +245,16 @@ export const authSignupServiceFactory = ({
tx tx
); );
await orgDal.updateMembership( const updatedMembersips = await orgDal.updateMembership(
{ inviteEmail: email, status: OrgMembershipStatus.Invited }, { inviteEmail: email, status: OrgMembershipStatus.Invited },
{ userId: us.id, status: OrgMembershipStatus.Accepted }, { userId: us.id, status: OrgMembershipStatus.Accepted },
tx tx
); );
const uniqueOrgId = [...new Set(updatedMembersips.map(({ orgId }) => orgId))];
await Promise.allSettled(
uniqueOrgId.map((orgId) => licenseService.updateSubscriptionOrgMemberCount(orgId))
);
return { info: us, key: userEncKey }; return { info: us, key: userEncKey };
}); });
@@ -5,6 +5,7 @@ import bcrypt from "bcrypt";
import jwt from "jsonwebtoken"; import jwt from "jsonwebtoken";
import { IdentityAuthMethod } from "@app/db/schemas"; import { IdentityAuthMethod } from "@app/db/schemas";
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
import { import {
OrgPermissionActions, OrgPermissionActions,
OrgPermissionSubjects OrgPermissionSubjects
@@ -13,7 +14,7 @@ import { TPermissionServiceFactory } from "@app/ee/services/permission/permissio
import { isAtLeastAsPrivileged } from "@app/lib/casl"; import { isAtLeastAsPrivileged } from "@app/lib/casl";
import { getConfig } from "@app/lib/config/env"; import { getConfig } from "@app/lib/config/env";
import { BadRequestError, ForbiddenRequestError, UnauthorizedError } from "@app/lib/errors"; 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 { ActorType, AuthTokenType } from "../auth/auth-type";
import { TIdentityDalFactory } from "../identity/identity-dal"; import { TIdentityDalFactory } from "../identity/identity-dal";
@@ -38,6 +39,7 @@ type TIdentityUaServiceFactoryDep = {
identityOrgMembershipDal: TIdentityOrgDalFactory; identityOrgMembershipDal: TIdentityOrgDalFactory;
identityDal: Pick<TIdentityDalFactory, "updateById">; identityDal: Pick<TIdentityDalFactory, "updateById">;
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">; permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">;
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
}; };
export type TIdentityUaServiceFactory = ReturnType<typeof identityUaServiceFactory>; export type TIdentityUaServiceFactory = ReturnType<typeof identityUaServiceFactory>;
@@ -48,13 +50,17 @@ export const identityUaServiceFactory = ({
identityAccessTokenDal, identityAccessTokenDal,
identityOrgMembershipDal, identityOrgMembershipDal,
identityDal, identityDal,
permissionService permissionService,
licenseService
}: TIdentityUaServiceFactoryDep) => { }: TIdentityUaServiceFactoryDep) => {
const login = async (clientId: string, clientSecret: string) => { const login = async (clientId: string, clientSecret: string, ip: string) => {
const identityUa = await identityUaDal.findOne({ clientId }); const identityUa = await identityUaDal.findOne({ clientId });
if (!identityUa) throw new UnauthorizedError(); if (!identityUa) throw new UnauthorizedError();
// TODO(akhilmhdh-pg): add ip checking checkIPAgainstBlocklist({
ipAddress: ip,
trustedIps: identityUa.clientSecretTrustedIps as TIp[]
});
const clientSecrtInfo = await identityUaClientSecretDal.find({ const clientSecrtInfo = await identityUaClientSecretDal.find({
identityUAId: identityUa.id, identityUAId: identityUa.id,
isClientSecretRevoked: false isClientSecretRevoked: false
@@ -166,10 +172,11 @@ export const identityUaServiceFactory = ({
OrgPermissionActions.Create, OrgPermissionActions.Create,
OrgPermissionSubjects.Identity OrgPermissionSubjects.Identity
); );
const plan = await licenseService.getPlan(identityMembershipOrg.orgId);
const reformattedClientSecretTrustedIps = clientSecretTrustedIps.map( const reformattedClientSecretTrustedIps = clientSecretTrustedIps.map(
(clientSecretTrustedIp) => { (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({ throw new BadRequestError({
message: message:
"Failed to add IP access range to service token due to plan restriction. Upgrade plan to add IP access range." "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) => { 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({ throw new BadRequestError({
message: message:
"Failed to add IP access range to service token due to plan restriction. Upgrade plan to add IP access range." "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, OrgPermissionActions.Edit,
OrgPermissionSubjects.Identity OrgPermissionSubjects.Identity
); );
const plan = await licenseService.getPlan(identityMembershipOrg.orgId);
const reformattedClientSecretTrustedIps = clientSecretTrustedIps?.map( const reformattedClientSecretTrustedIps = clientSecretTrustedIps?.map(
(clientSecretTrustedIp) => { (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({ throw new BadRequestError({
message: message:
"Failed to add IP access range to service token due to plan restriction. Upgrade plan to add IP access range." "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) => { 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({ throw new BadRequestError({
message: message:
"Failed to add IP access range to service token due to plan restriction. Upgrade plan to add IP access range." "Failed to add IP access range to service token due to plan restriction. Upgrade plan to add IP access range."
@@ -56,7 +56,7 @@ export const identityServiceFactory = ({
return newIdentity; return newIdentity;
}); });
// TODO(akhilmhdh-pg): add audit log here
return identity; return identity;
}; };
@@ -994,7 +994,6 @@ export const integrationAuthServiceFactory = ({
}); });
return delIntegrationAuth; return delIntegrationAuth;
// TODO(akhilmhdh-pg): add audit log
}; };
return { return {
@@ -90,7 +90,7 @@ export const integrationServiceFactory = ({
integration: integrationAuth.integration integration: integrationAuth.integration
}); });
// TODO(akhilmhdh-pg): audit log
return { integration, integrationAuth }; return { integration, integrationAuth };
}; };
+3 -4
View File
@@ -4,6 +4,7 @@ import { TDbClient } from "@app/db";
import { import {
TableName, TableName,
TOrganizations, TOrganizations,
TOrganizationsInsert,
TOrgMemberships, TOrgMemberships,
TOrgMembershipsInsert, TOrgMembershipsInsert,
TOrgMembershipsUpdate 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 { try {
const [organization] = await (tx || db)(TableName.Organization) const [organization] = await (tx || db)(TableName.Organization).insert(dto).returning("*");
.insert({ name })
.returning("*");
return organization; return organization;
} catch (error) { } catch (error) {
throw new DatabaseError({ error, name: "Create organization" }); throw new DatabaseError({ error, name: "Create organization" });
+43 -5
View File
@@ -2,11 +2,13 @@ import { ForbiddenError } from "@casl/ability";
import jwt from "jsonwebtoken"; import jwt from "jsonwebtoken";
import { OrgMembershipRole, OrgMembershipStatus } from "@app/db/schemas"; import { OrgMembershipRole, OrgMembershipStatus } from "@app/db/schemas";
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
import { import {
OrgPermissionActions, OrgPermissionActions,
OrgPermissionSubjects OrgPermissionSubjects
} from "@app/ee/services/permission/org-permission"; } from "@app/ee/services/permission/org-permission";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; 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 { getConfig } from "@app/lib/config/env";
import { generateAsymmetricKeyPair } from "@app/lib/crypto"; import { generateAsymmetricKeyPair } from "@app/lib/crypto";
import { generateSymmetricKey, infisicalSymmetricEncypt } from "@app/lib/crypto/encryption"; import { generateSymmetricKey, infisicalSymmetricEncypt } from "@app/lib/crypto/encryption";
@@ -35,9 +37,14 @@ type TOrgServiceFactoryDep = {
orgRoleDal: TOrgRoleDalFactory; orgRoleDal: TOrgRoleDalFactory;
userDal: TUserDalFactory; userDal: TUserDalFactory;
incidentContactDal: TIncidentContactsDalFactory; incidentContactDal: TIncidentContactsDalFactory;
samlConfigDal: Pick<TSamlConfigDalFactory, "findOne">;
smtpService: TSmtpService; smtpService: TSmtpService;
tokenService: TAuthTokenServiceFactory; tokenService: TAuthTokenServiceFactory;
permissionService: TPermissionServiceFactory; permissionService: TPermissionServiceFactory;
licenseService: Pick<
TLicenseServiceFactory,
"getPlan" | "updateSubscriptionOrgMemberCount" | "generateOrgCustomerId" | "removeOrgCustomer"
>;
}; };
export type TOrgServiceFactory = ReturnType<typeof orgServiceFactory>; export type TOrgServiceFactory = ReturnType<typeof orgServiceFactory>;
@@ -50,7 +57,9 @@ export const orgServiceFactory = ({
permissionService, permissionService,
smtpService, smtpService,
tokenService, tokenService,
orgBotDal orgBotDal,
licenseService,
samlConfigDal
}: TOrgServiceFactoryDep) => { }: TOrgServiceFactoryDep) => {
/* /*
* Get organization details by the organization id * Get organization details by the organization id
@@ -99,7 +108,7 @@ export const orgServiceFactory = ({
/* /*
* Create organization * Create organization
* */ * */
const createOrganization = async (userId: string, orgName: string) => { const createOrganization = async (userId: string, userEmail: string, orgName: string) => {
const { privateKey, publicKey } = generateAsymmetricKeyPair(); const { privateKey, publicKey } = generateAsymmetricKeyPair();
const key = generateSymmetricKey(); const key = generateSymmetricKey();
const { const {
@@ -117,8 +126,9 @@ export const orgServiceFactory = ({
algorithm: symmetricKeyAlgorithm algorithm: symmetricKeyAlgorithm
} = infisicalSymmetricEncypt(key); } = infisicalSymmetricEncypt(key);
const customerId = await licenseService.generateOrgCustomerId(orgName, userEmail);
const organization = await orgDal.transaction(async (tx) => { 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( await orgDal.createMembership(
{ {
userId, userId,
@@ -161,6 +171,9 @@ export const orgServiceFactory = ({
throw new UnauthorizedError({ name: "Delete org by id", message: "Not an admin" }); throw new UnauthorizedError({ name: "Delete org by id", message: "Not an admin" });
const organization = await orgDal.deleteById(orgId); const organization = await orgDal.deleteById(orgId);
if (organization.customerId) {
await licenseService.removeOrgCustomer(organization.customerId);
}
return organization; return organization;
}; };
/* /*
@@ -184,6 +197,14 @@ export const orgServiceFactory = ({
const customRole = await orgRoleDal.findOne({ slug: role, orgId }); const customRole = await orgRoleDal.findOne({ slug: role, orgId });
if (!customRole) if (!customRole)
throw new BadRequestError({ name: "Update membership", message: "Role not found" }); 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( const [membership] = await orgDal.updateMembership(
{ id: membershipId, orgId }, { id: membershipId, orgId },
{ {
@@ -210,7 +231,21 @@ export const orgServiceFactory = ({
OrgPermissionSubjects.Member 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 invitee = await orgDal.transaction(async (tx) => {
const inviteeUser = await userDal.findUserByEmail(inviteeEmail, tx); const inviteeUser = await userDal.findUserByEmail(inviteeEmail, tx);
if (inviteeUser) { if (inviteeUser) {
@@ -284,6 +319,7 @@ export const orgServiceFactory = ({
} }
}); });
await licenseService.updateSubscriptionOrgMemberCount(orgId);
if (!appCfg.isSmtpConfigured) { if (!appCfg.isSmtpConfigured) {
return `${appCfg.SITE_URL}/signupinvite?token=${token}&to=${inviteeEmail}&organization_id=${org?.id}`; return `${appCfg.SITE_URL}/signupinvite?token=${token}&to=${inviteeEmail}&organization_id=${org?.id}`;
} }
@@ -323,7 +359,7 @@ export const orgServiceFactory = ({
orgId, orgId,
status: OrgMembershipStatus.Accepted status: OrgMembershipStatus.Accepted
}); });
// TODO(akhilmhdh-pg): update org licence subscription await licenseService.updateSubscriptionOrgMemberCount(orgId);
return { user }; return { user };
} }
@@ -350,6 +386,8 @@ export const orgServiceFactory = ({
); );
const membership = await orgDal.deleteMembershipById(membershipId, orgId); const membership = await orgDal.deleteMembershipById(membershipId, orgId);
await licenseService.updateSubscriptionOrgMemberCount(orgId);
return membership; return membership;
}; };
@@ -1,5 +1,6 @@
import { ForbiddenError } from "@casl/ability"; import { ForbiddenError } from "@casl/ability";
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import { import {
ProjectPermissionActions, ProjectPermissionActions,
@@ -7,19 +8,24 @@ import {
} from "@app/ee/services/permission/project-permission"; } from "@app/ee/services/permission/project-permission";
import { BadRequestError } from "@app/lib/errors"; import { BadRequestError } from "@app/lib/errors";
import { TProjectDalFactory } from "../project/project-dal";
import { TProjectEnvDalFactory } from "./project-env-dal"; import { TProjectEnvDalFactory } from "./project-env-dal";
import { TCreateEnvDTO, TDeleteEnvDTO, TUpdateEnvDTO } from "./project-env-types"; import { TCreateEnvDTO, TDeleteEnvDTO, TUpdateEnvDTO } from "./project-env-types";
type TProjectEnvServiceFactoryDep = { type TProjectEnvServiceFactoryDep = {
projectEnvDal: TProjectEnvDalFactory; projectEnvDal: TProjectEnvDalFactory;
projectDal: Pick<TProjectDalFactory, "findById">;
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">; permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
}; };
export type TProjectEnvServiceFactory = ReturnType<typeof projectEnvServiceFactory>; export type TProjectEnvServiceFactory = ReturnType<typeof projectEnvServiceFactory>;
export const projectEnvServiceFactory = ({ export const projectEnvServiceFactory = ({
projectEnvDal, projectEnvDal,
permissionService permissionService,
licenseService,
projectDal
}: TProjectEnvServiceFactoryDep) => { }: TProjectEnvServiceFactoryDep) => {
const createEnvironment = async ({ projectId, actorId, actor, name, slug }: TCreateEnvDTO) => { const createEnvironment = async ({ projectId, actorId, actor, name, slug }: TCreateEnvDTO) => {
const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId); const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId);
@@ -28,14 +34,25 @@ export const projectEnvServiceFactory = ({
ProjectPermissionSub.Environments ProjectPermissionSub.Environments
); );
// TODO(akhilmhdh-pg): add licence service here const envs = await projectEnvDal.find({ projectId });
const existingEnv = await projectEnvDal.findOne({ slug }); const existingEnv = envs.find(({ slug: envSlug }) => envSlug === slug);
if (existingEnv) if (existingEnv)
throw new BadRequestError({ throw new BadRequestError({
message: "Environment with slug already exist", message: "Environment with slug already exist",
name: "Create envv" 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 env = await projectEnvDal.transaction(async (tx) => {
const lastPos = await projectEnvDal.findLastEnvPosition(projectId, tx); const lastPos = await projectEnvDal.findLastEnvPosition(projectId, tx);
const doc = await projectEnvDal.create({ slug, name, projectId, position: lastPos + 1 }, tx); const doc = await projectEnvDal.create({ slug, name, projectId, position: lastPos + 1 }, tx);
@@ -1,6 +1,7 @@
import { ForbiddenError } from "@casl/ability"; import { ForbiddenError } from "@casl/ability";
import { OrgMembershipStatus, ProjectMembershipRole } from "@app/db/schemas"; 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 { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import { import {
ProjectPermissionActions, ProjectPermissionActions,
@@ -34,6 +35,7 @@ type TProjectMembershipServiceFactoryDep = {
orgDal: Pick<TOrgDalFactory, "findMembership">; orgDal: Pick<TOrgDalFactory, "findMembership">;
projectDal: Pick<TProjectDalFactory, "findById">; projectDal: Pick<TProjectDalFactory, "findById">;
projectKeyDal: Pick<TProjectKeyDalFactory, "findLatestProjectKey" | "delete" | "insertMany">; projectKeyDal: Pick<TProjectKeyDalFactory, "findLatestProjectKey" | "delete" | "insertMany">;
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
}; };
export type TProjectMembershipServiceFactory = ReturnType<typeof projectMembershipServiceFactory>; export type TProjectMembershipServiceFactory = ReturnType<typeof projectMembershipServiceFactory>;
@@ -46,7 +48,8 @@ export const projectMembershipServiceFactory = ({
orgDal, orgDal,
userDal, userDal,
projectDal, projectDal,
projectKeyDal projectKeyDal,
licenseService
}: TProjectMembershipServiceFactoryDep) => { }: TProjectMembershipServiceFactoryDep) => {
const getProjectMemberships = async ({ actorId, actor, projectId }: TGetProjectMembershipDTO) => { const getProjectMemberships = async ({ actorId, actor, projectId }: TGetProjectMembershipDTO) => {
const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId); const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId);
@@ -120,7 +123,6 @@ export const projectMembershipServiceFactory = ({
} }
}); });
// TODO(akhilmhdh-pg): Audit log
return { invitee, latestKey }; return { invitee, latestKey };
}; };
@@ -209,6 +211,14 @@ export const projectMembershipServiceFactory = ({
const customRole = await projectRoleDal.findOne({ slug: role, projectId }); const customRole = await projectRoleDal.findOne({ slug: role, projectId });
if (!customRole) if (!customRole)
throw new BadRequestError({ name: "Update project membership", message: "Role not found" }); 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( const [membership] = await projectMembershipDal.update(
{ id: membershipId, projectId }, { id: membershipId, projectId },
{ {
@@ -1,6 +1,7 @@
import { ForbiddenError } from "@casl/ability"; import { ForbiddenError } from "@casl/ability";
import { ProjectMembershipRole } from "@app/db/schemas"; import { ProjectMembershipRole } from "@app/db/schemas";
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
import { import {
OrgPermissionActions, OrgPermissionActions,
OrgPermissionSubjects OrgPermissionSubjects
@@ -12,6 +13,7 @@ import {
} from "@app/ee/services/permission/project-permission"; } from "@app/ee/services/permission/project-permission";
import { getConfig } from "@app/lib/config/env"; import { getConfig } from "@app/lib/config/env";
import { createSecretBlindIndex } from "@app/lib/crypto"; import { createSecretBlindIndex } from "@app/lib/crypto";
import { BadRequestError } from "@app/lib/errors";
import { TProjectEnvDalFactory } from "../project-env/project-env-dal"; import { TProjectEnvDalFactory } from "../project-env/project-env-dal";
import { TProjectMembershipDalFactory } from "../project-membership/project-membership-dal"; import { TProjectMembershipDalFactory } from "../project-membership/project-membership-dal";
@@ -33,6 +35,7 @@ type TProjectServiceFactoryDep = {
projectMembershipDal: Pick<TProjectMembershipDalFactory, "create">; projectMembershipDal: Pick<TProjectMembershipDalFactory, "create">;
secretBlindIndexDal: Pick<TSecretBlindIndexDalFactory, "create">; secretBlindIndexDal: Pick<TSecretBlindIndexDalFactory, "create">;
permissionService: TPermissionServiceFactory; permissionService: TPermissionServiceFactory;
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
}; };
export type TProjectServiceFactory = ReturnType<typeof projectServiceFactory>; export type TProjectServiceFactory = ReturnType<typeof projectServiceFactory>;
@@ -43,7 +46,8 @@ export const projectServiceFactory = ({
folderDal, folderDal,
secretBlindIndexDal, secretBlindIndexDal,
projectMembershipDal, projectMembershipDal,
projectEnvDal projectEnvDal,
licenseService
}: TProjectServiceFactoryDep) => { }: TProjectServiceFactoryDep) => {
/* /*
* Create workspace. Make user the admin * Create workspace. Make user the admin
@@ -57,7 +61,17 @@ export const projectServiceFactory = ({
const appCfg = getConfig(); const appCfg = getConfig();
const blindIndex = createSecretBlindIndex(appCfg.ROOT_ENCRYPTION_KEY, appCfg.ENCRYPTION_KEY); 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 newProject = projectDal.transaction(async (tx) => {
const project = await projectDal.create({ name: workspaceName, orgId }, tx); const project = await projectDal.create({ name: workspaceName, orgId }, tx);
// set user as admin member for proeject // set user as admin member for proeject
@@ -82,7 +82,6 @@ export const serviceTokenServiceFactory = ({
}); });
const token = `st.${serviceToken.id.toString()}.${secret}`; const token = `st.${serviceToken.id.toString()}.${secret}`;
// TODO(akhilmhdh-pg): audit log
return { token, serviceToken }; return { token, serviceToken };
}; };
@@ -84,7 +84,6 @@ export const webhookServiceFactory = ({
} }
const webhook = await webhookDal.create(insertDoc); const webhook = await webhookDal.create(insertDoc);
// TODO(akhilmhdh-pg): add audit log
return { ...webhook, projectId, environment: env }; return { ...webhook, projectId, environment: env };
}; };
+1 -1
View File
@@ -1,6 +1,6 @@
export type TrustedIp = { export type TrustedIp = {
id: string; id: string;
workspace: string; projectId: string;
ipAddress: string; ipAddress: string;
type: "ipv4" | "ipv6"; type: "ipv4" | "ipv6";
isActive: boolean; isActive: boolean;
@@ -172,7 +172,7 @@ export const OrgMembersTable = ({ handlePopUpOpen, setCompleteInviteLink }: Prop
{!isLoading && {!isLoading &&
filterdUser?.map( filterdUser?.map(
({ user: u, inviteEmail, role, roleId, id: orgMembershipId, status }) => { ({ 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; const email = u?.email || inviteEmail;
return ( return (
<Tr key={`org-membership-${orgMembershipId}`} className="w-full"> <Tr key={`org-membership-${orgMembershipId}`} className="w-full">