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).
This commit is contained in:
@@ -0,0 +1,80 @@
|
||||
// Package authz is core's RBAC extension point -- deliberately minimal
|
||||
// and tenant-agnostic in shape, matching queryapi.AuditLogger's pattern:
|
||||
// core defines the interface and the types, enterprise/ supplies the
|
||||
// implementation. Unlike AuditLogger, the implementation Authorizer
|
||||
// wires in here is NOT enterprise Go code injected directly (that would
|
||||
// require api to import enterprise/, violating the module boundary
|
||||
// confirmed in /docs/phase-4-isolation-design.md) -- it's an HTTP client
|
||||
// to enterprise-auth's /internal/authorize endpoint (see httpauthz.go),
|
||||
// the same "network boundary, not import boundary" pattern already
|
||||
// established between /api and /alerting.
|
||||
package authz
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// Role is ordered for human roles (Viewer < Editor < Admin < Owner, per
|
||||
// /docs/phase-4-rbac-design.md) plus a separate, non-comparable Service
|
||||
// lane for machine callers like /alerting's evaluator -- see Satisfies.
|
||||
type Role string
|
||||
|
||||
const (
|
||||
RoleViewer Role = "viewer"
|
||||
RoleEditor Role = "editor"
|
||||
RoleAdmin Role = "admin"
|
||||
RoleOwner Role = "owner"
|
||||
RoleService Role = "service"
|
||||
)
|
||||
|
||||
var roleRank = map[Role]int{RoleViewer: 1, RoleEditor: 2, RoleAdmin: 3, RoleOwner: 4}
|
||||
|
||||
// Satisfies reports whether this role meets a requirement. RoleService
|
||||
// only ever satisfies RoleService -- a service credential never
|
||||
// satisfies a human-role requirement, and a human role never satisfies
|
||||
// a RoleService requirement, by design: /docs/phase-4-isolation-design.md's
|
||||
// alerting service identity is deliberately not a point on the human
|
||||
// role scale, so it can't accidentally inherit broader access by
|
||||
// ranking above Viewer.
|
||||
func (r Role) Satisfies(required Role) bool {
|
||||
if required == RoleService || r == RoleService {
|
||||
return r == required
|
||||
}
|
||||
return roleRank[r] >= roleRank[required]
|
||||
}
|
||||
|
||||
// Identity is what a successful Authorize call resolves. UserID is
|
||||
// empty for RoleService (see /docs/phase-4-isolation-design.md's
|
||||
// alerting↔api gap -- a service credential proves "this caller is
|
||||
// alerting," not "this caller is acting as a specific human").
|
||||
type Identity struct {
|
||||
TenantID string
|
||||
UserID string
|
||||
Role Role
|
||||
}
|
||||
|
||||
// Authorizer resolves an Identity from an incoming request's
|
||||
// credentials (session cookie or service token) without knowing
|
||||
// anything about *what* the caller is trying to do -- permission
|
||||
// checking against a required Role happens in the middleware
|
||||
// (middleware.go), not here, so this interface stays a pure
|
||||
// "who is this" question.
|
||||
type Authorizer interface {
|
||||
Authorize(r *http.Request) (Identity, error)
|
||||
}
|
||||
|
||||
type identityContextKey struct{}
|
||||
|
||||
// IdentityFromContext lets a handler read the resolved identity a
|
||||
// RequireRole/RequireRoleOrService middleware attached -- e.g. to
|
||||
// populate QueryAuditEntry's tenant/user once Phase 4's audit wiring
|
||||
// threads identity through (see queryapi.AuditLogger's doc comment).
|
||||
func IdentityFromContext(ctx context.Context) (Identity, bool) {
|
||||
id, ok := ctx.Value(identityContextKey{}).(Identity)
|
||||
return id, ok
|
||||
}
|
||||
|
||||
func withIdentity(ctx context.Context, id Identity) context.Context {
|
||||
return context.WithValue(ctx, identityContextKey{}, id)
|
||||
}
|
||||
Reference in New Issue
Block a user