repos / git-pr

a self-hosted git collaboration server
git clone https://github.com/picosh/git-pr.git

Eric Bower  ·  2025-03-13

backend.go

  1package git
  2
  3import (
  4	"encoding/base64"
  5	"fmt"
  6	"log/slog"
  7	"strings"
  8
  9	"github.com/charmbracelet/ssh"
 10	"github.com/jmoiron/sqlx"
 11	gossh "golang.org/x/crypto/ssh"
 12)
 13
 14type Backend struct {
 15	Logger *slog.Logger
 16	DB     *sqlx.DB
 17	Cfg    *GitCfg
 18}
 19
 20var ErrRepoNoNamespace = fmt.Errorf("repo must be namespaced by username")
 21
 22// Repo Namespace.
 23func (be *Backend) CreateRepoNs(userName, repoName string) string {
 24	if be.Cfg.CreateRepo == "admin" {
 25		return repoName
 26	}
 27	return fmt.Sprintf("%s/%s", userName, repoName)
 28}
 29
 30func (be *Backend) ValidateRepoNs(repoNs string) error {
 31	_, repoID := be.SplitRepoNs(repoNs)
 32	if strings.Contains(repoID, "/") {
 33		return fmt.Errorf("repo can only contain a single forward-slash")
 34	}
 35	return nil
 36}
 37
 38func (be *Backend) SplitRepoNs(repoNs string) (string, string) {
 39	results := strings.SplitN(repoNs, "/", 2)
 40	if len(results) == 1 {
 41		return "", results[0]
 42	}
 43
 44	return results[0], results[1]
 45}
 46
 47func (be *Backend) CanCreateRepo(repo *Repo, requester *User) error {
 48	pubkey, err := be.PubkeyToPublicKey(requester.Pubkey)
 49	if err != nil {
 50		return err
 51	}
 52	isAdmin := be.IsAdmin(pubkey)
 53	if isAdmin {
 54		return nil
 55	}
 56
 57	// can create repo is a misnomer since we are saying it's ok to create
 58	// a repo even though one already exists.  this is a hack since this function
 59	// is used exclusively inside pr creation flow.
 60	if repo != nil {
 61		return nil
 62	}
 63
 64	if be.Cfg.CreateRepo == "user" {
 65		return nil
 66	}
 67
 68	// new repo with cfg indicating only admins can create prs/repos
 69	return fmt.Errorf("you are not authorized to create repo")
 70}
 71
 72func (be *Backend) Pubkey(pk ssh.PublicKey) string {
 73	return be.KeyForKeyText(pk)
 74}
 75
 76func (be *Backend) KeyForFingerprint(pk ssh.PublicKey) string {
 77	return gossh.FingerprintSHA256(pk)
 78}
 79
 80func (be *Backend) PubkeyToPublicKey(pubkey string) (ssh.PublicKey, error) {
 81	kk, _, _, _, err := ssh.ParseAuthorizedKey([]byte(pubkey))
 82	return kk, err
 83}
 84
 85func (be *Backend) KeyForKeyText(pk ssh.PublicKey) string {
 86	kb := base64.StdEncoding.EncodeToString(pk.Marshal())
 87	return fmt.Sprintf("%s %s", pk.Type(), kb)
 88}
 89
 90func (be *Backend) KeysEqual(pka, pkb string) bool {
 91	return pka == pkb
 92}
 93
 94func (be *Backend) IsAdmin(pk ssh.PublicKey) bool {
 95	for _, apk := range be.Cfg.Admins {
 96		if ssh.KeysEqual(pk, apk) {
 97			return true
 98		}
 99	}
100	return false
101}
102
103func (be *Backend) IsPrOwner(pka, pkb int64) bool {
104	return pka == pkb
105}
106
107type PrAcl struct {
108	CanModify      bool
109	CanDelete      bool
110	CanReview      bool
111	CanAddPatchset bool
112}
113
114func (be *Backend) GetPatchRequestAcl(repo *Repo, prq *PatchRequest, requester *User) *PrAcl {
115	acl := &PrAcl{}
116	if requester == nil {
117		return acl
118	}
119
120	pubkey, err := be.PubkeyToPublicKey(requester.Pubkey)
121	if err != nil {
122		return acl
123	}
124
125	isAdmin := be.IsAdmin(pubkey)
126	// admin can do it all
127	if isAdmin {
128		acl.CanModify = true
129		acl.CanReview = true
130		acl.CanDelete = true
131		acl.CanAddPatchset = true
132		return acl
133	}
134
135	// repo owner can do it all
136	if repo.UserID == requester.ID {
137		acl.CanModify = true
138		acl.CanReview = true
139		acl.CanDelete = true
140		acl.CanAddPatchset = true
141		return acl
142	}
143
144	// pr creator have special priv
145	if be.IsPrOwner(prq.UserID, requester.ID) {
146		acl.CanModify = true
147		acl.CanReview = false
148		acl.CanDelete = true
149		acl.CanAddPatchset = true
150		return acl
151	}
152
153	// otherwise no perms
154	acl.CanModify = false
155	acl.CanDelete = false
156	// anyone can review or add a patchset
157	acl.CanReview = true
158	acl.CanAddPatchset = true
159
160	return acl
161}