From 57ffff93346cf3b9e6de60e2abd4e53da1008f09 Mon Sep 17 00:00:00 2001 From: Ayman Bagabas Date: Mon, 3 Apr 2023 14:30:54 -0400 Subject: [PATCH] feat(server): validate username --- server/backend/sqlite/sqlite.go | 6 +++++ server/backend/sqlite/user.go | 48 +++++++++++++++++++++++++++------ server/cmd/user.go | 15 +++++++---- server/config/config.go | 5 ++++ server/utils/utils.go | 21 +++++++++++++++ 5 files changed, 82 insertions(+), 13 deletions(-) diff --git a/server/backend/sqlite/sqlite.go b/server/backend/sqlite/sqlite.go index a1160d6ebf421bd6972e04a19e140766f425fa57..263885b492c9cb28a2e699de2d4cd0493011a7cb 100644 --- a/server/backend/sqlite/sqlite.go +++ b/server/backend/sqlite/sqlite.go @@ -7,6 +7,7 @@ import ( "os" "path/filepath" "strconv" + "strings" "text/template" "github.com/charmbracelet/log" @@ -410,6 +411,11 @@ func (d *SqliteBackend) SetProjectName(repo string, name string) error { // // It implements backend.Backend. func (d *SqliteBackend) AddCollaborator(repo string, username string) error { + username = strings.ToLower(username) + if err := utils.ValidateUsername(username); err != nil { + return err + } + repo = utils.SanitizeRepo(repo) return wrapDbErr(wrapTx(d.db, context.Background(), func(tx *sqlx.Tx) error { _, err := tx.Exec(`INSERT INTO collab (user_id, repo_id, updated_at) diff --git a/server/backend/sqlite/user.go b/server/backend/sqlite/user.go index fa5cff166f27e8bea24772942594b9c8d2691f10..d03410277ae45f60457eb48521198ff34f3646a5 100644 --- a/server/backend/sqlite/user.go +++ b/server/backend/sqlite/user.go @@ -2,8 +2,10 @@ package sqlite import ( "context" + "strings" "github.com/charmbracelet/soft-serve/server/backend" + "github.com/charmbracelet/soft-serve/server/utils" "github.com/jmoiron/sqlx" "golang.org/x/crypto/ssh" ) @@ -136,6 +138,11 @@ func (d *SqliteBackend) AccessLevelByPublicKey(repo string, pk ssh.PublicKey) ba // // It implements backend.Backend. func (d *SqliteBackend) AddPublicKey(username string, pk ssh.PublicKey) error { + username = strings.ToLower(username) + if err := utils.ValidateUsername(username); err != nil { + return err + } + return wrapDbErr( wrapTx(d.db, context.Background(), func(tx *sqlx.Tx) error { var userID int @@ -154,16 +161,16 @@ func (d *SqliteBackend) AddPublicKey(username string, pk ssh.PublicKey) error { // // It implements backend.Backend. func (d *SqliteBackend) CreateUser(username string, opts backend.UserOptions) (backend.User, error) { + username = strings.ToLower(username) + if err := utils.ValidateUsername(username); err != nil { + return nil, err + } + var user *User if err := wrapTx(d.db, context.Background(), func(tx *sqlx.Tx) error { - into := "INSERT INTO user (username" - values := "VALUES (?" - args := []interface{}{username} - if opts.Admin { - into += ", admin" - values += ", ?" - args = append(args, opts.Admin) - } + into := "INSERT INTO user (username, admin" + values := "VALUES (?, ?" + args := []interface{}{username, opts.Admin} into += ", updated_at)" values += ", CURRENT_TIMESTAMP)" @@ -202,6 +209,11 @@ func (d *SqliteBackend) CreateUser(username string, opts backend.UserOptions) (b // // It implements backend.Backend. func (d *SqliteBackend) DeleteUser(username string) error { + username = strings.ToLower(username) + if err := utils.ValidateUsername(username); err != nil { + return err + } + return wrapDbErr( wrapTx(d.db, context.Background(), func(tx *sqlx.Tx) error { _, err := tx.Exec("DELETE FROM user WHERE username = ?", username) @@ -226,6 +238,11 @@ func (d *SqliteBackend) RemovePublicKey(username string, pk ssh.PublicKey) error // ListPublicKeys lists the public keys of a user. func (d *SqliteBackend) ListPublicKeys(username string) ([]ssh.PublicKey, error) { + username = strings.ToLower(username) + if err := utils.ValidateUsername(username); err != nil { + return nil, err + } + keys := make([]ssh.PublicKey, 0) if err := wrapTx(d.db, context.Background(), func(tx *sqlx.Tx) error { var keyStrings []string @@ -256,6 +273,11 @@ func (d *SqliteBackend) ListPublicKeys(username string) ([]ssh.PublicKey, error) // // It implements backend.Backend. func (d *SqliteBackend) SetUsername(username string, newUsername string) error { + username = strings.ToLower(username) + if err := utils.ValidateUsername(username); err != nil { + return err + } + return wrapDbErr( wrapTx(d.db, context.Background(), func(tx *sqlx.Tx) error { _, err := tx.Exec("UPDATE user SET username = ? WHERE username = ?", newUsername, username) @@ -268,6 +290,11 @@ func (d *SqliteBackend) SetUsername(username string, newUsername string) error { // // It implements backend.Backend. func (d *SqliteBackend) SetAdmin(username string, admin bool) error { + username = strings.ToLower(username) + if err := utils.ValidateUsername(username); err != nil { + return err + } + return wrapDbErr( wrapTx(d.db, context.Background(), func(tx *sqlx.Tx) error { _, err := tx.Exec("UPDATE user SET admin = ? WHERE username = ?", admin, username) @@ -280,6 +307,11 @@ func (d *SqliteBackend) SetAdmin(username string, admin bool) error { // // It implements backend.Backend. func (d *SqliteBackend) User(username string) (backend.User, error) { + username = strings.ToLower(username) + if err := utils.ValidateUsername(username); err != nil { + return nil, err + } + if err := wrapTx(d.db, context.Background(), func(tx *sqlx.Tx) error { return tx.Get(&username, "SELECT username FROM user WHERE username = ?", username) }); err != nil { diff --git a/server/cmd/user.go b/server/cmd/user.go index 364f54b3c7c7ac0937523c49ca04e017e123671e..a2845d6311e378060b6568d6b9eb13d13e353a0c 100644 --- a/server/cmd/user.go +++ b/server/cmd/user.go @@ -25,19 +25,24 @@ func userCommand() *cobra.Command { Args: cobra.ExactArgs(1), PersistentPreRunE: checkIfAdmin, RunE: func(cmd *cobra.Command, args []string) error { + var pubkeys []ssh.PublicKey cfg, _ := fromContext(cmd) username := args[0] - pk, _, err := backend.ParseAuthorizedKey(key) - if err != nil { - return err + if key != "" { + pk, _, err := backend.ParseAuthorizedKey(key) + if err != nil { + return err + } + + pubkeys = []ssh.PublicKey{pk} } opts := backend.UserOptions{ Admin: admin, - PublicKeys: []ssh.PublicKey{pk}, + PublicKeys: pubkeys, } - _, err = cfg.Backend.CreateUser(username, opts) + _, err := cfg.Backend.CreateUser(username, opts) return err }, } diff --git a/server/config/config.go b/server/config/config.go index 9c8954de383fb36c1d13b77979a13256b69c510f..35de493ed86353b3385eb14d4e008b5013a651a6 100644 --- a/server/config/config.go +++ b/server/config/config.go @@ -109,6 +109,11 @@ func ParseConfig(path string) (*Config, error) { return cfg, nil } +// WriteConfig writes the configuration to the given file. +func WriteConfig(path string, cfg *Config) error { + return os.WriteFile(path, []byte(newConfigFile(cfg)), 0o600) // nolint: errcheck +} + // DefaultConfig returns a Config with the values populated with the defaults // or specified environment variables. func DefaultConfig() *Config { diff --git a/server/utils/utils.go b/server/utils/utils.go index 3f2fe5dabe16b0daa646a56d1bf3e01c0b5572cb..e6fcc332c611233124cb7692691d0d63dbd23bd2 100644 --- a/server/utils/utils.go +++ b/server/utils/utils.go @@ -1,8 +1,10 @@ package utils import ( + "fmt" "path/filepath" "strings" + "unicode" ) // SanitizeRepo returns a sanitized version of the given repository name. @@ -12,3 +14,22 @@ func SanitizeRepo(repo string) string { repo = strings.TrimSuffix(repo, ".git") return repo } + +// ValidateUsername returns an error if any of the given usernames are invalid. +func ValidateUsername(username string) error { + if username == "" { + return fmt.Errorf("username cannot be empty") + } + + if !unicode.IsLetter(rune(username[0])) { + return fmt.Errorf("username must start with a letter") + } + + for _, r := range username { + if !unicode.IsLetter(r) && !unicode.IsDigit(r) && r != '-' { + return fmt.Errorf("username can only contain letters, numbers, and hyphens") + } + } + + return nil +}