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

mendersoftware / useradm / 802076887

pending completion
802076887

Pull #354

gitlab-ci

Fabio Tranchitella
feat: limit the number of JWT tokens (sessions) per user
Pull Request #354: feat: add support for never expiring PATs

78 of 84 new or added lines in 6 files covered. (92.86%)

37 existing lines in 1 file now uncovered.

2620 of 2928 relevant lines covered (89.48%)

127.26 hits per line

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

86.86
/store/mongo/datastore_mongo.go
1
// Copyright 2023 Northern.tech AS
2
//
3
//    Licensed under the Apache License, Version 2.0 (the "License");
4
//    you may not use this file except in compliance with the License.
5
//    You may obtain a copy of the License at
6
//
7
//        http://www.apache.org/licenses/LICENSE-2.0
8
//
9
//    Unless required by applicable law or agreed to in writing, software
10
//    distributed under the License is distributed on an "AS IS" BASIS,
11
//    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
//    See the License for the specific language governing permissions and
13
//    limitations under the License.
14

15
package mongo
16

17
import (
18
        "context"
19
        "crypto/tls"
20
        "strings"
21
        "time"
22

23
        _ "github.com/mendersoftware/go-lib-micro/mongo/codec"
24
        "github.com/mendersoftware/go-lib-micro/mongo/oid"
25
        mstore "github.com/mendersoftware/go-lib-micro/store/v2"
26
        "github.com/pkg/errors"
27
        "go.mongodb.org/mongo-driver/bson"
28
        "go.mongodb.org/mongo-driver/mongo"
29
        mopts "go.mongodb.org/mongo-driver/mongo/options"
30
        "golang.org/x/crypto/bcrypt"
31

32
        "github.com/mendersoftware/useradm/jwt"
33
        "github.com/mendersoftware/useradm/model"
34
        "github.com/mendersoftware/useradm/store"
35
)
36

37
const (
38
        DbUsersColl        = "users"
39
        DbTokensColl       = "tokens"
40
        DbSettingsColl     = "settings"
41
        DbUserSettingsColl = "user_settings"
42

43
        DbUserEmail      = "email"
44
        DbUserPass       = "password"
45
        DbUserLoginTs    = "login_ts"
46
        DbTokenSubject   = "sub"
47
        DbTokenExpiresAt = "exp"
48
        DbTokenIssuedAt  = "iat"
49
        DbTokenTenant    = "tenant"
50
        DbTokenUser      = "user"
51
        DbTokenIssuer    = "iss"
52
        DbTokenScope     = "scp"
53
        DbTokenAudience  = "aud"
54
        DbTokenNotBefore = "nbf"
55
        DbTokenLastUsed  = "last_used"
56
        DbTokenName      = "name"
57
        DbID             = "_id"
58

59
        DbTokenIssuedAtTime = DbTokenIssuedAt + ".time"
60

61
        DbUniqueEmailIndexName     = "email_1"
62
        DbUniqueTokenNameIndexName = "token_name_1"
63
        DbTokenSubjectIndexName    = "token_subject_1"
64
        DbTokenExpirationIndexName = "token_expiration"
65

66
        DbTenantUniqueTokenNameIndexName = "tenant_1_subject_1_name_1"
67
        DbTenantTokenSubjectIndexName    = "tenant_1_subject_1"
68

69
        DbSettingsEtag            = "etag"
70
        DbSettingsTenantIndexName = "tenant"
71
        DbSettingsUserID          = "user_id"
72
)
73

74
type DataStoreMongoConfig struct {
75
        // MGO connection string
76
        ConnectionString string
77

78
        // SSL support
79
        SSL           bool
80
        SSLSkipVerify bool
81

82
        // Overwrites credentials provided in connection string if provided
83
        Username string
84
        Password string
85
}
86

87
type DataStoreMongo struct {
88
        client      *mongo.Client
89
        automigrate bool
90
        multitenant bool
91
}
92

93
func GetDataStoreMongo(config DataStoreMongoConfig) (*DataStoreMongo, error) {
94
        d, err := NewDataStoreMongo(config)
95
        if err != nil {
96
                return nil, errors.Wrap(err, "database connection failed")
97
        }
98
        return d, nil
99
}
100

101
func NewDataStoreMongoWithClient(client *mongo.Client) (*DataStoreMongo, error) {
178✔
102

178✔
103
        db := &DataStoreMongo{
178✔
104
                client: client,
178✔
105
        }
178✔
106

178✔
107
        return db, nil
178✔
108
}
178✔
109

110
func NewDataStoreMongo(config DataStoreMongoConfig) (*DataStoreMongo, error) {
111
        var err error
112
        var mongoURL string
113

114
        clientOptions := mopts.Client()
115
        if !strings.Contains(config.ConnectionString, "://") {
116
                mongoURL = "mongodb://" + config.ConnectionString
117
        } else {
118
                mongoURL = config.ConnectionString
119

120
        }
121
        clientOptions.ApplyURI(mongoURL)
122

123
        if config.Username != "" {
124
                credentials := mopts.Credential{
125
                        Username: config.Username,
126
                }
127
                if config.Password != "" {
128
                        credentials.Password = config.Password
129
                        credentials.PasswordSet = true
130
                }
131
                clientOptions.SetAuth(credentials)
132
        }
133

134
        if config.SSL {
135
                tlsConfig := &tls.Config{
136
                        InsecureSkipVerify: config.SSLSkipVerify,
137
                }
138
                clientOptions.SetTLSConfig(tlsConfig)
139
        }
140

141
        ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
142
        defer cancel()
143
        c, err := mongo.Connect(ctx, clientOptions)
144
        if err != nil {
145
                return nil, err
146
        }
147

148
        // Validate connection
149
        if err = c.Ping(ctx, nil); err != nil {
150
                return nil, err
151
        }
152

153
        db, err := NewDataStoreMongoWithClient(c)
154
        if err != nil {
155
                return nil, err
156
        }
157

158
        return db, nil
159
}
160

161
func (db *DataStoreMongo) Ping(ctx context.Context) error {
2✔
162
        res := db.client.Database(DbName).RunCommand(ctx, bson.M{"ping": 1})
2✔
163
        return res.Err()
2✔
164
}
2✔
165

166
func (db *DataStoreMongo) CreateUser(ctx context.Context, u *model.User) error {
14✔
167
        now := time.Now().UTC()
14✔
168

14✔
169
        u.CreatedTs = &now
14✔
170
        u.UpdatedTs = &now
14✔
171

14✔
172
        _, err := db.client.
14✔
173
                Database(mstore.DbFromContext(ctx, DbName)).
14✔
174
                Collection(DbUsersColl).
14✔
175
                InsertOne(ctx, mstore.WithTenantID(ctx, u))
14✔
176

14✔
177
        if err != nil {
22✔
178
                if strings.Contains(err.Error(), "duplicate key error") {
16✔
179
                        return store.ErrDuplicateEmail
8✔
180
                }
8✔
181

182
                return errors.Wrap(err, "failed to insert user")
183
        }
184

185
        return nil
6✔
186
}
187

188
func isDuplicateKeyError(err error) bool {
34✔
189
        const errCodeDupKey = 11000
34✔
190
        switch errType := err.(type) {
34✔
191
        case mongo.WriteException:
192
                if len(errType.WriteErrors) > 0 {
193
                        for _, we := range errType.WriteErrors {
194
                                if we.Code == errCodeDupKey {
195
                                        return true
196
                                }
197
                        }
198
                }
199
        case mongo.CommandError:
2✔
200
                if errType.Code == errCodeDupKey {
4✔
201
                        return true
2✔
202
                }
2✔
203
        }
204
        return false
32✔
205
}
206

207
// UpdateUser updates the user of a given ID.
208
// NOTE: This function supports using ETag matching using UserUpdate,
209
// but defaults to wildcard matching
210
func (db *DataStoreMongo) UpdateUser(
211
        ctx context.Context,
212
        id string,
213
        u *model.UserUpdate,
214
) (*model.User, error) {
16✔
215
        var updatedUser = new(model.User)
16✔
216
        //compute/set password hash
16✔
217
        if u.Password != "" {
30✔
218
                hash, err := bcrypt.GenerateFromPassword([]byte(u.Password), bcrypt.DefaultCost)
14✔
219
                if err != nil {
14✔
220
                        return nil, errors.Wrap(err,
221
                                "failed to generate password hash")
222
                }
223
                u.Password = string(hash)
14✔
224
        }
225

226
        now := time.Now().UTC()
16✔
227
        u.UpdatedTs = &now
16✔
228

16✔
229
        collUsers := db.client.
16✔
230
                Database(mstore.DbFromContext(ctx, DbName)).
16✔
231
                Collection(DbUsersColl)
16✔
232

16✔
233
        f := bson.M{"_id": id}
16✔
234
        if u.ETag != nil {
20✔
235
                if *u.ETag == model.ETagNil {
4✔
236
                        f["etag"] = bson.D{{Key: "$exists", Value: false}}
237
                } else {
4✔
238
                        f["etag"] = *u.ETag
4✔
239
                }
4✔
240
                if u.ETagUpdate == nil {
4✔
241
                        next := *u.ETag
242
                        next.Increment()
243
                        u.ETagUpdate = &next
244
                }
245
        }
246
        up := bson.M{"$set": u}
16✔
247
        fuOpts := mopts.FindOneAndUpdate().
16✔
248
                SetReturnDocument(mopts.Before)
16✔
249
        err := collUsers.FindOneAndUpdate(ctx, mstore.WithTenantID(ctx, f), up, fuOpts).
16✔
250
                Decode(updatedUser)
16✔
251

16✔
252
        switch {
16✔
253
        case err == mongo.ErrNoDocuments:
4✔
254
                return nil, store.ErrUserNotFound
4✔
255
        case isDuplicateKeyError(err):
2✔
256
                return nil, store.ErrDuplicateEmail
2✔
257
        case err != nil:
258
                return nil, errors.Wrap(err, "store: failed to update user")
259
        }
260

261
        return updatedUser, nil
10✔
262
}
263

264
func (db *DataStoreMongo) UpdateLoginTs(ctx context.Context, id string) error {
2✔
265
        collUsrs := db.client.
2✔
266
                Database(mstore.DbFromContext(ctx, DbName)).
2✔
267
                Collection(DbUsersColl)
2✔
268

2✔
269
        _, err := collUsrs.UpdateOne(ctx,
2✔
270
                mstore.WithTenantID(ctx, bson.D{{Key: "_id", Value: id}}),
2✔
271
                bson.D{{Key: "$set", Value: bson.D{
2✔
272
                        {Key: DbUserLoginTs, Value: time.Now()}},
2✔
273
                }},
2✔
274
        )
2✔
275
        return err
2✔
276
}
2✔
277

278
func (db *DataStoreMongo) GetUserByEmail(
279
        ctx context.Context,
280
        email model.Email,
281
) (*model.User, error) {
10✔
282
        var user model.User
10✔
283

10✔
284
        err := db.client.Database(mstore.DbFromContext(ctx, DbName)).
10✔
285
                Collection(DbUsersColl).
10✔
286
                FindOne(ctx, mstore.WithTenantID(ctx, bson.M{DbUserEmail: email})).
10✔
287
                Decode(&user)
10✔
288

10✔
289
        if err != nil {
14✔
290
                if err == mongo.ErrNoDocuments {
8✔
291
                        return nil, nil
4✔
292
                } else {
4✔
293
                        return nil, errors.Wrap(err, "failed to fetch user")
294
                }
295
        }
296

297
        return &user, nil
6✔
298
}
299

300
func (db *DataStoreMongo) GetUserById(ctx context.Context, id string) (*model.User, error) {
14✔
301
        user, err := db.GetUserAndPasswordById(ctx, id)
14✔
302
        if user != nil {
24✔
303
                user.Password = ""
10✔
304
        }
10✔
305
        return user, err
14✔
306
}
307

308
func (db *DataStoreMongo) GetUserAndPasswordById(
309
        ctx context.Context,
310
        id string,
311
) (*model.User, error) {
14✔
312
        var user model.User
14✔
313

14✔
314
        err := db.client.Database(mstore.DbFromContext(ctx, DbName)).
14✔
315
                Collection(DbUsersColl).
14✔
316
                FindOne(ctx, mstore.WithTenantID(ctx, bson.M{"_id": id})).
14✔
317
                Decode(&user)
14✔
318

14✔
319
        if err != nil {
18✔
320
                if err == mongo.ErrNoDocuments {
8✔
321
                        return nil, nil
4✔
322
                } else {
4✔
323
                        return nil, errors.Wrap(err, "failed to fetch user")
324
                }
325
        }
326

327
        return &user, nil
10✔
328
}
329

330
func (db *DataStoreMongo) GetTokenById(ctx context.Context, id oid.ObjectID) (*jwt.Token, error) {
10✔
331
        var token jwt.Token
10✔
332

10✔
333
        err := db.client.Database(mstore.DbFromContext(ctx, DbName)).
10✔
334
                Collection(DbTokensColl).
10✔
335
                FindOne(ctx, mstore.WithTenantID(ctx, bson.M{"_id": id})).
10✔
336
                Decode(&token)
10✔
337

10✔
338
        if err != nil {
14✔
339
                if err == mongo.ErrNoDocuments {
8✔
340
                        return nil, nil
4✔
341
                } else {
4✔
342
                        return nil, errors.Wrap(err, "failed to fetch token")
343
                }
344
        }
345

346
        return &token, nil
6✔
347
}
348

349
func (db *DataStoreMongo) GetUsers(
350
        ctx context.Context,
351
        fltr model.UserFilter,
352
) ([]model.User, error) {
16✔
353
        findOpts := mopts.Find().
16✔
354
                SetProjection(bson.M{DbUserPass: 0})
16✔
355

16✔
356
        collUsers := db.client.
16✔
357
                Database(mstore.DbFromContext(ctx, DbName)).
16✔
358
                Collection(DbUsersColl)
16✔
359

16✔
360
        var mgoFltr = bson.D{}
16✔
361
        if fltr.ID != nil {
18✔
362
                mgoFltr = append(mgoFltr, bson.E{Key: "_id", Value: bson.D{{
2✔
363
                        Key: "$in", Value: fltr.ID,
2✔
364
                }}})
2✔
365
        }
2✔
366
        if fltr.Email != nil {
18✔
367
                mgoFltr = append(mgoFltr, bson.E{Key: "email", Value: bson.D{{
2✔
368
                        Key: "$in", Value: fltr.Email,
2✔
369
                }}})
2✔
370
        }
2✔
371
        if fltr.CreatedAfter != nil {
18✔
372
                mgoFltr = append(mgoFltr, bson.E{
2✔
373
                        Key: "created_ts", Value: bson.D{{
2✔
374
                                Key: "$gt", Value: *fltr.CreatedAfter,
2✔
375
                        }},
2✔
376
                })
2✔
377
        }
2✔
378
        if fltr.CreatedBefore != nil {
18✔
379
                mgoFltr = append(mgoFltr, bson.E{
2✔
380
                        Key: "created_ts", Value: bson.D{{
2✔
381
                                Key: "$lt", Value: *fltr.CreatedBefore,
2✔
382
                        }},
2✔
383
                })
2✔
384
        }
2✔
385
        if fltr.UpdatedAfter != nil {
18✔
386
                mgoFltr = append(mgoFltr, bson.E{
2✔
387
                        Key: "updated_ts", Value: bson.D{{
2✔
388
                                Key: "$gt", Value: *fltr.UpdatedAfter,
2✔
389
                        }},
2✔
390
                })
2✔
391
        }
2✔
392
        if fltr.UpdatedBefore != nil {
18✔
393
                mgoFltr = append(mgoFltr, bson.E{
2✔
394
                        Key: "updated_ts", Value: bson.D{{
2✔
395
                                Key: "$lt", Value: *fltr.UpdatedBefore,
2✔
396
                        }},
2✔
397
                })
2✔
398
        }
2✔
399
        cur, err := collUsers.Find(ctx, mstore.WithTenantID(ctx, mgoFltr), findOpts)
16✔
400
        if err != nil {
18✔
401
                return nil, errors.Wrap(err, "store: failed to fetch users")
2✔
402
        }
2✔
403

404
        users := []model.User{}
14✔
405
        err = cur.All(ctx, &users)
14✔
406
        switch err {
14✔
407
        case nil, mongo.ErrNoDocuments:
14✔
408
                return users, nil
14✔
409
        default:
410
                return nil, errors.Wrap(err, "store: failed to decode users")
411
        }
412
}
413

414
func (db *DataStoreMongo) DeleteUser(ctx context.Context, id string) error {
6✔
415
        _, err := db.client.Database(mstore.DbFromContext(ctx, DbName)).
6✔
416
                Collection(DbUsersColl).
6✔
417
                DeleteOne(ctx, mstore.WithTenantID(ctx, bson.M{"_id": id}))
6✔
418

6✔
419
        if err != nil {
6✔
420
                return err
421
        }
422

423
        return nil
6✔
424
}
425

426
func (db *DataStoreMongo) SaveToken(ctx context.Context, token *jwt.Token) error {
22✔
427
        _, err := db.client.Database(mstore.DbFromContext(ctx, DbName)).
22✔
428
                Collection(DbTokensColl).
22✔
429
                InsertOne(ctx, mstore.WithTenantID(ctx, token))
22✔
430

22✔
431
        if isDuplicateKeyError(err) {
22✔
432
                return store.ErrDuplicateTokenName
433
        } else if err != nil {
22✔
434
                return errors.Wrap(err, "failed to store token")
435
        }
436
        return err
22✔
437
}
438

439
func (db *DataStoreMongo) EnsureSessionTokensLimit(ctx context.Context, userID oid.ObjectID,
440
        tokensLimit int) error {
6✔
441
        opts := &mopts.FindOptions{}
6✔
442
        opts.SetLimit(int64(tokensLimit))
6✔
443
        opts.SetSkip(int64(tokensLimit) - 1)
6✔
444
        opts.SetSort(bson.M{
6✔
445
                DbTokenIssuedAtTime: -1,
6✔
446
        })
6✔
447

6✔
448
        cur, err := db.client.Database(mstore.DbFromContext(ctx, DbName)).
6✔
449
                Collection(DbTokensColl).
6✔
450
                Find(ctx, mstore.WithTenantID(ctx, bson.M{
6✔
451
                        DbTokenSubject: userID,
6✔
452
                        DbTokenName:    bson.M{"$exists": false},
6✔
453
                }), opts)
6✔
454
        if err != nil {
6✔
NEW
455
                return err
NEW
456
        }
457

458
        tokens := []jwt.Token{}
6✔
459
        err = cur.All(ctx, &tokens)
6✔
460
        if err == mongo.ErrNoDocuments || len(tokens) == 0 {
6✔
461
                return nil
462
        } else if err != nil {
6✔
NEW
463
                return err
NEW
464
        }
465

466
        _, err = db.client.
6✔
467
                Database(mstore.DbFromContext(ctx, DbName)).
6✔
468
                Collection(DbTokensColl).
6✔
469
                DeleteMany(ctx, mstore.WithTenantID(ctx, bson.M{
6✔
470
                        DbTokenSubject: userID,
6✔
471
                        DbTokenName:    bson.M{"$exists": false},
6✔
472
                        DbTokenIssuedAtTime: bson.M{
6✔
473
                                "$lt": tokens[0].IssuedAt.Time,
6✔
474
                        },
6✔
475
                }))
6✔
476

6✔
477
        return err
6✔
478
}
479

480
// WithMultitenant enables multitenant support and returns a new datastore based
481
// on current one
482
func (db *DataStoreMongo) WithMultitenant() *DataStoreMongo {
2✔
483
        return &DataStoreMongo{
2✔
484
                client:      db.client,
2✔
485
                automigrate: db.automigrate,
2✔
486
                multitenant: true,
2✔
487
        }
2✔
488
}
2✔
489

490
// WithAutomigrate enables automatic migration and returns a new datastore based
491
// on current one
492
func (db *DataStoreMongo) WithAutomigrate() *DataStoreMongo {
54✔
493
        return &DataStoreMongo{
54✔
494
                client:      db.client,
54✔
495
                automigrate: true,
54✔
496
                multitenant: db.multitenant,
54✔
497
        }
54✔
498
}
54✔
499

500
func (db *DataStoreMongo) DeleteToken(ctx context.Context, userID, tokenID oid.ObjectID) error {
4✔
501
        _, err := db.client.
4✔
502
                Database(mstore.DbFromContext(ctx, DbName)).
4✔
503
                Collection(DbTokensColl).
4✔
504
                DeleteOne(ctx, mstore.WithTenantID(ctx, bson.M{DbID: tokenID, DbTokenSubject: userID}))
4✔
505
        return err
4✔
506
}
4✔
507

508
// deletes all tenant's tokens (identity in context)
509
func (db *DataStoreMongo) DeleteTokens(ctx context.Context) error {
6✔
510
        d, err := db.client.
6✔
511
                Database(mstore.DbFromContext(ctx, DbName)).
6✔
512
                Collection(DbTokensColl).
6✔
513
                DeleteMany(ctx, mstore.WithTenantID(ctx, bson.M{}))
6✔
514

6✔
515
        if err != nil {
6✔
516
                return err
517
        }
518

519
        if d.DeletedCount == 0 {
8✔
520
                return store.ErrTokenNotFound
2✔
521
        }
2✔
522

523
        return err
4✔
524
}
525

526
// deletes all user's tokens
527
func (db *DataStoreMongo) DeleteTokensByUserId(ctx context.Context, userId string) error {
6✔
528
        return db.DeleteTokensByUserIdExceptCurrentOne(ctx, userId, oid.ObjectID{})
6✔
529
}
6✔
530

531
// deletes all user's tokens except the current one
532
func (db *DataStoreMongo) DeleteTokensByUserIdExceptCurrentOne(
533
        ctx context.Context,
534
        userId string,
535
        tokenID oid.ObjectID,
536
) error {
8✔
537
        c := db.client.
8✔
538
                Database(mstore.DbFromContext(ctx, DbName)).
8✔
539
                Collection(DbTokensColl)
8✔
540

8✔
541
        id := oid.FromString(userId)
8✔
542
        filter := bson.M{
8✔
543
                "sub": id,
8✔
544
        }
8✔
545

8✔
546
        if tokenID != (oid.ObjectID{}) {
10✔
547
                filter["_id"] = bson.M{
2✔
548
                        "$ne": tokenID,
2✔
549
                }
2✔
550
        }
2✔
551

552
        _, err := c.DeleteMany(ctx, mstore.WithTenantID(ctx, filter))
8✔
553
        if err != nil {
8✔
554
                return errors.Wrap(err, "failed to remove tokens")
555
        }
556

557
        return nil
8✔
558
}
559

560
func (db *DataStoreMongo) SaveSettings(ctx context.Context, s *model.Settings, etag string) error {
12✔
561
        c := db.client.Database(mstore.DbFromContext(ctx, DbName)).
12✔
562
                Collection(DbSettingsColl)
12✔
563

12✔
564
        o := &mopts.ReplaceOptions{}
12✔
565
        o.SetUpsert(true)
12✔
566

12✔
567
        filters := bson.M{}
12✔
568
        if etag != "" {
16✔
569
                filters[DbSettingsEtag] = etag
4✔
570
        }
4✔
571
        _, err := c.ReplaceOne(ctx,
12✔
572
                mstore.WithTenantID(ctx, filters),
12✔
573
                mstore.WithTenantID(ctx, s),
12✔
574
                o,
12✔
575
        )
12✔
576
        if err != nil && !mongo.IsDuplicateKeyError(err) {
12✔
577
                return errors.Wrapf(err, "failed to store settings %v", s)
578
        } else if mongo.IsDuplicateKeyError(err) && etag != "" {
14✔
579
                return store.ErrETagMismatch
2✔
580
        }
2✔
581

582
        return err
10✔
583
}
584

585
func (db *DataStoreMongo) SaveUserSettings(ctx context.Context, userID string,
586
        s *model.Settings, etag string) error {
12✔
587
        c := db.client.Database(mstore.DbFromContext(ctx, DbName)).
12✔
588
                Collection(DbUserSettingsColl)
12✔
589

12✔
590
        o := &mopts.ReplaceOptions{}
12✔
591
        o.SetUpsert(true)
12✔
592

12✔
593
        filters := bson.M{
12✔
594
                DbSettingsUserID: userID,
12✔
595
        }
12✔
596
        if etag != "" {
16✔
597
                filters[DbSettingsEtag] = etag
4✔
598
        }
4✔
599
        settings := mstore.WithTenantID(ctx, s)
12✔
600
        settings = append(settings, bson.E{
12✔
601
                Key:   DbSettingsUserID,
12✔
602
                Value: userID,
12✔
603
        })
12✔
604
        _, err := c.ReplaceOne(ctx,
12✔
605
                mstore.WithTenantID(ctx, filters),
12✔
606
                settings,
12✔
607
                o,
12✔
608
        )
12✔
609
        if err != nil && !mongo.IsDuplicateKeyError(err) {
12✔
610
                return errors.Wrapf(err, "failed to store settings %v", s)
611
        } else if mongo.IsDuplicateKeyError(err) && etag != "" {
14✔
612
                return store.ErrETagMismatch
2✔
613
        }
2✔
614

615
        return err
10✔
616
}
617

618
func (db *DataStoreMongo) GetSettings(ctx context.Context) (*model.Settings, error) {
6✔
619
        c := db.client.Database(mstore.DbFromContext(ctx, DbName)).
6✔
620
                Collection(DbSettingsColl)
6✔
621

6✔
622
        var settings *model.Settings
6✔
623
        err := c.FindOne(ctx, mstore.WithTenantID(ctx, bson.M{})).Decode(&settings)
6✔
624

6✔
625
        switch err {
6✔
626
        case nil:
4✔
627
                return settings, nil
4✔
628
        case mongo.ErrNoDocuments:
2✔
629
                return nil, nil
2✔
630
        default:
631
                return nil, errors.Wrapf(err, "failed to get settings")
632
        }
633
}
634

635
func (db *DataStoreMongo) GetUserSettings(ctx context.Context,
636
        userID string) (*model.Settings, error) {
6✔
637
        c := db.client.Database(mstore.DbFromContext(ctx, DbName)).
6✔
638
                Collection(DbUserSettingsColl)
6✔
639

6✔
640
        filters := bson.M{
6✔
641
                DbSettingsUserID: userID,
6✔
642
        }
6✔
643
        var settings *model.Settings
6✔
644
        err := c.FindOne(ctx, mstore.WithTenantID(ctx, filters)).Decode(&settings)
6✔
645

6✔
646
        switch err {
6✔
647
        case nil:
4✔
648
                return settings, nil
4✔
649
        case mongo.ErrNoDocuments:
2✔
650
                return nil, nil
2✔
651
        default:
652
                return nil, errors.Wrapf(err, "failed to get settings")
653
        }
654
}
655

656
func (db *DataStoreMongo) GetPersonalAccessTokens(
657
        ctx context.Context,
658
        userID string,
659
) ([]model.PersonalAccessToken, error) {
12✔
660
        findOpts := mopts.Find().
12✔
661
                SetProjection(
12✔
662
                        bson.M{
12✔
663
                                DbID:             1,
12✔
664
                                DbTokenName:      1,
12✔
665
                                DbTokenExpiresAt: 1,
12✔
666
                                DbTokenLastUsed:  1,
12✔
667
                                DbTokenIssuedAt:  1,
12✔
668
                        },
12✔
669
                )
12✔
670

12✔
671
        collTokens := db.client.
12✔
672
                Database(mstore.DbFromContext(ctx, DbName)).
12✔
673
                Collection(DbTokensColl)
12✔
674

12✔
675
        var mgoFltr = bson.M{
12✔
676
                DbTokenSubject: oid.FromString(userID),
12✔
677
                DbTokenName:    bson.M{"$exists": true},
12✔
678
        }
12✔
679
        cur, err := collTokens.Find(ctx, mstore.WithTenantID(ctx, mgoFltr), findOpts)
12✔
680
        if err != nil {
12✔
681
                return nil, errors.Wrap(err, "store: failed to fetch tokens")
682
        }
683

684
        tokens := []model.PersonalAccessToken{}
12✔
685
        err = cur.All(ctx, &tokens)
12✔
686
        switch err {
12✔
687
        case nil, mongo.ErrNoDocuments:
12✔
688
                return tokens, nil
12✔
689
        default:
690
                return nil, errors.Wrap(err, "store: failed to decode tokens")
691
        }
692
}
693

694
func (db *DataStoreMongo) UpdateTokenLastUsed(ctx context.Context, id oid.ObjectID) error {
4✔
695
        collTokens := db.client.
4✔
696
                Database(mstore.DbFromContext(ctx, DbName)).
4✔
697
                Collection(DbTokensColl)
4✔
698

4✔
699
        _, err := collTokens.UpdateOne(ctx,
4✔
700
                mstore.WithTenantID(ctx, bson.D{{Key: DbID, Value: id}}),
4✔
701
                bson.D{{Key: "$set", Value: bson.D{
4✔
702
                        {Key: DbTokenLastUsed, Value: time.Now()}},
4✔
703
                }},
4✔
704
        )
4✔
705

4✔
706
        return err
4✔
707
}
4✔
708

709
func (db *DataStoreMongo) CountPersonalAccessTokens(
710
        ctx context.Context,
711
        userID string,
712
) (int64, error) {
4✔
713
        collTokens := db.client.
4✔
714
                Database(mstore.DbFromContext(ctx, DbName)).
4✔
715
                Collection(DbTokensColl)
4✔
716

4✔
717
        var mgoFltr = bson.M{
4✔
718
                DbTokenSubject: oid.FromString(userID),
4✔
719
                DbTokenName:    bson.M{"$exists": true},
4✔
720
        }
4✔
721
        count, err := collTokens.CountDocuments(ctx, mstore.WithTenantID(ctx, mgoFltr))
4✔
722
        if err != nil {
4✔
723
                return -1, errors.Wrap(err, "store: failed to count tokens")
724
        }
725
        return count, nil
4✔
726
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc