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

mendersoftware / deviceauth / 728240484

pending completion
728240484

push

gitlab-ci

GitHub
Merge pull request #622 from 0lmi/me-41_device_decommissioning_inconsistency

14 of 21 new or added lines in 2 files covered. (66.67%)

84 existing lines in 2 files now uncovered.

4403 of 5219 relevant lines covered (84.36%)

49.02 hits per line

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

90.39
/devauth/devauth.go
1
// Copyright 2022 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
        "fmt"
21
        "net/http"
22
        "strings"
23
        "time"
24

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

38
        "github.com/mendersoftware/deviceauth/access"
39
        "github.com/mendersoftware/deviceauth/cache"
40
        "github.com/mendersoftware/deviceauth/client/inventory"
41
        "github.com/mendersoftware/deviceauth/client/orchestrator"
42
        "github.com/mendersoftware/deviceauth/client/tenant"
43
        "github.com/mendersoftware/deviceauth/jwt"
44
        "github.com/mendersoftware/deviceauth/model"
45
        "github.com/mendersoftware/deviceauth/store"
46
        "github.com/mendersoftware/deviceauth/store/mongo"
47
        "github.com/mendersoftware/deviceauth/utils"
48
        uto "github.com/mendersoftware/deviceauth/utils/to"
49
)
50

51
const (
52
        MsgErrDevAuthUnauthorized = "dev auth: unauthorized"
53
        MsgErrDevAuthBadRequest   = "dev auth: bad request"
54
)
55

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

64
        ErrInvalidDeviceID  = errors.New("invalid device ID type")
65
        ErrInvalidAuthSetID = errors.New("auth set id is not a valid ID")
66
)
67

68
func IsErrDevAuthUnauthorized(e error) bool {
1✔
69
        return strings.HasPrefix(e.Error(), MsgErrDevAuthUnauthorized)
1✔
70
}
1✔
71

72
func MakeErrDevAuthUnauthorized(e error) error {
13✔
73
        return errors.Wrap(e, MsgErrDevAuthUnauthorized)
13✔
74
}
13✔
75

76
func IsErrDevAuthBadRequest(e error) bool {
1✔
77
        return strings.HasPrefix(e.Error(), MsgErrDevAuthBadRequest)
1✔
78
}
1✔
79

80
func MakeErrDevAuthBadRequest(e error) error {
4✔
81
        return errors.Wrap(e, MsgErrDevAuthBadRequest)
4✔
82
}
4✔
83

84
// helper for obtaining API clients
85
type ApiClientGetter func() apiclient.HttpRunner
86

87
func simpleApiClientGetter() apiclient.HttpRunner {
×
88
        return &apiclient.HttpApi{}
×
89
}
×
90

91
// this device auth service interface
92
//
93
//go:generate ../utils/mockgen.sh
94
type App interface {
95
        HealthCheck(ctx context.Context) error
96
        SubmitAuthRequest(ctx context.Context, r *model.AuthReq) (string, error)
97

98
        GetDevices(
99
                ctx context.Context,
100
                skip,
101
                limit uint,
102
                filter model.DeviceFilter,
103
        ) ([]model.Device, error)
104
        GetDevice(ctx context.Context, dev_id string) (*model.Device, error)
105
        DecommissionDevice(ctx context.Context, dev_id string) error
106
        DeleteDevice(ctx context.Context, dev_id string) error
107
        DeleteAuthSet(ctx context.Context, dev_id string, auth_id string) error
108
        AcceptDeviceAuth(ctx context.Context, dev_id string, auth_id string) error
109
        RejectDeviceAuth(ctx context.Context, dev_id string, auth_id string) error
110
        ResetDeviceAuth(ctx context.Context, dev_id string, auth_id string) error
111
        PreauthorizeDevice(ctx context.Context, req *model.PreAuthReq) (*model.Device, error)
112

113
        RevokeToken(ctx context.Context, tokenID string) error
114
        VerifyToken(ctx context.Context, token string) error
115
        DeleteTokens(ctx context.Context, tenantID, deviceID string) error
116

117
        SetTenantLimit(ctx context.Context, tenant_id string, limit model.Limit) error
118
        DeleteTenantLimit(ctx context.Context, tenant_id string, limit string) error
119

120
        GetLimit(ctx context.Context, name string) (*model.Limit, error)
121
        GetTenantLimit(ctx context.Context, name, tenant_id string) (*model.Limit, error)
122

123
        GetDevCountByStatus(ctx context.Context, status string) (int, error)
124

125
        ProvisionTenant(ctx context.Context, tenant_id string) error
126

127
        GetTenantDeviceStatus(ctx context.Context, tenantId, deviceId string) (*model.Status, error)
128
}
129

130
type DevAuth struct {
131
        db           store.DataStore
132
        invClient    inventory.Client
133
        cOrch        orchestrator.ClientRunner
134
        cTenant      tenant.ClientRunner
135
        jwt          jwt.Handler
136
        clientGetter ApiClientGetter
137
        verifyTenant bool
138
        config       Config
139
        cache        cache.Cache
140
        clock        utils.Clock
141
        checker      access.Checker
142
}
143

144
type Config struct {
145
        // token issuer
146
        Issuer string
147
        // token expiration time
148
        ExpirationTime int64
149
        // Default tenant token to use when the client supplies none. Can be
150
        // empty
151
        DefaultTenantToken string
152
        InventoryAddr      string
153

154
        EnableReporting bool
155
        HaveAddons      bool
156
}
157

158
func NewDevAuth(d store.DataStore, co orchestrator.ClientRunner,
159
        jwt jwt.Handler, config Config) *DevAuth {
291✔
160
        // initialize checker using an empty merge (returns nil on validate)
291✔
161
        checker := access.Merge()
291✔
162
        if config.HaveAddons {
292✔
163
                checker = access.NewAddonChecker()
1✔
164
        }
1✔
165

166
        return &DevAuth{
291✔
167
                db:           d,
291✔
168
                invClient:    inventory.NewClient(config.InventoryAddr, false),
291✔
169
                cOrch:        co,
291✔
170
                jwt:          jwt,
291✔
171
                clientGetter: simpleApiClientGetter,
291✔
172
                verifyTenant: false,
291✔
173
                config:       config,
291✔
174
                clock:        utils.NewClock(),
291✔
175
                checker:      checker,
291✔
176
        }
291✔
177
}
178

179
func (d *DevAuth) HealthCheck(ctx context.Context) error {
12✔
180
        err := d.db.Ping(ctx)
12✔
181
        if err != nil {
14✔
182
                return errors.Wrap(err, "error reaching MongoDB")
2✔
183
        }
2✔
184
        err = d.invClient.CheckHealth(ctx)
10✔
185
        if err != nil {
12✔
186
                return errors.Wrap(err, "Inventory service unhealthy")
2✔
187
        }
2✔
188
        err = d.cOrch.CheckHealth(ctx)
8✔
189
        if err != nil {
10✔
190
                return errors.Wrap(err, "Workflows service unhealthy")
2✔
191
        }
2✔
192
        if d.verifyTenant {
10✔
193
                err = d.cTenant.CheckHealth(ctx)
4✔
194
                if err != nil {
6✔
195
                        return errors.Wrap(err, "Tenantadm service unhealthy")
2✔
196
                }
2✔
197
        }
198
        return nil
4✔
199
}
200

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

242
func (d *DevAuth) getDeviceFromAuthRequest(
243
        ctx context.Context,
244
        r *model.AuthReq,
245
) (*model.Device, error) {
23✔
246
        dev := model.NewDevice("", r.IdData, r.PubKey)
23✔
247

23✔
248
        l := log.FromContext(ctx)
23✔
249

23✔
250
        idDataStruct, idDataSha256, err := parseIdData(r.IdData)
23✔
251
        if err != nil {
23✔
UNCOV
252
                return nil, MakeErrDevAuthBadRequest(err)
×
253
        }
×
254

255
        dev.IdDataStruct = idDataStruct
23✔
256
        dev.IdDataSha256 = idDataSha256
23✔
257

23✔
258
        // record device
23✔
259
        err = d.db.AddDevice(ctx, *dev)
23✔
260
        addDeviceErr := err
23✔
261
        if err != nil && err != store.ErrObjectExists {
25✔
262
                l.Errorf("failed to add/find device: %v", err)
2✔
263
                return nil, err
2✔
264
        }
2✔
265

266
        // either the device was added or it was already present, in any case,
267
        // pull it from DB
268
        dev, err = d.db.GetDeviceByIdentityDataHash(ctx, idDataSha256)
21✔
269
        if err != nil {
23✔
270
                l.Error("failed to find device but could not add either")
2✔
271
                return nil, errors.New("failed to locate device")
2✔
272
        }
2✔
273

274
        idData := identity.FromContext(ctx)
19✔
275
        tenantId := ""
19✔
276
        if idData != nil {
28✔
277
                tenantId = idData.Tenant
9✔
278
        }
9✔
279
        if addDeviceErr != store.ErrObjectExists {
22✔
280
                if err := d.setDeviceIdentity(ctx, dev, tenantId); err != nil {
3✔
UNCOV
281
                        return nil, err
×
282
                }
×
283
        }
284

285
        // check if the device is in the decommissioning state
286
        if dev.Decommissioning {
19✔
UNCOV
287
                l.Warnf("Device %s in the decommissioning state.", dev.Id)
×
288
                return nil, ErrDevAuthUnauthorized
×
289
        }
×
290

291
        return dev, nil
19✔
292
}
293

294
func (d *DevAuth) signToken(ctx context.Context) jwt.SignFunc {
11✔
295
        return func(t *jwt.Token) (string, error) {
22✔
296
                return d.jwt.ToJWT(t)
11✔
297
        }
11✔
298
}
299

300
// tenantWithContext will update `ctx` with tenant related data
301
func tenantWithContext(ctx context.Context, tenantToken string) (context.Context, error) {
13✔
302
        ident, err := identity.ExtractIdentity(tenantToken)
13✔
303
        if err != nil {
17✔
304
                return nil, errors.Wrap(err, "failed to extract identity from tenant token")
4✔
305
        }
4✔
306

307
        // update context to store the identity of the caller
308
        ctx = identity.WithContext(ctx, &ident)
9✔
309

9✔
310
        // setup authorization header so that outgoing requests are done for
9✔
311
        // *this* tenant
9✔
312
        ctx = ctxhttpheader.WithContext(ctx,
9✔
313
                http.Header{
9✔
314
                        "Authorization": []string{fmt.Sprintf("Bearer %s", tenantToken)},
9✔
315
                },
9✔
316
                "Authorization")
9✔
317

9✔
318
        return ctx, nil
9✔
319
}
320

321
func (d *DevAuth) doVerifyTenant(ctx context.Context, token string) (*tenant.Tenant, error) {
23✔
322
        t, err := d.cTenant.VerifyToken(ctx, token)
23✔
323

23✔
324
        if err != nil {
34✔
325
                if tenant.IsErrTokenVerificationFailed(err) {
20✔
326
                        return nil, MakeErrDevAuthUnauthorized(err)
9✔
327
                }
9✔
328

329
                return nil, errors.Wrap(err, "request to verify tenant token failed")
3✔
330
        }
331

332
        return t, nil
13✔
333
}
334

335
func (d *DevAuth) getTenantWithDefault(
336
        ctx context.Context,
337
        tenantToken,
338
        defaultToken string,
339
) (context.Context, *tenant.Tenant, error) {
23✔
340
        l := log.FromContext(ctx)
23✔
341

23✔
342
        if tenantToken == "" && defaultToken == "" {
26✔
343
                return nil, nil, MakeErrDevAuthUnauthorized(errors.New("tenant token missing"))
3✔
344
        }
3✔
345

346
        var chosenToken string
21✔
347
        var t *tenant.Tenant
21✔
348
        var err error
21✔
349

21✔
350
        // try the provided token
21✔
351
        // but continue on errors and maybe try the default token
21✔
352
        if tenantToken != "" {
38✔
353
                t, err = d.doVerifyTenant(ctx, tenantToken)
17✔
354

17✔
355
                if err == nil {
28✔
356
                        chosenToken = tenantToken
11✔
357
                } else {
18✔
358
                        l.Errorf("Failed to verify supplied tenant token: %s", err.Error())
7✔
359
                }
7✔
360
        }
361

362
        // if we still haven't selected a tenant - the token didn't work
363
        // try the default one
364
        if t == nil && defaultToken != "" {
27✔
365
                t, err = d.doVerifyTenant(ctx, defaultToken)
6✔
366

6✔
367
                if err == nil {
8✔
368
                        chosenToken = defaultToken
2✔
369
                }
2✔
370
                if err != nil {
10✔
371
                        l.Errorf("Failed to verify default tenant token: %s", err.Error())
4✔
372
                }
4✔
373
        }
374

375
        // none of the tokens worked
376
        if err != nil {
30✔
377
                if tenant.IsErrTokenVerificationFailed(err) {
9✔
UNCOV
378
                        return ctx, nil, MakeErrDevAuthUnauthorized(err)
×
379
                }
×
380
                return ctx, nil, err
9✔
381
        }
382

383
        // we do have a working token/valid tenant
384
        tCtx, err := tenantWithContext(ctx, chosenToken)
13✔
385
        if err != nil {
17✔
386
                l.Errorf("failed to setup tenant context: %v", err)
4✔
387
                return nil, nil, ErrDevAuthUnauthorized
4✔
388
        }
4✔
389

390
        return tCtx, t, nil
9✔
391
}
392

393
func (d *DevAuth) SubmitAuthRequest(ctx context.Context, r *model.AuthReq) (string, error) {
53✔
394
        l := log.FromContext(ctx)
53✔
395

53✔
396
        var tenant *tenant.Tenant
53✔
397
        var err error
53✔
398

53✔
399
        if d.verifyTenant {
76✔
400
                ctx, tenant, err = d.getTenantWithDefault(ctx, r.TenantToken, d.config.DefaultTenantToken)
23✔
401
                if err != nil {
38✔
402
                        return "", err
15✔
403
                }
15✔
404
        } else {
31✔
405
                // ignore identity data when tenant verification is off
31✔
406
                // it's possible that the device will provide old auth token or old tenant token
31✔
407
                // in the authorization header;
31✔
408
                // in that case we need to wipe identity data from the context
31✔
409
                ctx = identity.WithContext(ctx, nil)
31✔
410
        }
31✔
411

412
        // first, try to handle preauthorization
413
        authSet, err := d.processPreAuthRequest(ctx, r)
39✔
414
        if err != nil {
51✔
415
                return "", err
12✔
416
        }
12✔
417

418
        // if not a preauth request, process with regular auth request handling
419
        if authSet == nil {
50✔
420
                authSet, err = d.processAuthRequest(ctx, r)
23✔
421
                if err != nil {
29✔
422
                        return "", err
6✔
423
                }
6✔
424
        }
425

426
        // request was already present in DB, check its status
427
        if authSet.Status == model.DevStatusAccepted {
32✔
428
                jti := oid.FromString(authSet.Id)
11✔
429
                if jti.String() == "" {
11✔
UNCOV
430
                        return "", ErrInvalidAuthSetID
×
431
                }
×
432
                sub := oid.FromString(authSet.DeviceId)
11✔
433
                if sub.String() == "" {
11✔
UNCOV
434
                        return "", ErrInvalidDeviceID
×
435
                }
×
436
                now := time.Now()
11✔
437
                token := &jwt.Token{Claims: jwt.Claims{
11✔
438
                        ID:      jti,
11✔
439
                        Subject: sub,
11✔
440
                        Issuer:  d.config.Issuer,
11✔
441
                        ExpiresAt: jwt.Time{
11✔
442
                                Time: now.Add(time.Second *
11✔
443
                                        time.Duration(d.config.ExpirationTime)),
11✔
444
                        },
11✔
445
                        IssuedAt: jwt.Time{Time: now},
11✔
446
                        Device:   true,
11✔
447
                }}
11✔
448

11✔
449
                if d.verifyTenant {
16✔
450
                        token.Claims.Tenant = tenant.ID
5✔
451
                        token.Claims.Plan = tenant.Plan
5✔
452
                        token.Claims.Addons = tenant.Addons
5✔
453
                        token.Claims.Trial = tenant.Trial
5✔
454
                } else {
12✔
455
                        token.Claims.Plan = plan.PlanEnterprise
7✔
456
                        token.Addons = addons.AllAddonsEnabled
7✔
457
                }
7✔
458

459
                // sign and encode as JWT
460
                raw, err := token.MarshalJWT(d.signToken(ctx))
11✔
461
                if err != nil {
11✔
UNCOV
462
                        return "", errors.Wrap(err, "generate token error")
×
463
                }
×
464

465
                if err := d.db.AddToken(ctx, token); err != nil {
11✔
UNCOV
466
                        return "", errors.Wrap(err, "add token error")
×
467
                }
×
468

469
                l.Infof("Token %s assigned to device %s",
11✔
470
                        token.Claims.ID, token.Claims.Subject)
11✔
471
                return string(raw), nil
11✔
472
        }
473

474
        // no token, return device unauthorized
475
        return "", ErrDevAuthUnauthorized
11✔
476

477
}
478

479
func (d *DevAuth) processPreAuthRequest(
480
        ctx context.Context,
481
        r *model.AuthReq,
482
) (*model.AuthSet, error) {
39✔
483
        var deviceAlreadyAccepted bool
39✔
484

39✔
485
        _, idDataSha256, err := parseIdData(r.IdData)
39✔
486
        if err != nil {
41✔
487
                return nil, MakeErrDevAuthBadRequest(err)
2✔
488
        }
2✔
489

490
        // authset exists?
491
        aset, err := d.db.GetAuthSetByIdDataHashKey(ctx, idDataSha256, r.PubKey)
37✔
492
        switch err {
37✔
493
        case nil:
33✔
494
                break
33✔
495
        case store.ErrAuthSetNotFound:
3✔
496
                return nil, nil
3✔
497
        default:
2✔
498
                return nil, errors.Wrap(err, "failed to fetch auth set")
2✔
499
        }
500

501
        // if authset status is not 'preauthorized', nothing to do
502
        if aset.Status != model.DevStatusPreauth {
54✔
503
                return nil, nil
21✔
504
        }
21✔
505

506
        // check the device status
507
        // if the device status is accepted then do not trigger provisioning workflow
508
        // this needs to be checked before changing authentication set status
509
        dev, err := d.db.GetDeviceById(ctx, aset.DeviceId)
12✔
510
        if err != nil {
14✔
511
                return nil, err
2✔
512
        }
2✔
513

514
        currentStatus := dev.Status
10✔
515
        if dev.Status == model.DevStatusAccepted {
12✔
516
                deviceAlreadyAccepted = true
2✔
517
        }
2✔
518

519
        // auth set is ok for auto-accepting, check device limit
520
        allow, err := d.canAcceptDevice(ctx)
10✔
521
        if err != nil {
12✔
522
                return nil, err
2✔
523
        }
2✔
524

525
        if !allow {
10✔
526
                return nil, ErrMaxDeviceCountReached
2✔
527
        }
2✔
528

529
        update := model.AuthSetUpdate{
6✔
530
                Status: model.DevStatusAccepted,
6✔
531
        }
6✔
532
        // persist the 'accepted' status in both auth set, and device
6✔
533
        if err := d.db.UpdateAuthSetById(ctx, aset.Id, update); err != nil {
6✔
UNCOV
534
                return nil, errors.Wrap(err, "failed to update auth set status")
×
535
        }
×
536

537
        if err := d.updateDeviceStatus(
6✔
538
                ctx,
6✔
539
                aset.DeviceId,
6✔
540
                model.DevStatusAccepted,
6✔
541
                currentStatus,
6✔
542
        ); err != nil {
6✔
UNCOV
543
                return nil, err
×
544
        }
×
545

546
        aset.Status = model.DevStatusAccepted
6✔
547
        dev.Status = model.DevStatusAccepted
6✔
548
        dev.AuthSets = append(dev.AuthSets, *aset)
6✔
549

6✔
550
        if !deviceAlreadyAccepted {
10✔
551
                reqId := requestid.FromContext(ctx)
4✔
552
                var tenantID string
4✔
553
                if idty := identity.FromContext(ctx); idty != nil {
4✔
UNCOV
554
                        tenantID = idty.Tenant
×
555
                }
×
556

557
                // submit device accepted job
558
                if err := d.cOrch.SubmitProvisionDeviceJob(
4✔
559
                        ctx,
4✔
560
                        orchestrator.ProvisionDeviceReq{
4✔
561
                                RequestId: reqId,
4✔
562
                                DeviceID:  aset.DeviceId,
4✔
563
                                TenantID:  tenantID,
4✔
564
                                Device:    dev,
4✔
565
                                Status:    dev.Status,
4✔
566
                        }); err != nil {
6✔
567
                        return nil, errors.Wrap(err, "submit device provisioning job error")
2✔
568
                }
2✔
569
        }
570
        return aset, nil
4✔
571
}
572

573
func (d *DevAuth) updateDeviceStatus(
574
        ctx context.Context,
575
        devId,
576
        status string,
577
        currentStatus string,
578
) error {
65✔
579
        newStatus, err := d.db.GetDeviceStatus(ctx, devId)
65✔
580
        if currentStatus == newStatus {
70✔
581
                return nil
5✔
582
        }
5✔
583
        if status == "" {
110✔
584
                switch err {
49✔
585
                case nil:
45✔
586
                        status = newStatus
45✔
587
                case store.ErrAuthSetNotFound:
3✔
588
                        status = model.DevStatusNoAuth
3✔
589
                default:
2✔
590
                        return errors.Wrap(err, "Cannot determine device status")
2✔
591
                }
592
        }
593

594
        // submit device status change job
595
        dev, err := d.db.GetDeviceById(ctx, devId)
59✔
596
        if err != nil {
59✔
UNCOV
597
                return errors.Wrap(err, "db get device by id error")
×
598
        }
×
599

600
        tenantId := ""
59✔
601
        idData := identity.FromContext(ctx)
59✔
602
        if idData != nil {
72✔
603
                tenantId = idData.Tenant
13✔
604
        }
13✔
605
        req := orchestrator.UpdateDeviceStatusReq{
59✔
606
                RequestId: requestid.FromContext(ctx),
59✔
607
                Devices: []model.DeviceInventoryUpdate{{
59✔
608
                        Id:       dev.Id,
59✔
609
                        Revision: dev.Revision + 1,
59✔
610
                }},
59✔
611
                TenantId: tenantId,
59✔
612
                Status:   status,
59✔
613
        }
59✔
614
        if err := d.cOrch.SubmitUpdateDeviceStatusJob(ctx, req); err != nil {
60✔
615
                return errors.Wrap(err, "update device status job error")
1✔
616
        }
1✔
617

618
        if err := d.db.UpdateDevice(ctx,
59✔
619
                devId,
59✔
620
                model.DeviceUpdate{
59✔
621
                        Status:    status,
59✔
622
                        UpdatedTs: uto.TimePtr(time.Now().UTC()),
59✔
623
                }); err != nil {
61✔
624
                return errors.Wrap(err, "failed to update device status")
2✔
625
        }
2✔
626

627
        if d.config.EnableReporting {
61✔
628
                if err := d.cOrch.SubmitReindexReporting(ctx, devId); err != nil {
6✔
629
                        return errors.Wrap(err, "reindex reporting job error")
2✔
630
                }
2✔
631
        }
632

633
        return nil
55✔
634
}
635

636
// processAuthRequest will process incoming auth request and record authentication
637
// data information it contains. Returns a tupe (auth set, error). If no errors were
638
// present, model.AuthSet.Status will indicate the status of device admission
639
func (d *DevAuth) processAuthRequest(
640
        ctx context.Context,
641
        r *model.AuthReq,
642
) (*model.AuthSet, error) {
23✔
643

23✔
644
        l := log.FromContext(ctx)
23✔
645

23✔
646
        // get device associated with given authorization request
23✔
647
        dev, err := d.getDeviceFromAuthRequest(ctx, r)
23✔
648
        if err != nil {
27✔
649
                return nil, err
4✔
650
        }
4✔
651

652
        idDataStruct, idDataSha256, err := parseIdData(r.IdData)
19✔
653
        if err != nil {
19✔
UNCOV
654
                return nil, MakeErrDevAuthBadRequest(err)
×
655
        }
×
656

657
        areq := &model.AuthSet{
19✔
658
                Id:           oid.NewUUIDv4().String(),
19✔
659
                IdData:       r.IdData,
19✔
660
                IdDataStruct: idDataStruct,
19✔
661
                IdDataSha256: idDataSha256,
19✔
662
                PubKey:       r.PubKey,
19✔
663
                DeviceId:     dev.Id,
19✔
664
                Status:       model.DevStatusPending,
19✔
665
                Timestamp:    uto.TimePtr(time.Now()),
19✔
666
        }
19✔
667

19✔
668
        // record authentication request
19✔
669
        err = d.db.AddAuthSet(ctx, *areq)
19✔
670
        if err != nil && err != store.ErrObjectExists {
19✔
UNCOV
671
                return nil, err
×
672
        }
×
673

674
        // update the device status
675
        if err := d.updateDeviceStatus(ctx, dev.Id, "", dev.Status); err != nil {
19✔
UNCOV
676
                return nil, err
×
677
        }
×
678

679
        // either the request was added or it was already present in the DB, get
680
        // it now
681
        areq, err = d.db.GetAuthSetByIdDataHashKey(ctx, idDataSha256, r.PubKey)
19✔
682
        if err != nil {
21✔
683
                l.Error("failed to find device auth set but could not add one either")
2✔
684
                return nil, errors.New("failed to locate device auth set")
2✔
685
        }
2✔
686

687
        return areq, nil
17✔
688
}
689

690
func (d *DevAuth) GetDevices(
691
        ctx context.Context,
692
        skip,
693
        limit uint,
694
        filter model.DeviceFilter,
695
) ([]model.Device, error) {
1✔
696
        devs, err := d.db.GetDevices(ctx, skip, limit, filter)
1✔
697
        if err != nil {
1✔
UNCOV
698
                return nil, errors.Wrap(err, "failed to list devices")
×
699
        }
×
700

701
        for i := range devs {
2✔
702
                devs[i].AuthSets, err = d.db.GetAuthSetsForDevice(ctx, devs[i].Id)
1✔
703
                if err != nil && err != store.ErrAuthSetNotFound {
1✔
UNCOV
704
                        return nil, errors.Wrap(err, "db get auth sets error")
×
705
                }
×
706
        }
707
        return devs, err
1✔
708
}
709

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

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

729
// DecommissionDevice deletes device and all its tokens
730
func (d *DevAuth) DecommissionDevice(ctx context.Context, devID string) error {
17✔
731

17✔
732
        l := log.FromContext(ctx)
17✔
733

17✔
734
        l.Warnf("Decommission device with id: %s", devID)
17✔
735

17✔
736
        err := d.cacheDeleteToken(ctx, devID)
17✔
737
        if err != nil {
23✔
738
                return errors.Wrapf(err, "failed to delete token for %s from cache", devID)
6✔
739
        }
6✔
740

741
        // set decommissioning flag on the device
742
        updev := model.DeviceUpdate{
11✔
743
                Decommissioning: uto.BoolPtr(true),
11✔
744
        }
11✔
745
        if err := d.db.UpdateDevice(
11✔
746
                ctx, devID, updev,
11✔
747
        ); err != nil {
16✔
748
                return err
5✔
749
        }
5✔
750

751
        reqId := requestid.FromContext(ctx)
7✔
752

7✔
753
        tenantID := ""
7✔
754
        idData := identity.FromContext(ctx)
7✔
755
        if idData != nil {
9✔
756
                tenantID = idData.Tenant
2✔
757
        }
2✔
758

759
        // submit device decommissioning job
760
        if err := d.cOrch.SubmitDeviceDecommisioningJob(
7✔
761
                ctx,
7✔
762
                orchestrator.DecommissioningReq{
7✔
763
                        DeviceId:  devID,
7✔
764
                        RequestId: reqId,
7✔
765
                        TenantID:  tenantID,
7✔
766
                }); err != nil {
10✔
767
                return errors.Wrap(err, "submit device decommissioning job error")
3✔
768
        }
3✔
769

770
        return err
5✔
771
}
772

773
// Delete a device and its tokens from deviceauth db
774
func (d *DevAuth) DeleteDevice(ctx context.Context, devID string) error {
13✔
775
        // delete device authorization sets
13✔
776
        if err := d.db.DeleteAuthSetsForDevice(ctx, devID); err != nil &&
13✔
777
                err != store.ErrAuthSetNotFound {
15✔
778
                return errors.Wrap(err, "db delete device authorization sets error")
2✔
779
        }
2✔
780

781
        devOID := oid.FromString(devID)
11✔
782
        // If the devID is not a valid string, there's no token.
11✔
783
        if devOID.String() == "" {
13✔
784
                return ErrInvalidDeviceID
2✔
785
        }
2✔
786
        // delete device tokens
787
        if err := d.db.DeleteTokenByDevId(
9✔
788
                ctx, devOID,
9✔
789
        ); err != nil && err != store.ErrTokenNotFound {
11✔
790
                return errors.Wrap(err, "db delete device tokens error")
2✔
791
        }
2✔
792

793
        // delete device
794
        return d.db.DeleteDevice(ctx, devID)
7✔
795
}
796

797
// Deletes device authentication set, and optionally the device.
798
func (d *DevAuth) DeleteAuthSet(ctx context.Context, devID string, authId string) error {
39✔
799

39✔
800
        l := log.FromContext(ctx)
39✔
801

39✔
802
        l.Warnf("Delete authentication set with id: "+
39✔
803
                "%s for the device with id: %s",
39✔
804
                authId, devID)
39✔
805

39✔
806
        err := d.cacheDeleteToken(ctx, devID)
39✔
807
        if err != nil {
43✔
808
                return errors.Wrapf(err, "failed to delete token for %s from cache", devID)
4✔
809
        }
4✔
810

811
        // retrieve device authentication set to check its status
812
        authSet, err := d.db.GetAuthSetById(ctx, authId)
35✔
813
        if err != nil {
40✔
814
                if err == store.ErrAuthSetNotFound {
8✔
815
                        return err
3✔
816
                }
3✔
817
                return errors.Wrap(err, "db get auth set error")
2✔
818
        }
819

820
        // delete device authorization set
821
        if err := d.db.DeleteAuthSetForDevice(ctx, devID, authId); err != nil {
33✔
822
                return err
2✔
823
        }
2✔
824

825
        // if the device authentication set is accepted delete device tokens
826
        if authSet.Status == model.DevStatusAccepted {
31✔
827
                // If string is not a valid UUID there's no token.
2✔
828
                devOID := oid.FromString(devID)
2✔
829
                if err := d.db.DeleteTokenByDevId(
2✔
830
                        ctx, devOID,
2✔
831
                ); err != nil && err != store.ErrTokenNotFound {
4✔
832
                        return errors.Wrap(err,
2✔
833
                                "db delete device tokens error")
2✔
834
                }
2✔
835
        } else if authSet.Status == model.DevStatusPreauth {
33✔
836
                // if the auth set status is 'preauthorized', the device is deleted from
6✔
837
                // deviceauth. We cannot start the decommission_device workflow because
6✔
838
                // we don't provision devices until they are accepted. Still, we need to
6✔
839
                // remove the device from the inventory service because we index pre-authorized
6✔
840
                // devices for consumption via filtering APIs. To trigger the deletion
6✔
841
                // from the inventory service, we start the status update workflow with the
6✔
842
                // special value "decommissioned", which will cause the deletion of the
6✔
843
                // device from the inventory service's database
6✔
844
                tenantID := ""
6✔
845
                idData := identity.FromContext(ctx)
6✔
846
                if idData != nil {
6✔
UNCOV
847
                        tenantID = idData.Tenant
×
UNCOV
848
                }
×
849
                req := orchestrator.UpdateDeviceStatusReq{
6✔
850
                        RequestId: requestid.FromContext(ctx),
6✔
851
                        Devices:   []model.DeviceInventoryUpdate{{Id: devID}},
6✔
852
                        TenantId:  tenantID,
6✔
853
                        Status:    "decommissioned",
6✔
854
                }
6✔
855
                if err = d.cOrch.SubmitUpdateDeviceStatusJob(ctx, req); err != nil {
8✔
856
                        return errors.Wrap(err, "update device status job error")
2✔
857
                }
2✔
858

859
                // delete device
860
                if err := d.db.DeleteDevice(ctx, devID); err != nil {
6✔
861
                        return err
2✔
862
                }
2✔
863

864
                if d.config.EnableReporting {
4✔
865
                        if err := d.cOrch.SubmitReindexReporting(ctx, devID); err != nil {
4✔
866
                                return errors.Wrap(err, "reindex reporting job error")
2✔
867
                        }
2✔
868
                }
869

UNCOV
870
                return nil
×
871
        }
872

873
        return d.updateDeviceStatus(ctx, devID, "", authSet.Status)
21✔
874
}
875

876
func (d *DevAuth) AcceptDeviceAuth(ctx context.Context, device_id string, auth_id string) error {
31✔
877
        l := log.FromContext(ctx)
31✔
878

31✔
879
        aset, err := d.db.GetAuthSetById(ctx, auth_id)
31✔
880
        if err != nil {
34✔
881
                if err == store.ErrAuthSetNotFound {
6✔
882
                        return err
3✔
883
                }
3✔
UNCOV
884
                return errors.Wrap(err, "db get auth set error")
×
885
        }
886

887
        // device authentication set already accepted, nothing to do here
888
        if aset.Status == model.DevStatusAccepted {
31✔
889
                l.Debugf("Device %s already accepted", device_id)
2✔
890
                return nil
2✔
891
        } else if aset.Status != model.DevStatusRejected && aset.Status != model.DevStatusPending {
31✔
892
                // device authentication set can be accepted only from 'pending' or 'rejected' statuses
2✔
893
                return ErrDevAuthBadRequest
2✔
894
        }
2✔
895

896
        // check the device status
897
        // if the device status is accepted then do not trigger provisioning workflow
898
        // this needs to be checked before changing authentication set status
899
        dev, err := d.db.GetDeviceById(ctx, device_id)
25✔
900
        if err != nil {
27✔
901
                return err
2✔
902
        }
2✔
903

904
        // possible race, consider accept-count-unaccept pattern if that's problematic
905
        allow, err := d.canAcceptDevice(ctx)
23✔
906
        if err != nil {
27✔
907
                return err
4✔
908
        }
4✔
909

910
        if !allow {
24✔
911
                return ErrMaxDeviceCountReached
5✔
912
        }
5✔
913

914
        if err := d.setAuthSetStatus(ctx, device_id, auth_id, model.DevStatusAccepted); err != nil {
20✔
915
                return err
5✔
916
        }
5✔
917

918
        if dev.Status != model.DevStatusPending {
14✔
919
                // Device already exist in all services
3✔
920
                // We're done...
3✔
921
                return nil
3✔
922
        }
3✔
923

924
        dev.Status = model.DevStatusAccepted
9✔
925
        aset.Status = model.DevStatusAccepted
9✔
926
        dev.AuthSets = []model.AuthSet{*aset}
9✔
927

9✔
928
        reqId := requestid.FromContext(ctx)
9✔
929

9✔
930
        var tenantID string
9✔
931
        if idty := identity.FromContext(ctx); idty != nil {
10✔
932
                tenantID = idty.Tenant
1✔
933
        }
1✔
934

935
        // submit device accepted job
936
        if err := d.cOrch.SubmitProvisionDeviceJob(
9✔
937
                ctx,
9✔
938
                orchestrator.ProvisionDeviceReq{
9✔
939
                        RequestId: reqId,
9✔
940
                        DeviceID:  aset.DeviceId,
9✔
941
                        TenantID:  tenantID,
9✔
942
                        Device:    dev,
9✔
943
                        Status:    dev.Status,
9✔
944
                }); err != nil {
11✔
945
                return errors.Wrap(err, "submit device provisioning job error")
2✔
946
        }
2✔
947

948
        return nil
7✔
949
}
950

951
func (d *DevAuth) setAuthSetStatus(
952
        ctx context.Context,
953
        deviceID string,
954
        authID string,
955
        status string,
956
) error {
29✔
957
        aset, err := d.db.GetAuthSetById(ctx, authID)
29✔
958
        if err != nil {
29✔
UNCOV
959
                if err == store.ErrAuthSetNotFound {
×
UNCOV
960
                        return err
×
UNCOV
961
                }
×
UNCOV
962
                return errors.Wrap(err, "db get auth set error")
×
963
        }
964

965
        if aset.DeviceId != deviceID {
29✔
966
                return ErrDevIdAuthIdMismatch
×
967
        }
×
968

969
        if aset.Status == status {
29✔
UNCOV
970
                return nil
×
UNCOV
971
        }
×
972

973
        currentStatus := aset.Status
29✔
974

29✔
975
        if aset.Status == model.DevStatusAccepted &&
29✔
976
                (status == model.DevStatusRejected || status == model.DevStatusPending) {
42✔
977
                deviceOID := oid.FromString(aset.DeviceId)
13✔
978
                // delete device token
13✔
979
                err := d.db.DeleteTokenByDevId(ctx, deviceOID)
13✔
980
                if err != nil && err != store.ErrTokenNotFound {
17✔
981
                        return errors.Wrap(err, "db delete device token error")
4✔
982
                }
4✔
983
        }
984

985
        // if accepting an auth set
986
        if status == model.DevStatusAccepted {
40✔
987
                // reject all accepted auth sets for this device first
15✔
988
                if err := d.db.UpdateAuthSet(ctx,
15✔
989
                        bson.M{
15✔
990
                                model.AuthSetKeyDeviceId: deviceID,
15✔
991
                                "$or": []bson.M{
15✔
992
                                        {model.AuthSetKeyStatus: model.DevStatusAccepted},
15✔
993
                                        {model.AuthSetKeyStatus: model.DevStatusPreauth},
15✔
994
                                },
15✔
995
                        },
15✔
996
                        model.AuthSetUpdate{
15✔
997
                                Status: model.DevStatusRejected,
15✔
998
                        }); err != nil && err != store.ErrAuthSetNotFound {
17✔
999
                        return errors.Wrap(err, "failed to reject auth sets")
2✔
1000
                }
2✔
1001
        }
1002

1003
        if err := d.db.UpdateAuthSetById(ctx, aset.Id, model.AuthSetUpdate{
23✔
1004
                Status: status,
23✔
1005
        }); err != nil {
25✔
1006
                return errors.Wrap(err, "db update device auth set error")
2✔
1007
        }
2✔
1008

1009
        if status == model.DevStatusAccepted {
32✔
1010
                return d.updateDeviceStatus(ctx, deviceID, status, currentStatus)
11✔
1011
        }
11✔
1012
        return d.updateDeviceStatus(ctx, deviceID, "", currentStatus)
11✔
1013
}
1014

1015
func (d *DevAuth) RejectDeviceAuth(ctx context.Context, device_id string, auth_id string) error {
17✔
1016
        aset, err := d.db.GetAuthSetById(ctx, auth_id)
17✔
1017
        if err != nil {
20✔
1018
                if err == store.ErrAuthSetNotFound {
4✔
1019
                        return err
1✔
1020
                }
1✔
1021
                return errors.Wrap(err, "db get auth set error")
2✔
1022
        } else if aset.Status != model.DevStatusPending && aset.Status != model.DevStatusAccepted {
17✔
1023
                // device authentication set can be rejected only from 'accepted' or 'pending' statuses
2✔
1024
                return ErrDevAuthBadRequest
2✔
1025
        }
2✔
1026

1027
        err = d.cacheDeleteToken(ctx, device_id)
13✔
1028
        if err != nil {
17✔
1029
                return errors.Wrapf(err, "failed to delete token for %s from cache", device_id)
4✔
1030
        }
4✔
1031

1032
        return d.setAuthSetStatus(ctx, device_id, auth_id, model.DevStatusRejected)
9✔
1033
}
1034

1035
func (d *DevAuth) ResetDeviceAuth(ctx context.Context, device_id string, auth_id string) error {
9✔
1036
        aset, err := d.db.GetAuthSetById(ctx, auth_id)
9✔
1037
        if err != nil {
11✔
1038
                if err == store.ErrAuthSetNotFound {
2✔
UNCOV
1039
                        return err
×
UNCOV
1040
                }
×
1041
                return errors.Wrap(err, "db get auth set error")
2✔
1042
        } else if aset.Status == model.DevStatusPreauth {
7✔
UNCOV
1043
                // preauthorized auth set should not go into pending state
×
UNCOV
1044
                return ErrDevAuthBadRequest
×
1045
        }
×
1046
        return d.setAuthSetStatus(ctx, device_id, auth_id, model.DevStatusPending)
7✔
1047
}
1048

1049
func parseIdData(idData string) (map[string]interface{}, []byte, error) {
99✔
1050
        var idDataStruct map[string]interface{}
99✔
1051
        var idDataSha256 []byte
99✔
1052

99✔
1053
        err := json.Unmarshal([]byte(idData), &idDataStruct)
99✔
1054
        if err != nil {
103✔
1055
                return idDataStruct, idDataSha256, errors.Wrapf(
4✔
1056
                        err,
4✔
1057
                        "failed to parse identity data: %s",
4✔
1058
                        idData,
4✔
1059
                )
4✔
1060
        }
4✔
1061

1062
        hash := sha256.New()
95✔
1063
        _, _ = hash.Write([]byte(idData))
95✔
1064
        idDataSha256 = hash.Sum(nil)
95✔
1065

95✔
1066
        return idDataStruct, idDataSha256, nil
95✔
1067
}
1068

1069
func (d *DevAuth) PreauthorizeDevice(
1070
        ctx context.Context,
1071
        req *model.PreAuthReq,
1072
) (*model.Device, error) {
14✔
1073
        // try add device, if a device with the given id_data exists -
14✔
1074
        // the unique index on id_data will prevent it (conflict)
14✔
1075
        // this is the only safeguard against id data conflict - we won't try to handle it
14✔
1076
        // additionally on inserting the auth set (can't add an id data index on auth set - would
14✔
1077
        // prevent key rotation)
14✔
1078

14✔
1079
        // FIXME: tenant_token is "" on purpose, will be removed
14✔
1080

14✔
1081
        l := log.FromContext(ctx)
14✔
1082

14✔
1083
        dev := model.NewDevice(req.DeviceId, req.IdData, req.PubKey)
14✔
1084
        dev.Status = model.DevStatusPreauth
14✔
1085

14✔
1086
        idDataStruct, idDataSha256, err := parseIdData(req.IdData)
14✔
1087
        if err != nil {
16✔
1088
                return nil, MakeErrDevAuthBadRequest(err)
2✔
1089
        }
2✔
1090

1091
        dev.IdDataStruct = idDataStruct
12✔
1092
        dev.IdDataSha256 = idDataSha256
12✔
1093

12✔
1094
        err = d.db.AddDevice(ctx, *dev)
12✔
1095
        switch err {
12✔
1096
        case nil:
8✔
1097
                break
8✔
1098
        case store.ErrObjectExists:
2✔
1099
                dev, err = d.db.GetDeviceByIdentityDataHash(ctx, idDataSha256)
2✔
1100
                if err != nil {
2✔
UNCOV
1101
                        l.Error("failed to find device but could not preauthorize either")
×
UNCOV
1102
                        return nil, errors.New("failed to preauthorize device")
×
UNCOV
1103
                }
×
1104
                return dev, ErrDeviceExists
2✔
1105
        default:
2✔
1106
                return nil, errors.Wrap(err, "failed to add device")
2✔
1107
        }
1108

1109
        tenantId := ""
8✔
1110
        idData := identity.FromContext(ctx)
8✔
1111
        if idData != nil {
8✔
UNCOV
1112
                tenantId = idData.Tenant
×
UNCOV
1113
        }
×
1114

1115
        wfReq := orchestrator.UpdateDeviceStatusReq{
8✔
1116
                RequestId: requestid.FromContext(ctx),
8✔
1117
                Devices: []model.DeviceInventoryUpdate{{
8✔
1118
                        Id:       dev.Id,
8✔
1119
                        Revision: dev.Revision,
8✔
1120
                }},
8✔
1121
                TenantId: tenantId,
8✔
1122
                Status:   dev.Status,
8✔
1123
        }
8✔
1124
        if err = d.cOrch.SubmitUpdateDeviceStatusJob(ctx, wfReq); err != nil {
8✔
UNCOV
1125
                return nil, errors.Wrap(err, "update device status job error")
×
UNCOV
1126
        }
×
1127

1128
        // record authentication request
1129
        authset := model.AuthSet{
8✔
1130
                Id:           req.AuthSetId,
8✔
1131
                IdData:       req.IdData,
8✔
1132
                IdDataStruct: idDataStruct,
8✔
1133
                IdDataSha256: idDataSha256,
8✔
1134
                PubKey:       req.PubKey,
8✔
1135
                DeviceId:     req.DeviceId,
8✔
1136
                Status:       model.DevStatusPreauth,
8✔
1137
                Timestamp:    uto.TimePtr(time.Now()),
8✔
1138
        }
8✔
1139

8✔
1140
        err = d.db.AddAuthSet(ctx, authset)
8✔
1141
        switch err {
8✔
1142
        case nil:
4✔
1143
                if err := d.setDeviceIdentity(ctx, dev, tenantId); err != nil {
4✔
UNCOV
1144
                        return nil, err
×
UNCOV
1145
                }
×
1146
                return nil, nil
4✔
1147
        case store.ErrObjectExists:
2✔
1148
                dev, err = d.db.GetDeviceByIdentityDataHash(ctx, idDataSha256)
2✔
1149
                if err != nil {
2✔
1150
                        l.Error("failed to find device but could not preauthorize either")
×
1151
                        return nil, errors.New("failed to preauthorize device")
×
UNCOV
1152
                }
×
1153
                return dev, ErrDeviceExists
2✔
1154
        default:
2✔
1155
                return nil, errors.Wrap(err, "failed to add auth set")
2✔
1156
        }
1157
}
1158

1159
func (d *DevAuth) RevokeToken(ctx context.Context, tokenID string) error {
11✔
1160
        l := log.FromContext(ctx)
11✔
1161
        tokenOID := oid.FromString(tokenID)
11✔
1162

11✔
1163
        var token *jwt.Token
11✔
1164
        token, err := d.db.GetToken(ctx, tokenOID)
11✔
1165
        if err != nil {
13✔
1166
                return err
2✔
1167
        }
2✔
1168

1169
        l.Warnf("Revoke token with jti: %s", tokenID)
9✔
1170
        err = d.db.DeleteToken(ctx, tokenOID)
9✔
1171

9✔
1172
        if err == nil && d.cache != nil {
13✔
1173
                err = d.cacheDeleteToken(ctx, token.Claims.Subject.String())
4✔
1174
                err = errors.Wrapf(
4✔
1175
                        err,
4✔
1176
                        "failed to delete token for %s from cache",
4✔
1177
                        token.Claims.Subject.String(),
4✔
1178
                )
4✔
1179
        }
4✔
1180
        return err
9✔
1181
}
1182

1183
func verifyTenantClaim(ctx context.Context, verifyTenant bool, tenant string) error {
27✔
1184

27✔
1185
        l := log.FromContext(ctx)
27✔
1186

27✔
1187
        if verifyTenant {
48✔
1188
                if tenant == "" {
21✔
UNCOV
1189
                        l.Errorf("No tenant claim in the token")
×
UNCOV
1190
                        return jwt.ErrTokenInvalid
×
UNCOV
1191
                }
×
1192
        } else if tenant != "" {
7✔
UNCOV
1193
                l.Errorf("Unexpected tenant claim: %s in the token", tenant)
×
UNCOV
1194
                return jwt.ErrTokenInvalid
×
1195
        }
×
1196

1197
        return nil
27✔
1198
}
1199

1200
func (d *DevAuth) VerifyToken(ctx context.Context, raw string) error {
33✔
1201

33✔
1202
        l := log.FromContext(ctx)
33✔
1203

33✔
1204
        token := &jwt.Token{}
33✔
1205

33✔
1206
        err := token.UnmarshalJWT([]byte(raw), d.jwt.FromJWT)
33✔
1207
        jti := token.Claims.ID
33✔
1208
        if err != nil {
38✔
1209
                if err == jwt.ErrTokenExpired && jti.String() != "" {
7✔
1210
                        l.Errorf("Token %s expired: %v", jti, err)
2✔
1211

2✔
1212
                        err := d.db.DeleteToken(ctx, jti)
2✔
1213
                        if err == store.ErrTokenNotFound {
2✔
UNCOV
1214
                                l.Errorf("Token %s not found", jti)
×
UNCOV
1215
                                return err
×
UNCOV
1216
                        }
×
1217
                        if err != nil {
2✔
UNCOV
1218
                                return errors.Wrapf(err, "Cannot delete token with jti: %s : %s", jti, err)
×
UNCOV
1219
                        }
×
1220
                        return jwt.ErrTokenExpired
2✔
1221
                }
1222
                l.Errorf("Token %s invalid: %v", jti, err)
3✔
1223
                return jwt.ErrTokenInvalid
3✔
1224
        }
1225

1226
        if !token.Claims.Device {
31✔
1227
                l.Errorf("not a device token")
2✔
1228
                return jwt.ErrTokenInvalid
2✔
1229
        }
2✔
1230

1231
        if err := verifyTenantClaim(ctx, d.verifyTenant, token.Claims.Tenant); err != nil {
27✔
UNCOV
1232
                return err
×
UNCOV
1233
        }
×
1234
        if err = d.checker.ValidateWithContext(ctx); err != nil {
28✔
1235
                return err
1✔
1236
        }
1✔
1237

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

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

27✔
1246
        if err == cache.ErrTooManyRequests {
29✔
1247
                return err
2✔
1248
        }
2✔
1249

1250
        if cachedToken != "" {
29✔
1251
                return nil
4✔
1252
        }
4✔
1253

1254
        // caching is best effort, don't fail
1255
        if err != nil {
33✔
1256
                l.Errorf("Failed to throttle for token %v: %s, continue.", token, err.Error())
12✔
1257
        }
12✔
1258

1259
        // cache check was a MISS, hit the db for verification
1260
        // check if token is in the system
1261
        _, err = d.db.GetToken(ctx, jti)
21✔
1262
        if err != nil {
24✔
1263
                if err == store.ErrTokenNotFound {
6✔
1264
                        l.Errorf("Token %s not found", jti)
3✔
1265
                        return err
3✔
1266
                }
3✔
UNCOV
1267
                return errors.Wrapf(err, "Cannot get token with id: %s from database: %s", jti, err)
×
1268
        }
1269

1270
        auth, err := d.db.GetAuthSetById(ctx, jti.String())
19✔
1271
        if err != nil {
19✔
UNCOV
1272
                if err == store.ErrAuthSetNotFound {
×
1273
                        l.Errorf("Auth set %s not found", jti)
×
UNCOV
1274
                        return err
×
UNCOV
1275
                }
×
UNCOV
1276
                return err
×
1277
        }
1278

1279
        if auth.Status != model.DevStatusAccepted {
21✔
1280
                return jwt.ErrTokenInvalid
2✔
1281
        }
2✔
1282

1283
        // reject authentication for device that is in the process of
1284
        // decommissioning
1285
        dev, err := d.db.GetDeviceById(ctx, auth.DeviceId)
17✔
1286
        if err != nil {
17✔
UNCOV
1287
                return err
×
UNCOV
1288
        }
×
1289
        if dev.Decommissioning {
19✔
1290
                l.Errorf("Token %s rejected, device %s is being decommissioned", jti, auth.DeviceId)
2✔
1291
                return jwt.ErrTokenInvalid
2✔
1292
        }
2✔
1293

1294
        // after successful token verification - cache it (best effort)
1295
        _ = d.cacheSetToken(ctx, token, raw)
15✔
1296

15✔
1297
        return nil
15✔
1298
}
1299

1300
// purgeUriArgs removes query string args from an uri string
1301
// important for burst control (bursts are per uri without args)
1302
func purgeUriArgs(uri string) string {
31✔
1303
        return strings.Split(uri, "?")[0]
31✔
1304
}
31✔
1305

1306
func (d *DevAuth) cacheThrottleVerify(
1307
        ctx context.Context,
1308
        token *jwt.Token,
1309
        originalRaw,
1310
        origMethod,
1311
        origUri string,
1312
) (string, error) {
27✔
1313
        if d.cache == nil {
34✔
1314
                return "", nil
7✔
1315
        }
7✔
1316

1317
        // try get cached/precomputed limits
1318
        limits, err := d.getApiLimits(ctx,
20✔
1319
                token.Claims.Tenant,
20✔
1320
                token.Claims.Subject.String())
20✔
1321

20✔
1322
        if err != nil {
28✔
1323
                return "", err
8✔
1324
        }
8✔
1325

1326
        // apply throttling and fetch cached token
1327
        cached, err := d.cache.Throttle(ctx,
12✔
1328
                originalRaw,
12✔
1329
                *limits,
12✔
1330
                token.Claims.Tenant,
12✔
1331
                token.Claims.Subject.String(),
12✔
1332
                cache.IdTypeDevice,
12✔
1333
                origUri,
12✔
1334
                origMethod)
12✔
1335

12✔
1336
        return cached, err
12✔
1337
}
1338

1339
func (d *DevAuth) cacheSetToken(ctx context.Context, token *jwt.Token, raw string) error {
15✔
1340
        if d.cache == nil {
18✔
1341
                return nil
3✔
1342
        }
3✔
1343

1344
        expireIn := time.Duration(token.Claims.ExpiresAt.Unix()-d.clock.Now().Unix()) * time.Second
12✔
1345

12✔
1346
        return d.cache.CacheToken(ctx,
12✔
1347
                token.Claims.Tenant,
12✔
1348
                token.Claims.Subject.String(),
12✔
1349
                cache.IdTypeDevice,
12✔
1350
                raw,
12✔
1351
                expireIn)
12✔
1352
}
1353

1354
func (d *DevAuth) getApiLimits(
1355
        ctx context.Context,
1356
        tid,
1357
        did string,
1358
) (*ratelimits.ApiLimits, error) {
20✔
1359
        limits, err := d.cache.GetLimits(ctx, tid, did, cache.IdTypeDevice)
20✔
1360
        if err != nil {
22✔
1361
                return nil, err
2✔
1362
        }
2✔
1363

1364
        if limits != nil {
28✔
1365
                return limits, nil
10✔
1366
        }
10✔
1367

1368
        dev, err := d.db.GetDeviceById(ctx, did)
8✔
1369
        if err != nil {
8✔
UNCOV
1370
                return nil, err
×
UNCOV
1371
        }
×
1372

1373
        t, err := d.cTenant.GetTenant(ctx, tid)
8✔
1374
        if err != nil {
12✔
1375
                return nil, errors.Wrap(err, "request to get tenant failed")
4✔
1376
        }
4✔
1377
        if t == nil {
4✔
UNCOV
1378
                return nil, errors.New("tenant not found")
×
UNCOV
1379
        }
×
1380

1381
        finalLimits := apiLimitsOverride(t.ApiLimits.DeviceLimits, dev.ApiLimits)
4✔
1382

4✔
1383
        err = d.cache.CacheLimits(ctx, finalLimits, tid, did, cache.IdTypeDevice)
4✔
1384

4✔
1385
        return &finalLimits, err
4✔
1386
}
1387

1388
func (d *DevAuth) cacheDeleteToken(ctx context.Context, did string) error {
71✔
1389
        if d.cache == nil {
118✔
1390
                return nil
47✔
1391
        }
47✔
1392

1393
        idData := identity.FromContext(ctx)
24✔
1394
        if idData == nil {
30✔
1395
                return errors.New("can't unpack tenant identity data from context")
6✔
1396
        }
6✔
1397
        tid := idData.Tenant
18✔
1398

18✔
1399
        return d.cache.DeleteToken(ctx, tid, did, cache.IdTypeDevice)
18✔
1400
}
1401

1402
// TODO move to 'ratelimits', as ApiLimits methods maybe?
1403
func apiLimitsOverride(src, dest ratelimits.ApiLimits) ratelimits.ApiLimits {
18✔
1404
        // override only if not default
18✔
1405
        if dest.ApiQuota.MaxCalls != 0 && dest.ApiQuota.IntervalSec != 0 {
22✔
1406
                src.ApiQuota.MaxCalls = dest.ApiQuota.MaxCalls
4✔
1407
                src.ApiQuota.IntervalSec = dest.ApiQuota.IntervalSec
4✔
1408
        }
4✔
1409

1410
        out := make([]ratelimits.ApiBurst, len(src.ApiBursts))
18✔
1411
        copy(out, src.ApiBursts)
18✔
1412

18✔
1413
        for _, bdest := range dest.ApiBursts {
28✔
1414
                found := false
10✔
1415
                for i, bsrc := range src.ApiBursts {
18✔
1416
                        if bdest.Action == bsrc.Action &&
8✔
1417
                                bdest.Uri == bsrc.Uri {
12✔
1418
                                out[i].MinIntervalSec = bdest.MinIntervalSec
4✔
1419
                                found = true
4✔
1420
                        }
4✔
1421
                }
1422

1423
                if !found {
16✔
1424
                        out = append(out,
6✔
1425
                                ratelimits.ApiBurst{
6✔
1426
                                        Action:         bdest.Action,
6✔
1427
                                        Uri:            bdest.Uri,
6✔
1428
                                        MinIntervalSec: bdest.MinIntervalSec},
6✔
1429
                        )
6✔
1430
                }
6✔
1431
        }
1432

1433
        src.ApiBursts = out
18✔
1434
        return src
18✔
1435
}
1436

1437
func (d *DevAuth) GetLimit(ctx context.Context, name string) (*model.Limit, error) {
47✔
1438
        lim, err := d.db.GetLimit(ctx, name)
47✔
1439

47✔
1440
        switch err {
47✔
1441
        case nil:
35✔
1442
                return lim, nil
35✔
1443
        case store.ErrLimitNotFound:
5✔
1444
                return &model.Limit{Name: name, Value: 0}, nil
5✔
1445
        default:
8✔
1446
                return nil, err
8✔
1447
        }
1448
}
1449

1450
func (d *DevAuth) GetTenantLimit(
1451
        ctx context.Context,
1452
        name,
1453
        tenant_id string,
1454
) (*model.Limit, error) {
7✔
1455
        ctx = identity.WithContext(ctx, &identity.Identity{
7✔
1456
                Tenant: tenant_id,
7✔
1457
        })
7✔
1458

7✔
1459
        return d.GetLimit(ctx, name)
7✔
1460
}
7✔
1461

1462
// WithTenantVerification will force verification of tenant token with tenant
1463
// administrator when processing device authentication requests. Returns an
1464
// updated devauth.
1465
func (d *DevAuth) WithTenantVerification(c tenant.ClientRunner) *DevAuth {
49✔
1466
        d.cTenant = c
49✔
1467
        d.verifyTenant = true
49✔
1468
        return d
49✔
1469
}
49✔
1470

1471
func (d *DevAuth) WithCache(c cache.Cache) *DevAuth {
58✔
1472
        d.cache = c
58✔
1473
        return d
58✔
1474
}
58✔
1475

1476
func (d *DevAuth) WithClock(c utils.Clock) *DevAuth {
20✔
1477
        d.clock = c
20✔
1478
        return d
20✔
1479
}
20✔
1480

1481
func (d *DevAuth) SetTenantLimit(ctx context.Context, tenant_id string, limit model.Limit) error {
5✔
1482
        l := log.FromContext(ctx)
5✔
1483

5✔
1484
        ctx = identity.WithContext(ctx, &identity.Identity{
5✔
1485
                Tenant: tenant_id,
5✔
1486
        })
5✔
1487

5✔
1488
        l.Infof("setting limit %v for tenant %v", limit, tenant_id)
5✔
1489

5✔
1490
        if err := d.db.PutLimit(ctx, limit); err != nil {
7✔
1491
                l.Errorf("failed to save limit %v for tenant %v to database: %v",
2✔
1492
                        limit, tenant_id, err)
2✔
1493
                return errors.Wrapf(err, "failed to save limit %v for tenant %v to database",
2✔
1494
                        limit, tenant_id)
2✔
1495
        }
2✔
1496
        return nil
3✔
1497
}
1498

1499
func (d *DevAuth) DeleteTenantLimit(ctx context.Context, tenant_id string, limit string) error {
4✔
1500
        l := log.FromContext(ctx)
4✔
1501

4✔
1502
        ctx = identity.WithContext(ctx, &identity.Identity{
4✔
1503
                Tenant: tenant_id,
4✔
1504
        })
4✔
1505

4✔
1506
        l.Infof("removing limit %v for tenant %v", limit, tenant_id)
4✔
1507

4✔
1508
        if err := d.db.DeleteLimit(ctx, limit); err != nil {
6✔
1509
                l.Errorf("failed to delete limit %v for tenant %v to database: %v",
2✔
1510
                        limit, tenant_id, err)
2✔
1511
                return errors.Wrapf(err, "failed to delete limit %v for tenant %v to database",
2✔
1512
                        limit, tenant_id)
2✔
1513
        }
2✔
1514
        return nil
2✔
1515
}
1516

1517
func (d *DevAuth) GetDevCountByStatus(ctx context.Context, status string) (int, error) {
7✔
1518
        return d.db.GetDevCountByStatus(ctx, status)
7✔
1519
}
7✔
1520

1521
// canAcceptDevice checks if model.LimitMaxDeviceCount will be exceeded
1522
func (d *DevAuth) canAcceptDevice(ctx context.Context) (bool, error) {
33✔
1523
        limit, err := d.GetLimit(ctx, model.LimitMaxDeviceCount)
33✔
1524
        if err != nil {
37✔
1525
                return false, errors.Wrap(err, "can't get current device limit")
4✔
1526
        }
4✔
1527

1528
        if limit.Value == 0 {
40✔
1529
                return true, nil
11✔
1530
        }
11✔
1531

1532
        accepted, err := d.db.GetDevCountByStatus(ctx, model.DevStatusAccepted)
19✔
1533
        if err != nil {
21✔
1534
                return false, errors.Wrap(err, "can't get current device count")
2✔
1535
        }
2✔
1536

1537
        if uint64(accepted+1) <= limit.Value {
28✔
1538
                return true, nil
11✔
1539
        }
11✔
1540

1541
        return false, nil
7✔
1542
}
1543

1544
func (d *DevAuth) DeleteTokens(
1545
        ctx context.Context,
1546
        tenantID string,
1547
        deviceID string,
1548
) error {
13✔
1549
        var err error
13✔
1550
        ctx = identity.WithContext(ctx, &identity.Identity{
13✔
1551
                Tenant: tenantID,
13✔
1552
        })
13✔
1553

13✔
1554
        if deviceID != "" {
18✔
1555
                deviceOID := oid.FromString(deviceID)
5✔
1556
                if deviceOID.String() == "" {
5✔
UNCOV
1557
                        return ErrInvalidAuthSetID
×
UNCOV
1558
                }
×
1559
                err = d.db.DeleteTokenByDevId(ctx, deviceOID)
5✔
1560
        } else {
9✔
1561
                if err := d.cacheFlush(ctx); err != nil {
11✔
1562
                        return errors.Wrapf(
2✔
1563
                                err,
2✔
1564
                                "failed to flush cache when cleaning tokens for tenant %v",
2✔
1565
                                tenantID,
2✔
1566
                        )
2✔
1567
                }
2✔
1568

1569
                err = d.db.DeleteTokens(ctx)
7✔
1570
        }
1571

1572
        if err != nil && err != store.ErrTokenNotFound {
15✔
1573
                return errors.Wrapf(
4✔
1574
                        err,
4✔
1575
                        "failed to delete tokens for tenant: %v, device id: %v",
4✔
1576
                        tenantID,
4✔
1577
                        deviceID,
4✔
1578
                )
4✔
1579
        }
4✔
1580

1581
        return nil
7✔
1582
}
1583

1584
func (d *DevAuth) cacheFlush(ctx context.Context) error {
9✔
1585
        if d.cache == nil {
10✔
1586
                return nil
1✔
1587
        }
1✔
1588

1589
        return d.cache.FlushDB(ctx)
8✔
1590
}
1591

1592
func (d *DevAuth) ProvisionTenant(ctx context.Context, tenant_id string) error {
5✔
1593
        tenantCtx := identity.WithContext(ctx, &identity.Identity{
5✔
1594
                Tenant: tenant_id,
5✔
1595
        })
5✔
1596

5✔
1597
        dbname := mstore.DbFromContext(tenantCtx, mongo.DbName)
5✔
1598

5✔
1599
        return d.db.WithAutomigrate().MigrateTenant(tenantCtx, dbname, mongo.DbVersion)
5✔
1600
}
5✔
1601

1602
func (d *DevAuth) GetTenantDeviceStatus(
1603
        ctx context.Context,
1604
        tenantId,
1605
        deviceId string,
1606
) (*model.Status, error) {
6✔
1607
        if tenantId != "" {
12✔
1608
                ctx = identity.WithContext(ctx, &identity.Identity{
6✔
1609
                        Tenant: tenantId,
6✔
1610
                })
6✔
1611
        }
6✔
1612
        dev, err := d.db.GetDeviceById(ctx, deviceId)
6✔
1613
        switch err {
6✔
1614
        case nil:
2✔
1615
                return &model.Status{Status: dev.Status}, nil
2✔
1616
        case store.ErrDevNotFound:
2✔
1617
                return nil, ErrDeviceNotFound
2✔
1618
        default:
2✔
1619
                return nil, errors.Wrapf(err, "get device %s failed", deviceId)
2✔
1620

1621
        }
1622
}
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