147 lines
4.9 KiB
Go
147 lines
4.9 KiB
Go
// Package web enthält die HTTP-Schicht: Routen, Middleware, Handler und die
|
|
// Templates der Weboberfläche.
|
|
package web
|
|
|
|
import (
|
|
"context"
|
|
"net/http"
|
|
|
|
"munin/internal/auth"
|
|
"munin/internal/store"
|
|
)
|
|
|
|
// Session beschreibt, wer gerade zugreift.
|
|
//
|
|
// Es gibt zwei Ausprägungen:
|
|
//
|
|
// - Plattform-Admin (ADMIN_TOKEN): darf Projekte anlegen und löschen und auf
|
|
// jedes Projekt zugreifen. Key und Project sind leer.
|
|
// - Projekt-Zugang: an genau einen API-Key gebunden, der zu genau einem
|
|
// Projekt gehört. Was er darf, sagt Role.
|
|
type Session struct {
|
|
Subject string // für die CSRF-Token-Berechnung
|
|
IsPlatformAdmin bool
|
|
Key store.APIKey
|
|
Project store.Project
|
|
Role auth.Role
|
|
}
|
|
|
|
// CanAccessProject sagt, ob diese Sitzung auf ein Projekt zugreifen darf.
|
|
//
|
|
// Hier steckt die Mandantentrennung der Weboberfläche: Ein Projekt-Key kommt
|
|
// ausschließlich an sein eigenes Projekt, ganz gleich welche URL aufgerufen wird.
|
|
func (s Session) CanAccessProject(projectID string) bool {
|
|
if s.IsPlatformAdmin {
|
|
return true
|
|
}
|
|
return s.Key.ProjectID == projectID
|
|
}
|
|
|
|
// CanWrite sagt, ob Einträge angelegt oder geändert werden dürfen.
|
|
func (s Session) CanWrite() bool {
|
|
return s.IsPlatformAdmin || s.Role.CanWrite()
|
|
}
|
|
|
|
// CanManageKeys sagt, ob die Keys eines Projekts verwaltet werden dürfen.
|
|
func (s Session) CanManageKeys() bool {
|
|
return s.IsPlatformAdmin || s.Role.CanManageKeys()
|
|
}
|
|
|
|
// ActorName ist der Name, unter dem diese Sitzung im Audit-Log auftaucht.
|
|
func (s Session) ActorName() string {
|
|
if s.IsPlatformAdmin {
|
|
return "platform-admin"
|
|
}
|
|
return s.Key.Name
|
|
}
|
|
|
|
// contextKey ist ein eigener Typ für Context-Schlüssel.
|
|
//
|
|
// Ein eigener Typ (statt eines Strings) verhindert, dass zwei Pakete
|
|
// versehentlich denselben Schlüssel verwenden und sich gegenseitig die Werte
|
|
// überschreiben — der Typ ist hier nicht exportiert, also kann ihn niemand sonst
|
|
// erzeugen.
|
|
type contextKey int
|
|
|
|
const (
|
|
sessionContextKey contextKey = iota
|
|
projectContextKey
|
|
aktorContextKey
|
|
)
|
|
|
|
// aktorHalter ist ein veränderbarer Platz im Context, in den die
|
|
// Anmelde-Middleware den Namen des Zugangs schreibt.
|
|
//
|
|
// # Warum dieser Umweg?
|
|
//
|
|
// Das Zugriffsprotokoll liegt ganz außen, damit es auch abgewiesene Anfragen
|
|
// erfasst. Zu diesem Zeitpunkt ist aber noch niemand angemeldet — und die
|
|
// Sitzung landet später in einer ABGELEITETEN Kopie des Requests
|
|
// (r.WithContext), die das äußere Stück nie zu sehen bekommt.
|
|
//
|
|
// Deshalb legt die äußere Middleware einen leeren Halter ab, den die innere
|
|
// befüllt. Ein Zeiger im Context ist der übliche Weg dafür.
|
|
//
|
|
// Ohne Absicherung gegen gleichzeitigen Zugriff: Geschrieben und gelesen wird in
|
|
// derselben Goroutine, das Lesen erst nachdem der Handler zurückgekehrt ist.
|
|
type aktorHalter struct {
|
|
name string
|
|
}
|
|
|
|
// withAktorHalter legt einen leeren Halter im Context ab.
|
|
func withAktorHalter(ctx context.Context, h *aktorHalter) context.Context {
|
|
return context.WithValue(ctx, aktorContextKey, h)
|
|
}
|
|
|
|
// merkeAktor trägt den Namen des Zugangs in den Halter ein, falls einer da ist.
|
|
func merkeAktor(ctx context.Context, name string) {
|
|
if h, ok := ctx.Value(aktorContextKey).(*aktorHalter); ok {
|
|
h.name = name
|
|
}
|
|
}
|
|
|
|
// withSession legt die Sitzung im Request-Context ab.
|
|
//
|
|
// Nebenbei wird der Name des Zugangs für das Zugriffsprotokoll vermerkt — das
|
|
// liegt weiter außen und käme sonst nicht an ihn heran (siehe aktorHalter).
|
|
func withSession(ctx context.Context, s Session) context.Context {
|
|
merkeAktor(ctx, s.ActorName())
|
|
return context.WithValue(ctx, sessionContextKey, s)
|
|
}
|
|
|
|
// SessionFrom liest die Sitzung aus dem Request.
|
|
// Ohne durchlaufene Auth-Middleware ist ok false.
|
|
func SessionFrom(r *http.Request) (Session, bool) {
|
|
s, ok := r.Context().Value(sessionContextKey).(Session)
|
|
return s, ok
|
|
}
|
|
|
|
// withProject legt das Projekt der aktuellen Route im Request-Context ab.
|
|
func withProject(ctx context.Context, p store.Project) context.Context {
|
|
return context.WithValue(ctx, projectContextKey, p)
|
|
}
|
|
|
|
// ProjectFrom liest das Projekt der aktuellen Route.
|
|
func ProjectFrom(r *http.Request) (store.Project, bool) {
|
|
p, ok := r.Context().Value(projectContextKey).(store.Project)
|
|
return p, ok
|
|
}
|
|
|
|
// SessionFromContext liest die Sitzung direkt aus einem Context.
|
|
//
|
|
// Die MCP-Werkzeuge bekommen vom SDK nur den Context übergeben, keinen
|
|
// *http.Request — deshalb neben SessionFrom auch diese Variante.
|
|
func SessionFromContext(ctx context.Context) (Session, bool) {
|
|
s, ok := ctx.Value(sessionContextKey).(Session)
|
|
return s, ok
|
|
}
|
|
|
|
// WithSessionForTest legt eine Sitzung im Context ab.
|
|
//
|
|
// Im Betrieb macht das die Middleware. Andere Packages (etwa die Tests von
|
|
// internal/mcpserver) brauchen aber einen Weg, eine Sitzung zu setzen, ohne
|
|
// einen kompletten HTTP-Server aufzubauen — und der Context-Schlüssel ist
|
|
// bewusst nicht exportiert.
|
|
func WithSessionForTest(ctx context.Context, s Session) context.Context {
|
|
return withSession(ctx, s)
|
|
}
|