user.go

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