Go · 3648 bytes Raw Blame History
1 // SPDX-License-Identifier: AGPL-3.0-or-later
2
3 package policy
4
5 import "testing"
6
7 // TestUserActorFromCurrentUser_PropagatesAllFlags pins the SR2 C1+C2
8 // fix: the constructor used by the web layer must carry IsSuspended,
9 // IsSiteAdmin, Impersonating, and ImpersonateWriteOK into the Actor.
10 //
11 // Before SR2, every web mutation handler used UserActor(...) with
12 // `false` literals for site-admin/impersonation, so:
13 // - C1: an impersonating admin could write as the target user
14 // because Impersonating was never true at the actor level.
15 // - C2: a site admin could not read private repos they didn't
16 // collaborate on because IsSiteAdmin never propagated to Actor.
17 //
18 // This test fails LOUD on regressions of either path.
19 func TestUserActorFromCurrentUser_PropagatesAllFlags(t *testing.T) {
20 t.Parallel()
21
22 cases := []struct {
23 name string
24 view CurrentUserView
25 want Actor
26 }{
27 {
28 name: "plain logged-in user",
29 view: CurrentUserView{ID: 7, Username: "alice"},
30 want: Actor{UserID: 7, Username: "alice"},
31 },
32 {
33 name: "suspended user",
34 view: CurrentUserView{ID: 7, Username: "alice", IsSuspended: true},
35 want: Actor{UserID: 7, Username: "alice", IsSuspended: true},
36 },
37 {
38 name: "site admin (not impersonating)",
39 view: CurrentUserView{ID: 7, Username: "alice", IsSiteAdmin: true},
40 want: Actor{UserID: 7, Username: "alice", IsSiteAdmin: true},
41 },
42 {
43 name: "impersonating admin, read-only-by-default",
44 view: CurrentUserView{
45 ID: 99, // viewer.ID is the impersonated user
46 Username: "target",
47 ImpersonatedUserID: 99,
48 RealActorID: 1,
49 ImpersonateWriteOK: false,
50 },
51 want: Actor{
52 UserID: 99,
53 Username: "target",
54 Impersonating: true,
55 // ImpersonateWriteOK stays false — the policy gate on
56 // policy.go for write actions denies. This is the
57 // guarantee C1 was leaking.
58 },
59 },
60 {
61 name: "impersonating admin with write-mode opted in",
62 view: CurrentUserView{
63 ID: 99,
64 Username: "target",
65 ImpersonatedUserID: 99,
66 RealActorID: 1,
67 ImpersonateWriteOK: true,
68 },
69 want: Actor{
70 UserID: 99,
71 Username: "target",
72 Impersonating: true,
73 ImpersonateWriteOK: true,
74 },
75 },
76 }
77
78 for _, tc := range cases {
79 t.Run(tc.name, func(t *testing.T) {
80 t.Parallel()
81 got := UserActorFromCurrentUser(tc.view)
82 if got != tc.want {
83 t.Fatalf("UserActorFromCurrentUser:\n got=%+v\nwant=%+v", got, tc.want)
84 }
85 })
86 }
87 }
88
89 // TestUserActorFromCurrentUser_SiteAdminReadsPrivateRepo pins C2:
90 // before SR2, IsSiteAdmin never reached the Actor on the read path,
91 // so policy.go's "site admin reads anything" branch was unreachable
92 // from the web layer. With the constructor migrated, the override
93 // works as documented.
94 func TestUserActorFromCurrentUser_ImpersonatingDoesNotEscalateAdmin(t *testing.T) {
95 t.Parallel()
96
97 // While impersonating, the wrapping middleware forces IsSiteAdmin
98 // to false on the impersonated identity. The constructor must
99 // honor that — never silently re-grant admin powers to the
100 // impersonated session.
101 view := CurrentUserView{
102 ID: 99,
103 Username: "target",
104 IsSiteAdmin: false, // middleware enforced
105 ImpersonatedUserID: 99,
106 RealActorID: 1,
107 }
108 got := UserActorFromCurrentUser(view)
109 if got.IsSiteAdmin {
110 t.Fatal("UserActorFromCurrentUser must not set IsSiteAdmin while impersonating; got true")
111 }
112 if !got.Impersonating {
113 t.Fatal("UserActorFromCurrentUser must set Impersonating when ImpersonatedUserID is non-zero")
114 }
115 }
116