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).
6.6 KiB
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/sessionissues/validates signed (HS256/JWT) tokens for both human sessions and/alerting'sRoleServicecredential.internal/authhandlerservesPOST /internal/authorize(the endpointapi/internal/authz.HTTPAuthorizercalls) andGET /auth/features(the runtime-capability check/web's settings page reads).api's/queryand/dashboardsendpoints enforce RBAC viaauthz.RequireRole/RequireRoleOrService, nil-safe (no-op) whenENTERPRISE_AUTH_URLisn't configured -- matches Phase 0-3 behavior./alerting'squeryclientpresents aRoleServiceBearer token (API_SERVICE_TOKEN) when configured -- see/docs/phase-4-isolation-design.md'salerting↔apigap.sentryctlpresents$SENTRYCTL_TOKENas a Bearer credential on every request when set.internal/rbacstore: full CRUD overusers/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/samldo the protocol mechanics; nothing calls them from an HTTP handler yet).-mint-service-tokenis the only way to get a token today, and it only mintsRoleServicecredentials. dashboard_permissions/data_sourcesCRUD (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-scopedinternal/chrunner/internal/searchclientSQLRunner/SearchClientimplementations -- task 2's isolation model, not yet built against real per-tenant connections.- Wiring
internal/auditintoapi'squeryapi.AuditLoggerextension point (built in core since task 4, still passed asnil).
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 |