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}