Files
cairnobs/enterprise
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
..

enterprise

Commercial license, not AGPLv3 — see /CLAUDE.md's licensing boundary. SSO (OIDC/SAML), tenant provisioning, and RBAC. Nothing in /agent, /ingest, /storage, /api, /web core, or /cli imports from this module — confirmed by hack/check-tenant-boundary.sh, run in CI. enterprise/ supplies tenant-scoped implementations of core's already-shipped api/internal/querylang/executor.SQLRunner/ SearchClient interfaces rather than core growing tenant awareness — see /docs/phase-4-isolation-design.md for why.

Status

Tasks 3-5 (module skeleton, SSO library wiring, audit logging, and auth wiring in /api//web//cli) are built and tested. What's live end-to-end:

  • internal/session issues/validates signed (HS256/JWT) tokens for both human sessions and /alerting's RoleService credential.
  • internal/authhandler serves POST /internal/authorize (the endpoint api/internal/authz.HTTPAuthorizer calls) and GET /auth/features (the runtime-capability check /web's settings page reads).
  • api's /query and /dashboards endpoints enforce RBAC via authz.RequireRole/RequireRoleOrService, nil-safe (no-op) when ENTERPRISE_AUTH_URL isn't configured -- matches Phase 0-3 behavior.
  • /alerting's queryclient presents a RoleService Bearer token (API_SERVICE_TOKEN) when configured -- see /docs/phase-4-isolation-design.md's alertingapi gap.
  • sentryctl presents $SENTRYCTL_TOKEN as a Bearer credential on every request when set.
  • internal/rbacstore: full CRUD over users/tenants/ tenant_memberships (metadata/migrations/0017-0023), verified against a live Postgres.

Deliberately deferred, not half-built -- named explicitly rather than silently left out:

  • The actual OIDC/SAML login/callback HTTP handlers that would issue a human session after a real IdP round trip (internal/oidc/ internal/saml do the protocol mechanics; nothing calls them from an HTTP handler yet). -mint-service-token is the only way to get a token today, and it only mints RoleService credentials.
  • dashboard_permissions/data_sources CRUD (schema exists, metadata/migrations/0024-0026; no caller reads per-resource grants yet -- dashboards' handler enforces tenant-baseline role only, not the matrix's "(own/granted)" qualifier).
  • internal/tenantprovision (ClickHouse DB/user/grant + Tantivy index provisioning) and the tenant-scoped internal/chrunner/ internal/searchclient SQLRunner/SearchClient implementations -- task 2's isolation model, not yet built against real per-tenant connections.
  • Wiring internal/audit into api's queryapi.AuditLogger extension point (built in core since task 4, still passed as nil).

Package layout

cmd/enterprise-auth/   config loading, OIDC discovery at startup, health/authorize/features endpoints, -mint-service-token
internal/tenant/        the ID type -- see its package doc comment before touching it
internal/oidc/           coreos/go-oidc wiring: discovery, login redirect, code exchange + ID token verification
internal/saml/            crewjam/saml wiring: SP setup, login redirect, response parsing/validation
internal/session/          issues/validates signed session + RoleService tokens
internal/authhandler/       POST /internal/authorize, GET /auth/features
internal/rbacstore/          users/tenants/tenant_memberships CRUD (pgx against sentry_metadata)
internal/audit/            append-only, hash-chained query audit log -- see its own package
                            doc comment and /docs/phase-4-isolation-design.md's audit section
internal/config/          env-var config, same convention as every other Go service here

Future additions: internal/tenantprovision, internal/chrunner/ internal/searchclient (tenant-scoped SQLRunner/SearchClient implementations), the OIDC/SAML login/callback HTTP handlers, and dashboard_permissions/data_sources CRUD -- see "Status" above.

Why OIDC and SAML aren't hand-rolled

coreos/go-oidc (built on golang.org/x/oauth2) and crewjam/saml handle token/assertion signature verification, XML signing, and the protocol-level trust establishment — exactly the parts of an SSO integration where a from-scratch implementation is the highest-risk code in the whole feature. Both are well-established libraries, matching this project's existing "boring, well-understood dependency" pattern (clickhouse-go/v2, jackc/pgx/v5).

Building & testing

go build ./...
go vet ./...
go test ./...

internal/audit's real guarantees (the audit_writer grant restriction, the immutability trigger, hash-chain correctness under concurrency) can only be proven against a real Postgres — those integration tests are skipped by default and only run with AUDIT_TEST_POSTGRES_ADDR set:

docker run --rm --network sentry_default -v $(pwd)/..:/src -w /src/enterprise \
  -e AUDIT_TEST_POSTGRES_ADDR=metadata-postgres:5432 \
  -e AUDIT_TEST_POSTGRES_PASSWORD=audit-writer-dev-only \
  -e AUDIT_TEST_ADMIN_PASSWORD=sentry-dev-only \
  golang:1.25-alpine go test ./internal/audit/... -v

internal/rbacstore's tests are the same shape (real SQL, real constraints), skipped unless RBACSTORE_TEST_POSTGRES_ADDR is set:

docker run --rm --network sentry_default -v $(pwd)/..:/src -w /src/enterprise \
  -e RBACSTORE_TEST_POSTGRES_ADDR=metadata-postgres:5432 \
  -e RBACSTORE_TEST_POSTGRES_PASSWORD=sentry-dev-only \
  golang:1.25-alpine go test ./internal/rbacstore/... -v

Turning on auth enforcement for manual testing

Off by default (see "Status" above -- there's no login flow to issue a human session yet). To exercise the RoleService path end to end:

docker compose up -d enterprise-auth
TOKEN=$(docker compose run --rm enterprise-auth -mint-service-token=alerting)
# api: set ENTERPRISE_AUTH_URL=http://enterprise-auth:8082 and restart
# alerting: set API_SERVICE_TOKEN=$TOKEN and restart
docker build -f Dockerfile -t sentry-enterprise-auth .   # context is enterprise/, not the repo root

Environment variables

Var Default
HTTP_LISTEN_ADDR :8082
POSTGRES_ADDR localhost:5432
POSTGRES_DATABASE sentry_metadata
POSTGRES_USERNAME sentry
POSTGRES_PASSWORD (empty)
OIDC_ISSUER_URL (empty — OIDC discovery skipped if unset)
OIDC_CLIENT_ID (empty)
OIDC_CLIENT_SECRET (empty)
OIDC_REDIRECT_URL (empty)
SAML_ENTITY_ID (empty)
SAML_ACS_URL (empty)
SAML_IDP_METADATA_URL (empty — presence only feeds GET /auth/features; not yet fetched/parsed)
ENTERPRISE_SESSION_SIGNING_KEY required, min 32 bytes