internal/policy/access.go
112 lines · 3240 bytes
1package policy
2
3import (
4 "strconv"
5 "strings"
6
7 "gitbay.org/gitbay/internal/store"
8)
9
10// CanRead reports whether user may read repo over an authenticated channel.
11// Public repos are readable by any authenticated user; private repos require
12// ownership or an explicit grant.
13func CanRead(user store.User, repo store.Repo, grant string) bool {
14 if isOwner(user, repo) {
15 return true
16 }
17 if repo.Visibility == "public" {
18 return true
19 }
20 return grant == "read" || grant == "write" || grant == "admin"
21}
22
23// CanWrite reports whether user may push to repo.
24func CanWrite(user store.User, repo store.Repo, grant string) bool {
25 if isOwner(user, repo) {
26 return true
27 }
28 return grant == "write" || grant == "admin"
29}
30
31// CanAdmin reports whether user may change repo settings and access.
32func CanAdmin(user store.User, repo store.Repo, grant string) bool {
33 if isOwner(user, repo) {
34 return true
35 }
36 return grant == "admin"
37}
38
39func isOwner(user store.User, repo store.Repo) bool {
40 return repo.OwnerKind == "user" && repo.OwnerID == user.ID
41}
42
43// ScopeAllowsGit reports whether an account-scoped SSH key permits git
44// transport at all. Deploy scopes are decided by DeployScopeAllows instead.
45func ScopeAllowsGit(scope, repoPath string, write bool) bool {
46 switch scope {
47 case "full", "git":
48 return true
49 case "runner":
50 // A CI runner clones what it builds and pushes nothing.
51 return !write
52 }
53 return false
54}
55
56// DeployScopeAllows authorizes a deploy key purely by its scope: the key is
57// bound to a repository ID (rename- and transfer-proof), grants nothing
58// anywhere else, and never inherits the access of whoever registered it.
59func DeployScopeAllows(scope string, repoID int64, write bool) bool {
60 rest, ok := strings.CutPrefix(scope, "deploy:")
61 if !ok {
62 return false
63 }
64 idStr, mode, ok := strings.Cut(rest, ":")
65 if !ok || idStr != strconv.FormatInt(repoID, 10) {
66 return false
67 }
68 switch mode {
69 case "rw":
70 return true
71 case "ro":
72 return !write
73 }
74 return false
75}
76
77// IsDeployScope reports whether a key scope is a deploy binding.
78func IsDeployScope(scope string) bool { return strings.HasPrefix(scope, "deploy:") }
79
80// RefUpdate is one proposed ref change, with git facts computed by the hook
81// process (which can see quarantined objects; the daemon cannot).
82type RefUpdate struct {
83 Ref string `json:"ref"`
84 Old string `json:"old"`
85 New string `json:"new"`
86 IsDelete bool `json:"is_delete"`
87 IsForce bool `json:"is_force"`
88}
89
90// CheckPush applies ref policy for a push by a user with write access
91// already established. It returns a denial message, or "" to allow.
92func CheckPush(repo store.Repo, updates []RefUpdate) string {
93 protected := map[string]bool{}
94 for _, b := range repo.Settings.ProtectedBranches {
95 protected["refs/heads/"+b] = true
96 }
97 for _, u := range updates {
98 if strings.HasPrefix(u.Ref, "refs/merge-requests/") {
99 return "refs/merge-requests/* is server-owned and cannot be pushed"
100 }
101 if protected[u.Ref] {
102 branch := strings.TrimPrefix(u.Ref, "refs/heads/")
103 if u.IsDelete {
104 return "branch " + branch + " is protected: deletion refused"
105 }
106 if u.IsForce {
107 return "branch " + branch + " is protected: force-push refused"
108 }
109 }
110 }
111 return ""
112}