From ba71016f872a9eb213295e504988528b880be6e9 Mon Sep 17 00:00:00 2001 From: Owen Date: Mon, 1 Jun 2026 15:18:40 -0700 Subject: [PATCH] Add inline policy migration --- server/setup/scriptsPg/1.19.0.ts | 289 +++++++++++++++++++++++++- server/setup/scriptsSqlite/1.19.0.ts | 292 ++++++++++++++++++++++++++- 2 files changed, 577 insertions(+), 4 deletions(-) diff --git a/server/setup/scriptsPg/1.19.0.ts b/server/setup/scriptsPg/1.19.0.ts index 7381a245e..4e5b67faa 100644 --- a/server/setup/scriptsPg/1.19.0.ts +++ b/server/setup/scriptsPg/1.19.0.ts @@ -1,14 +1,38 @@ import { db } from "@server/db/pg/driver"; -import { APP_PATH } from "@server/lib/consts"; +import { APP_PATH, __DIRNAME } from "@server/lib/consts"; import { sql } from "drizzle-orm"; import fs from "fs"; import yaml from "js-yaml"; -import path from "path"; +import path, { join } from "path"; import z from "zod"; import { fromZodError } from "zod-validation-error"; const version = "1.19.0"; +const dev = process.env.ENVIRONMENT !== "prod"; +let namesFile; +if (!dev) { + namesFile = join(__DIRNAME, "names.json"); +} else { + namesFile = join("server/db/names.json"); +} +export const names = JSON.parse(fs.readFileSync(namesFile, "utf-8")); + +export function generateName(): string { + const name = ( + names.descriptors[ + Math.floor(Math.random() * names.descriptors.length) + ] + + "-" + + names.animals[Math.floor(Math.random() * names.animals.length)] + ) + .toLowerCase() + .replace(/\s/g, "-"); + + // Clean out non-alphanumeric characters except dashes. + return name.replace(/[^a-z0-9-]/g, ""); +} + export default async function migration() { console.log(`Running setup script ${version}...`); @@ -275,6 +299,267 @@ export default async function migration() { throw e; } + try { + const existingResourcesQuery = await db.execute(sql` + SELECT + "resourceId", + "orgId", + "niceId", + COALESCE("sso", true) AS "sso", + COALESCE("applyRules", false) AS "applyRules", + COALESCE("emailWhitelistEnabled", false) AS "emailWhitelistEnabled", + "skipToIdpId" + FROM "resources" + `); + const existingResources = existingResourcesQuery.rows as { + resourceId: number; + orgId: string; + niceId: string; + sso: boolean; + applyRules: boolean; + emailWhitelistEnabled: boolean; + skipToIdpId: number | null; + }[]; + + if (existingResources.length > 0) { + const usedPolicyNiceIds = new Set(); + + await db.execute(sql`BEGIN`); + try { + for (const resource of existingResources) { + let policyNiceId = ""; + let loops = 0; + while (true) { + if (loops > 100) { + throw new Error( + `Could not generate a unique policy name for resource ${resource.resourceId}` + ); + } + + const candidate = generateName(); + const existingPolicy = await db.execute(sql` + SELECT 1 + FROM "resourcePolicies" + WHERE "orgId" = ${resource.orgId} + AND "niceId" = ${candidate} + LIMIT 1 + `); + + if ( + !usedPolicyNiceIds.has(candidate) && + existingPolicy.rows.length === 0 + ) { + usedPolicyNiceIds.add(candidate); + policyNiceId = candidate; + break; + } + + loops++; + } + + const policyName = `default policy for ${resource.niceId}`; + + const insertedPolicy = await db.execute(sql` + INSERT INTO "resourcePolicies" ( + "sso", + "applyRules", + "scope", + "emailWhitelistEnabled", + "niceId", + "idpId", + "name", + "orgId" + ) VALUES ( + ${resource.sso}, + ${resource.applyRules}, + 'resource', + ${resource.emailWhitelistEnabled}, + ${policyNiceId}, + ${resource.skipToIdpId}, + ${policyName}, + ${resource.orgId} + ) + RETURNING "resourcePolicyId" + `); + const resourcePolicyId = ( + insertedPolicy.rows[0] as { resourcePolicyId: number } + ).resourcePolicyId; + + await db.execute(sql` + UPDATE "resources" + SET + "defaultResourcePolicyId" = ${resourcePolicyId} + WHERE "resourceId" = ${resource.resourceId} + `); + + const existingPincodes = await db.execute(sql` + SELECT "pincodeHash", "digitLength" + FROM "resourcePincode" + WHERE "resourceId" = ${resource.resourceId} + `); + for (const pincode of existingPincodes.rows as { + pincodeHash: string; + digitLength: number; + }[]) { + await db.execute(sql` + INSERT INTO "resourcePolicyPincode" ( + "pincodeHash", + "digitLength", + "resourcePolicyId" + ) VALUES ( + ${pincode.pincodeHash}, + ${pincode.digitLength}, + ${resourcePolicyId} + ) + `); + } + + const existingPasswords = await db.execute(sql` + SELECT "passwordHash" + FROM "resourcePassword" + WHERE "resourceId" = ${resource.resourceId} + `); + for (const password of existingPasswords.rows as { + passwordHash: string; + }[]) { + await db.execute(sql` + INSERT INTO "resourcePolicyPassword" ( + "passwordHash", + "resourcePolicyId" + ) VALUES ( + ${password.passwordHash}, + ${resourcePolicyId} + ) + `); + } + + const headerCompatibilityQuery = await db.execute(sql` + SELECT COALESCE("extendedCompatibilityIsActivated", true) AS "extendedCompatibility" + FROM "resourceHeaderAuthExtendedCompatibility" + WHERE "resourceId" = ${resource.resourceId} + LIMIT 1 + `); + const extendedCompatibility = + headerCompatibilityQuery.rows.length > 0 + ? ( + headerCompatibilityQuery.rows[0] as { + extendedCompatibility: boolean; + } + ).extendedCompatibility + : true; + + const existingHeaderAuth = await db.execute(sql` + SELECT "headerAuthHash" + FROM "resourceHeaderAuth" + WHERE "resourceId" = ${resource.resourceId} + `); + for (const headerAuth of existingHeaderAuth.rows as { + headerAuthHash: string; + }[]) { + await db.execute(sql` + INSERT INTO "resourcePolicyHeaderAuth" ( + "headerAuthHash", + "extendedCompatibility", + "resourcePolicyId" + ) VALUES ( + ${headerAuth.headerAuthHash}, + ${extendedCompatibility}, + ${resourcePolicyId} + ) + `); + } + + const existingRules = await db.execute(sql` + SELECT "enabled", "priority", "action", "match", "value" + FROM "resourceRules" + WHERE "resourceId" = ${resource.resourceId} + `); + for (const rule of existingRules.rows as { + enabled: boolean; + priority: number; + action: string; + match: string; + value: string; + }[]) { + await db.execute(sql` + INSERT INTO "resourcePolicyRules" ( + "resourcePolicyId", + "enabled", + "priority", + "action", + "match", + "value" + ) VALUES ( + ${resourcePolicyId}, + ${rule.enabled}, + ${rule.priority}, + ${rule.action}, + ${rule.match}, + ${rule.value} + ) + `); + } + + const existingWhitelist = await db.execute(sql` + SELECT "email" + FROM "resourceWhitelist" + WHERE "resourceId" = ${resource.resourceId} + `); + for (const whitelistRow of existingWhitelist.rows as { + email: string; + }[]) { + await db.execute(sql` + INSERT INTO "resourcePolicyWhitelist" ( + "email", + "resourcePolicyId" + ) VALUES ( + ${whitelistRow.email}, + ${resourcePolicyId} + ) + `); + } + + await db.execute(sql` + DELETE FROM "resourcePincode" + WHERE "resourceId" = ${resource.resourceId} + `); + await db.execute(sql` + DELETE FROM "resourcePassword" + WHERE "resourceId" = ${resource.resourceId} + `); + await db.execute(sql` + DELETE FROM "resourceHeaderAuth" + WHERE "resourceId" = ${resource.resourceId} + `); + await db.execute(sql` + DELETE FROM "resourceHeaderAuthExtendedCompatibility" + WHERE "resourceId" = ${resource.resourceId} + `); + await db.execute(sql` + DELETE FROM "resourceRules" + WHERE "resourceId" = ${resource.resourceId} + `); + await db.execute(sql` + DELETE FROM "resourceWhitelist" + WHERE "resourceId" = ${resource.resourceId} + `); + } + + await db.execute(sql`COMMIT`); + console.log( + `Migrated inline resource policies for ${existingResources.length} resource(s)` + ); + } catch (e) { + await db.execute(sql`ROLLBACK`); + throw e; + } + } + } catch (e) { + console.log("Unable to migrate inline resource policies"); + console.log(e); + throw e; + } + try { const traefikPath = path.join( APP_PATH, diff --git a/server/setup/scriptsSqlite/1.19.0.ts b/server/setup/scriptsSqlite/1.19.0.ts index b1c8d347e..9ab9db256 100644 --- a/server/setup/scriptsSqlite/1.19.0.ts +++ b/server/setup/scriptsSqlite/1.19.0.ts @@ -1,13 +1,37 @@ -import { APP_PATH } from "@server/lib/consts"; +import { APP_PATH, __DIRNAME } from "@server/lib/consts"; import Database from "better-sqlite3"; import z from "zod"; import { fromZodError } from "zod-validation-error"; import fs from "fs"; import yaml from "js-yaml"; -import path from "path"; +import path, { join } from "path"; const version = "1.19.0"; +const dev = process.env.ENVIRONMENT !== "prod"; +let namesFile; +if (!dev) { + namesFile = join(__DIRNAME, "names.json"); +} else { + namesFile = join("server/db/names.json"); +} +export const names = JSON.parse(fs.readFileSync(namesFile, "utf-8")); + +export function generateName(): string { + const name = ( + names.descriptors[ + Math.floor(Math.random() * names.descriptors.length) + ] + + "-" + + names.animals[Math.floor(Math.random() * names.animals.length)] + ) + .toLowerCase() + .replace(/\s/g, "-"); + + // Clean out non-alphanumeric characters except dashes. + return name.replace(/[^a-z0-9-]/g, ""); +} + export default async function migration() { console.log(`Running setup script ${version}...`); @@ -313,6 +337,270 @@ export default async function migration() { ).run(); })(); + const existingResources = db + .prepare( + `SELECT + "resourceId", + "orgId", + "niceId", + COALESCE("sso", 1) AS "sso", + COALESCE("applyRules", 0) AS "applyRules", + COALESCE("emailWhitelistEnabled", 0) AS "emailWhitelistEnabled", + "skipToIdpId" + FROM 'resources'` + ) + .all() as { + resourceId: number; + orgId: string; + niceId: string; + sso: number; + applyRules: number; + emailWhitelistEnabled: number; + skipToIdpId: number | null; + }[]; + + if (existingResources.length > 0) { + const insertResourcePolicy = db.prepare( + `INSERT INTO 'resourcePolicies' ( + "sso", + "applyRules", + "scope", + "emailWhitelistEnabled", + "niceId", + "idpId", + "name", + "orgId" + ) VALUES (?, ?, 'resource', ?, ?, ?, ?, ?)` + ); + const updateResourcePolicyRefs = db.prepare( + `UPDATE 'resources' + "defaultResourcePolicyId" = ? + WHERE "resourceId" = ?` + ); + const policyNiceIdExists = db.prepare( + `SELECT 1 + FROM 'resourcePolicies' + WHERE "niceId" = ? AND "orgId" = ? + LIMIT 1` + ); + + const selectResourcePincodes = db.prepare( + `SELECT "pincodeHash", "digitLength" + FROM 'resourcePincode' + WHERE "resourceId" = ?` + ); + const insertResourcePolicyPincode = db.prepare( + `INSERT INTO 'resourcePolicyPincode' ( + "pincodeHash", + "digitLength", + "resourcePolicyId" + ) VALUES (?, ?, ?)` + ); + + const selectResourcePasswords = db.prepare( + `SELECT "passwordHash" + FROM 'resourcePassword' + WHERE "resourceId" = ?` + ); + const insertResourcePolicyPassword = db.prepare( + `INSERT INTO 'resourcePolicyPassword' ( + "passwordHash", + "resourcePolicyId" + ) VALUES (?, ?)` + ); + + const selectResourceHeaderAuth = db.prepare( + `SELECT "headerAuthHash" + FROM 'resourceHeaderAuth' + WHERE "resourceId" = ?` + ); + const selectResourceHeaderCompatibility = db.prepare( + `SELECT COALESCE("extendedCompatibilityIsActivated", 1) AS "extendedCompatibility" + FROM 'resourceHeaderAuthExtendedCompatibility' + WHERE "resourceId" = ? + LIMIT 1` + ); + const insertResourcePolicyHeaderAuth = db.prepare( + `INSERT INTO 'resourcePolicyHeaderAuth' ( + "headerAuthHash", + "extendedCompatibility", + "resourcePolicyId" + ) VALUES (?, ?, ?)` + ); + + const selectResourceRules = db.prepare( + `SELECT "enabled", "priority", "action", "match", "value" + FROM 'resourceRules' + WHERE "resourceId" = ?` + ); + const insertResourcePolicyRule = db.prepare( + `INSERT INTO 'resourcePolicyRules' ( + "resourcePolicyId", + "enabled", + "priority", + "action", + "match", + "value" + ) VALUES (?, ?, ?, ?, ?, ?)` + ); + + const selectResourceWhitelist = db.prepare( + `SELECT "email" + FROM 'resourceWhitelist' + WHERE "resourceId" = ?` + ); + const insertResourcePolicyWhitelist = db.prepare( + `INSERT INTO 'resourcePolicyWhitelist' ( + "email", + "resourcePolicyId" + ) VALUES (?, ?)` + ); + + const deleteResourcePincodes = db.prepare( + `DELETE FROM 'resourcePincode' WHERE "resourceId" = ?` + ); + const deleteResourcePasswords = db.prepare( + `DELETE FROM 'resourcePassword' WHERE "resourceId" = ?` + ); + const deleteResourceHeaderAuth = db.prepare( + `DELETE FROM 'resourceHeaderAuth' WHERE "resourceId" = ?` + ); + const deleteResourceHeaderCompatibility = db.prepare( + `DELETE FROM 'resourceHeaderAuthExtendedCompatibility' WHERE "resourceId" = ?` + ); + const deleteResourceRules = db.prepare( + `DELETE FROM 'resourceRules' WHERE "resourceId" = ?` + ); + const deleteResourceWhitelist = db.prepare( + `DELETE FROM 'resourceWhitelist' WHERE "resourceId" = ?` + ); + + const usedPolicyNiceIds = new Set(); + + const migrateInlinePolicies = db.transaction(() => { + for (const resource of existingResources) { + let policyNiceId = ""; + let loops = 0; + while (true) { + if (loops > 100) { + throw new Error( + `Could not generate a unique policy name for resource ${resource.resourceId}` + ); + } + + const candidate = generateName(); + const exists = policyNiceIdExists.get( + candidate, + resource.orgId + ) as { 1: number } | undefined; + if (!usedPolicyNiceIds.has(candidate) && !exists) { + usedPolicyNiceIds.add(candidate); + policyNiceId = candidate; + break; + } + + loops++; + } + + const policyName = `default policy for ${resource.niceId}`; + + const inserted = insertResourcePolicy.run( + resource.sso, + resource.applyRules, + resource.emailWhitelistEnabled, + policyNiceId, + resource.skipToIdpId, + policyName, + resource.orgId + ); + const policyId = inserted.lastInsertRowid as number; + + updateResourcePolicyRefs.run(policyId, resource.resourceId); + + const resourcePincodes = selectResourcePincodes.all( + resource.resourceId + ) as { pincodeHash: string; digitLength: number }[]; + for (const pincode of resourcePincodes) { + insertResourcePolicyPincode.run( + pincode.pincodeHash, + pincode.digitLength, + policyId + ); + } + + const resourcePasswords = selectResourcePasswords.all( + resource.resourceId + ) as { passwordHash: string }[]; + for (const password of resourcePasswords) { + insertResourcePolicyPassword.run( + password.passwordHash, + policyId + ); + } + + const compatibilityRow = + selectResourceHeaderCompatibility.get( + resource.resourceId + ) as { extendedCompatibility: number } | undefined; + const extendedCompatibility = + compatibilityRow?.extendedCompatibility ?? 1; + + const resourceHeaderAuthRows = selectResourceHeaderAuth.all( + resource.resourceId + ) as { headerAuthHash: string }[]; + for (const headerAuth of resourceHeaderAuthRows) { + insertResourcePolicyHeaderAuth.run( + headerAuth.headerAuthHash, + extendedCompatibility, + policyId + ); + } + + const resourceRules = selectResourceRules.all( + resource.resourceId + ) as { + enabled: number; + priority: number; + action: string; + match: string; + value: string; + }[]; + for (const rule of resourceRules) { + insertResourcePolicyRule.run( + policyId, + rule.enabled, + rule.priority, + rule.action, + rule.match, + rule.value + ); + } + + const resourceWhitelist = selectResourceWhitelist.all( + resource.resourceId + ) as { email: string }[]; + for (const whitelistRow of resourceWhitelist) { + insertResourcePolicyWhitelist.run( + whitelistRow.email, + policyId + ); + } + + deleteResourcePincodes.run(resource.resourceId); + deleteResourcePasswords.run(resource.resourceId); + deleteResourceHeaderAuth.run(resource.resourceId); + deleteResourceHeaderCompatibility.run(resource.resourceId); + deleteResourceRules.run(resource.resourceId); + deleteResourceWhitelist.run(resource.resourceId); + } + }); + + migrateInlinePolicies(); + console.log( + `Migrated inline resource policies for ${existingResources.length} resource(s)` + ); + } + console.log("Migrated database"); } catch (e) { console.log("Failed to migrate db:", e);