Merge pull request #1689 from akhilmhdh/dynamic-secret/mysql

Dynamic secret mysql support
This commit is contained in:
Maidul Islam
2024-04-15 15:51:17 -04:00
committed by GitHub
6 changed files with 84 additions and 25 deletions
@@ -249,7 +249,7 @@ export const dynamicSecretLeaseServiceFactory = ({
if ((revokeResponse as { error?: Error })?.error) { if ((revokeResponse as { error?: Error })?.error) {
const { error } = revokeResponse as { error?: Error }; const { error } = revokeResponse as { error?: Error };
logger.error("Failed to revoke lease", { error: error?.message }); logger.error(error?.message, "Failed to revoke lease");
const deletedDynamicSecretLease = await dynamicSecretLeaseDAL.updateById(dynamicSecretLease.id, { const deletedDynamicSecretLease = await dynamicSecretLeaseDAL.updateById(dynamicSecretLease.id, {
status: DynamicSecretLeaseStatus.FailedDeletion, status: DynamicSecretLeaseStatus.FailedDeletion,
statusDetails: error?.message?.slice(0, 255) statusDetails: error?.message?.slice(0, 255)
@@ -1,7 +1,8 @@
import { z } from "zod"; import { z } from "zod";
export enum SqlProviders { export enum SqlProviders {
Postgres = "postgres" Postgres = "postgres",
MySQL = "mysql2"
} }
export const DynamicSecretSqlDBSchema = z.object({ export const DynamicSecretSqlDBSchema = z.object({
@@ -13,7 +14,7 @@ export const DynamicSecretSqlDBSchema = z.object({
password: z.string(), password: z.string(),
creationStatement: z.string(), creationStatement: z.string(),
revocationStatement: z.string(), revocationStatement: z.string(),
renewStatement: z.string(), renewStatement: z.string().optional(),
ca: z.string().optional() ca: z.string().optional()
}); });
@@ -48,10 +48,10 @@ export const SqlDatabaseProvider = (): TDynamicProviderFns => {
host: providerInputs.host, host: providerInputs.host,
user: providerInputs.username, user: providerInputs.username,
password: providerInputs.password, password: providerInputs.password,
connectionTimeoutMillis: EXTERNAL_REQUEST_TIMEOUT,
ssl, ssl,
pool: { min: 0, max: 1 } pool: { min: 0, max: 1 }
} },
acquireConnectionTimeout: EXTERNAL_REQUEST_TIMEOUT
}); });
return db; return db;
}; };
@@ -73,15 +73,25 @@ export const SqlDatabaseProvider = (): TDynamicProviderFns => {
const username = alphaNumericNanoId(32); const username = alphaNumericNanoId(32);
const password = generatePassword(); const password = generatePassword();
const { database } = providerInputs;
const expiration = new Date(expireAt).toISOString(); 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
}); });
await db.raw(creationStatement.toString()); await db.transaction(async (tx) =>
Promise.all(
creationStatement
.toString()
.split(";")
.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 } };
}; };
@@ -91,9 +101,18 @@ export const SqlDatabaseProvider = (): TDynamicProviderFns => {
const db = await getClient(providerInputs); const db = await getClient(providerInputs);
const username = entityId; const username = entityId;
const { database } = providerInputs;
const revokeStatement = handlebars.compile(providerInputs.revocationStatement)({ username }); const revokeStatement = handlebars.compile(providerInputs.revocationStatement)({ username, database });
await db.raw(revokeStatement); await db.transaction(async (tx) =>
Promise.all(
revokeStatement
.toString()
.split(";")
.filter(Boolean)
.map((query) => tx.raw(query))
)
);
await db.destroy(); await db.destroy();
return { entityId: username }; return { entityId: username };
@@ -105,9 +124,19 @@ export const SqlDatabaseProvider = (): TDynamicProviderFns => {
const username = entityId; const username = entityId;
const expiration = new Date(expireAt).toISOString(); const expiration = new Date(expireAt).toISOString();
const { database } = providerInputs;
const renewStatement = handlebars.compile(providerInputs.renewStatement)({ username, expiration }); const renewStatement = handlebars.compile(providerInputs.renewStatement)({ username, expiration, database });
await db.raw(renewStatement); if (renewStatement)
await db.transaction(async (tx) =>
Promise.all(
renewStatement
.toString()
.split(";")
.filter(Boolean)
.map((query) => tx.raw(query))
)
);
await db.destroy(); await db.destroy();
return { entityId: username }; return { entityId: username };
@@ -20,7 +20,8 @@ export enum DynamicSecretProviders {
} }
export enum SqlProviders { export enum SqlProviders {
Postgres = "postgres" Postgres = "postgres",
MySql = "mysql2"
} }
export type TDynamicSecretProvider = { export type TDynamicSecretProvider = {
@@ -34,7 +35,7 @@ export type TDynamicSecretProvider = {
password: string; password: string;
creationStatement: string; creationStatement: string;
revocationStatement: string; revocationStatement: string;
renewStatement: string; renewStatement?: string;
ca?: string | undefined; ca?: string | undefined;
}; };
}; };
@@ -31,7 +31,7 @@ const formSchema = z.object({
password: z.string().min(1), password: z.string().min(1),
creationStatement: z.string().min(1), creationStatement: z.string().min(1),
revocationStatement: z.string().min(1), revocationStatement: z.string().min(1),
renewStatement: z.string().min(1), renewStatement: z.string().optional(),
ca: z.string().optional() ca: z.string().optional()
}), }),
defaultTTL: z.string().superRefine((val, ctx) => { defaultTTL: z.string().superRefine((val, ctx) => {
@@ -66,6 +66,26 @@ type Props = {
environment: string; environment: string;
}; };
const getSqlStatements = (provider: SqlProviders) => {
if (provider === SqlProviders.MySql) {
return {
creationStatement:
"CREATE USER \"{{username}}\"@'%' IDENTIFIED BY '{{password}}';\nGRANT ALL ON \"{{database}}\".* TO \"{{username}}\"@'%';",
renewStatement: "",
revocationStatement:
'REVOKE ALL PRIVILEGES ON "{{database}}".* FROM "{{username}}"@\'%\';\nDROP USER "{{username}}"@\'%\';'
};
}
return {
creationStatement:
"CREATE USER \"{{username}}\" WITH ENCRYPTED PASSWORD '{{password}}' VALID UNTIL '{{expiration}}';\nGRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO \"{{username}}\";",
renewStatement: "ALTER ROLE \"{{username}}\" VALID UNTIL '{{expiration}}';",
revocationStatement:
'REVOKE ALL PRIVILEGES ON ALL TABLES IN SCHEMA public FROM "{{username}}";\nDROP ROLE "{{username}}";'
};
};
export const SqlDatabaseInputForm = ({ export const SqlDatabaseInputForm = ({
onCompleted, onCompleted,
onCancel, onCancel,
@@ -75,18 +95,13 @@ export const SqlDatabaseInputForm = ({
}: Props) => { }: Props) => {
const { const {
control, control,
setValue,
formState: { isSubmitting }, formState: { isSubmitting },
handleSubmit handleSubmit
} = useForm<TForm>({ } = useForm<TForm>({
resolver: zodResolver(formSchema), resolver: zodResolver(formSchema),
defaultValues: { defaultValues: {
provider: { provider: getSqlStatements(SqlProviders.Postgres)
creationStatement:
"CREATE USER \"{{username}}\" WITH ENCRYPTED PASSWORD '{{password}}' VALID UNTIL '{{expiration}}';\nGRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO \"{{username}}\";",
renewStatement: "ALTER ROLE \"{{username}}\" VALID UNTIL '{{expiration}}';",
revocationStatement:
'REVOKE ALL PRIVILEGES ON ALL TABLES IN SCHEMA public FROM "{{username}}";\nDROP ROLE "{{username}}";'
}
} }
}); });
@@ -182,10 +197,17 @@ export const SqlDatabaseInputForm = ({
<FormControl isError={Boolean(error?.message)} errorText={error?.message}> <FormControl isError={Boolean(error?.message)} errorText={error?.message}>
<Select <Select
value={value} value={value}
onValueChange={(val) => onChange(val)} onValueChange={(val) => {
onChange(val);
const sqlStatment = getSqlStatements(val as SqlProviders);
setValue("provider.creationStatement", sqlStatment.creationStatement);
setValue("provider.renewStatement", sqlStatment.renewStatement);
setValue("provider.revocationStatement", sqlStatment.revocationStatement);
}}
className="w-full border border-mineshaft-500" className="w-full border border-mineshaft-500"
> >
<SelectItem value={SqlProviders.Postgres}>PostgreSQL</SelectItem> <SelectItem value={SqlProviders.Postgres}>PostgreSQL</SelectItem>
<SelectItem value={SqlProviders.MySql}>MySQL</SelectItem>
</Select> </Select>
</FormControl> </FormControl>
)} )}
@@ -32,7 +32,7 @@ const formSchema = z.object({
password: z.string().min(1), password: z.string().min(1),
creationStatement: z.string().min(1), creationStatement: z.string().min(1),
revocationStatement: z.string().min(1), revocationStatement: z.string().min(1),
renewStatement: z.string().min(1), renewStatement: z.string().optional(),
ca: z.string().optional() ca: z.string().optional()
}) })
.partial(), .partial(),
@@ -94,7 +94,7 @@ export const EditDynamicSecretSqlProviderForm = ({
} }
} }
}); });
const updateDynamicSecret = useUpdateDynamicSecret(); const updateDynamicSecret = useUpdateDynamicSecret();
const handleUpdateDynamicSecret = async ({ inputs, maxTTL, defaultTTL, newName }: TForm) => { const handleUpdateDynamicSecret = async ({ inputs, maxTTL, defaultTTL, newName }: TForm) => {
@@ -186,11 +186,13 @@ export const EditDynamicSecretSqlProviderForm = ({
render={({ field: { value, onChange }, fieldState: { error } }) => ( render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl isError={Boolean(error?.message)} errorText={error?.message}> <FormControl isError={Boolean(error?.message)} errorText={error?.message}>
<Select <Select
isDisabled
value={value} value={value}
onValueChange={(val) => onChange(val)} onValueChange={(val) => onChange(val)}
className="w-full border border-mineshaft-500" className="w-full border border-mineshaft-500"
> >
<SelectItem value={SqlProviders.Postgres}>PostgreSQL</SelectItem> <SelectItem value={SqlProviders.Postgres}>PostgreSQL</SelectItem>
<SelectItem value={SqlProviders.MySql}>MySQL</SelectItem>
</Select> </Select>
</FormControl> </FormControl>
)} )}
@@ -221,7 +223,11 @@ export const EditDynamicSecretSqlProviderForm = ({
isError={Boolean(error?.message)} isError={Boolean(error?.message)}
errorText={error?.message} errorText={error?.message}
> >
<Input {...field} type="number" onChange={(el) => field.onChange(parseInt(el.target.value, 10))} /> <Input
{...field}
type="number"
onChange={(el) => field.onChange(parseInt(el.target.value, 10))}
/>
</FormControl> </FormControl>
)} )}
/> />