internal/policy/access.go

a7c9352033c319587613cb6825035e8d99f5871a
gitbay/internal/policy/access.go history · blame · raw

141 lines · 4272 bytes

  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") == "" }