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

mendersoftware / mender-server / 1622978334

13 Jan 2025 03:51PM UTC coverage: 72.802% (-3.8%) from 76.608%
1622978334

Pull #300

gitlab-ci

alfrunes
fix: Deployment device count should not exceed max devices

Added a condition to skip deployments when the device count reaches max
devices.

Changelog: Title
Ticket: MEN-7847
Signed-off-by: Alf-Rune Siqveland <alf.rune@northern.tech>
Pull Request #300: fix: Deployment device count should not exceed max devices

4251 of 6164 branches covered (68.96%)

Branch coverage included in aggregate %.

0 of 18 new or added lines in 1 file covered. (0.0%)

2544 existing lines in 83 files now uncovered.

42741 of 58384 relevant lines covered (73.21%)

21.49 hits per line

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

86.95
/backend/services/deviceauth/devauth/devauth.go
1
// Copyright 2024 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/pkg/errors"
24

25
        "github.com/mendersoftware/mender-server/pkg/addons"
26
        ctxhttpheader "github.com/mendersoftware/mender-server/pkg/context/httpheader"
27
        "github.com/mendersoftware/mender-server/pkg/identity"
28
        "github.com/mendersoftware/mender-server/pkg/log"
29
        "github.com/mendersoftware/mender-server/pkg/mongo/oid"
30
        "github.com/mendersoftware/mender-server/pkg/plan"
31
        "github.com/mendersoftware/mender-server/pkg/ratelimits"
32
        "github.com/mendersoftware/mender-server/pkg/requestid"
33

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

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

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

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

UNCOV
64
func IsErrDevAuthUnauthorized(e error) bool {
×
UNCOV
65
        return strings.HasPrefix(e.Error(), MsgErrDevAuthUnauthorized)
×
UNCOV
66
}
×
67

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

72
func IsErrDevAuthBadRequest(e error) bool {
×
73
        return strings.HasPrefix(e.Error(), MsgErrDevAuthBadRequest)
×
74
}
×
75

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

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

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

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

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

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

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

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

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

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

141
        EnableReporting bool
142
        HaveAddons      bool
143
}
144

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

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

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

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

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

1✔
233
        l := log.FromContext(ctx)
1✔
234

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

240
        dev.IdDataStruct = idDataStruct
1✔
241
        dev.IdDataSha256 = idDataSha256
1✔
242

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

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

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

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

276
        return dev, nil
1✔
277
}
278

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

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

1✔
288
        if err != nil {
2✔
289
                if tenant.IsErrTokenVerificationFailed(err) {
2✔
290
                        return nil, MakeErrDevAuthUnauthorized(err)
1✔
291
                }
1✔
292

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

296
        return t, nil
1✔
297
}
298

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

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

310
        var t *tenant.Tenant
1✔
311
        var err error
1✔
312

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

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

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

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

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

1✔
345
        return tCtx, t, nil
1✔
346
}
347

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

1✔
351
        var tenant *tenant.Tenant
1✔
352
        var err error
1✔
353

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

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

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

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

1✔
404
                if d.verifyTenant {
2✔
405
                        token.Claims.Tenant = tenant.ID
1✔
406
                        token.Claims.Plan = tenant.Plan
1✔
407
                        token.Claims.Addons = tenant.Addons
1✔
408
                        token.Claims.Trial = tenant.Trial
1✔
409
                } else {
2✔
410
                        token.Claims.Plan = plan.PlanEnterprise
1✔
411
                        token.Addons = addons.AllAddonsEnabled
1✔
412
                }
1✔
413

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

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

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

430
        // no token, return device unauthorized
431
        return "", ErrDevAuthUnauthorized
1✔
432

433
}
434

435
func (d *DevAuth) handlePreAuthDevice(
436
        ctx context.Context,
437
        aset *model.AuthSet,
438
) (*model.AuthSet, error) {
1✔
439
        var deviceAlreadyAccepted bool
1✔
440
        // check the device status
1✔
441
        // if the device status is accepted then do not trigger provisioning workflow
1✔
442
        // this needs to be checked before changing authentication set status
1✔
443
        dev, err := d.db.GetDeviceById(ctx, aset.DeviceId)
1✔
444
        if err != nil {
2✔
445
                return nil, err
1✔
446
        }
1✔
447

448
        // check if the device is in the decommissioning state
449
        if dev.Decommissioning {
2✔
450
                l := log.FromContext(ctx)
1✔
451
                l.Warnf("Device %s in the decommissioning state.", dev.Id)
1✔
452
                return nil, ErrDevAuthUnauthorized
1✔
453
        }
1✔
454

455
        currentStatus := dev.Status
1✔
456
        if dev.Status != model.DevStatusAccepted {
2✔
457
                // auth set is ok for auto-accepting, check device limit
1✔
458
                allow, err := d.canAcceptDevice(ctx)
1✔
459
                if err != nil {
2✔
460
                        return nil, err
1✔
461
                }
1✔
462

463
                if !allow {
2✔
464
                        return nil, ErrMaxDeviceCountReached
1✔
465
                }
1✔
466
        }
467

468
        // Ensure that the old acceptable auth sets are rejected
469
        if err := d.db.RejectAuthSetsForDevice(ctx, aset.DeviceId); err != nil &&
1✔
470
                !errors.Is(err, store.ErrAuthSetNotFound) {
1✔
471
                return nil, errors.Wrap(err, "failed to reject auth sets")
×
472
        }
×
473
        update := model.AuthSetUpdate{
1✔
474
                Status: model.DevStatusAccepted,
1✔
475
        }
1✔
476
        // persist the 'accepted' status in both auth set, and device
1✔
477
        if err := d.db.UpdateAuthSetById(ctx, aset.Id, update); err != nil {
1✔
478
                return nil, errors.Wrap(err, "failed to update auth set status")
×
479
        }
×
480

481
        if err := d.updateDeviceStatus(
1✔
482
                ctx,
1✔
483
                aset.DeviceId,
1✔
484
                model.DevStatusAccepted,
1✔
485
                currentStatus,
1✔
486
        ); err != nil {
1✔
487
                return nil, err
×
488
        }
×
489

490
        aset.Status = model.DevStatusAccepted
1✔
491
        dev.Status = model.DevStatusAccepted
1✔
492
        dev.AuthSets = append(dev.AuthSets, *aset)
1✔
493

1✔
494
        if !deviceAlreadyAccepted {
2✔
495
                reqId := requestid.FromContext(ctx)
1✔
496
                var tenantID string
1✔
497
                if idty := identity.FromContext(ctx); idty != nil {
1✔
498
                        tenantID = idty.Tenant
×
499
                }
×
500

501
                // submit device accepted job
502
                if err := d.cOrch.SubmitProvisionDeviceJob(
1✔
503
                        ctx,
1✔
504
                        orchestrator.ProvisionDeviceReq{
1✔
505
                                RequestId: reqId,
1✔
506
                                DeviceID:  aset.DeviceId,
1✔
507
                                TenantID:  tenantID,
1✔
508
                                Device:    dev,
1✔
509
                                Status:    dev.Status,
1✔
510
                        }); err != nil {
2✔
511
                        return nil, errors.Wrap(err, "submit device provisioning job error")
1✔
512
                }
1✔
513
        }
514
        return aset, nil
1✔
515
}
516

517
func (d *DevAuth) processPreAuthRequest(
518
        ctx context.Context,
519
        r *model.AuthReq,
520
) (*model.AuthSet, error) {
1✔
521

1✔
522
        _, idDataSha256, err := parseIdData(r.IdData)
1✔
523
        if err != nil {
2✔
524
                return nil, MakeErrDevAuthBadRequest(err)
1✔
525
        }
1✔
526

527
        // authset exists?
528
        aset, err := d.db.GetAuthSetByIdDataHashKeyByStatus(
1✔
529
                ctx,
1✔
530
                idDataSha256,
1✔
531
                r.PubKey,
1✔
532
                model.DevStatusPreauth,
1✔
533
        )
1✔
534
        switch err {
1✔
535
        case nil:
1✔
536
                break
1✔
537
        case store.ErrAuthSetNotFound:
1✔
538
                return nil, nil
1✔
539
        default:
1✔
540
                return nil, errors.Wrap(err, "failed to fetch auth set")
1✔
541
        }
542

543
        // if authset status is not 'preauthorized', nothing to do
544
        if aset.Status != model.DevStatusPreauth {
2✔
545
                return nil, nil
1✔
546
        }
1✔
547
        return d.handlePreAuthDevice(ctx, aset)
1✔
548
}
549

550
func (d *DevAuth) updateDeviceStatus(
551
        ctx context.Context,
552
        devId,
553
        status string,
554
        currentStatus string,
555
) error {
1✔
556
        newStatus, err := d.db.GetDeviceStatus(ctx, devId)
1✔
557
        if currentStatus == newStatus {
2✔
558
                return nil
1✔
559
        }
1✔
560
        if status == "" {
2✔
561
                switch err {
1✔
562
                case nil:
1✔
563
                        status = newStatus
1✔
564
                case store.ErrAuthSetNotFound:
1✔
565
                        status = model.DevStatusNoAuth
1✔
566
                default:
1✔
567
                        return errors.Wrap(err, "Cannot determine device status")
1✔
568
                }
569
        }
570

571
        // submit device status change job
572
        dev, err := d.db.GetDeviceById(ctx, devId)
1✔
573
        if err != nil {
1✔
574
                return errors.Wrap(err, "db get device by id error")
×
575
        }
×
576

577
        tenantId := ""
1✔
578
        idData := identity.FromContext(ctx)
1✔
579
        if idData != nil {
2✔
580
                tenantId = idData.Tenant
1✔
581
        }
1✔
582
        req := orchestrator.UpdateDeviceStatusReq{
1✔
583
                RequestId: requestid.FromContext(ctx),
1✔
584
                Devices: []model.DeviceInventoryUpdate{{
1✔
585
                        Id:       dev.Id,
1✔
586
                        Revision: dev.Revision + 1,
1✔
587
                }},
1✔
588
                TenantId: tenantId,
1✔
589
                Status:   status,
1✔
590
        }
1✔
591
        if err := d.cOrch.SubmitUpdateDeviceStatusJob(ctx, req); err != nil {
1✔
UNCOV
592
                return errors.Wrap(err, "update device status job error")
×
UNCOV
593
        }
×
594

595
        if err := d.db.UpdateDevice(ctx,
1✔
596
                devId,
1✔
597
                model.DeviceUpdate{
1✔
598
                        Status:    status,
1✔
599
                        UpdatedTs: uto.TimePtr(time.Now().UTC()),
1✔
600
                }); err != nil {
2✔
601
                return errors.Wrap(err, "failed to update device status")
1✔
602
        }
1✔
603

604
        if d.config.EnableReporting {
2✔
605
                if err := d.cOrch.SubmitReindexReporting(ctx, devId); err != nil {
2✔
606
                        return errors.Wrap(err, "reindex reporting job error")
1✔
607
                }
1✔
608
        }
609

610
        return nil
1✔
611
}
612

613
// processAuthRequest will process incoming auth request and record authentication
614
// data information it contains. Returns a tupe (auth set, error). If no errors were
615
// present, model.AuthSet.Status will indicate the status of device admission
616
func (d *DevAuth) processAuthRequest(
617
        ctx context.Context,
618
        r *model.AuthReq,
619
) (*model.AuthSet, error) {
1✔
620

1✔
621
        l := log.FromContext(ctx)
1✔
622

1✔
623
        // get device associated with given authorization request
1✔
624
        dev, err := d.getDeviceFromAuthRequest(ctx, r)
1✔
625
        if err != nil {
2✔
626
                return nil, err
1✔
627
        }
1✔
628

629
        idDataStruct, idDataSha256, err := parseIdData(r.IdData)
1✔
630
        if err != nil {
1✔
631
                return nil, MakeErrDevAuthBadRequest(err)
×
632
        }
×
633

634
        areq := &model.AuthSet{
1✔
635
                Id:           oid.NewUUIDv4().String(),
1✔
636
                IdData:       r.IdData,
1✔
637
                IdDataStruct: idDataStruct,
1✔
638
                IdDataSha256: idDataSha256,
1✔
639
                PubKey:       r.PubKey,
1✔
640
                DeviceId:     dev.Id,
1✔
641
                Status:       model.DevStatusPending,
1✔
642
                Timestamp:    uto.TimePtr(time.Now()),
1✔
643
        }
1✔
644

1✔
645
        // record authentication request
1✔
646
        err = d.db.AddAuthSet(ctx, *areq)
1✔
647
        if err != nil && err != store.ErrObjectExists {
1✔
648
                return nil, err
×
649
        }
×
650

651
        // update the device status
652
        if err := d.updateDeviceStatus(ctx, dev.Id, "", dev.Status); err != nil {
1✔
653
                return nil, err
×
654
        }
×
655

656
        // either the request was added or it was already present in the DB, get
657
        // it now
658
        areq, err = d.db.GetAuthSetByIdDataHashKey(ctx, idDataSha256, r.PubKey)
1✔
659
        if err != nil {
2✔
660
                l.Error("failed to find device auth set but could not add one either")
1✔
661
                return nil, errors.New("failed to locate device auth set")
1✔
662
        }
1✔
663

664
        return areq, nil
1✔
665
}
666

667
func (d *DevAuth) GetDevices(
668
        ctx context.Context,
669
        skip,
670
        limit uint,
671
        filter model.DeviceFilter,
672
) ([]model.Device, error) {
1✔
673
        devs, err := d.db.GetDevices(ctx, skip, limit, filter)
1✔
674
        if err != nil {
1✔
675
                return nil, errors.Wrap(err, "failed to list devices")
×
676
        }
×
677

678
        for i := range devs {
2✔
679
                devs[i].AuthSets, err = d.db.GetAuthSetsForDevice(ctx, devs[i].Id)
1✔
680
                if err != nil && err != store.ErrAuthSetNotFound {
1✔
681
                        return nil, errors.Wrap(err, "db get auth sets error")
×
682
                }
×
683
        }
684

685
        // update check-in time
686
        if d.cache != nil {
2✔
687
                tenantID := ""
1✔
688
                idData := identity.FromContext(ctx)
1✔
689
                if idData != nil {
2✔
690
                        tenantID = idData.Tenant
1✔
691
                }
1✔
692

693
                ids := make([]string, len(devs))
1✔
694
                for i := range devs {
2✔
695
                        ids[i] = devs[i].Id
1✔
696
                }
1✔
697
                checkInTimes, err := d.cache.GetCheckInTimes(ctx, tenantID, ids)
1✔
698
                if err != nil {
2✔
699
                        l := log.FromContext(ctx)
1✔
700
                        l.Errorf("Failed to get check-in times for devices")
1✔
701
                } else {
2✔
702
                        for i := range devs {
2✔
703
                                if checkInTimes[i] != nil {
2✔
704
                                        devs[i].CheckInTime = checkInTimes[i]
1✔
705
                                }
1✔
706
                        }
707
                }
708
        }
709

710
        return devs, err
1✔
711
}
712

713
func (d *DevAuth) GetDevice(ctx context.Context, devId string) (*model.Device, error) {
1✔
714
        dev, err := d.db.GetDeviceById(ctx, devId)
1✔
715
        if err != nil {
1✔
UNCOV
716
                if err != store.ErrDevNotFound {
×
717
                        return nil, errors.Wrap(err, "db get device by id error")
×
718
                }
×
UNCOV
719
                return nil, err
×
720
        }
721

722
        dev.AuthSets, err = d.db.GetAuthSetsForDevice(ctx, dev.Id)
1✔
723
        if err != nil {
1✔
724
                if err != store.ErrAuthSetNotFound {
×
725
                        return nil, errors.Wrap(err, "db get auth sets error")
×
726
                }
×
727
                return dev, nil
×
728
        }
729

730
        if d.cache != nil {
2✔
731
                tenantID := ""
1✔
732
                idData := identity.FromContext(ctx)
1✔
733
                if idData != nil {
2✔
734
                        tenantID = idData.Tenant
1✔
735
                }
1✔
736

737
                checkInTime, err := d.cache.GetCheckInTime(ctx, tenantID, devId)
1✔
738
                if err != nil {
2✔
739
                        l := log.FromContext(ctx)
1✔
740
                        l.Errorf("Failed to get check-in times for device")
1✔
741
                } else if checkInTime != nil {
3✔
742
                        dev.CheckInTime = checkInTime
1✔
743
                }
1✔
744
        }
745

746
        return dev, err
1✔
747
}
748

749
// DecommissionDevice deletes device and all its tokens
750
func (d *DevAuth) DecommissionDevice(ctx context.Context, devID string) error {
1✔
751

1✔
752
        l := log.FromContext(ctx)
1✔
753

1✔
754
        l.Warnf("Decommission device with id: %s", devID)
1✔
755

1✔
756
        err := d.cacheDeleteToken(ctx, devID)
1✔
757
        if err != nil {
2✔
758
                return errors.Wrapf(err, "failed to delete token for %s from cache", devID)
1✔
759
        }
1✔
760

761
        // set decommissioning flag on the device
762
        updev := model.DeviceUpdate{
1✔
763
                Decommissioning: uto.BoolPtr(true),
1✔
764
        }
1✔
765
        if err := d.db.UpdateDevice(
1✔
766
                ctx, devID, updev,
1✔
767
        ); err != nil {
2✔
768
                return err
1✔
769
        }
1✔
770

771
        reqId := requestid.FromContext(ctx)
1✔
772

1✔
773
        tenantID := ""
1✔
774
        idData := identity.FromContext(ctx)
1✔
775
        if idData != nil {
2✔
776
                tenantID = idData.Tenant
1✔
777
        }
1✔
778

779
        // submit device decommissioning job
780
        if err := d.cOrch.SubmitDeviceDecommisioningJob(
1✔
781
                ctx,
1✔
782
                orchestrator.DecommissioningReq{
1✔
783
                        DeviceId:  devID,
1✔
784
                        RequestId: reqId,
1✔
785
                        TenantID:  tenantID,
1✔
786
                }); err != nil {
2✔
787
                return errors.Wrap(err, "submit device decommissioning job error")
1✔
788
        }
1✔
789

790
        return err
1✔
791
}
792

793
// Delete a device and its tokens from deviceauth db
794
func (d *DevAuth) DeleteDevice(ctx context.Context, devID string) error {
1✔
795
        // delete device authorization sets
1✔
796
        if err := d.db.DeleteAuthSetsForDevice(ctx, devID); err != nil &&
1✔
797
                err != store.ErrAuthSetNotFound {
2✔
798
                return errors.Wrap(err, "db delete device authorization sets error")
1✔
799
        }
1✔
800

801
        devOID := oid.FromString(devID)
1✔
802
        // If the devID is not a valid string, there's no token.
1✔
803
        if devOID.String() == "" {
2✔
804
                return ErrInvalidDeviceID
1✔
805
        }
1✔
806
        // delete device tokens
807
        if err := d.db.DeleteTokenByDevId(
1✔
808
                ctx, devOID,
1✔
809
        ); err != nil && err != store.ErrTokenNotFound {
2✔
810
                return errors.Wrap(err, "db delete device tokens error")
1✔
811
        }
1✔
812

813
        // delete device
814
        if err := d.db.DeleteDevice(ctx, devID); err != nil {
2✔
815
                return err
1✔
816
        }
1✔
817

818
        if d.config.EnableReporting {
1✔
819
                if err := d.cOrch.SubmitReindexReporting(ctx, devID); err != nil {
×
820
                        return errors.Wrap(err, "reindex reporting job error")
×
821
                }
×
822
        }
823

824
        return nil
1✔
825
}
826

827
// Deletes device authentication set, and optionally the device.
828
func (d *DevAuth) DeleteAuthSet(ctx context.Context, devID string, authId string) error {
1✔
829

1✔
830
        l := log.FromContext(ctx)
1✔
831

1✔
832
        l.Warnf("Delete authentication set with id: "+
1✔
833
                "%s for the device with id: %s",
1✔
834
                authId, devID)
1✔
835

1✔
836
        err := d.cacheDeleteToken(ctx, devID)
1✔
837
        if err != nil {
2✔
838
                return errors.Wrapf(err, "failed to delete token for %s from cache", devID)
1✔
839
        }
1✔
840

841
        // retrieve device authentication set to check its status
842
        authSet, err := d.db.GetAuthSetById(ctx, authId)
1✔
843
        if err != nil {
2✔
844
                if err == store.ErrAuthSetNotFound {
2✔
845
                        return err
1✔
846
                }
1✔
847
                return errors.Wrap(err, "db get auth set error")
1✔
848
        }
849

850
        // delete device authorization set
851
        if err := d.db.DeleteAuthSetForDevice(ctx, devID, authId); err != nil {
2✔
852
                return err
1✔
853
        }
1✔
854

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

889
                // delete device
890
                if err := d.db.DeleteDevice(ctx, devID); err != nil {
2✔
891
                        return err
1✔
892
                }
1✔
893

894
                if d.config.EnableReporting {
2✔
895
                        if err := d.cOrch.SubmitReindexReporting(ctx, devID); err != nil {
2✔
896
                                return errors.Wrap(err, "reindex reporting job error")
1✔
897
                        }
1✔
898
                }
899

UNCOV
900
                return nil
×
901
        }
902

903
        return d.updateDeviceStatus(ctx, devID, "", authSet.Status)
1✔
904
}
905

906
func (d *DevAuth) AcceptDeviceAuth(ctx context.Context, device_id string, auth_id string) error {
1✔
907
        l := log.FromContext(ctx)
1✔
908

1✔
909
        aset, err := d.db.GetAuthSetById(ctx, auth_id)
1✔
910
        if err != nil {
2✔
911
                if err == store.ErrAuthSetNotFound {
2✔
912
                        return err
1✔
913
                }
1✔
914
                return errors.Wrap(err, "db get auth set error")
×
915
        }
916

917
        // device authentication set already accepted, nothing to do here
918
        if aset.Status == model.DevStatusAccepted {
2✔
919
                l.Debugf("Device %s already accepted", device_id)
1✔
920
                return nil
1✔
921
        } else if aset.Status != model.DevStatusRejected && aset.Status != model.DevStatusPending {
3✔
922
                // device authentication set can be accepted only from 'pending' or 'rejected' statuses
1✔
923
                return ErrDevAuthBadRequest
1✔
924
        }
1✔
925

926
        // check the device status
927
        // if the device status is accepted then do not trigger provisioning workflow
928
        // this needs to be checked before changing authentication set status
929
        dev, err := d.db.GetDeviceById(ctx, device_id)
1✔
930
        if err != nil {
2✔
931
                return err
1✔
932
        }
1✔
933

934
        // possible race, consider accept-count-unaccept pattern if that's problematic
935
        allow, err := d.canAcceptDevice(ctx)
1✔
936
        if err != nil {
2✔
937
                return err
1✔
938
        }
1✔
939

940
        if !allow {
2✔
941
                return ErrMaxDeviceCountReached
1✔
942
        }
1✔
943

944
        if err := d.setAuthSetStatus(ctx, device_id, auth_id, model.DevStatusAccepted); err != nil {
2✔
945
                return err
1✔
946
        }
1✔
947

948
        if dev.Status != model.DevStatusPending {
2✔
949
                // Device already exist in all services
1✔
950
                // We're done...
1✔
951
                return nil
1✔
952
        }
1✔
953

954
        dev.Status = model.DevStatusAccepted
1✔
955
        aset.Status = model.DevStatusAccepted
1✔
956
        dev.AuthSets = []model.AuthSet{*aset}
1✔
957

1✔
958
        reqId := requestid.FromContext(ctx)
1✔
959

1✔
960
        var tenantID string
1✔
961
        if idty := identity.FromContext(ctx); idty != nil {
1✔
UNCOV
962
                tenantID = idty.Tenant
×
UNCOV
963
        }
×
964

965
        // submit device accepted job
966
        if err := d.cOrch.SubmitProvisionDeviceJob(
1✔
967
                ctx,
1✔
968
                orchestrator.ProvisionDeviceReq{
1✔
969
                        RequestId: reqId,
1✔
970
                        DeviceID:  aset.DeviceId,
1✔
971
                        TenantID:  tenantID,
1✔
972
                        Device:    dev,
1✔
973
                        Status:    dev.Status,
1✔
974
                }); err != nil {
2✔
975
                return errors.Wrap(err, "submit device provisioning job error")
1✔
976
        }
1✔
977

978
        return nil
1✔
979
}
980

981
func (d *DevAuth) setAuthSetStatus(
982
        ctx context.Context,
983
        deviceID string,
984
        authID string,
985
        status string,
986
) error {
1✔
987
        aset, err := d.db.GetAuthSetById(ctx, authID)
1✔
988
        if err != nil {
1✔
989
                if err == store.ErrAuthSetNotFound {
×
990
                        return err
×
991
                }
×
992
                return errors.Wrap(err, "db get auth set error")
×
993
        }
994

995
        if aset.DeviceId != deviceID {
1✔
996
                return ErrDevIdAuthIdMismatch
×
997
        }
×
998

999
        if aset.Status == status {
1✔
1000
                return nil
×
1001
        }
×
1002

1003
        currentStatus := aset.Status
1✔
1004

1✔
1005
        if aset.Status == model.DevStatusAccepted &&
1✔
1006
                (status == model.DevStatusRejected || status == model.DevStatusPending) {
2✔
1007
                deviceOID := oid.FromString(aset.DeviceId)
1✔
1008
                // delete device token
1✔
1009
                err := d.db.DeleteTokenByDevId(ctx, deviceOID)
1✔
1010
                if err != nil && err != store.ErrTokenNotFound {
2✔
1011
                        return errors.Wrap(err, "db delete device token error")
1✔
1012
                }
1✔
1013
        }
1014

1015
        // if accepting an auth set
1016
        if status == model.DevStatusAccepted {
2✔
1017
                // reject all accepted auth sets for this device first
1✔
1018
                err := d.db.RejectAuthSetsForDevice(ctx, deviceID)
1✔
1019
                if err != nil && err != store.ErrAuthSetNotFound {
2✔
1020
                        return errors.Wrap(err, "failed to reject auth sets")
1✔
1021
                }
1✔
1022
        }
1023

1024
        if err := d.db.UpdateAuthSetById(ctx, aset.Id, model.AuthSetUpdate{
1✔
1025
                Status: status,
1✔
1026
        }); err != nil {
2✔
1027
                return errors.Wrap(err, "db update device auth set error")
1✔
1028
        }
1✔
1029

1030
        if status == model.DevStatusAccepted {
2✔
1031
                return d.updateDeviceStatus(ctx, deviceID, status, currentStatus)
1✔
1032
        }
1✔
1033
        return d.updateDeviceStatus(ctx, deviceID, "", currentStatus)
1✔
1034
}
1035

1036
func (d *DevAuth) RejectDeviceAuth(ctx context.Context, device_id string, auth_id string) error {
1✔
1037
        aset, err := d.db.GetAuthSetById(ctx, auth_id)
1✔
1038
        if err != nil {
2✔
1039
                if err == store.ErrAuthSetNotFound {
1✔
UNCOV
1040
                        return err
×
UNCOV
1041
                }
×
1042
                return errors.Wrap(err, "db get auth set error")
1✔
1043
        } else if aset.Status != model.DevStatusPending && aset.Status != model.DevStatusAccepted {
2✔
1044
                // device authentication set can be rejected only from 'accepted' or 'pending' statuses
1✔
1045
                return ErrDevAuthBadRequest
1✔
1046
        }
1✔
1047

1048
        err = d.cacheDeleteToken(ctx, device_id)
1✔
1049
        if err != nil {
2✔
1050
                return errors.Wrapf(err, "failed to delete token for %s from cache", device_id)
1✔
1051
        }
1✔
1052

1053
        return d.setAuthSetStatus(ctx, device_id, auth_id, model.DevStatusRejected)
1✔
1054
}
1055

1056
func (d *DevAuth) ResetDeviceAuth(ctx context.Context, device_id string, auth_id string) error {
1✔
1057
        aset, err := d.db.GetAuthSetById(ctx, auth_id)
1✔
1058
        if err != nil {
2✔
1059
                if err == store.ErrAuthSetNotFound {
1✔
1060
                        return err
×
1061
                }
×
1062
                return errors.Wrap(err, "db get auth set error")
1✔
1063
        } else if aset.Status == model.DevStatusPreauth {
1✔
1064
                // preauthorized auth set should not go into pending state
×
1065
                return ErrDevAuthBadRequest
×
1066
        }
×
1067
        return d.setAuthSetStatus(ctx, device_id, auth_id, model.DevStatusPending)
1✔
1068
}
1069

1070
func parseIdData(idData string) (map[string]interface{}, []byte, error) {
1✔
1071
        var idDataStruct map[string]interface{}
1✔
1072
        var idDataSha256 []byte
1✔
1073

1✔
1074
        err := json.Unmarshal([]byte(idData), &idDataStruct)
1✔
1075
        if err != nil {
2✔
1076
                return idDataStruct, idDataSha256, errors.Wrapf(
1✔
1077
                        err,
1✔
1078
                        "failed to parse identity data: %s",
1✔
1079
                        idData,
1✔
1080
                )
1✔
1081
        }
1✔
1082

1083
        hash := sha256.New()
1✔
1084
        _, _ = hash.Write([]byte(idData))
1✔
1085
        idDataSha256 = hash.Sum(nil)
1✔
1086

1✔
1087
        return idDataStruct, idDataSha256, nil
1✔
1088
}
1089

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

1✔
1100
        // FIXME: tenant_token is "" on purpose, will be removed
1✔
1101

1✔
1102
        l := log.FromContext(ctx)
1✔
1103

1✔
1104
        dev := model.NewDevice(req.DeviceId, req.IdData, req.PubKey)
1✔
1105
        dev.Status = model.DevStatusPreauth
1✔
1106

1✔
1107
        idDataStruct, idDataSha256, err := parseIdData(req.IdData)
1✔
1108
        if err != nil {
2✔
1109
                return nil, MakeErrDevAuthBadRequest(err)
1✔
1110
        }
1✔
1111

1112
        dev.IdDataStruct = idDataStruct
1✔
1113
        dev.IdDataSha256 = idDataSha256
1✔
1114

1✔
1115
        err = d.db.AddDevice(ctx, *dev)
1✔
1116
        switch err {
1✔
1117
        case nil:
1✔
1118
                break
1✔
1119
        case store.ErrObjectExists:
1✔
1120
                dev, err = d.db.GetDeviceByIdentityDataHash(ctx, idDataSha256)
1✔
1121
                if err != nil {
1✔
1122
                        l.Error("failed to find device but could not preauthorize either")
×
1123
                        return nil, errors.New("failed to preauthorize device")
×
1124
                }
×
1125
                // if device exists we handle force: just add the authset, if force is in req
1126
                if req.Force {
1✔
UNCOV
1127
                        // record authentication request
×
UNCOV
1128
                        authset := &model.AuthSet{
×
UNCOV
1129
                                Id:           req.AuthSetId,
×
UNCOV
1130
                                IdData:       req.IdData,
×
UNCOV
1131
                                IdDataStruct: idDataStruct,
×
UNCOV
1132
                                IdDataSha256: idDataSha256,
×
UNCOV
1133
                                PubKey:       req.PubKey,
×
UNCOV
1134
                                DeviceId:     dev.Id,
×
UNCOV
1135
                                Status:       model.DevStatusPreauth,
×
UNCOV
1136
                                Timestamp:    uto.TimePtr(time.Now()),
×
UNCOV
1137
                        }
×
UNCOV
1138
                        err = d.db.UpsertAuthSetStatus(ctx, authset)
×
UNCOV
1139
                        if err != nil {
×
1140
                                return nil, err
×
1141
                        }
×
UNCOV
1142
                        return dev, nil
×
1143
                }
1144
                return dev, ErrDeviceExists
1✔
1145
        default:
1✔
1146
                return nil, errors.Wrap(err, "failed to add device")
1✔
1147
        }
1148

1149
        tenantId := ""
1✔
1150
        idData := identity.FromContext(ctx)
1✔
1151
        if idData != nil {
1✔
UNCOV
1152
                tenantId = idData.Tenant
×
UNCOV
1153
        }
×
1154

1155
        wfReq := orchestrator.UpdateDeviceStatusReq{
1✔
1156
                RequestId: requestid.FromContext(ctx),
1✔
1157
                Devices: []model.DeviceInventoryUpdate{{
1✔
1158
                        Id:       dev.Id,
1✔
1159
                        Revision: dev.Revision,
1✔
1160
                }},
1✔
1161
                TenantId: tenantId,
1✔
1162
                Status:   dev.Status,
1✔
1163
        }
1✔
1164
        if err = d.cOrch.SubmitUpdateDeviceStatusJob(ctx, wfReq); err != nil {
1✔
1165
                return nil, errors.Wrap(err, "update device status job error")
×
1166
        }
×
1167

1168
        // record authentication request
1169
        authset := model.AuthSet{
1✔
1170
                Id:           req.AuthSetId,
1✔
1171
                IdData:       req.IdData,
1✔
1172
                IdDataStruct: idDataStruct,
1✔
1173
                IdDataSha256: idDataSha256,
1✔
1174
                PubKey:       req.PubKey,
1✔
1175
                DeviceId:     req.DeviceId,
1✔
1176
                Status:       model.DevStatusPreauth,
1✔
1177
                Timestamp:    uto.TimePtr(time.Now()),
1✔
1178
        }
1✔
1179

1✔
1180
        err = d.db.AddAuthSet(ctx, authset)
1✔
1181
        switch err {
1✔
1182
        case nil:
1✔
1183
                if err := d.setDeviceIdentity(ctx, dev, tenantId); err != nil {
1✔
1184
                        return nil, err
×
1185
                }
×
1186
                return nil, nil
1✔
1187
        case store.ErrObjectExists:
1✔
1188
                dev, err = d.db.GetDeviceByIdentityDataHash(ctx, idDataSha256)
1✔
1189
                if err != nil {
1✔
1190
                        l.Error("failed to find device but could not preauthorize either")
×
1191
                        return nil, errors.New("failed to preauthorize device")
×
1192
                }
×
1193
                return dev, ErrDeviceExists
1✔
1194
        default:
1✔
1195
                return nil, errors.Wrap(err, "failed to add auth set")
1✔
1196
        }
1197
}
1198

1199
func (d *DevAuth) RevokeToken(ctx context.Context, tokenID string) error {
1✔
1200
        l := log.FromContext(ctx)
1✔
1201
        tokenOID := oid.FromString(tokenID)
1✔
1202

1✔
1203
        var token *jwt.Token
1✔
1204
        token, err := d.db.GetToken(ctx, tokenOID)
1✔
1205
        if err != nil {
2✔
1206
                return err
1✔
1207
        }
1✔
1208

1209
        l.Warnf("Revoke token with jti: %s", tokenID)
1✔
1210
        err = d.db.DeleteToken(ctx, tokenOID)
1✔
1211

1✔
1212
        if err == nil && d.cache != nil {
2✔
1213
                err = d.cacheDeleteToken(ctx, token.Claims.Subject.String())
1✔
1214
                err = errors.Wrapf(
1✔
1215
                        err,
1✔
1216
                        "failed to delete token for %s from cache",
1✔
1217
                        token.Claims.Subject.String(),
1✔
1218
                )
1✔
1219
        }
1✔
1220
        return err
1✔
1221
}
1222

1223
func verifyTenantClaim(ctx context.Context, verifyTenant bool, tenant string) error {
1✔
1224

1✔
1225
        l := log.FromContext(ctx)
1✔
1226

1✔
1227
        if verifyTenant {
2✔
1228
                if tenant == "" {
2✔
1229
                        l.Errorf("No tenant claim in the token")
1✔
1230
                        return jwt.ErrTokenInvalid
1✔
1231
                }
1✔
1232
        } else if tenant != "" {
1✔
1233
                l.Errorf("Unexpected tenant claim: %s in the token", tenant)
×
1234
                return jwt.ErrTokenInvalid
×
1235
        }
×
1236

1237
        return nil
1✔
1238
}
1239

1240
func (d *DevAuth) validateJWTToken(ctx context.Context, jti oid.ObjectID, raw string) error {
1✔
1241
        err := d.jwt.Validate(raw)
1✔
1242
        if err != nil && d.jwtFallback != nil {
2✔
1243
                err = d.jwtFallback.Validate(raw)
1✔
1244
        }
1✔
1245
        if err == jwt.ErrTokenExpired && jti.String() != "" {
2✔
1246
                log.FromContext(ctx).Errorf("Token %s expired: %v", jti.String(), err)
1✔
1247
                return d.handleExpiredToken(ctx, jti)
1✔
1248
        } else if err != nil {
3✔
1249
                log.FromContext(ctx).Errorf("Token %s invalid: %v", jti.String(), err)
1✔
1250
                return jwt.ErrTokenInvalid
1✔
1251
        }
1✔
1252
        return nil
1✔
1253
}
1254

1255
func (d *DevAuth) VerifyToken(ctx context.Context, raw string) error {
1✔
1256
        l := log.FromContext(ctx)
1✔
1257

1✔
1258
        token := &jwt.Token{}
1✔
1259
        err := token.UnmarshalJWT([]byte(raw), d.jwt.FromJWT)
1✔
1260
        jti := token.Claims.ID
1✔
1261
        if err != nil {
2✔
1262
                l.Errorf("Token %s invalid: %v", jti.String(), err)
1✔
1263
                return jwt.ErrTokenInvalid
1✔
1264
        } else if !token.Claims.Device {
3✔
1265
                l.Errorf("not a device token")
1✔
1266
                return jwt.ErrTokenInvalid
1✔
1267
        }
1✔
1268

1269
        err = verifyTenantClaim(ctx, d.verifyTenant, token.Claims.Tenant)
1✔
1270
        if err == nil {
2✔
1271
                err = d.checker.ValidateWithContext(ctx)
1✔
1272
        }
1✔
1273
        if err != nil {
2✔
1274
                return err
1✔
1275
        }
1✔
1276

1277
        origMethod := ctxhttpheader.FromContext(ctx, "X-Forwarded-Method")
1✔
1278
        origUri := ctxhttpheader.FromContext(ctx, "X-Forwarded-Uri")
1✔
1279
        origUri = purgeUriArgs(origUri)
1✔
1280

1✔
1281
        // throttle and try fetch token from cache - if cached, it was
1✔
1282
        // already verified against the db checks below, we trust it
1✔
1283
        cachedToken, err := d.cacheThrottleVerify(ctx, token, raw, origMethod, origUri)
1✔
1284

1✔
1285
        if err == cache.ErrTooManyRequests {
2✔
1286
                return err
1✔
1287
        }
1✔
1288

1289
        if cachedToken != "" && raw == cachedToken {
2✔
1290
                // update device check-in time
1✔
1291
                d.updateCheckInTime(
1✔
1292
                        ctx,
1✔
1293
                        token.Claims.Subject.String(),
1✔
1294
                        token.Claims.Tenant,
1✔
1295
                        nil,
1✔
1296
                )
1✔
1297
                return nil
1✔
1298
        }
1✔
1299

1300
        // caching is best effort, don't fail
1301
        if err != nil {
2✔
1302
                l.Errorf("Failed to throttle for token %v: %s, continue.", token, err.Error())
1✔
1303
        }
1✔
1304

1305
        // perform JWT signature and claims validation
1306
        err = d.validateJWTToken(ctx, jti, raw)
1✔
1307
        if err != nil {
2✔
1308
                return err
1✔
1309
        }
1✔
1310

1311
        // cache check was a MISS, hit the db for verification
1312
        // check if token is in the system
1313
        _, err = d.db.GetToken(ctx, jti)
1✔
1314
        if err != nil {
2✔
1315
                if err == store.ErrTokenNotFound {
2✔
1316
                        l.Errorf("Token %s not found", jti.String())
1✔
1317
                        return err
1✔
1318
                }
1✔
1319
                return errors.Wrapf(err, "Cannot get token with id: %s from database: %s", jti, err)
×
1320
        }
1321

1322
        auth, err := d.db.GetAuthSetById(ctx, jti.String())
1✔
1323
        if err != nil {
1✔
1324
                if err == store.ErrAuthSetNotFound {
×
1325
                        l.Errorf("Auth set %s not found", jti.String())
×
1326
                        return err
×
1327
                }
×
1328
                return err
×
1329
        }
1330

1331
        if auth.Status != model.DevStatusAccepted {
2✔
1332
                return jwt.ErrTokenInvalid
1✔
1333
        }
1✔
1334

1335
        // reject authentication for device that is in the process of
1336
        // decommissioning
1337
        dev, err := d.db.GetDeviceById(ctx, auth.DeviceId)
1✔
1338
        if err != nil {
1✔
1339
                return err
×
1340
        }
×
1341
        if dev.Decommissioning {
2✔
1342
                l.Errorf(
1✔
1343
                        "Token %s rejected, device %s is being decommissioned",
1✔
1344
                        jti.String(),
1✔
1345
                        auth.DeviceId,
1✔
1346
                )
1✔
1347
                return jwt.ErrTokenInvalid
1✔
1348
        }
1✔
1349

1350
        // update device check-in time
1351
        d.updateCheckInTime(
1✔
1352
                ctx,
1✔
1353
                token.Claims.Subject.String(),
1✔
1354
                token.Claims.Tenant,
1✔
1355
                dev.CheckInTime,
1✔
1356
        )
1✔
1357

1✔
1358
        // after successful token verification - cache it (best effort)
1✔
1359
        _ = d.cacheSetToken(ctx, token, raw)
1✔
1360

1✔
1361
        return nil
1✔
1362
}
1363

1364
func (d *DevAuth) handleExpiredToken(ctx context.Context, jti oid.ObjectID) error {
1✔
1365
        err := d.db.DeleteToken(ctx, jti)
1✔
1366
        if err == store.ErrTokenNotFound {
1✔
1367
                l := log.FromContext(ctx)
×
1368
                l.Errorf("Token %s not found", jti.String())
×
1369
                return err
×
1370
        }
×
1371
        if err != nil {
1✔
1372
                return errors.Wrapf(err, "Cannot delete token with jti: %s : %s", jti, err)
×
1373
        }
×
1374
        return jwt.ErrTokenExpired
1✔
1375
}
1376

1377
// purgeUriArgs removes query string args from an uri string
1378
// important for burst control (bursts are per uri without args)
1379
func purgeUriArgs(uri string) string {
1✔
1380
        return strings.Split(uri, "?")[0]
1✔
1381
}
1✔
1382

1383
func (d *DevAuth) cacheThrottleVerify(
1384
        ctx context.Context,
1385
        token *jwt.Token,
1386
        originalRaw,
1387
        origMethod,
1388
        origUri string,
1389
) (string, error) {
1✔
1390
        if d.cache == nil {
2✔
1391
                return "", nil
1✔
1392
        }
1✔
1393

1394
        // try get cached/precomputed limits
1395
        limits, err := d.getApiLimits(ctx,
1✔
1396
                token.Claims.Tenant,
1✔
1397
                token.Claims.Subject.String())
1✔
1398

1✔
1399
        if err != nil {
2✔
1400
                return "", err
1✔
1401
        }
1✔
1402

1403
        // apply throttling and fetch cached token
1404
        cached, err := d.cache.Throttle(ctx,
1✔
1405
                originalRaw,
1✔
1406
                *limits,
1✔
1407
                token.Claims.Tenant,
1✔
1408
                token.Claims.Subject.String(),
1✔
1409
                cache.IdTypeDevice,
1✔
1410
                origUri,
1✔
1411
                origMethod)
1✔
1412

1✔
1413
        return cached, err
1✔
1414
}
1415

1416
func (d *DevAuth) cacheSetToken(ctx context.Context, token *jwt.Token, raw string) error {
1✔
1417
        if d.cache == nil {
2✔
1418
                return nil
1✔
1419
        }
1✔
1420

1421
        expireIn := time.Duration(token.Claims.ExpiresAt.Unix()-d.clock.Now().Unix()) * time.Second
1✔
1422

1✔
1423
        return d.cache.CacheToken(ctx,
1✔
1424
                token.Claims.Tenant,
1✔
1425
                token.Claims.Subject.String(),
1✔
1426
                cache.IdTypeDevice,
1✔
1427
                raw,
1✔
1428
                expireIn)
1✔
1429
}
1430

1431
func (d *DevAuth) getApiLimits(
1432
        ctx context.Context,
1433
        tid,
1434
        did string,
1435
) (*ratelimits.ApiLimits, error) {
1✔
1436
        limits, err := d.cache.GetLimits(ctx, tid, did, cache.IdTypeDevice)
1✔
1437
        if err != nil {
2✔
1438
                return nil, err
1✔
1439
        }
1✔
1440

1441
        if limits != nil {
2✔
1442
                return limits, nil
1✔
1443
        }
1✔
1444

1445
        dev, err := d.db.GetDeviceById(ctx, did)
1✔
1446
        if err != nil {
1✔
1447
                return nil, err
×
1448
        }
×
1449

1450
        t, err := d.cTenant.GetTenant(ctx, tid)
1✔
1451
        if err != nil {
2✔
1452
                return nil, errors.Wrap(err, "request to get tenant failed")
1✔
1453
        }
1✔
1454
        if t == nil {
1✔
1455
                return nil, errors.New("tenant not found")
×
1456
        }
×
1457

1458
        finalLimits := apiLimitsOverride(t.ApiLimits.DeviceLimits, dev.ApiLimits)
1✔
1459

1✔
1460
        err = d.cache.CacheLimits(ctx, finalLimits, tid, did, cache.IdTypeDevice)
1✔
1461

1✔
1462
        return &finalLimits, err
1✔
1463
}
1464

1465
func (d *DevAuth) cacheDeleteToken(ctx context.Context, did string) error {
1✔
1466
        if d.cache == nil {
2✔
1467
                return nil
1✔
1468
        }
1✔
1469

1470
        idData := identity.FromContext(ctx)
1✔
1471
        if idData == nil {
2✔
1472
                return errors.New("can't unpack tenant identity data from context")
1✔
1473
        }
1✔
1474
        tid := idData.Tenant
1✔
1475

1✔
1476
        return d.cache.DeleteToken(ctx, tid, did, cache.IdTypeDevice)
1✔
1477
}
1478

1479
// TODO move to 'ratelimits', as ApiLimits methods maybe?
1480
func apiLimitsOverride(src, dest ratelimits.ApiLimits) ratelimits.ApiLimits {
1✔
1481
        // override only if not default
1✔
1482
        if dest.ApiQuota.MaxCalls != 0 && dest.ApiQuota.IntervalSec != 0 {
2✔
1483
                src.ApiQuota.MaxCalls = dest.ApiQuota.MaxCalls
1✔
1484
                src.ApiQuota.IntervalSec = dest.ApiQuota.IntervalSec
1✔
1485
        }
1✔
1486

1487
        out := make([]ratelimits.ApiBurst, len(src.ApiBursts))
1✔
1488
        copy(out, src.ApiBursts)
1✔
1489

1✔
1490
        for _, bdest := range dest.ApiBursts {
2✔
1491
                found := false
1✔
1492
                for i, bsrc := range src.ApiBursts {
2✔
1493
                        if bdest.Action == bsrc.Action &&
1✔
1494
                                bdest.Uri == bsrc.Uri {
2✔
1495
                                out[i].MinIntervalSec = bdest.MinIntervalSec
1✔
1496
                                found = true
1✔
1497
                        }
1✔
1498
                }
1499

1500
                if !found {
2✔
1501
                        out = append(out,
1✔
1502
                                ratelimits.ApiBurst{
1✔
1503
                                        Action:         bdest.Action,
1✔
1504
                                        Uri:            bdest.Uri,
1✔
1505
                                        MinIntervalSec: bdest.MinIntervalSec},
1✔
1506
                        )
1✔
1507
                }
1✔
1508
        }
1509

1510
        src.ApiBursts = out
1✔
1511
        return src
1✔
1512
}
1513

1514
func (d *DevAuth) GetLimit(ctx context.Context, name string) (*model.Limit, error) {
1✔
1515
        lim, err := d.db.GetLimit(ctx, name)
1✔
1516

1✔
1517
        switch err {
1✔
1518
        case nil:
1✔
1519
                return lim, nil
1✔
1520
        case store.ErrLimitNotFound:
1✔
1521
                return &model.Limit{Name: name, Value: 0}, nil
1✔
1522
        default:
1✔
1523
                return nil, err
1✔
1524
        }
1525
}
1526

1527
func (d *DevAuth) GetTenantLimit(
1528
        ctx context.Context,
1529
        name,
1530
        tenant_id string,
1531
) (*model.Limit, error) {
1✔
1532
        ctx = identity.WithContext(ctx, &identity.Identity{
1✔
1533
                Tenant: tenant_id,
1✔
1534
        })
1✔
1535

1✔
1536
        return d.GetLimit(ctx, name)
1✔
1537
}
1✔
1538

1539
func (d *DevAuth) WithJWTFallbackHandler(handler jwt.Handler) *DevAuth {
1✔
1540
        d.jwtFallback = handler
1✔
1541
        return d
1✔
1542
}
1✔
1543

1544
// WithTenantVerification will force verification of tenant token with tenant
1545
// administrator when processing device authentication requests. Returns an
1546
// updated devauth.
1547
func (d *DevAuth) WithTenantVerification(c tenant.ClientRunner) *DevAuth {
1✔
1548
        d.cTenant = c
1✔
1549
        d.verifyTenant = true
1✔
1550
        return d
1✔
1551
}
1✔
1552

1553
func (d *DevAuth) WithCache(c cache.Cache) *DevAuth {
1✔
1554
        d.cache = c
1✔
1555
        return d
1✔
1556
}
1✔
1557

1558
func (d *DevAuth) WithClock(c utils.Clock) *DevAuth {
1✔
1559
        d.clock = c
1✔
1560
        return d
1✔
1561
}
1✔
1562

1563
func (d *DevAuth) SetTenantLimit(ctx context.Context, tenant_id string, limit model.Limit) error {
1✔
1564
        l := log.FromContext(ctx)
1✔
1565

1✔
1566
        ctx = identity.WithContext(ctx, &identity.Identity{
1✔
1567
                Tenant: tenant_id,
1✔
1568
        })
1✔
1569

1✔
1570
        l.Infof("setting limit %v for tenant %v", limit, tenant_id)
1✔
1571

1✔
1572
        if err := d.db.PutLimit(ctx, limit); err != nil {
2✔
1573
                l.Errorf("failed to save limit %v for tenant %v to database: %v",
1✔
1574
                        limit, tenant_id, err)
1✔
1575
                return errors.Wrapf(err, "failed to save limit %v for tenant %v to database",
1✔
1576
                        limit, tenant_id)
1✔
1577
        }
1✔
1578
        return nil
1✔
1579
}
1580

1581
func (d *DevAuth) DeleteTenantLimit(ctx context.Context, tenant_id string, limit string) error {
1✔
1582
        l := log.FromContext(ctx)
1✔
1583

1✔
1584
        ctx = identity.WithContext(ctx, &identity.Identity{
1✔
1585
                Tenant: tenant_id,
1✔
1586
        })
1✔
1587

1✔
1588
        l.Infof("removing limit %v for tenant %v", limit, tenant_id)
1✔
1589

1✔
1590
        if err := d.db.DeleteLimit(ctx, limit); err != nil {
2✔
1591
                l.Errorf("failed to delete limit %v for tenant %v to database: %v",
1✔
1592
                        limit, tenant_id, err)
1✔
1593
                return errors.Wrapf(err, "failed to delete limit %v for tenant %v to database",
1✔
1594
                        limit, tenant_id)
1✔
1595
        }
1✔
1596
        return nil
1✔
1597
}
1598

1599
func (d *DevAuth) GetDevCountByStatus(ctx context.Context, status string) (int, error) {
1✔
1600
        return d.db.GetDevCountByStatus(ctx, status)
1✔
1601
}
1✔
1602

1603
// canAcceptDevice checks if model.LimitMaxDeviceCount will be exceeded
1604
func (d *DevAuth) canAcceptDevice(ctx context.Context) (bool, error) {
1✔
1605
        limit, err := d.GetLimit(ctx, model.LimitMaxDeviceCount)
1✔
1606
        if err != nil {
2✔
1607
                return false, errors.Wrap(err, "can't get current device limit")
1✔
1608
        }
1✔
1609

1610
        if limit.Value == 0 {
2✔
1611
                return true, nil
1✔
1612
        }
1✔
1613

1614
        accepted, err := d.db.GetDevCountByStatus(ctx, model.DevStatusAccepted)
1✔
1615
        if err != nil {
2✔
1616
                return false, errors.Wrap(err, "can't get current device count")
1✔
1617
        }
1✔
1618

1619
        if uint64(accepted+1) <= limit.Value {
2✔
1620
                return true, nil
1✔
1621
        }
1✔
1622

1623
        return false, nil
1✔
1624
}
1625

1626
func (d *DevAuth) DeleteTokens(
1627
        ctx context.Context,
1628
        tenantID string,
1629
        deviceID string,
1630
) error {
1✔
1631
        var err error
1✔
1632
        ctx = identity.WithContext(ctx, &identity.Identity{
1✔
1633
                Tenant: tenantID,
1✔
1634
        })
1✔
1635

1✔
1636
        if deviceID != "" {
2✔
1637
                deviceOID := oid.FromString(deviceID)
1✔
1638
                if deviceOID.String() == "" {
1✔
1639
                        return ErrInvalidAuthSetID
×
1640
                }
×
1641
                err = d.db.DeleteTokenByDevId(ctx, deviceOID)
1✔
1642
        } else {
1✔
1643
                if err := d.cacheFlush(ctx, tenantID); err != nil {
2✔
1644
                        return errors.Wrapf(
1✔
1645
                                err,
1✔
1646
                                "failed to flush cache when cleaning tokens for tenant %v",
1✔
1647
                                tenantID,
1✔
1648
                        )
1✔
1649
                }
1✔
1650

1651
                err = d.db.DeleteTokens(ctx)
1✔
1652
        }
1653

1654
        if err != nil && err != store.ErrTokenNotFound {
2✔
1655
                return errors.Wrapf(
1✔
1656
                        err,
1✔
1657
                        "failed to delete tokens for tenant: %v, device id: %v",
1✔
1658
                        tenantID,
1✔
1659
                        deviceID,
1✔
1660
                )
1✔
1661
        }
1✔
1662

1663
        return nil
1✔
1664
}
1665

1666
func (d *DevAuth) cacheFlush(ctx context.Context, tenantID string) error {
1✔
1667
        if d.cache == nil {
1✔
1668
                return nil
×
1669
        }
×
1670

1671
        return d.cache.SuspendTenant(ctx, tenantID)
1✔
1672
}
1673

1674
func (d *DevAuth) GetTenantDeviceStatus(
1675
        ctx context.Context,
1676
        tenantId,
1677
        deviceId string,
1678
) (*model.Status, error) {
1✔
1679
        if tenantId != "" {
2✔
1680
                ctx = identity.WithContext(ctx, &identity.Identity{
1✔
1681
                        Tenant: tenantId,
1✔
1682
                })
1✔
1683
        }
1✔
1684
        dev, err := d.db.GetDeviceById(ctx, deviceId)
1✔
1685
        switch err {
1✔
1686
        case nil:
1✔
1687
                return &model.Status{Status: dev.Status}, nil
1✔
1688
        case store.ErrDevNotFound:
1✔
1689
                return nil, ErrDeviceNotFound
1✔
1690
        default:
1✔
1691
                return nil, errors.Wrapf(err, "get device %s failed", deviceId)
1✔
1692

1693
        }
1694
}
1695

1696
func (d *DevAuth) updateCheckInTime(
1697
        ctx context.Context,
1698
        deviceId string,
1699
        tenantId string,
1700
        previous *time.Time,
1701
) {
1✔
1702
        var err error
1✔
1703
        defer func() {
2✔
1704
                if err != nil {
1✔
1705
                        log.FromContext(ctx).Errorf(
×
1706
                                "failed to update device check-in time for device %s: %s",
×
1707
                                deviceId, err.Error(),
×
1708
                        )
×
1709
                }
×
1710
        }()
1711
        checkInTime := uto.TimePtr(time.Now().UTC())
1✔
1712
        // in case cache is disabled, use mongo
1✔
1713
        if d.cache == nil {
2✔
1714
                if err = d.db.UpdateDevice(ctx,
1✔
1715
                        deviceId,
1✔
1716
                        model.DeviceUpdate{
1✔
1717
                                CheckInTime: checkInTime,
1✔
1718
                        }); err != nil {
1✔
1719
                        return
×
1720
                }
×
1721
        } else {
1✔
1722
                // get check-in time from cache
1✔
1723
                previous, err = d.cache.GetCheckInTime(ctx, tenantId, deviceId)
1✔
1724
                if err != nil {
1✔
1725
                        return
×
1726
                }
×
1727
                // update check-in time in cache
1728
                err = d.cache.CacheCheckInTime(ctx, checkInTime, tenantId, deviceId)
1✔
1729
                if err != nil {
1✔
1730
                        return
×
1731
                }
×
1732
        }
1733
        // compare data without a time of current and previous check-in time
1734
        // and if it's different trigger reindexing (if enabled)
1735
        // and save check-in time in the database
1736
        if previous == nil ||
1✔
1737
                (previous != nil &&
1✔
1738
                        !previous.Truncate(24*time.Hour).Equal(checkInTime.Truncate(24*time.Hour))) {
2✔
1739
                // trigger reindexing
1✔
1740
                if d.config.EnableReporting {
1✔
1741
                        if err = d.cOrch.SubmitReindexReporting(ctx, deviceId); err != nil {
×
1742
                                err = errors.Wrap(err, "reindex reporting job error")
×
1743
                                return
×
1744
                        }
×
1745
                } else {
1✔
1746
                        // update check-in time in inventory
1✔
1747
                        if err := d.syncCheckInTime(ctx, checkInTime, deviceId, tenantId); err != nil {
1✔
1748
                                log.FromContext(ctx).Errorf(
×
1749
                                        "failed to synchronize device check-in time with inventory: device %s: %s",
×
1750
                                        deviceId, err.Error(),
×
1751
                                )
×
1752
                        }
×
1753
                }
1754
                // dump cached value to database
1755
                if d.cache != nil {
2✔
1756
                        if err = d.db.UpdateDevice(ctx,
1✔
1757
                                deviceId,
1✔
1758
                                model.DeviceUpdate{
1✔
1759
                                        CheckInTime: checkInTime,
1✔
1760
                                }); err != nil {
1✔
1761
                                return
×
1762
                        }
×
1763
                }
1764
        }
1765
}
1766

1767
func (d *DevAuth) syncCheckInTime(
1768
        ctx context.Context,
1769
        checkInTime *time.Time,
1770
        deviceId string,
1771
        tenantId string,
1772
) error {
1✔
1773
        attributes := []model.DeviceAttribute{
1✔
1774
                {
1✔
1775
                        Name:        "check_in_time",
1✔
1776
                        Description: nil,
1✔
1777
                        Value:       checkInTime,
1✔
1778
                        Scope:       InventoryScopeSystem,
1✔
1779
                },
1✔
1780
        }
1✔
1781
        attrJson, err := json.Marshal(attributes)
1✔
1782
        if err != nil {
1✔
1783
                return errors.New("internal error: cannot marshal attributes into json")
×
1784
        }
×
1785
        if err := d.cOrch.SubmitUpdateDeviceInventoryJob(
1✔
1786
                ctx,
1✔
1787
                orchestrator.UpdateDeviceInventoryReq{
1✔
1788
                        RequestId:  requestid.FromContext(ctx),
1✔
1789
                        TenantId:   tenantId,
1✔
1790
                        DeviceId:   deviceId,
1✔
1791
                        Scope:      InventoryScopeSystem,
1✔
1792
                        Attributes: string(attrJson),
1✔
1793
                }); err != nil {
1✔
1794
                return errors.Wrap(err, "failed to start device inventory update job")
×
1795
        }
×
1796
        return nil
1✔
1797
}
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