diff --git a/proto/access.go b/proto/access.go index 264f7e849ac428d1b7f4b4ced0e237ccbed97c60..478d0e9d988e3c50be5cf5d3684f1f7fa705aef0 100644 --- a/proto/access.go +++ b/proto/access.go @@ -60,4 +60,6 @@ func (a *AccessLevel) UnmarshalText(text []byte) error { // Access is an interface that defines the access level for repositories. type Access interface { AuthRepo(repo string, pk ssh.PublicKey) AccessLevel + IsCollab(repo string, pk ssh.PublicKey) bool + IsAdmin(pk ssh.PublicKey) bool } diff --git a/proto/provider.go b/proto/provider.go index c23e940b654edd9ff682c879b5d8acf559815ae9..80986fa0cbc0049f2a07ed76d853d39db01aece7 100644 --- a/proto/provider.go +++ b/proto/provider.go @@ -6,6 +6,20 @@ type Provider interface { Open(name string) (Repository, error) // ListRepos lists all repositories. ListRepos() ([]Metadata, error) + // Create creates a new repository. + Create(name string, projectName string, description string, isPrivate bool) error + // Delete deletes a repository. + Delete(name string) error + // Rename renames a repository. + Rename(name string, newName string) error + // SetProjectName sets a repository's project name. + SetProjectName(name string, projectName string) error + // SetDescription sets a repository's description. + SetDescription(name string, description string) error + // SetPrivate sets a repository's private flag. + SetPrivate(name string, isPrivate bool) error + // SetDefaultBranch sets a repository's default branch. + SetDefaultBranch(name string, branch string) error } // MetadataProvider is a Git repository metadata provider. diff --git a/server/config/access.go b/server/config/access.go index 94fe6f1b39e314edecb049277be57da24e2b1be6..6d7249d89376b3359d59692d6e713798d2994b86 100644 --- a/server/config/access.go +++ b/server/config/access.go @@ -1,6 +1,9 @@ package config import ( + "log" + "strings" + "github.com/charmbracelet/soft-serve/proto" "github.com/gliderlabs/ssh" gossh "golang.org/x/crypto/ssh" @@ -13,6 +16,56 @@ func (c *Config) AuthRepo(repo string, pk ssh.PublicKey) proto.AccessLevel { return c.accessForKey(repo, pk) } +// User returns the user for the given public key. Can return nil if no user is +// found. +func (c *Config) User(pk ssh.PublicKey) (proto.User, error) { + k := authorizedKey(pk) + u, err := c.db.GetUserByPublicKey(k) + if err != nil { + log.Printf("error getting user for key: %s", err) + return nil, err + } + if u == nil { + return nil, nil + } + return &user{ + cfg: c, + user: u, + }, nil +} + +// IsCollab returns whether or not the given key is a collaborator on the given +// repository. +func (c *Config) IsCollab(repo string, pk ssh.PublicKey) bool { + if c.isInitialAdminKey(pk) { + return true + } + + isCollab, err := c.db.IsRepoPublicKeyCollab(repo, authorizedKey(pk)) + if err != nil { + log.Printf("error checking if key is repo collab: %v", err) + return false + } + if isCollab { + return true + } + return false +} + +// IsAdmin returns whether or not the given key is an admin. +func (c *Config) IsAdmin(pk ssh.PublicKey) bool { + if c.isInitialAdminKey(pk) { + return true + } + + u, err := c.User(pk) + if err != nil { + log.Printf("error getting user for key: %s", err) + return false + } + return u.IsAdmin() +} + // PasswordHandler returns whether or not password access is allowed. func (c *Config) PasswordHandler(ctx ssh.Context, password string) bool { return (c.AnonAccess != proto.NoAccess) && c.SSH.AllowKeyless && @@ -37,32 +90,34 @@ func (c *Config) PublicKeyHandler(ctx ssh.Context, pk ssh.PublicKey) bool { // 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 (c *Config) accessForKey(repo string, pk ssh.PublicKey) proto.AccessLevel { + if c.isInitialAdminKey(pk) { + return proto.AdminAccess + } + anon := c.AnonAccess info, err := c.Metadata(repo) if err != nil || info == nil { + log.Printf("error getting repo info: %v", err) return anon } private := info.IsPrivate() - collabs := info.Collabs() if pk != nil { - for _, u := range collabs { - if u.IsAdmin() { - return proto.AdminAccess - } - for _, k := range u.PublicKeys() { - if ssh.KeysEqual(pk, k) { - if anon > proto.ReadWriteAccess { - return anon - } - return proto.ReadWriteAccess - } + isAdmin := c.IsAdmin(pk) + if isAdmin { + return proto.AdminAccess + } + isCollab := c.IsCollab(repo, pk) + if isCollab { + if anon > proto.ReadWriteAccess { + return anon } - if !private { - if anon > proto.ReadOnlyAccess { - return anon - } - return proto.ReadOnlyAccess + return proto.ReadWriteAccess + } + if !private { + if anon > proto.ReadOnlyAccess { + return anon } + return proto.ReadOnlyAccess } } // Don't restrict access to private repos if no users are configured. @@ -80,3 +135,21 @@ func (c *Config) countUsers() int { } return count } + +func (c *Config) isInitialAdminKey(key ssh.PublicKey) bool { + for _, k := range c.InitialAdminKeys { + pk, _, _, _, err := ssh.ParseAuthorizedKey([]byte(k)) + if err != nil { + log.Printf("error parsing initial admin key: %v", err) + continue + } + if ssh.KeysEqual(key, pk) { + return true + } + } + return false +} + +func authorizedKey(key ssh.PublicKey) string { + return strings.TrimSpace(string(gossh.MarshalAuthorizedKey(key))) +} diff --git a/server/config/config.go b/server/config/config.go index 0766cce59bf89eb8cc742e54aa808d036cc4e9f2..46e9defb527007a3adf5b8654d12c3a6e3e37f82 100644 --- a/server/config/config.go +++ b/server/config/config.go @@ -7,11 +7,13 @@ import ( "net/url" "os" "path/filepath" + "strings" "github.com/caarlos0/env/v6" "github.com/charmbracelet/soft-serve/proto" "github.com/charmbracelet/soft-serve/server/db" "github.com/charmbracelet/soft-serve/server/db/sqlite" + "github.com/gliderlabs/ssh" ) // Callbacks provides an interface that can be used to run callbacks on different events. @@ -33,7 +35,7 @@ type SSHConfig struct { IdleTimeout int `env:"IDLE_TIMEOUT" envDefault:"300"` } -// GitConfig is the Git protocol configuration for the server. +// GitConfig is the Git daemon configuration for the server. type GitConfig struct { Port int `env:"PORT" envDefault:"9418"` MaxTimeout int `env:"MAX_TIMEOUT" envDefault:"0"` @@ -83,10 +85,11 @@ func (d *DBConfig) URL() *url.URL { // Config is the configuration for Soft Serve. type Config struct { - Host string `env:"HOST" envDefault:"localhost"` - SSH SSHConfig `env:"SSH" envPrefix:"SSH_"` - Git GitConfig `env:"GIT" envPrefix:"GIT_"` - Db DBConfig `env:"DB" envPrefix:"DB_"` + Host string `env:"HOST" envDefault:"localhost"` + + SSH SSHConfig `env:"SSH" envPrefix:"SSH_"` + Git GitConfig `env:"GIT" envPrefix:"GIT_"` + Db DBConfig `env:"DB" envPrefix:"DB_"` ServerName string `env:"SERVER_NAME" envDefault:"Soft Serve"` AnonAccess proto.AccessLevel `env:"ANON_ACCESS" envDefault:"read-only"` @@ -151,11 +154,30 @@ func DefaultConfig() *Config { if migrateWarn { log.Printf("warning: please run `soft serve migrate` to migrate your server and configuration.") } + // initialize admin keys + for i, k := range cfg.InitialAdminKeys { + if bts, err := os.ReadFile(k); err == nil { + // k is a file path, read the file + k = string(bts) + } + pk := strings.TrimSpace(k) + if pk == "" { + // ignore empty keys + continue + } + if _, _, _, _, err := ssh.ParseAuthorizedKey([]byte(k)); err != nil { + // Fatal if the key is invalid + log.Fatalf("invalid initial admin key %q: %v", k, err) + } + // store the key in the config + cfg.InitialAdminKeys[i] = pk + } + log.Printf("initial admin keys are: %v", cfg.InitialAdminKeys) // init data path and db if err := os.MkdirAll(cfg.RepoPath(), 0755); err != nil { log.Fatalln(err) } - if err := cfg.createDefaultConfigRepo(); err != nil { + if err := cfg.createDefaultConfigRepoAndUsers(); err != nil { log.Fatalln(err) } var db db.Store diff --git a/server/config/default.go b/server/config/default.go index a3e9633828471a9259aa87743f81f7f1c339bc77..8ceda6f1fa1360dc618874d0c896d099917f5588 100644 --- a/server/config/default.go +++ b/server/config/default.go @@ -17,7 +17,7 @@ const ( defaultReadme = "# Soft Serve\n\n Welcome! You can configure your Soft Serve server by cloning this repo and pushing changes.\n\n```\ngit clone ssh://localhost:23231/config\n```" ) -func (cfg *Config) createDefaultConfigRepo() error { +func (cfg *Config) createDefaultConfigRepoAndUsers() error { rp := filepath.Join(cfg.RepoPath(), defaultConfigRepo) + ".git" _, err := gogit.PlainOpen(rp) if errors.Is(err, gogit.ErrRepositoryNotExists) { diff --git a/server/config/repo.go b/server/config/repo.go index 2c2010cf18dd3e8a1c7d86bfb329abb6111b2086..effb631d1aef4cd599fd47c7a45afa9811d19901 100644 --- a/server/config/repo.go +++ b/server/config/repo.go @@ -31,6 +31,7 @@ func (c *Config) Open(name string) (proto.Repository, error) { if name == "" { return nil, os.ErrNotExist } + name = strings.TrimSuffix(name, ".git") r, err := git.Open(filepath.Join(c.RepoPath(), name+".git")) if err != nil { log.Printf("error opening repository %q: %v", name, err) @@ -67,6 +68,83 @@ func (c *Config) ListRepos() ([]proto.Metadata, error) { return md, nil } +// Create creates a new repository. +func (c *Config) Create(name string, projectName string, description string, isPrivate bool) error { + name = strings.TrimSuffix(name, ".git") + name = strings.ToLower(name) + if _, err := git.Init(filepath.Join(c.RepoPath(), name+".git"), true); err != nil { + return err + } + if err := c.db.AddRepo(name, projectName, description, isPrivate); err != nil { + return err + } + return nil +} + +// Delete deletes a repository. +func (c *Config) Delete(name string) error { + name = strings.TrimSuffix(name, ".git") + if err := os.RemoveAll(filepath.Join(c.RepoPath(), name+".git")); err != nil { + return err + } + if err := c.db.DeleteRepo(name); err != nil { + return err + } + return nil +} + +// Rename renames a repository. +func (c *Config) Rename(name string, newName string) error { + name = strings.TrimSuffix(name, ".git") + newName = strings.TrimSuffix(newName, ".git") + if err := os.Rename(filepath.Join(c.RepoPath(), name+".git"), filepath.Join(c.RepoPath(), newName+".git")); err != nil { + return err + } + if err := c.db.SetRepoName(name, newName); err != nil { + return err + } + return nil +} + +// SetProjectName sets the repository's project name. +func (c *Config) SetProjectName(name string, projectName string) error { + name = strings.TrimSuffix(name, ".git") + if err := c.db.SetRepoProjectName(name, projectName); err != nil { + return err + } + return nil +} + +// SetDescription sets the repository's description. +func (c *Config) SetDescription(name string, description string) error { + name = strings.TrimSuffix(name, ".git") + if err := c.db.SetRepoDescription(name, description); err != nil { + return err + } + return nil +} + +// SetPrivate sets the repository's privacy. +func (c *Config) SetPrivate(name string, isPrivate bool) error { + name = strings.TrimSuffix(name, ".git") + if err := c.db.SetRepoPrivate(name, isPrivate); err != nil { + return err + } + return nil +} + +// SetDefaultBranch sets the repository's default branch. +func (c *Config) SetDefaultBranch(name string, branch string) error { + re, err := c.Open(name) + if err != nil { + return err + } + if _, err = re.Repository().SymbolicRef("HEAD", "refs/heads/"+branch); err != nil { + return err + } + return nil +} + var _ proto.Metadata = emptyMetadata{} type emptyMetadata struct { diff --git a/server/config/user.go b/server/config/user.go index dc703b09b40b9a8d76e013205c249cd0362b19c0..414dccd61751fbeffc9784f3913bf285dcb1d7f3 100644 --- a/server/config/user.go +++ b/server/config/user.go @@ -18,26 +18,44 @@ type user struct { } func (u *user) Name() string { + if u.user == nil { + return "" + } return u.user.Name } func (u *user) Email() *mail.Address { + if u.user == nil { + return nil + } return u.user.Address() } func (u *user) Login() *string { + if u.user == nil { + return nil + } return u.user.Login } func (u *user) Password() *string { + if u.user == nil { + return nil + } return u.user.Password } func (u *user) IsAdmin() bool { + if u.user == nil { + return false + } return u.user.Admin } func (u *user) PublicKeys() []ssh.PublicKey { + if u.user == nil { + return nil + } keys := u.keys if keys == nil || len(keys) == 0 { ks, err := u.cfg.db.GetUserPublicKeys(u.user)