Files
pangolin/server/setup/scriptsPg/1.16.0.ts
2026-02-25 11:37:31 -08:00

180 lines
5.8 KiB
TypeScript

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`);
}