2366f903b743e11f79eb2a9e25abcf54aff4f0f9

Author
Ayman Bagabas <ayman.bagabas@gmail.com>
Committer
Ayman Bagabas <ayman.bagabas@gmail.com>
Date

Message

feat(server): validate username

Diff

This diff is truncated to protect this page.

  1diff --git a/server/backend/sqlite/sqlite.go b/server/backend/sqlite/sqlite.go
  2index a1160d6ebf421bd6972e04a19e140766f425fa57..263885b492c9cb28a2e699de2d4cd0493011a7cb 100644
  3--- a/server/backend/sqlite/sqlite.go
  4+++ b/server/backend/sqlite/sqlite.go
  5@@ -7,6 +7,7 @@ import (
  6 	"os"
  7 	"path/filepath"
  8 	"strconv"
  9+	"strings"
 10 	"text/template"
 11 
 12 	"github.com/charmbracelet/log"
 13@@ -410,6 +411,11 @@ func (d *SqliteBackend) SetProjectName(repo string, name string) error {
 14 //
 15 // It implements backend.Backend.
 16 func (d *SqliteBackend) AddCollaborator(repo string, username string) error {
 17+	username = strings.ToLower(username)
 18+	if err := utils.ValidateUsername(username); err != nil {
 19+		return err
 20+	}
 21+
 22 	repo = utils.SanitizeRepo(repo)
 23 	return wrapDbErr(wrapTx(d.db, context.Background(), func(tx *sqlx.Tx) error {
 24 		_, err := tx.Exec(`INSERT INTO collab (user_id, repo_id, updated_at)
 25diff --git a/server/backend/sqlite/user.go b/server/backend/sqlite/user.go
 26index fa5cff166f27e8bea24772942594b9c8d2691f10..d03410277ae45f60457eb48521198ff34f3646a5 100644
 27--- a/server/backend/sqlite/user.go
 28+++ b/server/backend/sqlite/user.go
 29@@ -2,8 +2,10 @@ package sqlite
 30 
 31 import (
 32 	"context"
 33+	"strings"
 34 
 35 	"github.com/charmbracelet/soft-serve/server/backend"
 36+	"github.com/charmbracelet/soft-serve/server/utils"
 37 	"github.com/jmoiron/sqlx"
 38 	"golang.org/x/crypto/ssh"
 39 )
 40@@ -136,6 +138,11 @@ func (d *SqliteBackend) AccessLevelByPublicKey(repo string, pk ssh.PublicKey) ba
 41 //
 42 // It implements backend.Backend.
 43 func (d *SqliteBackend) AddPublicKey(username string, pk ssh.PublicKey) error {
 44+	username = strings.ToLower(username)
 45+	if err := utils.ValidateUsername(username); err != nil {
 46+		return err
 47+	}
 48+
 49 	return wrapDbErr(
 50 		wrapTx(d.db, context.Background(), func(tx *sqlx.Tx) error {
 51 			var userID int
 52@@ -154,16 +161,16 @@ func (d *SqliteBackend) AddPublicKey(username string, pk ssh.PublicKey) error {
 53 //
 54 // It implements backend.Backend.
 55 func (d *SqliteBackend) CreateUser(username string, opts backend.UserOptions) (backend.User, error) {
 56+	username = strings.ToLower(username)
 57+	if err := utils.ValidateUsername(username); err != nil {
 58+		return nil, err
 59+	}
 60+
 61 	var user *User
 62 	if err := wrapTx(d.db, context.Background(), func(tx *sqlx.Tx) error {
 63-		into := "INSERT INTO user (username"
 64-		values := "VALUES (?"
 65-		args := []interface{}{username}
 66-		if opts.Admin {
 67-			into += ", admin"
 68-			values += ", ?"
 69-			args = append(args, opts.Admin)
 70-		}
 71+		into := "INSERT INTO user (username, admin"
 72+		values := "VALUES (?, ?"
 73+		args := []interface{}{username, opts.Admin}
 74 		into += ", updated_at)"
 75 		values += ", CURRENT_TIMESTAMP)"
 76 
 77@@ -202,6 +209,11 @@ func (d *SqliteBackend) CreateUser(username string, opts backend.UserOptions) (b
 78 //
 79 // It implements backend.Backend.
 80 func (d *SqliteBackend) DeleteUser(username string) error {
 81+	username = strings.ToLower(username)
 82+	if err := utils.ValidateUsername(username); err != nil {
 83+		return err
 84+	}
 85+
 86 	return wrapDbErr(
 87 		wrapTx(d.db, context.Background(), func(tx *sqlx.Tx) error {
 88 			_, err := tx.Exec("DELETE FROM user WHERE username = ?", username)
 89@@ -226,6 +238,11 @@ func (d *SqliteBackend) RemovePublicKey(username string, pk ssh.PublicKey) error
 90 
 91 // ListPublicKeys lists the public keys of a user.
 92 func (d *SqliteBackend) ListPublicKeys(username string) ([]ssh.PublicKey, error) {
 93+	username = strings.ToLower(username)
 94+	if err := utils.ValidateUsername(username); err != nil {
 95+		return nil, err
 96+	}
 97+
 98 	keys := make([]ssh.PublicKey, 0)
 99 	if err := wrapTx(d.db, context.Background(), func(tx *sqlx.Tx) error {
100 		var keyStrings []string
101@@ -256,6 +273,11 @@ func (d *SqliteBackend) ListPublicKeys(username string) ([]ssh.PublicKey, error)
102 //
103 // It implements backend.Backend.
104 func (d *SqliteBackend) SetUsername(username string, newUsername string) error {
105+	username = strings.ToLower(username)
106+	if err := utils.ValidateUsername(username); err != nil {
107+		return err
108+	}
109+
110 	return wrapDbErr(
111 		wrapTx(d.db, context.Background(), func(tx *sqlx.Tx) error {
112 			_, err := tx.Exec("UPDATE user SET username = ? WHERE username = ?", newUsername, username)
113@@ -268,6 +290,11 @@ func (d *SqliteBackend) SetUsername(username string, newUsername string) error {
114 //
115 // It implements backend.Backend.
116 func (d *SqliteBackend) SetAdmin(username string, admin bool) error {
117+	username = strings.ToLower(username)
118+	if err := utils.ValidateUsername(username); err != nil {
119+		return err
120+	}
121+
122 	return wrapDbErr(
123 		wrapTx(d.db, context.Background(), func(tx *sqlx.Tx) error {
124 			_, err := tx.Exec("UPDATE user SET admin = ? WHERE username = ?", admin, username)
125@@ -280,6 +307,11 @@ func (d *SqliteBackend) SetAdmin(username string, admin bool) error {
126 //
127 // It implements backend.Backend.
128 func (d *SqliteBackend) User(username string) (backend.User, error) {
129diff --git a/server/cmd/user.go b/server/cmd/user.go
130index 364f54b3c7c7ac0937523c49ca04e017e123671e..a2845d6311e378060b6568d6b9eb13d13e353a0c 100644
131--- a/server/cmd/user.go
132+++ b/server/cmd/user.go
133@@ -25,19 +25,24 @@ func userCommand() *cobra.Command {
134 		Args:              cobra.ExactArgs(1),
135 		PersistentPreRunE: checkIfAdmin,
136 		RunE: func(cmd *cobra.Command, args []string) error {
137+			var pubkeys []ssh.PublicKey
138 			cfg, _ := fromContext(cmd)
139 			username := args[0]
140-			pk, _, err := backend.ParseAuthorizedKey(key)
141-			if err != nil {
142-				return err
143+			if key != "" {
144+				pk, _, err := backend.ParseAuthorizedKey(key)
145+				if err != nil {
146+					return err
147+				}
148+
149+				pubkeys = []ssh.PublicKey{pk}
150 			}
151 
152 			opts := backend.UserOptions{
153 				Admin:      admin,
154-				PublicKeys: []ssh.PublicKey{pk},
155+				PublicKeys: pubkeys,
156 			}
157 
158-			_, err = cfg.Backend.CreateUser(username, opts)
159+			_, err := cfg.Backend.CreateUser(username, opts)
160 			return err
161 		},
162 	}
163diff --git a/server/config/config.go b/server/config/config.go
164index 9c8954de383fb36c1d13b77979a13256b69c510f..35de493ed86353b3385eb14d4e008b5013a651a6 100644
165--- a/server/config/config.go
166+++ b/server/config/config.go
167@@ -109,6 +109,11 @@ func ParseConfig(path string) (*Config, error) {
168 	return cfg, nil
169 }
170 
171+// WriteConfig writes the configuration to the given file.
172+func WriteConfig(path string, cfg *Config) error {
173+	return os.WriteFile(path, []byte(newConfigFile(cfg)), 0o600) // nolint: errcheck
174+}
175+
176 // DefaultConfig returns a Config with the values populated with the defaults
177 // or specified environment variables.
178 func DefaultConfig() *Config {
179diff --git a/server/utils/utils.go b/server/utils/utils.go
180index 3f2fe5dabe16b0daa646a56d1bf3e01c0b5572cb..e6fcc332c611233124cb7692691d0d63dbd23bd2 100644
181--- a/server/utils/utils.go
182+++ b/server/utils/utils.go
183@@ -1,8 +1,10 @@
184 package utils
185 
186 import (
187+	"fmt"
188 	"path/filepath"
189 	"strings"
190+	"unicode"
191 )
192 
193 // SanitizeRepo returns a sanitized version of the given repository name.
194@@ -12,3 +14,22 @@ func SanitizeRepo(repo string) string {
195 	repo = strings.TrimSuffix(repo, ".git")
196 	return repo
197 }
198+
199+// ValidateUsername returns an error if any of the given usernames are invalid.
200+func ValidateUsername(username string) error {
201+	if username == "" {
202+		return fmt.Errorf("username cannot be empty")
203+	}
204+
205+	if !unicode.IsLetter(rune(username[0])) {
206+		return fmt.Errorf("username must start with a letter")
207+	}
208+
209+	for _, r := range username {
210+		if !unicode.IsLetter(r) && !unicode.IsDigit(r) && r != '-' {
211+			return fmt.Errorf("username can only contain letters, numbers, and hyphens")
212+		}
213+	}
214+
215+	return nil
216+}