ref(config): clarify repo auth for key

Ayman Bagabas created

Fix edge cases when anonAccess is greater than collab

Change summary

config/auth.go   | 88 +++++++++++++++++++++++++++++++++++--------------
config/config.go | 63 +++++++++++++++-------------------
2 files changed, 90 insertions(+), 61 deletions(-)

Detailed changes

config/auth.go 🔗

@@ -59,43 +59,79 @@ func (cfg *Config) PublicKeyHandler(ctx ssh.Context, pk ssh.PublicKey) bool {
 	return cfg.accessForKey("", pk) != gm.NoAccess
 }
 
+func (cfg *Config) anonAccessLevel() gm.AccessLevel {
+	switch cfg.AnonAccess {
+	case "no-access":
+		return gm.NoAccess
+	case "read-only":
+		return gm.ReadOnlyAccess
+	case "read-write":
+		return gm.ReadWriteAccess
+	case "admin-access":
+		return gm.AdminAccess
+	default:
+		return gm.NoAccess
+	}
+}
+
+// accessForKey returns the access level for the given repo.
+//
+// If repo doesn't exist, then access is based on user's admin privileges, or
+// config.AnonAccess.
+// If repo exists, and private, then admins and collabs are allowed access.
+// If repo exists, and not private, then access is based on config.AnonAccess.
 func (cfg *Config) accessForKey(repo string, pk ssh.PublicKey) gm.AccessLevel {
-	private := cfg.isPrivate(repo)
-	for _, u := range cfg.Users {
-		for _, k := range u.PublicKeys {
+	var u *User
+	var r *RepoConfig
+	anon := cfg.anonAccessLevel()
+OUT:
+	// Find user
+	for _, user := range cfg.Users {
+		for _, k := range user.PublicKeys {
 			apk, _, _, _, err := ssh.ParseAuthorizedKey([]byte(strings.TrimSpace(k)))
 			if err != nil {
 				log.Printf("error: malformed authorized key: '%s'", k)
 				return gm.NoAccess
 			}
 			if ssh.KeysEqual(pk, apk) {
-				if u.Admin {
-					return gm.AdminAccess
-				}
-				for _, r := range u.CollabRepos {
-					if repo == r {
-						return gm.ReadWriteAccess
-					}
-				}
-				if !private {
-					return gm.ReadOnlyAccess
-				}
+				us := user
+				u = &us
+				break OUT
 			}
 		}
 	}
-	if private && len(cfg.Users) > 0 {
-		return gm.NoAccess
+	// Find repo
+	for _, rp := range cfg.Repos {
+		if rp.Repo == repo {
+			rr := rp
+			r = &rr
+			break
+		}
 	}
-	switch cfg.AnonAccess {
-	case "no-access":
-		return gm.NoAccess
-	case "read-only":
-		return gm.ReadOnlyAccess
-	case "read-write":
-		return gm.ReadWriteAccess
-	case "admin-access":
+	if u != nil && u.Admin {
 		return gm.AdminAccess
-	default:
-		return gm.NoAccess
 	}
+	if r == nil || len(cfg.Users) == 0 {
+		return anon
+	}
+	// Collabs default access is read-write
+	if u != nil {
+		ac := gm.ReadWriteAccess
+		if anon > ac {
+			ac = anon
+		}
+		for _, rr := range u.CollabRepos {
+			if rr == r.Repo {
+				return ac
+			}
+		}
+	}
+	// Users default access is read-only
+	if !r.Private {
+		if anon > gm.ReadOnlyAccess {
+			return anon
+		}
+		return gm.ReadOnlyAccess
+	}
+	return gm.NoAccess
 }

config/config.go 🔗

@@ -27,11 +27,6 @@ import (
 	"github.com/go-git/go-git/v5/storage/memory"
 )
 
-var (
-	// ErrNoConfig is returned when no config file is found.
-	ErrNoConfig = errors.New("no config file found")
-)
-
 // Config is the Soft Serve configuration.
 type Config struct {
 	Name         string         `yaml:"name" json:"name"`
@@ -40,7 +35,7 @@ type Config struct {
 	AnonAccess   string         `yaml:"anon-access" json:"anon-access"`
 	AllowKeyless bool           `yaml:"allow-keyless" json:"allow-keyless"`
 	Users        []User         `yaml:"users" json:"users"`
-	Repos        []MenuRepo     `yaml:"repos" json:"repos"`
+	Repos        []RepoConfig   `yaml:"repos" json:"repos"`
 	Source       *RepoSource    `yaml:"-" json:"-"`
 	Cfg          *config.Config `yaml:"-" json:"-"`
 	mtx          sync.Mutex
@@ -54,8 +49,8 @@ type User struct {
 	CollabRepos []string `yaml:"collab-repos" json:"collab-repos"`
 }
 
-// Repo contains repository configuration information.
-type MenuRepo struct {
+// RepoConfig is a repository configuration.
+type RepoConfig struct {
 	Name    string `yaml:"name" json:"name"`
 	Repo    string `yaml:"repo" json:"repo"`
 	Note    string `yaml:"note" json:"note"`
@@ -128,38 +123,45 @@ func NewConfig(cfg *config.Config) (*Config, error) {
 	return c, nil
 }
 
-// Reload reloads the configuration.
-func (cfg *Config) Reload() error {
-	cfg.mtx.Lock()
-	defer cfg.mtx.Unlock()
-	err := cfg.Source.LoadRepos()
+func (cfg *Config) readConfig(repo string, v interface{}) error {
+	cr, err := cfg.Source.GetRepo(repo)
 	if err != nil {
 		return err
 	}
-	cr, err := cfg.Source.GetRepo("config")
-	if err != nil {
-		return err
-	}
-	cy, _, err := cr.LatestFile("config.yaml")
+	cy, _, err := cr.LatestFile(repo + ".yaml")
 	if err != nil && !errors.Is(err, git.ErrFileNotFound) {
-		return fmt.Errorf("error reading config.yaml: %w", err)
+		return fmt.Errorf("error reading %s.yaml: %w", repo, err)
 	}
-	cj, _, err := cr.LatestFile("config.json")
+	cj, _, err := cr.LatestFile(repo + ".json")
 	if err != nil && !errors.Is(err, git.ErrFileNotFound) {
-		return fmt.Errorf("error reading config.json: %w", err)
+		return fmt.Errorf("error reading %s.json: %w", repo, err)
 	}
 	if cy != "" {
-		err = yaml.Unmarshal([]byte(cy), cfg)
+		err = yaml.Unmarshal([]byte(cy), v)
 		if err != nil {
-			return fmt.Errorf("bad yaml in config.yaml: %s", err)
+			return fmt.Errorf("bad yaml in %s.yaml: %s", repo, err)
 		}
 	} else if cj != "" {
-		err = json.Unmarshal([]byte(cj), cfg)
+		err = json.Unmarshal([]byte(cj), v)
 		if err != nil {
-			return fmt.Errorf("bad json in config.json: %s", err)
+			return fmt.Errorf("bad json in %s.json: %s", repo, err)
 		}
 	} else {
-		return ErrNoConfig
+		return fmt.Errorf("no config file found for %q", repo)
+	}
+	return nil
+}
+
+// Reload reloads the configuration.
+func (cfg *Config) Reload() error {
+	cfg.mtx.Lock()
+	defer cfg.mtx.Unlock()
+	err := cfg.Source.LoadRepos()
+	if err != nil {
+		return err
+	}
+	if err := cfg.readConfig("config", cfg); err != nil {
+		return fmt.Errorf("error reading config: %w", err)
 	}
 	for _, r := range cfg.Source.AllRepos() {
 		name := r.Name()
@@ -276,15 +278,6 @@ func (cfg *Config) createDefaultConfigRepo(yaml string) error {
 	return cfg.Reload()
 }
 
-func (cfg *Config) isPrivate(repo string) bool {
-	for _, r := range cfg.Repos {
-		if r.Repo == repo {
-			return r.Private
-		}
-	}
-	return false
-}
-
 func templatize(mdt string, tmpl interface{}) (string, error) {
 	t, err := template.New("readme").Parse(mdt)
 	if err != nil {