feat: Support-Login — Betreiber kann sich als Kunden-Nutzer anmelden
Ebene 5 (Betreiber) kann sich jetzt auf der Account-Detailseite über
einen Button je Nutzer als dieser Kunden-Login anmelden, ohne dessen
Passwort zu kennen — für Support-Fälle, in denen der Betreiber
nachvollziehen muss, was ein Kunde sieht. Nicht für deaktivierte
Nutzer möglich. Neue Spalte session.impersonated_by_user_id (Migration
0014, store.CreateImpersonatedSession) hält fest, wer die Sitzung
ausgelöst hat — die Nav zeigt während der gesamten Sitzung einen
auffälligen Banner ("Support-Zugriff durch ..."), damit nie unklar
ist, im Kontext eines fremden Kontos zu handeln. Jede Nutzung erzeugt
einen audit_log-Eintrag. Die neue Sitzung ersetzt die eigene
Betreiber-Sitzung (kein Sitzungs-Stack) — nach der Nutzung meldet sich
der Betreiber mit den eigenen Zugangsdaten neu an.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
120
internal/web/impersonation_test.go
Normal file
120
internal/web/impersonation_test.go
Normal file
@@ -0,0 +1,120 @@
|
||||
package web_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestBetreiberKannAlsNutzerAnmelden(t *testing.T) {
|
||||
fs := newFakeStore()
|
||||
s := newServer(t, fs)
|
||||
betreiberCookie := seedAccountWithRole(t, fs, "Deklarix Betreiber", "betreiber@example.com", "betreiber")
|
||||
seedAccount(t, fs, "Kunde GmbH", "kunde@example.com")
|
||||
kunde, err := fs.GetUserByEmail(context.Background(), "kunde@example.com")
|
||||
if err != nil {
|
||||
t.Fatalf("GetUserByEmail: %v", err)
|
||||
}
|
||||
|
||||
resp := postForm(t, s, betreiberCookie, "/betreiber/accounts/"+kunde.AccountID+"/nutzer/"+kunde.ID+"/anmelden-als", url.Values{})
|
||||
if resp.Code != http.StatusSeeOther {
|
||||
t.Fatalf("status = %d, body: %s", resp.Code, resp.Body.String())
|
||||
}
|
||||
cookies := resp.Result().Cookies()
|
||||
if len(cookies) == 0 {
|
||||
t.Fatal("expected a new session cookie")
|
||||
}
|
||||
neueCookie := cookies[0]
|
||||
|
||||
// Die neue Sitzung ist tatsächlich der Kunden-Login.
|
||||
protected := getWithCookie(t, s, neueCookie, "/antraege")
|
||||
if protected.Code != http.StatusOK {
|
||||
t.Fatalf("expected the impersonated session to work, status = %d", protected.Code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBetreiberLoginAlsZeigtBanner(t *testing.T) {
|
||||
fs := newFakeStore()
|
||||
s := newServer(t, fs)
|
||||
betreiberCookie := seedAccountWithRole(t, fs, "Deklarix Betreiber", "betreiber@example.com", "betreiber")
|
||||
seedAccount(t, fs, "Kunde GmbH", "kunde@example.com")
|
||||
kunde, err := fs.GetUserByEmail(context.Background(), "kunde@example.com")
|
||||
if err != nil {
|
||||
t.Fatalf("GetUserByEmail: %v", err)
|
||||
}
|
||||
|
||||
loginResp := postForm(t, s, betreiberCookie, "/betreiber/accounts/"+kunde.AccountID+"/nutzer/"+kunde.ID+"/anmelden-als", url.Values{})
|
||||
neueCookie := loginResp.Result().Cookies()[0]
|
||||
|
||||
resp := getWithCookie(t, s, neueCookie, "/")
|
||||
if resp.Code != http.StatusOK {
|
||||
t.Fatalf("status = %d, body: %s", resp.Code, resp.Body.String())
|
||||
}
|
||||
if !strings.Contains(resp.Body.String(), "Support-Zugriff durch") || !strings.Contains(resp.Body.String(), "betreiber@example.com") {
|
||||
t.Errorf("expected the impersonation banner naming the betreiber, got: %s", resp.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestBetreiberLoginAlsProtokolliertAuditEintrag(t *testing.T) {
|
||||
fs := newFakeStore()
|
||||
s := newServer(t, fs)
|
||||
betreiberCookie := seedAccountWithRole(t, fs, "Deklarix Betreiber", "betreiber@example.com", "betreiber")
|
||||
seedAccount(t, fs, "Kunde GmbH", "kunde@example.com")
|
||||
kunde, err := fs.GetUserByEmail(context.Background(), "kunde@example.com")
|
||||
if err != nil {
|
||||
t.Fatalf("GetUserByEmail: %v", err)
|
||||
}
|
||||
betreiber, err := fs.GetUserByEmail(context.Background(), "betreiber@example.com")
|
||||
if err != nil {
|
||||
t.Fatalf("GetUserByEmail: %v", err)
|
||||
}
|
||||
|
||||
postForm(t, s, betreiberCookie, "/betreiber/accounts/"+kunde.AccountID+"/nutzer/"+kunde.ID+"/anmelden-als", url.Values{})
|
||||
|
||||
var gefunden bool
|
||||
for _, e := range fs.auditLog {
|
||||
if e.Action == "betreiber_login_als_nutzer" && e.ActorUserID == betreiber.ID && e.TargetID == kunde.ID {
|
||||
gefunden = true
|
||||
}
|
||||
}
|
||||
if !gefunden {
|
||||
t.Errorf("expected an audit log entry for the impersonation, got: %+v", fs.auditLog)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBetreiberCannotLoginAlsDeaktiviertenNutzer(t *testing.T) {
|
||||
fs := newFakeStore()
|
||||
s := newServer(t, fs)
|
||||
betreiberCookie := seedAccountWithRole(t, fs, "Deklarix Betreiber", "betreiber@example.com", "betreiber")
|
||||
adminCookie := seedAccountWithRole(t, fs, "Kunde GmbH", "kunde-admin@example.com", "admin")
|
||||
postForm(t, s, adminCookie, "/verwaltung/nutzer", url.Values{
|
||||
"email": {"weg@example.com"}, "password": {"ein-langes-passwort"}, "role": {"mitarbeiter"},
|
||||
})
|
||||
weg, err := fs.GetUserByEmail(context.Background(), "weg@example.com")
|
||||
if err != nil {
|
||||
t.Fatalf("GetUserByEmail: %v", err)
|
||||
}
|
||||
postForm(t, s, adminCookie, "/verwaltung/nutzer/"+weg.ID+"/deaktivieren", url.Values{})
|
||||
|
||||
resp := postForm(t, s, betreiberCookie, "/betreiber/accounts/"+weg.AccountID+"/nutzer/"+weg.ID+"/anmelden-als", url.Values{})
|
||||
if resp.Code != http.StatusBadRequest {
|
||||
t.Fatalf("status = %d, want 400 for a deactivated user", resp.Code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMitarbeiterCannotLoginAls(t *testing.T) {
|
||||
fs := newFakeStore()
|
||||
s := newServer(t, fs)
|
||||
cookie := seedAccountWithRole(t, fs, "Test-Mandant", "mitarbeiter@example.com", "mitarbeiter")
|
||||
self, err := fs.GetUserByEmail(context.Background(), "mitarbeiter@example.com")
|
||||
if err != nil {
|
||||
t.Fatalf("GetUserByEmail: %v", err)
|
||||
}
|
||||
|
||||
resp := postForm(t, s, cookie, "/betreiber/accounts/"+self.AccountID+"/nutzer/"+self.ID+"/anmelden-als", url.Values{})
|
||||
if resp.Code != http.StatusNotFound {
|
||||
t.Fatalf("status = %d, want 404 for role mitarbeiter", resp.Code)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user