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

mendersoftware / deviceauth / 1032492972

05 Oct 2023 08:34AM UTC coverage: 83.678% (-2.5%) from 86.171%
1032492972

push

gitlab-ci

web-flow
Merge pull request #672 from tranchitella/men-5497

feat: delay JWT token signature and claims validation after cache look-up

41 of 53 new or added lines in 3 files covered. (77.36%)

1 existing line in 1 file now uncovered.

4768 of 5698 relevant lines covered (83.68%)

47.05 hits per line

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

90.11
/devauth/devauth.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
package devauth
15

16
import (
17
        "context"
18
        "crypto/sha256"
19
        "encoding/json"
20
        "strings"
21
        "time"
22

23
        "github.com/mendersoftware/go-lib-micro/addons"
24
        ctxhttpheader "github.com/mendersoftware/go-lib-micro/context/httpheader"
25
        "github.com/mendersoftware/go-lib-micro/identity"
26
        "github.com/mendersoftware/go-lib-micro/log"
27
        "github.com/mendersoftware/go-lib-micro/mongo/oid"
28
        "github.com/mendersoftware/go-lib-micro/plan"
29
        "github.com/mendersoftware/go-lib-micro/ratelimits"
30
        "github.com/mendersoftware/go-lib-micro/requestid"
31
        "github.com/pkg/errors"
32
        "go.mongodb.org/mongo-driver/bson"
33

34
        "github.com/mendersoftware/deviceauth/access"
35
        "github.com/mendersoftware/deviceauth/cache"
36
        "github.com/mendersoftware/deviceauth/client/inventory"
37
        "github.com/mendersoftware/deviceauth/client/orchestrator"
38
        "github.com/mendersoftware/deviceauth/client/tenant"
39
        "github.com/mendersoftware/deviceauth/jwt"
40
        "github.com/mendersoftware/deviceauth/model"
41
        "github.com/mendersoftware/deviceauth/store"
42
        "github.com/mendersoftware/deviceauth/utils"
43
        uto "github.com/mendersoftware/deviceauth/utils/to"
44
)
45

46
const (
47
        MsgErrDevAuthUnauthorized = "dev auth: unauthorized"
48
        MsgErrDevAuthBadRequest   = "dev auth: bad request"
49
)
50

51
var (
52
        ErrDevAuthUnauthorized   = errors.New(MsgErrDevAuthUnauthorized)
53
        ErrDevIdAuthIdMismatch   = errors.New("dev auth: dev ID and auth ID mismatch")
54
        ErrMaxDeviceCountReached = errors.New("maximum number of accepted devices reached")
55
        ErrDeviceExists          = errors.New("device already exists")
56
        ErrDeviceNotFound        = errors.New("device not found")
57
        ErrDevAuthBadRequest     = errors.New(MsgErrDevAuthBadRequest)
58

59
        ErrInvalidDeviceID  = errors.New("invalid device ID type")
60
        ErrInvalidAuthSetID = errors.New("auth set id is not a valid ID")
61
)
62

63
func IsErrDevAuthUnauthorized(e error) bool {
1✔
64
        return strings.HasPrefix(e.Error(), MsgErrDevAuthUnauthorized)
1✔
65
}
1✔
66

67
func MakeErrDevAuthUnauthorized(e error) error {
6✔
68
        return errors.Wrap(e, MsgErrDevAuthUnauthorized)
6✔
69
}
6✔
70

71
func IsErrDevAuthBadRequest(e error) bool {
1✔
72
        return strings.HasPrefix(e.Error(), MsgErrDevAuthBadRequest)
1✔
73
}
1✔
74

75
func MakeErrDevAuthBadRequest(e error) error {
2✔
76
        return errors.Wrap(e, MsgErrDevAuthBadRequest)
2✔
77
}
2✔
78

79
// this device auth service interface
80
//
81
//go:generate ../utils/mockgen.sh
82
type App interface {
83
        HealthCheck(ctx context.Context) error
84
        SubmitAuthRequest(ctx context.Context, r *model.AuthReq) (string, error)
85

86
        GetDevices(
87
                ctx context.Context,
88
                skip,
89
                limit uint,
90
                filter model.DeviceFilter,
91
        ) ([]model.Device, error)
92
        GetDevice(ctx context.Context, dev_id string) (*model.Device, error)
93
        DecommissionDevice(ctx context.Context, dev_id string) error
94
        DeleteDevice(ctx context.Context, dev_id string) error
95
        DeleteAuthSet(ctx context.Context, dev_id string, auth_id string) error
96
        AcceptDeviceAuth(ctx context.Context, dev_id string, auth_id string) error
97
        RejectDeviceAuth(ctx context.Context, dev_id string, auth_id string) error
98
        ResetDeviceAuth(ctx context.Context, dev_id string, auth_id string) error
99
        PreauthorizeDevice(ctx context.Context, req *model.PreAuthReq) (*model.Device, error)
100

101
        RevokeToken(ctx context.Context, tokenID string) error
102
        VerifyToken(ctx context.Context, token string) error
103
        DeleteTokens(ctx context.Context, tenantID, deviceID string) error
104

105
        SetTenantLimit(ctx context.Context, tenant_id string, limit model.Limit) error
106
        DeleteTenantLimit(ctx context.Context, tenant_id string, limit string) error
107

108
        GetLimit(ctx context.Context, name string) (*model.Limit, error)
109
        GetTenantLimit(ctx context.Context, name, tenant_id string) (*model.Limit, error)
110

111
        GetDevCountByStatus(ctx context.Context, status string) (int, error)
112

113
        GetTenantDeviceStatus(ctx context.Context, tenantId, deviceId string) (*model.Status, error)
114
}
115

116
type DevAuth struct {
117
        db           store.DataStore
118
        invClient    inventory.Client
119
        cOrch        orchestrator.ClientRunner
120
        cTenant      tenant.ClientRunner
121
        jwt          jwt.Handler
122
        verifyTenant bool
123
        config       Config
124
        cache        cache.Cache
125
        clock        utils.Clock
126
        checker      access.Checker
127
}
128

129
type Config struct {
130
        // token issuer
131
        Issuer string
132
        // token expiration time
133
        ExpirationTime int64
134
        // Default tenant token to use when the client supplies none. Can be
135
        // empty
136
        DefaultTenantToken string
137
        InventoryAddr      string
138

139
        EnableReporting bool
140
        HaveAddons      bool
141
}
142

143
func NewDevAuth(d store.DataStore, co orchestrator.ClientRunner,
144
        jwt jwt.Handler, config Config) *DevAuth {
148✔
145
        // initialize checker using an empty merge (returns nil on validate)
148✔
146
        checker := access.Merge()
148✔
147
        if config.HaveAddons {
149✔
148
                checker = access.NewAddonChecker()
1✔
149
        }
1✔
150

151
        return &DevAuth{
148✔
152
                db:           d,
148✔
153
                invClient:    inventory.NewClient(config.InventoryAddr, false),
148✔
154
                cOrch:        co,
148✔
155
                jwt:          jwt,
148✔
156
                verifyTenant: false,
148✔
157
                config:       config,
148✔
158
                clock:        utils.NewClock(),
148✔
159
                checker:      checker,
148✔
160
        }
148✔
161
}
162

163
func (d *DevAuth) HealthCheck(ctx context.Context) error {
6✔
164
        err := d.db.Ping(ctx)
6✔
165
        if err != nil {
7✔
166
                return errors.Wrap(err, "error reaching MongoDB")
1✔
167
        }
1✔
168
        err = d.invClient.CheckHealth(ctx)
5✔
169
        if err != nil {
6✔
170
                return errors.Wrap(err, "Inventory service unhealthy")
1✔
171
        }
1✔
172
        err = d.cOrch.CheckHealth(ctx)
4✔
173
        if err != nil {
5✔
174
                return errors.Wrap(err, "Workflows service unhealthy")
1✔
175
        }
1✔
176
        if d.verifyTenant {
5✔
177
                err = d.cTenant.CheckHealth(ctx)
2✔
178
                if err != nil {
3✔
179
                        return errors.Wrap(err, "Tenantadm service unhealthy")
1✔
180
                }
1✔
181
        }
182
        return nil
2✔
183
}
184

185
func (d *DevAuth) setDeviceIdentity(ctx context.Context, dev *model.Device, tenantId string) error {
4✔
186
        attributes := make([]model.DeviceAttribute, len(dev.IdDataStruct))
4✔
187
        i := 0
4✔
188
        for name, value := range dev.IdDataStruct {
8✔
189
                if name == "status" {
4✔
190
                        //we have to forbid the client to override attribute status in identity scope
×
191
                        //since it stands for status of a device (as in: accepted, rejected, preauthorized)
×
192
                        continue
×
193
                }
194
                attribute := model.DeviceAttribute{
4✔
195
                        Name:        name,
4✔
196
                        Description: nil,
4✔
197
                        Value:       value,
4✔
198
                        Scope:       "identity",
4✔
199
                }
4✔
200
                attributes[i] = attribute
4✔
201
                i++
4✔
202
        }
203
        attrJson, err := json.Marshal(attributes)
4✔
204
        if err != nil {
4✔
205
                return errors.New("internal error: cannot marshal attributes into json")
×
206
        }
×
207
        if err := d.cOrch.SubmitUpdateDeviceInventoryJob(
4✔
208
                ctx,
4✔
209
                orchestrator.UpdateDeviceInventoryReq{
4✔
210
                        RequestId:  requestid.FromContext(ctx),
4✔
211
                        TenantId:   tenantId,
4✔
212
                        DeviceId:   dev.Id,
4✔
213
                        Scope:      "identity",
4✔
214
                        Attributes: string(attrJson),
4✔
215
                }); err != nil {
4✔
216
                return errors.Wrap(err, "failed to start device inventory update job")
×
217
        }
×
218
        if d.config.EnableReporting {
6✔
219
                if err := d.cOrch.SubmitReindexReporting(ctx, string(dev.Id)); err != nil {
2✔
220
                        return errors.Wrap(err, "reindex reporting job error")
×
221
                }
×
222
        }
223
        return nil
4✔
224
}
225

226
func (d *DevAuth) getDeviceFromAuthRequest(
227
        ctx context.Context,
228
        r *model.AuthReq,
229
) (*model.Device, error) {
13✔
230
        dev := model.NewDevice("", r.IdData, r.PubKey)
13✔
231

13✔
232
        l := log.FromContext(ctx)
13✔
233

13✔
234
        idDataStruct, idDataSha256, err := parseIdData(r.IdData)
13✔
235
        if err != nil {
13✔
236
                return nil, MakeErrDevAuthBadRequest(err)
×
237
        }
×
238

239
        dev.IdDataStruct = idDataStruct
13✔
240
        dev.IdDataSha256 = idDataSha256
13✔
241

13✔
242
        // record device
13✔
243
        err = d.db.AddDevice(ctx, *dev)
13✔
244
        addDeviceErr := err
13✔
245
        if err != nil && err != store.ErrObjectExists {
14✔
246
                l.Errorf("failed to add/find device: %v", err)
1✔
247
                return nil, err
1✔
248
        }
1✔
249

250
        // either the device was added or it was already present, in any case,
251
        // pull it from DB
252
        dev, err = d.db.GetDeviceByIdentityDataHash(ctx, idDataSha256)
12✔
253
        if err != nil {
13✔
254
                l.Error("failed to find device but could not add either")
1✔
255
                return nil, errors.New("failed to locate device")
1✔
256
        }
1✔
257

258
        idData := identity.FromContext(ctx)
11✔
259
        tenantId := ""
11✔
260
        if idData != nil {
17✔
261
                tenantId = idData.Tenant
6✔
262
        }
6✔
263
        if addDeviceErr != store.ErrObjectExists {
13✔
264
                if err := d.setDeviceIdentity(ctx, dev, tenantId); err != nil {
2✔
265
                        return nil, err
×
266
                }
×
267
        }
268

269
        // check if the device is in the decommissioning state
270
        if dev.Decommissioning {
11✔
271
                l.Warnf("Device %s in the decommissioning state.", dev.Id)
×
272
                return nil, ErrDevAuthUnauthorized
×
273
        }
×
274

275
        return dev, nil
11✔
276
}
277

278
func (d *DevAuth) signToken(ctx context.Context) jwt.SignFunc {
6✔
279
        return func(t *jwt.Token) (string, error) {
12✔
280
                return d.jwt.ToJWT(t)
6✔
281
        }
6✔
282
}
283

284
func (d *DevAuth) doVerifyTenant(ctx context.Context, token string) (*tenant.Tenant, error) {
11✔
285
        t, err := d.cTenant.VerifyToken(ctx, token)
11✔
286

11✔
287
        if err != nil {
17✔
288
                if tenant.IsErrTokenVerificationFailed(err) {
10✔
289
                        return nil, MakeErrDevAuthUnauthorized(err)
4✔
290
                }
4✔
291

292
                return nil, errors.Wrap(err, "request to verify tenant token failed")
3✔
293
        }
294

295
        return t, nil
6✔
296
}
297

298
func (d *DevAuth) getTenantWithDefault(
299
        ctx context.Context,
300
        tenantToken,
301
        defaultToken string,
302
) (context.Context, *tenant.Tenant, error) {
10✔
303
        l := log.FromContext(ctx)
10✔
304

10✔
305
        if tenantToken == "" && defaultToken == "" {
12✔
306
                return nil, nil, MakeErrDevAuthUnauthorized(errors.New("tenant token missing"))
2✔
307
        }
2✔
308

309
        var t *tenant.Tenant
9✔
310
        var err error
9✔
311

9✔
312
        // try the provided token
9✔
313
        // but continue on errors and maybe try the default token
9✔
314
        if tenantToken != "" {
17✔
315
                t, err = d.doVerifyTenant(ctx, tenantToken)
8✔
316

8✔
317
                if err != nil {
13✔
318
                        l.Errorf("Failed to verify supplied tenant token: %s", err.Error())
5✔
319
                }
5✔
320
        }
321

322
        // if we still haven't selected a tenant - the token didn't work
323
        // try the default one
324
        if t == nil && defaultToken != "" {
12✔
325
                t, err = d.doVerifyTenant(ctx, defaultToken)
3✔
326
                if err != nil {
4✔
327
                        l.Errorf("Failed to verify default tenant token: %s", err.Error())
1✔
328
                }
1✔
329
        }
330

331
        // none of the tokens worked
332
        if err != nil {
13✔
333
                if tenant.IsErrTokenVerificationFailed(err) {
4✔
334
                        return ctx, nil, MakeErrDevAuthUnauthorized(err)
×
335
                }
×
336
                return ctx, nil, err
4✔
337
        }
338

339
        tCtx := identity.WithContext(ctx, &identity.Identity{
6✔
340
                Subject: "internal",
6✔
341
                Tenant:  t.ID,
6✔
342
        })
6✔
343

6✔
344
        return tCtx, t, nil
6✔
345
}
346

347
func (d *DevAuth) SubmitAuthRequest(ctx context.Context, r *model.AuthReq) (string, error) {
26✔
348
        l := log.FromContext(ctx)
26✔
349

26✔
350
        var tenant *tenant.Tenant
26✔
351
        var err error
26✔
352

26✔
353
        if d.verifyTenant {
36✔
354
                ctx, tenant, err = d.getTenantWithDefault(ctx, r.TenantToken, d.config.DefaultTenantToken)
10✔
355
                if err != nil {
15✔
356
                        return "", err
5✔
357
                }
5✔
358
        } else {
17✔
359
                // ignore identity data when tenant verification is off
17✔
360
                // it's possible that the device will provide old auth token or old tenant token
17✔
361
                // in the authorization header;
17✔
362
                // in that case we need to wipe identity data from the context
17✔
363
                ctx = identity.WithContext(ctx, nil)
17✔
364
        }
17✔
365

366
        // first, try to handle preauthorization
367
        authSet, err := d.processPreAuthRequest(ctx, r)
22✔
368
        if err != nil {
29✔
369
                return "", err
7✔
370
        }
7✔
371

372
        // if not a preauth request, process with regular auth request handling
373
        if authSet == nil {
28✔
374
                authSet, err = d.processAuthRequest(ctx, r)
13✔
375
                if err != nil {
16✔
376
                        return "", err
3✔
377
                }
3✔
378
        }
379

380
        // request was already present in DB, check its status
381
        if authSet.Status == model.DevStatusAccepted {
18✔
382
                jti := oid.FromString(authSet.Id)
6✔
383
                if jti.String() == "" {
6✔
384
                        return "", ErrInvalidAuthSetID
×
385
                }
×
386
                sub := oid.FromString(authSet.DeviceId)
6✔
387
                if sub.String() == "" {
6✔
388
                        return "", ErrInvalidDeviceID
×
389
                }
×
390
                now := time.Now()
6✔
391
                token := &jwt.Token{Claims: jwt.Claims{
6✔
392
                        ID:      jti,
6✔
393
                        Subject: sub,
6✔
394
                        Issuer:  d.config.Issuer,
6✔
395
                        ExpiresAt: jwt.Time{
6✔
396
                                Time: now.Add(time.Second *
6✔
397
                                        time.Duration(d.config.ExpirationTime)),
6✔
398
                        },
6✔
399
                        IssuedAt: jwt.Time{Time: now},
6✔
400
                        Device:   true,
6✔
401
                }}
6✔
402

6✔
403
                if d.verifyTenant {
9✔
404
                        token.Claims.Tenant = tenant.ID
3✔
405
                        token.Claims.Plan = tenant.Plan
3✔
406
                        token.Claims.Addons = tenant.Addons
3✔
407
                        token.Claims.Trial = tenant.Trial
3✔
408
                } else {
7✔
409
                        token.Claims.Plan = plan.PlanEnterprise
4✔
410
                        token.Addons = addons.AllAddonsEnabled
4✔
411
                }
4✔
412

413
                // sign and encode as JWT
414
                raw, err := token.MarshalJWT(d.signToken(ctx))
6✔
415
                if err != nil {
6✔
416
                        return "", errors.Wrap(err, "generate token error")
×
417
                }
×
418

419
                if err := d.db.AddToken(ctx, token); err != nil {
6✔
420
                        return "", errors.Wrap(err, "add token error")
×
421
                }
×
422

423
                l.Infof("Token %s assigned to device %s",
6✔
424
                        token.Claims.ID, token.Claims.Subject)
6✔
425
                d.updateCheckInTime(ctx, authSet.DeviceId, token.Claims.Tenant, nil)
6✔
426
                return string(raw), nil
6✔
427
        }
428

429
        // no token, return device unauthorized
430
        return "", ErrDevAuthUnauthorized
7✔
431

432
}
433

434
func (d *DevAuth) processPreAuthRequest(
435
        ctx context.Context,
436
        r *model.AuthReq,
437
) (*model.AuthSet, error) {
22✔
438
        var deviceAlreadyAccepted bool
22✔
439

22✔
440
        _, idDataSha256, err := parseIdData(r.IdData)
22✔
441
        if err != nil {
23✔
442
                return nil, MakeErrDevAuthBadRequest(err)
1✔
443
        }
1✔
444

445
        // authset exists?
446
        aset, err := d.db.GetAuthSetByIdDataHashKey(ctx, idDataSha256, r.PubKey)
21✔
447
        switch err {
21✔
448
        case nil:
19✔
449
                break
19✔
450
        case store.ErrAuthSetNotFound:
2✔
451
                return nil, nil
2✔
452
        default:
1✔
453
                return nil, errors.Wrap(err, "failed to fetch auth set")
1✔
454
        }
455

456
        // if authset status is not 'preauthorized', nothing to do
457
        if aset.Status != model.DevStatusPreauth {
31✔
458
                return nil, nil
12✔
459
        }
12✔
460

461
        // check the device status
462
        // if the device status is accepted then do not trigger provisioning workflow
463
        // this needs to be checked before changing authentication set status
464
        dev, err := d.db.GetDeviceById(ctx, aset.DeviceId)
7✔
465
        if err != nil {
8✔
466
                return nil, err
1✔
467
        }
1✔
468

469
        // check if the device is in the decommissioning state
470
        if dev.Decommissioning {
7✔
471
                l := log.FromContext(ctx)
1✔
472
                l.Warnf("Device %s in the decommissioning state.", dev.Id)
1✔
473
                return nil, ErrDevAuthUnauthorized
1✔
474
        }
1✔
475

476
        currentStatus := dev.Status
5✔
477
        if dev.Status == model.DevStatusAccepted {
6✔
478
                deviceAlreadyAccepted = true
1✔
479
        }
1✔
480

481
        // auth set is ok for auto-accepting, check device limit
482
        allow, err := d.canAcceptDevice(ctx)
5✔
483
        if err != nil {
6✔
484
                return nil, err
1✔
485
        }
1✔
486

487
        if !allow {
5✔
488
                return nil, ErrMaxDeviceCountReached
1✔
489
        }
1✔
490

491
        update := model.AuthSetUpdate{
3✔
492
                Status: model.DevStatusAccepted,
3✔
493
        }
3✔
494
        // persist the 'accepted' status in both auth set, and device
3✔
495
        if err := d.db.UpdateAuthSetById(ctx, aset.Id, update); err != nil {
3✔
496
                return nil, errors.Wrap(err, "failed to update auth set status")
×
497
        }
×
498

499
        if err := d.updateDeviceStatus(
3✔
500
                ctx,
3✔
501
                aset.DeviceId,
3✔
502
                model.DevStatusAccepted,
3✔
503
                currentStatus,
3✔
504
        ); err != nil {
3✔
505
                return nil, err
×
506
        }
×
507

508
        aset.Status = model.DevStatusAccepted
3✔
509
        dev.Status = model.DevStatusAccepted
3✔
510
        dev.AuthSets = append(dev.AuthSets, *aset)
3✔
511

3✔
512
        if !deviceAlreadyAccepted {
5✔
513
                reqId := requestid.FromContext(ctx)
2✔
514
                var tenantID string
2✔
515
                if idty := identity.FromContext(ctx); idty != nil {
2✔
516
                        tenantID = idty.Tenant
×
517
                }
×
518

519
                // submit device accepted job
520
                if err := d.cOrch.SubmitProvisionDeviceJob(
2✔
521
                        ctx,
2✔
522
                        orchestrator.ProvisionDeviceReq{
2✔
523
                                RequestId: reqId,
2✔
524
                                DeviceID:  aset.DeviceId,
2✔
525
                                TenantID:  tenantID,
2✔
526
                                Device:    dev,
2✔
527
                                Status:    dev.Status,
2✔
528
                        }); err != nil {
3✔
529
                        return nil, errors.Wrap(err, "submit device provisioning job error")
1✔
530
                }
1✔
531
        }
532
        return aset, nil
2✔
533
}
534

535
func (d *DevAuth) updateDeviceStatus(
536
        ctx context.Context,
537
        devId,
538
        status string,
539
        currentStatus string,
540
) error {
34✔
541
        newStatus, err := d.db.GetDeviceStatus(ctx, devId)
34✔
542
        if currentStatus == newStatus {
37✔
543
                return nil
3✔
544
        }
3✔
545
        if status == "" {
58✔
546
                switch err {
26✔
547
                case nil:
24✔
548
                        status = newStatus
24✔
549
                case store.ErrAuthSetNotFound:
2✔
550
                        status = model.DevStatusNoAuth
2✔
551
                default:
1✔
552
                        return errors.Wrap(err, "Cannot determine device status")
1✔
553
                }
554
        }
555

556
        // submit device status change job
557
        dev, err := d.db.GetDeviceById(ctx, devId)
31✔
558
        if err != nil {
31✔
559
                return errors.Wrap(err, "db get device by id error")
×
560
        }
×
561

562
        tenantId := ""
31✔
563
        idData := identity.FromContext(ctx)
31✔
564
        if idData != nil {
39✔
565
                tenantId = idData.Tenant
8✔
566
        }
8✔
567
        req := orchestrator.UpdateDeviceStatusReq{
31✔
568
                RequestId: requestid.FromContext(ctx),
31✔
569
                Devices: []model.DeviceInventoryUpdate{{
31✔
570
                        Id:       dev.Id,
31✔
571
                        Revision: dev.Revision + 1,
31✔
572
                }},
31✔
573
                TenantId: tenantId,
31✔
574
                Status:   status,
31✔
575
        }
31✔
576
        if err := d.cOrch.SubmitUpdateDeviceStatusJob(ctx, req); err != nil {
32✔
577
                return errors.Wrap(err, "update device status job error")
1✔
578
        }
1✔
579

580
        if err := d.db.UpdateDevice(ctx,
31✔
581
                devId,
31✔
582
                model.DeviceUpdate{
31✔
583
                        Status:    status,
31✔
584
                        UpdatedTs: uto.TimePtr(time.Now().UTC()),
31✔
585
                }); err != nil {
32✔
586
                return errors.Wrap(err, "failed to update device status")
1✔
587
        }
1✔
588

589
        if d.config.EnableReporting {
33✔
590
                if err := d.cOrch.SubmitReindexReporting(ctx, devId); err != nil {
4✔
591
                        return errors.Wrap(err, "reindex reporting job error")
1✔
592
                }
1✔
593
        }
594

595
        return nil
29✔
596
}
597

598
// processAuthRequest will process incoming auth request and record authentication
599
// data information it contains. Returns a tupe (auth set, error). If no errors were
600
// present, model.AuthSet.Status will indicate the status of device admission
601
func (d *DevAuth) processAuthRequest(
602
        ctx context.Context,
603
        r *model.AuthReq,
604
) (*model.AuthSet, error) {
13✔
605

13✔
606
        l := log.FromContext(ctx)
13✔
607

13✔
608
        // get device associated with given authorization request
13✔
609
        dev, err := d.getDeviceFromAuthRequest(ctx, r)
13✔
610
        if err != nil {
15✔
611
                return nil, err
2✔
612
        }
2✔
613

614
        idDataStruct, idDataSha256, err := parseIdData(r.IdData)
11✔
615
        if err != nil {
11✔
616
                return nil, MakeErrDevAuthBadRequest(err)
×
617
        }
×
618

619
        areq := &model.AuthSet{
11✔
620
                Id:           oid.NewUUIDv4().String(),
11✔
621
                IdData:       r.IdData,
11✔
622
                IdDataStruct: idDataStruct,
11✔
623
                IdDataSha256: idDataSha256,
11✔
624
                PubKey:       r.PubKey,
11✔
625
                DeviceId:     dev.Id,
11✔
626
                Status:       model.DevStatusPending,
11✔
627
                Timestamp:    uto.TimePtr(time.Now()),
11✔
628
        }
11✔
629

11✔
630
        // record authentication request
11✔
631
        err = d.db.AddAuthSet(ctx, *areq)
11✔
632
        if err != nil && err != store.ErrObjectExists {
11✔
633
                return nil, err
×
634
        }
×
635

636
        // update the device status
637
        if err := d.updateDeviceStatus(ctx, dev.Id, "", dev.Status); err != nil {
11✔
638
                return nil, err
×
639
        }
×
640

641
        // either the request was added or it was already present in the DB, get
642
        // it now
643
        areq, err = d.db.GetAuthSetByIdDataHashKey(ctx, idDataSha256, r.PubKey)
11✔
644
        if err != nil {
12✔
645
                l.Error("failed to find device auth set but could not add one either")
1✔
646
                return nil, errors.New("failed to locate device auth set")
1✔
647
        }
1✔
648

649
        return areq, nil
10✔
650
}
651

652
func (d *DevAuth) GetDevices(
653
        ctx context.Context,
654
        skip,
655
        limit uint,
656
        filter model.DeviceFilter,
657
) ([]model.Device, error) {
3✔
658
        devs, err := d.db.GetDevices(ctx, skip, limit, filter)
3✔
659
        if err != nil {
3✔
660
                return nil, errors.Wrap(err, "failed to list devices")
×
661
        }
×
662

663
        for i := range devs {
6✔
664
                devs[i].AuthSets, err = d.db.GetAuthSetsForDevice(ctx, devs[i].Id)
3✔
665
                if err != nil && err != store.ErrAuthSetNotFound {
3✔
666
                        return nil, errors.Wrap(err, "db get auth sets error")
×
667
                }
×
668
        }
669

670
        // update check-in time
671
        if d.cache != nil {
5✔
672
                tenantID := ""
2✔
673
                idData := identity.FromContext(ctx)
2✔
674
                if idData != nil {
4✔
675
                        tenantID = idData.Tenant
2✔
676
                }
2✔
677

678
                ids := make([]string, len(devs))
2✔
679
                for i := range devs {
4✔
680
                        ids[i] = devs[i].Id
2✔
681
                }
2✔
682
                checkInTimes, err := d.cache.GetCheckInTimes(ctx, tenantID, ids)
2✔
683
                if err != nil {
3✔
684
                        l := log.FromContext(ctx)
1✔
685
                        l.Errorf("Failed to get check-in times for devices")
1✔
686
                } else {
2✔
687
                        for i := range devs {
2✔
688
                                if checkInTimes[i] != nil {
2✔
689
                                        devs[i].CheckInTime = checkInTimes[i]
1✔
690
                                }
1✔
691
                        }
692
                }
693
        }
694

695
        return devs, err
3✔
696
}
697

698
func (d *DevAuth) GetDevice(ctx context.Context, devId string) (*model.Device, error) {
3✔
699
        dev, err := d.db.GetDeviceById(ctx, devId)
3✔
700
        if err != nil {
4✔
701
                if err != store.ErrDevNotFound {
1✔
702
                        return nil, errors.Wrap(err, "db get device by id error")
×
703
                }
×
704
                return nil, err
1✔
705
        }
706

707
        dev.AuthSets, err = d.db.GetAuthSetsForDevice(ctx, dev.Id)
3✔
708
        if err != nil {
3✔
709
                if err != store.ErrAuthSetNotFound {
×
710
                        return nil, errors.Wrap(err, "db get auth sets error")
×
711
                }
×
712
                return dev, nil
×
713
        }
714

715
        if d.cache != nil {
5✔
716
                tenantID := ""
2✔
717
                idData := identity.FromContext(ctx)
2✔
718
                if idData != nil {
4✔
719
                        tenantID = idData.Tenant
2✔
720
                }
2✔
721

722
                checkInTime, err := d.cache.GetCheckInTime(ctx, tenantID, devId)
2✔
723
                if err != nil {
3✔
724
                        l := log.FromContext(ctx)
1✔
725
                        l.Errorf("Failed to get check-in times for device")
1✔
726
                } else if checkInTime != nil {
3✔
727
                        dev.CheckInTime = checkInTime
1✔
728
                }
1✔
729
        }
730

731
        return dev, err
3✔
732
}
733

734
// DecommissionDevice deletes device and all its tokens
735
func (d *DevAuth) DecommissionDevice(ctx context.Context, devID string) error {
9✔
736

9✔
737
        l := log.FromContext(ctx)
9✔
738

9✔
739
        l.Warnf("Decommission device with id: %s", devID)
9✔
740

9✔
741
        err := d.cacheDeleteToken(ctx, devID)
9✔
742
        if err != nil {
12✔
743
                return errors.Wrapf(err, "failed to delete token for %s from cache", devID)
3✔
744
        }
3✔
745

746
        // set decommissioning flag on the device
747
        updev := model.DeviceUpdate{
6✔
748
                Decommissioning: uto.BoolPtr(true),
6✔
749
        }
6✔
750
        if err := d.db.UpdateDevice(
6✔
751
                ctx, devID, updev,
6✔
752
        ); err != nil {
9✔
753
                return err
3✔
754
        }
3✔
755

756
        reqId := requestid.FromContext(ctx)
4✔
757

4✔
758
        tenantID := ""
4✔
759
        idData := identity.FromContext(ctx)
4✔
760
        if idData != nil {
5✔
761
                tenantID = idData.Tenant
1✔
762
        }
1✔
763

764
        // submit device decommissioning job
765
        if err := d.cOrch.SubmitDeviceDecommisioningJob(
4✔
766
                ctx,
4✔
767
                orchestrator.DecommissioningReq{
4✔
768
                        DeviceId:  devID,
4✔
769
                        RequestId: reqId,
4✔
770
                        TenantID:  tenantID,
4✔
771
                }); err != nil {
6✔
772
                return errors.Wrap(err, "submit device decommissioning job error")
2✔
773
        }
2✔
774

775
        return err
3✔
776
}
777

778
// Delete a device and its tokens from deviceauth db
779
func (d *DevAuth) DeleteDevice(ctx context.Context, devID string) error {
7✔
780
        // delete device authorization sets
7✔
781
        if err := d.db.DeleteAuthSetsForDevice(ctx, devID); err != nil &&
7✔
782
                err != store.ErrAuthSetNotFound {
8✔
783
                return errors.Wrap(err, "db delete device authorization sets error")
1✔
784
        }
1✔
785

786
        devOID := oid.FromString(devID)
6✔
787
        // If the devID is not a valid string, there's no token.
6✔
788
        if devOID.String() == "" {
7✔
789
                return ErrInvalidDeviceID
1✔
790
        }
1✔
791
        // delete device tokens
792
        if err := d.db.DeleteTokenByDevId(
5✔
793
                ctx, devOID,
5✔
794
        ); err != nil && err != store.ErrTokenNotFound {
6✔
795
                return errors.Wrap(err, "db delete device tokens error")
1✔
796
        }
1✔
797

798
        // delete device
799
        if err := d.db.DeleteDevice(ctx, devID); err != nil {
6✔
800
                return err
2✔
801
        }
2✔
802

803
        if d.config.EnableReporting {
3✔
804
                if err := d.cOrch.SubmitReindexReporting(ctx, devID); err != nil {
1✔
805
                        return errors.Wrap(err, "reindex reporting job error")
×
806
                }
×
807
        }
808

809
        return nil
2✔
810
}
811

812
// Deletes device authentication set, and optionally the device.
813
func (d *DevAuth) DeleteAuthSet(ctx context.Context, devID string, authId string) error {
20✔
814

20✔
815
        l := log.FromContext(ctx)
20✔
816

20✔
817
        l.Warnf("Delete authentication set with id: "+
20✔
818
                "%s for the device with id: %s",
20✔
819
                authId, devID)
20✔
820

20✔
821
        err := d.cacheDeleteToken(ctx, devID)
20✔
822
        if err != nil {
22✔
823
                return errors.Wrapf(err, "failed to delete token for %s from cache", devID)
2✔
824
        }
2✔
825

826
        // retrieve device authentication set to check its status
827
        authSet, err := d.db.GetAuthSetById(ctx, authId)
18✔
828
        if err != nil {
21✔
829
                if err == store.ErrAuthSetNotFound {
5✔
830
                        return err
2✔
831
                }
2✔
832
                return errors.Wrap(err, "db get auth set error")
1✔
833
        }
834

835
        // delete device authorization set
836
        if err := d.db.DeleteAuthSetForDevice(ctx, devID, authId); err != nil {
17✔
837
                return err
1✔
838
        }
1✔
839

840
        // if the device authentication set is accepted delete device tokens
841
        if authSet.Status == model.DevStatusAccepted {
16✔
842
                // If string is not a valid UUID there's no token.
1✔
843
                devOID := oid.FromString(devID)
1✔
844
                if err := d.db.DeleteTokenByDevId(
1✔
845
                        ctx, devOID,
1✔
846
                ); err != nil && err != store.ErrTokenNotFound {
2✔
847
                        return errors.Wrap(err,
1✔
848
                                "db delete device tokens error")
1✔
849
                }
1✔
850
        } else if authSet.Status == model.DevStatusPreauth {
17✔
851
                // if the auth set status is 'preauthorized', the device is deleted from
3✔
852
                // deviceauth. We cannot start the decommission_device workflow because
3✔
853
                // we don't provision devices until they are accepted. Still, we need to
3✔
854
                // remove the device from the inventory service because we index pre-authorized
3✔
855
                // devices for consumption via filtering APIs. To trigger the deletion
3✔
856
                // from the inventory service, we start the status update workflow with the
3✔
857
                // special value "decommissioned", which will cause the deletion of the
3✔
858
                // device from the inventory service's database
3✔
859
                tenantID := ""
3✔
860
                idData := identity.FromContext(ctx)
3✔
861
                if idData != nil {
3✔
862
                        tenantID = idData.Tenant
×
863
                }
×
864
                req := orchestrator.UpdateDeviceStatusReq{
3✔
865
                        RequestId: requestid.FromContext(ctx),
3✔
866
                        Devices:   []model.DeviceInventoryUpdate{{Id: devID}},
3✔
867
                        TenantId:  tenantID,
3✔
868
                        Status:    "decommissioned",
3✔
869
                }
3✔
870
                if err = d.cOrch.SubmitUpdateDeviceStatusJob(ctx, req); err != nil {
4✔
871
                        return errors.Wrap(err, "update device status job error")
1✔
872
                }
1✔
873

874
                // delete device
875
                if err := d.db.DeleteDevice(ctx, devID); err != nil {
3✔
876
                        return err
1✔
877
                }
1✔
878

879
                if d.config.EnableReporting {
2✔
880
                        if err := d.cOrch.SubmitReindexReporting(ctx, devID); err != nil {
2✔
881
                                return errors.Wrap(err, "reindex reporting job error")
1✔
882
                        }
1✔
883
                }
884

885
                return nil
×
886
        }
887

888
        return d.updateDeviceStatus(ctx, devID, "", authSet.Status)
11✔
889
}
890

891
func (d *DevAuth) AcceptDeviceAuth(ctx context.Context, device_id string, auth_id string) error {
16✔
892
        l := log.FromContext(ctx)
16✔
893

16✔
894
        aset, err := d.db.GetAuthSetById(ctx, auth_id)
16✔
895
        if err != nil {
18✔
896
                if err == store.ErrAuthSetNotFound {
4✔
897
                        return err
2✔
898
                }
2✔
899
                return errors.Wrap(err, "db get auth set error")
×
900
        }
901

902
        // device authentication set already accepted, nothing to do here
903
        if aset.Status == model.DevStatusAccepted {
16✔
904
                l.Debugf("Device %s already accepted", device_id)
1✔
905
                return nil
1✔
906
        } else if aset.Status != model.DevStatusRejected && aset.Status != model.DevStatusPending {
16✔
907
                // device authentication set can be accepted only from 'pending' or 'rejected' statuses
1✔
908
                return ErrDevAuthBadRequest
1✔
909
        }
1✔
910

911
        // check the device status
912
        // if the device status is accepted then do not trigger provisioning workflow
913
        // this needs to be checked before changing authentication set status
914
        dev, err := d.db.GetDeviceById(ctx, device_id)
13✔
915
        if err != nil {
14✔
916
                return err
1✔
917
        }
1✔
918

919
        // possible race, consider accept-count-unaccept pattern if that's problematic
920
        allow, err := d.canAcceptDevice(ctx)
12✔
921
        if err != nil {
14✔
922
                return err
2✔
923
        }
2✔
924

925
        if !allow {
13✔
926
                return ErrMaxDeviceCountReached
3✔
927
        }
3✔
928

929
        if err := d.setAuthSetStatus(ctx, device_id, auth_id, model.DevStatusAccepted); err != nil {
11✔
930
                return err
3✔
931
        }
3✔
932

933
        if dev.Status != model.DevStatusPending {
8✔
934
                // Device already exist in all services
2✔
935
                // We're done...
2✔
936
                return nil
2✔
937
        }
2✔
938

939
        dev.Status = model.DevStatusAccepted
5✔
940
        aset.Status = model.DevStatusAccepted
5✔
941
        dev.AuthSets = []model.AuthSet{*aset}
5✔
942

5✔
943
        reqId := requestid.FromContext(ctx)
5✔
944

5✔
945
        var tenantID string
5✔
946
        if idty := identity.FromContext(ctx); idty != nil {
6✔
947
                tenantID = idty.Tenant
1✔
948
        }
1✔
949

950
        // submit device accepted job
951
        if err := d.cOrch.SubmitProvisionDeviceJob(
5✔
952
                ctx,
5✔
953
                orchestrator.ProvisionDeviceReq{
5✔
954
                        RequestId: reqId,
5✔
955
                        DeviceID:  aset.DeviceId,
5✔
956
                        TenantID:  tenantID,
5✔
957
                        Device:    dev,
5✔
958
                        Status:    dev.Status,
5✔
959
                }); err != nil {
6✔
960
                return errors.Wrap(err, "submit device provisioning job error")
1✔
961
        }
1✔
962

963
        return nil
4✔
964
}
965

966
func (d *DevAuth) setAuthSetStatus(
967
        ctx context.Context,
968
        deviceID string,
969
        authID string,
970
        status string,
971
) error {
15✔
972
        aset, err := d.db.GetAuthSetById(ctx, authID)
15✔
973
        if err != nil {
15✔
974
                if err == store.ErrAuthSetNotFound {
×
975
                        return err
×
976
                }
×
977
                return errors.Wrap(err, "db get auth set error")
×
978
        }
979

980
        if aset.DeviceId != deviceID {
15✔
981
                return ErrDevIdAuthIdMismatch
×
982
        }
×
983

984
        if aset.Status == status {
15✔
985
                return nil
×
986
        }
×
987

988
        currentStatus := aset.Status
15✔
989

15✔
990
        if aset.Status == model.DevStatusAccepted &&
15✔
991
                (status == model.DevStatusRejected || status == model.DevStatusPending) {
22✔
992
                deviceOID := oid.FromString(aset.DeviceId)
7✔
993
                // delete device token
7✔
994
                err := d.db.DeleteTokenByDevId(ctx, deviceOID)
7✔
995
                if err != nil && err != store.ErrTokenNotFound {
9✔
996
                        return errors.Wrap(err, "db delete device token error")
2✔
997
                }
2✔
998
        }
999

1000
        // if accepting an auth set
1001
        if status == model.DevStatusAccepted {
21✔
1002
                // reject all accepted auth sets for this device first
8✔
1003
                if err := d.db.UpdateAuthSet(ctx,
8✔
1004
                        bson.M{
8✔
1005
                                model.AuthSetKeyDeviceId: deviceID,
8✔
1006
                                "$or": []bson.M{
8✔
1007
                                        {model.AuthSetKeyStatus: model.DevStatusAccepted},
8✔
1008
                                        {model.AuthSetKeyStatus: model.DevStatusPreauth},
8✔
1009
                                },
8✔
1010
                        },
8✔
1011
                        model.AuthSetUpdate{
8✔
1012
                                Status: model.DevStatusRejected,
8✔
1013
                        }); err != nil && err != store.ErrAuthSetNotFound {
9✔
1014
                        return errors.Wrap(err, "failed to reject auth sets")
1✔
1015
                }
1✔
1016
        }
1017

1018
        if err := d.db.UpdateAuthSetById(ctx, aset.Id, model.AuthSetUpdate{
12✔
1019
                Status: status,
12✔
1020
        }); err != nil {
13✔
1021
                return errors.Wrap(err, "db update device auth set error")
1✔
1022
        }
1✔
1023

1024
        if status == model.DevStatusAccepted {
17✔
1025
                return d.updateDeviceStatus(ctx, deviceID, status, currentStatus)
6✔
1026
        }
6✔
1027
        return d.updateDeviceStatus(ctx, deviceID, "", currentStatus)
6✔
1028
}
1029

1030
func (d *DevAuth) RejectDeviceAuth(ctx context.Context, device_id string, auth_id string) error {
9✔
1031
        aset, err := d.db.GetAuthSetById(ctx, auth_id)
9✔
1032
        if err != nil {
11✔
1033
                if err == store.ErrAuthSetNotFound {
3✔
1034
                        return err
1✔
1035
                }
1✔
1036
                return errors.Wrap(err, "db get auth set error")
1✔
1037
        } else if aset.Status != model.DevStatusPending && aset.Status != model.DevStatusAccepted {
9✔
1038
                // device authentication set can be rejected only from 'accepted' or 'pending' statuses
1✔
1039
                return ErrDevAuthBadRequest
1✔
1040
        }
1✔
1041

1042
        err = d.cacheDeleteToken(ctx, device_id)
7✔
1043
        if err != nil {
9✔
1044
                return errors.Wrapf(err, "failed to delete token for %s from cache", device_id)
2✔
1045
        }
2✔
1046

1047
        return d.setAuthSetStatus(ctx, device_id, auth_id, model.DevStatusRejected)
5✔
1048
}
1049

1050
func (d *DevAuth) ResetDeviceAuth(ctx context.Context, device_id string, auth_id string) error {
5✔
1051
        aset, err := d.db.GetAuthSetById(ctx, auth_id)
5✔
1052
        if err != nil {
6✔
1053
                if err == store.ErrAuthSetNotFound {
1✔
1054
                        return err
×
1055
                }
×
1056
                return errors.Wrap(err, "db get auth set error")
1✔
1057
        } else if aset.Status == model.DevStatusPreauth {
4✔
1058
                // preauthorized auth set should not go into pending state
×
1059
                return ErrDevAuthBadRequest
×
1060
        }
×
1061
        return d.setAuthSetStatus(ctx, device_id, auth_id, model.DevStatusPending)
4✔
1062
}
1063

1064
func parseIdData(idData string) (map[string]interface{}, []byte, error) {
54✔
1065
        var idDataStruct map[string]interface{}
54✔
1066
        var idDataSha256 []byte
54✔
1067

54✔
1068
        err := json.Unmarshal([]byte(idData), &idDataStruct)
54✔
1069
        if err != nil {
56✔
1070
                return idDataStruct, idDataSha256, errors.Wrapf(
2✔
1071
                        err,
2✔
1072
                        "failed to parse identity data: %s",
2✔
1073
                        idData,
2✔
1074
                )
2✔
1075
        }
2✔
1076

1077
        hash := sha256.New()
52✔
1078
        _, _ = hash.Write([]byte(idData))
52✔
1079
        idDataSha256 = hash.Sum(nil)
52✔
1080

52✔
1081
        return idDataStruct, idDataSha256, nil
52✔
1082
}
1083

1084
func (d *DevAuth) PreauthorizeDevice(
1085
        ctx context.Context,
1086
        req *model.PreAuthReq,
1087
) (*model.Device, error) {
7✔
1088
        // try add device, if a device with the given id_data exists -
7✔
1089
        // the unique index on id_data will prevent it (conflict)
7✔
1090
        // this is the only safeguard against id data conflict - we won't try to handle it
7✔
1091
        // additionally on inserting the auth set (can't add an id data index on auth set - would
7✔
1092
        // prevent key rotation)
7✔
1093

7✔
1094
        // FIXME: tenant_token is "" on purpose, will be removed
7✔
1095

7✔
1096
        l := log.FromContext(ctx)
7✔
1097

7✔
1098
        dev := model.NewDevice(req.DeviceId, req.IdData, req.PubKey)
7✔
1099
        dev.Status = model.DevStatusPreauth
7✔
1100

7✔
1101
        idDataStruct, idDataSha256, err := parseIdData(req.IdData)
7✔
1102
        if err != nil {
8✔
1103
                return nil, MakeErrDevAuthBadRequest(err)
1✔
1104
        }
1✔
1105

1106
        dev.IdDataStruct = idDataStruct
6✔
1107
        dev.IdDataSha256 = idDataSha256
6✔
1108

6✔
1109
        err = d.db.AddDevice(ctx, *dev)
6✔
1110
        switch err {
6✔
1111
        case nil:
4✔
1112
                break
4✔
1113
        case store.ErrObjectExists:
1✔
1114
                dev, err = d.db.GetDeviceByIdentityDataHash(ctx, idDataSha256)
1✔
1115
                if err != nil {
1✔
1116
                        l.Error("failed to find device but could not preauthorize either")
×
1117
                        return nil, errors.New("failed to preauthorize device")
×
1118
                }
×
1119
                return dev, ErrDeviceExists
1✔
1120
        default:
1✔
1121
                return nil, errors.Wrap(err, "failed to add device")
1✔
1122
        }
1123

1124
        tenantId := ""
4✔
1125
        idData := identity.FromContext(ctx)
4✔
1126
        if idData != nil {
4✔
1127
                tenantId = idData.Tenant
×
1128
        }
×
1129

1130
        wfReq := orchestrator.UpdateDeviceStatusReq{
4✔
1131
                RequestId: requestid.FromContext(ctx),
4✔
1132
                Devices: []model.DeviceInventoryUpdate{{
4✔
1133
                        Id:       dev.Id,
4✔
1134
                        Revision: dev.Revision,
4✔
1135
                }},
4✔
1136
                TenantId: tenantId,
4✔
1137
                Status:   dev.Status,
4✔
1138
        }
4✔
1139
        if err = d.cOrch.SubmitUpdateDeviceStatusJob(ctx, wfReq); err != nil {
4✔
1140
                return nil, errors.Wrap(err, "update device status job error")
×
1141
        }
×
1142

1143
        // record authentication request
1144
        authset := model.AuthSet{
4✔
1145
                Id:           req.AuthSetId,
4✔
1146
                IdData:       req.IdData,
4✔
1147
                IdDataStruct: idDataStruct,
4✔
1148
                IdDataSha256: idDataSha256,
4✔
1149
                PubKey:       req.PubKey,
4✔
1150
                DeviceId:     req.DeviceId,
4✔
1151
                Status:       model.DevStatusPreauth,
4✔
1152
                Timestamp:    uto.TimePtr(time.Now()),
4✔
1153
        }
4✔
1154

4✔
1155
        err = d.db.AddAuthSet(ctx, authset)
4✔
1156
        switch err {
4✔
1157
        case nil:
2✔
1158
                if err := d.setDeviceIdentity(ctx, dev, tenantId); err != nil {
2✔
1159
                        return nil, err
×
1160
                }
×
1161
                return nil, nil
2✔
1162
        case store.ErrObjectExists:
1✔
1163
                dev, err = d.db.GetDeviceByIdentityDataHash(ctx, idDataSha256)
1✔
1164
                if err != nil {
1✔
1165
                        l.Error("failed to find device but could not preauthorize either")
×
1166
                        return nil, errors.New("failed to preauthorize device")
×
1167
                }
×
1168
                return dev, ErrDeviceExists
1✔
1169
        default:
1✔
1170
                return nil, errors.Wrap(err, "failed to add auth set")
1✔
1171
        }
1172
}
1173

1174
func (d *DevAuth) RevokeToken(ctx context.Context, tokenID string) error {
6✔
1175
        l := log.FromContext(ctx)
6✔
1176
        tokenOID := oid.FromString(tokenID)
6✔
1177

6✔
1178
        var token *jwt.Token
6✔
1179
        token, err := d.db.GetToken(ctx, tokenOID)
6✔
1180
        if err != nil {
7✔
1181
                return err
1✔
1182
        }
1✔
1183

1184
        l.Warnf("Revoke token with jti: %s", tokenID)
5✔
1185
        err = d.db.DeleteToken(ctx, tokenOID)
5✔
1186

5✔
1187
        if err == nil && d.cache != nil {
7✔
1188
                err = d.cacheDeleteToken(ctx, token.Claims.Subject.String())
2✔
1189
                err = errors.Wrapf(
2✔
1190
                        err,
2✔
1191
                        "failed to delete token for %s from cache",
2✔
1192
                        token.Claims.Subject.String(),
2✔
1193
                )
2✔
1194
        }
2✔
1195
        return err
5✔
1196
}
1197

1198
func verifyTenantClaim(ctx context.Context, verifyTenant bool, tenant string) error {
16✔
1199

16✔
1200
        l := log.FromContext(ctx)
16✔
1201

16✔
1202
        if verifyTenant {
28✔
1203
                if tenant == "" {
13✔
1204
                        l.Errorf("No tenant claim in the token")
1✔
1205
                        return jwt.ErrTokenInvalid
1✔
1206
                }
1✔
1207
        } else if tenant != "" {
5✔
1208
                l.Errorf("Unexpected tenant claim: %s in the token", tenant)
×
1209
                return jwt.ErrTokenInvalid
×
1210
        }
×
1211

1212
        return nil
15✔
1213
}
1214

1215
func (d *DevAuth) VerifyToken(ctx context.Context, raw string) error {
18✔
1216
        l := log.FromContext(ctx)
18✔
1217

18✔
1218
        token := &jwt.Token{}
18✔
1219
        err := token.UnmarshalJWT([]byte(raw), d.jwt.FromJWT)
18✔
1220
        jti := token.Claims.ID
18✔
1221
        if err != nil {
20✔
1222
                l.Errorf("Token %s invalid: %v", jti.String(), err)
2✔
1223
                return jwt.ErrTokenInvalid
2✔
1224
        } else if !token.Claims.Device {
20✔
1225
                l.Errorf("not a device token")
1✔
1226
                return jwt.ErrTokenInvalid
1✔
1227
        }
1✔
1228

1229
        err = verifyTenantClaim(ctx, d.verifyTenant, token.Claims.Tenant)
16✔
1230
        if err == nil {
31✔
1231
                err = d.checker.ValidateWithContext(ctx)
15✔
1232
        }
15✔
1233
        if err != nil {
18✔
1234
                return err
2✔
1235
        }
2✔
1236

1237
        origMethod := ctxhttpheader.FromContext(ctx, "X-Forwarded-Method")
15✔
1238
        origUri := ctxhttpheader.FromContext(ctx, "X-Forwarded-Uri")
15✔
1239
        origUri = purgeUriArgs(origUri)
15✔
1240

15✔
1241
        // throttle and try fetch token from cache - if cached, it was
15✔
1242
        // already verified against the db checks below, we trust it
15✔
1243
        cachedToken, err := d.cacheThrottleVerify(ctx, token, raw, origMethod, origUri)
15✔
1244

15✔
1245
        if err == cache.ErrTooManyRequests {
16✔
1246
                return err
1✔
1247
        }
1✔
1248

1249
        if cachedToken != "" && raw == cachedToken {
16✔
1250
                // update device check-in time
2✔
1251
                d.updateCheckInTime(
2✔
1252
                        ctx,
2✔
1253
                        token.Claims.Subject.String(),
2✔
1254
                        token.Claims.Tenant,
2✔
1255
                        nil,
2✔
1256
                )
2✔
1257
                return nil
2✔
1258
        }
2✔
1259

1260
        // caching is best effort, don't fail
1261
        if err != nil {
18✔
1262
                l.Errorf("Failed to throttle for token %v: %s, continue.", token, err.Error())
6✔
1263
        }
6✔
1264

1265
        // perform JWT signature and claims validation
1266
        if err := d.jwt.Validate(raw); err != nil {
14✔
1267
                if err == jwt.ErrTokenExpired && jti.String() != "" {
3✔
1268
                        l.Errorf("Token %s expired: %v", jti.String(), err)
1✔
1269
                        return d.handleExpiredToken(ctx, jti)
1✔
1270
                }
1✔
1271
                l.Errorf("Token %s invalid: %v", jti.String(), err)
1✔
1272
                return jwt.ErrTokenInvalid
1✔
1273
        }
1274

1275
        // cache check was a MISS, hit the db for verification
1276
        // check if token is in the system
1277
        _, err = d.db.GetToken(ctx, jti)
11✔
1278
        if err != nil {
13✔
1279
                if err == store.ErrTokenNotFound {
4✔
1280
                        l.Errorf("Token %s not found", jti.String())
2✔
1281
                        return err
2✔
1282
                }
2✔
1283
                return errors.Wrapf(err, "Cannot get token with id: %s from database: %s", jti, err)
×
1284
        }
1285

1286
        auth, err := d.db.GetAuthSetById(ctx, jti.String())
10✔
1287
        if err != nil {
10✔
1288
                if err == store.ErrAuthSetNotFound {
×
1289
                        l.Errorf("Auth set %s not found", jti.String())
×
1290
                        return err
×
1291
                }
×
1292
                return err
×
1293
        }
1294

1295
        if auth.Status != model.DevStatusAccepted {
11✔
1296
                return jwt.ErrTokenInvalid
1✔
1297
        }
1✔
1298

1299
        // reject authentication for device that is in the process of
1300
        // decommissioning
1301
        dev, err := d.db.GetDeviceById(ctx, auth.DeviceId)
9✔
1302
        if err != nil {
9✔
1303
                return err
×
1304
        }
×
1305
        if dev.Decommissioning {
10✔
1306
                l.Errorf(
1✔
1307
                        "Token %s rejected, device %s is being decommissioned",
1✔
1308
                        jti.String(),
1✔
1309
                        auth.DeviceId,
1✔
1310
                )
1✔
1311
                return jwt.ErrTokenInvalid
1✔
1312
        }
1✔
1313

1314
        // update device check-in time
1315
        d.updateCheckInTime(
8✔
1316
                ctx,
8✔
1317
                token.Claims.Subject.String(),
8✔
1318
                token.Claims.Tenant,
8✔
1319
                dev.CheckInTime,
8✔
1320
        )
8✔
1321

8✔
1322
        // after successful token verification - cache it (best effort)
8✔
1323
        _ = d.cacheSetToken(ctx, token, raw)
8✔
1324

8✔
1325
        return nil
8✔
1326
}
1327

1328
func (d *DevAuth) handleExpiredToken(ctx context.Context, jti oid.ObjectID) error {
1✔
1329
        err := d.db.DeleteToken(ctx, jti)
1✔
1330
        if err == store.ErrTokenNotFound {
1✔
1331
                l := log.FromContext(ctx)
×
1332
                l.Errorf("Token %s not found", jti.String())
×
1333
                return err
×
1334
        }
×
1335
        if err != nil {
1✔
1336
                return errors.Wrapf(err, "Cannot delete token with jti: %s : %s", jti, err)
×
1337
        }
×
1338
        return jwt.ErrTokenExpired
1✔
1339
}
1340

1341
// purgeUriArgs removes query string args from an uri string
1342
// important for burst control (bursts are per uri without args)
1343
func purgeUriArgs(uri string) string {
17✔
1344
        return strings.Split(uri, "?")[0]
17✔
1345
}
17✔
1346

1347
func (d *DevAuth) cacheThrottleVerify(
1348
        ctx context.Context,
1349
        token *jwt.Token,
1350
        originalRaw,
1351
        origMethod,
1352
        origUri string,
1353
) (string, error) {
15✔
1354
        if d.cache == nil {
20✔
1355
                return "", nil
5✔
1356
        }
5✔
1357

1358
        // try get cached/precomputed limits
1359
        limits, err := d.getApiLimits(ctx,
10✔
1360
                token.Claims.Tenant,
10✔
1361
                token.Claims.Subject.String())
10✔
1362

10✔
1363
        if err != nil {
14✔
1364
                return "", err
4✔
1365
        }
4✔
1366

1367
        // apply throttling and fetch cached token
1368
        cached, err := d.cache.Throttle(ctx,
6✔
1369
                originalRaw,
6✔
1370
                *limits,
6✔
1371
                token.Claims.Tenant,
6✔
1372
                token.Claims.Subject.String(),
6✔
1373
                cache.IdTypeDevice,
6✔
1374
                origUri,
6✔
1375
                origMethod)
6✔
1376

6✔
1377
        return cached, err
6✔
1378
}
1379

1380
func (d *DevAuth) cacheSetToken(ctx context.Context, token *jwt.Token, raw string) error {
8✔
1381
        if d.cache == nil {
10✔
1382
                return nil
2✔
1383
        }
2✔
1384

1385
        expireIn := time.Duration(token.Claims.ExpiresAt.Unix()-d.clock.Now().Unix()) * time.Second
6✔
1386

6✔
1387
        return d.cache.CacheToken(ctx,
6✔
1388
                token.Claims.Tenant,
6✔
1389
                token.Claims.Subject.String(),
6✔
1390
                cache.IdTypeDevice,
6✔
1391
                raw,
6✔
1392
                expireIn)
6✔
1393
}
1394

1395
func (d *DevAuth) getApiLimits(
1396
        ctx context.Context,
1397
        tid,
1398
        did string,
1399
) (*ratelimits.ApiLimits, error) {
10✔
1400
        limits, err := d.cache.GetLimits(ctx, tid, did, cache.IdTypeDevice)
10✔
1401
        if err != nil {
11✔
1402
                return nil, err
1✔
1403
        }
1✔
1404

1405
        if limits != nil {
14✔
1406
                return limits, nil
5✔
1407
        }
5✔
1408

1409
        dev, err := d.db.GetDeviceById(ctx, did)
4✔
1410
        if err != nil {
4✔
1411
                return nil, err
×
1412
        }
×
1413

1414
        t, err := d.cTenant.GetTenant(ctx, tid)
4✔
1415
        if err != nil {
6✔
1416
                return nil, errors.Wrap(err, "request to get tenant failed")
2✔
1417
        }
2✔
1418
        if t == nil {
2✔
1419
                return nil, errors.New("tenant not found")
×
1420
        }
×
1421

1422
        finalLimits := apiLimitsOverride(t.ApiLimits.DeviceLimits, dev.ApiLimits)
2✔
1423

2✔
1424
        err = d.cache.CacheLimits(ctx, finalLimits, tid, did, cache.IdTypeDevice)
2✔
1425

2✔
1426
        return &finalLimits, err
2✔
1427
}
1428

1429
func (d *DevAuth) cacheDeleteToken(ctx context.Context, did string) error {
36✔
1430
        if d.cache == nil {
60✔
1431
                return nil
24✔
1432
        }
24✔
1433

1434
        idData := identity.FromContext(ctx)
12✔
1435
        if idData == nil {
15✔
1436
                return errors.New("can't unpack tenant identity data from context")
3✔
1437
        }
3✔
1438
        tid := idData.Tenant
9✔
1439

9✔
1440
        return d.cache.DeleteToken(ctx, tid, did, cache.IdTypeDevice)
9✔
1441
}
1442

1443
// TODO move to 'ratelimits', as ApiLimits methods maybe?
1444
func apiLimitsOverride(src, dest ratelimits.ApiLimits) ratelimits.ApiLimits {
9✔
1445
        // override only if not default
9✔
1446
        if dest.ApiQuota.MaxCalls != 0 && dest.ApiQuota.IntervalSec != 0 {
11✔
1447
                src.ApiQuota.MaxCalls = dest.ApiQuota.MaxCalls
2✔
1448
                src.ApiQuota.IntervalSec = dest.ApiQuota.IntervalSec
2✔
1449
        }
2✔
1450

1451
        out := make([]ratelimits.ApiBurst, len(src.ApiBursts))
9✔
1452
        copy(out, src.ApiBursts)
9✔
1453

9✔
1454
        for _, bdest := range dest.ApiBursts {
14✔
1455
                found := false
5✔
1456
                for i, bsrc := range src.ApiBursts {
9✔
1457
                        if bdest.Action == bsrc.Action &&
4✔
1458
                                bdest.Uri == bsrc.Uri {
6✔
1459
                                out[i].MinIntervalSec = bdest.MinIntervalSec
2✔
1460
                                found = true
2✔
1461
                        }
2✔
1462
                }
1463

1464
                if !found {
8✔
1465
                        out = append(out,
3✔
1466
                                ratelimits.ApiBurst{
3✔
1467
                                        Action:         bdest.Action,
3✔
1468
                                        Uri:            bdest.Uri,
3✔
1469
                                        MinIntervalSec: bdest.MinIntervalSec},
3✔
1470
                        )
3✔
1471
                }
3✔
1472
        }
1473

1474
        src.ApiBursts = out
9✔
1475
        return src
9✔
1476
}
1477

1478
func (d *DevAuth) GetLimit(ctx context.Context, name string) (*model.Limit, error) {
24✔
1479
        lim, err := d.db.GetLimit(ctx, name)
24✔
1480

24✔
1481
        switch err {
24✔
1482
        case nil:
18✔
1483
                return lim, nil
18✔
1484
        case store.ErrLimitNotFound:
3✔
1485
                return &model.Limit{Name: name, Value: 0}, nil
3✔
1486
        default:
4✔
1487
                return nil, err
4✔
1488
        }
1489
}
1490

1491
func (d *DevAuth) GetTenantLimit(
1492
        ctx context.Context,
1493
        name,
1494
        tenant_id string,
1495
) (*model.Limit, error) {
4✔
1496
        ctx = identity.WithContext(ctx, &identity.Identity{
4✔
1497
                Tenant: tenant_id,
4✔
1498
        })
4✔
1499

4✔
1500
        return d.GetLimit(ctx, name)
4✔
1501
}
4✔
1502

1503
// WithTenantVerification will force verification of tenant token with tenant
1504
// administrator when processing device authentication requests. Returns an
1505
// updated devauth.
1506
func (d *DevAuth) WithTenantVerification(c tenant.ClientRunner) *DevAuth {
24✔
1507
        d.cTenant = c
24✔
1508
        d.verifyTenant = true
24✔
1509
        return d
24✔
1510
}
24✔
1511

1512
func (d *DevAuth) WithCache(c cache.Cache) *DevAuth {
33✔
1513
        d.cache = c
33✔
1514
        return d
33✔
1515
}
33✔
1516

1517
func (d *DevAuth) WithClock(c utils.Clock) *DevAuth {
10✔
1518
        d.clock = c
10✔
1519
        return d
10✔
1520
}
10✔
1521

1522
func (d *DevAuth) SetTenantLimit(ctx context.Context, tenant_id string, limit model.Limit) error {
3✔
1523
        l := log.FromContext(ctx)
3✔
1524

3✔
1525
        ctx = identity.WithContext(ctx, &identity.Identity{
3✔
1526
                Tenant: tenant_id,
3✔
1527
        })
3✔
1528

3✔
1529
        l.Infof("setting limit %v for tenant %v", limit, tenant_id)
3✔
1530

3✔
1531
        if err := d.db.PutLimit(ctx, limit); err != nil {
4✔
1532
                l.Errorf("failed to save limit %v for tenant %v to database: %v",
1✔
1533
                        limit, tenant_id, err)
1✔
1534
                return errors.Wrapf(err, "failed to save limit %v for tenant %v to database",
1✔
1535
                        limit, tenant_id)
1✔
1536
        }
1✔
1537
        return nil
2✔
1538
}
1539

1540
func (d *DevAuth) DeleteTenantLimit(ctx context.Context, tenant_id string, limit string) error {
2✔
1541
        l := log.FromContext(ctx)
2✔
1542

2✔
1543
        ctx = identity.WithContext(ctx, &identity.Identity{
2✔
1544
                Tenant: tenant_id,
2✔
1545
        })
2✔
1546

2✔
1547
        l.Infof("removing limit %v for tenant %v", limit, tenant_id)
2✔
1548

2✔
1549
        if err := d.db.DeleteLimit(ctx, limit); err != nil {
3✔
1550
                l.Errorf("failed to delete limit %v for tenant %v to database: %v",
1✔
1551
                        limit, tenant_id, err)
1✔
1552
                return errors.Wrapf(err, "failed to delete limit %v for tenant %v to database",
1✔
1553
                        limit, tenant_id)
1✔
1554
        }
1✔
1555
        return nil
1✔
1556
}
1557

1558
func (d *DevAuth) GetDevCountByStatus(ctx context.Context, status string) (int, error) {
4✔
1559
        return d.db.GetDevCountByStatus(ctx, status)
4✔
1560
}
4✔
1561

1562
// canAcceptDevice checks if model.LimitMaxDeviceCount will be exceeded
1563
func (d *DevAuth) canAcceptDevice(ctx context.Context) (bool, error) {
17✔
1564
        limit, err := d.GetLimit(ctx, model.LimitMaxDeviceCount)
17✔
1565
        if err != nil {
19✔
1566
                return false, errors.Wrap(err, "can't get current device limit")
2✔
1567
        }
2✔
1568

1569
        if limit.Value == 0 {
21✔
1570
                return true, nil
6✔
1571
        }
6✔
1572

1573
        accepted, err := d.db.GetDevCountByStatus(ctx, model.DevStatusAccepted)
10✔
1574
        if err != nil {
11✔
1575
                return false, errors.Wrap(err, "can't get current device count")
1✔
1576
        }
1✔
1577

1578
        if uint64(accepted+1) <= limit.Value {
15✔
1579
                return true, nil
6✔
1580
        }
6✔
1581

1582
        return false, nil
4✔
1583
}
1584

1585
func (d *DevAuth) DeleteTokens(
1586
        ctx context.Context,
1587
        tenantID string,
1588
        deviceID string,
1589
) error {
7✔
1590
        var err error
7✔
1591
        ctx = identity.WithContext(ctx, &identity.Identity{
7✔
1592
                Tenant: tenantID,
7✔
1593
        })
7✔
1594

7✔
1595
        if deviceID != "" {
10✔
1596
                deviceOID := oid.FromString(deviceID)
3✔
1597
                if deviceOID.String() == "" {
3✔
1598
                        return ErrInvalidAuthSetID
×
1599
                }
×
1600
                err = d.db.DeleteTokenByDevId(ctx, deviceOID)
3✔
1601
        } else {
5✔
1602
                if err := d.cacheFlush(ctx); err != nil {
6✔
1603
                        return errors.Wrapf(
1✔
1604
                                err,
1✔
1605
                                "failed to flush cache when cleaning tokens for tenant %v",
1✔
1606
                                tenantID,
1✔
1607
                        )
1✔
1608
                }
1✔
1609

1610
                err = d.db.DeleteTokens(ctx)
4✔
1611
        }
1612

1613
        if err != nil && err != store.ErrTokenNotFound {
8✔
1614
                return errors.Wrapf(
2✔
1615
                        err,
2✔
1616
                        "failed to delete tokens for tenant: %v, device id: %v",
2✔
1617
                        tenantID,
2✔
1618
                        deviceID,
2✔
1619
                )
2✔
1620
        }
2✔
1621

1622
        return nil
4✔
1623
}
1624

1625
func (d *DevAuth) cacheFlush(ctx context.Context) error {
5✔
1626
        if d.cache == nil {
6✔
1627
                return nil
1✔
1628
        }
1✔
1629

1630
        return d.cache.FlushDB(ctx)
4✔
1631
}
1632

1633
func (d *DevAuth) GetTenantDeviceStatus(
1634
        ctx context.Context,
1635
        tenantId,
1636
        deviceId string,
1637
) (*model.Status, error) {
3✔
1638
        if tenantId != "" {
6✔
1639
                ctx = identity.WithContext(ctx, &identity.Identity{
3✔
1640
                        Tenant: tenantId,
3✔
1641
                })
3✔
1642
        }
3✔
1643
        dev, err := d.db.GetDeviceById(ctx, deviceId)
3✔
1644
        switch err {
3✔
1645
        case nil:
1✔
1646
                return &model.Status{Status: dev.Status}, nil
1✔
1647
        case store.ErrDevNotFound:
1✔
1648
                return nil, ErrDeviceNotFound
1✔
1649
        default:
1✔
1650
                return nil, errors.Wrapf(err, "get device %s failed", deviceId)
1✔
1651

1652
        }
1653
}
1654

1655
func (d *DevAuth) updateCheckInTime(
1656
        ctx context.Context,
1657
        deviceId string,
1658
        tenantId string,
1659
        previous *time.Time,
1660
) {
15✔
1661
        var err error
15✔
1662
        defer func() {
30✔
1663
                if err != nil {
15✔
NEW
1664
                        log.FromContext(ctx).Errorf(
×
NEW
1665
                                "failed to update device check-in time for device %s: %s",
×
NEW
1666
                                deviceId, err.Error(),
×
NEW
1667
                        )
×
NEW
1668
                }
×
1669
        }()
1670
        checkInTime := uto.TimePtr(time.Now().UTC())
15✔
1671
        // in case cache is disabled, use mongo
15✔
1672
        if d.cache == nil {
22✔
1673
                if err = d.db.UpdateDevice(ctx,
7✔
1674
                        deviceId,
7✔
1675
                        model.DeviceUpdate{
7✔
1676
                                CheckInTime: checkInTime,
7✔
1677
                        }); err != nil {
7✔
NEW
1678
                        return
×
1679
                }
×
1680
        } else {
8✔
1681
                // get check-in time from cache
8✔
1682
                previous, err = d.cache.GetCheckInTime(ctx, tenantId, deviceId)
8✔
1683
                if err != nil {
8✔
NEW
1684
                        return
×
1685
                }
×
1686
                // update check-in time in cache
1687
                err = d.cache.CacheCheckInTime(ctx, checkInTime, tenantId, deviceId)
8✔
1688
                if err != nil {
8✔
NEW
1689
                        return
×
1690
                }
×
1691
        }
1692
        // compare data without a time of current and previous check-in time
1693
        // and if it's different trigger reindexing (if enabled)
1694
        // and save check-in time in the database
1695
        if previous == nil ||
15✔
1696
                (previous != nil &&
15✔
1697
                        !previous.Truncate(24*time.Hour).Equal(checkInTime.Truncate(24*time.Hour))) {
30✔
1698
                // trigger reindexing
15✔
1699
                if d.config.EnableReporting {
16✔
1700
                        if err = d.cOrch.SubmitReindexReporting(ctx, deviceId); err != nil {
1✔
NEW
1701
                                err = errors.Wrap(err, "reindex reporting job error")
×
NEW
1702
                                return
×
UNCOV
1703
                        }
×
1704
                }
1705
                // dump cached value to database
1706
                if d.cache != nil {
23✔
1707
                        if err = d.db.UpdateDevice(ctx,
8✔
1708
                                deviceId,
8✔
1709
                                model.DeviceUpdate{
8✔
1710
                                        CheckInTime: checkInTime,
8✔
1711
                                }); err != nil {
8✔
NEW
1712
                                return
×
1713
                        }
×
1714
                }
1715
        }
1716
}
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

© 2026 Coveralls, Inc