# member-console — system context for the auditor Supplied because you cannot infer it from a slice of code. These are facts about this system, with locations you can verify. Nothing here is a finding. ## What the application is A Go web application for worker-cooperative membership: members buy plans, which confer entitlements, which provision resources (federated wiki sites, forum access, custom domains). Server-rendered `html/template` with HTMX; PostgreSQL via sqlc/pgx; Valkey for sessions; Temporal for workflows; Keycloak as the OIDC identity provider; Stripe for payments. Module `git.coopcloud.tech/wiki-cafe/member-console`. ## Two surfaces, one role - `internal/server/shell.go` defines the member and operator surfaces. - **The only privileged role is `OperatorRole = "operator-member"`**, a literal in `internal/server/operator_partials.go:33`. There is no admin role yet. - Roles originate in the OIDC ID token and are **stored in the session** at login. `HasRole` reads that session copy, not a live token (`internal/auth/auth.go:757-782`). - There are only three non-test `HasRole` call sites: `internal/server/server.go:723`, `:754`, and `internal/server/operator.go:165`. ## Request pipeline, in order `internal/server/server.go:668-699` builds `preAuthStack`: RequestID → Logging → Recovery → Timeout(32s) → MaxBodySize(1MB) → SecureHeaders → CORS → CSRF → Compress. That wraps `routeAware` (`:688`), which applies the authentication middleware **only to requests that match a registered route pattern**. Server timeouts: Read 4s, Write 8s, Idle 16s, MaxHeaderBytes 1MB. ## Authentication middleware `internal/auth/auth.go:126+`. In order: 1. Public paths pass straight through: `/login`, `/callback`, `/logout`, `/logout-callback`, `/register`, `/favicon.ico`, **plus every path passed in as `selfAuthenticatedPaths`**. 2. Any path under `/static/` passes through. 3. Session must carry `authenticated`. 4. The session's person is looked up in the database **on every authenticated request**; if the row is gone the session is ended and the request bounces to `/login`. This is deliberate (a session can outlive its person after an identity merge, purge, or demo-database reset). ## The exemption coupling — important and deliberate One list, `csrfExemptPaths`, feeds **both** CSRF bypass and authentication bypass: - It is seeded with `/domains/ask` (`internal/server/server.go:404`), which is unauthenticated by design: a TLS-terminating proxy calls it mid-handshake and holds no session and no CSRF token. - Each integration appends its own via `RouteMount.CSRFExemptPaths` (`server.go:411`). - It flows to `csrfConfig.Ignore` (`server.go:561-564`) **and** to `authConfig.Middleware(csrfExemptPaths...)` (`server.go:688`). So declaring a path CSRF-exempt also makes it unauthenticated. Current declarations: Stripe `/webhooks/stripe` (`internal/integrations/stripe/stripe.go:79,147`); Discourse `web.WebhookPath` (`internal/integrations/discourse/discourse.go:300`); FedWiki declares none (`internal/integrations/fedwiki/fedwiki.go:297`). ## Response headers `internal/middleware/security.go` sets `Cache-Control: no-store`, `X-Content-Type-Options: nosniff`, `X-Frame-Options: DENY`, `Strict-Transport-Security: max-age=3600; includeSubDomains`, `Referrer-Policy: no-referrer`, `X-XSS-Protection`, `Cross-Origin-Embedder-Policy: require-corp`, `Cross-Origin-Opener-Policy: same-origin`, `Cross-Origin-Resource-Policy: same-origin`, and this CSP: ``` default-src 'self'; script-src 'self' https://unpkg.com/htmx.org@*; style-src 'self'; img-src 'self' data:; font-src 'self'; connect-src 'self'; object-src 'none'; frame-ancestors 'none'; form-action 'self'; base-uri 'self'; ``` `upgrade-insecure-requests` is appended only when `env == "production"`. Project intent, stated in its own docs, is a strict CSP with **no inline scripts** and **self-hosted** front-end assets (see `NOTICE` and `internal/embeds/static/`). ## CSRF `internal/middleware/csrf.go` wraps a gorilla-style CSRF with a 32-byte secret that must persist across restarts, per-path `Ignore` predicates, configurable `TrustedOrigins`, and configurable cookie `Secure`/`HttpOnly`/`SameSite`. Browser writes send the token in an `X-CSRF-Token` header from the page, because native form POSTs were failing an Origin check under `Referrer-Policy: no-referrer`. ## Tenancy model Person → organization → workspace. A person's session carries person, org, and workspace ids (`auth.go:722-747`). Grants form an append-only ledger; what is "currently delivering" lives on `pool_provisions.status`, not on `grants.status`. Cross-tenant isolation is the property that matters: a member of org A must not read or mutate anything belonging to org B. ## Trust boundaries | Boundary | Direction | Authenticated by | |---|---|---| | Browser ↔ app | inbound | session cookie + CSRF token | | Keycloak ↔ app | inbound identity | OIDC authorization code flow; ID token | | Stripe ↔ app | inbound webhook | provider signature (no session, no CSRF) | | Discourse ↔ app | inbound webhook | provider secret (no session, no CSRF) | | TLS proxy ↔ app | inbound `/domains/ask` | nothing, by design | | app → FedWiki farm | outbound | admin bearer token | | app → Stripe API | outbound | restricted key from a file | | app → Postgres / Valkey / Temporal | outbound | connection credentials | ## What is deliberately test-only Everything under `test/` is fixtures. `test/mc-config.yaml` carries `TEST-ONLY-*` secrets on purpose and `test/secrets/` is gitignored. Do not report these as leaked credentials. They matter only if the same value or pattern reaches a production code path.