mirror of
https://github.com/fosrl/pangolin.git
synced 2026-02-25 22:36:38 +00:00
add pg migration
This commit is contained in:
@@ -5,6 +5,7 @@ import semver from "semver";
|
||||
import { versionMigrations } from "../db/pg";
|
||||
import { __DIRNAME, APP_VERSION } from "@server/lib/consts";
|
||||
import path from "path";
|
||||
import { build } from "@server/build";
|
||||
import m1 from "./scriptsPg/1.6.0";
|
||||
import m2 from "./scriptsPg/1.7.0";
|
||||
import m3 from "./scriptsPg/1.8.0";
|
||||
@@ -19,7 +20,7 @@ import m11 from "./scriptsPg/1.14.0";
|
||||
import m12 from "./scriptsPg/1.15.0";
|
||||
import m13 from "./scriptsPg/1.15.3";
|
||||
import m14 from "./scriptsPg/1.15.4";
|
||||
import { build } from "@server/build";
|
||||
import m15 from "./scriptsPg/1.16.0";
|
||||
|
||||
// THIS CANNOT IMPORT ANYTHING FROM THE SERVER
|
||||
// EXCEPT FOR THE DATABASE AND THE SCHEMA
|
||||
@@ -39,7 +40,8 @@ const migrations = [
|
||||
{ version: "1.14.0", run: m11 },
|
||||
{ version: "1.15.0", run: m12 },
|
||||
{ version: "1.15.3", run: m13 },
|
||||
{ version: "1.15.4", run: m14 }
|
||||
{ version: "1.15.4", run: m14 },
|
||||
{ version: "1.16.0", run: m15 }
|
||||
// Add new migrations here as they are created
|
||||
] as {
|
||||
version: string;
|
||||
|
||||
179
server/setup/scriptsPg/1.16.0.ts
Normal file
179
server/setup/scriptsPg/1.16.0.ts
Normal file
@@ -0,0 +1,179 @@
|
||||
import { db } from "@server/db/pg/driver";
|
||||
import { sql } from "drizzle-orm";
|
||||
import { configFilePath1, configFilePath2 } from "@server/lib/consts";
|
||||
import { encrypt } from "@server/lib/crypto";
|
||||
import { generateCA } from "@server/private/lib/sshCA";
|
||||
import fs from "fs";
|
||||
import yaml from "js-yaml";
|
||||
|
||||
const version = "1.16.0";
|
||||
|
||||
function getServerSecret(): string {
|
||||
const envSecret = process.env.SERVER_SECRET;
|
||||
|
||||
const configPath = fs.existsSync(configFilePath1)
|
||||
? configFilePath1
|
||||
: fs.existsSync(configFilePath2)
|
||||
? configFilePath2
|
||||
: null;
|
||||
|
||||
// If no config file but an env secret is set, use the env secret directly
|
||||
if (!configPath) {
|
||||
if (envSecret && envSecret.length > 0) {
|
||||
return envSecret;
|
||||
}
|
||||
|
||||
throw new Error(
|
||||
"Cannot generate org CA keys: no config file found and SERVER_SECRET env var is not set. " +
|
||||
"Expected config.yml or config.yaml in the config directory, or set SERVER_SECRET."
|
||||
);
|
||||
}
|
||||
|
||||
const configContent = fs.readFileSync(configPath, "utf8");
|
||||
const config = yaml.load(configContent) as {
|
||||
server?: { secret?: string };
|
||||
};
|
||||
|
||||
let secret = config?.server?.secret;
|
||||
if (!secret || secret.length === 0) {
|
||||
// Fall back to SERVER_SECRET env var if config does not contain server.secret
|
||||
if (envSecret && envSecret.length > 0) {
|
||||
secret = envSecret;
|
||||
}
|
||||
}
|
||||
|
||||
if (!secret || secret.length === 0) {
|
||||
throw new Error(
|
||||
"Cannot generate org CA keys: no server.secret in config and SERVER_SECRET env var is not set. " +
|
||||
"Set server.secret in config.yml/config.yaml or set SERVER_SECRET."
|
||||
);
|
||||
}
|
||||
|
||||
return secret;
|
||||
}
|
||||
|
||||
export default async function migration() {
|
||||
console.log(`Running setup script ${version}...`);
|
||||
|
||||
// Ensure server secret exists before running migration (required for org CA key generation)
|
||||
getServerSecret();
|
||||
|
||||
try {
|
||||
await db.execute(sql`BEGIN`);
|
||||
|
||||
// Schema changes
|
||||
await db.execute(sql`
|
||||
CREATE TABLE "roundTripMessageTracker" (
|
||||
"messageId" serial PRIMARY KEY NOT NULL,
|
||||
"clientId" varchar,
|
||||
"messageType" varchar,
|
||||
"sentAt" bigint NOT NULL,
|
||||
"receivedAt" bigint,
|
||||
"error" text,
|
||||
"complete" boolean DEFAULT false NOT NULL
|
||||
);
|
||||
`);
|
||||
|
||||
await db.execute(
|
||||
sql`ALTER TABLE "orgs" ADD COLUMN "sshCaPrivateKey" text;`
|
||||
);
|
||||
await db.execute(
|
||||
sql`ALTER TABLE "orgs" ADD COLUMN "sshCaPublicKey" text;`
|
||||
);
|
||||
await db.execute(
|
||||
sql`ALTER TABLE "orgs" ADD COLUMN "isBillingOrg" boolean;`
|
||||
);
|
||||
await db.execute(
|
||||
sql`ALTER TABLE "orgs" ADD COLUMN "billingOrgId" varchar;`
|
||||
);
|
||||
|
||||
await db.execute(
|
||||
sql`ALTER TABLE "roles" ADD COLUMN "sshSudoMode" varchar(32) DEFAULT 'none';`
|
||||
);
|
||||
await db.execute(
|
||||
sql`ALTER TABLE "roles" ADD COLUMN "sshSudoCommands" text DEFAULT '[]';`
|
||||
);
|
||||
await db.execute(
|
||||
sql`ALTER TABLE "roles" ADD COLUMN "sshCreateHomeDir" boolean DEFAULT true;`
|
||||
);
|
||||
await db.execute(
|
||||
sql`ALTER TABLE "roles" ADD COLUMN "sshUnixGroups" text DEFAULT '[]';`
|
||||
);
|
||||
|
||||
await db.execute(
|
||||
sql`ALTER TABLE "siteResources" ADD COLUMN "authDaemonPort" integer DEFAULT 22123;`
|
||||
);
|
||||
await db.execute(
|
||||
sql`ALTER TABLE "siteResources" ADD COLUMN "authDaemonMode" varchar(32) DEFAULT 'site';`
|
||||
);
|
||||
|
||||
await db.execute(
|
||||
sql`ALTER TABLE "userOrgs" ADD COLUMN "pamUsername" varchar;`
|
||||
);
|
||||
|
||||
// Set all admin role sudo to "full"; other roles keep default "none"
|
||||
await db.execute(
|
||||
sql`UPDATE "roles" SET "sshSudoMode" = 'full' WHERE "isAdmin" = true;`
|
||||
);
|
||||
|
||||
await db.execute(sql`COMMIT`);
|
||||
console.log("Migrated database");
|
||||
} catch (e) {
|
||||
await db.execute(sql`ROLLBACK`);
|
||||
console.log("Unable to migrate database");
|
||||
console.log(e);
|
||||
throw e;
|
||||
}
|
||||
|
||||
// Generate and store encrypted SSH CA keys for all orgs
|
||||
try {
|
||||
const secret = getServerSecret();
|
||||
|
||||
const orgQuery = await db.execute(sql`SELECT "orgId" FROM "orgs"`);
|
||||
const orgRows = orgQuery.rows as { orgId: string }[];
|
||||
|
||||
const failedOrgIds: string[] = [];
|
||||
|
||||
for (const row of orgRows) {
|
||||
try {
|
||||
const ca = generateCA(`pangolin-ssh-ca-${row.orgId}`);
|
||||
const encryptedPrivateKey = encrypt(ca.privateKeyPem, secret);
|
||||
|
||||
await db.execute(sql`
|
||||
UPDATE "orgs"
|
||||
SET "sshCaPrivateKey" = ${encryptedPrivateKey},
|
||||
"sshCaPublicKey" = ${ca.publicKeyOpenSSH}
|
||||
WHERE "orgId" = ${row.orgId};
|
||||
`);
|
||||
} catch (err) {
|
||||
failedOrgIds.push(row.orgId);
|
||||
console.error(
|
||||
`Error: No CA was generated for organization "${row.orgId}".`,
|
||||
err instanceof Error ? err.message : err
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (orgRows.length > 0) {
|
||||
const succeeded = orgRows.length - failedOrgIds.length;
|
||||
console.log(
|
||||
`Generated and stored SSH CA keys for ${succeeded} org(s).`
|
||||
);
|
||||
}
|
||||
|
||||
if (failedOrgIds.length > 0) {
|
||||
console.error(
|
||||
`No CA was generated for ${failedOrgIds.length} organization(s): ${failedOrgIds.join(
|
||||
", "
|
||||
)}`
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(
|
||||
"Error while generating SSH CA keys for orgs after migration:",
|
||||
e
|
||||
);
|
||||
}
|
||||
|
||||
console.log(`${version} migration complete`);
|
||||
}
|
||||
@@ -55,12 +55,13 @@ function getServerSecret(): string {
|
||||
export default async function migration() {
|
||||
console.log(`Running setup script ${version}...`);
|
||||
|
||||
// Ensure server secret exists before running migration (required for org CA key generation)
|
||||
getServerSecret();
|
||||
|
||||
const location = path.join(APP_PATH, "db", "db.sqlite");
|
||||
const db = new Database(location);
|
||||
|
||||
try {
|
||||
const secret = getServerSecret();
|
||||
|
||||
db.pragma("foreign_keys = OFF");
|
||||
|
||||
db.transaction(() => {
|
||||
@@ -123,6 +124,8 @@ export default async function migration() {
|
||||
}[];
|
||||
|
||||
// Generate and store encrypted SSH CA keys for all orgs
|
||||
const secret = getServerSecret();
|
||||
|
||||
const updateOrgCaKeys = db.prepare(
|
||||
"UPDATE orgs SET sshCaPrivateKey = ?, sshCaPublicKey = ? WHERE orgId = ?"
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user