178 lines
5.6 KiB
Go
178 lines
5.6 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.IsWorkspaceAdmin {
|
|
http.Redirect(w, r, "/admin/projects", http.StatusSeeOther)
|
|
return
|
|
}
|
|
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 enthält zunächst nur den Fingerabdruck des ADMIN_TOKEN. 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 s.plattformSubjekt(""), true
|
|
}
|
|
|
|
if auth.LooksLikeWorkspaceAdminToken(key) {
|
|
return s.resolveWorkspaceAdminLogin(r, key)
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
// resolveWorkspaceAdminLogin prüft das Token eines Workspace-Admins und gibt
|
|
// sein Session-Subject zurück.
|
|
//
|
|
// Die Anmeldung landet im Protokoll des Workspace, ohne Projekt — sie betrifft
|
|
// keines im Besonderen. Sichtbar ist sie im Protokoll aller Projekte
|
|
// (/admin/audit).
|
|
func (s *Server) resolveWorkspaceAdminLogin(r *http.Request, token string) (string, bool) {
|
|
admin, err := s.control.ResolveWorkspaceAdminHash(r.Context(), auth.HashKey(token))
|
|
if err != nil {
|
|
return "", false
|
|
}
|
|
|
|
ws, err := s.workspaces.Get(admin.WorkspaceID)
|
|
if err != nil {
|
|
return "", false
|
|
}
|
|
|
|
ws.Store.LogAudit(r.Context(), store.AuditEvent{
|
|
Actor: Session{IsWorkspaceAdmin: true, WorkspaceAdmin: admin}.ActorName(),
|
|
Action: store.ActionLoginSuccess, Source: store.SourceWeb, IP: auth.ClientIP(r),
|
|
})
|
|
|
|
return auth.SubjectWorkspaceAdminPrefix + admin.WorkspaceID + ":" + admin.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)
|
|
}
|
|
|
|
// plattformSubjekt baut das Session-Subject des Plattform-Admins mit dem
|
|
// Fingerabdruck des aktuellen ADMIN_TOKEN. workspaceID darf leer sein.
|
|
func (s *Server) plattformSubjekt(workspaceID string) string {
|
|
return auth.PlatformSubject(s.adminFingerprint, workspaceID)
|
|
}
|
|
|
|
// 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)
|
|
}
|