Run demo with restricted runtime database role

This commit is contained in:
Admin 2026-09-16 06:57:52 +00:00
parent dbfff06382
commit 84138917d4

View File

@ -11,10 +11,16 @@ const issuerUrl = process.env.KEYCLOAK_ISSUER;
const clientId = process.env.KEYCLOAK_CLIENT_ID; const clientId = process.env.KEYCLOAK_CLIENT_ID;
const clientSecret = process.env.KEYCLOAK_CLIENT_SECRET; const clientSecret = process.env.KEYCLOAK_CLIENT_SECRET;
const databaseUrl = process.env.DATABASE_URL; const databaseUrl = process.env.DATABASE_URL;
const tenantId = process.env.TENANT_ID || 'shared-demo-tenant'; const tenantId = process.env.TENANT_ID || 'demo-tenant';
const tenantMode = process.env.TENANT_DB_MODE || 'unknown'; const tenantMode = process.env.TENANT_DB_MODE || 'unknown';
for (const [key, value] of Object.entries({ APP_URL: appUrl, KEYCLOAK_ISSUER: issuerUrl, KEYCLOAK_CLIENT_ID: clientId, KEYCLOAK_CLIENT_SECRET: clientSecret, DATABASE_URL: databaseUrl })) { for (const [key, value] of Object.entries({
APP_URL: appUrl,
KEYCLOAK_ISSUER: issuerUrl,
KEYCLOAK_CLIENT_ID: clientId,
KEYCLOAK_CLIENT_SECRET: clientSecret,
DATABASE_URL: databaseUrl,
})) {
if (!value) throw new Error(`${key} is required`); if (!value) throw new Error(`${key} is required`);
} }
@ -27,16 +33,6 @@ async function waitForDatabase() {
for (let attempt = 1; attempt <= 45; attempt++) { for (let attempt = 1; attempt <= 45; attempt++) {
try { try {
await pool.query('SELECT 1'); await pool.query('SELECT 1');
await pool.query(`
CREATE TABLE IF NOT EXISTS shared_tenant_visits (
id BIGSERIAL PRIMARY KEY,
tenant_id TEXT NOT NULL,
subject TEXT NOT NULL,
username TEXT,
visited_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
)
`);
await pool.query('CREATE INDEX IF NOT EXISTS idx_shared_tenant_visits_tenant ON shared_tenant_visits (tenant_id)');
return; return;
} catch (error) { } catch (error) {
lastError = error; lastError = error;
@ -117,32 +113,24 @@ async function main() {
const user = req.session.user; const user = req.session.user;
const username = user.preferred_username || user.email || user.name || user.sub; const username = user.preferred_username || user.email || user.name || user.sub;
await pool.query( const db = await pool.query('SELECT current_database() AS database, current_user AS role, NOW() AS checked_at');
'INSERT INTO shared_tenant_visits (tenant_id, subject, username) VALUES ($1, $2, $3)', const info = db.rows[0];
[tenantId, user.sub, username]
);
const tenantCount = await pool.query('SELECT COUNT(*)::bigint AS count FROM shared_tenant_visits WHERE tenant_id = $1', [tenantId]);
const totalCount = await pool.query('SELECT COUNT(*)::bigint AS count FROM shared_tenant_visits');
const tenantTotal = await pool.query('SELECT COUNT(DISTINCT tenant_id)::bigint AS count FROM shared_tenant_visits');
const lastWrite = await pool.query('SELECT visited_at FROM shared_tenant_visits WHERE tenant_id = $1 ORDER BY id DESC LIMIT 1', [tenantId]);
res.type('html').send(`<!doctype html> res.type('html').send(`<!doctype html>
<html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Arrowstack Shared DB Demo</title></head> <html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Arrowstack DB Demo</title></head>
<body style="font-family:system-ui,sans-serif;max-width:820px;margin:4rem auto;padding:0 1.5rem;line-height:1.5"> <body style="font-family:system-ui,sans-serif;max-width:820px;margin:4rem auto;padding:0 1.5rem;line-height:1.5">
<h1>Authenticated + shared DB persisted ✅</h1> <h1>Authenticated + tenant database connected ✅</h1>
<p>Keycloak authenticated this request, then the app wrote a tenant-scoped row into Arrowstack's shared PostgreSQL database.</p> <p>Keycloak authenticated this request and the application connected to PostgreSQL using its restricted runtime role.</p>
<table style="border-collapse:collapse;width:100%;max-width:720px"> <table style="border-collapse:collapse;width:100%;max-width:720px">
<tr><td style="padding:.5rem;border-bottom:1px solid #ddd"><strong>User</strong></td><td style="padding:.5rem;border-bottom:1px solid #ddd">${escapeHtml(username)}</td></tr> <tr><td style="padding:.5rem;border-bottom:1px solid #ddd"><strong>User</strong></td><td style="padding:.5rem;border-bottom:1px solid #ddd">${escapeHtml(username)}</td></tr>
<tr><td style="padding:.5rem;border-bottom:1px solid #ddd"><strong>Tenant</strong></td><td style="padding:.5rem;border-bottom:1px solid #ddd">${escapeHtml(tenantId)}</td></tr> <tr><td style="padding:.5rem;border-bottom:1px solid #ddd"><strong>Tenant</strong></td><td style="padding:.5rem;border-bottom:1px solid #ddd">${escapeHtml(tenantId)}</td></tr>
<tr><td style="padding:.5rem;border-bottom:1px solid #ddd"><strong>DB mode</strong></td><td style="padding:.5rem;border-bottom:1px solid #ddd">${escapeHtml(tenantMode)}</td></tr> <tr><td style="padding:.5rem;border-bottom:1px solid #ddd"><strong>DB mode</strong></td><td style="padding:.5rem;border-bottom:1px solid #ddd">${escapeHtml(tenantMode)}</td></tr>
<tr><td style="padding:.5rem;border-bottom:1px solid #ddd"><strong>This tenant visits</strong></td><td style="padding:.5rem;border-bottom:1px solid #ddd">${escapeHtml(tenantCount.rows[0].count)}</td></tr> <tr><td style="padding:.5rem;border-bottom:1px solid #ddd"><strong>Database</strong></td><td style="padding:.5rem;border-bottom:1px solid #ddd">${escapeHtml(info.database)}</td></tr>
<tr><td style="padding:.5rem;border-bottom:1px solid #ddd"><strong>All shared rows</strong></td><td style="padding:.5rem;border-bottom:1px solid #ddd">${escapeHtml(totalCount.rows[0].count)}</td></tr> <tr><td style="padding:.5rem;border-bottom:1px solid #ddd"><strong>Runtime role</strong></td><td style="padding:.5rem;border-bottom:1px solid #ddd">${escapeHtml(info.role)}</td></tr>
<tr><td style="padding:.5rem;border-bottom:1px solid #ddd"><strong>Distinct tenants in table</strong></td><td style="padding:.5rem;border-bottom:1px solid #ddd">${escapeHtml(tenantTotal.rows[0].count)}</td></tr> <tr><td style="padding:.5rem;border-bottom:1px solid #ddd"><strong>DB check</strong></td><td style="padding:.5rem;border-bottom:1px solid #ddd">${escapeHtml(info.checked_at)}</td></tr>
<tr><td style="padding:.5rem;border-bottom:1px solid #ddd"><strong>Last write</strong></td><td style="padding:.5rem;border-bottom:1px solid #ddd">${escapeHtml(lastWrite.rows[0]?.visited_at || '')}</td></tr>
<tr><td style="padding:.5rem;border-bottom:1px solid #ddd"><strong>Keycloak client</strong></td><td style="padding:.5rem;border-bottom:1px solid #ddd">${escapeHtml(clientId)}</td></tr> <tr><td style="padding:.5rem;border-bottom:1px solid #ddd"><strong>Keycloak client</strong></td><td style="padding:.5rem;border-bottom:1px solid #ddd">${escapeHtml(clientId)}</td></tr>
</table> </table>
<p style="margin-top:2rem"><a href="/">Record another visit</a> · <a href="/logout">Local sign out</a></p> <p style="margin-top:2rem"><a href="/">Check again</a> · <a href="/logout">Local sign out</a></p>
</body></html>`); </body></html>`);
})); }));