auth.go

  1package config
  2
  3import (
  4	"log"
  5	"strings"
  6
  7	gm "github.com/charmbracelet/soft-serve/server/git"
  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) gm.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 != gm.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 != gm.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) != gm.NoAccess
 60}
 61
 62func (cfg *Config) anonAccessLevel() gm.AccessLevel {
 63	cfg.mtx.RLock()
 64	defer cfg.mtx.RUnlock()
 65	switch cfg.AnonAccess {
 66	case "no-access":
 67		return gm.NoAccess
 68	case "read-only":
 69		return gm.ReadOnlyAccess
 70	case "read-write":
 71		return gm.ReadWriteAccess
 72	case "admin-access":
 73		return gm.AdminAccess
 74	default:
 75		return gm.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) gm.AccessLevel {
 86	anon := cfg.anonAccessLevel()
 87	private := cfg.isPrivate(repo)
 88	// Find user
 89	for _, user := range cfg.Users {
 90		for _, k := range user.PublicKeys {
 91			apk, _, _, _, err := ssh.ParseAuthorizedKey([]byte(strings.TrimSpace(k)))
 92			if err != nil {
 93				log.Printf("error: malformed authorized key: '%s'", k)
 94				return gm.NoAccess
 95			}
 96			if ssh.KeysEqual(pk, apk) {
 97				if user.Admin {
 98					return gm.AdminAccess
 99				}
100				u := user
101				if cfg.isCollab(repo, &u) {
102					if anon > gm.ReadWriteAccess {
103						return anon
104					}
105					return gm.ReadWriteAccess
106				}
107				if !private {
108					if anon > gm.ReadOnlyAccess {
109						return anon
110					}
111					return gm.ReadOnlyAccess
112				}
113			}
114		}
115	}
116	// Don't restrict access to private repos if no users are configured.
117	// Return anon access level.
118	if private && len(cfg.Users) > 0 {
119		return gm.NoAccess
120	}
121	return anon
122}
123
124func (cfg *Config) findRepo(repo string) *RepoConfig {
125	for _, r := range cfg.Repos {
126		if r.Repo == repo {
127			return &r
128		}
129	}
130	return nil
131}
132
133func (cfg *Config) isPrivate(repo string) bool {
134	if r := cfg.findRepo(repo); r != nil {
135		return r.Private
136	}
137	return false
138}
139
140func (cfg *Config) isCollab(repo string, user *User) bool {
141	if user != nil {
142		for _, r := range user.CollabRepos {
143			if r == repo {
144				return true
145			}
146		}
147		if r := cfg.findRepo(repo); r != nil {
148			for _, c := range r.Collabs {
149				if c == user.Name {
150					return true
151				}
152			}
153		}
154	}
155	return false
156}