repos / git-pr

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

Eric Bower  ·  2026-02-24

backend.go

  1package git
  2
  3import (
  4	"bytes"
  5	"encoding/base64"
  6	"fmt"
  7	"log/slog"
  8	"strings"
  9
 10	"github.com/jmoiron/sqlx"
 11	"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 ssh.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) PublicKeysEqual(a, b ssh.PublicKey) bool {
 95	return bytes.Equal(a.Marshal(), b.Marshal())
 96}
 97
 98func (be *Backend) IsAdmin(pk ssh.PublicKey) bool {
 99	for _, apk := range be.Cfg.Admins {
100		if be.PublicKeysEqual(pk, apk) {
101			return true
102		}
103	}
104	return false
105}
106
107func (be *Backend) IsPrOwner(pka, pkb int64) bool {
108	return pka == pkb
109}
110
111type PrAcl struct {
112	CanModify      bool
113	CanDelete      bool
114	CanReview      bool
115	CanAddPatchset bool
116}
117
118func (be *Backend) GetPatchRequestAcl(repo *Repo, prq *PatchRequest, requester *User) *PrAcl {
119	acl := &PrAcl{}
120	if requester == nil {
121		return acl
122	}
123
124	pubkey, err := be.PubkeyToPublicKey(requester.Pubkey)
125	if err != nil {
126		return acl
127	}
128
129	isAdmin := be.IsAdmin(pubkey)
130	// admin can do it all
131	if isAdmin {
132		acl.CanModify = true
133		acl.CanReview = true
134		acl.CanDelete = true
135		acl.CanAddPatchset = true
136		return acl
137	}
138
139	// repo owner can do it all
140	if repo.UserID == requester.ID {
141		acl.CanModify = true
142		acl.CanReview = true
143		acl.CanDelete = true
144		acl.CanAddPatchset = true
145		return acl
146	}
147
148	// pr creator has special priv
149	if be.IsPrOwner(prq.UserID, requester.ID) {
150		acl.CanModify = true
151		acl.CanReview = false
152		acl.CanDelete = true
153		acl.CanAddPatchset = true
154		return acl
155	}
156
157	// otherwise no perms
158	acl.CanModify = false
159	acl.CanDelete = false
160	acl.CanReview = false
161	// anyone can add a patchset
162	acl.CanAddPatchset = true
163
164	return acl
165}