diff --git a/.github/workflows/run-backend-tests.yml b/.github/workflows/run-backend-tests.yml index 0ca20311b0..e1c88a3125 100644 --- a/.github/workflows/run-backend-tests.yml +++ b/.github/workflows/run-backend-tests.yml @@ -34,6 +34,9 @@ jobs: working-directory: backend - name: Start postgres and redis run: touch .env && docker compose -f docker-compose.dev.yml up -d db redis + - name: Login to Oracle Container Registry + run: echo "${{ secrets.ORACLE_DOCKER_REGISTRY_PASSWORD }}" | docker login container-registry.oracle.com -u "${{ secrets.ORACLE_DOCKER_REGISTRY_USERNAME }}" --password-stdin + - name: Start Secret Rotation testing databases run: docker compose -f docker-compose.e2e-dbs.yml up -d - name: Run unit test diff --git a/backend/e2e-test/routes/v3/secret-rotations.spec.ts b/backend/e2e-test/routes/v3/secret-rotations.spec.ts index f43f47c024..5237510bfa 100644 --- a/backend/e2e-test/routes/v3/secret-rotations.spec.ts +++ b/backend/e2e-test/routes/v3/secret-rotations.spec.ts @@ -7,7 +7,8 @@ import { seedData1 } from "@app/db/seed-data"; enum SecretRotationType { OracleDb = "oracledb", - MySQL = "mysql" + MySQL = "mysql", + Postgres = "postgres" } type TGenericSqlCredentials = { @@ -147,6 +148,40 @@ const createMySQLAppConnection = async (credentials: TGenericSqlCredentials) => return json.appConnection.id as string; }; +const createPostgresAppConnection = async (credentials: TGenericSqlCredentials) => { + const createPostgresAppConnectionReqBody = { + credentials: { + host: credentials.host, + port: credentials.port, + database: credentials.database, + username: credentials.username, + password: credentials.password, + sslEnabled: false, + sslRejectUnauthorized: true + }, + name: `postgres-test-${uuidv4()}`, + description: "test-postgres", + gatewayId: null, + method: "username-and-password" + }; + + const res = await testServer.inject({ + method: "POST", + url: `/api/v1/app-connections/postgres`, + headers: { + authorization: `Bearer ${jwtAuthToken}` + }, + body: createPostgresAppConnectionReqBody + }); + + const json = JSON.parse(res.payload); + + expect(res.statusCode).toBe(200); + expect(json.appConnection).toBeDefined(); + + return json.appConnection.id as string; +}; + const createOracleInfisicalUsers = async ( credentials: TGenericSqlCredentials, userCredentials: TDatabaseUserCredentials[] @@ -169,7 +204,7 @@ const createOracleInfisicalUsers = async ( for await (const { username } of userCredentials) { // check if user exists, and if it does, don't create it - const existingUser = await client.raw(`SELECT * FROM all_users WHERE username = '${username.toUpperCase()}'`); + const existingUser = await client.raw(`SELECT * FROM all_users WHERE username = '${username}'`); if (!existingUser.length) { await client.raw(`CREATE USER ${username} IDENTIFIED BY "temporary_password"`); @@ -220,6 +255,36 @@ const createMySQLInfisicalUsers = async ( await client.destroy(); }; +const createPostgresInfisicalUsers = async ( + credentials: TGenericSqlCredentials, + userCredentials: TDatabaseUserCredentials[] +) => { + const client = knex({ + client: "pg", + connection: { + database: credentials.database, + port: credentials.port, + host: credentials.host, + user: credentials.username, + password: credentials.password, + connectionTimeoutMillis: 10000 + } + }); + + for await (const { username } of userCredentials) { + // check if user exists, and if it does, don't create it + const existingUser = await client.raw("SELECT * FROM pg_catalog.pg_user WHERE usename = ?", [username]); + + if (!existingUser.rows.length) { + await client.raw(`CREATE USER "${username}" WITH PASSWORD 'temporary_password'`); + } + + await client.raw("GRANT ALL PRIVILEGES ON DATABASE ?? TO ??", [credentials.database, username]); + } + + await client.destroy(); +}; + const createOracleDBSecretRotation = async ( appConnectionId: string, credentials: TGenericSqlCredentials, @@ -324,6 +389,58 @@ const createMySQLSecretRotation = async ( return res; }; +const createPostgresSecretRotation = async ( + appConnectionId: string, + credentials: TGenericSqlCredentials, + userCredentials: TDatabaseUserCredentials[], + secretMapping: TSecretMapping +) => { + const now = new Date(); + const rotationTime = new Date(now.getTime() - 2 * 60 * 1000); // 2 minutes ago + + await createPostgresInfisicalUsers(credentials, userCredentials); + + const createPostgresSecretRotationReqBody = { + parameters: userCredentials.reduce( + (acc, user, index) => { + acc[`username${index + 1}`] = user.username; + return acc; + }, + {} as Record + ), + secretsMapping: { + username: secretMapping.username, + password: secretMapping.password + }, + name: `test-postgres-rotation-${uuidv4()}`, + description: "Test Postgres Secret Rotation", + secretPath: "/", + isAutoRotationEnabled: true, + rotationInterval: 5, + rotateAtUtc: { + hours: rotationTime.getUTCHours(), + minutes: rotationTime.getUTCMinutes() + }, + connectionId: appConnectionId, + environment: seedData1.environment.slug, + projectId: seedData1.projectV3.id + }; + + const res = await testServer.inject({ + method: "POST", + url: `/api/v2/secret-rotations/postgres-credentials`, + headers: { + authorization: `Bearer ${jwtAuthToken}` + }, + body: createPostgresSecretRotationReqBody + }); + + expect(res.statusCode).toBe(200); + expect(res.json().secretRotation).toBeDefined(); + + return res; +}; + describe("Secret Rotations", async () => { const testCases = [ { @@ -349,6 +466,52 @@ describe("Secret Rotations", async () => { } ] }, + { + type: SecretRotationType.MySQL, + name: "MySQL (8.0.29) Secret Rotation", + dbCredentials: { + database: "mysql-test", + host: "127.0.0.1", + username: "root", + password: "mysql-test", + port: 3307 + }, + secretMapping: { + username: formatSqlUsername("MYSQL_USERNAME"), + password: formatSqlUsername("MYSQL_PASSWORD") + }, + userCredentials: [ + { + username: formatSqlUsername("MYSQL_USER_1") + }, + { + username: formatSqlUsername("MYSQL_USER_2") + } + ] + }, + { + type: SecretRotationType.MySQL, + name: "MySQL (5.7.31) Secret Rotation", + dbCredentials: { + database: "mysql-test", + host: "127.0.0.1", + username: "root", + password: "mysql-test", + port: 3308 + }, + secretMapping: { + username: formatSqlUsername("MYSQL_USERNAME"), + password: formatSqlUsername("MYSQL_PASSWORD") + }, + userCredentials: [ + { + username: formatSqlUsername("MYSQL_USER_1") + }, + { + username: formatSqlUsername("MYSQL_USER_2") + } + ] + }, { type: SecretRotationType.OracleDb, name: "OracleDB (23.8) Secret Rotation", @@ -371,6 +534,98 @@ describe("Secret Rotations", async () => { username: formatSqlUsername("INFISICAL_USER_2") } ] + }, + { + type: SecretRotationType.OracleDb, + name: "OracleDB (19.3) Secret Rotation", + dbCredentials: { + database: "ORCLPDB1", + host: "127.0.0.1", + username: "system", + password: "OrCAKF112aaSfAdfdA2Ac3@@!", + port: 1522 + }, + secretMapping: { + username: formatSqlUsername("ORACLEDB_USERNAME"), + password: formatSqlUsername("ORACLEDB_PASSWORD") + }, + userCredentials: [ + { + username: formatSqlUsername("INFISICAL_USER_1") + }, + { + username: formatSqlUsername("INFISICAL_USER_2") + } + ] + }, + { + type: SecretRotationType.Postgres, + name: "Postgres (17) Secret Rotation", + dbCredentials: { + database: "postgres-test", + host: "127.0.0.1", + username: "postgres-test", + password: "postgres-test", + port: 5433 + }, + secretMapping: { + username: formatSqlUsername("POSTGRES_USERNAME"), + password: formatSqlUsername("POSTGRES_PASSWORD") + }, + userCredentials: [ + { + username: formatSqlUsername("INFISICAL_USER_1") + }, + { + username: formatSqlUsername("INFISICAL_USER_2") + } + ] + }, + { + type: SecretRotationType.Postgres, + name: "Postgres (16) Secret Rotation", + dbCredentials: { + database: "postgres-test", + host: "127.0.0.1", + username: "postgres-test", + password: "postgres-test", + port: 5434 + }, + secretMapping: { + username: formatSqlUsername("POSTGRES_USERNAME"), + password: formatSqlUsername("POSTGRES_PASSWORD") + }, + userCredentials: [ + { + username: formatSqlUsername("INFISICAL_USER_1") + }, + { + username: formatSqlUsername("INFISICAL_USER_2") + } + ] + }, + { + type: SecretRotationType.Postgres, + name: "Postgres (10.12) Secret Rotation", + dbCredentials: { + database: "postgres-test", + host: "127.0.0.1", + username: "postgres-test", + password: "postgres-test", + port: 5435 + }, + secretMapping: { + username: formatSqlUsername("POSTGRES_USERNAME"), + password: formatSqlUsername("POSTGRES_PASSWORD") + }, + userCredentials: [ + { + username: formatSqlUsername("INFISICAL_USER_1") + }, + { + username: formatSqlUsername("INFISICAL_USER_2") + } + ] } ] as { type: SecretRotationType; @@ -382,12 +637,14 @@ describe("Secret Rotations", async () => { const createAppConnectionMap = { [SecretRotationType.OracleDb]: createOracleDBAppConnection, - [SecretRotationType.MySQL]: createMySQLAppConnection + [SecretRotationType.MySQL]: createMySQLAppConnection, + [SecretRotationType.Postgres]: createPostgresAppConnection }; const createRotationMap = { [SecretRotationType.OracleDb]: createOracleDBSecretRotation, - [SecretRotationType.MySQL]: createMySQLSecretRotation + [SecretRotationType.MySQL]: createMySQLSecretRotation, + [SecretRotationType.Postgres]: createPostgresSecretRotation }; const appConnectionIds: { id: string; type: SecretRotationType }[] = []; diff --git a/docker-compose.e2e-dbs.yml b/docker-compose.e2e-dbs.yml index 1e3777e10a..af74167120 100644 --- a/docker-compose.e2e-dbs.yml +++ b/docker-compose.e2e-dbs.yml @@ -1,6 +1,7 @@ version: '3.8' services: + # Oracle Databases oracle-db-23.8: image: container-registry.oracle.com/database/free:23.8.0.0 container_name: oracle-db-23.8 @@ -18,6 +19,26 @@ services: timeout: 10s retries: 5 + oracle-db-19.19: + # Official Oracle 19.19.0.0 - requires docker login container-registry.oracle.com + image: container-registry.oracle.com/database/enterprise:19.19.0.0 + container_name: oracle-db-19.19 + ports: + - "1522:1521" + environment: + - ORACLE_SID=ORCLCDB + - ORACLE_PDB=ORCLPDB1 + - ORACLE_PWD=OrCAKF112aaSfAdfdA2Ac3@@! + - ORACLE_EDITION=enterprise + - ORACLE_CHARACTERSET=AL32UTF8 + volumes: + - oracle-data-19.19:/opt/oracle/oradata + shm_size: 2gb + restart: unless-stopped + healthcheck: + disable: true + + # MySQL Databases mysql-8.4.6: image: mysql:8.4.6 container_name: mysql-8.4.6 @@ -38,6 +59,113 @@ services: timeout: 10s retries: 5 + mysql-8.0.29: + image: mysql:8.0.29 + container_name: mysql-8.0.28 + ports: + - "3307:3306" + environment: + - MYSQL_ROOT_PASSWORD=mysql-test + - MYSQL_DATABASE=mysql-test + - MYSQL_ROOT_HOST=% + - MYSQL_USER=mysql-test + - MYSQL_PASSWORD=mysql-test + volumes: + - mysql-data-8.0.29:/var/lib/mysql + restart: unless-stopped + healthcheck: + test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-u", "mysql-test", "-pmysql-test"] + interval: 30s + timeout: 10s + retries: 5 + + mysql-5.7.31: + image: mysql:5.7.31 + container_name: mysql-5.7.31 + platform: linux/amd64 + ports: + - "3308:3306" + environment: + - MYSQL_ROOT_PASSWORD=mysql-test + - MYSQL_DATABASE=mysql-test + - MYSQL_ROOT_HOST=% + - MYSQL_USER=mysql-test + - MYSQL_PASSWORD=mysql-test + volumes: + - mysql-data-5.7.31:/var/lib/mysql + restart: unless-stopped + healthcheck: + test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-u", "mysql-test", "-pmysql-test"] + interval: 30s + timeout: 10s + retries: 5 + + + + # PostgreSQL Databases + postgres-17: + image: postgres:17 + platform: linux/amd64 + container_name: postgres-17 + ports: + - "5433:5432" + environment: + - POSTGRES_DB=postgres-test + - POSTGRES_USER=postgres-test + - POSTGRES_PASSWORD=postgres-test + volumes: + - postgres-data-17:/var/lib/postgresql/data + restart: unless-stopped + healthcheck: + test: ["CMD-SHELL", "pg_isready -U postgres-test -d postgres-test"] + interval: 30s + timeout: 10s + retries: 5 + + postgres-16: + image: postgres:16 + platform: linux/amd64 + container_name: postgres-16 + ports: + - "5434:5432" + environment: + - POSTGRES_DB=postgres-test + - POSTGRES_USER=postgres-test + - POSTGRES_PASSWORD=postgres-test + volumes: + - postgres-data-16:/var/lib/postgresql/data + restart: unless-stopped + healthcheck: + test: ["CMD-SHELL", "pg_isready -U postgres-test -d postgres-test"] + interval: 30s + timeout: 10s + retries: 5 + + postgres-10.12: + image: postgres:10.12 + platform: linux/amd64 + container_name: postgres-10.12 + ports: + - "5435:5432" + environment: + - POSTGRES_DB=postgres-test + - POSTGRES_USER=postgres-test + - POSTGRES_PASSWORD=postgres-test + volumes: + - postgres-data-10.12:/var/lib/postgresql/data + restart: unless-stopped + healthcheck: + test: ["CMD-SHELL", "pg_isready -U postgres-test -d postgres-test"] + interval: 30s + timeout: 10s + retries: 5 + volumes: oracle-data-23.8: - mysql-data-8.4.6: \ No newline at end of file + oracle-data-19.19: + mysql-data-8.4.6: + mysql-data-8.0.29: + mysql-data-5.7.31: + postgres-data-17: + postgres-data-16: + postgres-data-10.12: \ No newline at end of file