munin/internal/web/handlers_auth.go
Thomas Adamek 466f1e40af Workspaces, Änderungshistorie, Papierkorb, Nutzungsübersicht, Hilfe
Umsetzung der Ausbaustufen 2.1, 3.6, 3.8, 3.9, 4.1, 4.2, 4.4, 4.6,
5.1–5.4 (Details in AUSBAUSTUFEN.md).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 07:44:52 +02:00

139 lines
4.2 KiB
Go

package web
import (
"fmt"
"log"
"net/http"
"strconv"
"time"
"munin/internal/auth"
"munin/internal/store"
)
// handleHome leitet nach der Anmeldung an die passende Startseite weiter.
func (s *Server) handleHome(w http.ResponseWriter, r *http.Request) {
sess, _ := SessionFrom(r)
if sess.IsPlatformAdmin {
if sess.WorkspaceID == "" {
http.Redirect(w, r, "/admin/workspaces", http.StatusSeeOther)
return
}
http.Redirect(w, r, "/admin/projects", http.StatusSeeOther)
return
}
http.Redirect(w, r, "/p/"+sess.Project.Slug+"/", http.StatusSeeOther)
}
// handleLoginForm zeigt das Anmeldeformular.
func (s *Server) handleLoginForm(w http.ResponseWriter, r *http.Request) {
s.render(w, r, "login.html", http.StatusOK, pageData{Title: "Anmelden"})
}
// handleLoginSubmit prüft den eingegebenen Key und setzt den Session-Cookie.
func (s *Server) handleLoginSubmit(w http.ResponseWriter, r *http.Request) {
ip := auth.ClientIP(r)
key := r.FormValue("key")
subject, ok := s.resolveLoginKey(r, key)
if !ok {
// Erst nach einem Fehlversuch die Sperre prüfen und hochzählen — wer den
// richtigen Key hat, kommt immer durch (siehe Kommentar an APIAuth).
if allowed, retryAfter := s.limiter.Allow(ip); !allowed {
sekunden := int(retryAfter.Seconds()) + 1
w.Header().Set("Retry-After", strconv.Itoa(sekunden))
http.Error(w, fmt.Sprintf("Zu viele Anmeldeversuche. Bitte in %d Sekunden erneut versuchen.", sekunden),
http.StatusTooManyRequests)
return
}
s.limiter.RecordFailure(ip)
// Ein ungültiger Key oder Klartext lässt sich keinem Workspace zuordnen
// — siehe der entsprechende Kommentar in APIAuth (middleware.go).
log.Printf("Ungültiger Anmeldeversuch von %s", ip)
s.render(w, r, "login.html", http.StatusUnauthorized, pageData{
Title: "Anmelden",
Error: "Ungültiger Key.",
})
return
}
s.limiter.Reset(ip)
s.setSessionCookie(w, r, subject)
http.Redirect(w, r, "/", http.StatusSeeOther)
}
// resolveLoginKey prüft einen Key beim Anmelden und gibt das Session-Subject zurück.
//
// Der Plattform-Admin bekommt hier noch keinen Workspace zugewiesen — das
// Subject ist zunächst nur "platform". Erst über /admin/workspaces wird ein
// Workspace gewählt und das Subject um dessen ID ergänzt (siehe
// handleWorkspaceSelect).
func (s *Server) resolveLoginKey(r *http.Request, key string) (string, bool) {
if key == "" {
return "", false
}
// Der Plattform-Admin meldet sich mit dem ADMIN_TOKEN an. Verglichen werden
// die Hashes, damit auch hier keine Zeichenkette Zeichen für Zeichen
// verglichen wird und die Antwortzeit nichts verrät.
if auth.HashKey(key) == s.adminTokenHash {
log.Printf("Anmeldung Plattform-Admin von %s", auth.ClientIP(r))
return auth.SubjectPlatformAdmin, true
}
if !auth.LooksLikeKey(key) {
return "", false
}
hash := auth.HashKey(key)
workspaceID, err := s.control.ResolveKeyWorkspace(r.Context(), hash)
if err != nil {
return "", false
}
ws, err := s.workspaces.Get(workspaceID)
if err != nil {
return "", false
}
resolved, err := ws.Store.ResolveKeyHash(r.Context(), hash)
if err != nil {
return "", false
}
ws.Store.LogAudit(r.Context(), store.AuditEvent{
ProjectID: resolved.Project.ID, APIKeyID: resolved.Key.ID, Actor: resolved.Key.Name,
Action: store.ActionLoginSuccess, Source: store.SourceWeb, IP: auth.ClientIP(r),
})
return auth.SubjectKeyPrefix + workspaceID + ":" + resolved.Key.ID, true
}
// handleLogout löscht den Session-Cookie.
func (s *Server) handleLogout(w http.ResponseWriter, r *http.Request) {
cookie := &http.Cookie{
Name: auth.SessionCookieName,
Value: "",
Expires: time.Unix(0, 0),
MaxAge: -1,
}
// Ein Lösch-Cookie muss dieselben Merkmale tragen wie das Original, sonst
// behandelt der Browser es als ein anderes Cookie und das alte bleibt liegen.
s.cookies.Apply(cookie, r)
http.SetCookie(w, cookie)
http.Redirect(w, r, "/login", http.StatusSeeOther)
}
// setSessionCookie signiert das Subject und setzt es als Cookie.
func (s *Server) setSessionCookie(w http.ResponseWriter, r *http.Request, subject string) {
cookie := &http.Cookie{
Name: auth.SessionCookieName,
Value: s.signer.SignSession(subject),
Expires: time.Now().Add(auth.SessionTTL),
}
s.cookies.Apply(cookie, r)
http.SetCookie(w, cookie)
}