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