user.go

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