internal/policy/access.go

7effe3feb4612cffb5ed38c02300299559aa8971
gitbay/internal/policy/access.go history · blame · raw

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}