internal/httpd/control.go

151 lines · 4280 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// account returns the account name behind an address, if any, so callers
112// can link the displayed name to a profile.
113func (a *authorNames) account(email string) (string, bool) {
114	if email == "" {
115		return "", false
116	}
117	if got, ok := a.cache[email]; ok {
118		return got, got != ""
119	}
120	name, _ := a.st.UsernameByVerifiedEmail(email)
121	a.cache[email] = name
122	return name, name != ""
123}
124
125// namedCommit is a listing commit plus the account behind its author
126// address, when there is one, so the name can link to a profile.
127type namedCommit struct {
128	gitutil.EntryCommit
129	User string
130}
131
132// namedCommits rewrites listing authors to account names where the
133// address is verified here.
134func (s *Server) namedCommits(m map[string]gitutil.EntryCommit) map[string]namedCommit {
135	names := s.authorNames()
136	out := make(map[string]namedCommit, len(m))
137	for k, c := range m {
138		user, _ := names.account(c.Email)
139		c.Author = names.name(c.Email, c.Author)
140		out[k] = namedCommit{EntryCommit: c, User: user}
141	}
142	return out
143}
144
145// namedTip does the same for the single commit above a tree listing.
146func (s *Server) namedTip(c gitutil.EntryCommit) namedCommit {
147	names := s.authorNames()
148	user, _ := names.account(c.Email)
149	c.Author = names.name(c.Email, c.Author)
150	return namedCommit{EntryCommit: c, User: user}
151}