diff --git a/backend/src/ee/services/dynamic-secret-lease/dynamic-secret-lease-service.ts b/backend/src/ee/services/dynamic-secret-lease/dynamic-secret-lease-service.ts index e46c3c6ee..1e5487d22 100644 --- a/backend/src/ee/services/dynamic-secret-lease/dynamic-secret-lease-service.ts +++ b/backend/src/ee/services/dynamic-secret-lease/dynamic-secret-lease-service.ts @@ -249,7 +249,7 @@ export const dynamicSecretLeaseServiceFactory = ({ if ((revokeResponse as { error?: 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, { status: DynamicSecretLeaseStatus.FailedDeletion, statusDetails: error?.message?.slice(0, 255) diff --git a/backend/src/ee/services/dynamic-secret/providers/models.ts b/backend/src/ee/services/dynamic-secret/providers/models.ts index 2932fd924..9d45a7401 100644 --- a/backend/src/ee/services/dynamic-secret/providers/models.ts +++ b/backend/src/ee/services/dynamic-secret/providers/models.ts @@ -1,7 +1,8 @@ import { z } from "zod"; export enum SqlProviders { - Postgres = "postgres" + Postgres = "postgres", + MySQL = "mysql2" } export const DynamicSecretSqlDBSchema = z.object({ @@ -13,7 +14,7 @@ export const DynamicSecretSqlDBSchema = z.object({ password: z.string(), creationStatement: z.string(), revocationStatement: z.string(), - renewStatement: z.string(), + renewStatement: z.string().optional(), ca: z.string().optional() }); diff --git a/backend/src/ee/services/dynamic-secret/providers/sql-database.ts b/backend/src/ee/services/dynamic-secret/providers/sql-database.ts index 7107fe3a3..ae61fa011 100644 --- a/backend/src/ee/services/dynamic-secret/providers/sql-database.ts +++ b/backend/src/ee/services/dynamic-secret/providers/sql-database.ts @@ -48,10 +48,10 @@ export const SqlDatabaseProvider = (): TDynamicProviderFns => { host: providerInputs.host, user: providerInputs.username, password: providerInputs.password, - connectionTimeoutMillis: EXTERNAL_REQUEST_TIMEOUT, ssl, pool: { min: 0, max: 1 } - } + }, + acquireConnectionTimeout: EXTERNAL_REQUEST_TIMEOUT }); return db; }; @@ -73,15 +73,25 @@ export const SqlDatabaseProvider = (): TDynamicProviderFns => { const username = alphaNumericNanoId(32); const password = generatePassword(); + const { database } = providerInputs; const expiration = new Date(expireAt).toISOString(); const creationStatement = handlebars.compile(providerInputs.creationStatement, { noEscape: true })({ username, 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(); return { entityId: username, data: { DB_USERNAME: username, DB_PASSWORD: password } }; }; @@ -91,9 +101,18 @@ export const SqlDatabaseProvider = (): TDynamicProviderFns => { const db = await getClient(providerInputs); const username = entityId; + const { database } = providerInputs; - const revokeStatement = handlebars.compile(providerInputs.revocationStatement)({ username }); - await db.raw(revokeStatement); + const revokeStatement = handlebars.compile(providerInputs.revocationStatement)({ username, database }); + await db.transaction(async (tx) => + Promise.all( + revokeStatement + .toString() + .split(";") + .filter(Boolean) + .map((query) => tx.raw(query)) + ) + ); await db.destroy(); return { entityId: username }; @@ -105,9 +124,19 @@ export const SqlDatabaseProvider = (): TDynamicProviderFns => { const username = entityId; const expiration = new Date(expireAt).toISOString(); + const { database } = providerInputs; - const renewStatement = handlebars.compile(providerInputs.renewStatement)({ username, expiration }); - await db.raw(renewStatement); + const renewStatement = handlebars.compile(providerInputs.renewStatement)({ username, expiration, database }); + if (renewStatement) + await db.transaction(async (tx) => + Promise.all( + renewStatement + .toString() + .split(";") + .filter(Boolean) + .map((query) => tx.raw(query)) + ) + ); await db.destroy(); return { entityId: username }; diff --git a/frontend/src/hooks/api/dynamicSecret/types.ts b/frontend/src/hooks/api/dynamicSecret/types.ts index b68ad33a1..cab17bda1 100644 --- a/frontend/src/hooks/api/dynamicSecret/types.ts +++ b/frontend/src/hooks/api/dynamicSecret/types.ts @@ -20,7 +20,8 @@ export enum DynamicSecretProviders { } export enum SqlProviders { - Postgres = "postgres" + Postgres = "postgres", + MySql = "mysql2" } export type TDynamicSecretProvider = { @@ -34,7 +35,7 @@ export type TDynamicSecretProvider = { password: string; creationStatement: string; revocationStatement: string; - renewStatement: string; + renewStatement?: string; ca?: string | undefined; }; }; diff --git a/frontend/src/views/SecretMainPage/components/ActionBar/CreateDynamicSecretForm/SqlDatabaseInputForm.tsx b/frontend/src/views/SecretMainPage/components/ActionBar/CreateDynamicSecretForm/SqlDatabaseInputForm.tsx index 674b25dd0..8eb787606 100644 --- a/frontend/src/views/SecretMainPage/components/ActionBar/CreateDynamicSecretForm/SqlDatabaseInputForm.tsx +++ b/frontend/src/views/SecretMainPage/components/ActionBar/CreateDynamicSecretForm/SqlDatabaseInputForm.tsx @@ -31,7 +31,7 @@ const formSchema = z.object({ password: z.string().min(1), creationStatement: z.string().min(1), revocationStatement: z.string().min(1), - renewStatement: z.string().min(1), + renewStatement: z.string().optional(), ca: z.string().optional() }), defaultTTL: z.string().superRefine((val, ctx) => { @@ -66,6 +66,26 @@ type Props = { 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 = ({ onCompleted, onCancel, @@ -75,18 +95,13 @@ export const SqlDatabaseInputForm = ({ }: Props) => { const { control, + setValue, formState: { isSubmitting }, handleSubmit } = useForm({ resolver: zodResolver(formSchema), defaultValues: { - provider: { - 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}}";' - } + provider: getSqlStatements(SqlProviders.Postgres) } }); @@ -182,10 +197,17 @@ export const SqlDatabaseInputForm = ({ )} diff --git a/frontend/src/views/SecretMainPage/components/DynamicSecretListView/EditDynamicSecretForm/EditDynamicSecretSqlProviderForm.tsx b/frontend/src/views/SecretMainPage/components/DynamicSecretListView/EditDynamicSecretForm/EditDynamicSecretSqlProviderForm.tsx index 691d73779..4b4a2088f 100644 --- a/frontend/src/views/SecretMainPage/components/DynamicSecretListView/EditDynamicSecretForm/EditDynamicSecretSqlProviderForm.tsx +++ b/frontend/src/views/SecretMainPage/components/DynamicSecretListView/EditDynamicSecretForm/EditDynamicSecretSqlProviderForm.tsx @@ -32,7 +32,7 @@ const formSchema = z.object({ password: z.string().min(1), creationStatement: z.string().min(1), revocationStatement: z.string().min(1), - renewStatement: z.string().min(1), + renewStatement: z.string().optional(), ca: z.string().optional() }) .partial(), @@ -94,7 +94,7 @@ export const EditDynamicSecretSqlProviderForm = ({ } } }); - + const updateDynamicSecret = useUpdateDynamicSecret(); const handleUpdateDynamicSecret = async ({ inputs, maxTTL, defaultTTL, newName }: TForm) => { @@ -186,11 +186,13 @@ export const EditDynamicSecretSqlProviderForm = ({ render={({ field: { value, onChange }, fieldState: { error } }) => ( )} @@ -221,7 +223,11 @@ export const EditDynamicSecretSqlProviderForm = ({ isError={Boolean(error?.message)} errorText={error?.message} > - field.onChange(parseInt(el.target.value, 10))} /> + field.onChange(parseInt(el.target.value, 10))} + /> )} />