Parent directory

user.go

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