internal/httpd/control.go

32a583340a8db2b242ed452180dd239fd118bc26
gitbay/internal/httpd/control.go history · blame · raw

140 lines · 3842 bytes

  1package httpd
  2
  3import (
  4	"bytes"
  5	"encoding/json"
  6	"strings"
  7
  8	"gitbay.org/gitbay/internal/control"
  9	"gitbay.org/gitbay/internal/gitutil"
 10	"gitbay.org/gitbay/internal/protocol"
 11	"gitbay.org/gitbay/internal/store"
 12)
 13
 14// runControl executes a control command as the browser session's user,
 15// through the same registry the CLI and the JSON API reach. Web writes
 16// never reimplement command logic — merge gates, review rules, and audit
 17// entries stay in one place — so the surfaces cannot drift apart.
 18//
 19// ViaAPI is set, which refuses SSHOnly commands: anything whose input is a
 20// credential (secrets, mirror tokens, session minting) stays on SSH.
 21func (s *Server) runControl(u store.User, argv []string) (out string, msg string, ok bool) {
 22	var stdout, stderr bytes.Buffer
 23	ctx := &control.Ctx{
 24		User:   u,
 25		Source: "web",
 26		Scope:  "full",
 27		Store:  s.st,
 28		Cfg:    s.cfg,
 29		Stdin:  strings.NewReader(""),
 30		Stdout: &stdout,
 31		Stderr: &stderr,
 32		ViaAPI: true,
 33	}
 34	code := control.Dispatch(ctx, argv)
 35	m := strings.TrimSpace(stderr.String())
 36	if m == "" {
 37		m = strings.TrimSpace(stdout.String())
 38	}
 39	return stdout.String(), m, code == protocol.ExitOK
 40}
 41
 42// runControlJSON runs a command in JSON mode and returns its data object.
 43// In JSON mode a failure is an envelope carrying the message rather than
 44// stderr text, so both paths are read from the same envelope.
 45func (s *Server) runControlJSON(u store.User, argv []string) (data map[string]any, msg string, ok bool) {
 46	var stdout, stderr bytes.Buffer
 47	ctx := &control.Ctx{
 48		User:   u,
 49		Source: "web",
 50		Scope:  "full",
 51		Store:  s.st,
 52		Cfg:    s.cfg,
 53		Stdin:  strings.NewReader(""),
 54		Stdout: &stdout,
 55		Stderr: &stderr,
 56		JSON:   true,
 57		ViaAPI: true,
 58	}
 59	code := control.Dispatch(ctx, argv)
 60	var env struct {
 61		Data  map[string]any `json:"data"`
 62		Error string         `json:"error"`
 63	}
 64	json.Unmarshal(stdout.Bytes(), &env)
 65	if code != protocol.ExitOK {
 66		m := env.Error
 67		if m == "" {
 68			m = strings.TrimSpace(stderr.String())
 69		}
 70		if m == "" {
 71			m = "the command failed"
 72		}
 73		return nil, m, false
 74	}
 75	return env.Data, "", true
 76}
 77
 78// authorNames maps commit author addresses to account names for one
 79// request. A commit carries whatever name git was configured with; when
 80// the address is a verified address here, the account's own name is the
 81// truthful one to show, and it links somewhere.
 82type authorNames struct {
 83	st    *store.Store
 84	cache map[string]string
 85}
 86
 87func (s *Server) authorNames() *authorNames {
 88	return &authorNames{st: s.st, cache: map[string]string{}}
 89}
 90
 91// name returns the account name for an address, or the commit's own
 92// author name when no account has verified it.
 93func (a *authorNames) name(email, fallback string) string {
 94	if email == "" {
 95		return fallback
 96	}
 97	if got, ok := a.cache[email]; ok {
 98		if got == "" {
 99			return fallback
100		}
101		return got
102	}
103	name, _ := a.st.UsernameByVerifiedEmail(email)
104	a.cache[email] = name
105	if name == "" {
106		return fallback
107	}
108	return name
109}
110
111// known reports whether the address belongs to an account, so callers can
112// decide to link the name.
113func (a *authorNames) known(email string) bool {
114	if email == "" {
115		return false
116	}
117	if got, ok := a.cache[email]; ok {
118		return got != ""
119	}
120	name, _ := a.st.UsernameByVerifiedEmail(email)
121	a.cache[email] = name
122	return name != ""
123}
124
125// namedCommits rewrites listing authors to account names where the
126// address is verified here.
127func (s *Server) namedCommits(m map[string]gitutil.EntryCommit) map[string]gitutil.EntryCommit {
128	names := s.authorNames()
129	for k, c := range m {
130		c.Author = names.name(c.Email, c.Author)
131		m[k] = c
132	}
133	return m
134}
135
136// namedTip does the same for the single commit above a tree listing.
137func (s *Server) namedTip(c gitutil.EntryCommit) gitutil.EntryCommit {
138	c.Author = s.authorNames().name(c.Email, c.Author)
139	return c
140}