Finish CSOC-WP-0003 and CSOC-WP-0004; residual intakes for Case B

Close self-registration workplan with Case A proven and Create account
entry implemented; park public registration enablement and identity
negatives as CSOC-IN-0001/0002. Complete app-shell workplan with T06
operator runbook (seed, bind, webhook, smoke) and smoke doc updates.
This commit is contained in:
tegwick 2026-08-12 10:59:25 +02:00
parent 61de4a8165
commit edfdd79208
13 changed files with 440 additions and 105 deletions

View file

@ -13,7 +13,7 @@ Chart/values/ingress live in **`railiance-apps`**
| Host | Role |
|------|------|
| **`https://app.coulomb.social`** | **Live** Railiance rebuild (identity shell now; product later) |
| **`https://app.coulomb.social`** | **Live** Railiance rebuild (app home + spaces) |
| **`https://coulomb.social`** | **Bubble.io** until data + self-registration + content ready |
| `http://127.0.0.1:8008` | Local OIDC/dev |
@ -30,7 +30,16 @@ Apex DNS cutover is **deferred**. Retire Bubble only after an explicit decision.
## Runtime secrets (names only)
K8s Secret `coulomb-social-env` (`SECRET_KEY`, `DATABASE_URL`, `USER_ENGINE_PROXY_SECRET`).
K8s Secret `coulomb-social-env` (mounted via `envFrom`).
| Key | Required | Purpose |
|-----|----------|---------|
| `SECRET_KEY` | yes | Django secret |
| `DATABASE_URL` | yes | Postgres URL |
| `USER_ENGINE_PROXY_SECRET` | yes (live UE) | trusted proxy to user-engine |
| `FORGEJO_TOKEN` | no | private Forgejo raw/API reads |
| `FORGEJO_WEBHOOK_SECRET` | no | push webhook HMAC / shared secret |
| `NETKINGDOM_REGISTRATION_URL` | no | enable landing **Create account** when NK ships |
```bash
# railiance-apps:
@ -38,7 +47,9 @@ make coulomb-social-env-secret-dry-run
make coulomb-social-env-secret
```
OIDC is a **public** client — no client secret.
OIDC is a **public** client — no client secret. Non-secret OIDC / Forgejo base
URL settings live in Helm values (`FORGEJO_BASE_URL` defaults to
`https://forgejo.coulomb.social` in app settings).
## Health
@ -58,10 +69,10 @@ docker build -t forgejo.coulomb.social/coulomb/coulomb-social:$SHA .
1. [x] Helm env / ingress use `app.coulomb.social`
2. [x] KeyCape redirects include `https://app.coulomb.social/auth/callback/`
3. [ ] DNS: `app.coulomb.social` **A → `92.205.62.239`** (prefer DNS-only for LE)
4. [ ] `certificate/coulomb-social-tls` Ready for `app.coulomb.social`
5. [ ] `curl -fsS https://app.coulomb.social/healthz`
6. [ ] Browser Sign in as tegwick → `/app/`
3. [x] DNS: `app.coulomb.social` resolves; HTTPS live (healthz 200)
4. [x] `curl -fsS https://app.coulomb.social/healthz`
5. [x] Browser Sign in as tegwick → `/app/` (Case A; see `docs/identity/smoke.md`)
6. [ ] Seed demo space + open rendered markdown (operator; steps below)
## Future apex cutover (not now)
@ -76,21 +87,135 @@ When Bubble can retire:
```bash
make test
make run # offline or local OIDC redirect
uv run manage.py seed_demo_space
# Sign in (dev login) → /app/ → Demo space → rendered markdown
```
## Space content (Forgejo)
---
See `docs/spaces-content.md`. Demo:
## Operator runbook — spaces on app.coulomb.social
Product path for **CSOC-WP-0004**. Content model: **ADR-0002**. Detail on
read/write/webhook: `docs/spaces-content.md`. Identity smoke:
`docs/identity/smoke.md`.
### A. Prerequisites
1. Image includes spaces app (`spaces` migrations + Forgejo client) and is
deployed to namespace `coulomb-social`.
2. Member can Sign in (existing LLDAP/KeyCape user, e.g. tegwick).
3. Forgejo reachable from the pod (`FORGEJO_BASE_URL`, default
`https://forgejo.coulomb.social`). Public raw needs **no** token; private
repos need `FORGEJO_TOKEN` in `coulomb-social-env`.
### B. Demo seed (fastest smoke — uses this repo as content)
In-repo fixture (already on `main`):
```text
docs/space-fixtures/demo/pages/index.md
```
Public raw check (no auth):
```bash
kubectl -n coulomb-social exec deploy/coulomb-social -- python manage.py seed_demo_space
curl -fsS \
"https://forgejo.coulomb.social/coulomb/coulomb-social/raw/branch/main/docs/space-fixtures/demo/pages/index.md" \
| head
```
Optional webhook secret (when configured in cluster env):
Seed metadata in the app DB (binds `demo` → that path):
| Key | Purpose |
|-----|---------|
| `FORGEJO_WEBHOOK_SECRET` | Push webhook cache bust |
| `FORGEJO_TOKEN` | Private repo raw/API reads |
```bash
kubectl -n coulomb-social exec deploy/coulomb-social -- \
python manage.py seed_demo_space
# optional: --slug demo --title "Demo space" --tenant tenant:coulomb
```
Webhook URL: `https://app.coulomb.social/app/spaces/hooks/forgejo/`
Browser:
1. https://app.coulomb.social/ → **Sign in** (tegwick)
2. Land on `/app/`**Spaces** lists **Demo space**
3. Open `/app/spaces/demo/` — see rendered markdown from Forgejo
4. Profile menu → **Session details** (`/account/session/`) still works
5. **Edit in Forgejo** / **View source** open the bound file; after a commit,
**Refresh content** re-fetches (or configure webhook, section D)
### C. Production-shaped space (dedicated Forgejo repo)
ADR-0002: **one repo per space**.
1. In Forgejo, create org (recommended) `coulomb-spaces` (or tenant org later).
2. Create repo e.g. `space-my-space` with branch `main` and layout:
```text
pages/
index.md # default landing page
<page-slug>.md # optional extra pages
assets/ # optional images
README.md # optional
```
3. Bind in the app (Django admin, or shell):
```bash
kubectl -n coulomb-social exec -it deploy/coulomb-social -- python manage.py shell
```
```python
from coulomb_social.apps.spaces.models import Space
Space.objects.update_or_create(
tenant_id="tenant:coulomb",
slug="my-space",
defaults={
"title": "My space",
"forgejo_owner": "coulomb-spaces",
"forgejo_repo": "space-my-space",
"default_branch": "main",
"content_root": "pages",
"is_active": True,
},
)
```
4. If the repo is private, set `FORGEJO_TOKEN` (read-only deploy token) via
`railiance-apps` env-secret tooling — **never** commit the token.
5. Open `https://app.coulomb.social/app/spaces/my-space/` as a signed-in member
of that tenant.
### D. Optional push webhook (auto cache bust)
1. Put a random secret in cluster env as `FORGEJO_WEBHOOK_SECRET`.
2. Forgejo repo → **Settings → Webhooks → Add webhook → Gitea**:
- URL: `https://app.coulomb.social/app/spaces/hooks/forgejo/`
- Method POST, content type JSON
- Secret: same value as env
- Trigger: **Push**
3. Push a commit; confirm response JSON includes `cache_entries_cleared` /
`spaces_matched`. Authors can still use **Refresh content** without a webhook.
### E. Env names quick reference
| Variable | Secret? | Where |
|----------|---------|--------|
| `FORGEJO_BASE_URL` | no | settings default / Helm |
| `FORGEJO_TOKEN` | yes | `coulomb-social-env` |
| `FORGEJO_WEBHOOK_SECRET` | yes | `coulomb-social-env` |
| `FORGEJO_TIMEOUT_SECONDS` | no | optional |
| `NETKINGDOM_REGISTRATION_URL` | no | enable Create account (residual; NK-WP-0025) |
### F. Smoke checklist (app.coulomb.social)
| # | Check | Pass criteria |
|---|--------|----------------|
| 1 | `GET /healthz` | `{"status":"ok","service":"coulomb-social"}` |
| 2 | Landing | Sign in visible; Create account only if registration URL set |
| 3 | OIDC login (known user) | tegwick → `/app/` app home (not principal dump) |
| 4 | Spaces list | empty state or seeded spaces |
| 5 | Space detail | markdown rendered from Forgejo (or clear fail-closed error) |
| 6 | Session details | profile menu → `/account/session/` shows issuer/subject/UE id |
| 7 | Sign out | session cleared; `/app/` requires login |
| 8 | (optional) Edit in Forgejo + Refresh | new commit visible without redeploy |
Identity detail and historical evidence: `docs/identity/smoke.md`.
Content mechanics: `docs/spaces-content.md`.

View file

@ -44,6 +44,10 @@ See `.env.example`. Summary:
| `USER_ENGINE_APPLICATION_ID` | `coulomb-social` | App id |
| `USER_ENGINE_EXPECTED_AUDIENCE` | `user-engine-portal` | required aud for /me |
| `FLEX_AUTH_BASE_URL` | empty (fail-closed except shell:view) | PDP |
| `FORGEJO_BASE_URL` | `https://forgejo.coulomb.social` | Space markdown source |
| `FORGEJO_TOKEN` | empty | Private repo raw reads (secret) |
| `FORGEJO_WEBHOOK_SECRET` | empty | Push webhook cache bust (secret) |
| `NETKINGDOM_REGISTRATION_URL` | empty | Landing Create account (when NK ships) |
### Platform OIDC (KeyCape)

View file

@ -1,6 +1,6 @@
# Identity smoke checklist
Evidence updated: **2026-08-09**.
Evidence updated: **2026-08-12**.
## Offline (dev claims) — **passed**
@ -13,47 +13,84 @@ uv sync && uv run manage.py migrate && make run
|------|--------|
| Open `/`**Sign in** | → `/auth/dev-login/` |
| Dev form subject `smoke-1` | 302 → `/app/` |
| Shell shows display name + subject | OK |
| App home shows Spaces + profile chrome | OK (principal dump is under Session details) |
| Profile menu → Session details | issuer / subject / user-engine id |
| **Sign out** | session cleared |
| `/app/` after logout | 302 → login |
| Second login same subject | single `Member` row (idempotent) |
| `make test` | **15 passed** |
| `make test` | green |
Automated POST probe (2026-08-09):
Automated POST probe:
```text
dev_login_post → /app/ 200 with subject smoke-1
logout → app 302 to /auth/login/?next=/app/
```
## Cluster in-cluster (port-forward) — **passed (start of OIDC)**
## Public host app.coulomb.social — **Case A passed**
DNS for `coulomb.social` still points at Cloudflare/Bubble; TLS ACME is
blocked until cutover. Smoke via:
**Strategy:** live Railiance app on **`app.coulomb.social`**; apex
**`coulomb.social`** stays Bubble until content + self-registration cutover.
| Check | Detail |
|-------|--------|
| Cluster host | `app.coulomb.social` (Helm + ingress) |
| OIDC redirect | `https://app.coulomb.social/auth/callback/` |
| Health | `curl -fsS https://app.coulomb.social/healthz` → ok |
| Landing | 200; **Sign in** present |
| Case A — known LLDAP user | **done** (tegwick, AAL1, no MFA forced) → `/app/` |
| Create account | hidden until `NETKINGDOM_REGISTRATION_URL` is set |
| Case B — public registration → OIDC → Member | **blocked** on NK mailbox verification (`NK-WP-0025` / residual intakes) |
| Apex | **do not** repoint until Bubble retirement |
### Product smoke (spaces — after deploy + seed)
Operator runbook: `docs/deploy.md` § Operator runbook. Short path:
```bash
kubectl -n coulomb-social port-forward svc/coulomb-social 18088:80
curl -H 'Host: coulomb.social' http://127.0.0.1:18088/healthz
# {"status": "ok", "service": "coulomb-social"}
# fixture raw (public)
curl -fsS \
"https://forgejo.coulomb.social/coulomb/coulomb-social/raw/branch/main/docs/space-fixtures/demo/pages/index.md" \
| head
# seed in cluster
kubectl -n coulomb-social exec deploy/coulomb-social -- \
python manage.py seed_demo_space
```
| Check | Result |
|-------|--------|
| Image | `forgejo.coulomb.social/coulomb/coulomb-social:7067145` |
| `OIDC_ENABLED` | `true` (values) |
| `GET /healthz` + Host | 200 JSON ok |
| `GET /` + Host | 200 landing shell |
| `GET /auth/login/` + Host | **302**`https://kc.coulomb.social/authorize?...` with `client_id=coulomb-social`, `redirect_uri=https://coulomb.social/auth/callback/`, PKCE S256 |
| Session cookie | `HttpOnly; Secure; SameSite=Lax` (prod settings) |
| Step | Expected |
|------|----------|
| Sign in tegwick | `/app/` Spaces list |
| Open Demo space | rendered markdown from Forgejo |
| Session details | profile menu diagnostics (no secrets) |
| Refresh content | re-fetch after git edit (or webhook) |
Full browser login against the **cluster** redirect URI requires public HTTPS
on `coulomb.social` (Secure cookie + callback host). Use **local OIDC** below
before DNS cutover, or complete browser MFA after cutover.
## Case matrix (CSOC-WP-0003-T04)
## Platform OIDC (local redirect) — **ready for human MFA**
| Case | Status |
|------|--------|
| A — known LLDAP user, first/repeat login on app.* | **done** (tegwick) |
| B — brand-new public registration → OIDC → Member | **deferred** → residual `CSOC-IN-0001` / NK-WP-0025 |
Client registration and authorize handoff verified; **human Authelia + MFA**
is the remaining interactive step.
### Negatives to run when Case B unblocks (or ad-hoc)
Record results here or in residual notes; residual `CSOC-IN-0002`.
| Negative | Intent |
|----------|--------|
| State / nonce replay | callback with recycled `state` fails closed |
| Subject collision | same issuer+subject always one Member |
| Email-only collision | email must not take over another subject |
| Step-up ACR | when `OIDC_STEP_UP_ACR` requested, insufficient acr rejected |
| Registration open redirect | `/auth/register/` ignores `?next=` host; only configured URL |
App registration entry (when URL configured):
```bash
# only after NetKingdom publishes public registration URL
# set NETKINGDOM_REGISTRATION_URL in coulomb-social-env (no secrets in git)
```
## Platform OIDC (local redirect) — optional
```bash
export OIDC_ENABLED=true
@ -61,28 +98,14 @@ export OIDC_ISSUER=https://kc.coulomb.social
export OIDC_CLIENT_ID=coulomb-social
export OIDC_REDIRECT_URI=http://127.0.0.1:8008/auth/callback/
export OIDC_SCOPES="openid profile email groups"
# optional live user-engine (else stub):
# export USER_ENGINE_BASE_URL=https://users.92-205-62-239.nip.io
# export USER_ENGINE_PROXY_SECRET="$(kubectl -n user-engine get secret user-engine-runtime \
# -o jsonpath='{.data.proxy-secret}' | base64 -d)"
make run
```
| Step | Expected |
|------|----------|
| Open http://127.0.0.1:8008/ → Sign in | redirect KeyCape → Authelia |
| Complete MFA | callback → `/app/` with subject / display name |
| Complete MFA if prompted | callback → `/app/` |
| Sign out | landing; `/app/` requires login |
| Second login | same Member / user_engine user_id |
Authorize probe (no browser) 2026-08-09:
| redirect_uri | KeyCape |
|--------------|---------|
| `http://127.0.0.1:8008/auth/callback/` | **302** → Authelia OIDC |
| `https://coulomb.social/auth/callback/` | **302** → Authelia OIDC |
Unregistered redirects still fail with `invalid_profile_usage` (T03).
## Automated
@ -90,15 +113,4 @@ Unregistered redirects still fail with `invalid_profile_usage` (T03).
make test
```
## Public host (parallel to Bubble)
**Strategy:** live Railiance app on **`app.coulomb.social`**; apex **`coulomb.social`** stays Bubble until content + self-registration cutover.
| Check | Detail |
|-------|--------|
| Cluster host | `app.coulomb.social` (Helm + ingress) |
| OIDC redirect | `https://app.coulomb.social/auth/callback/` |
| DNS | operator: `app.coulomb.social` A → `92.205.62.239` |
| Apex | **do not** repoint until Bubble retirement |
See `docs/deploy.md`.
See also: `docs/deploy.md`, `docs/spaces-content.md`, `docs/adr/ADR-0001-netkingdom-identity.md`.

View file

@ -0,0 +1,73 @@
# coulomb-social residual intakes
File-backed intakes for leftovers from finished workplans. Canon: work-record
types § Residuals (`origin: residual`, `origin_ref: <WP-id>`).
---
## CSOC-IN-0001 — Enable public Create account + Case B smoke
```yaml
id: CSOC-IN-0001
kind: intake
title: "Enable Create account and Case B registration smoke on app.coulomb.social"
lane: blue
status: open
priority: medium
owner: coulomb-social
repo: coulomb-social
origin: residual
origin_ref: CSOC-WP-0003
description: |
App entry for Create account is implemented (NETKINGDOM_REGISTRATION_URL,
/auth/register/, landing button). Public enablement waits on NetKingdom
mailbox verification and published registration URL (NK-WP-0025 / USER-WP-0022).
When the platform registration URL is live:
1. Set NETKINGDOM_REGISTRATION_URL on coulomb-social-env (names only in git;
example shape in .env.example).
2. Confirm landing shows Create account and /auth/register/ redirects only to
that URL (no open redirect via ?next=).
3. Run Case B: brand-new public registration → verified mail → OIDC → first
Member JIT on app.coulomb.social.
4. Record evidence under docs/identity/smoke.md.
DoC-Ok: residual of finished CSOC-WP-0003; platform dependency explicit.
notes: |
Depends on NK-WP-0025 (registration orchestration + mail path).
Related: CSOC-IN-0002 (identity negatives after Case B).
state_hub_intake_id: "019ff532-3694-7868-a3a7-f4b4f28a647a"
```
---
## CSOC-IN-0002 — Identity negative matrix after Case B
```yaml
id: CSOC-IN-0002
kind: intake
title: "Record identity negatives: state replay, collision, step-up"
lane: green
status: open
priority: low
owner: coulomb-social
repo: coulomb-social
origin: residual
origin_ref: CSOC-WP-0003
description: |
CSOC-WP-0003 Case A (known LLDAP login) is proven. Remaining negatives listed
in docs/identity/smoke.md should be executed when Case B unblocks (or sooner
offline/dev if useful):
- OIDC state/nonce replay
- issuer+subject collision (single Member)
- email-only must not take over another subject
- step-up ACR rejection when assurance insufficient
- registration redirect allow-list only
Capture pass/fail in docs/identity/smoke.md; close this intake when recorded.
notes: |
Pulls from CSOC-IN-0001 for live registration path; some checks can run offline.
state_hub_intake_id: "019ff532-3ade-7d75-acb7-2bbfa264fcb8"
```

View file

@ -2,6 +2,9 @@
See **ADR-0002**. App DB holds metadata; page bodies are markdown in Forgejo.
**Operator runbook (seed, bind, cluster smoke):** `docs/deploy.md`
§ Operator runbook — spaces on app.coulomb.social.
## Read (T04)
1. Space row has `forgejo_owner`, `forgejo_repo`, `default_branch`, `content_root`.