internal/policy/access.go
109 lines · 3149 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 }
50 return false
51}
52
53// DeployScopeAllows authorizes a deploy key purely by its scope: the key is
54// bound to a repository ID (rename- and transfer-proof), grants nothing
55// anywhere else, and never inherits the access of whoever registered it.
56func DeployScopeAllows(scope string, repoID int64, write bool) bool {
57 rest, ok := strings.CutPrefix(scope, "deploy:")
58 if !ok {
59 return false
60 }
61 idStr, mode, ok := strings.Cut(rest, ":")
62 if !ok || idStr != strconv.FormatInt(repoID, 10) {
63 return false
64 }
65 switch mode {
66 case "rw":
67 return true
68 case "ro":
69 return !write
70 }
71 return false
72}
73
74// IsDeployScope reports whether a key scope is a deploy binding.
75func IsDeployScope(scope string) bool { return strings.HasPrefix(scope, "deploy:") }
76
77// RefUpdate is one proposed ref change, with git facts computed by the hook
78// process (which can see quarantined objects; the daemon cannot).
79type RefUpdate struct {
80 Ref string `json:"ref"`
81 Old string `json:"old"`
82 New string `json:"new"`
83 IsDelete bool `json:"is_delete"`
84 IsForce bool `json:"is_force"`
85}
86
87// CheckPush applies ref policy for a push by a user with write access
88// already established. It returns a denial message, or "" to allow.
89func CheckPush(repo store.Repo, updates []RefUpdate) string {
90 protected := map[string]bool{}
91 for _, b := range repo.Settings.ProtectedBranches {
92 protected["refs/heads/"+b] = true
93 }
94 for _, u := range updates {
95 if strings.HasPrefix(u.Ref, "refs/merge-requests/") {
96 return "refs/merge-requests/* is server-owned and cannot be pushed"
97 }
98 if protected[u.Ref] {
99 branch := strings.TrimPrefix(u.Ref, "refs/heads/")
100 if u.IsDelete {
101 return "branch " + branch + " is protected: deletion refused"
102 }
103 if u.IsForce {
104 return "branch " + branch + " is protected: force-push refused"
105 }
106 }
107 }
108 return ""
109}