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>
121 lines
4.4 KiB
Go
121 lines
4.4 KiB
Go
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)
|
|
}
|
|
}
|