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}