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