Detailed changes
@@ -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
}
@@ -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.
@@ -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)))
+}
@@ -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
@@ -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) {
@@ -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 {
@@ -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)