feat: completed backend for gateway

This commit is contained in:
=
2025-02-26 20:45:37 +05:30
parent a7ca242f5d
commit 1307fa49d4
18 changed files with 718 additions and 109 deletions
+1 -1
View File
@@ -13,6 +13,7 @@ import { TCertificateEstServiceFactory } from "@app/ee/services/certificate-est/
import { TDynamicSecretServiceFactory } from "@app/ee/services/dynamic-secret/dynamic-secret-service"; import { TDynamicSecretServiceFactory } from "@app/ee/services/dynamic-secret/dynamic-secret-service";
import { TDynamicSecretLeaseServiceFactory } from "@app/ee/services/dynamic-secret-lease/dynamic-secret-lease-service"; import { TDynamicSecretLeaseServiceFactory } from "@app/ee/services/dynamic-secret-lease/dynamic-secret-lease-service";
import { TExternalKmsServiceFactory } from "@app/ee/services/external-kms/external-kms-service"; import { TExternalKmsServiceFactory } from "@app/ee/services/external-kms/external-kms-service";
import { TGatewayServiceFactory } from "@app/ee/services/gateway/gateway-service";
import { TGroupServiceFactory } from "@app/ee/services/group/group-service"; import { TGroupServiceFactory } from "@app/ee/services/group/group-service";
import { TIdentityProjectAdditionalPrivilegeServiceFactory } from "@app/ee/services/identity-project-additional-privilege/identity-project-additional-privilege-service"; import { TIdentityProjectAdditionalPrivilegeServiceFactory } from "@app/ee/services/identity-project-additional-privilege/identity-project-additional-privilege-service";
import { TIdentityProjectAdditionalPrivilegeV2ServiceFactory } from "@app/ee/services/identity-project-additional-privilege-v2/identity-project-additional-privilege-v2-service"; import { TIdentityProjectAdditionalPrivilegeV2ServiceFactory } from "@app/ee/services/identity-project-additional-privilege-v2/identity-project-additional-privilege-v2-service";
@@ -95,7 +96,6 @@ import { TUserServiceFactory } from "@app/services/user/user-service";
import { TUserEngagementServiceFactory } from "@app/services/user-engagement/user-engagement-service"; import { TUserEngagementServiceFactory } from "@app/services/user-engagement/user-engagement-service";
import { TWebhookServiceFactory } from "@app/services/webhook/webhook-service"; import { TWebhookServiceFactory } from "@app/services/webhook/webhook-service";
import { TWorkflowIntegrationServiceFactory } from "@app/services/workflow-integration/workflow-integration-service"; import { TWorkflowIntegrationServiceFactory } from "@app/services/workflow-integration/workflow-integration-service";
import { TGatewayServiceFactory } from "@app/ee/services/gateway/gateway-service";
declare module "@fastify/request-context" { declare module "@fastify/request-context" {
interface RequestContextData { interface RequestContextData {
@@ -66,6 +66,14 @@ export async function up(knex: Knex): Promise<void> {
await createOnUpdateTrigger(knex, TableName.Gateway); await createOnUpdateTrigger(knex, TableName.Gateway);
} }
if (await knex.schema.hasTable(TableName.DynamicSecret)) {
const doesGatewayColExist = await knex.schema.hasColumn(TableName.DynamicSecret, "gatewayId");
await knex.schema.alterTable(TableName.DynamicSecret, (t) => {
// not setting a foreign constraint so that cascade effects are not triggered
if (!doesGatewayColExist) t.uuid("gatewayId");
});
}
} }
export async function down(knex: Knex): Promise<void> { export async function down(knex: Knex): Promise<void> {
@@ -74,4 +82,11 @@ export async function down(knex: Knex): Promise<void> {
await knex.schema.dropTableIfExists(TableName.OrgGatewayConfig); await knex.schema.dropTableIfExists(TableName.OrgGatewayConfig);
await dropOnUpdateTrigger(knex, TableName.OrgGatewayConfig); await dropOnUpdateTrigger(knex, TableName.OrgGatewayConfig);
if (await knex.schema.hasTable(TableName.DynamicSecret)) {
const doesGatewayColExist = await knex.schema.hasColumn(TableName.DynamicSecret, "gatewayId");
await knex.schema.alterTable(TableName.DynamicSecret, (t) => {
if (doesGatewayColExist) t.dropColumn("gatewayId");
});
}
} }
+2 -1
View File
@@ -26,7 +26,8 @@ export const DynamicSecretsSchema = z.object({
statusDetails: z.string().nullable().optional(), statusDetails: z.string().nullable().optional(),
createdAt: z.date(), createdAt: z.date(),
updatedAt: z.date(), updatedAt: z.date(),
encryptedInput: zodBuffer encryptedInput: zodBuffer,
gatewayId: z.string().uuid().nullable().optional()
}); });
export type TDynamicSecrets = z.infer<typeof DynamicSecretsSchema>; export type TDynamicSecrets = z.infer<typeof DynamicSecretsSchema>;
+47 -3
View File
@@ -2,6 +2,7 @@ import { z } from "zod";
import { GatewaysSchema } from "@app/db/schemas"; import { GatewaysSchema } from "@app/db/schemas";
import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { slugSchema } from "@app/server/lib/schemas";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type"; import { AuthMode } from "@app/services/auth/auth-type";
@@ -37,7 +38,8 @@ export const registerGatewayRouter = async (server: FastifyZodProvider) => {
handler: async (req) => { handler: async (req) => {
const relayDetails = await server.services.gateway.getGatewayRelayDetails( const relayDetails = await server.services.gateway.getGatewayRelayDetails(
req.permission.id, req.permission.id,
req.permission.orgId req.permission.orgId,
req.permission.authMethod
); );
return relayDetails; return relayDetails;
} }
@@ -67,7 +69,8 @@ export const registerGatewayRouter = async (server: FastifyZodProvider) => {
const gatewayCertificates = await server.services.gateway.exchangeAllocatedRelayAddress({ const gatewayCertificates = await server.services.gateway.exchangeAllocatedRelayAddress({
identityOrg: req.permission.orgId, identityOrg: req.permission.orgId,
identityId: req.permission.id, identityId: req.permission.id,
relayAddress: req.body.relayAddress relayAddress: req.body.relayAddress,
identityOrgAuthMethod: req.permission.authMethod
}); });
return gatewayCertificates; return gatewayCertificates;
} }
@@ -80,6 +83,9 @@ export const registerGatewayRouter = async (server: FastifyZodProvider) => {
rateLimit: readLimit rateLimit: readLimit
}, },
schema: { schema: {
querystring: z.object({
projectId: z.string().optional()
}),
response: { response: {
200: z.object({ 200: z.object({
gateways: SanitizedGatewaySchema.extend({ gateways: SanitizedGatewaySchema.extend({
@@ -93,6 +99,14 @@ export const registerGatewayRouter = async (server: FastifyZodProvider) => {
}, },
onRequest: verifyAuth([AuthMode.IDENTITY_ACCESS_TOKEN, AuthMode.JWT]), onRequest: verifyAuth([AuthMode.IDENTITY_ACCESS_TOKEN, AuthMode.JWT]),
handler: async (req) => { handler: async (req) => {
if (req.query.projectId) {
const gateways = await server.services.gateway.getProjectGateways({
projectId: req.query.projectId,
projectPermission: req.permission
});
return { gateways };
}
const gateways = await server.services.gateway.listGateways({ const gateways = await server.services.gateway.listGateways({
orgPermission: req.permission orgPermission: req.permission
}); });
@@ -131,11 +145,41 @@ export const registerGatewayRouter = async (server: FastifyZodProvider) => {
} }
}); });
server.route({
method: "PATCH",
url: "/:id",
config: {
rateLimit: writeLimit
},
schema: {
params: z.object({
id: z.string()
}),
body: z.object({
name: slugSchema({ field: "name" }).optional()
}),
response: {
200: z.object({
gateway: SanitizedGatewaySchema
})
}
},
onRequest: verifyAuth([AuthMode.IDENTITY_ACCESS_TOKEN, AuthMode.JWT]),
handler: async (req) => {
const gateway = await server.services.gateway.updateGatewayById({
orgPermission: req.permission,
id: req.params.id,
name: req.body.name
});
return { gateway };
}
});
server.route({ server.route({
method: "DELETE", method: "DELETE",
url: "/:id", url: "/:id",
config: { config: {
rateLimit: readLimit rateLimit: writeLimit
}, },
schema: { schema: {
params: z.object({ params: z.object({
@@ -1,20 +1,31 @@
import crypto from "node:crypto";
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 { getDbConnectionHost } from "@app/lib/knex"; import { getDbConnectionHost } from "@app/lib/knex";
export const verifyHostInputValidity = (host: string) => { export const verifyHostInputValidity = (host: string, isGateway = false) => {
const appCfg = getConfig(); const appCfg = getConfig();
const dbHost = appCfg.DB_HOST || getDbConnectionHost(appCfg.DB_CONNECTION_URI); const dbHost = appCfg.DB_HOST || getDbConnectionHost(appCfg.DB_CONNECTION_URI);
// no need for validation when it's dev
if (appCfg.NODE_ENV === "development") return;
if (host === "host.docker.internal") throw new BadRequestError({ message: "Invalid db host" });
if ( if (
appCfg.isCloud && appCfg.isCloud &&
!isGateway &&
// localhost // localhost
// internal ips // internal ips
(host === "host.docker.internal" || host.match(/^10\.\d+\.\d+\.\d+/) || host.match(/^192\.168\.\d+\.\d+/)) (host.match(/^10\.\d+\.\d+\.\d+/) || host.match(/^192\.168\.\d+\.\d+/))
) )
throw new BadRequestError({ message: "Invalid db host" }); throw new BadRequestError({ message: "Invalid db host" });
if (host === "localhost" || host === "127.0.0.1" || dbHost === host) { if (
host === "localhost" ||
host === "127.0.0.1" ||
crypto.timingSafeEqual(Buffer.from(dbHost || ""), Buffer.from(host))
) {
throw new BadRequestError({ message: "Invalid db host" }); throw new BadRequestError({ message: "Invalid db host" });
} }
}; };
@@ -16,6 +16,8 @@ import { TSecretFolderDALFactory } from "@app/services/secret-folder/secret-fold
import { TDynamicSecretLeaseDALFactory } from "../dynamic-secret-lease/dynamic-secret-lease-dal"; import { TDynamicSecretLeaseDALFactory } from "../dynamic-secret-lease/dynamic-secret-lease-dal";
import { TDynamicSecretLeaseQueueServiceFactory } from "../dynamic-secret-lease/dynamic-secret-lease-queue"; import { TDynamicSecretLeaseQueueServiceFactory } from "../dynamic-secret-lease/dynamic-secret-lease-queue";
import { TGatewayDALFactory } from "../gateway/gateway-dal";
import { TOrgGatewayConfigDALFactory } from "../gateway/org-gateway-config-dal";
import { TDynamicSecretDALFactory } from "./dynamic-secret-dal"; import { TDynamicSecretDALFactory } from "./dynamic-secret-dal";
import { import {
DynamicSecretStatus, DynamicSecretStatus,
@@ -44,6 +46,8 @@ type TDynamicSecretServiceFactoryDep = {
projectDAL: Pick<TProjectDALFactory, "findProjectBySlug">; projectDAL: Pick<TProjectDALFactory, "findProjectBySlug">;
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">; permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">; kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
gatewayDAL: Pick<TGatewayDALFactory, "findOne">;
orgGatewayConfigDAL: Pick<TOrgGatewayConfigDALFactory, "findOne">;
}; };
export type TDynamicSecretServiceFactory = ReturnType<typeof dynamicSecretServiceFactory>; export type TDynamicSecretServiceFactory = ReturnType<typeof dynamicSecretServiceFactory>;
@@ -57,7 +61,9 @@ export const dynamicSecretServiceFactory = ({
permissionService, permissionService,
dynamicSecretQueueService, dynamicSecretQueueService,
projectDAL, projectDAL,
kmsService kmsService,
gatewayDAL,
orgGatewayConfigDAL
}: TDynamicSecretServiceFactoryDep) => { }: TDynamicSecretServiceFactoryDep) => {
const create = async ({ const create = async ({
path, path,
@@ -108,6 +114,23 @@ export const dynamicSecretServiceFactory = ({
const selectedProvider = dynamicSecretProviders[provider.type]; const selectedProvider = dynamicSecretProviders[provider.type];
const inputs = await selectedProvider.validateProviderInputs(provider.inputs); const inputs = await selectedProvider.validateProviderInputs(provider.inputs);
let selectedGatewayId: string | null = null;
if (inputs && typeof inputs === "object" && "gatewayId" in inputs && inputs.gatewayId) {
const gatewayId = inputs.gatewayId as string;
const orgGateway = await orgGatewayConfigDAL.findOne({ orgId: actorOrgId });
if (!orgGateway)
throw new NotFoundError({
message: `Gateway with ${gatewayId} not found`
});
const gateway = await gatewayDAL.findOne({ id: gatewayId, orgGatewayRootCaId: orgGateway.id });
if (!gateway)
throw new NotFoundError({
message: `Gateway with ${gatewayId} not found`
});
selectedGatewayId = gateway.id;
}
const isConnected = await selectedProvider.validateConnection(provider.inputs); const isConnected = await selectedProvider.validateConnection(provider.inputs);
if (!isConnected) throw new BadRequestError({ message: "Provider connection failed" }); if (!isConnected) throw new BadRequestError({ message: "Provider connection failed" });
@@ -123,7 +146,8 @@ export const dynamicSecretServiceFactory = ({
maxTTL, maxTTL,
defaultTTL, defaultTTL,
folderId: folder.id, folderId: folder.id,
name name,
gatewayId: selectedGatewayId
}); });
return dynamicSecretCfg; return dynamicSecretCfg;
}; };
@@ -195,6 +219,23 @@ export const dynamicSecretServiceFactory = ({
const newInput = { ...decryptedStoredInput, ...(inputs || {}) }; const newInput = { ...decryptedStoredInput, ...(inputs || {}) };
const updatedInput = await selectedProvider.validateProviderInputs(newInput); const updatedInput = await selectedProvider.validateProviderInputs(newInput);
let selectedGatewayId: string | null = null;
if (updatedInput && typeof updatedInput === "object" && "gatewayId" in updatedInput && updatedInput?.gatewayId) {
const gatewayId = updatedInput.gatewayId as string;
const orgGateway = await orgGatewayConfigDAL.findOne({ orgId: actorOrgId });
if (!orgGateway)
throw new NotFoundError({
message: `Gateway with ${gatewayId} not found`
});
const gateway = await gatewayDAL.findOne({ id: gatewayId, orgGatewayRootCaId: orgGateway.id });
if (!gateway)
throw new NotFoundError({
message: `Gateway with ${gatewayId} not found`
});
selectedGatewayId = gateway.id;
}
const isConnected = await selectedProvider.validateConnection(newInput); const isConnected = await selectedProvider.validateConnection(newInput);
if (!isConnected) throw new BadRequestError({ message: "Provider connection failed" }); if (!isConnected) throw new BadRequestError({ message: "Provider connection failed" });
@@ -204,7 +245,8 @@ export const dynamicSecretServiceFactory = ({
defaultTTL, defaultTTL,
name: newName ?? name, name: newName ?? name,
status: null, status: null,
statusDetails: null statusDetails: null,
gatewayId: selectedGatewayId
}); });
return updatedDynamicCfg; return updatedDynamicCfg;
@@ -1,5 +1,6 @@
import { SnowflakeProvider } from "@app/ee/services/dynamic-secret/providers/snowflake"; import { SnowflakeProvider } from "@app/ee/services/dynamic-secret/providers/snowflake";
import { TGatewayServiceFactory } from "../../gateway/gateway-service";
import { AwsElastiCacheDatabaseProvider } from "./aws-elasticache"; import { AwsElastiCacheDatabaseProvider } from "./aws-elasticache";
import { AwsIamProvider } from "./aws-iam"; import { AwsIamProvider } from "./aws-iam";
import { AzureEntraIDProvider } from "./azure-entra-id"; import { AzureEntraIDProvider } from "./azure-entra-id";
@@ -16,8 +17,14 @@ import { SapHanaProvider } from "./sap-hana";
import { SqlDatabaseProvider } from "./sql-database"; import { SqlDatabaseProvider } from "./sql-database";
import { TotpProvider } from "./totp"; import { TotpProvider } from "./totp";
export const buildDynamicSecretProviders = (): Record<DynamicSecretProviders, TDynamicProviderFns> => ({ type TBuildDynamicSecretProviderDTO = {
[DynamicSecretProviders.SqlDatabase]: SqlDatabaseProvider(), gatewayService: Pick<TGatewayServiceFactory, "fnGetGatewayClientTls">;
};
export const buildDynamicSecretProviders = ({
gatewayService
}: TBuildDynamicSecretProviderDTO): Record<DynamicSecretProviders, TDynamicProviderFns> => ({
[DynamicSecretProviders.SqlDatabase]: SqlDatabaseProvider({ gatewayService }),
[DynamicSecretProviders.Cassandra]: CassandraProvider(), [DynamicSecretProviders.Cassandra]: CassandraProvider(),
[DynamicSecretProviders.AwsIam]: AwsIamProvider(), [DynamicSecretProviders.AwsIam]: AwsIamProvider(),
[DynamicSecretProviders.Redis]: RedisDatabaseProvider(), [DynamicSecretProviders.Redis]: RedisDatabaseProvider(),
@@ -103,7 +103,8 @@ export const DynamicSecretSqlDBSchema = z.object({
creationStatement: z.string().trim(), creationStatement: z.string().trim(),
revocationStatement: z.string().trim(), revocationStatement: z.string().trim(),
renewStatement: z.string().trim().optional(), renewStatement: z.string().trim().optional(),
ca: z.string().optional() ca: z.string().optional(),
gatewayId: z.string().nullable().optional()
}); });
export const DynamicSecretCassandraSchema = z.object({ export const DynamicSecretCassandraSchema = z.object({
@@ -3,8 +3,10 @@ import knex from "knex";
import { customAlphabet } from "nanoid"; import { customAlphabet } from "nanoid";
import { z } from "zod"; import { z } from "zod";
import { withGatewayProxy } from "@app/lib/gateway";
import { alphaNumericNanoId } from "@app/lib/nanoid"; import { alphaNumericNanoId } from "@app/lib/nanoid";
import { TGatewayServiceFactory } from "../../gateway/gateway-service";
import { verifyHostInputValidity } from "../dynamic-secret-fns"; import { verifyHostInputValidity } from "../dynamic-secret-fns";
import { DynamicSecretSqlDBSchema, SqlProviders, TDynamicProviderFns } from "./models"; import { DynamicSecretSqlDBSchema, SqlProviders, TDynamicProviderFns } from "./models";
@@ -25,10 +27,14 @@ const generateUsername = (provider: SqlProviders) => {
return alphaNumericNanoId(32); return alphaNumericNanoId(32);
}; };
export const SqlDatabaseProvider = (): TDynamicProviderFns => { type TSqlDatabaseProviderDTO = {
gatewayService: Pick<TGatewayServiceFactory, "fnGetGatewayClientTls">;
};
export const SqlDatabaseProvider = ({ gatewayService }: TSqlDatabaseProviderDTO): TDynamicProviderFns => {
const validateProviderInputs = async (inputs: unknown) => { const validateProviderInputs = async (inputs: unknown) => {
const providerInputs = await DynamicSecretSqlDBSchema.parseAsync(inputs); const providerInputs = await DynamicSecretSqlDBSchema.parseAsync(inputs);
verifyHostInputValidity(providerInputs.host); verifyHostInputValidity(providerInputs.host, Boolean(providerInputs.gatewayId));
return providerInputs; return providerInputs;
}; };
@@ -61,61 +67,107 @@ export const SqlDatabaseProvider = (): TDynamicProviderFns => {
return db; return db;
}; };
const gatewayProxyWrapper = async (
providerInputs: z.infer<typeof DynamicSecretSqlDBSchema>,
gatewayCallback: (port: number) => Promise<void>
) => {
const relayDetails = await gatewayService.fnGetGatewayClientTls(providerInputs.gatewayId as string);
const [relayHost, relayPort] = relayDetails.relayAddress.split(":");
await withGatewayProxy(
async (port) => {
await gatewayCallback(port);
},
{
targetHost: providerInputs.host,
targetPort: providerInputs.port,
relayHost,
relayPort: Number(relayPort),
identityId: relayDetails.identityId,
orgId: relayDetails.orgId,
tlsOptions: {
ca: relayDetails.certChain,
cert: relayDetails.certificate,
key: relayDetails.privateKey
}
}
);
};
const validateConnection = async (inputs: unknown) => { const validateConnection = async (inputs: unknown) => {
const providerInputs = await validateProviderInputs(inputs); const providerInputs = await validateProviderInputs(inputs);
const db = await $getClient(providerInputs); let isConnected = false;
// oracle needs from keyword const gatewayCallback = async (port = providerInputs.port) => {
const testStatement = providerInputs.client === SqlProviders.Oracle ? "SELECT 1 FROM DUAL" : "SELECT 1"; const db = await $getClient({ ...providerInputs, port });
// oracle needs from keyword
const testStatement = providerInputs.client === SqlProviders.Oracle ? "SELECT 1 FROM DUAL" : "SELECT 1";
const isConnected = await db.raw(testStatement).then(() => true); isConnected = await db.raw(testStatement).then(() => true);
await db.destroy(); await db.destroy();
};
if (providerInputs.gatewayId) {
await gatewayProxyWrapper(providerInputs, gatewayCallback);
} else {
await gatewayCallback();
}
return isConnected; return isConnected;
}; };
const create = async (inputs: unknown, expireAt: number) => { const create = async (inputs: unknown, expireAt: number) => {
const providerInputs = await validateProviderInputs(inputs); const providerInputs = await validateProviderInputs(inputs);
const db = await $getClient(providerInputs);
const username = generateUsername(providerInputs.client); const username = generateUsername(providerInputs.client);
const password = generatePassword(providerInputs.client); const password = generatePassword(providerInputs.client);
const { database } = providerInputs; const gatewayCallback = async (port = providerInputs.port) => {
const expiration = new Date(expireAt).toISOString(); const db = await $getClient({ ...providerInputs, port });
const { database } = providerInputs;
const expiration = new Date(expireAt).toISOString();
const creationStatement = handlebars.compile(providerInputs.creationStatement, { noEscape: true })({ const creationStatement = handlebars.compile(providerInputs.creationStatement, { noEscape: true })({
username, username,
password, password,
expiration, expiration,
database database
}); });
const queries = creationStatement.toString().split(";").filter(Boolean); const queries = creationStatement.toString().split(";").filter(Boolean);
await db.transaction(async (tx) => { await db.transaction(async (tx) => {
for (const query of queries) { for (const query of queries) {
// eslint-disable-next-line // eslint-disable-next-line
await tx.raw(query); await tx.raw(query);
} }
}); });
await db.destroy(); await db.destroy();
};
if (providerInputs.gatewayId) {
await gatewayProxyWrapper(providerInputs, gatewayCallback);
} else {
await gatewayCallback();
}
return { entityId: username, data: { DB_USERNAME: username, DB_PASSWORD: password } }; return { entityId: username, data: { DB_USERNAME: username, DB_PASSWORD: password } };
}; };
const revoke = async (inputs: unknown, entityId: string) => { const revoke = async (inputs: unknown, entityId: string) => {
const providerInputs = await validateProviderInputs(inputs); const providerInputs = await validateProviderInputs(inputs);
const db = await $getClient(providerInputs);
const username = entityId; const username = entityId;
const { database } = providerInputs; const { database } = providerInputs;
const gatewayCallback = async (port = providerInputs.port) => {
const db = await $getClient({ ...providerInputs, port });
const revokeStatement = handlebars.compile(providerInputs.revocationStatement)({ username, database });
const queries = revokeStatement.toString().split(";").filter(Boolean);
await db.transaction(async (tx) => {
for (const query of queries) {
// eslint-disable-next-line
await tx.raw(query);
}
});
const revokeStatement = handlebars.compile(providerInputs.revocationStatement)({ username, database }); await db.destroy();
const queries = revokeStatement.toString().split(";").filter(Boolean); };
await db.transaction(async (tx) => { if (providerInputs.gatewayId) {
for (const query of queries) { await gatewayProxyWrapper(providerInputs, gatewayCallback);
// eslint-disable-next-line } else {
await tx.raw(query); await gatewayCallback();
} }
});
await db.destroy();
return { entityId: username }; return { entityId: username };
}; };
@@ -123,28 +175,34 @@ export const SqlDatabaseProvider = (): TDynamicProviderFns => {
const providerInputs = await validateProviderInputs(inputs); const providerInputs = await validateProviderInputs(inputs);
if (!providerInputs.renewStatement) return { entityId }; if (!providerInputs.renewStatement) return { entityId };
const db = await $getClient(providerInputs); const gatewayCallback = async (port = providerInputs.port) => {
const db = await $getClient({ ...providerInputs, port });
const expiration = new Date(expireAt).toISOString();
const { database } = providerInputs;
const expiration = new Date(expireAt).toISOString(); const renewStatement = handlebars.compile(providerInputs.renewStatement)({
const { database } = providerInputs; username: entityId,
expiration,
const renewStatement = handlebars.compile(providerInputs.renewStatement)({ database
username: entityId,
expiration,
database
});
if (renewStatement) {
const queries = renewStatement.toString().split(";").filter(Boolean);
await db.transaction(async (tx) => {
for (const query of queries) {
// eslint-disable-next-line
await tx.raw(query);
}
}); });
}
await db.destroy(); if (renewStatement) {
const queries = renewStatement.toString().split(";").filter(Boolean);
await db.transaction(async (tx) => {
for (const query of queries) {
// eslint-disable-next-line
await tx.raw(query);
}
});
}
await db.destroy();
};
if (providerInputs.gatewayId) {
await gatewayProxyWrapper(providerInputs, gatewayCallback);
} else {
await gatewayCallback();
}
return { entityId }; return { entityId };
}; };
+22 -1
View File
@@ -1,3 +1,5 @@
import { Knex } from "knex";
import { TDbClient } from "@app/db"; import { TDbClient } from "@app/db";
import { TableName, TGateways } from "@app/db/schemas"; import { TableName, TGateways } from "@app/db/schemas";
import { DatabaseError } from "@app/lib/errors"; import { DatabaseError } from "@app/lib/errors";
@@ -29,5 +31,24 @@ export const gatewayDALFactory = (db: TDbClient) => {
} }
}; };
return { ...orm, find }; const findByProjectId = async (projectId: string, tx?: Knex) => {
try {
const query = (tx || db)(TableName.Gateway)
.join(TableName.Identity, `${TableName.Identity}.id`, `${TableName.Gateway}.identityId`)
.join(
TableName.IdentityProjectMembership,
`${TableName.Identity}.id`,
`${TableName.IdentityProjectMembership}.identityId`
)
.select(selectAllTableCols(TableName.Gateway))
.select(db.ref("name").withSchema(TableName.Identity).as("identityName"))
.where({ [`${TableName.IdentityProjectMembership}.projectId` as "projectId"]: projectId });
const docs = await query;
return docs.map((el) => ({ ...el, identity: { id: el.identityId, name: el.identityName } }));
} catch (error) {
throw new DatabaseError({ error, name: `${TableName.Gateway}: Find by project id` });
}
};
return { ...orm, find, findByProjectId };
}; };
@@ -1,15 +1,16 @@
import crypto from "node:crypto"; import crypto from "node:crypto";
import { ForbiddenError } from "@casl/ability"; import { ForbiddenError, subject } from "@casl/ability";
import * as x509 from "@peculiar/x509"; import * as x509 from "@peculiar/x509";
import fs from "fs/promises"; import { z } from "zod";
import path from "path/posix";
import { PgSqlLock } from "@app/keystore/keystore"; import { ActionProjectType } from "@app/db/schemas";
import { KeyStorePrefixes, PgSqlLock, TKeyStoreFactory } from "@app/keystore/keystore";
import { getConfig } from "@app/lib/config/env"; import { getConfig } from "@app/lib/config/env";
import { BadRequestError, NotFoundError } from "@app/lib/errors"; import { BadRequestError, NotFoundError } from "@app/lib/errors";
import { alphaNumericNanoId } from "@app/lib/nanoid"; import { alphaNumericNanoId } from "@app/lib/nanoid";
import { getTurnCredentials } from "@app/lib/turn/credentials"; import { getTurnCredentials } from "@app/lib/turn/credentials";
import { ActorAuthMethod, ActorType } from "@app/services/auth/auth-type";
import { CertExtendedKeyUsage, CertKeyAlgorithm, CertKeyUsage } from "@app/services/certificate/certificate-types"; import { CertExtendedKeyUsage, CertKeyAlgorithm, CertKeyUsage } from "@app/services/certificate/certificate-types";
import { import {
createSerialNumber, createSerialNumber,
@@ -21,29 +22,41 @@ import { KmsDataKey } from "@app/services/kms/kms-types";
import { TLicenseServiceFactory } from "../license/license-service"; import { TLicenseServiceFactory } from "../license/license-service";
import { OrgGatewayPermissionActions, OrgPermissionSubjects } from "../permission/org-permission"; import { OrgGatewayPermissionActions, OrgPermissionSubjects } from "../permission/org-permission";
import { TPermissionServiceFactory } from "../permission/permission-service"; import { TPermissionServiceFactory } from "../permission/permission-service";
import { ProjectPermissionActions, ProjectPermissionSub } from "../permission/project-permission";
import { TGatewayDALFactory } from "./gateway-dal"; import { TGatewayDALFactory } from "./gateway-dal";
import { TExchangeAllocatedRelayAddressDTO, TGetGatewayByIdDTO, TListGatewaysDTO } from "./gateway-types"; import {
TExchangeAllocatedRelayAddressDTO,
TGetGatewayByIdDTO,
TGetProjectGatewayByIdDTO,
TListGatewaysDTO,
TUpdateGatewayByIdDTO
} from "./gateway-types";
import { TOrgGatewayConfigDALFactory } from "./org-gateway-config-dal"; import { TOrgGatewayConfigDALFactory } from "./org-gateway-config-dal";
type TGatewayServiceFactoryDep = { type TGatewayServiceFactoryDep = {
gatewayDAL: TGatewayDALFactory; gatewayDAL: TGatewayDALFactory;
orgGatewayConfigDAL: Pick<TOrgGatewayConfigDALFactory, "findOne" | "create" | "transaction">; orgGatewayConfigDAL: Pick<TOrgGatewayConfigDALFactory, "findOne" | "create" | "transaction" | "findById">;
licenseService: Pick<TLicenseServiceFactory, "onPremFeatures" | "getPlan">; licenseService: Pick<TLicenseServiceFactory, "onPremFeatures" | "getPlan">;
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey" | "decryptWithRootKey">; kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey" | "decryptWithRootKey">;
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">; permissionService: Pick<TPermissionServiceFactory, "getOrgPermission" | "getProjectPermission">;
keyStore: Pick<TKeyStoreFactory, "getItem" | "setItemWithExpiry">;
}; };
export type TGatewayServiceFactory = ReturnType<typeof gatewayServiceFactory>; export type TGatewayServiceFactory = ReturnType<typeof gatewayServiceFactory>;
const TURN_SERVER_CREDENTIALS_SCHEMA = z.object({
username: z.string(),
password: z.string()
});
// TODO(gateway): missing permission check
export const gatewayServiceFactory = ({ export const gatewayServiceFactory = ({
gatewayDAL, gatewayDAL,
licenseService, licenseService,
kmsService, kmsService,
permissionService, permissionService,
orgGatewayConfigDAL orgGatewayConfigDAL,
keyStore
}: TGatewayServiceFactoryDep) => { }: TGatewayServiceFactoryDep) => {
const $validateOrgAccessToGateway = async (orgId: string) => { const $validateOrgAccessToGateway = async (orgId: string, actorId: string, actorAuthMethod: ActorAuthMethod) => {
if (!licenseService.onPremFeatures.gateway) { if (!licenseService.onPremFeatures.gateway) {
throw new BadRequestError({ throw new BadRequestError({
message: message:
@@ -57,11 +70,20 @@ export const gatewayServiceFactory = ({
"Gateway handshake failed due to organization plan restrictions. Please upgrade your instance to Infisical's Enterprise plan." "Gateway handshake failed due to organization plan restrictions. Please upgrade your instance to Infisical's Enterprise plan."
}); });
} }
const { permission } = await permissionService.getOrgPermission(
ActorType.IDENTITY,
actorId,
orgId,
actorAuthMethod,
orgId
);
ForbiddenError.from(permission).throwUnlessCan(OrgGatewayPermissionActions.Create, OrgPermissionSubjects.Gateway);
}; };
const getGatewayRelayDetails = async (actorId: string, actorOrgId: string) => { const getGatewayRelayDetails = async (actorId: string, actorOrgId: string, actorAuthMethod: ActorAuthMethod) => {
const TURN_CRED_EXPIRY = 5 * 60;
const envCfg = getConfig(); const envCfg = getConfig();
await $validateOrgAccessToGateway(actorOrgId); await $validateOrgAccessToGateway(actorOrgId, actorId, actorAuthMethod);
if ( if (
!envCfg.GATEWAY_RELAY_AUTH_SECRET || !envCfg.GATEWAY_RELAY_AUTH_SECRET ||
@@ -73,11 +95,25 @@ export const gatewayServiceFactory = ({
message: "Gateway handshake failed due to missing instance config." message: "Gateway handshake failed due to missing instance config."
}); });
} }
// TODO(gateway): keep it in 30mins redis after encryption to avoid multiple credentials spinning up
const { username: turnServerUsername, password: turnServerPassword } = getTurnCredentials( let turnServerUsername = "";
actorId, let turnServerPassword = "";
envCfg.GATEWAY_RELAY_AUTH_SECRET // keep it in redis for 5mins to avoid generating so many credentials
); const previousCredential = await keyStore.getItem(KeyStorePrefixes.GatewayIdentityCredential(actorId));
if (previousCredential) {
const el = await TURN_SERVER_CREDENTIALS_SCHEMA.parseAsync(JSON.parse(previousCredential));
turnServerUsername = el.username;
turnServerPassword = el.password;
} else {
const el = getTurnCredentials(actorId, envCfg.GATEWAY_RELAY_AUTH_SECRET);
await keyStore.setItemWithExpiry(
KeyStorePrefixes.GatewayIdentityCredential(actorId),
TURN_CRED_EXPIRY,
JSON.stringify({ username: el.username, password: el.password })
);
turnServerUsername = el.username;
turnServerPassword = el.password;
}
return { return {
turnServerUsername, turnServerUsername,
@@ -91,8 +127,10 @@ export const gatewayServiceFactory = ({
const exchangeAllocatedRelayAddress = async ({ const exchangeAllocatedRelayAddress = async ({
identityId, identityId,
identityOrg, identityOrg,
relayAddress relayAddress,
identityOrgAuthMethod
}: TExchangeAllocatedRelayAddressDTO) => { }: TExchangeAllocatedRelayAddressDTO) => {
await $validateOrgAccessToGateway(identityOrg, identityId, identityOrgAuthMethod);
const { encryptor: orgKmsEncryptor, decryptor: orgKmsDecryptor } = await kmsService.createCipherPairWithDataKey({ const { encryptor: orgKmsEncryptor, decryptor: orgKmsDecryptor } = await kmsService.createCipherPairWithDataKey({
type: KmsDataKey.Organization, type: KmsDataKey.Organization,
orgId: identityOrg orgId: identityOrg
@@ -160,7 +198,7 @@ export const gatewayServiceFactory = ({
const clientCertSerialNumber = createSerialNumber(); const clientCertSerialNumber = createSerialNumber();
const clientCert = await x509.X509CertificateGenerator.create({ const clientCert = await x509.X509CertificateGenerator.create({
serialNumber: clientCertSerialNumber, serialNumber: clientCertSerialNumber,
subject: "O=infisical,OU=gateway,CN=cloud-client", subject: `O=${identityOrg},OU=gateway-client,CN=cloud`,
issuer: clientCaCert.subject, issuer: clientCaCert.subject,
notAfter: clientCaExpiration, notAfter: clientCaExpiration,
notBefore: clientCaIssuedAt, notBefore: clientCaIssuedAt,
@@ -214,15 +252,6 @@ export const gatewayServiceFactory = ({
] ]
}); });
await fs.writeFile(path.join(__dirname, "./root-ca-cert"), rootCaCert.toString("pem"));
await fs.writeFile(path.join(__dirname, "./client-ca-cert"), clientCaCert.toString("pem"));
await fs.writeFile(path.join(__dirname, "./gateway-ca-cert"), gatewayCaCert.toString("pem"));
await fs.writeFile(path.join(__dirname, "./client-cert"), clientCert.toString("pem"));
await fs.writeFile(
path.join(__dirname, "./client-key"),
clientSkObj.export({ type: "pkcs8", format: "pem" }) as string
);
return orgGatewayConfigDAL.create({ return orgGatewayConfigDAL.create({
orgId: identityOrg, orgId: identityOrg,
rootCaIssuedAt, rootCaIssuedAt,
@@ -322,7 +351,6 @@ export const gatewayServiceFactory = ({
), ),
new x509.ExtendedKeyUsageExtension([x509.ExtendedKeyUsage[CertExtendedKeyUsage.SERVER_AUTH]], true), new x509.ExtendedKeyUsageExtension([x509.ExtendedKeyUsage[CertExtendedKeyUsage.SERVER_AUTH]], true),
// san // san
// TODO(gateway): change this later
new x509.SubjectAlternativeNameExtension([{ type: "ip", value: "127.0.0.1" }], false) new x509.SubjectAlternativeNameExtension([{ type: "ip", value: "127.0.0.1" }], false)
]; ];
@@ -340,16 +368,23 @@ export const gatewayServiceFactory = ({
extensions extensions
}); });
const appCfg = getConfig();
// just for local development
const formatedRelayAddress =
appCfg.NODE_ENV === "development" ? relayAddress.replace("127.0.0.1", "host.docker.internal") : relayAddress;
await gatewayDAL.transaction(async (tx) => { await gatewayDAL.transaction(async (tx) => {
await tx.raw("SELECT pg_advisory_xact_lock(?)", [PgSqlLock.OrgGatewayCertExchange(identityOrg)]); await tx.raw("SELECT pg_advisory_xact_lock(?)", [PgSqlLock.OrgGatewayCertExchange(identityOrg)]);
const existingGateway = await gatewayDAL.findOne({ identityId, orgGatewayRootCaId: orgGatewayConfig.id }); const existingGateway = await gatewayDAL.findOne({ identityId, orgGatewayRootCaId: orgGatewayConfig.id });
if (existingGateway) { if (existingGateway) {
return gatewayDAL.updateById(existingGateway.id, { return gatewayDAL.updateById(existingGateway.id, {
keyAlgorithm: CertKeyAlgorithm.RSA_2048, keyAlgorithm: CertKeyAlgorithm.RSA_2048,
issuedAt: certIssuedAt, issuedAt: certIssuedAt,
expiration: certExpireAt, expiration: certExpireAt,
serialNumber, serialNumber,
relayAddress: orgKmsEncryptor({ plainText: Buffer.from(relayAddress) }).cipherTextBlob relayAddress: orgKmsEncryptor({
plainText: Buffer.from(formatedRelayAddress)
}).cipherTextBlob
}); });
} }
@@ -358,7 +393,9 @@ export const gatewayServiceFactory = ({
issuedAt: certIssuedAt, issuedAt: certIssuedAt,
expiration: certExpireAt, expiration: certExpireAt,
serialNumber, serialNumber,
relayAddress: orgKmsEncryptor({ plainText: Buffer.from(relayAddress) }).cipherTextBlob, relayAddress: orgKmsEncryptor({
plainText: Buffer.from(formatedRelayAddress)
}).cipherTextBlob,
identityId, identityId,
orgGatewayRootCaId: orgGatewayConfig.id, orgGatewayRootCaId: orgGatewayConfig.id,
name: alphaNumericNanoId(8) name: alphaNumericNanoId(8)
@@ -410,6 +447,23 @@ export const gatewayServiceFactory = ({
return gateway; return gateway;
}; };
const updateGatewayById = async ({ orgPermission, id, name }: TUpdateGatewayByIdDTO) => {
const { permission } = await permissionService.getOrgPermission(
orgPermission.type,
orgPermission.id,
orgPermission.orgId,
orgPermission.authMethod,
orgPermission.orgId
);
ForbiddenError.from(permission).throwUnlessCan(OrgGatewayPermissionActions.Delete, OrgPermissionSubjects.Gateway);
const orgGatewayConfig = await orgGatewayConfigDAL.findOne({ orgId: orgPermission.orgId });
if (!orgGatewayConfig) throw new NotFoundError({ message: `Gateway with ID ${id} not found.` });
const [gateway] = await gatewayDAL.update({ id, orgGatewayRootCaId: orgGatewayConfig.id }, { name });
if (!gateway) throw new NotFoundError({ message: `Gateway with ID ${id} not found.` });
return gateway;
};
const deleteGatewayById = async ({ orgPermission, id }: TGetGatewayByIdDTO) => { const deleteGatewayById = async ({ orgPermission, id }: TGetGatewayByIdDTO) => {
const { permission } = await permissionService.getOrgPermission( const { permission } = await permissionService.getOrgPermission(
orgPermission.type, orgPermission.type,
@@ -427,11 +481,78 @@ export const gatewayServiceFactory = ({
return gateway; return gateway;
}; };
const getProjectGateways = async ({ projectId, projectPermission }: TGetProjectGatewayByIdDTO) => {
const { permission } = await permissionService.getProjectPermission({
projectId,
actor: projectPermission.type,
actorId: projectPermission.id,
actorOrgId: projectPermission.orgId,
actorAuthMethod: projectPermission.authMethod,
actionProjectType: ActionProjectType.Any
});
const gateways = await gatewayDAL.findByProjectId(projectId);
const allowedGateways = gateways.filter((el) =>
permission.can(
ProjectPermissionActions.Read,
subject(ProjectPermissionSub.Identity, { identityId: el.identityId })
)
);
return allowedGateways;
};
// this has no permission check and used for dynamic secrets directly
// assumes permission check is already done
const fnGetGatewayClientTls = async (gatewayId: string) => {
const gateway = await gatewayDAL.findById(gatewayId);
if (!gateway) throw new NotFoundError({ message: `Gateway with ID ${gatewayId} not found.` });
const orgGatewayConfig = await orgGatewayConfigDAL.findById(gateway.orgGatewayRootCaId);
const { decryptor: orgKmsDecryptor } = await kmsService.createCipherPairWithDataKey({
type: KmsDataKey.Organization,
orgId: orgGatewayConfig.orgId
});
const rootCaCert = new x509.X509Certificate(
orgKmsDecryptor({
cipherTextBlob: orgGatewayConfig.encryptedRootCaCertificate
})
);
const gatewayCaCert = new x509.X509Certificate(
orgKmsDecryptor({
cipherTextBlob: orgGatewayConfig.encryptedGatewayCaCertificate
})
);
const clientCert = new x509.X509Certificate(
orgKmsDecryptor({
cipherTextBlob: orgGatewayConfig.encryptedClientCertificate
})
);
const clientSkObj = crypto.createPrivateKey({
key: orgKmsDecryptor({ cipherTextBlob: orgGatewayConfig.encryptedClientPrivateKey }),
format: "der",
type: "pkcs8"
});
return {
relayAddress: orgKmsDecryptor({ cipherTextBlob: gateway.relayAddress }).toString(),
privateKey: clientSkObj.export({ type: "pkcs8", format: "pem" }),
certificate: clientCert.toString("pem"),
certChain: `${gatewayCaCert.toString("pem")}\n${rootCaCert.toString("pem")}`.trim(),
identityId: gateway.identityId,
orgId: orgGatewayConfig.orgId
};
};
return { return {
getGatewayRelayDetails, getGatewayRelayDetails,
exchangeAllocatedRelayAddress, exchangeAllocatedRelayAddress,
listGateways, listGateways,
getGatewayById, getGatewayById,
deleteGatewayById updateGatewayById,
deleteGatewayById,
getProjectGateways,
fnGetGatewayClientTls
}; };
}; };
@@ -1,8 +1,10 @@
import { OrgServiceActor } from "@app/lib/types"; import { OrgServiceActor } from "@app/lib/types";
import { ActorAuthMethod } from "@app/services/auth/auth-type";
export type TExchangeAllocatedRelayAddressDTO = { export type TExchangeAllocatedRelayAddressDTO = {
identityId: string; identityId: string;
identityOrg: string; identityOrg: string;
identityOrgAuthMethod: ActorAuthMethod;
relayAddress: string; relayAddress: string;
}; };
@@ -15,7 +17,18 @@ export type TGetGatewayByIdDTO = {
orgPermission: OrgServiceActor; orgPermission: OrgServiceActor;
}; };
export type TUpdateGatewayByIdDTO = {
id: string;
name?: string;
orgPermission: OrgServiceActor;
};
export type TDeleteGatewayByIdDTO = { export type TDeleteGatewayByIdDTO = {
id: string; id: string;
orgPermission: OrgServiceActor; orgPermission: OrgServiceActor;
}; };
export type TGetProjectGatewayByIdDTO = {
projectId: string;
projectPermission: OrgServiceActor;
};
@@ -52,7 +52,7 @@ export const getDefaultOnPremFeatures = (): TFeatureSet => ({
enforceMfa: false, enforceMfa: false,
projectTemplates: false, projectTemplates: false,
kmip: false, kmip: false,
gateway: true gateway: false
}); });
export const setupLicenseRequestWithStore = (baseURL: string, refreshUrl: string, licenseKey: string) => { export const setupLicenseRequestWithStore = (baseURL: string, refreshUrl: string, licenseKey: string) => {
@@ -69,7 +69,7 @@ export type TFeatureSet = {
enforceMfa: boolean; enforceMfa: boolean;
projectTemplates: false; projectTemplates: false;
kmip: false; kmip: false;
gateway: true; gateway: false;
}; };
export type TOrgPlansTableDTO = { export type TOrgPlansTableDTO = {
@@ -275,6 +275,7 @@ const buildAdminPermission = () => {
can(OrgPermissionAppConnectionActions.Connect, OrgPermissionSubjects.AppConnections); can(OrgPermissionAppConnectionActions.Connect, OrgPermissionSubjects.AppConnections);
can(OrgGatewayPermissionActions.Read, OrgPermissionSubjects.Gateway); can(OrgGatewayPermissionActions.Read, OrgPermissionSubjects.Gateway);
can(OrgGatewayPermissionActions.Create, OrgPermissionSubjects.Gateway);
can(OrgGatewayPermissionActions.Edit, OrgPermissionSubjects.Gateway); can(OrgGatewayPermissionActions.Edit, OrgPermissionSubjects.Gateway);
can(OrgGatewayPermissionActions.Delete, OrgPermissionSubjects.Gateway); can(OrgGatewayPermissionActions.Delete, OrgPermissionSubjects.Gateway);
@@ -315,6 +316,7 @@ const buildMemberPermission = () => {
can(OrgPermissionAppConnectionActions.Connect, OrgPermissionSubjects.AppConnections); can(OrgPermissionAppConnectionActions.Connect, OrgPermissionSubjects.AppConnections);
can(OrgGatewayPermissionActions.Read, OrgPermissionSubjects.Gateway); can(OrgGatewayPermissionActions.Read, OrgPermissionSubjects.Gateway);
can(OrgGatewayPermissionActions.Create, OrgPermissionSubjects.Gateway);
return rules; return rules;
}; };
+3 -2
View File
@@ -1,7 +1,7 @@
import { Redis } from "ioredis"; import { Redis } from "ioredis";
import { Redlock, Settings } from "@app/lib/red-lock";
import { pgAdvisoryLockHashText } from "@app/lib/crypto/hashtext"; import { pgAdvisoryLockHashText } from "@app/lib/crypto/hashtext";
import { Redlock, Settings } from "@app/lib/red-lock";
export const PgSqlLock = { export const PgSqlLock = {
BootUpMigration: 2023, BootUpMigration: 2023,
@@ -36,7 +36,8 @@ export const KeyStorePrefixes = {
SecretSyncLastRunTimestamp: (syncId: string) => `secret-sync-last-run-${syncId}` as const, SecretSyncLastRunTimestamp: (syncId: string) => `secret-sync-last-run-${syncId}` as const,
IdentityAccessTokenStatusUpdate: (identityAccessTokenId: string) => IdentityAccessTokenStatusUpdate: (identityAccessTokenId: string) =>
`identity-access-token-status:${identityAccessTokenId}`, `identity-access-token-status:${identityAccessTokenId}`,
ServiceTokenStatusUpdate: (serviceTokenId: string) => `service-token-status:${serviceTokenId}` ServiceTokenStatusUpdate: (serviceTokenId: string) => `service-token-status:${serviceTokenId}`,
GatewayIdentityCredential: (identityId: string) => `gateway-credentails:${identityId}`
}; };
export const KeyStoreTtls = { export const KeyStoreTtls = {
+258
View File
@@ -0,0 +1,258 @@
/* eslint-disable no-await-in-loop */
import net from "node:net";
import tls from "node:tls";
import { BadRequestError } from "../errors";
import { logger } from "../logger";
const DEFAULT_MAX_RETRIES = 3;
const DEFAULT_RETRY_DELAY = 1000; // 1 second
const createTLSConnection = (relayHost: string, relayPort: number, tlsOptions: tls.TlsOptions = {}) => {
return new Promise<tls.TLSSocket>((resolve, reject) => {
// @ts-expect-error this is resolved in next connect
const socket = new tls.TLSSocket(null, {
rejectUnauthorized: true,
...tlsOptions
});
const cleanup = () => {
socket.removeAllListeners();
socket.end();
};
socket.once("error", (err) => {
cleanup();
reject(err);
});
socket.connect(relayPort, relayHost, () => {
resolve(socket);
});
});
};
type TPingGatewayAndVerifyDTO = {
relayHost: string;
relayPort: number;
tlsOptions: tls.TlsOptions;
maxRetries: number;
identityId: string;
orgId: string;
};
const pingGatewayAndVerifyIdentity = async ({
relayHost,
relayPort,
tlsOptions = {},
maxRetries = DEFAULT_MAX_RETRIES,
identityId,
orgId
}: TPingGatewayAndVerifyDTO) => {
let lastError: Error | null = null;
for (let attempt = 1; attempt <= maxRetries; attempt += 1) {
try {
const socket = await createTLSConnection(relayHost, relayPort, tlsOptions);
socket.setTimeout(2000);
const pingResult = await new Promise((resolve, reject) => {
socket.once("timeout", () => {
socket.destroy();
reject(new Error("Timeout"));
});
socket.once("close", () => {
socket.destroy();
});
socket.once("end", () => {
socket.destroy();
});
socket.once("error", (err) => {
reject(err);
});
socket.write(Buffer.from("PING\n"), () => {
socket.once("data", (data) => {
const response = (data as string).toString();
const certificate = socket.getPeerCertificate();
if (certificate.subject.CN !== identityId || certificate.subject.O !== orgId) {
throw new BadRequestError({
message: `Invalid gateway. Certificate not found for ${identityId} in organisation ${orgId}`
});
}
if (response === "PONG") {
resolve(true);
} else {
reject(new Error(`Unexpected response: ${response}`));
}
});
});
});
socket.end();
return pingResult;
} catch (err) {
lastError = err as Error;
if (attempt < maxRetries) {
await new Promise((resolve) => {
setTimeout(resolve, DEFAULT_RETRY_DELAY);
});
}
}
}
throw new Error(`Failed to ping gateway after ${maxRetries} attempts. Last error: ${lastError?.message}`);
};
interface TProxyServer {
server: net.Server;
port: number;
cleanup: () => void;
}
const setupProxyServer = ({
targetPort,
targetHost,
tlsOptions = {},
relayHost,
relayPort
}: {
targetHost: string;
targetPort: number;
relayPort: number;
relayHost: string;
tlsOptions: tls.TlsOptions;
}): Promise<TProxyServer> => {
return new Promise((resolve, reject) => {
const server = net.createServer();
// eslint-disable-next-line @typescript-eslint/no-misused-promises
server.on("connection", async (clientSocket) => {
try {
const targetSocket = await createTLSConnection(relayHost, relayPort, tlsOptions);
targetSocket.write(Buffer.from(`FORWARD-TCP ${targetHost}:${targetPort}\n`), () => {
clientSocket.on("data", (data) => {
const flushed = targetSocket.write(data);
if (!flushed) {
clientSocket.pause();
targetSocket.once("drain", () => {
clientSocket.resume();
});
}
});
targetSocket.on("data", (data) => {
const flushed = clientSocket.write(data as string);
if (!flushed) {
targetSocket.pause();
clientSocket.once("drain", () => {
targetSocket.resume();
});
}
});
});
const cleanup = () => {
clientSocket?.unpipe();
clientSocket?.end();
targetSocket?.unpipe();
targetSocket?.end();
};
clientSocket.on("error", (err) => {
logger.error(err, "Client socket error");
cleanup();
reject(err);
});
targetSocket.on("error", (err) => {
logger.error(err, "Target socket error");
cleanup();
reject(err);
});
clientSocket.on("end", cleanup);
targetSocket.on("end", cleanup);
} catch (err) {
logger.error(err, "Failed to establish target connection:");
clientSocket.end();
reject(err);
}
});
server.on("error", (err) => {
reject(err);
});
server.listen(0, () => {
const address = server.address();
if (!address || typeof address === "string") {
server.close();
reject(new Error("Failed to get server port"));
return;
}
logger.info("Gateway proxy started");
resolve({
server,
port: address.port,
cleanup: () => {
server.close();
}
});
});
});
};
interface ProxyOptions {
targetHost: string;
targetPort: number;
relayHost: string;
relayPort: number;
tlsOptions?: tls.TlsOptions;
maxRetries?: number;
identityId: string;
orgId: string;
}
export const withGatewayProxy = async (
callback: (port: number) => Promise<void>,
options: ProxyOptions
): Promise<void> => {
const {
relayHost,
relayPort,
targetHost,
targetPort,
tlsOptions = {},
maxRetries = DEFAULT_MAX_RETRIES,
identityId,
orgId
} = options;
// First, try to ping the gateway
await pingGatewayAndVerifyIdentity({
relayHost,
relayPort,
tlsOptions,
maxRetries,
identityId,
orgId
});
// Setup the proxy server
const { port, cleanup } = await setupProxyServer({ targetHost, targetPort, relayPort, relayHost, tlsOptions });
try {
// Execute the callback with the allocated port
await callback(port);
} finally {
// Ensure cleanup happens regardless of success or failure
cleanup();
}
};
+16 -2
View File
@@ -1306,7 +1306,18 @@ export const registerRoutes = async (
kmsService kmsService
}); });
const dynamicSecretProviders = buildDynamicSecretProviders(); const gatewayService = gatewayServiceFactory({
permissionService,
gatewayDAL,
kmsService,
licenseService,
orgGatewayConfigDAL,
keyStore
});
const dynamicSecretProviders = buildDynamicSecretProviders({
gatewayService
});
const dynamicSecretQueueService = dynamicSecretLeaseQueueServiceFactory({ const dynamicSecretQueueService = dynamicSecretLeaseQueueServiceFactory({
queueService, queueService,
dynamicSecretLeaseDAL, dynamicSecretLeaseDAL,
@@ -1324,8 +1335,11 @@ export const registerRoutes = async (
folderDAL, folderDAL,
permissionService, permissionService,
licenseService, licenseService,
kmsService kmsService,
gatewayDAL,
orgGatewayConfigDAL
}); });
const dynamicSecretLeaseService = dynamicSecretLeaseServiceFactory({ const dynamicSecretLeaseService = dynamicSecretLeaseServiceFactory({
projectDAL, projectDAL,
permissionService, permissionService,