mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-04 08:26:10 +00:00
feat(dynamic-secrets): ElasticSearch support
This commit is contained in:
@@ -0,0 +1,177 @@
|
|||||||
|
/* eslint-disable no-console */
|
||||||
|
import { Client as ElasticCacheClient } from "@elastic/elasticsearch";
|
||||||
|
import handlebars from "handlebars";
|
||||||
|
import { customAlphabet } from "nanoid";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { getConfig } from "@app/lib/config/env";
|
||||||
|
import { BadRequestError } from "@app/lib/errors";
|
||||||
|
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
||||||
|
|
||||||
|
import { DynamicSecretElasticSearchSchema, TDynamicProviderFns } from "./models";
|
||||||
|
|
||||||
|
const generatePassword = () => {
|
||||||
|
const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_.~!*$#";
|
||||||
|
return customAlphabet(charset, 64)();
|
||||||
|
};
|
||||||
|
|
||||||
|
const generateUsername = () => {
|
||||||
|
return alphaNumericNanoId(32);
|
||||||
|
};
|
||||||
|
|
||||||
|
const parseJsonStatement = (str: string) => {
|
||||||
|
try {
|
||||||
|
return JSON.parse(str) as object;
|
||||||
|
} catch {
|
||||||
|
throw new BadRequestError({ message: "Failed to parse ElasticSearch statements" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const CreateElasticSearchUserSchema = z
|
||||||
|
.object({
|
||||||
|
username: z.string(),
|
||||||
|
password: z.string().optional(),
|
||||||
|
password_hash: z.string().optional(),
|
||||||
|
|
||||||
|
refresh: z.any(),
|
||||||
|
email: z.string().optional(),
|
||||||
|
full_name: z.string().optional().default("Managed by Infisical.com"), // We are overriding this
|
||||||
|
metadata: z.any().optional(),
|
||||||
|
|
||||||
|
roles: z.array(z.string()).min(1), // i.e ['superuser']
|
||||||
|
enabled: z.boolean().default(true)
|
||||||
|
})
|
||||||
|
.refine((data) => {
|
||||||
|
// Ensure either password_hash or password is present
|
||||||
|
if (!data.password && !data.password_hash) {
|
||||||
|
throw new Error("Either password or password_hash is required");
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
|
const DeleteElasticSearchUserSchema = z.object({
|
||||||
|
username: z.string().trim().min(1)
|
||||||
|
});
|
||||||
|
|
||||||
|
export const ElasticSearchDatabaseProvider = (): TDynamicProviderFns => {
|
||||||
|
const validateProviderInputs = async (inputs: unknown) => {
|
||||||
|
const appCfg = getConfig();
|
||||||
|
const isCloud = Boolean(appCfg.LICENSE_SERVER_KEY); // quick and dirty way to check if its cloud or not
|
||||||
|
|
||||||
|
const providerInputs = await DynamicSecretElasticSearchSchema.parseAsync(inputs);
|
||||||
|
if (
|
||||||
|
isCloud &&
|
||||||
|
// 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 === "127.0.0.1") {
|
||||||
|
throw new BadRequestError({ message: "Invalid db host" });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!CreateElasticSearchUserSchema.safeParse(parseJsonStatement(providerInputs.creationStatement)).success) {
|
||||||
|
throw new BadRequestError({ message: "Invalid creation statement" });
|
||||||
|
}
|
||||||
|
if (!DeleteElasticSearchUserSchema.safeParse(parseJsonStatement(providerInputs.revocationStatement)).success) {
|
||||||
|
throw new BadRequestError({ message: "Invalid revocation statement" });
|
||||||
|
}
|
||||||
|
|
||||||
|
return providerInputs;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getClient = async (providerInputs: z.infer<typeof DynamicSecretElasticSearchSchema>) => {
|
||||||
|
const connection = new ElasticCacheClient({
|
||||||
|
node: {
|
||||||
|
url: new URL(`${providerInputs.host}:${providerInputs.port}`),
|
||||||
|
...(providerInputs.ca && {
|
||||||
|
ssl: {
|
||||||
|
rejectUnauthorized: false,
|
||||||
|
ca: providerInputs.ca
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
auth: {
|
||||||
|
...(providerInputs.auth.type === "api-key"
|
||||||
|
? {
|
||||||
|
apiKey: {
|
||||||
|
api_key: providerInputs.auth.apiKey,
|
||||||
|
id: providerInputs.auth.apiKeyId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
username: providerInputs.auth.username,
|
||||||
|
password: providerInputs.auth.password
|
||||||
|
})
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return connection;
|
||||||
|
};
|
||||||
|
|
||||||
|
const validateConnection = async (inputs: unknown) => {
|
||||||
|
const providerInputs = await validateProviderInputs(inputs);
|
||||||
|
const connection = await getClient(providerInputs);
|
||||||
|
|
||||||
|
const infoResponse = await connection
|
||||||
|
.info()
|
||||||
|
.then(() => true)
|
||||||
|
.catch(() => false);
|
||||||
|
|
||||||
|
return infoResponse;
|
||||||
|
};
|
||||||
|
|
||||||
|
const create = async (inputs: unknown, expireAt: number) => {
|
||||||
|
const providerInputs = await validateProviderInputs(inputs);
|
||||||
|
const connection = await getClient(providerInputs);
|
||||||
|
|
||||||
|
const username = generateUsername();
|
||||||
|
const password = generatePassword();
|
||||||
|
|
||||||
|
const expiration = new Date(expireAt).toISOString();
|
||||||
|
|
||||||
|
const creationStatement = handlebars.compile(providerInputs.creationStatement, { noEscape: true })({
|
||||||
|
username,
|
||||||
|
password,
|
||||||
|
expiration
|
||||||
|
});
|
||||||
|
|
||||||
|
const parsedStatement = CreateElasticSearchUserSchema.parse(parseJsonStatement(creationStatement));
|
||||||
|
|
||||||
|
await connection.security.putUser(parsedStatement);
|
||||||
|
|
||||||
|
await connection.close();
|
||||||
|
return { entityId: username, data: { DB_USERNAME: username, DB_PASSWORD: password } };
|
||||||
|
};
|
||||||
|
|
||||||
|
const revoke = async (inputs: unknown, entityId: string) => {
|
||||||
|
const providerInputs = await validateProviderInputs(inputs);
|
||||||
|
const connection = await getClient(providerInputs);
|
||||||
|
|
||||||
|
const username = entityId;
|
||||||
|
|
||||||
|
const revokeStatement = handlebars.compile(providerInputs.revocationStatement)({ username });
|
||||||
|
const parsedStatement = DeleteElasticSearchUserSchema.parse(parseJsonStatement(revokeStatement));
|
||||||
|
|
||||||
|
await connection.security.deleteUser(parsedStatement);
|
||||||
|
|
||||||
|
await connection.close();
|
||||||
|
return { entityId: username };
|
||||||
|
};
|
||||||
|
|
||||||
|
const renew = async (inputs: unknown, entityId: string) => {
|
||||||
|
// Do nothing
|
||||||
|
return { entityId };
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
validateProviderInputs,
|
||||||
|
validateConnection,
|
||||||
|
create,
|
||||||
|
revoke,
|
||||||
|
renew
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import { AwsElastiCacheDatabaseProvider } from "./aws-elasticache";
|
import { AwsElastiCacheDatabaseProvider } from "./aws-elasticache";
|
||||||
import { AwsIamProvider } from "./aws-iam";
|
import { AwsIamProvider } from "./aws-iam";
|
||||||
import { CassandraProvider } from "./cassandra";
|
import { CassandraProvider } from "./cassandra";
|
||||||
|
import { ElasticSearchDatabaseProvider } from "./elastic-search";
|
||||||
import { DynamicSecretProviders } from "./models";
|
import { DynamicSecretProviders } from "./models";
|
||||||
import { MongoAtlasProvider } from "./mongo-atlas";
|
import { MongoAtlasProvider } from "./mongo-atlas";
|
||||||
import { RedisDatabaseProvider } from "./redis";
|
import { RedisDatabaseProvider } from "./redis";
|
||||||
@@ -12,5 +13,6 @@ export const buildDynamicSecretProviders = () => ({
|
|||||||
[DynamicSecretProviders.AwsIam]: AwsIamProvider(),
|
[DynamicSecretProviders.AwsIam]: AwsIamProvider(),
|
||||||
[DynamicSecretProviders.Redis]: RedisDatabaseProvider(),
|
[DynamicSecretProviders.Redis]: RedisDatabaseProvider(),
|
||||||
[DynamicSecretProviders.AwsElastiCache]: AwsElastiCacheDatabaseProvider(),
|
[DynamicSecretProviders.AwsElastiCache]: AwsElastiCacheDatabaseProvider(),
|
||||||
[DynamicSecretProviders.MongoAtlas]: MongoAtlasProvider()
|
[DynamicSecretProviders.MongoAtlas]: MongoAtlasProvider(),
|
||||||
|
[DynamicSecretProviders.ElasticSearch]: ElasticSearchDatabaseProvider()
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -30,6 +30,29 @@ export const DynamicSecretAwsElastiCacheSchema = z.object({
|
|||||||
ca: z.string().optional()
|
ca: z.string().optional()
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const DynamicSecretElasticSearchSchema = z.object({
|
||||||
|
host: z.string().trim().min(1),
|
||||||
|
port: z.number(),
|
||||||
|
|
||||||
|
// two auth types "user, apikey"
|
||||||
|
auth: z.discriminatedUnion("type", [
|
||||||
|
z.object({
|
||||||
|
type: z.literal("user"),
|
||||||
|
username: z.string().trim(),
|
||||||
|
password: z.string().trim()
|
||||||
|
}),
|
||||||
|
z.object({
|
||||||
|
type: z.literal("api-key"),
|
||||||
|
apiKey: z.string().trim(),
|
||||||
|
apiKeyId: z.string().trim()
|
||||||
|
})
|
||||||
|
]),
|
||||||
|
|
||||||
|
creationStatement: z.string().trim(),
|
||||||
|
revocationStatement: z.string().trim(),
|
||||||
|
ca: z.string().optional()
|
||||||
|
});
|
||||||
|
|
||||||
export const DynamicSecretSqlDBSchema = z.object({
|
export const DynamicSecretSqlDBSchema = z.object({
|
||||||
client: z.nativeEnum(SqlProviders),
|
client: z.nativeEnum(SqlProviders),
|
||||||
host: z.string().trim().toLowerCase(),
|
host: z.string().trim().toLowerCase(),
|
||||||
@@ -110,7 +133,8 @@ export enum DynamicSecretProviders {
|
|||||||
AwsIam = "aws-iam",
|
AwsIam = "aws-iam",
|
||||||
Redis = "redis",
|
Redis = "redis",
|
||||||
AwsElastiCache = "aws-elasticache",
|
AwsElastiCache = "aws-elasticache",
|
||||||
MongoAtlas = "mongo-db-atlas"
|
MongoAtlas = "mongo-db-atlas",
|
||||||
|
ElasticSearch = "elastic-search"
|
||||||
}
|
}
|
||||||
|
|
||||||
export const DynamicSecretProviderSchema = z.discriminatedUnion("type", [
|
export const DynamicSecretProviderSchema = z.discriminatedUnion("type", [
|
||||||
@@ -119,7 +143,8 @@ export const DynamicSecretProviderSchema = z.discriminatedUnion("type", [
|
|||||||
z.object({ type: z.literal(DynamicSecretProviders.AwsIam), inputs: DynamicSecretAwsIamSchema }),
|
z.object({ type: z.literal(DynamicSecretProviders.AwsIam), inputs: DynamicSecretAwsIamSchema }),
|
||||||
z.object({ type: z.literal(DynamicSecretProviders.Redis), inputs: DynamicSecretRedisDBSchema }),
|
z.object({ type: z.literal(DynamicSecretProviders.Redis), inputs: DynamicSecretRedisDBSchema }),
|
||||||
z.object({ type: z.literal(DynamicSecretProviders.AwsElastiCache), inputs: DynamicSecretAwsElastiCacheSchema }),
|
z.object({ type: z.literal(DynamicSecretProviders.AwsElastiCache), inputs: DynamicSecretAwsElastiCacheSchema }),
|
||||||
z.object({ type: z.literal(DynamicSecretProviders.MongoAtlas), inputs: DynamicSecretMongoAtlasSchema })
|
z.object({ type: z.literal(DynamicSecretProviders.MongoAtlas), inputs: DynamicSecretMongoAtlasSchema }),
|
||||||
|
z.object({ type: z.literal(DynamicSecretProviders.ElasticSearch), inputs: DynamicSecretElasticSearchSchema })
|
||||||
]);
|
]);
|
||||||
|
|
||||||
export type TDynamicProviderFns = {
|
export type TDynamicProviderFns = {
|
||||||
|
|||||||
Reference in New Issue
Block a user