fix(admin): attribute impersonated container switches to the operator

- handleSwitchUser logged the raw session username, so a container switch
  made under an operator ("log in as user") session was recorded as the
  user's own action; use auditActor to record "000+<user>"
- localize the session.switch.<target> and color.update audit actions,
  which rendered as raw action strings on the audit page
- add a regression test for the impersonated switch actor

Co-authored-by: CommandCodeBot <noreply@commandcode.ai>
This commit is contained in:
bluebluesodaandCommandCodeBot committed 2026-09-12 07:15:25 +00:00
1 parent 2e2022fd1f
commit 0c159ae504
3 files changed
+46 -2

No files matched your search

@@ -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 '';
+1 -1
View File
@@ -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)
}
+37
View File
@@ -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+<user>", 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) {