@@ -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)
@@ -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 {
@@ -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
},
}
@@ -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 {
@@ -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
+}