diff --git a/Dockerfile.fips.standalone-infisical b/Dockerfile.fips.standalone-infisical index dfcb87deb..34cd3eed8 100644 --- a/Dockerfile.fips.standalone-infisical +++ b/Dockerfile.fips.standalone-infisical @@ -69,13 +69,21 @@ RUN groupadd -r -g 1001 nodejs && useradd -r -u 1001 -g nodejs non-root-user WORKDIR /app -# Required for pkcs11js +# Required for pkcs11js and ODBC RUN apt-get update && apt-get install -y \ python3 \ make \ g++ \ + unixodbc \ + unixodbc-dev \ + freetds-dev \ + freetds-bin \ + tdsodbc \ && rm -rf /var/lib/apt/lists/* +# Configure ODBC +RUN printf "[FreeTDS]\nDescription = FreeTDS Driver\nDriver = /usr/lib/x86_64-linux-gnu/odbc/libtdsodbc.so\nSetup = /usr/lib/x86_64-linux-gnu/odbc/libtdsS.so\nFileUsage = 1\n" > /etc/odbcinst.ini + COPY backend/package*.json ./ RUN npm ci --only-production @@ -91,13 +99,21 @@ ENV ChrystokiConfigurationPath=/usr/safenet/lunaclient/ WORKDIR /app -# Required for pkcs11js +# Required for pkcs11js and ODBC RUN apt-get update && apt-get install -y \ python3 \ make \ g++ \ + unixodbc \ + unixodbc-dev \ + freetds-dev \ + freetds-bin \ + tdsodbc \ && rm -rf /var/lib/apt/lists/* +# Configure ODBC +RUN printf "[FreeTDS]\nDescription = FreeTDS Driver\nDriver = /usr/lib/x86_64-linux-gnu/odbc/libtdsodbc.so\nSetup = /usr/lib/x86_64-linux-gnu/odbc/libtdsS.so\nFileUsage = 1\n" > /etc/odbcinst.ini + COPY backend/package*.json ./ RUN npm ci --only-production @@ -108,13 +124,24 @@ RUN mkdir frontend-build # Production stage FROM base AS production -# Install necessary packages +# Install necessary packages including ODBC RUN apt-get update && apt-get install -y \ ca-certificates \ curl \ git \ + python3 \ + make \ + g++ \ + unixodbc \ + unixodbc-dev \ + freetds-dev \ + freetds-bin \ + tdsodbc \ && rm -rf /var/lib/apt/lists/* +# Configure ODBC in production +RUN printf "[FreeTDS]\nDescription = FreeTDS Driver\nDriver = /usr/lib/x86_64-linux-gnu/odbc/libtdsodbc.so\nSetup = /usr/lib/x86_64-linux-gnu/odbc/libtdsS.so\nFileUsage = 1\n" > /etc/odbcinst.ini + # Install Infisical CLI RUN curl -1sLf 'https://dl.cloudsmith.io/public/infisical/infisical-cli/setup.deb.sh' | bash \ && apt-get update && apt-get install -y infisical=0.31.1 \ diff --git a/Dockerfile.standalone-infisical b/Dockerfile.standalone-infisical index 41c898b79..cd5477083 100644 --- a/Dockerfile.standalone-infisical +++ b/Dockerfile.standalone-infisical @@ -72,8 +72,16 @@ RUN addgroup --system --gid 1001 nodejs \ WORKDIR /app -# Required for pkcs11js -RUN apk add --no-cache python3 make g++ +# Install all required dependencies for build +RUN apk --update add \ + python3 \ + make \ + g++ \ + unixodbc \ + freetds \ + unixodbc-dev \ + libc-dev \ + freetds-dev COPY backend/package*.json ./ RUN npm ci --only-production @@ -88,8 +96,19 @@ FROM base AS backend-runner WORKDIR /app -# Required for pkcs11js -RUN apk add --no-cache python3 make g++ +# Install all required dependencies for runtime +RUN apk --update add \ + python3 \ + make \ + g++ \ + unixodbc \ + freetds \ + unixodbc-dev \ + libc-dev \ + freetds-dev + +# Configure ODBC +RUN printf "[FreeTDS]\nDescription = FreeTDS Driver\nDriver = /usr/lib/libtdsodbc.so\nSetup = /usr/lib/libtdsodbc.so\nFileUsage = 1\n" > /etc/odbcinst.ini COPY backend/package*.json ./ RUN npm ci --only-production @@ -100,11 +119,32 @@ RUN mkdir frontend-build # Production stage FROM base AS production + RUN apk add --upgrade --no-cache ca-certificates RUN apk add --no-cache bash curl && curl -1sLf \ 'https://dl.cloudsmith.io/public/infisical/infisical-cli/setup.alpine.sh' | bash \ && apk add infisical=0.31.1 && apk add --no-cache git +WORKDIR / + +# Install all required runtime dependencies +RUN apk --update add \ + python3 \ + make \ + g++ \ + unixodbc \ + freetds \ + unixodbc-dev \ + libc-dev \ + freetds-dev \ + bash \ + curl \ + git + +# Configure ODBC in production +RUN printf "[FreeTDS]\nDescription = FreeTDS Driver\nDriver = /usr/lib/libtdsodbc.so\nSetup = /usr/lib/libtdsodbc.so\nFileUsage = 1\n" > /etc/odbcinst.ini + +# Setup user permissions RUN addgroup --system --gid 1001 nodejs \ && adduser --system --uid 1001 non-root-user @@ -127,7 +167,6 @@ ARG CAPTCHA_SITE_KEY ENV NEXT_PUBLIC_CAPTCHA_SITE_KEY=$CAPTCHA_SITE_KEY \ BAKED_NEXT_PUBLIC_CAPTCHA_SITE_KEY=$CAPTCHA_SITE_KEY -WORKDIR / COPY --from=backend-runner /app /backend @@ -149,4 +188,4 @@ EXPOSE 443 USER non-root-user -CMD ["./standalone-entrypoint.sh"] +CMD ["./standalone-entrypoint.sh"] \ No newline at end of file diff --git a/backend/Dockerfile b/backend/Dockerfile index 582264946..0bb358ee0 100644 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -9,6 +9,15 @@ RUN apk --update add \ make \ g++ +# install dependencies for TDS driver (required for SAP ASE dynamic secrets) +RUN apk add --no-cache \ + unixodbc \ + freetds \ + unixodbc-dev \ + libc-dev \ + freetds-dev + + COPY package*.json ./ RUN npm ci --only-production @@ -28,6 +37,17 @@ RUN apk --update add \ make \ g++ +# install dependencies for TDS driver (required for SAP ASE dynamic secrets) +RUN apk add --no-cache \ + unixodbc \ + freetds \ + unixodbc-dev \ + libc-dev \ + freetds-dev + + +RUN printf "[FreeTDS]\nDescription = FreeTDS Driver\nDriver = /usr/lib/libtdsodbc.so\nSetup = /usr/lib/libtdsodbc.so\nFileUsage = 1\n" > /etc/odbcinst.ini + RUN npm ci --only-production && npm cache clean --force COPY --from=build /app . diff --git a/backend/Dockerfile.dev b/backend/Dockerfile.dev index 97bc2c6a3..3eda2ad03 100644 --- a/backend/Dockerfile.dev +++ b/backend/Dockerfile.dev @@ -7,7 +7,7 @@ ARG SOFTHSM2_VERSION=2.5.0 ENV SOFTHSM2_VERSION=${SOFTHSM2_VERSION} \ SOFTHSM2_SOURCES=/tmp/softhsm2 -# install build dependencies including python3 +# install build dependencies including python3 (required for pkcs11js and partially TDS driver) RUN apk --update add \ alpine-sdk \ autoconf \ @@ -19,7 +19,19 @@ RUN apk --update add \ make \ g++ +# install dependencies for TDS driver (required for SAP ASE dynamic secrets) +RUN apk add --no-cache \ + unixodbc \ + freetds \ + unixodbc-dev \ + libc-dev \ + freetds-dev + + +RUN printf "[FreeTDS]\nDescription = FreeTDS Driver\nDriver = /usr/lib/libtdsodbc.so\nSetup = /usr/lib/libtdsodbc.so\nFileUsage = 1\n" > /etc/odbcinst.ini + # build and install SoftHSM2 + RUN git clone https://github.com/opendnssec/SoftHSMv2.git ${SOFTHSM2_SOURCES} WORKDIR ${SOFTHSM2_SOURCES} diff --git a/backend/package-lock.json b/backend/package-lock.json index ef7203d2c..5fba9833f 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -81,6 +81,7 @@ "mysql2": "^3.9.8", "nanoid": "^3.3.4", "nodemailer": "^6.9.9", + "odbc": "^2.4.9", "openid-client": "^5.6.5", "ora": "^7.0.1", "oracledb": "^6.4.0", @@ -17872,6 +17873,27 @@ "jsonwebtoken": "^9.0.2" } }, + "node_modules/odbc": { + "version": "2.4.9", + "resolved": "https://registry.npmjs.org/odbc/-/odbc-2.4.9.tgz", + "integrity": "sha512-sHFWOKfyj4oFYds7YBlN+fq9ZjC2J6CsCN5CNMABpKLp+NZdb8bnanb57OaoDy1VFXEOTE91S+F900J/aIPu6w==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "@mapbox/node-pre-gyp": "^1.0.5", + "async": "^3.0.1", + "node-addon-api": "^3.0.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/odbc/node_modules/node-addon-api": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-3.2.1.tgz", + "integrity": "sha512-mmcei9JghVNDYydghQmeDX8KoAm0FAiYyIcUt/N4nhyAipB17pllZQDOJD2fotxABnt4Mdz+dKTO7eftLg4d0A==", + "license": "MIT" + }, "node_modules/oidc-token-hash": { "version": "5.0.3", "resolved": "https://registry.npmjs.org/oidc-token-hash/-/oidc-token-hash-5.0.3.tgz", diff --git a/backend/package.json b/backend/package.json index 7db48d56d..24d4e0775 100644 --- a/backend/package.json +++ b/backend/package.json @@ -189,6 +189,7 @@ "mysql2": "^3.9.8", "nanoid": "^3.3.4", "nodemailer": "^6.9.9", + "odbc": "^2.4.9", "openid-client": "^5.6.5", "ora": "^7.0.1", "oracledb": "^6.4.0", diff --git a/backend/src/ee/services/dynamic-secret/providers/aws-elasticache.ts b/backend/src/ee/services/dynamic-secret/providers/aws-elasticache.ts index 46360976d..5fd218f19 100644 --- a/backend/src/ee/services/dynamic-secret/providers/aws-elasticache.ts +++ b/backend/src/ee/services/dynamic-secret/providers/aws-elasticache.ts @@ -80,7 +80,7 @@ const ElastiCacheUserManager = (credentials: TBasicAWSCredentials, region: strin } }; - const addUserToInfisicalGroup = async (userId: string) => { + const $addUserToInfisicalGroup = async (userId: string) => { // figure out if the default user is already in the group, if it is, then we shouldn't add it again const addUserToGroupCommand = new ModifyUserGroupCommand({ @@ -96,7 +96,7 @@ const ElastiCacheUserManager = (credentials: TBasicAWSCredentials, region: strin await ensureInfisicalGroupExists(clusterName); await elastiCache.send(new CreateUserCommand(creationInput)); // First create the user - await addUserToInfisicalGroup(creationInput.UserId); // Then add the user to the group. We know the group is already a part of the cluster because of ensureInfisicalGroupExists() + await $addUserToInfisicalGroup(creationInput.UserId); // Then add the user to the group. We know the group is already a part of the cluster because of ensureInfisicalGroupExists() return { userId: creationInput.UserId, diff --git a/backend/src/ee/services/dynamic-secret/providers/aws-iam.ts b/backend/src/ee/services/dynamic-secret/providers/aws-iam.ts index a1aa780f7..64ea6a02e 100644 --- a/backend/src/ee/services/dynamic-secret/providers/aws-iam.ts +++ b/backend/src/ee/services/dynamic-secret/providers/aws-iam.ts @@ -33,7 +33,7 @@ export const AwsIamProvider = (): TDynamicProviderFns => { return providerInputs; }; - const getClient = async (providerInputs: z.infer) => { + const $getClient = async (providerInputs: z.infer) => { const client = new IAMClient({ region: providerInputs.region, credentials: { @@ -47,7 +47,7 @@ export const AwsIamProvider = (): TDynamicProviderFns => { const validateConnection = async (inputs: unknown) => { const providerInputs = await validateProviderInputs(inputs); - const client = await getClient(providerInputs); + const client = await $getClient(providerInputs); const isConnected = await client.send(new GetUserCommand({})).then(() => true); return isConnected; @@ -55,7 +55,7 @@ export const AwsIamProvider = (): TDynamicProviderFns => { const create = async (inputs: unknown) => { const providerInputs = await validateProviderInputs(inputs); - const client = await getClient(providerInputs); + const client = await $getClient(providerInputs); const username = generateUsername(); const { policyArns, userGroups, policyDocument, awsPath, permissionBoundaryPolicyArn } = providerInputs; @@ -118,7 +118,7 @@ export const AwsIamProvider = (): TDynamicProviderFns => { const revoke = async (inputs: unknown, entityId: string) => { const providerInputs = await validateProviderInputs(inputs); - const client = await getClient(providerInputs); + const client = await $getClient(providerInputs); const username = entityId; diff --git a/backend/src/ee/services/dynamic-secret/providers/azure-entra-id.ts b/backend/src/ee/services/dynamic-secret/providers/azure-entra-id.ts index 88d333aad..9e876f616 100644 --- a/backend/src/ee/services/dynamic-secret/providers/azure-entra-id.ts +++ b/backend/src/ee/services/dynamic-secret/providers/azure-entra-id.ts @@ -23,7 +23,7 @@ export const AzureEntraIDProvider = (): TDynamicProviderFns & { return providerInputs; }; - const getToken = async ( + const $getToken = async ( tenantId: string, applicationId: string, clientSecret: string @@ -51,13 +51,13 @@ export const AzureEntraIDProvider = (): TDynamicProviderFns & { const validateConnection = async (inputs: unknown) => { const providerInputs = await validateProviderInputs(inputs); - const data = await getToken(providerInputs.tenantId, providerInputs.applicationId, providerInputs.clientSecret); + const data = await $getToken(providerInputs.tenantId, providerInputs.applicationId, providerInputs.clientSecret); return data.success; }; const create = async (inputs: unknown) => { const providerInputs = await validateProviderInputs(inputs); - const data = await getToken(providerInputs.tenantId, providerInputs.applicationId, providerInputs.clientSecret); + const data = await $getToken(providerInputs.tenantId, providerInputs.applicationId, providerInputs.clientSecret); if (!data.success) { throw new BadRequestError({ message: "Failed to authorize to Microsoft Entra ID" }); } @@ -93,7 +93,7 @@ export const AzureEntraIDProvider = (): TDynamicProviderFns & { }; const fetchAzureEntraIdUsers = async (tenantId: string, applicationId: string, clientSecret: string) => { - const data = await getToken(tenantId, applicationId, clientSecret); + const data = await $getToken(tenantId, applicationId, clientSecret); if (!data.success) { throw new BadRequestError({ message: "Failed to authorize to Microsoft Entra ID" }); } diff --git a/backend/src/ee/services/dynamic-secret/providers/cassandra.ts b/backend/src/ee/services/dynamic-secret/providers/cassandra.ts index 20e93e927..c030a6813 100644 --- a/backend/src/ee/services/dynamic-secret/providers/cassandra.ts +++ b/backend/src/ee/services/dynamic-secret/providers/cassandra.ts @@ -27,7 +27,7 @@ export const CassandraProvider = (): TDynamicProviderFns => { return providerInputs; }; - const getClient = async (providerInputs: z.infer) => { + const $getClient = async (providerInputs: z.infer) => { const sslOptions = providerInputs.ca ? { rejectUnauthorized: false, ca: providerInputs.ca } : undefined; const client = new cassandra.Client({ sslOptions, @@ -47,7 +47,7 @@ export const CassandraProvider = (): TDynamicProviderFns => { const validateConnection = async (inputs: unknown) => { const providerInputs = await validateProviderInputs(inputs); - const client = await getClient(providerInputs); + const client = await $getClient(providerInputs); const isConnected = await client.execute("SELECT * FROM system_schema.keyspaces").then(() => true); await client.shutdown(); @@ -56,7 +56,7 @@ export const CassandraProvider = (): TDynamicProviderFns => { const create = async (inputs: unknown, expireAt: number) => { const providerInputs = await validateProviderInputs(inputs); - const client = await getClient(providerInputs); + const client = await $getClient(providerInputs); const username = generateUsername(); const password = generatePassword(); @@ -82,7 +82,7 @@ export const CassandraProvider = (): TDynamicProviderFns => { const revoke = async (inputs: unknown, entityId: string) => { const providerInputs = await validateProviderInputs(inputs); - const client = await getClient(providerInputs); + const client = await $getClient(providerInputs); const username = entityId; const { keyspace } = providerInputs; @@ -101,7 +101,7 @@ export const CassandraProvider = (): TDynamicProviderFns => { const providerInputs = await validateProviderInputs(inputs); if (!providerInputs.renewStatement) return { entityId }; - const client = await getClient(providerInputs); + const client = await $getClient(providerInputs); const expiration = new Date(expireAt).toISOString(); const { keyspace } = providerInputs; diff --git a/backend/src/ee/services/dynamic-secret/providers/elastic-search.ts b/backend/src/ee/services/dynamic-secret/providers/elastic-search.ts index 18881834d..50ab2c694 100644 --- a/backend/src/ee/services/dynamic-secret/providers/elastic-search.ts +++ b/backend/src/ee/services/dynamic-secret/providers/elastic-search.ts @@ -24,7 +24,7 @@ export const ElasticSearchProvider = (): TDynamicProviderFns => { return providerInputs; }; - const getClient = async (providerInputs: z.infer) => { + const $getClient = async (providerInputs: z.infer) => { const connection = new ElasticSearchClient({ node: { url: new URL(`${providerInputs.host}:${providerInputs.port}`), @@ -55,7 +55,7 @@ export const ElasticSearchProvider = (): TDynamicProviderFns => { const validateConnection = async (inputs: unknown) => { const providerInputs = await validateProviderInputs(inputs); - const connection = await getClient(providerInputs); + const connection = await $getClient(providerInputs); const infoResponse = await connection .info() @@ -67,7 +67,7 @@ export const ElasticSearchProvider = (): TDynamicProviderFns => { const create = async (inputs: unknown) => { const providerInputs = await validateProviderInputs(inputs); - const connection = await getClient(providerInputs); + const connection = await $getClient(providerInputs); const username = generateUsername(); const password = generatePassword(); @@ -85,7 +85,7 @@ export const ElasticSearchProvider = (): TDynamicProviderFns => { const revoke = async (inputs: unknown, entityId: string) => { const providerInputs = await validateProviderInputs(inputs); - const connection = await getClient(providerInputs); + const connection = await $getClient(providerInputs); await connection.security.deleteUser({ username: entityId diff --git a/backend/src/ee/services/dynamic-secret/providers/index.ts b/backend/src/ee/services/dynamic-secret/providers/index.ts index e51462be6..9ed757a37 100644 --- a/backend/src/ee/services/dynamic-secret/providers/index.ts +++ b/backend/src/ee/services/dynamic-secret/providers/index.ts @@ -6,16 +6,17 @@ import { AzureEntraIDProvider } from "./azure-entra-id"; import { CassandraProvider } from "./cassandra"; import { ElasticSearchProvider } from "./elastic-search"; import { LdapProvider } from "./ldap"; -import { DynamicSecretProviders } from "./models"; +import { DynamicSecretProviders, TDynamicProviderFns } from "./models"; import { MongoAtlasProvider } from "./mongo-atlas"; import { MongoDBProvider } from "./mongo-db"; import { RabbitMqProvider } from "./rabbit-mq"; import { RedisDatabaseProvider } from "./redis"; +import { SapAseProvider } from "./sap-ase"; import { SapHanaProvider } from "./sap-hana"; import { SqlDatabaseProvider } from "./sql-database"; import { TotpProvider } from "./totp"; -export const buildDynamicSecretProviders = () => ({ +export const buildDynamicSecretProviders = (): Record => ({ [DynamicSecretProviders.SqlDatabase]: SqlDatabaseProvider(), [DynamicSecretProviders.Cassandra]: CassandraProvider(), [DynamicSecretProviders.AwsIam]: AwsIamProvider(), @@ -29,5 +30,6 @@ export const buildDynamicSecretProviders = () => ({ [DynamicSecretProviders.Ldap]: LdapProvider(), [DynamicSecretProviders.SapHana]: SapHanaProvider(), [DynamicSecretProviders.Snowflake]: SnowflakeProvider(), - [DynamicSecretProviders.Totp]: TotpProvider() + [DynamicSecretProviders.Totp]: TotpProvider(), + [DynamicSecretProviders.SapAse]: SapAseProvider() }); diff --git a/backend/src/ee/services/dynamic-secret/providers/ldap.ts b/backend/src/ee/services/dynamic-secret/providers/ldap.ts index fc1ef01d6..992c9098e 100644 --- a/backend/src/ee/services/dynamic-secret/providers/ldap.ts +++ b/backend/src/ee/services/dynamic-secret/providers/ldap.ts @@ -52,7 +52,7 @@ export const LdapProvider = (): TDynamicProviderFns => { return providerInputs; }; - const getClient = async (providerInputs: z.infer): Promise => { + const $getClient = async (providerInputs: z.infer): Promise => { return new Promise((resolve, reject) => { const client = ldapjs.createClient({ url: providerInputs.url, @@ -83,7 +83,7 @@ export const LdapProvider = (): TDynamicProviderFns => { const validateConnection = async (inputs: unknown) => { const providerInputs = await validateProviderInputs(inputs); - const client = await getClient(providerInputs); + const client = await $getClient(providerInputs); return client.connected; }; @@ -191,7 +191,7 @@ export const LdapProvider = (): TDynamicProviderFns => { const create = async (inputs: unknown) => { const providerInputs = await validateProviderInputs(inputs); - const client = await getClient(providerInputs); + const client = await $getClient(providerInputs); if (providerInputs.credentialType === LdapCredentialType.Static) { const dnMatch = providerInputs.rotationLdif.match(/^dn:\s*(.+)/m); @@ -235,7 +235,7 @@ export const LdapProvider = (): TDynamicProviderFns => { const revoke = async (inputs: unknown, entityId: string) => { const providerInputs = await validateProviderInputs(inputs); - const client = await getClient(providerInputs); + const client = await $getClient(providerInputs); if (providerInputs.credentialType === LdapCredentialType.Static) { const dnMatch = providerInputs.rotationLdif.match(/^dn:\s*(.+)/m); diff --git a/backend/src/ee/services/dynamic-secret/providers/models.ts b/backend/src/ee/services/dynamic-secret/providers/models.ts index d803aab5b..e8b9e6548 100644 --- a/backend/src/ee/services/dynamic-secret/providers/models.ts +++ b/backend/src/ee/services/dynamic-secret/providers/models.ts @@ -4,7 +4,8 @@ export enum SqlProviders { Postgres = "postgres", MySQL = "mysql2", Oracle = "oracledb", - MsSQL = "mssql" + MsSQL = "mssql", + SapAse = "sap-ase" } export enum ElasticSearchAuthTypes { @@ -118,6 +119,16 @@ export const DynamicSecretCassandraSchema = z.object({ ca: z.string().optional() }); +export const DynamicSecretSapAseSchema = z.object({ + host: z.string().trim().toLowerCase(), + port: z.number(), + database: z.string().trim(), + username: z.string().trim(), + password: z.string().trim(), + creationStatement: z.string().trim(), + revocationStatement: z.string().trim() +}); + export const DynamicSecretAwsIamSchema = z.object({ accessKey: z.string().trim().min(1), secretAccessKey: z.string().trim().min(1), @@ -274,12 +285,14 @@ export enum DynamicSecretProviders { Ldap = "ldap", SapHana = "sap-hana", Snowflake = "snowflake", - Totp = "totp" + Totp = "totp", + SapAse = "sap-ase" } export const DynamicSecretProviderSchema = z.discriminatedUnion("type", [ z.object({ type: z.literal(DynamicSecretProviders.SqlDatabase), inputs: DynamicSecretSqlDBSchema }), z.object({ type: z.literal(DynamicSecretProviders.Cassandra), inputs: DynamicSecretCassandraSchema }), + z.object({ type: z.literal(DynamicSecretProviders.SapAse), inputs: DynamicSecretSapAseSchema }), z.object({ type: z.literal(DynamicSecretProviders.AwsIam), inputs: DynamicSecretAwsIamSchema }), z.object({ type: z.literal(DynamicSecretProviders.Redis), inputs: DynamicSecretRedisDBSchema }), z.object({ type: z.literal(DynamicSecretProviders.SapHana), inputs: DynamicSecretSapHanaSchema }), diff --git a/backend/src/ee/services/dynamic-secret/providers/mongo-atlas.ts b/backend/src/ee/services/dynamic-secret/providers/mongo-atlas.ts index 69f54ce77..95d7e590f 100644 --- a/backend/src/ee/services/dynamic-secret/providers/mongo-atlas.ts +++ b/backend/src/ee/services/dynamic-secret/providers/mongo-atlas.ts @@ -22,7 +22,7 @@ export const MongoAtlasProvider = (): TDynamicProviderFns => { return providerInputs; }; - const getClient = async (providerInputs: z.infer) => { + const $getClient = async (providerInputs: z.infer) => { const client = axios.create({ baseURL: "https://cloud.mongodb.com/api/atlas", headers: { @@ -40,7 +40,7 @@ export const MongoAtlasProvider = (): TDynamicProviderFns => { const validateConnection = async (inputs: unknown) => { const providerInputs = await validateProviderInputs(inputs); - const client = await getClient(providerInputs); + const client = await $getClient(providerInputs); const isConnected = await client({ method: "GET", @@ -59,7 +59,7 @@ export const MongoAtlasProvider = (): TDynamicProviderFns => { const create = async (inputs: unknown, expireAt: number) => { const providerInputs = await validateProviderInputs(inputs); - const client = await getClient(providerInputs); + const client = await $getClient(providerInputs); const username = generateUsername(); const password = generatePassword(); @@ -87,7 +87,7 @@ export const MongoAtlasProvider = (): TDynamicProviderFns => { const revoke = async (inputs: unknown, entityId: string) => { const providerInputs = await validateProviderInputs(inputs); - const client = await getClient(providerInputs); + const client = await $getClient(providerInputs); const username = entityId; const isExisting = await client({ @@ -114,7 +114,7 @@ export const MongoAtlasProvider = (): TDynamicProviderFns => { const renew = async (inputs: unknown, entityId: string, expireAt: number) => { const providerInputs = await validateProviderInputs(inputs); - const client = await getClient(providerInputs); + const client = await $getClient(providerInputs); const username = entityId; const expiration = new Date(expireAt).toISOString(); diff --git a/backend/src/ee/services/dynamic-secret/providers/mongo-db.ts b/backend/src/ee/services/dynamic-secret/providers/mongo-db.ts index a50af88bc..5a64e0f7b 100644 --- a/backend/src/ee/services/dynamic-secret/providers/mongo-db.ts +++ b/backend/src/ee/services/dynamic-secret/providers/mongo-db.ts @@ -23,7 +23,7 @@ export const MongoDBProvider = (): TDynamicProviderFns => { return providerInputs; }; - const getClient = async (providerInputs: z.infer) => { + const $getClient = async (providerInputs: z.infer) => { const isSrv = !providerInputs.port; const uri = isSrv ? `mongodb+srv://${providerInputs.host}` @@ -42,7 +42,7 @@ export const MongoDBProvider = (): TDynamicProviderFns => { const validateConnection = async (inputs: unknown) => { const providerInputs = await validateProviderInputs(inputs); - const client = await getClient(providerInputs); + const client = await $getClient(providerInputs); const isConnected = await client .db(providerInputs.database) @@ -55,7 +55,7 @@ export const MongoDBProvider = (): TDynamicProviderFns => { const create = async (inputs: unknown) => { const providerInputs = await validateProviderInputs(inputs); - const client = await getClient(providerInputs); + const client = await $getClient(providerInputs); const username = generateUsername(); const password = generatePassword(); @@ -74,7 +74,7 @@ export const MongoDBProvider = (): TDynamicProviderFns => { const revoke = async (inputs: unknown, entityId: string) => { const providerInputs = await validateProviderInputs(inputs); - const client = await getClient(providerInputs); + const client = await $getClient(providerInputs); const username = entityId; diff --git a/backend/src/ee/services/dynamic-secret/providers/rabbit-mq.ts b/backend/src/ee/services/dynamic-secret/providers/rabbit-mq.ts index e78fa3725..9647ec6d8 100644 --- a/backend/src/ee/services/dynamic-secret/providers/rabbit-mq.ts +++ b/backend/src/ee/services/dynamic-secret/providers/rabbit-mq.ts @@ -84,7 +84,7 @@ export const RabbitMqProvider = (): TDynamicProviderFns => { return providerInputs; }; - const getClient = async (providerInputs: z.infer) => { + const $getClient = async (providerInputs: z.infer) => { const axiosInstance = axios.create({ baseURL: `${removeTrailingSlash(providerInputs.host)}:${providerInputs.port}/api`, auth: { @@ -105,7 +105,7 @@ export const RabbitMqProvider = (): TDynamicProviderFns => { const validateConnection = async (inputs: unknown) => { const providerInputs = await validateProviderInputs(inputs); - const connection = await getClient(providerInputs); + const connection = await $getClient(providerInputs); const infoResponse = await connection.get("/whoami").then(() => true); @@ -114,7 +114,7 @@ export const RabbitMqProvider = (): TDynamicProviderFns => { const create = async (inputs: unknown) => { const providerInputs = await validateProviderInputs(inputs); - const connection = await getClient(providerInputs); + const connection = await $getClient(providerInputs); const username = generateUsername(); const password = generatePassword(); @@ -134,7 +134,7 @@ export const RabbitMqProvider = (): TDynamicProviderFns => { const revoke = async (inputs: unknown, entityId: string) => { const providerInputs = await validateProviderInputs(inputs); - const connection = await getClient(providerInputs); + const connection = await $getClient(providerInputs); await deleteRabbitMqUser({ axiosInstance: connection, usernameToDelete: entityId }); diff --git a/backend/src/ee/services/dynamic-secret/providers/redis.ts b/backend/src/ee/services/dynamic-secret/providers/redis.ts index b08c5d421..92ba1d4f9 100644 --- a/backend/src/ee/services/dynamic-secret/providers/redis.ts +++ b/backend/src/ee/services/dynamic-secret/providers/redis.ts @@ -55,7 +55,7 @@ export const RedisDatabaseProvider = (): TDynamicProviderFns => { return providerInputs; }; - const getClient = async (providerInputs: z.infer) => { + const $getClient = async (providerInputs: z.infer) => { let connection: Redis | null = null; try { connection = new Redis({ @@ -92,7 +92,7 @@ export const RedisDatabaseProvider = (): TDynamicProviderFns => { const validateConnection = async (inputs: unknown) => { const providerInputs = await validateProviderInputs(inputs); - const connection = await getClient(providerInputs); + const connection = await $getClient(providerInputs); const pingResponse = await connection .ping() @@ -104,7 +104,7 @@ export const RedisDatabaseProvider = (): TDynamicProviderFns => { const create = async (inputs: unknown, expireAt: number) => { const providerInputs = await validateProviderInputs(inputs); - const connection = await getClient(providerInputs); + const connection = await $getClient(providerInputs); const username = generateUsername(); const password = generatePassword(); @@ -126,7 +126,7 @@ export const RedisDatabaseProvider = (): TDynamicProviderFns => { const revoke = async (inputs: unknown, entityId: string) => { const providerInputs = await validateProviderInputs(inputs); - const connection = await getClient(providerInputs); + const connection = await $getClient(providerInputs); const username = entityId; @@ -143,7 +143,7 @@ export const RedisDatabaseProvider = (): TDynamicProviderFns => { const providerInputs = await validateProviderInputs(inputs); if (!providerInputs.renewStatement) return { entityId }; - const connection = await getClient(providerInputs); + const connection = await $getClient(providerInputs); const username = entityId; const expiration = new Date(expireAt).toISOString(); diff --git a/backend/src/ee/services/dynamic-secret/providers/sap-ase.ts b/backend/src/ee/services/dynamic-secret/providers/sap-ase.ts new file mode 100644 index 000000000..f349d36bd --- /dev/null +++ b/backend/src/ee/services/dynamic-secret/providers/sap-ase.ts @@ -0,0 +1,145 @@ +import handlebars from "handlebars"; +import { customAlphabet } from "nanoid"; +import odbc from "odbc"; +import { z } from "zod"; + +import { BadRequestError } from "@app/lib/errors"; +import { alphaNumericNanoId } from "@app/lib/nanoid"; + +import { verifyHostInputValidity } from "../dynamic-secret-fns"; +import { DynamicSecretSapAseSchema, TDynamicProviderFns } from "./models"; + +const generatePassword = (size = 48) => { + const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; + return customAlphabet(charset, 48)(size); +}; + +const generateUsername = () => { + return alphaNumericNanoId(25); +}; + +enum SapCommands { + CreateLogin = "sp_addlogin", + DropLogin = "sp_droplogin" +} + +export const SapAseProvider = (): TDynamicProviderFns => { + const validateProviderInputs = async (inputs: unknown) => { + const providerInputs = await DynamicSecretSapAseSchema.parseAsync(inputs); + + verifyHostInputValidity(providerInputs.host); + return providerInputs; + }; + + const $getClient = async (providerInputs: z.infer, useMaster?: boolean) => { + const connectionString = + `DRIVER={FreeTDS};` + + `SERVER=${providerInputs.host};` + + `PORT=${providerInputs.port};` + + `DATABASE=${useMaster ? "master" : providerInputs.database};` + + `UID=${providerInputs.username};` + + `PWD=${providerInputs.password};` + + `TDS_VERSION=5.0`; + + const client = await odbc.connect(connectionString); + + return client; + }; + + const validateConnection = async (inputs: unknown) => { + const providerInputs = await validateProviderInputs(inputs); + const masterClient = await $getClient(providerInputs, true); + const client = await $getClient(providerInputs); + + const [resultFromMasterDatabase] = await masterClient.query<{ version: string }>("SELECT @@VERSION AS version"); + const [resultFromSelectedDatabase] = await client.query<{ version: string }>("SELECT @@VERSION AS version"); + + if (!resultFromSelectedDatabase.version) { + throw new BadRequestError({ + message: "Failed to validate SAP ASE connection, version query failed" + }); + } + + if (resultFromMasterDatabase.version !== resultFromSelectedDatabase.version) { + throw new BadRequestError({ + message: "Failed to validate SAP ASE connection (master), version mismatch" + }); + } + + return true; + }; + + const create = async (inputs: unknown) => { + const providerInputs = await validateProviderInputs(inputs); + + const username = `inf_${generateUsername()}`; + const password = `${generatePassword()}`; + + const client = await $getClient(providerInputs); + const masterClient = await $getClient(providerInputs, true); + + const creationStatement = handlebars.compile(providerInputs.creationStatement, { noEscape: true })({ + username, + password + }); + + const queries = creationStatement.trim().replace(/\n/g, "").split(";").filter(Boolean); + + for await (const query of queries) { + // If it's an adduser query, we need to first call sp_addlogin on the MASTER database. + // If not done, then the newly created user won't be able to authenticate. + await (query.startsWith(SapCommands.CreateLogin) ? masterClient : client).query(query); + } + + await masterClient.close(); + await client.close(); + + return { entityId: username, data: { DB_USERNAME: username, DB_PASSWORD: password } }; + }; + + const revoke = async (inputs: unknown, username: string) => { + const providerInputs = await validateProviderInputs(inputs); + + const revokeStatement = handlebars.compile(providerInputs.revocationStatement, { noEscape: true })({ + username + }); + + const queries = revokeStatement.trim().replace(/\n/g, "").split(";").filter(Boolean); + + const client = await $getClient(providerInputs); + const masterClient = await $getClient(providerInputs, true); + + // Get all processes for this login and kill them. If there are active connections to the database when drop login happens, it will throw an error. + const result = await masterClient.query<{ spid?: string }>(`sp_who '${username}'`); + + if (result && result.length > 0) { + for await (const row of result) { + if (row.spid) { + await masterClient.query(`KILL ${row.spid.trim()}`); + } + } + } + + for await (const query of queries) { + await (query.startsWith(SapCommands.DropLogin) ? masterClient : client).query(query); + } + + await masterClient.close(); + await client.close(); + + return { entityId: username }; + }; + + const renew = async (_: unknown, username: string) => { + // No need for renewal + return { entityId: username }; + }; + + return { + validateProviderInputs, + validateConnection, + create, + revoke, + renew + }; +}; diff --git a/backend/src/ee/services/dynamic-secret/providers/sap-hana.ts b/backend/src/ee/services/dynamic-secret/providers/sap-hana.ts index 388994fb4..ecd7ba3d3 100644 --- a/backend/src/ee/services/dynamic-secret/providers/sap-hana.ts +++ b/backend/src/ee/services/dynamic-secret/providers/sap-hana.ts @@ -32,7 +32,7 @@ export const SapHanaProvider = (): TDynamicProviderFns => { return providerInputs; }; - const getClient = async (providerInputs: z.infer) => { + const $getClient = async (providerInputs: z.infer) => { const client = hdb.createClient({ host: providerInputs.host, port: providerInputs.port, @@ -64,9 +64,9 @@ export const SapHanaProvider = (): TDynamicProviderFns => { const validateConnection = async (inputs: unknown) => { const providerInputs = await validateProviderInputs(inputs); - const client = await getClient(providerInputs); + const client = await $getClient(providerInputs); - const testResult: boolean = await new Promise((resolve, reject) => { + const testResult = await new Promise((resolve, reject) => { client.exec("SELECT 1 FROM DUMMY;", (err: any) => { if (err) { reject(); @@ -86,7 +86,7 @@ export const SapHanaProvider = (): TDynamicProviderFns => { const password = generatePassword(); const expiration = new Date(expireAt).toISOString(); - const client = await getClient(providerInputs); + const client = await $getClient(providerInputs); const creationStatement = handlebars.compile(providerInputs.creationStatement, { noEscape: true })({ username, password, @@ -114,7 +114,7 @@ export const SapHanaProvider = (): TDynamicProviderFns => { const revoke = async (inputs: unknown, username: string) => { const providerInputs = await validateProviderInputs(inputs); - const client = await getClient(providerInputs); + const client = await $getClient(providerInputs); const revokeStatement = handlebars.compile(providerInputs.revocationStatement)({ username }); const queries = revokeStatement.toString().split(";").filter(Boolean); for await (const query of queries) { @@ -139,7 +139,7 @@ export const SapHanaProvider = (): TDynamicProviderFns => { const providerInputs = await validateProviderInputs(inputs); if (!providerInputs.renewStatement) return { entityId }; - const client = await getClient(providerInputs); + const client = await $getClient(providerInputs); try { const expiration = new Date(expireAt).toISOString(); diff --git a/backend/src/ee/services/dynamic-secret/providers/snowflake.ts b/backend/src/ee/services/dynamic-secret/providers/snowflake.ts index 26e7a590a..1b4376f43 100644 --- a/backend/src/ee/services/dynamic-secret/providers/snowflake.ts +++ b/backend/src/ee/services/dynamic-secret/providers/snowflake.ts @@ -34,7 +34,7 @@ export const SnowflakeProvider = (): TDynamicProviderFns => { return providerInputs; }; - const getClient = async (providerInputs: z.infer) => { + const $getClient = async (providerInputs: z.infer) => { const client = snowflake.createConnection({ account: `${providerInputs.orgId}-${providerInputs.accountId}`, username: providerInputs.username, @@ -49,7 +49,7 @@ export const SnowflakeProvider = (): TDynamicProviderFns => { const validateConnection = async (inputs: unknown) => { const providerInputs = await validateProviderInputs(inputs); - const client = await getClient(providerInputs); + const client = await $getClient(providerInputs); let isValidConnection: boolean; @@ -72,7 +72,7 @@ export const SnowflakeProvider = (): TDynamicProviderFns => { const create = async (inputs: unknown, expireAt: number) => { const providerInputs = await validateProviderInputs(inputs); - const client = await getClient(providerInputs); + const client = await $getClient(providerInputs); const username = generateUsername(); const password = generatePassword(); @@ -107,7 +107,7 @@ export const SnowflakeProvider = (): TDynamicProviderFns => { const revoke = async (inputs: unknown, username: string) => { const providerInputs = await validateProviderInputs(inputs); - const client = await getClient(providerInputs); + const client = await $getClient(providerInputs); try { const revokeStatement = handlebars.compile(providerInputs.revocationStatement)({ username }); @@ -135,7 +135,7 @@ export const SnowflakeProvider = (): TDynamicProviderFns => { const providerInputs = await validateProviderInputs(inputs); if (!providerInputs.renewStatement) return { entityId }; - const client = await getClient(providerInputs); + const client = await $getClient(providerInputs); try { const expiration = getDaysToExpiry(new Date(expireAt)); 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 511017586..835761211 100644 --- a/backend/src/ee/services/dynamic-secret/providers/sql-database.ts +++ b/backend/src/ee/services/dynamic-secret/providers/sql-database.ts @@ -32,7 +32,7 @@ export const SqlDatabaseProvider = (): TDynamicProviderFns => { return providerInputs; }; - const getClient = async (providerInputs: z.infer) => { + const $getClient = async (providerInputs: z.infer) => { const ssl = providerInputs.ca ? { rejectUnauthorized: false, ca: providerInputs.ca } : undefined; const db = knex({ client: providerInputs.client, @@ -52,7 +52,7 @@ export const SqlDatabaseProvider = (): TDynamicProviderFns => { const validateConnection = async (inputs: unknown) => { const providerInputs = await validateProviderInputs(inputs); - const db = await getClient(providerInputs); + const db = await $getClient(providerInputs); // oracle needs from keyword const testStatement = providerInputs.client === SqlProviders.Oracle ? "SELECT 1 FROM DUAL" : "SELECT 1"; @@ -63,7 +63,7 @@ export const SqlDatabaseProvider = (): TDynamicProviderFns => { const create = async (inputs: unknown, expireAt: number) => { const providerInputs = await validateProviderInputs(inputs); - const db = await getClient(providerInputs); + const db = await $getClient(providerInputs); const username = generateUsername(providerInputs.client); const password = generatePassword(providerInputs.client); @@ -90,7 +90,7 @@ export const SqlDatabaseProvider = (): TDynamicProviderFns => { const revoke = async (inputs: unknown, entityId: string) => { const providerInputs = await validateProviderInputs(inputs); - const db = await getClient(providerInputs); + const db = await $getClient(providerInputs); const username = entityId; const { database } = providerInputs; @@ -112,7 +112,7 @@ export const SqlDatabaseProvider = (): TDynamicProviderFns => { const providerInputs = await validateProviderInputs(inputs); if (!providerInputs.renewStatement) return { entityId }; - const db = await getClient(providerInputs); + const db = await $getClient(providerInputs); const expiration = new Date(expireAt).toISOString(); const { database } = providerInputs; diff --git a/docs/documentation/platform/dynamic-secrets/sap-ase.mdx b/docs/documentation/platform/dynamic-secrets/sap-ase.mdx new file mode 100644 index 000000000..3b7a895fb --- /dev/null +++ b/docs/documentation/platform/dynamic-secrets/sap-ase.mdx @@ -0,0 +1,116 @@ +--- +title: "SAP ASE" +description: "Learn how to dynamically generate SAP ASE database account credentials." +--- + +The Infisical SAP ASE dynamic secret allows you to generate SAP ASE database credentials on demand. + +## Prerequisite + +- Infisical requires that you have a user in your SAP ASE instance, configured with the appropriate permissions. This user will facilitate the creation of new accounts as needed. + Ensure the user possesses privileges for creating, dropping, and granting permissions to roles for it to be able to create dynamic secrets. + The user used for authentication must have access to the `master` database. You can use the `sa` user for this purpose or create a new user with the necessary permissions. + +- The SAP ASE instance should be reachable by Infisical. + +## Set up Dynamic Secrets with SAP ASE + + + + Open the Secret Overview dashboard and select the environment in which you would like to add a dynamic secret. + + + ![Add Dynamic Secret Button](../../../images/platform/dynamic-secrets/add-dynamic-secret-button.png) + + + ![Dynamic Secret Modal](../../../images/platform/dynamic-secrets/sap-ase/dynamic-secret-sap-ase-modal.png) + + + + Name by which you want the secret to be referenced + + + + Default time-to-live for a generated secret (it is possible to modify this value when a secret is generate) + + + + The maximum time-to-live for a generated secret + + + + Your SAP ASE instance host (IP or domain) + + + + Your SAP ASE instance port. On default SAP ASE instances this is usually `5000`. + + + + The database name that you want to generate credentials for. This database must exist on the SAP ASE instance. + Please note that the user/password used for authentication must have access to this database, **and** the `master` database. + + + + Username that will be used to create dynamic secrets + + + + Password that will be used to create dynamic secrets + + + ![Dynamic Secret Setup Modal](../../../images/platform/dynamic-secrets/sap-ase/dynamic-secret-sap-ase-setup-modal.png) + + + + If you want to provide specific privileges for the generated dynamic credentials, you can modify the SQL statement to your needs. + ![Modify SQL Statements Modal](../../../images/platform/dynamic-secrets/sap-ase/dynamic-secret-sap-ase-statements.png) + + + Due to SAP ASE limitations, the attached SQL statements are not executed as a transaction. + + + + + After submitting the form, you will see a dynamic secret created in the dashboard. + + + Once you've successfully configured the dynamic secret, you're ready to generate on-demand credentials. + To do this, simply click on the 'Generate' button which appears when hovering over the dynamic secret item. + Alternatively, you can initiate the creation of a new lease by selecting 'New Lease' from the dynamic secret lease list section. + + ![Dynamic Secret](/images/platform/dynamic-secrets/dynamic-secret-generate.png) + ![Dynamic Secret](/images/platform/dynamic-secrets/dynamic-secret-lease-empty.png) + + When generating these secrets, it's important to specify a Time-to-Live (TTL) duration. This will dictate how long the credentials are valid for. + + ![Provision Lease](/images/platform/dynamic-secrets/provision-lease.png) + + + Ensure that the TTL for the lease fall within the maximum TTL defined when configuring the dynamic secret in step 4. + + + + Once you click the `Submit` button, a new secret lease will be generated and the credentials for it will be shown to you. + + ![Provision Lease](/images/platform/dynamic-secrets/lease-values.png) + + + + +## Audit or Revoke Leases + +Once you have created one or more leases, you will be able to access them by clicking on the respective dynamic secret item on the dashboard. +This will allow you see the lease details and delete the lease ahead of its expiration time. + +![Provision Lease](/images/platform/dynamic-secrets/lease-data.png) + +## Renew Leases + +To extend the life of the generated dynamic secret lease past its initial time to live, simply click on the **Renew** as illustrated below. +![Provision Lease](/images/platform/dynamic-secrets/dynamic-secret-lease-renew.png) + + + Lease renewals cannot exceed the maximum TTL set when configuring the dynamic + secret. + diff --git a/docs/images/platform/dynamic-secrets/sap-ase/dynamic-secret-sap-ase-modal.png b/docs/images/platform/dynamic-secrets/sap-ase/dynamic-secret-sap-ase-modal.png new file mode 100644 index 000000000..2d48a93a3 Binary files /dev/null and b/docs/images/platform/dynamic-secrets/sap-ase/dynamic-secret-sap-ase-modal.png differ diff --git a/docs/images/platform/dynamic-secrets/sap-ase/dynamic-secret-sap-ase-setup-modal.png b/docs/images/platform/dynamic-secrets/sap-ase/dynamic-secret-sap-ase-setup-modal.png new file mode 100644 index 000000000..ceb474770 Binary files /dev/null and b/docs/images/platform/dynamic-secrets/sap-ase/dynamic-secret-sap-ase-setup-modal.png differ diff --git a/docs/images/platform/dynamic-secrets/sap-ase/dynamic-secret-sap-ase-statements.png b/docs/images/platform/dynamic-secrets/sap-ase/dynamic-secret-sap-ase-statements.png new file mode 100644 index 000000000..9ac56f456 Binary files /dev/null and b/docs/images/platform/dynamic-secrets/sap-ase/dynamic-secret-sap-ase-statements.png differ diff --git a/docs/mint.json b/docs/mint.json index e4e487915..bd65745c4 100644 --- a/docs/mint.json +++ b/docs/mint.json @@ -188,6 +188,7 @@ "documentation/platform/dynamic-secrets/mongo-db", "documentation/platform/dynamic-secrets/azure-entra-id", "documentation/platform/dynamic-secrets/ldap", + "documentation/platform/dynamic-secrets/sap-ase", "documentation/platform/dynamic-secrets/sap-hana", "documentation/platform/dynamic-secrets/snowflake", "documentation/platform/dynamic-secrets/totp" diff --git a/frontend/src/hooks/api/dynamicSecret/types.ts b/frontend/src/hooks/api/dynamicSecret/types.ts index 35b08df32..96fc3d6c8 100644 --- a/frontend/src/hooks/api/dynamicSecret/types.ts +++ b/frontend/src/hooks/api/dynamicSecret/types.ts @@ -29,7 +29,8 @@ export enum DynamicSecretProviders { Ldap = "ldap", SapHana = "sap-hana", Snowflake = "snowflake", - Totp = "totp" + Totp = "totp", + SapAse = "sap-ase" } export enum SqlProviders { @@ -220,6 +221,18 @@ export type TDynamicSecretProvider = ca?: string | undefined; }; } + | { + type: DynamicSecretProviders.SapAse; + inputs: { + host: string; + port: number; + username: string; + database: string; + password: string; + creationStatement: string; + revocationStatement: string; + }; + } | { type: DynamicSecretProviders.Snowflake; inputs: { diff --git a/frontend/src/views/SecretMainPage/components/ActionBar/CreateDynamicSecretForm/CreateDynamicSecretForm.tsx b/frontend/src/views/SecretMainPage/components/ActionBar/CreateDynamicSecretForm/CreateDynamicSecretForm.tsx index 44a435958..66855e815 100644 --- a/frontend/src/views/SecretMainPage/components/ActionBar/CreateDynamicSecretForm/CreateDynamicSecretForm.tsx +++ b/frontend/src/views/SecretMainPage/components/ActionBar/CreateDynamicSecretForm/CreateDynamicSecretForm.tsx @@ -29,6 +29,7 @@ import { MongoAtlasInputForm } from "./MongoAtlasInputForm"; import { MongoDBDatabaseInputForm } from "./MongoDBInputForm"; import { RabbitMqInputForm } from "./RabbitMqInputForm"; import { RedisInputForm } from "./RedisInputForm"; +import { SapAseInputForm } from "./SapAseInputForm"; import { SapHanaInputForm } from "./SapHanaInputForm"; import { SqlDatabaseInputForm } from "./SqlDatabaseInputForm"; import { TotpInputForm } from "./TotpInputForm"; @@ -107,6 +108,11 @@ const DYNAMIC_SECRET_LIST = [ provider: DynamicSecretProviders.SapHana, title: "SAP HANA" }, + { + icon: , + provider: DynamicSecretProviders.SapAse, + title: "SAP ASE" + }, { icon: , provider: DynamicSecretProviders.Snowflake, @@ -393,6 +399,25 @@ export const CreateDynamicSecretForm = ({ /> )} + + {wizardStep === WizardSteps.ProviderInputs && + selectedProvider === DynamicSecretProviders.SapAse && ( + + + + )} + {wizardStep === WizardSteps.ProviderInputs && selectedProvider === DynamicSecretProviders.Snowflake && ( { + const valMs = ms(val); + if (valMs < 60 * 1000) + ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be a greater than 1min" }); + // a day + if (valMs > 24 * 60 * 60 * 1000) + ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be less than a day" }); + }), + maxTTL: z + .string() + .optional() + .superRefine((val, ctx) => { + if (!val) return; + const valMs = ms(val); + if (valMs < 60 * 1000) + ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be a greater than 1min" }); + // a day + if (valMs > 24 * 60 * 60 * 1000) + ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be less than a day" }); + }), + name: z.string().refine((val) => val.toLowerCase() === val, "Must be lowercase") +}); +type TForm = z.infer; + +type Props = { + onCompleted: () => void; + onCancel: () => void; + secretPath: string; + projectSlug: string; + environment: string; +}; + +export const SapAseInputForm = ({ + onCompleted, + onCancel, + environment, + secretPath, + projectSlug +}: Props) => { + const { + control, + formState: { isSubmitting }, + handleSubmit + } = useForm({ + resolver: zodResolver(formSchema), + defaultValues: { + provider: { + database: "master", + port: 5000, + creationStatement: `sp_addlogin '{{username}}', '{{password}}'; +sp_adduser '{{username}}', '{{username}}', null; +sp_role 'grant', 'mon_role', '{{username}}';`, + revocationStatement: `sp_dropuser '{{username}}'; +sp_droplogin '{{username}}';` + } + } + }); + + const createDynamicSecret = useCreateDynamicSecret(); + + const handleCreateDynamicSecret = async ({ name, maxTTL, provider, defaultTTL }: TForm) => { + // wait till previous request is finished + if (createDynamicSecret.isLoading) return; + try { + await createDynamicSecret.mutateAsync({ + provider: { type: DynamicSecretProviders.SapAse, inputs: provider }, + maxTTL, + name, + path: secretPath, + defaultTTL, + projectSlug, + environmentSlug: environment + }); + onCompleted(); + } catch (err) { + createNotification({ + type: "error", + text: "Failed to create dynamic secret" + }); + } + }; + + return ( +
+
+
+
+
+ ( + + + + )} + /> +
+
+ ( + } + isError={Boolean(error?.message)} + errorText={error?.message} + > + + + )} + /> +
+
+ ( + } + isError={Boolean(error?.message)} + errorText={error?.message} + > + + + )} + /> +
+
+
+
+ Configuration +
+
+
+ ( + + + + )} + /> + ( + + + + )} + /> + ( + + + + )} + /> +
+
+ ( + + + + )} + /> + ( + + + + )} + /> +
+
+ + + Modify SQL Statements + + ( + +