From 0fb897c0b95bb1970043cee5daa7ca3b7df07eda Mon Sep 17 00:00:00 2001 From: Sakurasan <26715255+Sakurasan@users.noreply.github.com> Date: Thu, 1 Oct 2026 23:33:16 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E5=8D=87=E7=BA=A7=E5=90=8E?= =?UTF-8?q?=E7=AB=99=E4=B8=BB=E9=99=8D=E7=BA=A7=E4=B8=BA=20reader=EF=BC=9A?= =?UTF-8?q?EnsureOwner=20=E7=BA=A0=E6=AD=A3=E5=8E=86=E5=8F=B2=E8=BA=AB?= =?UTF-8?q?=E4=BB=BD=E8=A1=8C=E7=9A=84=E8=A7=92=E8=89=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 多用户之前以 admin 会话评论会留下 provider=admin、role=reader 的历史行, 升级后 EnsureOwner 找到该行时未纠正角色,站主登录即拿 reader 角色, 用户管理 403。现在 GetOwner 优先取 owner 行,EnsureOwner 登录即把 历史行纠正成 owner。存量会话角色每请求查库,重启后重登一次即恢复。 --- backend/internal/store/store.go | 12 ++++++++- backend/internal/store/store_test.go | 39 ++++++++++++++++++++++++++++ 2 files changed, 50 insertions(+), 1 deletion(-) diff --git a/backend/internal/store/store.go b/backend/internal/store/store.go index 8ebb949..5337502 100644 --- a/backend/internal/store/store.go +++ b/backend/internal/store/store.go @@ -1897,6 +1897,15 @@ func (s *Store) EnsureOwner(handle string) (model.Reader, error) { } cur.Handle = handle } + // 老库升级路径:站主在评论区的历史身份行 role 可能是 reader + // (多用户之前用 admin 会话评论时自动建的)。登录即纠正成 owner, + // 否则升级后站主拿到的会话是 reader 角色,用户管理直接 403。 + if cur.Role != model.RoleOwner { + if _, uerr := s.db.Exec(s.db.Q(`UPDATE users SET role = 'owner' WHERE id = ?`), cur.ID); uerr != nil { + return model.Reader{}, uerr + } + cur.Role = model.RoleOwner + } return cur, nil } if !errors.Is(err, ErrNotFound) { @@ -1913,7 +1922,8 @@ func (s *Store) EnsureOwner(handle string) (model.Reader, error) { // (老库里站主行只有 provider='admin',没有 role)。 func (s *Store) GetOwner() (model.Reader, error) { r, err := scanReader(s.db.QueryRow(s.db.Q( - `SELECT ` + userCols + ` FROM users WHERE role = 'owner' OR provider = 'admin' ORDER BY id LIMIT 1`))) + `SELECT ` + userCols + ` FROM users WHERE role = 'owner' OR provider = 'admin' + ORDER BY CASE WHEN role = 'owner' THEN 0 ELSE 1 END, id LIMIT 1`))) if errors.Is(err, sql.ErrNoRows) { return model.Reader{}, ErrNotFound } diff --git a/backend/internal/store/store_test.go b/backend/internal/store/store_test.go index 33c665f..d472fe0 100644 --- a/backend/internal/store/store_test.go +++ b/backend/internal/store/store_test.go @@ -490,3 +490,42 @@ func TestProjectCRUD(t *testing.T) { t.Errorf("after delete: got %v, want ErrNotFound", err) } } + +// 老库升级:多用户之前以 admin 会话评论会留下 provider=admin、role=reader +// 的历史身份行。升级后 EnsureOwner 必须把它纠正成 owner——否则站主登录 +// 拿到的是 reader 角色,用户管理直接 403(真实事故)。 +func TestEnsureOwnerPromotesLegacyReaderRow(t *testing.T) { + d, err := db.Open("sqlite", ":memory:") + if err != nil { + t.Fatal(err) + } + defer d.Close() + st, err := New(d) + if err != nil { + t.Fatal(err) + } + // 模拟老库:站主发过评论,留下 role=reader 的 admin 身份行 + if _, err := st.UpsertReader(model.Reader{Provider: "admin", Handle: "admin", Name: "站主"}); err != nil { + t.Fatal(err) + } + legacy, err := st.GetStaffByHandle("admin") + if err != nil { + t.Fatal(err) + } + if legacy.Role != model.RoleReader { + t.Fatalf("setup: legacy role = %q, want reader", legacy.Role) + } + + owner, err := st.EnsureOwner("admin") + if err != nil { + t.Fatal(err) + } + if owner.Role != model.RoleOwner { + t.Fatalf("after EnsureOwner: role = %q, want owner", owner.Role) + } + // 行上是真的改了,不是只改返回值 + again, _ := st.GetStaffByHandle("admin") + if again.Role != model.RoleOwner { + t.Fatalf("persisted role = %q, want owner", again.Role) + } +}