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}