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