feat: restrict customer DB user permissions on provision
Some checks failed
Build & Release / build (push) Failing after 1m3s

This commit is contained in:
Ryan Moon
2026-04-03 06:25:51 -05:00
parent 32399a417a
commit 6e68cb83c0
5 changed files with 45 additions and 0 deletions

View File

@@ -7,6 +7,7 @@
"dependencies": { "dependencies": {
"@fastify/static": "^9.0.0", "@fastify/static": "^9.0.0",
"fastify": "^5.8.4", "fastify": "^5.8.4",
"postgres": "^3.4.8",
"zod": "^4.3.6", "zod": "^4.3.6",
}, },
"devDependencies": { "devDependencies": {
@@ -121,6 +122,8 @@
"pino-std-serializers": ["pino-std-serializers@7.1.0", "", {}, "sha512-BndPH67/JxGExRgiX1dX0w1FvZck5Wa4aal9198SrRhZjH3GxKQUKIBnYJTdj2HDN3UQAS06HlfcSbQj2OHmaw=="], "pino-std-serializers": ["pino-std-serializers@7.1.0", "", {}, "sha512-BndPH67/JxGExRgiX1dX0w1FvZck5Wa4aal9198SrRhZjH3GxKQUKIBnYJTdj2HDN3UQAS06HlfcSbQj2OHmaw=="],
"postgres": ["postgres@3.4.8", "", {}, "sha512-d+JFcLM17njZaOLkv6SCev7uoLaBtfK86vMUXhW1Z4glPWh4jozno9APvW/XKFJ3CCxVoC7OL38BqRydtu5nGg=="],
"process-warning": ["process-warning@5.0.0", "", {}, "sha512-a39t9ApHNx2L4+HBnQKqxxHNs1r7KF+Intd8Q/g1bUh6q0WIp9voPXJ/x0j+ZL45KF1pJd9+q2jLIRMfvEshkA=="], "process-warning": ["process-warning@5.0.0", "", {}, "sha512-a39t9ApHNx2L4+HBnQKqxxHNs1r7KF+Intd8Q/g1bUh6q0WIp9voPXJ/x0j+ZL45KF1pJd9+q2jLIRMfvEshkA=="],
"quick-format-unescaped": ["quick-format-unescaped@4.0.4", "", {}, "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg=="], "quick-format-unescaped": ["quick-format-unescaped@4.0.4", "", {}, "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg=="],

View File

@@ -17,6 +17,7 @@
"dependencies": { "dependencies": {
"@fastify/static": "^9.0.0", "@fastify/static": "^9.0.0",
"fastify": "^5.8.4", "fastify": "^5.8.4",
"postgres": "^3.4.8",
"zod": "^4.3.6" "zod": "^4.3.6"
} }
} }

View File

@@ -5,6 +5,7 @@ export const config = {
gitSshKey: process.env.GIT_SSH_KEY!, gitSshKey: process.env.GIT_SSH_KEY!,
gitRepoUrl: process.env.GIT_REPO_URL ?? "ssh://git@git-ssh.lunarfront.tech/ryan/lunarfront-charts.git", gitRepoUrl: process.env.GIT_REPO_URL ?? "ssh://git@git-ssh.lunarfront.tech/ryan/lunarfront-charts.git",
dbUrl: process.env.DATABASE_URL!, dbUrl: process.env.DATABASE_URL!,
doadminDbUrl: process.env.DOADMIN_DATABASE_URL!,
}; };
for (const [key, val] of Object.entries(config)) { for (const [key, val] of Object.entries(config)) {

View File

@@ -3,6 +3,7 @@ import { z } from "zod";
import { createDatabase, createDatabaseUser, deleteDatabase, deleteDatabaseUser } from "../services/do"; import { createDatabase, createDatabaseUser, deleteDatabase, deleteDatabaseUser } from "../services/do";
import { addCustomerToPool, removeCustomerFromPool } from "../services/pgbouncer"; import { addCustomerToPool, removeCustomerFromPool } from "../services/pgbouncer";
import { addCustomerChart, removeCustomerChart } from "../services/git"; import { addCustomerChart, removeCustomerChart } from "../services/git";
import { setupCustomerDatabase, teardownCustomerDatabase } from "../services/db";
const ProvisionSchema = z.object({ const ProvisionSchema = z.object({
name: z.string().min(2).max(32).regex(/^[a-z0-9-]+$/, "lowercase letters, numbers, and hyphens only"), name: z.string().min(2).max(32).regex(/^[a-z0-9-]+$/, "lowercase letters, numbers, and hyphens only"),
@@ -21,6 +22,7 @@ export async function customerRoutes(app: FastifyInstance) {
createDatabaseUser(slug), createDatabaseUser(slug),
]); ]);
await setupCustomerDatabase(slug, user.name);
await addCustomerToPool(slug, user.password); await addCustomerToPool(slug, user.password);
addCustomerChart(slug, body.appVersion); addCustomerChart(slug, body.appVersion);
@@ -35,6 +37,7 @@ export async function customerRoutes(app: FastifyInstance) {
removeCustomerChart(slug); removeCustomerChart(slug);
await removeCustomerFromPool(slug); await removeCustomerFromPool(slug);
await teardownCustomerDatabase(slug, slug);
await Promise.all([ await Promise.all([
deleteDatabase(slug), deleteDatabase(slug),
deleteDatabaseUser(slug), deleteDatabaseUser(slug),

37
src/services/db.ts Normal file
View File

@@ -0,0 +1,37 @@
import postgres from "postgres";
import { config } from "../lib/config";
// Runs setup SQL as doadmin against a specific database
export async function setupCustomerDatabase(dbName: string, username: string) {
const sql = postgres(config.doadminDbUrl.replace(/\/\w+(\?|$)/, `/${dbName}$1`), { max: 1 });
try {
// Revoke all public access, then grant only to this user
await sql.unsafe(`
REVOKE ALL ON DATABASE "${dbName}" FROM PUBLIC;
GRANT CONNECT ON DATABASE "${dbName}" TO "${username}";
GRANT ALL PRIVILEGES ON DATABASE "${dbName}" TO "${username}";
ALTER DATABASE "${dbName}" OWNER TO "${username}";
`);
// Set default privileges so any tables the app creates are accessible to itself
await sql.unsafe(`
ALTER DEFAULT PRIVILEGES FOR ROLE "${username}" IN SCHEMA public
GRANT ALL ON TABLES TO "${username}";
ALTER DEFAULT PRIVILEGES FOR ROLE "${username}" IN SCHEMA public
GRANT ALL ON SEQUENCES TO "${username}";
`);
} finally {
await sql.end();
}
}
export async function teardownCustomerDatabase(dbName: string, username: string) {
// Reassign ownership back to doadmin before dropping
const sql = postgres(config.doadminDbUrl.replace(/\/\w+(\?|$)/, `/${dbName}$1`), { max: 1 });
try {
await sql.unsafe(`REASSIGN OWNED BY "${username}" TO doadmin;`);
} finally {
await sql.end();
}
}