mirror of
https://github.com/fosrl/pangolin.git
synced 2026-02-26 11:13:37 +00:00
171 lines
5.8 KiB
TypeScript
171 lines
5.8 KiB
TypeScript
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);
|
|
|
|
try {
|
|
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) {
|
|
console.log("Failed to migrate db:", e);
|
|
throw e;
|
|
}
|
|
|
|
console.log(`${version} migration complete`);
|
|
}
|