mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-09-22 13:39:35 +00:00
feat(server): added dynamic secret cassandra
This commit is contained in:
32
backend/package-lock.json
generated
32
backend/package-lock.json
generated
@@ -35,6 +35,7 @@
|
|||||||
"axios-retry": "^4.0.0",
|
"axios-retry": "^4.0.0",
|
||||||
"bcrypt": "^5.1.1",
|
"bcrypt": "^5.1.1",
|
||||||
"bullmq": "^5.3.3",
|
"bullmq": "^5.3.3",
|
||||||
|
"cassandra-driver": "^4.7.2",
|
||||||
"dotenv": "^16.4.1",
|
"dotenv": "^16.4.1",
|
||||||
"fastify": "^4.26.0",
|
"fastify": "^4.26.0",
|
||||||
"fastify-plugin": "^4.5.1",
|
"fastify-plugin": "^4.5.1",
|
||||||
@@ -4565,6 +4566,15 @@
|
|||||||
"@types/lodash": "*"
|
"@types/lodash": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/long": {
|
||||||
|
"version": "5.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/long/-/long-5.0.0.tgz",
|
||||||
|
"integrity": "sha512-eQs9RsucA/LNjnMoJvWG/nXa7Pot/RbBzilF/QRIU/xRl+0ApxrSUFsV5lmf01SvSlqMzJ7Zwxe440wmz2SJGA==",
|
||||||
|
"deprecated": "This is a stub types definition. long provides its own type definitions, so you do not need this installed.",
|
||||||
|
"dependencies": {
|
||||||
|
"long": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@types/mime": {
|
"node_modules/@types/mime": {
|
||||||
"version": "1.3.5",
|
"version": "1.3.5",
|
||||||
"resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz",
|
"resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz",
|
||||||
@@ -5313,6 +5323,14 @@
|
|||||||
"node": ">=0.4.0"
|
"node": ">=0.4.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/adm-zip": {
|
||||||
|
"version": "0.5.12",
|
||||||
|
"resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.5.12.tgz",
|
||||||
|
"integrity": "sha512-6TVU49mK6KZb4qG6xWaaM4C7sA/sgUMLy/JYMOzkcp3BvVLpW0fXDFQiIzAuxFCt/2+xD7fNIiPFAoLZPhVNLQ==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/agent-base": {
|
"node_modules/agent-base": {
|
||||||
"version": "6.0.2",
|
"version": "6.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz",
|
||||||
@@ -6190,6 +6208,20 @@
|
|||||||
"node": ">=6"
|
"node": ">=6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/cassandra-driver": {
|
||||||
|
"version": "4.7.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/cassandra-driver/-/cassandra-driver-4.7.2.tgz",
|
||||||
|
"integrity": "sha512-gwl1DeYvL8Wy3i1GDMzFtpUg5G473fU7EnHFZj7BUtdLB7loAfgZgB3zBhROc9fbaDSUDs6YwOPPojS5E1kbSA==",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/long": "~5.0.0",
|
||||||
|
"@types/node": ">=8",
|
||||||
|
"adm-zip": "~0.5.10",
|
||||||
|
"long": "~5.2.3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=16"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/chai": {
|
"node_modules/chai": {
|
||||||
"version": "4.4.1",
|
"version": "4.4.1",
|
||||||
"resolved": "https://registry.npmjs.org/chai/-/chai-4.4.1.tgz",
|
"resolved": "https://registry.npmjs.org/chai/-/chai-4.4.1.tgz",
|
||||||
|
|||||||
@@ -96,6 +96,7 @@
|
|||||||
"axios-retry": "^4.0.0",
|
"axios-retry": "^4.0.0",
|
||||||
"bcrypt": "^5.1.1",
|
"bcrypt": "^5.1.1",
|
||||||
"bullmq": "^5.3.3",
|
"bullmq": "^5.3.3",
|
||||||
|
"cassandra-driver": "^4.7.2",
|
||||||
"dotenv": "^16.4.1",
|
"dotenv": "^16.4.1",
|
||||||
"fastify": "^4.26.0",
|
"fastify": "^4.26.0",
|
||||||
"fastify-plugin": "^4.5.1",
|
"fastify-plugin": "^4.5.1",
|
||||||
|
|||||||
125
backend/src/ee/services/dynamic-secret/providers/cassandra.ts
Normal file
125
backend/src/ee/services/dynamic-secret/providers/cassandra.ts
Normal file
@@ -0,0 +1,125 @@
|
|||||||
|
import cassandra from "cassandra-driver";
|
||||||
|
import handlebars from "handlebars";
|
||||||
|
import { customAlphabet } from "nanoid";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { BadRequestError } from "@app/lib/errors";
|
||||||
|
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
||||||
|
|
||||||
|
import { DynamicSecretCassandraSchema, TDynamicProviderFns } from "./models";
|
||||||
|
|
||||||
|
const generatePassword = (size = 48) => {
|
||||||
|
const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_.~!*$#";
|
||||||
|
return customAlphabet(charset, 48)(size);
|
||||||
|
};
|
||||||
|
|
||||||
|
const generateUsername = () => {
|
||||||
|
return alphaNumericNanoId(32);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const CassandraProvider = (): TDynamicProviderFns => {
|
||||||
|
const validateProviderInputs = async (inputs: unknown) => {
|
||||||
|
const providerInputs = await DynamicSecretCassandraSchema.parseAsync(inputs);
|
||||||
|
if (providerInputs.host === "localhost" || providerInputs.host === "127.0.0.1") {
|
||||||
|
throw new BadRequestError({ message: "Invalid db host" });
|
||||||
|
}
|
||||||
|
|
||||||
|
return providerInputs;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getClient = async (providerInputs: z.infer<typeof DynamicSecretCassandraSchema>) => {
|
||||||
|
const sslOptions = providerInputs.ca ? { rejectUnauthorized: false, ca: providerInputs.ca } : undefined;
|
||||||
|
const client = new cassandra.Client({
|
||||||
|
sslOptions,
|
||||||
|
protocolOptions: {
|
||||||
|
port: providerInputs.port
|
||||||
|
},
|
||||||
|
credentials: {
|
||||||
|
username: providerInputs.username,
|
||||||
|
password: providerInputs.password
|
||||||
|
},
|
||||||
|
keyspace: providerInputs.keyspace,
|
||||||
|
localDataCenter: providerInputs?.localDataCenter,
|
||||||
|
contactPoints: providerInputs.host.split(",").filter(Boolean)
|
||||||
|
});
|
||||||
|
return client;
|
||||||
|
};
|
||||||
|
|
||||||
|
const validateConnection = async (inputs: unknown) => {
|
||||||
|
const providerInputs = await validateProviderInputs(inputs);
|
||||||
|
const client = await getClient(providerInputs);
|
||||||
|
|
||||||
|
const isConnected = await client.execute("SELECT * FROM system_schema.keyspaces").then(() => true);
|
||||||
|
await client.shutdown();
|
||||||
|
return isConnected;
|
||||||
|
};
|
||||||
|
|
||||||
|
const create = async (inputs: unknown, expireAt: number) => {
|
||||||
|
const providerInputs = await validateProviderInputs(inputs);
|
||||||
|
const client = await getClient(providerInputs);
|
||||||
|
|
||||||
|
const username = generateUsername();
|
||||||
|
const password = generatePassword();
|
||||||
|
const { keyspace } = providerInputs;
|
||||||
|
const expiration = new Date(expireAt).toISOString();
|
||||||
|
|
||||||
|
const creationStatement = handlebars.compile(providerInputs.creationStatement, { noEscape: true })({
|
||||||
|
username,
|
||||||
|
password,
|
||||||
|
expiration,
|
||||||
|
keyspace
|
||||||
|
});
|
||||||
|
|
||||||
|
const queries = creationStatement.toString().split(";").filter(Boolean);
|
||||||
|
for (const query of queries) {
|
||||||
|
// eslint-disable-next-line
|
||||||
|
await client.execute(query);
|
||||||
|
}
|
||||||
|
await client.shutdown();
|
||||||
|
|
||||||
|
return { entityId: username, data: { DB_USERNAME: username, DB_PASSWORD: password } };
|
||||||
|
};
|
||||||
|
|
||||||
|
const revoke = async (inputs: unknown, entityId: string) => {
|
||||||
|
const providerInputs = await validateProviderInputs(inputs);
|
||||||
|
const client = await getClient(providerInputs);
|
||||||
|
|
||||||
|
const username = entityId;
|
||||||
|
const { keyspace } = providerInputs;
|
||||||
|
|
||||||
|
const revokeStatement = handlebars.compile(providerInputs.revocationStatement)({ username, keyspace });
|
||||||
|
const queries = revokeStatement.toString().split(";").filter(Boolean);
|
||||||
|
for (const query of queries) {
|
||||||
|
// eslint-disable-next-line
|
||||||
|
await client.execute(query);
|
||||||
|
}
|
||||||
|
await client.shutdown();
|
||||||
|
return { entityId: username };
|
||||||
|
};
|
||||||
|
|
||||||
|
const renew = async (inputs: unknown, entityId: string, expireAt: number) => {
|
||||||
|
const providerInputs = await validateProviderInputs(inputs);
|
||||||
|
const client = await getClient(providerInputs);
|
||||||
|
|
||||||
|
const username = entityId;
|
||||||
|
const expiration = new Date(expireAt).toISOString();
|
||||||
|
const { keyspace } = providerInputs;
|
||||||
|
|
||||||
|
const renewStatement = handlebars.compile(providerInputs.revocationStatement)({ username, keyspace, expiration });
|
||||||
|
const queries = renewStatement.toString().split(";").filter(Boolean);
|
||||||
|
for (const query of queries) {
|
||||||
|
// eslint-disable-next-line
|
||||||
|
await client.execute(query);
|
||||||
|
}
|
||||||
|
await client.shutdown();
|
||||||
|
return { entityId: username };
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
validateProviderInputs,
|
||||||
|
validateConnection,
|
||||||
|
create,
|
||||||
|
revoke,
|
||||||
|
renew
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -1,6 +1,8 @@
|
|||||||
|
import { CassandraProvider } from "./cassandra";
|
||||||
import { DynamicSecretProviders } from "./models";
|
import { DynamicSecretProviders } from "./models";
|
||||||
import { SqlDatabaseProvider } from "./sql-database";
|
import { SqlDatabaseProvider } from "./sql-database";
|
||||||
|
|
||||||
export const buildDynamicSecretProviders = () => ({
|
export const buildDynamicSecretProviders = () => ({
|
||||||
[DynamicSecretProviders.SqlDatabase]: SqlDatabaseProvider()
|
[DynamicSecretProviders.SqlDatabase]: SqlDatabaseProvider(),
|
||||||
|
[DynamicSecretProviders.Cassandra]: CassandraProvider()
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -19,12 +19,27 @@ export const DynamicSecretSqlDBSchema = z.object({
|
|||||||
ca: z.string().optional()
|
ca: z.string().optional()
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const DynamicSecretCassandraSchema = z.object({
|
||||||
|
host: z.string().toLowerCase(),
|
||||||
|
port: z.number(),
|
||||||
|
localDataCenter: z.string().min(1),
|
||||||
|
keyspace: z.string().optional(),
|
||||||
|
username: z.string(),
|
||||||
|
password: z.string(),
|
||||||
|
creationStatement: z.string(),
|
||||||
|
revocationStatement: z.string(),
|
||||||
|
renewStatement: z.string().optional(),
|
||||||
|
ca: z.string().optional()
|
||||||
|
});
|
||||||
|
|
||||||
export enum DynamicSecretProviders {
|
export enum DynamicSecretProviders {
|
||||||
SqlDatabase = "sql-database"
|
SqlDatabase = "sql-database",
|
||||||
|
Cassandra = "cassandra"
|
||||||
}
|
}
|
||||||
|
|
||||||
export const DynamicSecretProviderSchema = z.discriminatedUnion("type", [
|
export const DynamicSecretProviderSchema = z.discriminatedUnion("type", [
|
||||||
z.object({ type: z.literal(DynamicSecretProviders.SqlDatabase), inputs: DynamicSecretSqlDBSchema })
|
z.object({ type: z.literal(DynamicSecretProviders.SqlDatabase), inputs: DynamicSecretSqlDBSchema }),
|
||||||
|
z.object({ type: z.literal(DynamicSecretProviders.Cassandra), inputs: DynamicSecretCassandraSchema })
|
||||||
]);
|
]);
|
||||||
|
|
||||||
export type TDynamicProviderFns = {
|
export type TDynamicProviderFns = {
|
||||||
|
|||||||
@@ -30,19 +30,24 @@ const generateUsername = (provider: SqlProviders) => {
|
|||||||
export const SqlDatabaseProvider = (): TDynamicProviderFns => {
|
export const SqlDatabaseProvider = (): TDynamicProviderFns => {
|
||||||
const validateProviderInputs = async (inputs: unknown) => {
|
const validateProviderInputs = async (inputs: unknown) => {
|
||||||
const appCfg = getConfig();
|
const appCfg = getConfig();
|
||||||
|
const isCloud = Boolean(appCfg.LICENSE_SERVER_KEY); // quick and dirty way to check if its cloud or not
|
||||||
const dbHost = appCfg.DB_HOST || getDbConnectionHost(appCfg.DB_CONNECTION_URI);
|
const dbHost = appCfg.DB_HOST || getDbConnectionHost(appCfg.DB_CONNECTION_URI);
|
||||||
|
|
||||||
const providerInputs = await DynamicSecretSqlDBSchema.parseAsync(inputs);
|
const providerInputs = await DynamicSecretSqlDBSchema.parseAsync(inputs);
|
||||||
if (
|
if (
|
||||||
|
isCloud &&
|
||||||
// localhost
|
// localhost
|
||||||
|
// internal ips
|
||||||
|
(providerInputs.host === "host.docker.internal" ||
|
||||||
|
providerInputs.host.match(/^10\.\d+\.\d+\.\d+/) ||
|
||||||
|
providerInputs.host.match(/^192\.168\.\d+\.\d+/))
|
||||||
|
)
|
||||||
|
throw new BadRequestError({ message: "Invalid db host" });
|
||||||
|
if (
|
||||||
providerInputs.host === "localhost" ||
|
providerInputs.host === "localhost" ||
|
||||||
providerInputs.host === "127.0.0.1" ||
|
providerInputs.host === "127.0.0.1" ||
|
||||||
// database infisical uses
|
// database infisical uses
|
||||||
dbHost === providerInputs.host ||
|
dbHost === providerInputs.host
|
||||||
// internal ips
|
|
||||||
providerInputs.host === "host.docker.internal" ||
|
|
||||||
providerInputs.host.match(/^10\.\d+\.\d+\.\d+/) ||
|
|
||||||
providerInputs.host.match(/^192\.168\.\d+\.\d+/)
|
|
||||||
)
|
)
|
||||||
throw new BadRequestError({ message: "Invalid db host" });
|
throw new BadRequestError({ message: "Invalid db host" });
|
||||||
return providerInputs;
|
return providerInputs;
|
||||||
@@ -93,15 +98,13 @@ export const SqlDatabaseProvider = (): TDynamicProviderFns => {
|
|||||||
database
|
database
|
||||||
});
|
});
|
||||||
|
|
||||||
await db.transaction(async (tx) =>
|
const queries = creationStatement.toString().split(";").filter(Boolean);
|
||||||
Promise.all(
|
await db.transaction(async (tx) => {
|
||||||
creationStatement
|
for (const query of queries) {
|
||||||
.toString()
|
// eslint-disable-next-line
|
||||||
.split(";")
|
await tx.raw(query);
|
||||||
.filter(Boolean)
|
}
|
||||||
.map((query) => tx.raw(query))
|
});
|
||||||
)
|
|
||||||
);
|
|
||||||
await db.destroy();
|
await db.destroy();
|
||||||
return { entityId: username, data: { DB_USERNAME: username, DB_PASSWORD: password } };
|
return { entityId: username, data: { DB_USERNAME: username, DB_PASSWORD: password } };
|
||||||
};
|
};
|
||||||
@@ -114,15 +117,13 @@ export const SqlDatabaseProvider = (): TDynamicProviderFns => {
|
|||||||
const { database } = providerInputs;
|
const { database } = providerInputs;
|
||||||
|
|
||||||
const revokeStatement = handlebars.compile(providerInputs.revocationStatement)({ username, database });
|
const revokeStatement = handlebars.compile(providerInputs.revocationStatement)({ username, database });
|
||||||
await db.transaction(async (tx) =>
|
const queries = revokeStatement.toString().split(";").filter(Boolean);
|
||||||
Promise.all(
|
await db.transaction(async (tx) => {
|
||||||
revokeStatement
|
for (const query of queries) {
|
||||||
.toString()
|
// eslint-disable-next-line
|
||||||
.split(";")
|
await tx.raw(query);
|
||||||
.filter(Boolean)
|
}
|
||||||
.map((query) => tx.raw(query))
|
});
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
await db.destroy();
|
await db.destroy();
|
||||||
return { entityId: username };
|
return { entityId: username };
|
||||||
@@ -137,16 +138,15 @@ export const SqlDatabaseProvider = (): TDynamicProviderFns => {
|
|||||||
const { database } = providerInputs;
|
const { database } = providerInputs;
|
||||||
|
|
||||||
const renewStatement = handlebars.compile(providerInputs.renewStatement)({ username, expiration, database });
|
const renewStatement = handlebars.compile(providerInputs.renewStatement)({ username, expiration, database });
|
||||||
if (renewStatement)
|
if (renewStatement) {
|
||||||
await db.transaction(async (tx) =>
|
const queries = renewStatement.toString().split(";").filter(Boolean);
|
||||||
Promise.all(
|
await db.transaction(async (tx) => {
|
||||||
renewStatement
|
for (const query of queries) {
|
||||||
.toString()
|
// eslint-disable-next-line
|
||||||
.split(";")
|
await tx.raw(query);
|
||||||
.filter(Boolean)
|
}
|
||||||
.map((query) => tx.raw(query))
|
});
|
||||||
)
|
}
|
||||||
);
|
|
||||||
|
|
||||||
await db.destroy();
|
await db.destroy();
|
||||||
return { entityId: username };
|
return { entityId: username };
|
||||||
|
|||||||
Reference in New Issue
Block a user