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).
149 lines
6.6 KiB
Markdown
149 lines
6.6 KiB
Markdown
# 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 `alerting`↔`api` 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
|
|
|
|
```sh
|
|
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:
|
|
|
|
```sh
|
|
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:
|
|
|
|
```sh
|
|
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:
|
|
|
|
```sh
|
|
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
|
|
```
|
|
|
|
```sh
|
|
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 |
|