mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-06 19:28:16 +00:00
feat: completed backend for gateway
This commit is contained in:
Vendored
+1
-1
@@ -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");
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>;
|
||||||
|
|||||||
@@ -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 };
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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 = {
|
||||||
|
|||||||
@@ -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();
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -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,
|
||||||
|
|||||||
Reference in New Issue
Block a user