From 2366f903b743e11f79eb2a9e25abcf54aff4f0f9 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 a1160d6eb..263885b49 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 fa5cff166..d03410277 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 364f54b3c..a2845d631 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 9c8954de3..35de493ed 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 3f2fe5dab..e6fcc332c 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 +}