• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

codenotary / immudb / 30656653651

31 Jul 2026 06:48PM UTC coverage: 84.849% (+0.01%) from 84.839%
30656653651

Pull #2137

gh-ci

vchaindz
test(stdlib): expect the tx to survive a parse error

TestTx_Errors asserted the symptom that d491954a removes: a parse error used
to leave sqlTx nil, so IsClosed reported true, the session dropped the
transaction and the next statement answered "no transaction found". The
transaction now stays open, so that statement reaches the parser and returns
its own syntax error instead.

Assert that error, and roll back at the end - on the old code the rollback is
exactly what failed, so it pins the fix through the full client and session
path rather than only at the transactions package.

The sessions import goes with the ErrTransactionNotFound reference.
Pull Request #2137: fix(server): keep the ongoing tx when a statement fails before execution

18 of 18 new or added lines in 2 files covered. (100.0%)

8 existing lines in 3 files now uncovered.

45282 of 53368 relevant lines covered (84.85%)

126521.27 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

88.09
/pkg/server/user.go
1
/*
2
Copyright 2026 Codenotary Inc. All rights reserved.
3

4
SPDX-License-Identifier: BUSL-1.1
5
you may not use this file except in compliance with the License.
6
You may obtain a copy of the License at
7

8
    https://mariadb.com/bsl11/
9

10
Unless required by applicable law or agreed to in writing, software
11
distributed under the License is distributed on an "AS IS" BASIS,
12
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
See the License for the specific language governing permissions and
14
limitations under the License.
15
*/
16

17
package server
18

19
import (
20
        "context"
21
        "encoding/json"
22
        "fmt"
23
        "time"
24

25
        "github.com/codenotary/immudb/embedded/sql"
26
        "github.com/codenotary/immudb/pkg/database"
27
        "github.com/codenotary/immudb/pkg/server/sessions"
28

29
        "github.com/codenotary/immudb/pkg/api/schema"
30
        "github.com/codenotary/immudb/pkg/auth"
31
        "github.com/codenotary/immudb/pkg/errors"
32
        "github.com/golang/protobuf/ptypes/empty"
33
        "google.golang.org/grpc/codes"
34
        "google.golang.org/grpc/status"
35
)
36

37
// Login ...
38
func (s *ImmuServer) Login(ctx context.Context, r *schema.LoginRequest) (*schema.LoginResponse, error) {
82✔
39
        if !s.Options.auth {
83✔
40
                return nil, errors.New(ErrAuthDisabled).WithCode(errors.CodProtocolViolation)
1✔
41
        }
1✔
42

43
        u, err := s.getValidatedUser(ctx, r.User, r.Password)
81✔
44
        if err != nil {
84✔
45
                return nil, errors.Wrap(err, ErrInvalidUsernameOrPassword)
3✔
46
        }
3✔
47

48
        if !u.Active {
79✔
49
                return nil, errors.New(ErrUserNotActive)
1✔
50
        }
1✔
51

52
        var token string
77✔
53

77✔
54
        if s.multidbmode {
86✔
55
                //-1 no database yet, must exec the "use" (UseDatabase) command first
9✔
56
                token, err = auth.GenerateToken(*u, -1, s.Options.TokenExpiryTimeMin)
9✔
57
        } else {
77✔
58
                token, err = auth.GenerateToken(*u, defaultDbIndex, s.Options.TokenExpiryTimeMin)
68✔
59
        }
68✔
60
        if err != nil {
77✔
61
                return nil, err
×
62
        }
×
63

64
        loginResponse := &schema.LoginResponse{Token: token}
77✔
65
        if u.Username == auth.SysAdminUsername && string(r.GetPassword()) == auth.SysAdminPassword {
143✔
66
                loginResponse.Warning = []byte(auth.WarnDefaultAdminPassword)
66✔
67
        }
66✔
68

69
        if u.Username == auth.SysAdminUsername {
143✔
70
                u.IsSysAdmin = true
66✔
71
        }
66✔
72

73
        //add user to loggedin list
74
        s.addUserToLoginList(u)
77✔
75

77✔
76
        return loginResponse, nil
77✔
77
}
78

79
// Logout ...
80
func (s *ImmuServer) Logout(ctx context.Context, r *empty.Empty) (*empty.Empty, error) {
34✔
81
        if !s.Options.auth {
35✔
82
                return nil, errors.New(ErrAuthDisabled).WithCode(errors.CodProtocolViolation)
1✔
83
        }
1✔
84

85
        _, user, err := s.getLoggedInUserdataFromCtx(ctx)
33✔
86
        if err != nil {
62✔
87
                return nil, err
29✔
88
        }
29✔
89

90
        // remove user from loggedin users; only rotate token keys when last session ends
91
        if s.removeUserFromLoginList(user.Username) {
8✔
92
                _, err = auth.DropTokenKeysForCtx(ctx)
4✔
93
        }
4✔
94

95
        return new(empty.Empty), err
4✔
96
}
97

98
// CreateUser Creates a new user
99
func (s *ImmuServer) CreateUser(ctx context.Context, r *schema.CreateUserRequest) (*empty.Empty, error) {
39✔
100
        s.Logger.Debugf("CreateUser")
39✔
101

39✔
102
        if s.Options.GetMaintenance() {
40✔
103
                return nil, ErrNotAllowedInMaintenanceMode
1✔
104
        }
1✔
105

106
        if !s.Options.GetAuth() {
39✔
107
                return nil, fmt.Errorf("this command is available only with authentication on")
1✔
108
        }
1✔
109

110
        _, loggedInuser, err := s.getLoggedInUserdataFromCtx(ctx)
37✔
111
        if err != nil {
38✔
112
                return nil, err
1✔
113
        }
1✔
114

115
        if len(r.User) == 0 {
37✔
116
                return nil, fmt.Errorf("username can not be empty")
1✔
117
        }
1✔
118

119
        if (len(r.Database) == 0) && s.multidbmode {
36✔
120
                return nil, fmt.Errorf("database name can not be empty when there are multiple databases")
1✔
121
        }
1✔
122

123
        if (len(r.Database) == 0) && !s.multidbmode {
34✔
124
                r.Database = DefaultDBName
×
125
        }
×
126

127
        //check if database exists
128
        if s.dbList.GetId(r.Database) < 0 {
35✔
129
                return nil, fmt.Errorf("database %s does not exist", r.Database)
1✔
130
        }
1✔
131

132
        //check permission is a known value
133
        if (r.Permission == auth.PermissionNone) ||
33✔
134
                (r.Permission > auth.PermissionRW && r.Permission < auth.PermissionAdmin) {
34✔
135
                return nil, fmt.Errorf("unrecognized permission")
1✔
136
        }
1✔
137

138
        //if the requesting user has admin permission on this database
139
        if (!loggedInuser.IsSysAdmin) &&
32✔
140
                (!loggedInuser.HasPermission(r.Database, auth.PermissionAdmin)) {
33✔
141
                return nil, fmt.Errorf("you do not have permission on this database")
1✔
142
        }
1✔
143

144
        //do not allow to create another system admin
145
        if r.Permission == auth.PermissionSysAdmin {
32✔
146
                return nil, fmt.Errorf("can not create another system admin")
1✔
147
        }
1✔
148

149
        _, err = s.getUser(ctx, r.User)
30✔
150
        if err == nil {
32✔
151
                return nil, fmt.Errorf("user already exists")
2✔
152
        }
2✔
153

154
        _, _, err = s.insertNewUser(ctx, r.User, r.Password, r.GetPermission(), r.Database, loggedInuser.Username)
28✔
155
        if err != nil {
28✔
156
                return nil, err
×
157
        }
×
158

159
        s.Logger.Infof("user %s was created by user %s", r.User, loggedInuser.Username)
28✔
160

28✔
161
        return &empty.Empty{}, nil
28✔
162
}
163

164
// ListUsers returns a list of users based on the requesting user permissions
165
func (s *ImmuServer) ListUsers(ctx context.Context, req *empty.Empty) (*schema.UserList, error) {
18✔
166
        s.Logger.Debugf("ListUsers")
18✔
167

18✔
168
        loggedInuser := &auth.User{}
18✔
169
        var db database.DB
18✔
170
        var err error
18✔
171
        userlist := &schema.UserList{}
18✔
172

18✔
173
        if !s.Options.GetMaintenance() {
36✔
174
                if !s.Options.GetAuth() {
19✔
175
                        return nil, fmt.Errorf("this command is available only with authentication on")
1✔
176
                }
1✔
177

178
                var dbInd int
17✔
179

17✔
180
                dbInd, loggedInuser, err = s.getLoggedInUserdataFromCtx(ctx)
17✔
181
                if err != nil {
18✔
182
                        return nil, err
1✔
183
                }
1✔
184

185
                if dbInd >= 0 {
31✔
186
                        db, err = s.dbList.GetByIndex(dbInd)
15✔
187
                        if err != nil {
15✔
188
                                return nil, err
×
189
                        }
×
190
                }
191
        }
192

193
        itemList, err := s.sysDB.Scan(ctx, &schema.ScanRequest{
16✔
194
                Prefix: []byte{KeyPrefixUser},
16✔
195
                NoWait: true,
16✔
196
        })
16✔
197
        if err != nil {
16✔
198
                s.Logger.Errorf("error getting users: %v", err)
×
199
                return nil, err
×
200
        }
×
201

202
        if loggedInuser.IsSysAdmin || s.Options.GetMaintenance() {
30✔
203
                // return all users, including the deactivated ones
14✔
204
                for i := 0; i < len(itemList.Entries); i++ {
45✔
205
                        itemList.Entries[i].Key = itemList.Entries[i].Key[1:]
31✔
206

31✔
207
                        usr, err := unmarshalSchemaUser(itemList.Entries[i].Value)
31✔
208
                        if err != nil {
31✔
209
                                return nil, err
×
210
                        }
×
211
                        userlist.Users = append(userlist.Users, usr)
31✔
212
                }
213
                return userlist, nil
14✔
214
        } else if db != nil && loggedInuser.WhichPermission(db.GetName()) == auth.PermissionAdmin {
3✔
215
                // for admin users return only users for the database that is has selected
1✔
216
                selectedDbname := db.GetName()
1✔
217
                userlist := &schema.UserList{}
1✔
218

1✔
219
                for i := 0; i < len(itemList.Entries); i++ {
3✔
220
                        itemList.Entries[i].Key = itemList.Entries[i].Key[1:]
2✔
221

2✔
222
                        usr, err := unmarshalSchemaUser(itemList.Entries[i].Value)
2✔
223
                        if err != nil {
2✔
224
                                return nil, err
×
225
                        }
×
226

227
                        include := false
2✔
228

2✔
229
                        for _, val := range usr.Permissions {
4✔
230
                                //check if this user has any permission for this database
2✔
231
                                //include in the reply only if it has any permission for the currently selected database
2✔
232
                                if val.Database == selectedDbname {
3✔
233
                                        include = true
1✔
234
                                }
1✔
235
                        }
236

237
                        if include {
3✔
238
                                userlist.Users = append(userlist.Users, usr)
1✔
239
                        }
1✔
240
                }
241
                return userlist, nil
1✔
242
        } else {
1✔
243
                // any other permission return only its data
1✔
244
                usr, err := toSchemaUser(loggedInuser)
1✔
245
                if err != nil {
1✔
246
                        return nil, err
×
247
                }
×
248
                return &schema.UserList{Users: []*schema.User{usr}}, nil
1✔
249
        }
250
}
251

252
func unmarshalSchemaUser(data []byte) (*schema.User, error) {
34✔
253
        var u auth.User
34✔
254
        if err := json.Unmarshal(data, &u); err != nil {
34✔
255
                return nil, err
×
256
        }
×
257
        u.SetSQLPrivileges()
34✔
258
        return toSchemaUser(&u)
34✔
259
}
260

261
func toSchemaUser(u *auth.User) (*schema.User, error) {
35✔
262
        permissions := make([]*schema.Permission, len(u.Permissions))
35✔
263
        for i, val := range u.Permissions {
69✔
264
                permissions[i] = &schema.Permission{
34✔
265
                        Database:   val.Database,
34✔
266
                        Permission: val.Permission,
34✔
267
                }
34✔
268
        }
34✔
269

270
        privileges := make([]*schema.SQLPrivilege, len(u.SQLPrivileges))
35✔
271
        for i, p := range u.SQLPrivileges {
220✔
272
                privileges[i] = &schema.SQLPrivilege{Database: p.Database, Privilege: p.Privilege}
185✔
273
        }
185✔
274

275
        return &schema.User{
35✔
276
                User:          []byte(u.Username),
35✔
277
                Createdat:     u.CreatedAt.String(),
35✔
278
                Createdby:     u.CreatedBy,
35✔
279
                Permissions:   permissions,
35✔
280
                SqlPrivileges: privileges,
35✔
281
                Active:        u.Active,
35✔
282
        }, nil
35✔
283
}
284

285
// ChangePassword ...
286
func (s *ImmuServer) ChangePassword(ctx context.Context, r *schema.ChangePasswordRequest) (*empty.Empty, error) {
13✔
287
        s.Logger.Debugf("ChangePassword")
13✔
288

13✔
289
        if s.Options.GetMaintenance() {
14✔
290
                return nil, ErrNotAllowedInMaintenanceMode
1✔
291
        }
1✔
292

293
        if !s.Options.GetAuth() {
13✔
294
                return nil, fmt.Errorf("this command is available only with authentication on")
1✔
295
        }
1✔
296

297
        _, user, err := s.getLoggedInUserdataFromCtx(ctx)
11✔
298
        if err != nil {
12✔
299
                return nil, err
1✔
300
        }
1✔
301

302
        if string(r.User) == auth.SysAdminUsername {
11✔
303
                if err = auth.ComparePasswords(user.HashedPassword, r.OldPassword); err != nil {
2✔
304
                        return new(empty.Empty), status.Errorf(codes.PermissionDenied, "old password is incorrect")
1✔
305
                }
1✔
306
        }
307

308
        if !user.IsSysAdmin {
11✔
309
                if !user.HasAtLeastOnePermission(auth.PermissionAdmin) {
3✔
310
                        return nil, fmt.Errorf("user is not system admin nor admin in any of the databases")
1✔
311
                }
1✔
312
        }
313

314
        if len(r.User) == 0 {
9✔
315
                return nil, fmt.Errorf("username can not be empty")
1✔
316
        }
1✔
317

318
        targetUser, err := s.getUser(ctx, r.User)
7✔
319
        if err != nil {
8✔
320
                return nil, fmt.Errorf("user %s was not found or it was not created by you", string(r.User))
1✔
321
        }
1✔
322

323
        //if the user is not sys admin then let's make sure the target was created from this admin
324
        if !user.IsSysAdmin {
7✔
325
                if user.Username != targetUser.CreatedBy {
2✔
326
                        return nil, fmt.Errorf("user %s was not found or it was not created by you", string(r.User))
1✔
327
                }
1✔
328
        }
329

330
        _, err = targetUser.SetPassword(r.NewPassword)
5✔
331
        if err != nil {
5✔
332
                return nil, err
×
333
        }
×
334

335
        targetUser.CreatedBy = user.Username
5✔
336
        targetUser.CreatedAt = time.Now()
5✔
337
        if err := s.saveUser(ctx, targetUser); err != nil {
5✔
338
                return nil, err
×
339
        }
×
340

341
        s.Logger.Infof("password for user %s was changed by user %s", targetUser.Username, user.Username)
5✔
342

5✔
343
        // remove user from logged in users
5✔
344
        s.removeUserFromLoginList(targetUser.Username)
5✔
345

5✔
346
        // invalidate the token for this user
5✔
347
        auth.DropTokenKeys(targetUser.Username)
5✔
348

5✔
349
        // terminate active sessions for this user
5✔
350
        s.SessManager.CloseSessionsForUser(targetUser.Username)
5✔
351

5✔
352
        return new(empty.Empty), nil
5✔
353
}
354

355
// ChangePermission grant or revoke user permissions on databases
356
func (s *ImmuServer) ChangePermission(ctx context.Context, r *schema.ChangePermissionRequest) (*empty.Empty, error) {
19✔
357
        s.Logger.Debugf("ChangePermission %+v", r)
19✔
358

19✔
359
        if s.Options.GetMaintenance() {
20✔
360
                return nil, ErrNotAllowedInMaintenanceMode
1✔
361
        }
1✔
362

363
        //sanitize input
364
        {
18✔
365
                if len(r.Username) == 0 {
19✔
366
                        return nil, status.Errorf(codes.InvalidArgument, "username can not be empty")
1✔
367
                }
1✔
368

369
                if len(r.Database) == 0 {
18✔
370
                        return nil, status.Errorf(codes.InvalidArgument, "database can not be empty")
1✔
371
                }
1✔
372

373
                _, err := s.dbList.GetByName(r.Database)
16✔
374
                if r.Database != SystemDBName && err != nil {
16✔
375
                        return nil, status.Errorf(codes.InvalidArgument, "database does not exist")
×
376
                }
×
377

378
                if (r.Action != schema.PermissionAction_GRANT) &&
16✔
379
                        (r.Action != schema.PermissionAction_REVOKE) {
17✔
380
                        return nil, status.Errorf(codes.InvalidArgument, "action not recognized")
1✔
381
                }
1✔
382
                if (r.Permission == auth.PermissionNone) ||
15✔
383
                        ((r.Permission > auth.PermissionRW) &&
15✔
384
                                (r.Permission < auth.PermissionAdmin)) {
16✔
385
                        return nil, status.Errorf(codes.InvalidArgument, "unrecognized permission")
1✔
386
                }
1✔
387
        }
388

389
        _, user, err := s.getLoggedInUserdataFromCtx(ctx)
14✔
390
        if err != nil {
16✔
391
                return nil, err
2✔
392
        }
2✔
393

394
        //do not allow to change own permissions, user can lock itsself out
395
        if r.Username == user.Username {
13✔
396
                return nil, status.Errorf(codes.InvalidArgument, "changing your own permissions is not allowed")
1✔
397
        }
1✔
398

399
        if r.Username == auth.SysAdminUsername {
12✔
400
                return nil, status.Errorf(codes.InvalidArgument, "changing sysadmin permissions is not allowed")
1✔
401
        }
1✔
402

403
        if r.Database == SystemDBName && r.Permission == auth.PermissionRW {
11✔
404
                return nil, ErrPermissionDenied
1✔
405
        }
1✔
406

407
        // check if user exists
408
        targetUser, err := s.getUser(ctx, []byte(r.Username))
9✔
409
        if err != nil {
10✔
410
                return nil, status.Errorf(codes.NotFound, "user %s not found", string(r.Username))
1✔
411
        }
1✔
412

413
        // target user should be active
414
        if !targetUser.Active {
9✔
415
                return nil, status.Errorf(codes.FailedPrecondition, "user %s is not active", string(r.Username))
1✔
416
        }
1✔
417

418
        // check if requesting user has permission on this database
419
        if !user.IsSysAdmin {
7✔
420
                if !user.HasPermission(r.Database, auth.PermissionAdmin) {
×
421
                        return nil, status.Errorf(codes.PermissionDenied, "you do not have permission on this database")
×
422
                }
×
423
        }
424

425
        if r.Action == schema.PermissionAction_REVOKE {
9✔
426
                targetUser.RevokePermission(r.Database)
2✔
427
        } else {
7✔
428
                targetUser.GrantPermission(r.Database, r.Permission)
5✔
429
        }
5✔
430

431
        targetUser.CreatedBy = user.Username
7✔
432
        targetUser.CreatedAt = time.Now()
7✔
433
        targetUser.SQLPrivileges = defaultSQLPrivilegesForPermission(r.Database, r.Permission)
7✔
434
        targetUser.HasPrivileges = true
7✔
435

7✔
436
        if err := s.saveUser(ctx, targetUser); err != nil {
7✔
437
                return nil, err
×
438
        }
×
439

440
        s.Logger.Infof("permissions of user %s for database %s was changed by user %s", targetUser.Username, r.Database, user.Username)
7✔
441

7✔
442
        // remove user from loggedin users
7✔
443
        s.removeUserFromLoginList(targetUser.Username)
7✔
444

7✔
445
        // terminate active sessions for this user
7✔
446
        s.SessManager.CloseSessionsForUser(targetUser.Username)
7✔
447

7✔
448
        return new(empty.Empty), nil
7✔
449
}
450

451
// SetActiveUser activate or deactivate a user
452
func (s *ImmuServer) SetActiveUser(ctx context.Context, r *schema.SetActiveUserRequest) (*empty.Empty, error) {
31✔
453
        s.Logger.Debugf("SetActiveUser")
31✔
454

31✔
455
        if s.Options.GetMaintenance() {
32✔
456
                return nil, ErrNotAllowedInMaintenanceMode
1✔
457
        }
1✔
458

459
        if !s.Options.GetAuth() {
31✔
460
                return nil, fmt.Errorf("this command is available only with authentication on")
1✔
461
        }
1✔
462

463
        if len(r.Username) == 0 {
30✔
464
                return nil, fmt.Errorf("username can not be empty")
1✔
465
        }
1✔
466

467
        _, user, err := s.getLoggedInUserdataFromCtx(ctx)
28✔
468
        if err != nil {
29✔
469
                return nil, err
1✔
470
        }
1✔
471

472
        if !user.IsSysAdmin {
28✔
473
                if !user.HasAtLeastOnePermission(auth.PermissionAdmin) {
2✔
474
                        return nil, fmt.Errorf("user is not system admin nor admin in any of the databases")
1✔
475
                }
1✔
476
        }
477

478
        if r.Username == user.Username {
27✔
479
                return nil, fmt.Errorf("changing your own status is not allowed")
1✔
480
        }
1✔
481

482
        targetUser, err := s.getUser(ctx, []byte(r.Username))
25✔
483
        if err != nil {
27✔
484
                return nil, fmt.Errorf("user %s not found", r.Username)
2✔
485
        }
2✔
486

487
        //if the user is not sys admin then let's make sure the target was created from this admin
488
        if !user.IsSysAdmin && user.Username != targetUser.CreatedBy {
23✔
489
                return nil, fmt.Errorf("%s was not created by you", r.Username)
×
490
        }
×
491

492
        targetUser.Active = r.Active
23✔
493
        targetUser.CreatedBy = user.Username
23✔
494
        targetUser.CreatedAt = time.Now()
23✔
495

23✔
496
        if err := s.saveUser(ctx, targetUser); err != nil {
23✔
497
                return nil, err
×
498
        }
×
499

500
        s.Logger.Infof("user %s was %s by user %s", targetUser.Username, map[bool]string{
23✔
501
                true:  "activated",
23✔
502
                false: "deactivated",
23✔
503
        }[r.Active], user.Username)
23✔
504

23✔
505
        //remove user from loggedin users
23✔
506
        s.removeUserFromLoginList(targetUser.Username)
23✔
507

23✔
508
        // terminate active sessions for this user
23✔
509
        s.SessManager.CloseSessionsForUser(targetUser.Username)
23✔
510

23✔
511
        return new(empty.Empty), nil
23✔
512
}
513

514
// insertNewUser inserts a new user to the system database and returns username and plain text password
515
// A new password is generated automatically if passed parameter is empty
516
// If enforceStrongAuth is true it checks if username and password meet security criteria
517
func (s *ImmuServer) insertNewUser(ctx context.Context, username []byte, plainPassword []byte, permission uint32, database string, createdBy string) ([]byte, []byte, error) {
324✔
518
        if !auth.IsValidUsername(string(username)) {
325✔
519
                return nil, nil, status.Errorf(
1✔
520
                        codes.InvalidArgument,
1✔
521
                        "username can only contain letters, digits and underscores")
1✔
522
        }
1✔
523

524
        userdata := new(auth.User)
323✔
525
        plainpassword, err := userdata.SetPassword(plainPassword)
323✔
526
        if err != nil {
323✔
527
                return nil, nil, err
×
528
        }
×
529

530
        userdata.Active = true
323✔
531
        userdata.HasPrivileges = true
323✔
532
        userdata.Username = string(username)
323✔
533
        userdata.Permissions = append(userdata.Permissions, auth.Permission{Permission: permission, Database: database})
323✔
534
        userdata.SQLPrivileges = defaultSQLPrivilegesForPermission(database, permission)
323✔
535
        userdata.CreatedBy = createdBy
323✔
536
        userdata.CreatedAt = time.Now()
323✔
537

323✔
538
        if permission == auth.PermissionSysAdmin {
617✔
539
                userdata.IsSysAdmin = true
294✔
540
        }
294✔
541

542
        if (permission > auth.PermissionRW) && (permission < auth.PermissionAdmin) {
324✔
543
                return nil, nil, fmt.Errorf("unknown permission")
1✔
544
        }
1✔
545

546
        err = s.saveUser(ctx, userdata)
322✔
547

322✔
548
        return username, plainpassword, err
322✔
549
}
550

551
func (s *ImmuServer) getValidatedUser(ctx context.Context, username []byte, password []byte) (*auth.User, error) {
610✔
552
        userdata, err := s.getUser(ctx, username)
610✔
553
        if err != nil {
613✔
554
                return nil, err
3✔
555
        }
3✔
556

557
        err = userdata.ComparePasswords(password)
607✔
558
        if err != nil {
620✔
559
                return nil, err
13✔
560
        }
13✔
561

562
        return userdata, nil
594✔
563
}
564

565
// getUser returns userdata (username,hashed password, permission, active) from username
566
func (s *ImmuServer) getUser(ctx context.Context, username []byte) (*auth.User, error) {
693✔
567
        key := make([]byte, 1+len(username))
693✔
568
        key[0] = KeyPrefixUser
693✔
569
        copy(key[1:], username)
693✔
570

693✔
571
        item, err := s.sysDB.Get(ctx, &schema.KeyRequest{Key: key})
693✔
572
        if err != nil {
730✔
573
                return nil, err
37✔
574
        }
37✔
575

576
        var usr auth.User
656✔
577

656✔
578
        err = json.Unmarshal(item.Value, &usr)
656✔
579
        if err != nil {
656✔
580
                return nil, err
×
581
        }
×
582

583
        usr.SetSQLPrivileges()
656✔
584
        return &usr, nil
656✔
585
}
586

587
func (s *ImmuServer) saveUser(ctx context.Context, user *auth.User) error {
362✔
588
        userData, err := json.Marshal(user)
362✔
589
        if err != nil {
362✔
590
                return logErr(s.Logger, "error saving user: %v", err)
×
591
        }
×
592

593
        userKey := make([]byte, 1+len(user.Username))
362✔
594
        userKey[0] = KeyPrefixUser
362✔
595
        copy(userKey[1:], []byte(user.Username))
362✔
596

362✔
597
        userKV := &schema.KeyValue{Key: userKey, Value: userData}
362✔
598
        hdr, err := s.sysDB.Set(ctx, &schema.SetRequest{KVs: []*schema.KeyValue{userKV}})
362✔
599
        if err != nil {
363✔
600
                return logErr(s.Logger, "error saving user: %v", err)
1✔
601
        }
1✔
602

603
        // Block until the write is visible to subsequent Get calls. Replaces a prior
604
        // unconditional 10ms sleep; this returns immediately when indexing has
605
        // already caught up and only waits as long as indexing actually requires.
606
        if err := s.sysDB.WaitForIndexingUpto(ctx, hdr.Id); err != nil {
361✔
607
                return logErr(s.Logger, "error awaiting user indexing: %v", err)
×
608
        }
×
609
        return nil
361✔
610
}
611

612
// removeUserFromLoginList decrements the session count for username and removes the
613
// entry when no sessions remain. Returns true when the last session was removed.
614
func (s *ImmuServer) removeUserFromLoginList(username string) bool {
42✔
615
        return s.userdata.RemoveSession(username)
42✔
616
}
42✔
617

618
func (s *ImmuServer) addUserToLoginList(u *auth.User) {
77✔
619
        s.userdata.AddSession(u)
77✔
620
}
77✔
621

622
func (s *ImmuServer) getLoggedInUserdataFromCtx(ctx context.Context) (int, *auth.User, error) {
17,263✔
623
        if sessionID, err := sessions.GetSessionIDFromContext(ctx); err == nil {
33,871✔
624
                sess, e := s.SessManager.GetSession(sessionID)
16,608✔
625
                if e != nil {
16,608✔
UNCOV
626
                        return 0, nil, e
×
UNCOV
627
                }
×
628

629
                if sess.GetDatabase().GetName() == SystemDBName {
16,610✔
630
                        return sysDBIndex, sess.GetUser(), nil
2✔
631
                }
2✔
632

633
                return s.dbList.GetId(sess.GetDatabase().GetName()), sess.GetUser(), nil
16,606✔
634
        }
635
        jsUser, err := auth.GetLoggedInUser(ctx)
655✔
636
        if err != nil {
787✔
637
                return -1, nil, err
132✔
638
        }
132✔
639

640
        u, err := s.getLoggedInUserDataFromUsername(jsUser.Username)
523✔
641
        return int(jsUser.DatabaseIndex), u, err
523✔
642
}
643

644
func (s *ImmuServer) getLoggedInUserDataFromUsername(username string) (*auth.User, error) {
524✔
645
        // Get acquires only the per-shard read lock. Every authenticated RPC
524✔
646
        // passes through this path, so the previous global writer lock — and
524✔
647
        // even the global RLock that replaced it — serialised auth across all
524✔
648
        // usernames. Per-shard sharding (A3) lets reads on disjoint usernames
524✔
649
        // proceed without any cross-coherence cost.
524✔
650
        userdata, ok := s.userdata.Get(username)
524✔
651
        if !ok {
528✔
652
                return nil, ErrNotLoggedIn
4✔
653
        }
4✔
654
        return userdata, nil
520✔
655
}
656

657
func (s *ImmuServer) ChangeSQLPrivileges(ctx context.Context, r *schema.ChangeSQLPrivilegesRequest) (*schema.ChangeSQLPrivilegesResponse, error) {
6✔
658
        s.Logger.Debugf("ChangeSQLPrivileges %+v", r)
6✔
659

6✔
660
        if s.Options.GetMaintenance() {
6✔
661
                return nil, ErrNotAllowedInMaintenanceMode
×
662
        }
×
663

664
        // sanitize input
665
        {
6✔
666
                if len(r.Username) == 0 {
6✔
667
                        return nil, status.Errorf(codes.InvalidArgument, "username can not be empty")
×
668
                }
×
669
                if _, err := s.dbList.GetByName(r.Database); err != nil {
6✔
670
                        return nil, status.Errorf(codes.InvalidArgument, "%s", err.Error())
×
671
                }
×
672
                if (r.Action != schema.PermissionAction_GRANT) &&
6✔
673
                        (r.Action != schema.PermissionAction_REVOKE) {
6✔
674
                        return nil, status.Errorf(codes.InvalidArgument, "action not recognized")
×
675
                }
×
676
        }
677

678
        privileges := make([]string, len(r.Privileges))
6✔
679
        for i, p := range r.Privileges {
19✔
680
                if !isValidPrivilege(p) {
13✔
681
                        return nil, status.Errorf(codes.InvalidArgument, "SQL privilege not recognized")
×
682
                }
×
683
                privileges[i] = string(p)
13✔
684
        }
685

686
        _, user, err := s.getLoggedInUserdataFromCtx(ctx)
6✔
687
        if err != nil {
6✔
688
                return nil, err
×
689
        }
×
690

691
        //do not allow to change own permissions, user can lock itsself out
692
        if r.Username == user.Username {
7✔
693
                return nil, status.Errorf(codes.InvalidArgument, "changing your own privileges is not allowed")
1✔
694
        }
1✔
695

696
        if r.Username == auth.SysAdminUsername {
6✔
697
                return nil, status.Errorf(codes.InvalidArgument, "changing sysadmin privileges is not allowed")
1✔
698
        }
1✔
699

700
        // check if user exists
701
        targetUser, err := s.getUser(ctx, []byte(r.Username))
4✔
702
        if err != nil {
4✔
703
                return nil, status.Errorf(codes.NotFound, "user %s not found", r.Username)
×
704
        }
×
705

706
        // target user should be active
707
        if !targetUser.Active {
4✔
708
                return nil, status.Errorf(codes.FailedPrecondition, "user %s is not active", r.Username)
×
709
        }
×
710

711
        // target user should have permission on the requested database
712
        if targetUser.WhichPermission(r.Database) == auth.PermissionNone {
5✔
713
                return nil, status.Errorf(codes.FailedPrecondition, "user %s doesn't have permission on database %s", r.Username, r.Database)
1✔
714
        }
1✔
715

716
        // check if requesting user has permission on this database
717
        if !user.IsSysAdmin {
3✔
718
                if !user.HasPermission(r.Database, auth.PermissionAdmin) {
×
719
                        return nil, status.Errorf(codes.PermissionDenied, "you do not have permission on this database")
×
720
                }
×
721
        }
722

723
        if r.Action == schema.PermissionAction_REVOKE {
5✔
724
                targetUser.RevokeSQLPrivileges(r.Database, privileges)
2✔
725
        } else {
3✔
726
                targetUser.GrantSQLPrivileges(r.Database, privileges)
1✔
727
        }
1✔
728

729
        targetUser.CreatedBy = user.Username
3✔
730
        targetUser.CreatedAt = time.Now()
3✔
731
        targetUser.HasPrivileges = true
3✔
732

3✔
733
        if err := s.saveUser(ctx, targetUser); err != nil {
3✔
734
                return nil, err
×
735
        }
×
736

737
        s.Logger.Infof("permissions of user %s for database %s was changed by user %s", targetUser.Username, r.Database, user.Username)
3✔
738

3✔
739
        // remove user from loggedin users
3✔
740
        s.removeUserFromLoginList(targetUser.Username)
3✔
741

3✔
742
        // terminate active sessions for this user
3✔
743
        s.SessManager.CloseSessionsForUser(targetUser.Username)
3✔
744

3✔
745
        return &schema.ChangeSQLPrivilegesResponse{}, nil
3✔
746
}
747

748
func isValidPrivilege(p string) bool {
13✔
749
        switch sql.SQLPrivilege(p) {
13✔
750
        case sql.SQLPrivilegeSelect,
751
                sql.SQLPrivilegeCreate,
752
                sql.SQLPrivilegeInsert,
753
                sql.SQLPrivilegeUpdate,
754
                sql.SQLPrivilegeDelete,
755
                sql.SQLPrivilegeDrop,
756
                sql.SQLPrivilegeAlter:
13✔
757
                return true
13✔
758
        }
759
        return false
×
760
}
761

762
func defaultSQLPrivilegesForPermission(database string, permission uint32) []auth.SQLPrivilege {
330✔
763
        sqlPrivileges := sql.DefaultSQLPrivilegesForPermission(sql.PermissionFromCode(permission))
330✔
764
        privileges := make([]auth.SQLPrivilege, len(sqlPrivileges))
330✔
765
        for i, p := range sqlPrivileges {
2,580✔
766
                privileges[i] = auth.SQLPrivilege{
2,250✔
767
                        Database:  database,
2,250✔
768
                        Privilege: string(p),
2,250✔
769
                }
2,250✔
770
        }
2,250✔
771
        return privileges
330✔
772
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc