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}