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