diff --git a/backend/package.json b/backend/package.json index 97e951242..a713728e9 100644 --- a/backend/package.json +++ b/backend/package.json @@ -58,6 +58,7 @@ "migration:latest": "npm run auditlog-migration:latest && knex --knexfile ./src/db/knexfile.ts --client pg migrate:latest", "migration:status": "npm run auditlog-migration:status && knex --knexfile ./src/db/knexfile.ts --client pg migrate:status", "migration:rollback": "npm run auditlog-migration:rollback && knex --knexfile ./src/db/knexfile.ts migrate:rollback", + "migrate:org": "tsx ./scripts/migrate-organization.ts", "seed:new": "tsx ./scripts/create-seed-file.ts", "seed": "knex --knexfile ./src/db/knexfile.ts --client pg seed:run", "db:reset": "npm run migration:rollback -- --all && npm run migration:latest" diff --git a/backend/scripts/migrate-organization.ts b/backend/scripts/migrate-organization.ts new file mode 100644 index 000000000..ca6aa904d --- /dev/null +++ b/backend/scripts/migrate-organization.ts @@ -0,0 +1,84 @@ +/* eslint-disable */ +import promptSync from "prompt-sync"; +import { execSync } from "child_process"; +import path from "path"; +import { existsSync } from "fs"; + +const prompt = promptSync({ + sigint: true +}); + +const exportDb = () => { + const exportHost = prompt("Enter your Postgres Host to migrate from: "); + const exportPort = prompt("Enter your Postgres Port to migrate from [Default = 5432]: ") ?? "5432"; + const exportUser = prompt("Enter your Postgres User to migrate from: [Default = infisical]: ") ?? "infisical"; + const exportPassword = prompt("Enter your Postgres Password to migrate from: "); + const exportDatabase = prompt("Enter your Postgres Database to migrate from [Default = infisical]: ") ?? "infisical"; + + // we do not include the audit_log and secret_sharing entries + execSync( + `PGDATABASE="${exportDatabase}" PGPASSWORD="${exportPassword}" PGHOST="${exportHost}" PGPORT=${exportPort} PGUSER=${exportUser} pg_dump infisical --exclude-table-data="secret_sharing" --exclude-table-data="audit_log*" > ${path.join( + __dirname, + "../src/db/dump.sql" + )}`, + { stdio: "inherit" } + ); +}; + +const importDbForOrg = () => { + const importHost = prompt("Enter your Postgres Host to migrate to: "); + const importPort = prompt("Enter your Postgres Port to migrate to [Default = 5432]: ") ?? "5432"; + const importUser = prompt("Enter your Postgres User to migrate to: [Default = infisical]: ") ?? "infisical"; + const importPassword = prompt("Enter your Postgres Password to migrate to: "); + const importDatabase = prompt("Enter your Postgres Database to migrate to [Default = infisical]: ") ?? "infisical"; + const orgId = prompt("Enter the organization ID to migrate: "); + + if (!existsSync(path.join(__dirname, "../src/db/dump.sql"))) { + console.log("File not found, please export the database first."); + return; + } + + execSync( + `PGDATABASE="${importDatabase}" PGPASSWORD="${importPassword}" PGHOST="${importHost}" PGPORT=${importPort} PGUSER=${importUser} psql -f ${path.join( + __dirname, + "../src/db/dump.sql" + )}` + ); + + execSync( + `PGDATABASE="${importDatabase}" PGPASSWORD="${importPassword}" PGHOST="${importHost}" PGPORT=${importPort} PGUSER=${importUser} psql -c "DELETE FROM public.organizations WHERE id != '${orgId}'"` + ); + + // delete global/instance-level resources not relevant to the organization to migrate + // users + execSync( + `PGDATABASE="${importDatabase}" PGPASSWORD="${importPassword}" PGHOST="${importHost}" PGPORT=${importPort} PGUSER=${importUser} psql -c 'DELETE FROM users WHERE users.id NOT IN (SELECT org_memberships."userId" FROM org_memberships)'` + ); + + // identities + execSync( + `PGDATABASE="${importDatabase}" PGPASSWORD="${importPassword}" PGHOST="${importHost}" PGPORT=${importPort} PGUSER=${importUser} psql -c 'DELETE FROM identities WHERE id NOT IN (SELECT "identityId" FROM identity_org_memberships)'` + ); + + // reset slack configuration in superAdmin + execSync( + `PGDATABASE="${importDatabase}" PGPASSWORD="${importPassword}" PGHOST="${importHost}" PGPORT=${importPort} PGUSER=${importUser} psql -c 'UPDATE super_admin SET "encryptedSlackClientId" = null, "encryptedSlackClientSecret" = null'` + ); + + console.log("Organization migrated successfully."); +}; + +const main = () => { + const action = prompt( + "Enter the action to perform\n 1. Export from existing instance.\n 2. Import org to instance.\n \n Action: " + ); + if (action === "1") { + exportDb(); + } else if (action === "2") { + importDbForOrg(); + } else { + console.log("Invalid action"); + } +}; + +main(); diff --git a/backend/src/db/migrations/20241021114650_add-missing-org-cascade-references.ts b/backend/src/db/migrations/20241021114650_add-missing-org-cascade-references.ts new file mode 100644 index 000000000..7a2cf688b --- /dev/null +++ b/backend/src/db/migrations/20241021114650_add-missing-org-cascade-references.ts @@ -0,0 +1,21 @@ +import { Knex } from "knex"; + +import { TableName } from "../schemas"; + +export async function up(knex: Knex): Promise { + if (await knex.schema.hasColumn(TableName.SamlConfig, "orgId")) { + await knex.schema.alterTable(TableName.SamlConfig, (t) => { + t.dropForeign("orgId"); + t.foreign("orgId").references("id").inTable(TableName.Organization).onDelete("CASCADE"); + }); + } +} + +export async function down(knex: Knex): Promise { + if (await knex.schema.hasColumn(TableName.SamlConfig, "orgId")) { + await knex.schema.alterTable(TableName.SamlConfig, (t) => { + t.dropForeign("orgId"); + t.foreign("orgId").references("id").inTable(TableName.Organization); + }); + } +}