1package config
  2
  3import (
  4	"strings"
  5
  6	"github.com/charmbracelet/log"
  7
  8	gm "github.com/charmbracelet/wish/git"
  9	"github.com/gliderlabs/ssh"
 10	gossh "golang.org/x/crypto/ssh"
 11)
 12
 13// Push registers Git push functionality for the given repo and key.
 14func (cfg *Config) Push(repo string, pk ssh.PublicKey) {
 15	go func() {
 16		err := cfg.Reload()
 17		if err != nil {
 18			log.Error("error reloading after push", "err", err)
 19		}
 20		if cfg.Cfg.Callbacks != nil {
 21			cfg.Cfg.Callbacks.Push(repo)
 22		}
 23		r, err := cfg.Source.GetRepo(repo)
 24		if err != nil {
 25			log.Error("error getting repo after push", "err", err)
 26			return
 27		}
 28		err = r.UpdateServerInfo()
 29		if err != nil {
 30			log.Error("error updating server info after push", "err", err)
 31		}
 32	}()
 33}
 34
 35// Fetch registers Git fetch functionality for the given repo and key.
 36func (cfg *Config) Fetch(repo string, pk ssh.PublicKey) {
 37	if cfg.Cfg.Callbacks != nil {
 38		cfg.Cfg.Callbacks.Fetch(repo)
 39	}
 40}
 41
 42// AuthRepo grants repo authorization to the given key.
 43func (cfg *Config) AuthRepo(repo string, pk ssh.PublicKey) gm.AccessLevel {
 44	return cfg.accessForKey(repo, pk)
 45}
 46
 47// PasswordHandler returns whether or not password access is allowed.
 48func (cfg *Config) PasswordHandler(ctx ssh.Context, password string) bool {
 49	return (cfg.AnonAccess != "no-access") && cfg.AllowKeyless
 50}
 51
 52// KeyboardInteractiveHandler returns whether or not keyboard interactive is allowed.
 53func (cfg *Config) KeyboardInteractiveHandler(ctx ssh.Context, _ gossh.KeyboardInteractiveChallenge) bool {
 54	return (cfg.AnonAccess != "no-access") && cfg.AllowKeyless
 55}
 56
 57// PublicKeyHandler returns whether or not the given public key may access the
 58// repo.
 59func (cfg *Config) PublicKeyHandler(ctx ssh.Context, pk ssh.PublicKey) bool {
 60	return cfg.accessForKey("", pk) != gm.NoAccess
 61}
 62
 63func (cfg *Config) anonAccessLevel() gm.AccessLevel {
 64	switch cfg.AnonAccess {
 65	case "no-access":
 66		return gm.NoAccess
 67	case "read-only":
 68		return gm.ReadOnlyAccess
 69	case "read-write":
 70		return gm.ReadWriteAccess
 71	case "admin-access":
 72		return gm.AdminAccess
 73	default:
 74		return gm.NoAccess
 75	}
 76}
 77
 78// accessForKey returns the access level for the given repo.
 79//
 80// If repo doesn't exist, then access is based on user's admin privileges, or
 81// config.AnonAccess.
 82// If repo exists, and private, then admins and collabs are allowed access.
 83// If repo exists, and not private, then access is based on config.AnonAccess.
 84func (cfg *Config) accessForKey(repo string, pk ssh.PublicKey) gm.AccessLevel {
 85	anon := cfg.anonAccessLevel()
 86	private := cfg.isPrivate(repo)
 87	// Find user
 88	for _, user := range cfg.Users {
 89		for _, k := range user.PublicKeys {
 90			apk, _, _, _, err := ssh.ParseAuthorizedKey([]byte(strings.TrimSpace(k)))
 91			if err != nil {
 92				log.Error("malformed authorized key", "key", k)
 93				return gm.NoAccess
 94			}
 95			if ssh.KeysEqual(pk, apk) {
 96				if user.Admin {
 97					return gm.AdminAccess
 98				}
 99				u := user
100				if cfg.isCollab(repo, &u) {
101					if anon > gm.ReadWriteAccess {
102						return anon
103					}
104					return gm.ReadWriteAccess
105				}
106				if !private {
107					if anon > gm.ReadOnlyAccess {
108						return anon
109					}
110					return gm.ReadOnlyAccess
111				}
112			}
113		}
114	}
115	// Don't restrict access to private repos if no users are configured.
116	// Return anon access level.
117	if private && len(cfg.Users) > 0 {
118		return gm.NoAccess
119	}
120	return anon
121}
122
123func (cfg *Config) findRepo(repo string) *RepoConfig {
124	for _, r := range cfg.Repos {
125		if r.Repo == repo {
126			return &r
127		}
128	}
129	return nil
130}
131
132func (cfg *Config) isPrivate(repo string) bool {
133	if r := cfg.findRepo(repo); r != nil {
134		return r.Private
135	}
136	return false
137}
138
139func (cfg *Config) isCollab(repo string, user *User) bool {
140	if user != nil {
141		for _, r := range user.CollabRepos {
142			if r == repo {
143				return true
144			}
145		}
146		if r := cfg.findRepo(repo); r != nil {
147			for _, c := range r.Collabs {
148				if c == user.Name {
149					return true
150				}
151			}
152		}
153	}
154	return false
155}