package web import ( "errors" "log" "net/http" "strings" "munin/internal/auth" "munin/internal/store" ) // Workspace-Admins anlegen und zurückziehen (AUSBAUSTUFEN.md 4.8). // // Beides darf nur der Plattform-Admin (siehe Routes): Ein Workspace-Admin soll // sich keine weiteren Workspace-Admins schaffen können — sonst ließe sich ein // zurückgezogener Zugang durch einen vorher angelegten zweiten ersetzen, ohne // dass der Plattform-Admin davon erfährt. // // Die Seite dazu ist die Bearbeitungsseite des Workspace // (/admin/workspaces/{id}/edit), die Formulare senden an die Routen hier. // handleWorkspaceAdminCreate erzeugt einen Workspace-Admin-Zugang und zeigt // das Token genau einmal an. func (s *Server) handleWorkspaceAdminCreate(w http.ResponseWriter, r *http.Request) { ws, ok := s.workspaceFromPath(w, r) if !ok { return } name := strings.TrimSpace(r.FormValue("name")) klartext, err := auth.GenerateWorkspaceAdminToken() if err != nil { s.serverError(w, "Token konnte nicht erzeugt werden", err) return } admin, err := s.control.CreateWorkspaceAdmin(r.Context(), ws.ID, name, auth.HashKey(klartext)) if err != nil { s.renderWorkspaceEdit(w, r, http.StatusBadRequest, ws, workspaceEditData{}, "", err.Error()) return } sess, _ := SessionFrom(r) s.logWorkspaceAdminEvent(r, ws.ID, sess, store.ActionWorkspaceAdminCreated, admin.Name) s.renderWorkspaceEdit(w, r, http.StatusOK, ws, workspaceEditData{NewToken: klartext, NewTokenName: admin.Name}, "Workspace-Admin angelegt. Das Token wird nur jetzt angezeigt — bitte gleich übernehmen.", "") } // handleWorkspaceAdminRevoke zieht einen Workspace-Admin-Zugang zurück. Eine // laufende Sitzung endet damit beim nächsten Seitenaufruf (siehe // workspaceAdminSession in middleware.go). func (s *Server) handleWorkspaceAdminRevoke(w http.ResponseWriter, r *http.Request) { ws, ok := s.workspaceFromPath(w, r) if !ok { return } id := r.FormValue("id") admin, err := s.control.WorkspaceAdminByID(r.Context(), id) // Gehört der Zugang zu einem anderen Workspace, wie "nicht vorhanden" // behandeln — dieselbe Regel wie bei den Keys (handleKeyRevoke). if errors.Is(err, store.ErrNotFound) || (err == nil && admin.WorkspaceID != ws.ID) { http.NotFound(w, r) return } if err != nil { s.serverError(w, "Workspace-Admin konnte nicht gelesen werden", err) return } if err := s.control.RevokeWorkspaceAdmin(r.Context(), ws.ID, id); err != nil && !errors.Is(err, store.ErrNotFound) { s.serverError(w, "Workspace-Admin konnte nicht zurückgezogen werden", err) return } sess, _ := SessionFrom(r) s.logWorkspaceAdminEvent(r, ws.ID, sess, store.ActionWorkspaceAdminRevoked, admin.Name) s.renderWorkspaceEdit(w, r, http.StatusOK, ws, workspaceEditData{}, "Zugang zurückgezogen. Er gilt ab sofort nicht mehr.", "") } // logWorkspaceAdminEvent schreibt ein Ereignis ins Protokoll des betroffenen // Workspace — dort, wo auch alles steht, was der Zugang später tut. Der // Workspace muss dafür nicht der gerade gewählte des Plattform-Admins sein. func (s *Server) logWorkspaceAdminEvent(r *http.Request, workspaceID string, sess Session, aktion, name string) { ws, err := s.workspaces.Get(workspaceID) if err != nil { log.Printf("Protokoll von Workspace %s nicht erreichbar: %v", workspaceID, err) return } ws.Store.LogAudit(r.Context(), store.AuditEvent{ Actor: sess.ActorName(), Action: aktion, Source: store.SourceWeb, IP: auth.ClientIP(r), Detail: name, }) }