1package sqlite
  2
  3import (
  4	"context"
  5	"strings"
  6
  7	"github.com/charmbracelet/soft-serve/server/backend"
  8	"github.com/charmbracelet/soft-serve/server/utils"
  9	"github.com/jmoiron/sqlx"
 10	"golang.org/x/crypto/ssh"
 11)
 12
 13// User represents a user.
 14type User struct {
 15	username string
 16	db       *sqlx.DB
 17}
 18
 19var _ backend.User = (*User)(nil)
 20
 21// IsAdmin returns whether the user is an admin.
 22//
 23// It implements backend.User.
 24func (u *User) IsAdmin() bool {
 25	var admin bool
 26	if err := wrapTx(u.db, context.Background(), func(tx *sqlx.Tx) error {
 27		return tx.Get(&admin, "SELECT admin FROM user WHERE username = ?", u.username)
 28	}); err != nil {
 29		return false
 30	}
 31
 32	return admin
 33}
 34
 35// PublicKeys returns the user's public keys.
 36//
 37// It implements backend.User.
 38func (u *User) PublicKeys() []ssh.PublicKey {
 39	var keys []ssh.PublicKey
 40	if err := wrapTx(u.db, context.Background(), func(tx *sqlx.Tx) error {
 41		var keyStrings []string
 42		if err := tx.Select(&keyStrings, `SELECT public_key
 43			FROM public_key
 44			INNER JOIN user ON user.id = public_key.user_id
 45			WHERE user.username = ?;`, u.username); err != nil {
 46			return err
 47		}
 48
 49		for _, keyString := range keyStrings {
 50			key, _, _, _, err := ssh.ParseAuthorizedKey([]byte(keyString))
 51			if err != nil {
 52				return err
 53			}
 54			keys = append(keys, key)
 55		}
 56
 57		return nil
 58	}); err != nil {
 59		return nil
 60	}
 61
 62	return keys
 63}
 64
 65// Username returns the user's username.
 66//
 67// It implements backend.User.
 68func (u *User) Username() string {
 69	return u.username
 70}
 71
 72// AccessLevel returns the access level of a user for a repository.
 73//
 74// It implements backend.Backend.
 75func (d *SqliteBackend) AccessLevel(repo string, username string) backend.AccessLevel {
 76	anon := d.AnonAccess()
 77	user, _ := d.User(username)
 78	// If the user is an admin, they have admin access.
 79	if user != nil && user.IsAdmin() {
 80		return backend.AdminAccess
 81	}
 82
 83	// If the repository exists, check if the user is a collaborator.
 84	r, _ := d.Repository(repo)
 85	if r != nil {
 86		// If the user is a collaborator, they have read/write access.
 87		isCollab, _ := d.IsCollaborator(repo, username)
 88		if isCollab {
 89			if anon > backend.ReadWriteAccess {
 90				return anon
 91			}
 92			return backend.ReadWriteAccess
 93		}
 94
 95		// If the repository is private, the user has no access.
 96		if r.IsPrivate() {
 97			return backend.NoAccess
 98		}
 99
100		// Otherwise, the user has read-only access.
101		return backend.ReadOnlyAccess
102	}
103
104	if user != nil {
105		// If the repository doesn't exist, the user has read/write access.
106		if anon > backend.ReadWriteAccess {
107			return anon
108		}
109
110		return backend.ReadWriteAccess
111	}
112
113	// If the user doesn't exist, give them the anonymous access level.
114	return anon
115}
116
117// AccessLevelByPublicKey returns the access level of a user's public key for a repository.
118//
119// It implements backend.Backend.
120func (d *SqliteBackend) AccessLevelByPublicKey(repo string, pk ssh.PublicKey) backend.AccessLevel {
121	for _, k := range d.cfg.AdminKeys() {
122		if backend.KeysEqual(pk, k) {
123			return backend.AdminAccess
124		}
125	}
126
127	user, _ := d.UserByPublicKey(pk)
128	if user != nil {
129		return d.AccessLevel(repo, user.Username())
130	}
131
132	return d.AccessLevel(repo, "")
133}
134
135// AddPublicKey adds a public key to a user.
136//
137// It implements backend.Backend.
138func (d *SqliteBackend) AddPublicKey(username string, pk ssh.PublicKey) error {
139	username = strings.ToLower(username)
140	if err := utils.ValidateUsername(username); err != nil {
141		return err
142	}
143
144	return wrapDbErr(
145		wrapTx(d.db, context.Background(), func(tx *sqlx.Tx) error {
146			var userID int
147			if err := tx.Get(&userID, "SELECT id FROM user WHERE username = ?", username); err != nil {
148				return err
149			}
150
151			_, err := tx.Exec(`INSERT INTO public_key (user_id, public_key, updated_at)
152			VALUES (?, ?, CURRENT_TIMESTAMP);`, userID, backend.MarshalAuthorizedKey(pk))
153			return err
154		}),
155	)
156}
157
158// CreateUser creates a new user.
159//
160// It implements backend.Backend.
161func (d *SqliteBackend) CreateUser(username string, opts backend.UserOptions) (backend.User, error) {
162	username = strings.ToLower(username)
163	if err := utils.ValidateUsername(username); err != nil {
164		return nil, err
165	}
166
167	var user *User
168	if err := wrapTx(d.db, context.Background(), func(tx *sqlx.Tx) error {
169		stmt, err := tx.Prepare("INSERT INTO user (username, admin, updated_at) VALUES (?, ?, CURRENT_TIMESTAMP);")
170		if err != nil {
171			return err
172		}
173
174		defer stmt.Close() // nolint: errcheck
175		r, err := stmt.Exec(username, opts.Admin)
176		if err != nil {
177			return err
178		}
179
180		if len(opts.PublicKeys) > 0 {
181			userID, err := r.LastInsertId()
182			if err != nil {
183				d.logger.Error("error getting last insert id")
184				return err
185			}
186
187			for _, pk := range opts.PublicKeys {
188				stmt, err := tx.Prepare(`INSERT INTO public_key (user_id, public_key, updated_at)
189					VALUES (?, ?, CURRENT_TIMESTAMP);`)
190				if err != nil {
191					return err
192				}
193
194				defer stmt.Close() // nolint: errcheck
195				if _, err := stmt.Exec(userID, backend.MarshalAuthorizedKey(pk)); err != nil {
196					return err
197				}
198			}
199		}
200
201		user = &User{
202			db:       d.db,
203			username: username,
204		}
205		return nil
206	}); err != nil {
207		return nil, wrapDbErr(err)
208	}
209
210	return user, nil
211}
212
213// DeleteUser deletes a user.
214//
215// It implements backend.Backend.
216func (d *SqliteBackend) DeleteUser(username string) error {
217	username = strings.ToLower(username)
218	if err := utils.ValidateUsername(username); err != nil {
219		return err
220	}
221
222	return wrapDbErr(
223		wrapTx(d.db, context.Background(), func(tx *sqlx.Tx) error {
224			_, err := tx.Exec("DELETE FROM user WHERE username = ?", username)
225			return err
226		}),
227	)
228}
229
230// RemovePublicKey removes a public key from a user.
231//
232// It implements backend.Backend.
233func (d *SqliteBackend) RemovePublicKey(username string, pk ssh.PublicKey) error {
234	return wrapDbErr(
235		wrapTx(d.db, context.Background(), func(tx *sqlx.Tx) error {
236			_, err := tx.Exec(`DELETE FROM public_key
237			WHERE user_id = (SELECT id FROM user WHERE username = ?)
238			AND public_key = ?;`, username, backend.MarshalAuthorizedKey(pk))
239			return err
240		}),
241	)
242}
243
244// ListPublicKeys lists the public keys of a user.
245func (d *SqliteBackend) ListPublicKeys(username string) ([]ssh.PublicKey, error) {
246	username = strings.ToLower(username)
247	if err := utils.ValidateUsername(username); err != nil {
248		return nil, err
249	}
250
251	keys := make([]ssh.PublicKey, 0)
252	if err := wrapTx(d.db, context.Background(), func(tx *sqlx.Tx) error {
253		var keyStrings []string
254		if err := tx.Select(&keyStrings, `SELECT public_key
255			FROM public_key
256			INNER JOIN user ON user.id = public_key.user_id
257			WHERE user.username = ?;`, username); err != nil {
258			return err
259		}
260
261		for _, keyString := range keyStrings {
262			key, _, _, _, err := ssh.ParseAuthorizedKey([]byte(keyString))
263			if err != nil {
264				return err
265			}
266			keys = append(keys, key)
267		}
268
269		return nil
270	}); err != nil {
271		return nil, wrapDbErr(err)
272	}
273
274	return keys, nil
275}
276
277// SetUsername sets the username of a user.
278//
279// It implements backend.Backend.
280func (d *SqliteBackend) SetUsername(username string, newUsername string) error {
281	username = strings.ToLower(username)
282	if err := utils.ValidateUsername(username); err != nil {
283		return err
284	}
285
286	return wrapDbErr(
287		wrapTx(d.db, context.Background(), func(tx *sqlx.Tx) error {
288			_, err := tx.Exec("UPDATE user SET username = ? WHERE username = ?", newUsername, username)
289			return err
290		}),
291	)
292}
293
294// SetAdmin sets the admin flag of a user.
295//
296// It implements backend.Backend.
297func (d *SqliteBackend) SetAdmin(username string, admin bool) error {
298	username = strings.ToLower(username)
299	if err := utils.ValidateUsername(username); err != nil {
300		return err
301	}
302
303	return wrapDbErr(
304		wrapTx(d.db, context.Background(), func(tx *sqlx.Tx) error {
305			_, err := tx.Exec("UPDATE user SET admin = ? WHERE username = ?", admin, username)
306			return err
307		}),
308	)
309}
310
311// User finds a user by username.
312//
313// It implements backend.Backend.
314func (d *SqliteBackend) User(username string) (backend.User, error) {
315	username = strings.ToLower(username)
316	if err := utils.ValidateUsername(username); err != nil {
317		return nil, err
318	}
319
320	if err := wrapTx(d.db, context.Background(), func(tx *sqlx.Tx) error {
321		return tx.Get(&username, "SELECT username FROM user WHERE username = ?", username)
322	}); err != nil {
323		return nil, wrapDbErr(err)
324	}
325
326	return &User{
327		db:       d.db,
328		username: username,
329	}, nil
330}
331
332// UserByPublicKey finds a user by public key.
333//
334// It implements backend.Backend.
335func (d *SqliteBackend) UserByPublicKey(pk ssh.PublicKey) (backend.User, error) {
336	var username string
337	if err := wrapTx(d.db, context.Background(), func(tx *sqlx.Tx) error {
338		return tx.Get(&username, `SELECT user.username
339			FROM public_key
340			INNER JOIN user ON user.id = public_key.user_id
341			WHERE public_key.public_key = ?;`, backend.MarshalAuthorizedKey(pk))
342	}); err != nil {
343		return nil, wrapDbErr(err)
344	}
345
346	return &User{
347		db:       d.db,
348		username: username,
349	}, nil
350}
351
352// Users returns all users.
353//
354// It implements backend.Backend.
355func (d *SqliteBackend) Users() ([]string, error) {
356	var users []string
357	if err := wrapTx(d.db, context.Background(), func(tx *sqlx.Tx) error {
358		return tx.Select(&users, "SELECT username FROM user")
359	}); err != nil {
360		return nil, wrapDbErr(err)
361	}
362
363	return users, nil
364}