diff --git a/src/internal/admin/templates/admin_audit.html b/src/internal/admin/templates/admin_audit.html index 9b99c75..a58e923 100644 --- a/src/internal/admin/templates/admin_audit.html +++ b/src/internal/admin/templates/admin_audit.html @@ -80,6 +80,7 @@ var ACT={ 'passwd.reset':{zh:'重置面板密码',en:'reset panel password'}, 'admin.passwd':{zh:'修改管理员密码',en:'change admin password'}, 'quota.update':{zh:'修改配额',en:'update quota'}, + 'color.update':{zh:'修改用户标识色',en:'update user color'}, 'blocked.update':{zh:'修改封禁域名列表',en:'update blocked domains'}, 'snapshot.create':{zh:'创建检查点',en:'create checkpoint'}, 'snapshot.delete':{zh:'删除检查点',en:'delete checkpoint'}, @@ -93,7 +94,13 @@ var ACT={ 'ipv6pool.del':{zh:'移除 IPv6 地址',en:'remove IPv6 address'}, 'bandwidth.reset':{zh:'重置流量统计',en:'reset bandwidth'} }; -function actLabel(a){var m=ACT[a]||{};return LANG==='zh'?(m.zh||a):(m.en||a);} +function actLabel(a){ + if(a.indexOf('session.switch.')===0){ + var t=a.slice('session.switch.'.length); + return LANG==='zh'?('切换机器 → '+t):('switch container → '+t); + } + var m=ACT[a]||{};return LANG==='zh'?(m.zh||a):(m.en||a); +} function pad2(n){return (n<10?'0':'')+n;} function fmtTime(s){ if(!s) return ''; diff --git a/src/internal/panel/handlers.go b/src/internal/panel/handlers.go index 7960dc3..f527706 100644 --- a/src/internal/panel/handlers.go +++ b/src/internal/panel/handlers.go @@ -231,7 +231,7 @@ func (s *Server) handleSwitchUser(w http.ResponseWriter, r *http.Request) { } } s.setSessionCookie(w, sess.Token) - _ = s.db.AddAuditLog(current.Name, "session.switch."+target.Name) + _ = s.db.AddAuditLog(s.auditActor(r, current.Name), "session.switch."+target.Name) http.Redirect(w, r, s.p("/"), http.StatusFound) } diff --git a/src/internal/panel/panel_test.go b/src/internal/panel/panel_test.go index f71d3d3..da00f55 100644 --- a/src/internal/panel/panel_test.go +++ b/src/internal/panel/panel_test.go @@ -1798,6 +1798,43 @@ func TestImpersonatedSessionAuditActor(t *testing.T) { } } +// TestImpersonatedSwitchAuditActor verifies that switching containers inside a +// multi-container group under an operator-created session is attributed to the +// operator "000+", not recorded as a plain user action. +func TestImpersonatedSwitchAuditActor(t *testing.T) { + srv, d := newTestServer(t) + h := srv.Handler() + prefix := "/" + testSecret + hash, _ := pw.Hash("pw") + u, err := d.CreateUser("alice", hash, "10.42.0.2", 1, 30001, 10000, 1, 1024, 10) + if err != nil { + t.Fatal(err) + } + if _, err := d.CreateUser("alice-1", hash, "10.42.0.3", 2, 30002, 10100, 2, 2048, 20); err != nil { + t.Fatal(err) + } + sess, err := d.CreateImpersonatedSession(u.ID, 3) + if err != nil { + t.Fatal(err) + } + cookie := &http.Cookie{Name: "vpsmgr_session", Value: sess.Token, Path: prefix} + + rr := doReq(t, h, http.MethodPost, prefix+"/switch-user", url.Values{"name": {"alice-1"}}, cookie) + if rr.Code != http.StatusFound { + t.Fatalf("switch-user = %d, want 302", rr.Code) + } + rows, _ := d.ListAuditLog(0, 10) + var found bool + for _, r := range rows { + if r.Action == "session.switch.alice-1" && r.Actor == "000+alice" { + found = true + } + } + if !found { + t.Errorf("audit rows = %+v, want session.switch.alice-1 attributed 000+alice", rows) + } +} + // TestOverviewImpersonationBanner verifies the "logged in as" banner renders // only when the session is impersonated, with a link back to the admin panel. func TestOverviewImpersonationBanner(t *testing.T) {