Go · 3164 bytes Raw Blame History
1 // SPDX-License-Identifier: AGPL-3.0-or-later
2
3 package policy
4
5 // Actor is the authenticated identity asking for a decision. The web
6 // layer constructs one from middleware.CurrentUserFromContext + a
7 // suspended/admin check. SSH and HTTP git transports build their own
8 // from the resolved auth principal.
9 //
10 // An anonymous request has UserID == 0; IsAnonymous == true. Convention
11 // is that callers fill IsAnonymous explicitly even when UserID == 0
12 // implies it — duplication is cheap and keeps the boolean visible at
13 // every call site.
14 type Actor struct {
15 UserID int64
16 Username string
17 IsAnonymous bool
18 IsSuspended bool
19 IsSiteAdmin bool
20 // Impersonating reports whether this actor was constructed from
21 // an admin viewing-as another user. ImpersonateWriteOK enables
22 // the admin's writes during the impersonation; without it, every
23 // write action gets denied (the canonical foot-gun guard).
24 Impersonating bool
25 ImpersonateWriteOK bool
26 }
27
28 // AnonymousActor returns the canonical anonymous Actor. Use in tests
29 // and at unauthenticated entrypoints.
30 func AnonymousActor() Actor {
31 return Actor{IsAnonymous: true}
32 }
33
34 // UserActor wraps a logged-in user. Suspension and site-admin flags
35 // must be loaded from the DB by the caller — the policy package does
36 // not query users on its own to keep the decision pure.
37 //
38 // New web-layer code should prefer UserActorFromCurrentUser, which
39 // also propagates the impersonation pair so admin viewing-as flows
40 // don't silently leak write permission. UserActor is preserved for
41 // callers that don't have a CurrentUser handy (SSH/HTTP git, worker
42 // jobs, tests).
43 func UserActor(userID int64, username string, suspended, siteAdmin bool) Actor {
44 return Actor{
45 UserID: userID,
46 Username: username,
47 IsSuspended: suspended,
48 IsSiteAdmin: siteAdmin,
49 }
50 }
51
52 // CurrentUserView is the minimal view of the request-bound user that
53 // UserActorFromCurrentUser needs. The web's middleware.CurrentUser
54 // satisfies this implicitly — keeping the type local avoids a
55 // policy → middleware import cycle.
56 type CurrentUserView struct {
57 ID int64
58 Username string
59 IsSuspended bool
60 IsSiteAdmin bool
61 ImpersonatedUserID int64
62 RealActorID int64
63 ImpersonateWriteOK bool
64 }
65
66 // UserActorFromCurrentUser is the canonical web-layer constructor.
67 // Beyond UserActor, it propagates the impersonation pair so the
68 // "read-only by default" promise on policy.go's impersonation gate
69 // is actually enforced — and so audit rows can be tagged with the
70 // real actor ID when impersonating.
71 //
72 // Convention: every handler that can run for an impersonating admin
73 // should construct the actor through this constructor. Any callsite
74 // still on plain UserActor with IsSiteAdmin=false hardcoded is a
75 // silent permission leak (audit 2026-05-10 C1/C2).
76 func UserActorFromCurrentUser(v CurrentUserView) Actor {
77 return Actor{
78 UserID: v.ID,
79 Username: v.Username,
80 IsSuspended: v.IsSuspended,
81 IsSiteAdmin: v.IsSiteAdmin,
82 Impersonating: v.ImpersonatedUserID != 0,
83 ImpersonateWriteOK: v.ImpersonateWriteOK,
84 }
85 }
86