mirror of
https://github.com/dredx/prole.git
synced 2026-09-24 14:54:34 +00:00
Bringing the long-running session-feature branch back into main in one deliberate sweep. The branch carried the cluster work that's been live for weeks (cross-cluster CNPG metrics, Grafana w/ Google OAuth, supabase oauth2-proxy, cluster recovery, pg.0.knoe.dev + per-engineer onboarding, GCS-backed CNPG backups via Workload Identity, the env-contamination guard, the Junie brief queue, the cnpg-grafana CSRF + memory-request fixes from today), while main accumulated Junie's parallel knoe-auth Phase 2 OIDC work (full provider surface: discovery, authorize, token, userinfo, JWKS, RS256 signing, code exchange, session services). Key decision: the two branches did COMPETING rebrands off the same starting point (5ba9b63, 2026-04-27): - claude branch (commit b355855, earlier): org.prole.authority.* → dev.knoe.auth.* (artifact renamed to knoe-auth.jar) - main (commit9daa94b, recent): org.prole.authority.* → dev.knoe.authority.* (kept "authority" artifact name) dev.knoe.auth wins: cluster runs from this name, the Maven artifact is already knoe-auth.jar, and the broader rename is the documented namespace direction (per ~/.claude/projects/-Users-chrisfu-dev-knoe-db/ memory/MEMORY.md). All of main's recent Phase 2 OIDC content was ported from authority/src/.../dev/knoe/authority/ into authority/src/.../dev/knoe/auth/ with package declarations rewritten. == File-level resolution summary == Textual conflicts (4): authority/pom.xml - Took our artifactId="auth" - Took our branch's removal of spring-security-kerberos-client (verified: Junie's Phase 2 OIDC code does not import it; the dep was already-dead config) docs/pipeline-phases.md - Took our branch's "Phase 1 not started" status. Main had a misplaced "✅ Complete" with a knoe-auth-Phase-1 commit ref in the autobuild Phase 1 section — different domain. docs/plans/knoe-auth-round-1.md - Took our branch's dev.knoe.auth file table (vs main's dev.knoe.authority listing). Pure rename mismatch. supabase/helm/knoe-supabase/templates/kong/config.yaml - Took our branch's onboard route + plain dashboard wiring. Main had an oauth2proxy.enabled toggle that put oauth2-proxy as a Kong upstream — but the deployed architecture (commit 25f1b2e) has oauth2-proxy in FRONT of Kong, not behind. Main's wrapper reflected an architecture that was never deployed. - Took our branch's removal of basic-auth from dashboard route (queue #15 brief still tracks the matching values.yaml / kong/deployment.yaml cleanup). Java tree reconciliation (44 file-pairs): 20 dual-path source files + 2 dual-path tests Body-identical between main's authority/ and our branch's auth/ after stripping package decls — main's commit9daa94bwas a pure rebrand. Took our branch's auth/ version for all 22. 8 main-only source files (Phase 2 OIDC), ported into auth/: web/JwksController.java web/OidcAuthorizeController.java web/OidcDiscoveryController.java web/OidcTokenController.java web/OidcUserInfoController.java session/OidcCodeService.java session/OidcTokenService.java session/SessionService.java 12 main-only test files, ported into auth/: HealthControllerTest.java enroll/EnrollValueTypesTest.java enroll/EnrollmentControllerTest.java enroll/TotpServiceTest.java kerberos/KadminClientTest.java kerberos/KerberosSpnegoResultTest.java web/LoginControllerTest.java admin/AdminControllerTest.java user/PrincipalNormalizerTest.java regression/IdentityRegressionTest.java session/OidcCodeServiceTest.java session/SessionServiceTest.java Port mechanics: read main:authority/...<file> via git show, then sed rewrite `package dev.knoe.authority` → `package dev.knoe.auth` and `import dev.knoe.authority` → `import dev.knoe.auth`. Body content unchanged. authority/src/main/java/dev/knoe/authority/ — DELETED (duplicate) authority/src/test/java/dev/knoe/authority/ — DELETED (duplicate) == Verification == - grep -rln '<<<<<<<' across .java/.md/.yaml/.yml/.sh/.xml/.tpl: clean - find authority/src -path '*/dev/knoe/authority*': empty (subtree gone) - grep 'package dev.knoe.authority' across repo: clean - bash -n install.sh deploy.sh etc/preflight_kubecontext.sh: clean - git ls-files -u | wc -l: 0 unmerged paths - helm lint supabase/helm/knoe-supabase: pre-existing failure on studioIngress.enabled undefined in values.yaml (introduced by Junie on main; unrelated to this merge — flagging as follow-up). == Followups (carried into TODO ranked queue or noted here) == - helm lint failure: studioIngress block in values.yaml is missing enable flag; templates/studio/{ingress,oauth2proxy-deployment, oauth2proxy-service}.yaml all reference studioIngress.enabled with no default. Pre-existing on main; not introduced by this merge. - The five Junie briefs filed on this branch are now reachable from main at docs/plans/junie/{02,06,07,13,15}-*.md. Junie can pick them up in any order. - knoe-auth Phase 2 OIDC source (now at dev.knoe.auth.*) is not yet deployed to the cluster. Deployment is its own task. - The branch claude/crazy-bose-fec256 stays in place (worktree at .claude/worktrees/crazy-bose-fec256 may have ongoing context for Claude Code sessions). Safe to delete once next session starts cleanly from main. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
348 lines
12 KiB
YAML
348 lines
12 KiB
YAML
# knoe-onboard: tiny static-content service that renders an engineer's
|
|
# psql onboarding details from a URL fragment.
|
|
#
|
|
# How it fits:
|
|
# etc/onboard_engineer.sh <user> <email>
|
|
# -> CREATE ROLE + temp password (24h)
|
|
# -> Builds URL: https://db.0.knoe.dev/onboard.html#user=...&pw=...&exp=...
|
|
# -> Outputs URL + QR-code rendering for chrisfu to email or screenshare
|
|
# Engineer clicks URL or scans QR
|
|
# -> oauth2-proxy `--skip-auth-route` matches /onboard.* (page itself is
|
|
# ungated; URL secrecy + 24h expiry + immediate rotation = the security
|
|
# envelope, since URL fragments don't survive an OAuth redirect anyway)
|
|
# -> Kong routes /onboard/* to this nginx pod
|
|
# -> Browser loads onboard.html; vanilla JS reads window.location.hash,
|
|
# decodes the password, displays it with [Copy] buttons + a ready-to-paste
|
|
# psql connection string. No backend calls. No state.
|
|
#
|
|
# Phase 2 (Junie's queue): when libpq OAUTHBEARER lands, the script stops
|
|
# generating passwords; the onboard page becomes "you're already authorized
|
|
# (Google), here's your connection string with oauth_issuer=...". The
|
|
# Deployment + Service + Kong route here stay; only the HTML changes.
|
|
---
|
|
apiVersion: v1
|
|
kind: ConfigMap
|
|
metadata:
|
|
name: knoe-onboard-html
|
|
namespace: supabase
|
|
labels:
|
|
app: knoe-onboard
|
|
app.kubernetes.io/managed-by: knoe-installer
|
|
data:
|
|
onboard.html: |
|
|
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
|
|
<meta name="robots" content="noindex, nofollow, noarchive">
|
|
<title>knoe-db onboarding</title>
|
|
<style>
|
|
:root {
|
|
color-scheme: dark;
|
|
--bg: #0e1116;
|
|
--fg: #e6edf3;
|
|
--muted: #8b949e;
|
|
--accent: #58a6ff;
|
|
--warn: #f85149;
|
|
--ok: #3fb950;
|
|
--code-bg: #161b22;
|
|
--border: #30363d;
|
|
}
|
|
* { box-sizing: border-box; }
|
|
body {
|
|
margin: 0; padding: 2rem 1rem; min-height: 100vh;
|
|
background: var(--bg); color: var(--fg);
|
|
font: 16px/1.5 -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif;
|
|
}
|
|
main { max-width: 720px; margin: 0 auto; }
|
|
h1 { font-size: 1.5rem; margin: 0 0 0.5rem; }
|
|
h2 { font-size: 1.05rem; margin: 1.5rem 0 0.5rem; color: var(--muted); font-weight: 600; text-transform: uppercase; letter-spacing: 0.05em; }
|
|
p { margin: 0.5rem 0; }
|
|
a { color: var(--accent); }
|
|
code, pre { font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, monospace; font-size: 0.92em; }
|
|
pre {
|
|
background: var(--code-bg); border: 1px solid var(--border);
|
|
border-radius: 6px; padding: 0.85rem 1rem; margin: 0;
|
|
overflow-x: auto; word-break: break-all; white-space: pre-wrap;
|
|
}
|
|
.row { display: flex; gap: 0.5rem; align-items: stretch; margin: 0.5rem 0 1rem; }
|
|
.row pre { flex: 1; }
|
|
button {
|
|
background: var(--accent); color: var(--bg);
|
|
border: 0; border-radius: 6px;
|
|
padding: 0 1rem; font-weight: 600; font-size: 0.92rem;
|
|
cursor: pointer; min-width: 90px;
|
|
}
|
|
button:hover { filter: brightness(1.1); }
|
|
button.copied { background: var(--ok); }
|
|
.warn {
|
|
background: rgba(248, 81, 73, 0.1);
|
|
border-left: 3px solid var(--warn);
|
|
padding: 0.85rem 1rem; margin: 1.5rem 0; border-radius: 4px;
|
|
}
|
|
.ok {
|
|
background: rgba(63, 185, 80, 0.1);
|
|
border-left: 3px solid var(--ok);
|
|
padding: 0.85rem 1rem; margin: 1rem 0; border-radius: 4px;
|
|
}
|
|
.muted { color: var(--muted); font-size: 0.9rem; }
|
|
.expired { background: rgba(248, 81, 73, 0.15); padding: 1.5rem; border-radius: 6px; text-align: center; }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<main id="app">
|
|
<noscript>This page needs JavaScript to decode your onboarding URL.</noscript>
|
|
</main>
|
|
<script>
|
|
(function() {
|
|
var app = document.getElementById('app');
|
|
|
|
function parseFragment() {
|
|
var h = window.location.hash.replace(/^#/, '');
|
|
if (!h) return null;
|
|
var params = {};
|
|
h.split('&').forEach(function(kv) {
|
|
var i = kv.indexOf('=');
|
|
if (i < 0) return;
|
|
params[decodeURIComponent(kv.slice(0, i))] = decodeURIComponent(kv.slice(i + 1));
|
|
});
|
|
return params;
|
|
}
|
|
|
|
function renderExpired(reason) {
|
|
app.innerHTML =
|
|
'<h1>knoe-db onboarding</h1>' +
|
|
'<div class="expired">' +
|
|
'<p><strong>This onboarding link is no longer valid.</strong></p>' +
|
|
'<p class="muted">' + reason + '</p>' +
|
|
'<p>Ask chrisfu to rerun <code>etc/onboard_engineer.sh</code> for a fresh link.</p>' +
|
|
'</div>';
|
|
}
|
|
|
|
function el(tag, attrs, children) {
|
|
var e = document.createElement(tag);
|
|
if (attrs) Object.keys(attrs).forEach(function(k) {
|
|
if (k === 'class') e.className = attrs[k];
|
|
else e.setAttribute(k, attrs[k]);
|
|
});
|
|
(children || []).forEach(function(c) {
|
|
e.appendChild(typeof c === 'string' ? document.createTextNode(c) : c);
|
|
});
|
|
return e;
|
|
}
|
|
|
|
function copyButton(text) {
|
|
var btn = el('button', null, ['Copy']);
|
|
btn.addEventListener('click', function() {
|
|
navigator.clipboard.writeText(text).then(function() {
|
|
btn.textContent = 'Copied';
|
|
btn.classList.add('copied');
|
|
setTimeout(function() { btn.textContent = 'Copy'; btn.classList.remove('copied'); }, 1500);
|
|
}).catch(function() {
|
|
btn.textContent = 'Copy failed';
|
|
});
|
|
});
|
|
return btn;
|
|
}
|
|
|
|
function copyableBlock(text) {
|
|
return el('div', { class: 'row' }, [
|
|
el('pre', null, [text]),
|
|
copyButton(text)
|
|
]);
|
|
}
|
|
|
|
function render(p) {
|
|
// exp is ISO 8601 (UTC). If present and past, page expires.
|
|
if (p.exp) {
|
|
var expDate = new Date(p.exp);
|
|
if (!isNaN(expDate) && expDate < new Date()) {
|
|
renderExpired('Expired at ' + expDate.toLocaleString() + '.');
|
|
return;
|
|
}
|
|
}
|
|
if (!p.user || !p.pw) {
|
|
renderExpired('URL is missing required fields.');
|
|
return;
|
|
}
|
|
|
|
var pw = p.pw;
|
|
// Try base64 decode (script encodes pw as base64 for URL safety).
|
|
try { pw = atob(pw.replace(/-/g, '+').replace(/_/g, '/')); } catch (e) { /* leave as-is */ }
|
|
|
|
var connStr = 'psql "host=pg.0.knoe.dev port=5432 user=' + p.user +
|
|
' dbname=postgres sslmode=verify-full' +
|
|
' sslrootcert=$HOME/.knoe/knoe-db-ca.crt"';
|
|
|
|
var bootstrap =
|
|
'mkdir -p ~/.knoe && \\\n' +
|
|
'curl -sSfL https://raw.githubusercontent.com/knoey/knoe-db/main/etc/knoe-db-ca.crt -o ~/.knoe/knoe-db-ca.crt && \\\n' +
|
|
connStr;
|
|
|
|
app.innerHTML = '';
|
|
app.appendChild(el('h1', null, ['Welcome to knoe-db, ' + p.user + '.']));
|
|
app.appendChild(el('p', { class: 'muted' }, [
|
|
'This is a one-time onboarding link. The temporary password below ' +
|
|
'is valid for 24 hours; rotate it as soon as you connect.'
|
|
]));
|
|
|
|
app.appendChild(el('h2', null, ['Your temporary password (24h)']));
|
|
app.appendChild(copyableBlock(pw));
|
|
|
|
app.appendChild(el('div', { class: 'warn' }, [
|
|
el('strong', null, ['Save this to your personal 1Password vault now.']),
|
|
el('br'),
|
|
el('span', { class: 'muted' }, ['You won\'t be able to retrieve it from this page after closing the browser.'])
|
|
]));
|
|
|
|
app.appendChild(el('h2', null, ['psql connection string']));
|
|
app.appendChild(copyableBlock(connStr));
|
|
|
|
app.appendChild(el('h2', null, ['First-connect bootstrap (fetches CA cert + connects)']));
|
|
app.appendChild(copyableBlock(bootstrap));
|
|
|
|
app.appendChild(el('h2', null, ['Rotate immediately after connecting']));
|
|
app.appendChild(el('pre', null, [
|
|
connStr + '\n' +
|
|
'Password: <paste the password from above>\n\n' +
|
|
'postgres=> \\password\n' +
|
|
'Enter new password: <a strong password you generate locally>\n' +
|
|
'Enter it again: <repeat>\n' +
|
|
'postgres=> -- save the new password to your personal 1Password,\n' +
|
|
'postgres=> -- then everything from here uses your real password.\n'
|
|
]));
|
|
|
|
app.appendChild(el('div', { class: 'ok' }, [
|
|
'Phase 2 (queued): the password mechanism is being replaced by ' +
|
|
'libpq OAUTHBEARER (Google sign-in). Your role name (',
|
|
el('code', null, [p.user]),
|
|
') stays the same — only the connection string will swap from ',
|
|
el('code', null, ['-W']),
|
|
' to ',
|
|
el('code', null, ['oauth_issuer=...']),
|
|
'.'
|
|
]));
|
|
|
|
// Strip the fragment from history so reload / back-button don't re-leak.
|
|
if (history.replaceState) {
|
|
history.replaceState(null, '', window.location.pathname);
|
|
}
|
|
}
|
|
|
|
var p = parseFragment();
|
|
if (p) render(p);
|
|
else renderExpired('No onboarding details in URL.');
|
|
})();
|
|
</script>
|
|
</body>
|
|
</html>
|
|
---
|
|
apiVersion: v1
|
|
kind: Service
|
|
metadata:
|
|
name: knoe-onboard
|
|
namespace: supabase
|
|
labels:
|
|
app: knoe-onboard
|
|
app.kubernetes.io/managed-by: knoe-installer
|
|
spec:
|
|
type: ClusterIP
|
|
selector:
|
|
app: knoe-onboard
|
|
ports:
|
|
- name: http
|
|
port: 80
|
|
targetPort: 80
|
|
protocol: TCP
|
|
---
|
|
apiVersion: apps/v1
|
|
kind: Deployment
|
|
metadata:
|
|
name: knoe-onboard
|
|
namespace: supabase
|
|
labels:
|
|
app: knoe-onboard
|
|
app.kubernetes.io/managed-by: knoe-installer
|
|
spec:
|
|
replicas: 1
|
|
strategy:
|
|
type: RollingUpdate
|
|
rollingUpdate:
|
|
maxSurge: 1
|
|
maxUnavailable: 0
|
|
selector:
|
|
matchLabels:
|
|
app: knoe-onboard
|
|
template:
|
|
metadata:
|
|
labels:
|
|
app: knoe-onboard
|
|
spec:
|
|
securityContext:
|
|
runAsNonRoot: true
|
|
runAsUser: 101 # nginx in alpine image
|
|
runAsGroup: 101
|
|
seccompProfile:
|
|
type: RuntimeDefault
|
|
containers:
|
|
- name: nginx
|
|
image: nginx:1.27-alpine
|
|
imagePullPolicy: IfNotPresent
|
|
ports:
|
|
- name: http
|
|
containerPort: 80
|
|
protocol: TCP
|
|
volumeMounts:
|
|
# ConfigMap mounted directly at the nginx html root so we don't
|
|
# need a writable rootfs (readOnlyRootFilesystem: true).
|
|
- name: html
|
|
mountPath: /usr/share/nginx/html
|
|
readOnly: true
|
|
- name: nginx-cache
|
|
mountPath: /var/cache/nginx
|
|
- name: nginx-run
|
|
mountPath: /var/run
|
|
livenessProbe:
|
|
httpGet:
|
|
path: /onboard.html
|
|
port: http
|
|
initialDelaySeconds: 5
|
|
periodSeconds: 10
|
|
timeoutSeconds: 3
|
|
readinessProbe:
|
|
httpGet:
|
|
path: /onboard.html
|
|
port: http
|
|
initialDelaySeconds: 2
|
|
periodSeconds: 5
|
|
timeoutSeconds: 3
|
|
resources:
|
|
requests:
|
|
cpu: 10m
|
|
memory: 16Mi
|
|
limits:
|
|
cpu: 50m
|
|
memory: 32Mi
|
|
securityContext:
|
|
allowPrivilegeEscalation: false
|
|
capabilities:
|
|
drop: ["ALL"]
|
|
readOnlyRootFilesystem: true
|
|
volumes:
|
|
- name: html
|
|
configMap:
|
|
name: knoe-onboard-html
|
|
items:
|
|
# Two paths under the same volume: nginx serves /onboard.html
|
|
# directly, and / falls back to index.html (same content) for
|
|
# convenience if the trailing .html ever gets dropped.
|
|
- key: onboard.html
|
|
path: onboard.html
|
|
- key: onboard.html
|
|
path: index.html
|
|
- name: nginx-cache
|
|
emptyDir: {}
|
|
- name: nginx-run
|
|
emptyDir: {}
|