user.go

  1package backend
  2
  3import (
  4	"context"
  5	"errors"
  6	"strings"
  7	"time"
  8
  9	"github.com/charmbracelet/soft-serve/pkg/db"
 10	"github.com/charmbracelet/soft-serve/pkg/db/models"
 11	"github.com/charmbracelet/soft-serve/pkg/proto"
 12	"github.com/charmbracelet/soft-serve/pkg/sshutils"
 13	"github.com/charmbracelet/soft-serve/pkg/utils"
 14	"golang.org/x/crypto/ssh"
 15)
 16
 17// User finds a user by username.
 18//
 19// It implements backend.Backend.
 20func (d *Backend) User(ctx context.Context, username string) (proto.User, error) {
 21	username = strings.ToLower(username)
 22	if err := utils.ValidateHandle(username); err != nil {
 23		return nil, err
 24	}
 25
 26	var m models.User
 27	var pks []ssh.PublicKey
 28	var hl models.Handle
 29	if err := d.db.TransactionContext(ctx, func(tx *db.Tx) error {
 30		var err error
 31		m, err = d.store.FindUserByUsername(ctx, tx, username)
 32		if err != nil {
 33			return err
 34		}
 35
 36		pks, err = d.store.ListPublicKeysByUserID(ctx, tx, m.ID)
 37		if err != nil {
 38			return err
 39		}
 40
 41		hl, err = d.store.GetHandleByUserID(ctx, tx, m.ID)
 42		return err
 43	}); err != nil {
 44		err = db.WrapError(err)
 45		if errors.Is(err, db.ErrRecordNotFound) {
 46			return nil, proto.ErrUserNotFound
 47		}
 48		d.logger.Error("error finding user", "username", username, "error", err)
 49		return nil, err
 50	}
 51
 52	return &user{
 53		user:       m,
 54		publicKeys: pks,
 55		handle:     hl,
 56	}, nil
 57}
 58
 59// UserByID finds a user by ID.
 60func (d *Backend) UserByID(ctx context.Context, id int64) (proto.User, error) {
 61	var m models.User
 62	var pks []ssh.PublicKey
 63	var hl models.Handle
 64	if err := d.db.TransactionContext(ctx, func(tx *db.Tx) error {
 65		var err error
 66		m, err = d.store.GetUserByID(ctx, tx, id)
 67		if err != nil {
 68			return err
 69		}
 70
 71		pks, err = d.store.ListPublicKeysByUserID(ctx, tx, m.ID)
 72		if err != nil {
 73			return err
 74		}
 75
 76		hl, err = d.store.GetHandleByUserID(ctx, tx, m.ID)
 77		return err
 78	}); err != nil {
 79		err = db.WrapError(err)
 80		if errors.Is(err, db.ErrRecordNotFound) {
 81			return nil, proto.ErrUserNotFound
 82		}
 83		d.logger.Error("error finding user", "id", id, "error", err)
 84		return nil, err
 85	}
 86
 87	return &user{
 88		user:       m,
 89		publicKeys: pks,
 90		handle:     hl,
 91	}, nil
 92}
 93
 94// UserByPublicKey finds a user by public key.
 95//
 96// It implements backend.Backend.
 97func (d *Backend) UserByPublicKey(ctx context.Context, pk ssh.PublicKey) (proto.User, error) {
 98	var m models.User
 99	var pks []ssh.PublicKey
100	var hl models.Handle
101	if err := d.db.TransactionContext(ctx, func(tx *db.Tx) error {
102		var err error
103		m, err = d.store.FindUserByPublicKey(ctx, tx, pk)
104		if err != nil {
105			return db.WrapError(err)
106		}
107
108		pks, err = d.store.ListPublicKeysByUserID(ctx, tx, m.ID)
109		if err != nil {
110			return err
111		}
112
113		hl, err = d.store.GetHandleByUserID(ctx, tx, m.ID)
114		return err
115	}); err != nil {
116		err = db.WrapError(err)
117		if errors.Is(err, db.ErrRecordNotFound) {
118			return nil, proto.ErrUserNotFound
119		}
120		d.logger.Error("error finding user", "pk", sshutils.MarshalAuthorizedKey(pk), "error", err)
121		return nil, err
122	}
123
124	return &user{
125		user:       m,
126		publicKeys: pks,
127		handle:     hl,
128	}, nil
129}
130
131// UserByAccessToken finds a user by access token.
132// This also validates the token for expiration and returns proto.ErrTokenExpired.
133func (d *Backend) UserByAccessToken(ctx context.Context, token string) (proto.User, error) {
134	var m models.User
135	var pks []ssh.PublicKey
136	var hl models.Handle
137	token = HashToken(token)
138
139	if err := d.db.TransactionContext(ctx, func(tx *db.Tx) error {
140		t, err := d.store.GetAccessTokenByToken(ctx, tx, token)
141		if err != nil {
142			return db.WrapError(err)
143		}
144
145		if t.ExpiresAt.Valid && t.ExpiresAt.Time.Before(time.Now()) {
146			return proto.ErrTokenExpired
147		}
148
149		m, err = d.store.FindUserByAccessToken(ctx, tx, token)
150		if err != nil {
151			return db.WrapError(err)
152		}
153
154		pks, err = d.store.ListPublicKeysByUserID(ctx, tx, m.ID)
155		if err != nil {
156			return err
157		}
158
159		hl, err = d.store.GetHandleByUserID(ctx, tx, m.ID)
160		return err
161	}); err != nil {
162		err = db.WrapError(err)
163		if errors.Is(err, db.ErrRecordNotFound) {
164			return nil, proto.ErrUserNotFound
165		}
166		d.logger.Error("failed to find user by access token", "err", err, "token", token)
167		return nil, err
168	}
169
170	return &user{
171		user:       m,
172		publicKeys: pks,
173		handle:     hl,
174	}, nil
175}
176
177// Users returns all users.
178//
179// It implements backend.Backend.
180func (d *Backend) Users(ctx context.Context) ([]string, error) {
181	var users []string
182	if err := d.db.TransactionContext(ctx, func(tx *db.Tx) error {
183		ms, err := d.store.GetAllUsers(ctx, tx)
184		if err != nil {
185			return err
186		}
187
188		ids := make([]int64, len(ms))
189		for i, m := range ms {
190			ids[i] = m.ID
191		}
192
193		handles, err := d.store.ListHandlesForIDs(ctx, tx, ids)
194		if err != nil {
195			return err
196		}
197
198		for _, h := range handles {
199			users = append(users, h.Handle)
200		}
201
202		return nil
203	}); err != nil {
204		return nil, db.WrapError(err)
205	}
206
207	return users, nil
208}
209
210// AddPublicKey adds a public key to a user.
211//
212// It implements backend.Backend.
213func (d *Backend) AddPublicKey(ctx context.Context, username string, pk ssh.PublicKey) error {
214	username = strings.ToLower(username)
215	if err := utils.ValidateHandle(username); err != nil {
216		return err
217	}
218
219	return db.WrapError(
220		d.db.TransactionContext(ctx, func(tx *db.Tx) error {
221			return d.store.AddPublicKeyByUsername(ctx, tx, username, pk)
222		}),
223	)
224}
225
226// CreateUser creates a new user.
227//
228// It implements backend.Backend.
229func (d *Backend) CreateUser(ctx context.Context, username string, opts proto.UserOptions) (proto.User, error) {
230	if err := d.db.TransactionContext(ctx, func(tx *db.Tx) error {
231		return d.store.CreateUser(ctx, tx, username, opts.Admin, opts.PublicKeys)
232	}); err != nil {
233		return nil, db.WrapError(err)
234	}
235
236	return d.User(ctx, username)
237}
238
239// DeleteUser deletes a user.
240//
241// It implements backend.Backend.
242func (d *Backend) DeleteUser(ctx context.Context, username string) error {
243	username = strings.ToLower(username)
244	if err := utils.ValidateHandle(username); err != nil {
245		return err
246	}
247
248	return d.db.TransactionContext(ctx, func(tx *db.Tx) error {
249		if err := d.store.DeleteUserByUsername(ctx, tx, username); err != nil {
250			return db.WrapError(err)
251		}
252
253		return d.DeleteUserRepositories(ctx, username)
254	})
255}
256
257// RemovePublicKey removes a public key from a user.
258//
259// It implements backend.Backend.
260func (d *Backend) RemovePublicKey(ctx context.Context, username string, pk ssh.PublicKey) error {
261	return db.WrapError(
262		d.db.TransactionContext(ctx, func(tx *db.Tx) error {
263			return d.store.RemovePublicKeyByUsername(ctx, tx, username, pk)
264		}),
265	)
266}
267
268// ListPublicKeys lists the public keys of a user.
269func (d *Backend) ListPublicKeys(ctx context.Context, username string) ([]ssh.PublicKey, error) {
270	username = strings.ToLower(username)
271	if err := utils.ValidateHandle(username); err != nil {
272		return nil, err
273	}
274
275	var keys []ssh.PublicKey
276	if err := d.db.TransactionContext(ctx, func(tx *db.Tx) error {
277		var err error
278		keys, err = d.store.ListPublicKeysByUsername(ctx, tx, username)
279		return err
280	}); err != nil {
281		return nil, db.WrapError(err)
282	}
283
284	return keys, nil
285}
286
287// SetUsername sets the username of a user.
288//
289// It implements backend.Backend.
290func (d *Backend) SetUsername(ctx context.Context, username string, newUsername string) error {
291	username = strings.ToLower(username)
292	if err := utils.ValidateHandle(username); err != nil {
293		return err
294	}
295
296	return db.WrapError(
297		d.db.TransactionContext(ctx, func(tx *db.Tx) error {
298			return d.store.SetUsernameByUsername(ctx, tx, username, newUsername)
299		}),
300	)
301}
302
303// SetAdmin sets the admin flag of a user.
304//
305// It implements backend.Backend.
306func (d *Backend) SetAdmin(ctx context.Context, username string, admin bool) error {
307	username = strings.ToLower(username)
308	if err := utils.ValidateHandle(username); err != nil {
309		return err
310	}
311
312	return db.WrapError(
313		d.db.TransactionContext(ctx, func(tx *db.Tx) error {
314			return d.store.SetAdminByUsername(ctx, tx, username, admin)
315		}),
316	)
317}
318
319// SetPassword sets the password of a user.
320func (d *Backend) SetPassword(ctx context.Context, username string, rawPassword string) error {
321	username = strings.ToLower(username)
322	if err := utils.ValidateHandle(username); err != nil {
323		return err
324	}
325
326	password, err := HashPassword(rawPassword)
327	if err != nil {
328		return err
329	}
330
331	return db.WrapError(
332		d.db.TransactionContext(ctx, func(tx *db.Tx) error {
333			return d.store.SetUserPasswordByUsername(ctx, tx, username, password)
334		}),
335	)
336}
337
338type user struct {
339	user       models.User
340	publicKeys []ssh.PublicKey
341	handle     models.Handle
342}
343
344var _ proto.User = (*user)(nil)
345
346// IsAdmin implements proto.User
347func (u *user) IsAdmin() bool {
348	return u.user.Admin
349}
350
351// PublicKeys implements proto.User
352func (u *user) PublicKeys() []ssh.PublicKey {
353	return u.publicKeys
354}
355
356// Username implements proto.User
357func (u *user) Username() string {
358	return u.handle.Handle
359}
360
361// ID implements proto.User.
362func (u *user) ID() int64 {
363	return u.user.ID
364}
365
366// Password implements proto.User.
367func (u *user) Password() string {
368	if u.user.Password.Valid {
369		return u.user.Password.String
370	}
371
372	return ""
373}