mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-15 04:14:38 +00:00
feat: completed backend for gateway
This commit is contained in:
@@ -1,20 +1,31 @@
|
||||
import crypto from "node:crypto";
|
||||
|
||||
import { getConfig } from "@app/lib/config/env";
|
||||
import { BadRequestError } from "@app/lib/errors";
|
||||
import { getDbConnectionHost } from "@app/lib/knex";
|
||||
|
||||
export const verifyHostInputValidity = (host: string) => {
|
||||
export const verifyHostInputValidity = (host: string, isGateway = false) => {
|
||||
const appCfg = getConfig();
|
||||
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 (
|
||||
appCfg.isCloud &&
|
||||
!isGateway &&
|
||||
// localhost
|
||||
// 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" });
|
||||
|
||||
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" });
|
||||
}
|
||||
};
|
||||
|
||||
@@ -16,6 +16,8 @@ import { TSecretFolderDALFactory } from "@app/services/secret-folder/secret-fold
|
||||
|
||||
import { TDynamicSecretLeaseDALFactory } from "../dynamic-secret-lease/dynamic-secret-lease-dal";
|
||||
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 {
|
||||
DynamicSecretStatus,
|
||||
@@ -44,6 +46,8 @@ type TDynamicSecretServiceFactoryDep = {
|
||||
projectDAL: Pick<TProjectDALFactory, "findProjectBySlug">;
|
||||
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
|
||||
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
|
||||
gatewayDAL: Pick<TGatewayDALFactory, "findOne">;
|
||||
orgGatewayConfigDAL: Pick<TOrgGatewayConfigDALFactory, "findOne">;
|
||||
};
|
||||
|
||||
export type TDynamicSecretServiceFactory = ReturnType<typeof dynamicSecretServiceFactory>;
|
||||
@@ -57,7 +61,9 @@ export const dynamicSecretServiceFactory = ({
|
||||
permissionService,
|
||||
dynamicSecretQueueService,
|
||||
projectDAL,
|
||||
kmsService
|
||||
kmsService,
|
||||
gatewayDAL,
|
||||
orgGatewayConfigDAL
|
||||
}: TDynamicSecretServiceFactoryDep) => {
|
||||
const create = async ({
|
||||
path,
|
||||
@@ -108,6 +114,23 @@ export const dynamicSecretServiceFactory = ({
|
||||
const selectedProvider = dynamicSecretProviders[provider.type];
|
||||
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);
|
||||
if (!isConnected) throw new BadRequestError({ message: "Provider connection failed" });
|
||||
|
||||
@@ -123,7 +146,8 @@ export const dynamicSecretServiceFactory = ({
|
||||
maxTTL,
|
||||
defaultTTL,
|
||||
folderId: folder.id,
|
||||
name
|
||||
name,
|
||||
gatewayId: selectedGatewayId
|
||||
});
|
||||
return dynamicSecretCfg;
|
||||
};
|
||||
@@ -195,6 +219,23 @@ export const dynamicSecretServiceFactory = ({
|
||||
const newInput = { ...decryptedStoredInput, ...(inputs || {}) };
|
||||
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);
|
||||
if (!isConnected) throw new BadRequestError({ message: "Provider connection failed" });
|
||||
|
||||
@@ -204,7 +245,8 @@ export const dynamicSecretServiceFactory = ({
|
||||
defaultTTL,
|
||||
name: newName ?? name,
|
||||
status: null,
|
||||
statusDetails: null
|
||||
statusDetails: null,
|
||||
gatewayId: selectedGatewayId
|
||||
});
|
||||
|
||||
return updatedDynamicCfg;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { SnowflakeProvider } from "@app/ee/services/dynamic-secret/providers/snowflake";
|
||||
|
||||
import { TGatewayServiceFactory } from "../../gateway/gateway-service";
|
||||
import { AwsElastiCacheDatabaseProvider } from "./aws-elasticache";
|
||||
import { AwsIamProvider } from "./aws-iam";
|
||||
import { AzureEntraIDProvider } from "./azure-entra-id";
|
||||
@@ -16,8 +17,14 @@ import { SapHanaProvider } from "./sap-hana";
|
||||
import { SqlDatabaseProvider } from "./sql-database";
|
||||
import { TotpProvider } from "./totp";
|
||||
|
||||
export const buildDynamicSecretProviders = (): Record<DynamicSecretProviders, TDynamicProviderFns> => ({
|
||||
[DynamicSecretProviders.SqlDatabase]: SqlDatabaseProvider(),
|
||||
type TBuildDynamicSecretProviderDTO = {
|
||||
gatewayService: Pick<TGatewayServiceFactory, "fnGetGatewayClientTls">;
|
||||
};
|
||||
|
||||
export const buildDynamicSecretProviders = ({
|
||||
gatewayService
|
||||
}: TBuildDynamicSecretProviderDTO): Record<DynamicSecretProviders, TDynamicProviderFns> => ({
|
||||
[DynamicSecretProviders.SqlDatabase]: SqlDatabaseProvider({ gatewayService }),
|
||||
[DynamicSecretProviders.Cassandra]: CassandraProvider(),
|
||||
[DynamicSecretProviders.AwsIam]: AwsIamProvider(),
|
||||
[DynamicSecretProviders.Redis]: RedisDatabaseProvider(),
|
||||
|
||||
@@ -103,7 +103,8 @@ export const DynamicSecretSqlDBSchema = z.object({
|
||||
creationStatement: z.string().trim(),
|
||||
revocationStatement: z.string().trim(),
|
||||
renewStatement: z.string().trim().optional(),
|
||||
ca: z.string().optional()
|
||||
ca: z.string().optional(),
|
||||
gatewayId: z.string().nullable().optional()
|
||||
});
|
||||
|
||||
export const DynamicSecretCassandraSchema = z.object({
|
||||
|
||||
@@ -3,8 +3,10 @@ import knex from "knex";
|
||||
import { customAlphabet } from "nanoid";
|
||||
import { z } from "zod";
|
||||
|
||||
import { withGatewayProxy } from "@app/lib/gateway";
|
||||
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
||||
|
||||
import { TGatewayServiceFactory } from "../../gateway/gateway-service";
|
||||
import { verifyHostInputValidity } from "../dynamic-secret-fns";
|
||||
import { DynamicSecretSqlDBSchema, SqlProviders, TDynamicProviderFns } from "./models";
|
||||
|
||||
@@ -25,10 +27,14 @@ const generateUsername = (provider: SqlProviders) => {
|
||||
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 providerInputs = await DynamicSecretSqlDBSchema.parseAsync(inputs);
|
||||
verifyHostInputValidity(providerInputs.host);
|
||||
verifyHostInputValidity(providerInputs.host, Boolean(providerInputs.gatewayId));
|
||||
return providerInputs;
|
||||
};
|
||||
|
||||
@@ -61,61 +67,107 @@ export const SqlDatabaseProvider = (): TDynamicProviderFns => {
|
||||
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 providerInputs = await validateProviderInputs(inputs);
|
||||
const db = await $getClient(providerInputs);
|
||||
// oracle needs from keyword
|
||||
const testStatement = providerInputs.client === SqlProviders.Oracle ? "SELECT 1 FROM DUAL" : "SELECT 1";
|
||||
let isConnected = false;
|
||||
const gatewayCallback = async (port = providerInputs.port) => {
|
||||
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);
|
||||
await db.destroy();
|
||||
isConnected = await db.raw(testStatement).then(() => true);
|
||||
await db.destroy();
|
||||
};
|
||||
|
||||
if (providerInputs.gatewayId) {
|
||||
await gatewayProxyWrapper(providerInputs, gatewayCallback);
|
||||
} else {
|
||||
await gatewayCallback();
|
||||
}
|
||||
return isConnected;
|
||||
};
|
||||
|
||||
const create = async (inputs: unknown, expireAt: number) => {
|
||||
const providerInputs = await validateProviderInputs(inputs);
|
||||
const db = await $getClient(providerInputs);
|
||||
|
||||
const username = generateUsername(providerInputs.client);
|
||||
const password = generatePassword(providerInputs.client);
|
||||
const { database } = providerInputs;
|
||||
const expiration = new Date(expireAt).toISOString();
|
||||
const gatewayCallback = async (port = providerInputs.port) => {
|
||||
const db = await $getClient({ ...providerInputs, port });
|
||||
const { database } = providerInputs;
|
||||
const expiration = new Date(expireAt).toISOString();
|
||||
|
||||
const creationStatement = handlebars.compile(providerInputs.creationStatement, { noEscape: true })({
|
||||
username,
|
||||
password,
|
||||
expiration,
|
||||
database
|
||||
});
|
||||
const creationStatement = handlebars.compile(providerInputs.creationStatement, { noEscape: true })({
|
||||
username,
|
||||
password,
|
||||
expiration,
|
||||
database
|
||||
});
|
||||
|
||||
const queries = creationStatement.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();
|
||||
const queries = creationStatement.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: username, data: { DB_USERNAME: username, DB_PASSWORD: password } };
|
||||
};
|
||||
|
||||
const revoke = async (inputs: unknown, entityId: string) => {
|
||||
const providerInputs = await validateProviderInputs(inputs);
|
||||
const db = await $getClient(providerInputs);
|
||||
|
||||
const username = entityId;
|
||||
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 });
|
||||
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);
|
||||
}
|
||||
});
|
||||
|
||||
await db.destroy();
|
||||
await db.destroy();
|
||||
};
|
||||
if (providerInputs.gatewayId) {
|
||||
await gatewayProxyWrapper(providerInputs, gatewayCallback);
|
||||
} else {
|
||||
await gatewayCallback();
|
||||
}
|
||||
return { entityId: username };
|
||||
};
|
||||
|
||||
@@ -123,28 +175,34 @@ export const SqlDatabaseProvider = (): TDynamicProviderFns => {
|
||||
const providerInputs = await validateProviderInputs(inputs);
|
||||
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 { database } = providerInputs;
|
||||
|
||||
const renewStatement = handlebars.compile(providerInputs.renewStatement)({
|
||||
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);
|
||||
}
|
||||
const renewStatement = handlebars.compile(providerInputs.renewStatement)({
|
||||
username: entityId,
|
||||
expiration,
|
||||
database
|
||||
});
|
||||
}
|
||||
|
||||
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 };
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user