Commit Graph
3 Commits
Author SHA1 Message Date
jcoffey-dev 08a90a27aa Build SAML login (enterprise/internal/loginhandler), mirroring OIDC
Adds GET /auth/saml/login + POST /auth/saml/acs alongside the existing
OIDC pair, both converging on the same upsert-user/resolve-tenant/
issue-session path. loginhandler.New now takes an optional
*saml.ServiceProvider, RegisterRoutes registers each protocol's routes
independently so either, both, or neither can be configured. SAML's
replay/unsolicited-response defense (InResponseTo, standing in for
OIDC's state) is carried via a SameSite=None sentry_saml_request cookie
-- None because the ACS endpoint receives a cross-site POST from the
IdP's origin, which SameSite=Lax cookies are never sent on.
enterprise-auth's main.go now fetches+parses SAML_IDP_METADATA_URL at
startup (samlsp.FetchMetadata) and wires the result through.

Verified to the same bar as OIDC: a real fake IdP
(crewjam/saml/samlidp, genuine XML signing/verification) drives the
full login->ACS->session-cookie round trip and negative paths (bad
InResponseTo, missing request cookie, missing email/NameID, no/multiple
tenant memberships), all in loginhandler/saml_test.go, no Docker
needed. The login-form HTML is bypassed by pre-seeding a saml.Session
directly into samlidp's session store and presenting the matching
`session` cookie -- an IdP-supported shortcut (confirmed by reading
GetSession), the same "skip the UI, keep the crypto real" approach
oidctest gave the OIDC tests.

Writing that test caught two real bugs in internal/saml.ParseResponse,
both fixed here: it never called r.ParseForm() before reading the
POSTed SAMLResponse field, so every real ACS POST would have silently
decoded an empty response; and its email-attribute matching missed
urn:oid:0.9.2342.19200300.100.1.3 (the standard LDAP "mail" OID), which
is what an IdP sends by default absent an explicit
AttributeConsumingService request for "email" -- exactly what
samlidp's own DefaultAssertionMaker does, and plausibly what real IdPs'
default SAML app templates do too.

Docs (CLAUDE.md, threat-model.md, architecture.md, enterprise/README.md,
phase-4-runbook.md, docker-compose.yml's enterprise-auth comment)
updated in lockstep: SAML login moves from "protocol mechanics only" to
"built, verified with a real fake IdP, not yet tried against a real
external IdP or a running enterprise-auth container" -- the same
disclosed gap OIDC already carried.
2026-08-14 06:39:36 -07:00
jcoffey-dev 1fab02abd5 Phase 4: real OIDC human login (enterprise/internal/loginhandler)
Closes the other major named gap from this phase: until now, there was
no way for a human to actually log in -- only /alerting's RoleService
credential could be minted. GET /auth/oidc/login and GET
/auth/oidc/callback drive the real coreos/go-oidc flow already wired in
enterprise/internal/oidc: CSRF state in a short-lived cookie, code
exchange, ID token verification, upserting a users row, resolving
tenant/role from exactly one tenant_memberships row (refusing outright
on zero or more than one, rather than guessing), and issuing a real
session cookie.

Unlike everything else built this phase, this one is genuinely verified
end to end: the tests spin up coreos/go-oidc's own oidctest fake IdP,
which signs real RS256 ID tokens, and drive the full login->callback->
session-cookie round trip through actual signature verification -- no
live database or Docker needed, so nothing here is asserted without
having actually been run in this session. Also fixes a real bug caught
while wiring this into enterprise-auth's main.go: assigning a nil
*oidc.Provider to the handler's interface field would have produced a
non-nil interface wrapping a nil pointer (Go's classic typed-nil trap),
silently breaking the "OIDC not configured" no-op path -- New() now
takes the concrete pointer type and checks it before ever converting to
the interface, with a regression test pinning the fix down.

Still missing: SAML's equivalent (ACS endpoint), a tenant-picker UI for
multi-membership identities, and any admin UI to actually create a
tenant_memberships row (today that's manual SQL, documented in the
runbook's new bootstrap walkthrough).
2026-08-13 23:00:35 -07:00
jcoffey-dev 3eb0f4c589 Phase 4: SSO scaffolding, RBAC enforcement, tenant-scoped dashboards, audit logging, K8s deployment
RBAC (api/internal/authz) is live on /query and /dashboards, backed by a
new enterprise/ module (session issuance, audit logging, RBAC storage,
OIDC/SAML protocol wiring) that core never imports -- only calls over
HTTP. Found and fixed a real cross-tenant vulnerability in dashboards
(no tenant_id filtering at all) while writing the threat model doc.

Two things are explicitly NOT done, documented rather than hidden:
tenant isolation for log data itself (/query still shares one ClickHouse
connection and Tantivy index across every tenant -- RBAC controls who
can query, not what a query can see), and human SSO login (protocol
wiring exists, no HTTP handler calls it yet). See
docs/security/threat-model.md and docs/phase-4-runbook.md.

Also adds deploy/ (Go Operator + Helm chart, validated offline only --
no cluster was reachable in this environment).
2026-08-13 22:16:59 -07:00