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).
81 lines
3.1 KiB
Go
81 lines
3.1 KiB
Go
// 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)
|
|
}
|