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