diff --git a/config/auth.go b/config/auth.go index a1ba59fe9..0bbcaa6e1 100644 --- a/config/auth.go +++ b/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 } diff --git a/config/config.go b/config/config.go index 85e7ce831..da6ff3e5d 100644 --- a/config/config.go +++ b/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 {