user.go

  1package backend
  2
  3import (
  4	"context"
  5	"errors"
  6	"strings"
  7
  8	"github.com/charmbracelet/soft-serve/server/access"
  9	"github.com/charmbracelet/soft-serve/server/db"
 10	"github.com/charmbracelet/soft-serve/server/db/models"
 11	"github.com/charmbracelet/soft-serve/server/proto"
 12	"github.com/charmbracelet/soft-serve/server/sshutils"
 13	"github.com/charmbracelet/soft-serve/server/utils"
 14	"golang.org/x/crypto/ssh"
 15)
 16
 17// AccessLevel returns the access level of a user for a repository.
 18//
 19// It implements backend.Backend.
 20func (d *Backend) AccessLevel(ctx context.Context, repo string, username string) access.AccessLevel {
 21	user, _ := d.User(ctx, username)
 22	return d.AccessLevelForUser(ctx, repo, user)
 23}
 24
 25// AccessLevelByPublicKey returns the access level of a user's public key for a repository.
 26//
 27// It implements backend.Backend.
 28func (d *Backend) AccessLevelByPublicKey(ctx context.Context, repo string, pk ssh.PublicKey) access.AccessLevel {
 29	for _, k := range d.cfg.AdminKeys() {
 30		if sshutils.KeysEqual(pk, k) {
 31			return access.AdminAccess
 32		}
 33	}
 34
 35	user, _ := d.UserByPublicKey(ctx, pk)
 36	if user != nil {
 37		return d.AccessLevel(ctx, repo, user.Username())
 38	}
 39
 40	return d.AccessLevel(ctx, repo, "")
 41}
 42
 43// AccessLevelForUser returns the access level of a user for a repository.
 44// TODO: user repository ownership
 45func (d *Backend) AccessLevelForUser(ctx context.Context, repo string, user proto.User) access.AccessLevel {
 46	var username string
 47	anon := d.AnonAccess(ctx)
 48	if user != nil {
 49		username = user.Username()
 50	}
 51
 52	// If the user is an admin, they have admin access.
 53	if user != nil && user.IsAdmin() {
 54		return access.AdminAccess
 55	}
 56
 57	// If the repository exists, check if the user is a collaborator.
 58	r := proto.RepositoryFromContext(ctx)
 59	if r == nil {
 60		r, _ = d.Repository(ctx, repo)
 61	}
 62
 63	if r != nil {
 64		// If the user is a collaborator, they have read/write access.
 65		isCollab, _ := d.IsCollaborator(ctx, repo, username)
 66		if isCollab {
 67			if anon > access.ReadWriteAccess {
 68				return anon
 69			}
 70			return access.ReadWriteAccess
 71		}
 72
 73		// If the repository is private, the user has no access.
 74		if r.IsPrivate() {
 75			return access.NoAccess
 76		}
 77
 78		// Otherwise, the user has read-only access.
 79		return access.ReadOnlyAccess
 80	}
 81
 82	if user != nil {
 83		// If the repository doesn't exist, the user has read/write access.
 84		if anon > access.ReadWriteAccess {
 85			return anon
 86		}
 87
 88		return access.ReadWriteAccess
 89	}
 90
 91	// If the user doesn't exist, give them the anonymous access level.
 92	return anon
 93}
 94
 95// User finds a user by username.
 96//
 97// It implements backend.Backend.
 98func (d *Backend) User(ctx context.Context, username string) (proto.User, error) {
 99	username = strings.ToLower(username)
100	if err := utils.ValidateUsername(username); err != nil {
101		return nil, err
102	}
103
104	var m models.User
105	var pks []ssh.PublicKey
106	if err := d.db.TransactionContext(ctx, func(tx *db.Tx) error {
107		var err error
108		m, err = d.store.FindUserByUsername(ctx, tx, username)
109		if err != nil {
110			return err
111		}
112
113		pks, err = d.store.ListPublicKeysByUserID(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		return nil, err
121	}
122
123	return &user{
124		user:       m,
125		publicKeys: pks,
126	}, nil
127}
128
129// UserByPublicKey finds a user by public key.
130//
131// It implements backend.Backend.
132func (d *Backend) UserByPublicKey(ctx context.Context, pk ssh.PublicKey) (proto.User, error) {
133	var m models.User
134	var pks []ssh.PublicKey
135	if err := d.db.TransactionContext(ctx, func(tx *db.Tx) error {
136		var err error
137		m, err = d.store.FindUserByPublicKey(ctx, tx, pk)
138		if err != nil {
139			return db.WrapError(err)
140		}
141
142		pks, err = d.store.ListPublicKeysByUserID(ctx, tx, m.ID)
143		return err
144	}); err != nil {
145		err = db.WrapError(err)
146		if errors.Is(err, db.ErrRecordNotFound) {
147			return nil, proto.ErrUserNotFound
148		}
149		return nil, err
150	}
151
152	return &user{
153		user:       m,
154		publicKeys: pks,
155	}, nil
156}
157
158// Users returns all users.
159//
160// It implements backend.Backend.
161func (d *Backend) Users(ctx context.Context) ([]string, error) {
162	var users []string
163	if err := d.db.TransactionContext(ctx, func(tx *db.Tx) error {
164		ms, err := d.store.GetAllUsers(ctx, tx)
165		if err != nil {
166			return err
167		}
168
169		for _, m := range ms {
170			users = append(users, m.Username)
171		}
172
173		return nil
174	}); err != nil {
175		return nil, db.WrapError(err)
176	}
177
178	return users, nil
179}
180
181// AddPublicKey adds a public key to a user.
182//
183// It implements backend.Backend.
184func (d *Backend) AddPublicKey(ctx context.Context, username string, pk ssh.PublicKey) error {
185	username = strings.ToLower(username)
186	if err := utils.ValidateUsername(username); err != nil {
187		return err
188	}
189
190	return db.WrapError(
191		d.db.TransactionContext(ctx, func(tx *db.Tx) error {
192			return d.store.AddPublicKeyByUsername(ctx, tx, username, pk)
193		}),
194	)
195}
196
197// CreateUser creates a new user.
198//
199// It implements backend.Backend.
200func (d *Backend) CreateUser(ctx context.Context, username string, opts proto.UserOptions) (proto.User, error) {
201	username = strings.ToLower(username)
202	if err := utils.ValidateUsername(username); err != nil {
203		return nil, err
204	}
205
206	if err := d.db.TransactionContext(ctx, func(tx *db.Tx) error {
207		return d.store.CreateUser(ctx, tx, username, opts.Admin, opts.PublicKeys)
208	}); err != nil {
209		return nil, db.WrapError(err)
210	}
211
212	return d.User(ctx, username)
213}
214
215// DeleteUser deletes a user.
216//
217// It implements backend.Backend.
218func (d *Backend) DeleteUser(ctx context.Context, username string) error {
219	username = strings.ToLower(username)
220	if err := utils.ValidateUsername(username); err != nil {
221		return err
222	}
223
224	return db.WrapError(
225		d.db.TransactionContext(ctx, func(tx *db.Tx) error {
226			return d.store.DeleteUserByUsername(ctx, tx, username)
227		}),
228	)
229}
230
231// RemovePublicKey removes a public key from a user.
232//
233// It implements backend.Backend.
234func (d *Backend) RemovePublicKey(ctx context.Context, username string, pk ssh.PublicKey) error {
235	return db.WrapError(
236		d.db.TransactionContext(ctx, func(tx *db.Tx) error {
237			return d.store.RemovePublicKeyByUsername(ctx, tx, username, pk)
238		}),
239	)
240}
241
242// ListPublicKeys lists the public keys of a user.
243func (d *Backend) ListPublicKeys(ctx context.Context, username string) ([]ssh.PublicKey, error) {
244	username = strings.ToLower(username)
245	if err := utils.ValidateUsername(username); err != nil {
246		return nil, err
247	}
248
249	var keys []ssh.PublicKey
250	if err := d.db.TransactionContext(ctx, func(tx *db.Tx) error {
251		var err error
252		keys, err = d.store.ListPublicKeysByUsername(ctx, tx, username)
253		return err
254	}); err != nil {
255		return nil, db.WrapError(err)
256	}
257
258	return keys, nil
259}
260
261// SetUsername sets the username of a user.
262//
263// It implements backend.Backend.
264func (d *Backend) SetUsername(ctx context.Context, username string, newUsername string) 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.SetUsernameByUsername(ctx, tx, username, newUsername)
273		}),
274	)
275}
276
277// SetAdmin sets the admin flag of a user.
278//
279// It implements backend.Backend.
280func (d *Backend) SetAdmin(ctx context.Context, username string, admin bool) error {
281	username = strings.ToLower(username)
282	if err := utils.ValidateUsername(username); err != nil {
283		return err
284	}
285
286	return db.WrapError(
287		d.db.TransactionContext(ctx, func(tx *db.Tx) error {
288			return d.store.SetAdminByUsername(ctx, tx, username, admin)
289		}),
290	)
291}
292
293// SetPassword sets the password of a user.
294func (d *Backend) SetPassword(ctx context.Context, username string, rawPassword string) error {
295	username = strings.ToLower(username)
296	if err := utils.ValidateUsername(username); err != nil {
297		return err
298	}
299
300	password, err := HashPassword(rawPassword)
301	if err != nil {
302		return err
303	}
304
305	return db.WrapError(
306		d.db.TransactionContext(ctx, func(tx *db.Tx) error {
307			return d.store.SetUserPasswordByUsername(ctx, tx, username, password)
308		}),
309	)
310}
311
312type user struct {
313	user       models.User
314	publicKeys []ssh.PublicKey
315}
316
317var _ proto.User = (*user)(nil)
318
319// IsAdmin implements proto.User
320func (u *user) IsAdmin() bool {
321	return u.user.Admin
322}
323
324// PublicKeys implements proto.User
325func (u *user) PublicKeys() []ssh.PublicKey {
326	return u.publicKeys
327}
328
329// Username implements proto.User
330func (u *user) Username() string {
331	return u.user.Username
332}
333
334// ID implements proto.User.
335func (u *user) ID() int64 {
336	return u.user.ID
337}