add sqlite migration

This commit is contained in:
miloschwartz
2026-02-25 11:24:24 -08:00
parent 2282d3ae39
commit e18c9afc2d
2 changed files with 153 additions and 10 deletions

View File

@@ -7,6 +7,7 @@ import { versionMigrations } from "../db/sqlite";
import { __DIRNAME, APP_PATH, APP_VERSION } from "@server/lib/consts";
import { SqliteError } from "better-sqlite3";
import fs from "fs";
import { build } from "@server/build";
import m1 from "./scriptsSqlite/1.0.0-beta1";
import m2 from "./scriptsSqlite/1.0.0-beta2";
import m3 from "./scriptsSqlite/1.0.0-beta3";
@@ -37,7 +38,7 @@ import m32 from "./scriptsSqlite/1.14.0";
import m33 from "./scriptsSqlite/1.15.0";
import m34 from "./scriptsSqlite/1.15.3";
import m35 from "./scriptsSqlite/1.15.4";
import { build } from "@server/build";
import m36 from "./scriptsSqlite/1.16.0";
// THIS CANNOT IMPORT ANYTHING FROM THE SERVER
// EXCEPT FOR THE DATABASE AND THE SCHEMA
@@ -73,7 +74,8 @@ const migrations = [
{ version: "1.14.0", run: m32 },
{ version: "1.15.0", run: m33 },
{ version: "1.15.3", run: m34 },
{ version: "1.15.4", run: m35 }
{ version: "1.15.4", run: m35 },
{ version: "1.16.0", run: m36 }
// Add new migrations here as they are created
] as const;

View File

@@ -1,23 +1,164 @@
import { __DIRNAME, APP_PATH } from "@server/lib/consts";
import { APP_PATH, configFilePath1, configFilePath2 } from "@server/lib/consts";
import { encrypt } from "@server/lib/crypto";
import { generateCA } from "@server/private/lib/sshCA";
import Database from "better-sqlite3";
import fs from "fs";
import path from "path";
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}...`);
const location = path.join(APP_PATH, "db", "db.sqlite");
const db = new Database(location);
// set all admin role sudo to "full"; all other roles to "none"
// all roles set hoemdir to true
// generate ca certs for all orgs?
// set authDaemonMode to "site" for all site-resources
try {
db.transaction(() => {})();
const secret = getServerSecret();
db.pragma("foreign_keys = OFF");
db.transaction(() => {
// Create roundTripMessageTracker table for tracking message round-trips
db.prepare(
`
CREATE TABLE 'roundTripMessageTracker' (
'messageId' integer PRIMARY KEY AUTOINCREMENT NOT NULL,
'clientId' text,
'messageType' text,
'sentAt' integer NOT NULL,
'receivedAt' integer,
'error' text,
'complete' integer DEFAULT 0 NOT NULL
);
`
).run();
// Org SSH CA and billing columns
db.prepare(`ALTER TABLE 'orgs' ADD 'sshCaPrivateKey' text;`).run();
db.prepare(`ALTER TABLE 'orgs' ADD 'sshCaPublicKey' text;`).run();
db.prepare(`ALTER TABLE 'orgs' ADD 'isBillingOrg' integer;`).run();
db.prepare(`ALTER TABLE 'orgs' ADD 'billingOrgId' text;`).run();
// Role SSH sudo and unix group columns
db.prepare(
`ALTER TABLE 'roles' ADD 'sshSudoMode' text DEFAULT 'none';`
).run();
db.prepare(
`ALTER TABLE 'roles' ADD 'sshSudoCommands' text DEFAULT '[]';`
).run();
db.prepare(
`ALTER TABLE 'roles' ADD 'sshCreateHomeDir' integer DEFAULT 1;`
).run();
db.prepare(
`ALTER TABLE 'roles' ADD 'sshUnixGroups' text DEFAULT '[]';`
).run();
// Site resource auth daemon columns
db.prepare(
`ALTER TABLE 'siteResources' ADD 'authDaemonPort' integer DEFAULT 22123;`
).run();
db.prepare(
`ALTER TABLE 'siteResources' ADD 'authDaemonMode' text DEFAULT 'site';`
).run();
// UserOrg PAM username for SSH
db.prepare(`ALTER TABLE 'userOrgs' ADD 'pamUsername' text;`).run();
// Set all admin role sudo to "full"; other roles keep default "none"
db.prepare(
`UPDATE 'roles' SET 'sshSudoMode' = 'full' WHERE isAdmin = 1;`
).run();
})();
db.pragma("foreign_keys = ON");
const orgRows = db.prepare("SELECT orgId FROM orgs").all() as {
orgId: string;
}[];
// Generate and store encrypted SSH CA keys for all orgs
const updateOrgCaKeys = db.prepare(
"UPDATE orgs SET sshCaPrivateKey = ?, sshCaPublicKey = ? WHERE orgId = ?"
);
const failedOrgIds: string[] = [];
for (const row of orgRows) {
try {
const ca = generateCA(`pangolin-ssh-ca-${row.orgId}`);
const encryptedPrivateKey = encrypt(ca.privateKeyPem, secret);
updateOrgCaKeys.run(
encryptedPrivateKey,
ca.publicKeyOpenSSH,
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(", ")}`
);
}
console.log(`Migrated database`);
} catch (e) {