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

mendersoftware / mender-server / 1806018826

07 May 2025 07:21PM UTC coverage: 65.308% (+0.02%) from 65.286%
1806018826

push

gitlab-ci

web-flow
Merge pull request #646 from oldgiova/QA-983-resource-group

Further pipeline optimizations

31828 of 48735 relevant lines covered (65.31%)

1.37 hits per line

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

88.15
/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/rate"
32
        "github.com/mendersoftware/mender-server/pkg/ratelimits"
33
        "github.com/mendersoftware/mender-server/pkg/requestid"
34

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

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

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

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

65
func IsErrDevAuthUnauthorized(e error) bool {
2✔
66
        return strings.HasPrefix(e.Error(), MsgErrDevAuthUnauthorized)
2✔
67
}
2✔
68

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

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

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

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

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

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

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

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

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

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

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

131
        // deviceRatelimiter is used to limit authenticated device requests
132
        rateLimiter              rate.Limiter
133
        rateLimiterWeights       map[string]float64
134
        rateLimiterWeightDefault float64
135
}
136

137
type Config struct {
138
        // token issuer
139
        Issuer string
140
        // token expiration time
141
        ExpirationTime int64
142
        // Default tenant token to use when the client supplies none. Can be
143
        // empty
144
        DefaultTenantToken string
145
        InventoryAddr      string
146

147
        EnableReporting bool
148
        HaveAddons      bool
149
}
150

151
func NewDevAuth(d store.DataStore, co orchestrator.ClientRunner,
152
        jwt jwt.Handler, config Config,
153
) *DevAuth {
3✔
154
        // initialize checker using an empty merge (returns nil on validate)
3✔
155
        checker := access.Merge()
3✔
156
        if config.HaveAddons {
3✔
157
                checker = access.NewAddonChecker()
×
158
        }
×
159

160
        return &DevAuth{
3✔
161
                db:           d,
3✔
162
                invClient:    inventory.NewClient(config.InventoryAddr, false),
3✔
163
                cOrch:        co,
3✔
164
                jwt:          jwt,
3✔
165
                verifyTenant: false,
3✔
166
                config:       config,
3✔
167
                clock:        utils.NewClock(),
3✔
168
                checker:      checker,
3✔
169
        }
3✔
170
}
171

172
func (d *DevAuth) HealthCheck(ctx context.Context) error {
2✔
173
        err := d.db.Ping(ctx)
2✔
174
        if err != nil {
3✔
175
                return errors.Wrap(err, "error reaching MongoDB")
1✔
176
        }
1✔
177
        err = d.invClient.CheckHealth(ctx)
2✔
178
        if err != nil {
3✔
179
                return errors.Wrap(err, "Inventory service unhealthy")
1✔
180
        }
1✔
181
        err = d.cOrch.CheckHealth(ctx)
2✔
182
        if err != nil {
3✔
183
                return errors.Wrap(err, "Workflows service unhealthy")
1✔
184
        }
1✔
185
        if d.verifyTenant {
3✔
186
                err = d.cTenant.CheckHealth(ctx)
1✔
187
                if err != nil {
2✔
188
                        return errors.Wrap(err, "Tenantadm service unhealthy")
1✔
189
                }
1✔
190
        }
191
        return nil
2✔
192
}
193

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

234
func (d *DevAuth) getDeviceFromAuthRequest(
235
        ctx context.Context,
236
        r *model.AuthReq,
237
) (*model.Device, error) {
3✔
238
        dev := model.NewDevice("", r.IdData, r.PubKey)
3✔
239

3✔
240
        l := log.FromContext(ctx)
3✔
241

3✔
242
        idDataStruct, idDataSha256, err := parseIdData(r.IdData)
3✔
243
        if err != nil {
3✔
244
                return nil, MakeErrDevAuthBadRequest(err)
×
245
        }
×
246

247
        dev.IdDataStruct = idDataStruct
3✔
248
        dev.IdDataSha256 = idDataSha256
3✔
249

3✔
250
        // record device
3✔
251
        err = d.db.AddDevice(ctx, *dev)
3✔
252
        addDeviceErr := err
3✔
253
        if err != nil && err != store.ErrObjectExists {
4✔
254
                l.Errorf("failed to add/find device: %v", err)
1✔
255
                return nil, err
1✔
256
        }
1✔
257

258
        // either the device was added or it was already present, in any case,
259
        // pull it from DB
260
        dev, err = d.db.GetDeviceByIdentityDataHash(ctx, idDataSha256)
3✔
261
        if err != nil {
4✔
262
                l.Error("failed to find device but could not add either")
1✔
263
                return nil, errors.New("failed to locate device")
1✔
264
        }
1✔
265

266
        idData := identity.FromContext(ctx)
3✔
267
        tenantId := ""
3✔
268
        if idData != nil {
4✔
269
                tenantId = idData.Tenant
1✔
270
        }
1✔
271
        if addDeviceErr != store.ErrObjectExists {
6✔
272
                if err := d.setDeviceIdentity(ctx, dev, tenantId); err != nil {
3✔
273
                        return nil, err
×
274
                }
×
275
        }
276

277
        // check if the device is in the decommissioning state
278
        if dev.Decommissioning {
3✔
279
                l.Warnf("Device %s in the decommissioning state.", dev.Id)
×
280
                return nil, ErrDevAuthUnauthorized
×
281
        }
×
282

283
        return dev, nil
3✔
284
}
285

286
func (d *DevAuth) signToken(ctx context.Context) jwt.SignFunc {
3✔
287
        return func(t *jwt.Token) (string, error) {
6✔
288
                return d.jwt.ToJWT(t)
3✔
289
        }
3✔
290
}
291

292
func (d *DevAuth) doVerifyTenant(ctx context.Context, token string) (*tenant.Tenant, error) {
1✔
293
        t, err := d.cTenant.VerifyToken(ctx, token)
1✔
294
        if err != nil {
2✔
295
                if tenant.IsErrTokenVerificationFailed(err) {
2✔
296
                        return nil, MakeErrDevAuthUnauthorized(err)
1✔
297
                }
1✔
298

299
                return nil, errors.Wrap(err, "request to verify tenant token failed")
1✔
300
        }
301

302
        return t, nil
1✔
303
}
304

305
func (d *DevAuth) getTenantWithDefault(
306
        ctx context.Context,
307
        tenantToken,
308
        defaultToken string,
309
) (context.Context, *tenant.Tenant, error) {
1✔
310
        l := log.FromContext(ctx)
1✔
311

1✔
312
        if tenantToken == "" && defaultToken == "" {
2✔
313
                return nil, nil, MakeErrDevAuthUnauthorized(errors.New("tenant token missing"))
1✔
314
        }
1✔
315

316
        var t *tenant.Tenant
1✔
317
        var err error
1✔
318

1✔
319
        // try the provided token
1✔
320
        // but continue on errors and maybe try the default token
1✔
321
        if tenantToken != "" {
2✔
322
                t, err = d.doVerifyTenant(ctx, tenantToken)
1✔
323
                if err != nil {
2✔
324
                        l.Errorf("Failed to verify supplied tenant token: %s", err.Error())
1✔
325
                }
1✔
326
        }
327

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

337
        // none of the tokens worked
338
        if err != nil {
2✔
339
                if tenant.IsErrTokenVerificationFailed(err) {
1✔
340
                        return ctx, nil, MakeErrDevAuthUnauthorized(err)
×
341
                }
×
342
                return ctx, nil, err
1✔
343
        }
344

345
        tCtx := identity.WithContext(ctx, &identity.Identity{
1✔
346
                Subject: "internal",
1✔
347
                Tenant:  t.ID,
1✔
348
        })
1✔
349

1✔
350
        return tCtx, t, nil
1✔
351
}
352

353
func (d *DevAuth) SubmitAuthRequest(ctx context.Context, r *model.AuthReq) (string, error) {
3✔
354
        l := log.FromContext(ctx)
3✔
355

3✔
356
        var tenant *tenant.Tenant
3✔
357
        var err error
3✔
358

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

372
        // first, try to handle preauthorization
373
        authSet, err := d.processPreAuthRequest(ctx, r)
3✔
374
        if err != nil {
4✔
375
                return "", err
1✔
376
        }
1✔
377

378
        // if not a preauth request, process with regular auth request handling
379
        if authSet == nil {
6✔
380
                authSet, err = d.processAuthRequest(ctx, r)
3✔
381
                if err != nil {
4✔
382
                        return "", err
1✔
383
                }
1✔
384
        }
385

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

3✔
409
                if d.verifyTenant {
4✔
410
                        token.Claims.Tenant = tenant.ID
1✔
411
                        token.Claims.Plan = tenant.Plan
1✔
412
                        token.Claims.Addons = tenant.Addons
1✔
413
                        token.Claims.Trial = tenant.Trial
1✔
414
                } else {
4✔
415
                        token.Claims.Plan = plan.PlanEnterprise
3✔
416
                        token.Addons = addons.AllAddonsEnabled
3✔
417
                }
3✔
418

419
                // sign and encode as JWT
420
                raw, err := token.MarshalJWT(d.signToken(ctx))
3✔
421
                if err != nil {
3✔
422
                        return "", errors.Wrap(err, "generate token error")
×
423
                }
×
424

425
                if err := d.db.AddToken(ctx, token); err != nil {
3✔
426
                        return "", errors.Wrap(err, "add token error")
×
427
                }
×
428

429
                l.Infof("Token %s assigned to device %s",
3✔
430
                        token.Claims.ID, token.Claims.Subject)
3✔
431
                d.updateCheckInTime(ctx, authSet.DeviceId, token.Claims.Tenant, nil)
3✔
432
                return string(raw), nil
3✔
433
        }
434

435
        // no token, return device unauthorized
436
        return "", ErrDevAuthUnauthorized
3✔
437
}
438

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

452
        // check if the device is in the decommissioning state
453
        if dev.Decommissioning {
3✔
454
                l := log.FromContext(ctx)
1✔
455
                l.Warnf("Device %s in the decommissioning state.", dev.Id)
1✔
456
                return nil, ErrDevAuthUnauthorized
1✔
457
        }
1✔
458

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

467
                if !allow {
3✔
468
                        return nil, ErrMaxDeviceCountReached
1✔
469
                }
1✔
470
        }
471

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

485
        if err := d.updateDeviceStatus(
2✔
486
                ctx,
2✔
487
                aset.DeviceId,
2✔
488
                model.DevStatusAccepted,
2✔
489
                currentStatus,
2✔
490
        ); err != nil {
2✔
491
                return nil, err
×
492
        }
×
493

494
        aset.Status = model.DevStatusAccepted
2✔
495
        dev.Status = model.DevStatusAccepted
2✔
496
        dev.AuthSets = append(dev.AuthSets, *aset)
2✔
497

2✔
498
        if !deviceAlreadyAccepted {
4✔
499
                reqId := requestid.FromContext(ctx)
2✔
500
                var tenantID string
2✔
501
                if idty := identity.FromContext(ctx); idty != nil {
2✔
502
                        tenantID = idty.Tenant
×
503
                }
×
504

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

521
func (d *DevAuth) processPreAuthRequest(
522
        ctx context.Context,
523
        r *model.AuthReq,
524
) (*model.AuthSet, error) {
3✔
525
        _, idDataSha256, err := parseIdData(r.IdData)
3✔
526
        if err != nil {
4✔
527
                return nil, MakeErrDevAuthBadRequest(err)
1✔
528
        }
1✔
529

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

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

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

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

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

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

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

613
        return nil
3✔
614
}
615

616
// processAuthRequest will process incoming auth request and record authentication
617
// data information it contains. Returns a tupe (auth set, error). If no errors were
618
// present, model.AuthSet.Status will indicate the status of device admission
619
func (d *DevAuth) processAuthRequest(
620
        ctx context.Context,
621
        r *model.AuthReq,
622
) (*model.AuthSet, error) {
3✔
623
        l := log.FromContext(ctx)
3✔
624

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

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

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

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

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

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

666
        return areq, nil
3✔
667
}
668

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

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

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

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

712
        return devs, err
3✔
713
}
714

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

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

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

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

748
        return dev, err
3✔
749
}
750

751
// DecommissionDevice deletes device and all its tokens
752
func (d *DevAuth) DecommissionDevice(ctx context.Context, devID string) error {
3✔
753
        l := log.FromContext(ctx)
3✔
754

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

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

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

772
        reqId := requestid.FromContext(ctx)
3✔
773

3✔
774
        tenantID := ""
3✔
775
        idData := identity.FromContext(ctx)
3✔
776
        if idData != nil {
6✔
777
                tenantID = idData.Tenant
3✔
778
        }
3✔
779

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

791
        return err
3✔
792
}
793

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

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

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

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

825
        return nil
3✔
826
}
827

828
// Deletes device authentication set, and optionally the device.
829
func (d *DevAuth) DeleteAuthSet(ctx context.Context, devID string, authId string) error {
3✔
830
        l := log.FromContext(ctx)
3✔
831

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

3✔
836
        err := d.cacheDeleteToken(ctx, devID)
3✔
837
        if err != nil {
4✔
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)
3✔
843
        if err != nil {
6✔
844
                if err == store.ErrAuthSetNotFound {
6✔
845
                        return err
3✔
846
                }
3✔
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 {
4✔
852
                return err
1✔
853
        }
1✔
854

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

894
                if d.config.EnableReporting {
3✔
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

900
                return nil
1✔
901
        }
902

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

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

3✔
909
        aset, err := d.db.GetAuthSetById(ctx, auth_id)
3✔
910
        if err != nil {
6✔
911
                if err == store.ErrAuthSetNotFound {
6✔
912
                        return err
3✔
913
                }
3✔
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 {
4✔
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 {
5✔
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)
3✔
930
        if err != nil {
4✔
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)
3✔
936
        if err != nil {
4✔
937
                return err
1✔
938
        }
1✔
939

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

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

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

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

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

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

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

978
        return nil
3✔
979
}
980

981
func (d *DevAuth) setAuthSetStatus(
982
        ctx context.Context,
983
        deviceID string,
984
        authID string,
985
        status string,
986
) error {
3✔
987
        aset, err := d.db.GetAuthSetById(ctx, authID)
3✔
988
        if err != nil {
3✔
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 {
3✔
996
                return ErrDevIdAuthIdMismatch
×
997
        }
×
998

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

1003
        currentStatus := aset.Status
3✔
1004

3✔
1005
        if aset.Status == model.DevStatusAccepted &&
3✔
1006
                (status == model.DevStatusRejected || status == model.DevStatusPending) {
6✔
1007
                deviceOID := oid.FromString(aset.DeviceId)
3✔
1008
                // delete device token
3✔
1009
                err := d.db.DeleteTokenByDevId(ctx, deviceOID)
3✔
1010
                if err != nil && err != store.ErrTokenNotFound {
4✔
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 {
6✔
1017
                // reject all accepted auth sets for this device first
3✔
1018
                err := d.db.RejectAuthSetsForDevice(ctx, deviceID)
3✔
1019
                if err != nil && err != store.ErrAuthSetNotFound {
4✔
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{
3✔
1025
                Status: status,
3✔
1026
        }); err != nil {
4✔
1027
                return errors.Wrap(err, "db update device auth set error")
1✔
1028
        }
1✔
1029

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

1036
func (d *DevAuth) RejectDeviceAuth(ctx context.Context, device_id string, auth_id string) error {
3✔
1037
        aset, err := d.db.GetAuthSetById(ctx, auth_id)
3✔
1038
        if err != nil {
5✔
1039
                if err == store.ErrAuthSetNotFound {
3✔
1040
                        return err
1✔
1041
                }
1✔
1042
                return errors.Wrap(err, "db get auth set error")
1✔
1043
        } else if aset.Status != model.DevStatusPending && aset.Status != model.DevStatusAccepted {
4✔
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)
3✔
1049
        if err != nil {
4✔
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)
3✔
1054
}
1055

1056
func (d *DevAuth) ResetDeviceAuth(ctx context.Context, device_id string, auth_id string) error {
2✔
1057
        aset, err := d.db.GetAuthSetById(ctx, auth_id)
2✔
1058
        if err != nil {
3✔
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 {
2✔
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)
2✔
1068
}
1069

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

3✔
1074
        err := json.Unmarshal([]byte(idData), &idDataStruct)
3✔
1075
        if err != nil {
4✔
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()
3✔
1084
        _, _ = hash.Write([]byte(idData))
3✔
1085
        idDataSha256 = hash.Sum(nil)
3✔
1086

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

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

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

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

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

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

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

2✔
1115
        err = d.db.AddDevice(ctx, *dev)
2✔
1116
        switch err {
2✔
1117
        case nil:
2✔
1118
                break
2✔
1119
        case store.ErrObjectExists:
2✔
1120
                dev, err = d.db.GetDeviceByIdentityDataHash(ctx, idDataSha256)
2✔
1121
                if err != nil {
2✔
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 {
3✔
1127
                        // record authentication request
1✔
1128
                        authset := &model.AuthSet{
1✔
1129
                                Id:           req.AuthSetId,
1✔
1130
                                IdData:       req.IdData,
1✔
1131
                                IdDataStruct: idDataStruct,
1✔
1132
                                IdDataSha256: idDataSha256,
1✔
1133
                                PubKey:       req.PubKey,
1✔
1134
                                DeviceId:     dev.Id,
1✔
1135
                                Status:       model.DevStatusPreauth,
1✔
1136
                                Timestamp:    uto.TimePtr(time.Now()),
1✔
1137
                        }
1✔
1138
                        err = d.db.UpsertAuthSetStatus(ctx, authset)
1✔
1139
                        if err != nil {
1✔
1140
                                return nil, err
×
1141
                        }
×
1142
                        return dev, nil
1✔
1143
                }
1144
                return dev, ErrDeviceExists
2✔
1145
        default:
1✔
1146
                return nil, errors.Wrap(err, "failed to add device")
1✔
1147
        }
1148

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

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

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

2✔
1180
        err = d.db.AddAuthSet(ctx, authset)
2✔
1181
        switch err {
2✔
1182
        case nil:
2✔
1183
                if err := d.setDeviceIdentity(ctx, dev, tenantId); err != nil {
2✔
1184
                        return nil, err
×
1185
                }
×
1186
                return nil, nil
2✔
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 {
2✔
1200
        l := log.FromContext(ctx)
2✔
1201
        tokenOID := oid.FromString(tokenID)
2✔
1202

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

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

2✔
1212
        if err == nil && d.cache != nil {
3✔
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
2✔
1221
}
1222

1223
func verifyTenantClaim(ctx context.Context, verifyTenant bool, tenant string) error {
3✔
1224
        l := log.FromContext(ctx)
3✔
1225

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

1236
        return nil
3✔
1237
}
1238

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

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

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

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

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

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

3✔
1284
        if err == cache.ErrTooManyRequests {
4✔
1285
                return err
1✔
1286
        } else if errRate := d.checkRateLimits(ctx); errRate != nil {
4✔
1287
                return errRate
×
1288
        }
×
1289

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

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

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

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

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

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

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

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

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

3✔
1362
        return nil
3✔
1363
}
1364

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

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

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

1395
        // try get cached/precomputed limits
1396
        limits, err := d.getApiLimits(ctx,
1✔
1397
                token.Claims.Tenant,
1✔
1398
                token.Claims.Subject.String())
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 {
3✔
1417
        if d.cache == nil {
6✔
1418
                return nil
3✔
1419
        }
3✔
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 {
3✔
1466
        if d.cache == nil {
6✔
1467
                return nil
3✔
1468
        }
3✔
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
                }
1✔
1509
        }
1510

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

1515
func (d *DevAuth) GetLimit(ctx context.Context, name string) (*model.Limit, error) {
3✔
1516
        l := log.FromContext(ctx)
3✔
1517
        var (
3✔
1518
                limit *model.Limit
3✔
1519
                err   error
3✔
1520
        )
3✔
1521
        if d.cache != nil {
3✔
1522
                limit, err = d.cache.GetLimit(ctx, name)
×
1523
                if err != nil {
×
1524
                        l.Warnf("error fetching limit from cache: %s", err.Error())
×
1525
                }
×
1526
        }
1527
        if limit == nil {
6✔
1528
                limit, err = d.db.GetLimit(ctx, name)
3✔
1529
                if err != nil {
6✔
1530
                        if errors.Is(err, store.ErrLimitNotFound) {
6✔
1531
                                limit = &model.Limit{Name: name, Value: 0}
3✔
1532
                                err = nil
3✔
1533
                        } else {
4✔
1534
                                return nil, err
1✔
1535
                        }
1✔
1536
                }
1537
                if d.cache != nil {
3✔
1538
                        errCache := d.cache.SetLimit(ctx, limit)
×
1539
                        if errCache != nil {
×
1540
                                l.Warnf("failed to store limit %q in cache: %s", name, errCache.Error())
×
1541
                        }
×
1542
                }
1543
        }
1544
        return limit, err
3✔
1545
}
1546

1547
func (d *DevAuth) GetTenantLimit(
1548
        ctx context.Context,
1549
        name,
1550
        tenant_id string,
1551
) (*model.Limit, error) {
3✔
1552
        ctx = identity.WithContext(ctx, &identity.Identity{
3✔
1553
                Tenant: tenant_id,
3✔
1554
        })
3✔
1555

3✔
1556
        return d.GetLimit(ctx, name)
3✔
1557
}
3✔
1558

1559
func (d *DevAuth) WithJWTFallbackHandler(handler jwt.Handler) *DevAuth {
1✔
1560
        d.jwtFallback = handler
1✔
1561
        return d
1✔
1562
}
1✔
1563

1564
// WithTenantVerification will force verification of tenant token with tenant
1565
// administrator when processing device authentication requests. Returns an
1566
// updated devauth.
1567
func (d *DevAuth) WithTenantVerification(c tenant.ClientRunner) *DevAuth {
1✔
1568
        d.cTenant = c
1✔
1569
        d.verifyTenant = true
1✔
1570
        return d
1✔
1571
}
1✔
1572

1573
func (d *DevAuth) WithCache(c cache.Cache) *DevAuth {
1✔
1574
        d.cache = c
1✔
1575
        return d
1✔
1576
}
1✔
1577

1578
func (d *DevAuth) WithRatelimits(
1579
        rl rate.Limiter,
1580
        weights map[string]float64,
1581
        defaultQuota float64,
1582
) *DevAuth {
1✔
1583
        if rl != nil {
2✔
1584
                d.rateLimiter = rl
1✔
1585
                d.rateLimiterWeights = weights
1✔
1586
                d.rateLimiterWeightDefault = defaultQuota
1✔
1587
        }
1✔
1588
        return d
1✔
1589
}
1590

1591
func (d *DevAuth) WithClock(c utils.Clock) *DevAuth {
1✔
1592
        d.clock = c
1✔
1593
        return d
1✔
1594
}
1✔
1595

1596
func (d *DevAuth) SetTenantLimit(ctx context.Context, tenant_id string, limit model.Limit) error {
2✔
1597
        l := log.FromContext(ctx)
2✔
1598

2✔
1599
        ctx = identity.WithContext(ctx, &identity.Identity{
2✔
1600
                Tenant: tenant_id,
2✔
1601
        })
2✔
1602

2✔
1603
        l.Infof("setting limit %v for tenant %v", limit, tenant_id)
2✔
1604

2✔
1605
        if err := d.db.PutLimit(ctx, limit); err != nil {
3✔
1606
                l.Errorf("failed to save limit %v for tenant %v to database: %v",
1✔
1607
                        limit, tenant_id, err)
1✔
1608
                return errors.Wrapf(err, "failed to save limit %v for tenant %v to database",
1✔
1609
                        limit, tenant_id)
1✔
1610
        }
1✔
1611
        if d.cache != nil {
2✔
1612
                errCache := d.cache.SetLimit(ctx, &limit)
×
1613
                if errCache != nil {
×
1614
                        l.Warnf("failed to store limit %q in cache: %s", limit.Name, errCache.Error())
×
1615
                }
×
1616
        }
1617
        return nil
2✔
1618
}
1619

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

2✔
1623
        ctx = identity.WithContext(ctx, &identity.Identity{
2✔
1624
                Tenant: tenant_id,
2✔
1625
        })
2✔
1626

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

2✔
1629
        if err := d.db.DeleteLimit(ctx, limit); err != nil {
3✔
1630
                l.Errorf("failed to delete limit %v for tenant %v to database: %v",
1✔
1631
                        limit, tenant_id, err)
1✔
1632
                return errors.Wrapf(err, "failed to delete limit %v for tenant %v to database",
1✔
1633
                        limit, tenant_id)
1✔
1634
        }
1✔
1635
        if d.cache != nil {
2✔
1636
                errCache := d.cache.DeleteLimit(ctx, limit)
×
1637
                if errCache != nil {
×
1638
                        l.Warnf("error removing limit %q from cache: %s", limit, errCache.Error())
×
1639
                }
×
1640
        }
1641
        return nil
2✔
1642
}
1643

1644
func (d *DevAuth) GetDevCountByStatus(ctx context.Context, status string) (int, error) {
3✔
1645
        return d.db.GetDevCountByStatus(ctx, status)
3✔
1646
}
3✔
1647

1648
// canAcceptDevice checks if model.LimitMaxDeviceCount will be exceeded
1649
func (d *DevAuth) canAcceptDevice(ctx context.Context) (bool, error) {
3✔
1650
        limit, err := d.GetLimit(ctx, model.LimitMaxDeviceCount)
3✔
1651
        if err != nil {
4✔
1652
                return false, errors.Wrap(err, "can't get current device limit")
1✔
1653
        }
1✔
1654

1655
        if limit.Value == 0 {
6✔
1656
                return true, nil
3✔
1657
        }
3✔
1658

1659
        accepted, err := d.db.GetDevCountByStatus(ctx, model.DevStatusAccepted)
1✔
1660
        if err != nil {
2✔
1661
                return false, errors.Wrap(err, "can't get current device count")
1✔
1662
        }
1✔
1663

1664
        if uint64(accepted+1) <= limit.Value {
2✔
1665
                return true, nil
1✔
1666
        }
1✔
1667

1668
        return false, nil
1✔
1669
}
1670

1671
func (d *DevAuth) DeleteTokens(
1672
        ctx context.Context,
1673
        tenantID string,
1674
        deviceID string,
1675
) error {
1✔
1676
        var err error
1✔
1677
        ctx = identity.WithContext(ctx, &identity.Identity{
1✔
1678
                Tenant: tenantID,
1✔
1679
        })
1✔
1680

1✔
1681
        if deviceID != "" {
2✔
1682
                deviceOID := oid.FromString(deviceID)
1✔
1683
                if deviceOID.String() == "" {
1✔
1684
                        return ErrInvalidAuthSetID
×
1685
                }
×
1686
                err = d.db.DeleteTokenByDevId(ctx, deviceOID)
1✔
1687
        } else {
1✔
1688
                if err := d.cacheFlush(ctx, tenantID); err != nil {
2✔
1689
                        return errors.Wrapf(
1✔
1690
                                err,
1✔
1691
                                "failed to flush cache when cleaning tokens for tenant %v",
1✔
1692
                                tenantID,
1✔
1693
                        )
1✔
1694
                }
1✔
1695

1696
                err = d.db.DeleteTokens(ctx)
1✔
1697
        }
1698

1699
        if err != nil && err != store.ErrTokenNotFound {
2✔
1700
                return errors.Wrapf(
1✔
1701
                        err,
1✔
1702
                        "failed to delete tokens for tenant: %v, device id: %v",
1✔
1703
                        tenantID,
1✔
1704
                        deviceID,
1✔
1705
                )
1✔
1706
        }
1✔
1707

1708
        return nil
1✔
1709
}
1710

1711
func (d *DevAuth) cacheFlush(ctx context.Context, tenantID string) error {
1✔
1712
        if d.cache == nil {
1✔
1713
                return nil
×
1714
        }
×
1715

1716
        return d.cache.SuspendTenant(ctx, tenantID)
1✔
1717
}
1718

1719
func (d *DevAuth) GetTenantDeviceStatus(
1720
        ctx context.Context,
1721
        tenantId,
1722
        deviceId string,
1723
) (*model.Status, error) {
2✔
1724
        if tenantId != "" {
4✔
1725
                ctx = identity.WithContext(ctx, &identity.Identity{
2✔
1726
                        Tenant: tenantId,
2✔
1727
                })
2✔
1728
        }
2✔
1729
        dev, err := d.db.GetDeviceById(ctx, deviceId)
2✔
1730
        switch err {
2✔
1731
        case nil:
1✔
1732
                return &model.Status{Status: dev.Status}, nil
1✔
1733
        case store.ErrDevNotFound:
2✔
1734
                return nil, ErrDeviceNotFound
2✔
1735
        default:
1✔
1736
                return nil, errors.Wrapf(err, "get device %s failed", deviceId)
1✔
1737

1738
        }
1739
}
1740

1741
func (d *DevAuth) updateCheckInTime(
1742
        ctx context.Context,
1743
        deviceId string,
1744
        tenantId string,
1745
        previous *time.Time,
1746
) {
3✔
1747
        var err error
3✔
1748
        defer func() {
6✔
1749
                if err != nil {
3✔
1750
                        log.FromContext(ctx).Errorf(
×
1751
                                "failed to update device check-in time for device %s: %s",
×
1752
                                deviceId, err.Error(),
×
1753
                        )
×
1754
                }
×
1755
        }()
1756
        checkInTime := uto.TimePtr(time.Now().UTC())
3✔
1757
        // in case cache is disabled, use mongo
3✔
1758
        if d.cache == nil {
6✔
1759
                if err = d.db.UpdateDevice(ctx,
3✔
1760
                        deviceId,
3✔
1761
                        model.DeviceUpdate{
3✔
1762
                                CheckInTime: checkInTime,
3✔
1763
                        }); err != nil {
3✔
1764
                        return
×
1765
                }
×
1766
        } else {
1✔
1767
                // get check-in time from cache
1✔
1768
                previous, err = d.cache.GetCheckInTime(ctx, tenantId, deviceId)
1✔
1769
                if err != nil {
1✔
1770
                        return
×
1771
                }
×
1772
                // update check-in time in cache
1773
                err = d.cache.CacheCheckInTime(ctx, checkInTime, tenantId, deviceId)
1✔
1774
                if err != nil {
1✔
1775
                        return
×
1776
                }
×
1777
        }
1778
        // compare data without a time of current and previous check-in time
1779
        // and if it's different trigger reindexing (if enabled)
1780
        // and save check-in time in the database
1781
        if previous == nil ||
3✔
1782
                (previous != nil &&
3✔
1783
                        !previous.Truncate(24*time.Hour).Equal(checkInTime.Truncate(24*time.Hour))) {
6✔
1784
                // trigger reindexing
3✔
1785
                if d.config.EnableReporting {
3✔
1786
                        if err = d.cOrch.SubmitReindexReporting(ctx, deviceId); err != nil {
×
1787
                                err = errors.Wrap(err, "reindex reporting job error")
×
1788
                                return
×
1789
                        }
×
1790
                } else {
3✔
1791
                        // update check-in time in inventory
3✔
1792
                        if err := d.syncCheckInTime(ctx, checkInTime, deviceId, tenantId); err != nil {
3✔
1793
                                log.FromContext(ctx).Errorf(
×
1794
                                        "failed to synchronize device check-in time with inventory: device %s: %s",
×
1795
                                        deviceId, err.Error(),
×
1796
                                )
×
1797
                        }
×
1798
                }
1799
                // dump cached value to database
1800
                if d.cache != nil {
4✔
1801
                        if err = d.db.UpdateDevice(ctx,
1✔
1802
                                deviceId,
1✔
1803
                                model.DeviceUpdate{
1✔
1804
                                        CheckInTime: checkInTime,
1✔
1805
                                }); err != nil {
1✔
1806
                                return
×
1807
                        }
×
1808
                }
1809
        }
1810
}
1811

1812
func (d *DevAuth) syncCheckInTime(
1813
        ctx context.Context,
1814
        checkInTime *time.Time,
1815
        deviceId string,
1816
        tenantId string,
1817
) error {
3✔
1818
        attributes := []model.DeviceAttribute{
3✔
1819
                {
3✔
1820
                        Name:        "check_in_time",
3✔
1821
                        Description: nil,
3✔
1822
                        Value:       checkInTime,
3✔
1823
                        Scope:       InventoryScopeSystem,
3✔
1824
                },
3✔
1825
        }
3✔
1826
        attrJson, err := json.Marshal(attributes)
3✔
1827
        if err != nil {
3✔
1828
                return errors.New("internal error: cannot marshal attributes into json")
×
1829
        }
×
1830
        if err := d.cOrch.SubmitUpdateDeviceInventoryJob(
3✔
1831
                ctx,
3✔
1832
                orchestrator.UpdateDeviceInventoryReq{
3✔
1833
                        RequestId:  requestid.FromContext(ctx),
3✔
1834
                        TenantId:   tenantId,
3✔
1835
                        DeviceId:   deviceId,
3✔
1836
                        Scope:      InventoryScopeSystem,
3✔
1837
                        Attributes: string(attrJson),
3✔
1838
                }); err != nil {
3✔
1839
                return errors.Wrap(err, "failed to start device inventory update job")
×
1840
        }
×
1841
        return nil
3✔
1842
}
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