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

mendersoftware / mender-server / 2050087827

19 Sep 2025 02:01PM UTC coverage: 65.285% (+0.004%) from 65.281%
2050087827

Pull #966

gitlab-ci

alfrunes
fix(deviceauth): `request_size_limit` configuration not applied

Ticket: MEN-8788
Signed-off-by: Alf-Rune Siqveland <alf.rune@northern.tech>
Pull Request #966: fix(deviceauth): `request_size_limit` configuration not applied

3 of 3 new or added lines in 1 file covered. (100.0%)

96 existing lines in 6 files now uncovered.

31637 of 48460 relevant lines covered (65.28%)

1.4 hits per line

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

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

15
package http
16

17
import (
18
        "context"
19
        "fmt"
20
        "net/http"
21
        "strconv"
22
        "strings"
23
        "time"
24

25
        "github.com/gin-gonic/gin"
26
        validation "github.com/go-ozzo/ozzo-validation/v4"
27
        "github.com/pkg/errors"
28

29
        "github.com/mendersoftware/mender-server/pkg/identity"
30
        "github.com/mendersoftware/mender-server/pkg/rest.utils"
31
        inventory "github.com/mendersoftware/mender-server/services/inventory/inv"
32
        "github.com/mendersoftware/mender-server/services/inventory/model"
33
        "github.com/mendersoftware/mender-server/services/inventory/store"
34
        "github.com/mendersoftware/mender-server/services/inventory/utils"
35
)
36

37
const (
38
        apiUrlLegacy       = "/api/0.1.0"
39
        apiUrlManagementV1 = "/api/management/v1/inventory"
40
        apiUrlDevicesV1    = "/api/devices/v1/inventory"
41

42
        uriDevices          = "/devices"
43
        uriDevice           = "/devices/:id"
44
        uriDeviceTags       = "/devices/:id/tags"
45
        uriDeviceGroups     = "/devices/:id/group"
46
        uriDeviceGroup      = "/devices/:id/group/:name"
47
        uriGroups           = "/groups"
48
        uriGroupsName       = "/groups/:name"
49
        uriGroupsDevices    = "/groups/:name/devices"
50
        uriAttributes       = "/attributes"
51
        uriDeviceAttributes = "/device/attributes"
52

53
        apiUrlInternalV1         = "/api/internal/v1/inventory"
54
        uriInternalAlive         = "/alive"
55
        uriInternalHealth        = "/health"
56
        uriInternalTenants       = "/tenants"
57
        uriInternalDevices       = "/tenants/:tenant_id/devices"
58
        urlInternalDevicesStatus = "/tenants/:tenant_id/devices/status/:status"
59
        uriInternalDeviceDetails = "/tenants/:tenant_id/devices/:device_id"
60
        uriInternalDeviceGroups  = "/tenants/:tenant_id/devices/:device_id/groups"
61
        urlInternalAttributes    = "/tenants/:tenant_id/device/:device_id/attribute/scope/:scope"
62
        urlInternalReindex       = "/tenants/:tenant_id/devices/:device_id/reindex"
63
        apiUrlManagementV2       = "/api/management/v2/inventory"
64
        urlFiltersAttributes     = "/filters/attributes"
65
        urlFiltersSearch         = "/filters/search"
66

67
        apiUrlInternalV2         = "/api/internal/v2/inventory"
68
        urlInternalFiltersSearch = "/tenants/:tenant_id/filters/search"
69

70
        hdrTotalCount = "X-Total-Count"
71
)
72

73
const (
74
        queryParamGroup          = "group"
75
        queryParamSort           = "sort"
76
        queryParamHasGroup       = "has_group"
77
        queryParamValueSeparator = ":"
78
        queryParamScopeSeparator = "/"
79
        sortOrderAsc             = "asc"
80
        sortOrderDesc            = "desc"
81
        sortAttributeNameIdx     = 0
82
        sortOrderIdx             = 1
83
)
84

85
const (
86
        DefaultTimeout = time.Second * 10
87
)
88

89
const (
90
        checkInTimeParamName  = "check_in_time"
91
        checkInTimeParamScope = "system"
92
)
93

94
// model of device's group name response at /devices/:id/group endpoint
95
type InventoryApiGroup struct {
96
        Group model.GroupName `json:"group"`
97
}
98

99
func (g InventoryApiGroup) Validate() error {
3✔
100
        return g.Group.Validate()
3✔
101
}
3✔
102

103
func (i *InternalAPI) LivelinessHandler(c *gin.Context) {
2✔
104
        c.Status(http.StatusNoContent)
2✔
105
}
2✔
106

107
func (i *InternalAPI) HealthCheckHandler(c *gin.Context) {
2✔
108
        ctx := c.Request.Context()
2✔
109
        ctx, cancel := context.WithTimeout(ctx, DefaultTimeout)
2✔
110
        defer cancel()
2✔
111

2✔
112
        err := i.App.HealthCheck(ctx)
2✔
113
        if err != nil {
3✔
114
                rest.RenderError(c, http.StatusServiceUnavailable, err)
1✔
115
                return
1✔
116
        }
1✔
117

118
        c.Status(http.StatusNoContent)
2✔
119
}
120

121
// `sort` paramater value is an attribute name with optional direction (desc or asc)
122
// separated by colon (:)
123
//
124
// eg. `sort=attr_name1` or `sort=attr_name1:asc`
125
func parseSortParam(c *gin.Context) (*store.Sort, error) {
3✔
126
        sortStr, err := utils.ParseQueryParmStr(c.Request, queryParamSort, false, nil)
3✔
127
        if err != nil {
3✔
128
                return nil, err
×
129
        }
×
130
        if sortStr == "" {
6✔
131
                return nil, nil
3✔
132
        }
3✔
133
        sortValArray := strings.Split(sortStr, queryParamValueSeparator)
2✔
134
        attrNameWithScope := strings.SplitN(
2✔
135
                sortValArray[sortAttributeNameIdx],
2✔
136
                queryParamScopeSeparator,
2✔
137
                2,
2✔
138
        )
2✔
139
        var scope, attrName string
2✔
140
        if len(attrNameWithScope) == 1 {
4✔
141
                scope = model.AttrScopeInventory
2✔
142
                attrName = attrNameWithScope[0]
2✔
143
        } else {
2✔
144
                scope = attrNameWithScope[0]
×
145
                attrName = attrNameWithScope[1]
×
146
        }
×
147
        sort := store.Sort{AttrName: attrName, AttrScope: scope}
2✔
148
        if len(sortValArray) == 2 {
4✔
149
                sortOrder := sortValArray[sortOrderIdx]
2✔
150
                if sortOrder != sortOrderAsc && sortOrder != sortOrderDesc {
3✔
151
                        return nil, errors.New("invalid sort order")
1✔
152
                }
1✔
153
                sort.Ascending = sortOrder == sortOrderAsc
2✔
154
        }
155
        return &sort, nil
2✔
156
}
157

158
// Filter paramaters name are attributes name. Value can be prefixed
159
// with equality operator code (`eq` for =), separated from value by colon (:).
160
// Equality operator default value is `eq`
161
//
162
// eg. `attr_name1=value1` or `attr_name1=eq:value1`
163
func parseFilterParams(c *gin.Context) ([]store.Filter, error) {
3✔
164
        knownParams := []string{
3✔
165
                utils.PageName,
3✔
166
                utils.PerPageName,
3✔
167
                queryParamSort,
3✔
168
                queryParamHasGroup,
3✔
169
                queryParamGroup,
3✔
170
        }
3✔
171
        filters := make([]store.Filter, 0)
3✔
172
        var filter store.Filter
3✔
173
        for name := range c.Request.URL.Query() {
6✔
174
                if utils.ContainsString(name, knownParams) {
6✔
175
                        continue
3✔
176
                }
177
                valueStr, err := utils.ParseQueryParmStr(c.Request, name, false, nil)
3✔
178
                if err != nil {
3✔
179
                        return nil, err
×
180
                }
×
181

182
                attrNameWithScope := strings.SplitN(name, queryParamScopeSeparator, 2)
3✔
183
                var scope, attrName string
3✔
184
                if len(attrNameWithScope) == 1 {
6✔
185
                        scope = model.AttrScopeInventory
3✔
186
                        attrName = attrNameWithScope[0]
3✔
187
                } else {
4✔
188
                        scope = attrNameWithScope[0]
1✔
189
                        attrName = attrNameWithScope[1]
1✔
190
                }
1✔
191
                filter = store.Filter{AttrName: attrName, AttrScope: scope}
3✔
192

3✔
193
                // make sure we parse ':'s in value, it's either:
3✔
194
                // not there
3✔
195
                // after a valid operator specifier
3✔
196
                // or/and inside the value itself(mac, etc), in which case leave it alone
3✔
197
                sepIdx := strings.Index(valueStr, ":")
3✔
198
                if sepIdx == -1 {
5✔
199
                        filter.Value = valueStr
2✔
200
                        filter.Operator = store.Eq
2✔
201
                } else {
4✔
202
                        validOps := []string{"eq"}
2✔
203
                        for _, o := range validOps {
4✔
204
                                if valueStr[:sepIdx] == o {
3✔
205
                                        switch o {
1✔
206
                                        case "eq":
1✔
207
                                                filter.Operator = store.Eq
1✔
208
                                                filter.Value = valueStr[sepIdx+1:]
1✔
209
                                        }
210
                                        break
1✔
211
                                }
212
                        }
213

214
                        if filter.Value == "" {
4✔
215
                                filter.Value = valueStr
2✔
216
                                filter.Operator = store.Eq
2✔
217
                        }
2✔
218
                }
219

220
                floatValue, err := strconv.ParseFloat(filter.Value, 64)
3✔
221
                if err == nil {
5✔
222
                        filter.ValueFloat = &floatValue
2✔
223
                }
2✔
224

225
                timeValue, err := time.Parse("2006-01-02T15:04:05Z", filter.Value)
3✔
226
                if err == nil {
4✔
227
                        filter.ValueTime = &timeValue
1✔
228
                }
1✔
229

230
                filters = append(filters, filter)
3✔
231
        }
232
        return filters, nil
3✔
233
}
234

235
func (i *ManagementAPI) GetDevicesHandler(c *gin.Context) {
3✔
236
        ctx := c.Request.Context()
3✔
237

3✔
238
        page, perPage, err := rest.ParsePagingParameters(c.Request)
3✔
239
        if err != nil {
4✔
240
                rest.RenderError(c, http.StatusBadRequest, err)
1✔
241
                return
1✔
242
        }
1✔
243

244
        hasGroup, err := utils.ParseQueryParmBool(c.Request, queryParamHasGroup, false, nil)
3✔
245
        if err != nil {
4✔
246
                rest.RenderError(c, http.StatusBadRequest, err)
1✔
247
                return
1✔
248
        }
1✔
249

250
        groupName, err := utils.ParseQueryParmStr(c.Request, "group", false, nil)
3✔
251
        if err != nil {
3✔
252
                rest.RenderError(c, http.StatusBadRequest, err)
×
253
                return
×
254
        }
×
255

256
        sort, err := parseSortParam(c)
3✔
257
        if err != nil {
4✔
258
                rest.RenderError(c, http.StatusBadRequest, err)
1✔
259
                return
1✔
260
        }
1✔
261

262
        filters, err := parseFilterParams(c)
3✔
263
        if err != nil {
3✔
264
                rest.RenderError(c, http.StatusBadRequest, err)
×
265
                return
×
266
        }
×
267

268
        ld := store.ListQuery{Skip: int((page - 1) * perPage),
3✔
269
                Limit:     int(perPage),
3✔
270
                Filters:   filters,
3✔
271
                Sort:      sort,
3✔
272
                HasGroup:  hasGroup,
3✔
273
                GroupName: groupName}
3✔
274

3✔
275
        devs, totalCount, err := i.App.ListDevices(ctx, ld)
3✔
276

3✔
277
        if err != nil {
4✔
278
                rest.RenderInternalError(c, err)
1✔
279
                return
1✔
280
        }
1✔
281

282
        hasNext := totalCount > int(page*perPage)
3✔
283

3✔
284
        hints := rest.NewPagingHints().
3✔
285
                SetPage(page).
3✔
286
                SetPerPage(perPage).
3✔
287
                SetHasNext(hasNext).
3✔
288
                SetTotalCount(int64(totalCount))
3✔
289

3✔
290
        links, err := rest.MakePagingHeaders(c.Request, hints)
3✔
291
        if err != nil {
3✔
292
                rest.RenderInternalError(c, err)
×
293
                return
×
294
        }
×
295
        for _, l := range links {
6✔
296
                c.Writer.Header().Add("Link", l)
3✔
297
        }
3✔
298
        // the response writer will ensure the header name is in Kebab-Pascal-Case
299
        c.Writer.Header().Add(hdrTotalCount, strconv.Itoa(totalCount))
3✔
300
        c.JSON(http.StatusOK, devs)
3✔
301
}
302

303
func (i *ManagementAPI) GetDeviceHandler(c *gin.Context) {
3✔
304
        ctx := c.Request.Context()
3✔
305

3✔
306
        deviceID := c.Param("id")
3✔
307

3✔
308
        dev, err := i.App.GetDevice(ctx, model.DeviceID(deviceID))
3✔
309
        if err != nil {
4✔
310
                rest.RenderInternalError(c, err)
1✔
311
                return
1✔
312
        }
1✔
313
        if dev == nil {
5✔
314
                rest.RenderError(c,
2✔
315
                        http.StatusNotFound,
2✔
316
                        store.ErrDevNotFound,
2✔
317
                )
2✔
318
                return
2✔
319
        }
2✔
320
        if dev.TagsEtag != "" {
4✔
321
                c.Header("ETag", dev.TagsEtag)
1✔
322
        }
1✔
323
        c.JSON(http.StatusOK, dev)
3✔
324
}
325

326
func (i *ManagementAPI) DeleteDeviceInventoryHandler(c *gin.Context) {
1✔
327
        ctx := c.Request.Context()
1✔
328

1✔
329
        deviceID := c.Param("id")
1✔
330

1✔
331
        err := i.App.ReplaceAttributes(ctx, model.DeviceID(deviceID),
1✔
332
                model.DeviceAttributes{}, model.AttrScopeInventory, "")
1✔
333
        if err != nil && err != store.ErrDevNotFound {
2✔
334
                rest.RenderInternalError(c, err)
1✔
335
                return
1✔
336
        }
1✔
337

338
        c.Status(http.StatusNoContent)
1✔
339
}
340

341
func (i *InternalAPI) DeleteDeviceHandler(c *gin.Context) {
2✔
342
        ctx := c.Request.Context()
2✔
343
        tenantId := c.Param("tenant_id")
2✔
344
        if tenantId != "" {
4✔
345
                id := &identity.Identity{
2✔
346
                        Tenant: tenantId,
2✔
347
                }
2✔
348
                ctx = identity.WithContext(ctx, id)
2✔
349
        }
2✔
350

351
        deviceID := c.Param("device_id")
2✔
352

2✔
353
        err := i.App.DeleteDevice(ctx, model.DeviceID(deviceID))
2✔
354
        if err != nil && err != store.ErrDevNotFound {
3✔
355
                rest.RenderInternalError(c, err)
1✔
356
                return
1✔
357
        }
1✔
358

359
        c.Status(http.StatusNoContent)
2✔
360
}
361

362
func (i *InternalAPI) AddDeviceHandler(c *gin.Context) {
3✔
363
        ctx := c.Request.Context()
3✔
364
        tenantId := c.Param("tenant_id")
3✔
365
        if tenantId != "" {
5✔
366
                id := &identity.Identity{
2✔
367
                        Tenant: tenantId,
2✔
368
                }
2✔
369
                ctx = identity.WithContext(ctx, id)
2✔
370
        }
2✔
371

372
        dev, err := parseDevice(c)
3✔
373
        if err != nil {
5✔
374
                rest.RenderError(c,
2✔
375
                        http.StatusBadRequest,
2✔
376
                        err,
2✔
377
                )
2✔
378
                return
2✔
379
        }
2✔
380

381
        err = dev.Attributes.Validate()
3✔
382
        if err != nil {
3✔
383
                rest.RenderError(c,
×
384
                        http.StatusBadRequest,
×
385
                        err,
×
386
                )
×
387
                return
×
388
        }
×
389

390
        err = i.App.AddDevice(ctx, dev)
3✔
391
        if err != nil {
4✔
392
                rest.RenderInternalError(c, err)
1✔
393
                return
1✔
394
        }
1✔
395

396
        c.Writer.Header().Add("Location", "devices/"+dev.ID.String())
3✔
397
        c.Status(http.StatusCreated)
3✔
398
}
399

400
func (i *ManagementAPI) UpdateDeviceAttributesHandler(c *gin.Context) {
2✔
401
        ctx := c.Request.Context()
2✔
402

2✔
403
        var idata *identity.Identity
2✔
404
        if idata = identity.FromContext(ctx); idata == nil || !idata.IsDevice {
3✔
405
                rest.RenderError(c,
1✔
406
                        http.StatusUnauthorized,
1✔
407
                        errors.New("unauthorized"),
1✔
408
                )
1✔
409
                return
1✔
410
        }
1✔
411
        deviceID := model.DeviceID(idata.Subject)
2✔
412
        //extract attributes from body
2✔
413
        attrs, err := parseAttributes(c)
2✔
414
        if err != nil {
4✔
415
                rest.RenderError(c,
2✔
416
                        http.StatusBadRequest,
2✔
417
                        err,
2✔
418
                )
2✔
419
                return
2✔
420
        }
2✔
421
        i.updateDeviceAttributes(c, attrs, deviceID, model.AttrScopeInventory, "")
2✔
422
}
423

424
func (i *ManagementAPI) UpdateDeviceTagsHandler(c *gin.Context) {
2✔
425
        // get device ID from uri
2✔
426
        deviceID := model.DeviceID(c.Param("id"))
2✔
427
        if len(deviceID) < 1 {
2✔
428
                rest.RenderError(c,
×
429
                        http.StatusBadRequest,
×
430
                        errors.New("device id cannot be empty"),
×
431
                )
×
432
                return
×
433
        }
×
434

435
        ifMatchHeader := c.Request.Header.Get("If-Match")
2✔
436

2✔
437
        // extract attributes from body
2✔
438
        attrs, err := parseAttributes(c)
2✔
439
        if err != nil {
2✔
440
                rest.RenderError(c,
×
441
                        http.StatusBadRequest,
×
442
                        err,
×
443
                )
×
444
                return
×
445
        }
×
446

447
        // set scope and timestamp for tags attributes
448
        now := time.Now()
2✔
449
        for i := range attrs {
4✔
450
                attrs[i].Scope = model.AttrScopeTags
2✔
451
                if attrs[i].Timestamp == nil {
4✔
452
                        attrs[i].Timestamp = &now
2✔
453
                }
2✔
454
        }
455

456
        i.updateDeviceAttributes(c, attrs, deviceID, model.AttrScopeTags, ifMatchHeader)
2✔
457
}
458

459
func (i *ManagementAPI) updateDeviceAttributes(
460
        c *gin.Context,
461
        attrs model.DeviceAttributes,
462
        deviceID model.DeviceID,
463
        scope string,
464
        etag string,
465
) {
3✔
466
        ctx := c.Request.Context()
3✔
467

3✔
468
        var err error
3✔
469

3✔
470
        // upsert or replace the attributes
3✔
471
        if c.Request.Method == http.MethodPatch {
6✔
472
                err = i.App.UpsertAttributesWithUpdated(ctx, deviceID, attrs, scope, etag)
3✔
473
        } else if c.Request.Method == http.MethodPut {
7✔
474
                err = i.App.ReplaceAttributes(ctx, deviceID, attrs, scope, etag)
2✔
475
        } else {
2✔
476
                rest.RenderError(c,
×
477
                        http.StatusMethodNotAllowed,
×
478
                        errors.New("method not alllowed"),
×
479
                )
×
480
                return
×
481
        }
×
482

483
        cause := errors.Cause(err)
3✔
484
        switch cause {
3✔
485
        case store.ErrNoAttrName:
×
486
        case inventory.ErrTooManyAttributes:
1✔
487
                rest.RenderError(c, http.StatusBadRequest, err)
1✔
488
                return
1✔
489
        case inventory.ErrETagDoesntMatch:
2✔
490
                rest.RenderError(c,
2✔
491
                        http.StatusPreconditionFailed,
2✔
492
                        cause,
2✔
493
                )
2✔
494
                return
2✔
495
        }
496
        if err != nil {
4✔
497
                rest.RenderInternalError(c, err)
1✔
498
                return
1✔
499
        }
1✔
500

501
        c.Status(http.StatusOK)
3✔
502
}
503

504
func (i *InternalAPI) PatchDeviceAttributesInternalHandler(
505
        c *gin.Context,
506
) {
2✔
507
        ctx := c.Request.Context()
2✔
508
        tenantId := c.Param("tenant_id")
2✔
509
        ctx = getTenantContext(ctx, tenantId)
2✔
510

2✔
511
        deviceId := c.Param("device_id")
2✔
512
        if len(deviceId) < 1 {
3✔
513
                rest.RenderError(c, http.StatusBadRequest, errors.New("device id cannot be empty"))
1✔
514
                return
1✔
515
        }
1✔
516
        //extract attributes from body
517
        attrs, err := parseAttributes(c)
2✔
518
        if err != nil {
4✔
519
                rest.RenderError(c, http.StatusBadRequest, err)
2✔
520
                return
2✔
521
        }
2✔
522
        for i := range attrs {
4✔
523
                attrs[i].Scope = c.Param("scope")
2✔
524
                if attrs[i].Name == checkInTimeParamName && attrs[i].Scope == checkInTimeParamScope {
3✔
525
                        t, err := time.Parse(time.RFC3339, fmt.Sprintf("%v", attrs[i].Value))
1✔
526
                        if err != nil {
1✔
527
                                rest.RenderError(c, http.StatusBadRequest, err)
×
528
                                return
×
529
                        }
×
530
                        attrs[i].Value = t
1✔
531
                }
532
        }
533

534
        //upsert the attributes
535
        err = i.App.UpsertAttributes(ctx, model.DeviceID(deviceId), attrs)
2✔
536
        cause := errors.Cause(err)
2✔
537
        switch cause {
2✔
538
        case store.ErrNoAttrName:
×
539
                rest.RenderError(c, http.StatusBadRequest, cause)
×
540
                return
×
541
        }
542
        if err != nil {
3✔
543
                rest.RenderInternalError(c, err)
1✔
544
                return
1✔
545
        }
1✔
546

547
        c.Status(http.StatusOK)
2✔
548
}
549

550
func (i *ManagementAPI) DeleteDeviceGroupHandler(c *gin.Context) {
2✔
551
        ctx := c.Request.Context()
2✔
552

2✔
553
        deviceID := c.Param("id")
2✔
554
        groupName := c.Param("name")
2✔
555

2✔
556
        err := i.App.UnsetDeviceGroup(ctx, model.DeviceID(deviceID), model.GroupName(groupName))
2✔
557
        if err != nil {
4✔
558
                cause := errors.Cause(err)
2✔
559
                if cause != nil {
4✔
560
                        if cause.Error() == store.ErrDevNotFound.Error() {
4✔
561
                                rest.RenderError(c, http.StatusNotFound, err)
2✔
562
                                return
2✔
563
                        }
2✔
564
                }
565
                rest.RenderInternalError(c, err)
1✔
566
                return
1✔
567
        }
568

569
        c.Status(http.StatusNoContent)
2✔
570
}
571

572
func (i *ManagementAPI) AddDeviceToGroupHandler(c *gin.Context) {
3✔
573
        ctx := c.Request.Context()
3✔
574

3✔
575
        devId := c.Param("id")
3✔
576

3✔
577
        var group InventoryApiGroup
3✔
578
        err := c.ShouldBindJSON(&group)
3✔
579
        if err != nil {
4✔
580
                rest.RenderError(c,
1✔
581
                        http.StatusBadRequest,
1✔
582
                        errors.Wrap(err, "failed to decode device group data"))
1✔
583
                return
1✔
584
        }
1✔
585

586
        if err = group.Validate(); err != nil {
4✔
587
                rest.RenderError(c, http.StatusBadRequest, err)
1✔
588
                return
1✔
589
        }
1✔
590

591
        err = i.App.UpdateDeviceGroup(ctx, model.DeviceID(devId), model.GroupName(group.Group))
3✔
592
        if err != nil {
4✔
593
                if cause := errors.Cause(err); cause != nil && cause == store.ErrDevNotFound {
2✔
594
                        rest.RenderError(c, http.StatusNotFound, err)
1✔
595
                        return
1✔
596
                }
1✔
597
                rest.RenderInternalError(c, err)
1✔
598
                return
1✔
599
        }
600
        c.Status(http.StatusNoContent)
3✔
601
}
602

603
func (i *ManagementAPI) GetDevicesByGroupHandler(c *gin.Context) {
2✔
604
        ctx := c.Request.Context()
2✔
605

2✔
606
        group := c.Param("name")
2✔
607

2✔
608
        page, perPage, err := rest.ParsePagingParameters(c.Request)
2✔
609
        if err != nil {
3✔
610
                rest.RenderError(c, http.StatusBadRequest, err)
1✔
611
                return
1✔
612
        }
1✔
613

614
        //get one extra device to see if there's a 'next' page
615
        ids, totalCount, err := i.App.ListDevicesByGroup(
2✔
616
                ctx,
2✔
617
                model.GroupName(group),
2✔
618
                int((page-1)*perPage),
2✔
619
                int(perPage),
2✔
620
        )
2✔
621
        if err != nil {
4✔
622
                if err == store.ErrGroupNotFound {
4✔
623
                        rest.RenderError(c, http.StatusNotFound, err)
2✔
624

2✔
625
                } else {
3✔
626
                        rest.RenderError(c,
1✔
627
                                http.StatusInternalServerError,
1✔
628
                                errors.New("internal error"),
1✔
629
                        )
1✔
630
                }
1✔
631
                return
2✔
632
        }
633

634
        hasNext := totalCount > int(page*perPage)
2✔
635

2✔
636
        hints := rest.NewPagingHints().
2✔
637
                SetPage(page).
2✔
638
                SetPerPage(perPage).
2✔
639
                SetHasNext(hasNext).
2✔
640
                SetTotalCount(int64(totalCount))
2✔
641

2✔
642
        links, err := rest.MakePagingHeaders(c.Request, hints)
2✔
643
        if err != nil {
2✔
644
                rest.RenderError(c, http.StatusBadRequest, err)
×
645
                return
×
646
        }
×
647
        for _, l := range links {
4✔
648
                c.Writer.Header().Add("Link", l)
2✔
649
        }
2✔
650
        // the response writer will ensure the header name is in Kebab-Pascal-Case
651
        c.Writer.Header().Add(hdrTotalCount, strconv.Itoa(totalCount))
2✔
652
        c.JSON(http.StatusOK, ids)
2✔
653
}
654

655
func (i *ManagementAPI) AppendDevicesToGroup(c *gin.Context) {
1✔
656
        var deviceIDs []model.DeviceID
1✔
657
        ctx := c.Request.Context()
1✔
658
        groupName := model.GroupName(c.Param("name"))
1✔
659
        if err := groupName.Validate(); err != nil {
2✔
660
                rest.RenderError(c, http.StatusBadRequest, err)
1✔
661
                return
1✔
662
        }
1✔
663

664
        if err := c.ShouldBindJSON(&deviceIDs); err != nil {
2✔
665
                rest.RenderError(c,
1✔
666
                        http.StatusBadRequest,
1✔
667
                        errors.Wrap(err, "invalid payload schema"),
1✔
668
                )
1✔
669
                return
1✔
670
        } else if len(deviceIDs) == 0 {
3✔
671
                rest.RenderError(c,
1✔
672
                        http.StatusBadRequest,
1✔
673
                        errors.New("no device IDs present in payload"),
1✔
674
                )
1✔
675
                return
1✔
676
        }
1✔
677
        updated, err := i.App.UpdateDevicesGroup(
1✔
678
                ctx, deviceIDs, groupName,
1✔
679
        )
1✔
680
        if err != nil {
2✔
681
                rest.RenderInternalError(c, err)
1✔
682
                return
1✔
683
        }
1✔
684
        c.JSON(http.StatusOK, updated)
1✔
685
}
686

687
func (i *ManagementAPI) DeleteGroupHandler(c *gin.Context) {
1✔
688
        ctx := c.Request.Context()
1✔
689

1✔
690
        groupName := model.GroupName(c.Param("name"))
1✔
691
        if err := groupName.Validate(); err != nil {
2✔
692
                rest.RenderError(c, http.StatusBadRequest, err)
1✔
693
                return
1✔
694
        }
1✔
695

696
        updated, err := i.App.DeleteGroup(ctx, groupName)
1✔
697
        if err != nil {
2✔
698
                rest.RenderInternalError(c, err)
1✔
699
                return
1✔
700
        }
1✔
701
        c.JSON(http.StatusOK, updated)
1✔
702
}
703

704
func (i *ManagementAPI) ClearDevicesGroupHandler(c *gin.Context) {
1✔
705
        var deviceIDs []model.DeviceID
1✔
706
        ctx := c.Request.Context()
1✔
707

1✔
708
        groupName := model.GroupName(c.Param("name"))
1✔
709
        if err := groupName.Validate(); err != nil {
2✔
710
                rest.RenderError(c, http.StatusBadRequest, err)
1✔
711
                return
1✔
712
        }
1✔
713

714
        if err := c.ShouldBindJSON(&deviceIDs); err != nil {
2✔
715
                rest.RenderError(c,
1✔
716
                        http.StatusBadRequest,
1✔
717
                        errors.Wrap(err, "invalid payload schema"),
1✔
718
                )
1✔
719
                return
1✔
720
        } else if len(deviceIDs) == 0 {
3✔
721
                rest.RenderError(c,
1✔
722
                        http.StatusBadRequest,
1✔
723
                        errors.New("no device IDs present in payload"),
1✔
724
                )
1✔
725
                return
1✔
726
        }
1✔
727

728
        updated, err := i.App.UnsetDevicesGroup(ctx, deviceIDs, groupName)
1✔
729
        if err != nil {
2✔
730
                rest.RenderInternalError(c, err)
1✔
731
                return
1✔
732
        }
1✔
733

734
        c.JSON(http.StatusOK, updated)
1✔
735
}
736

737
func parseDevice(c *gin.Context) (*model.Device, error) {
3✔
738
        dev := model.Device{}
3✔
739

3✔
740
        //decode body
3✔
741
        err := c.ShouldBindJSON(&dev)
3✔
742
        if err != nil {
5✔
743
                return nil, errors.Wrap(err, "failed to decode request body")
2✔
744
        }
2✔
745

746
        if err := dev.Validate(); err != nil {
4✔
747
                return nil, err
1✔
748
        }
1✔
749

750
        return &dev, nil
3✔
751
}
752

753
func parseAttributes(c *gin.Context) (model.DeviceAttributes, error) {
3✔
754
        var attrs model.DeviceAttributes
3✔
755

3✔
756
        err := c.ShouldBindJSON(&attrs)
3✔
757
        if err != nil {
5✔
758
                return nil, errors.Wrap(err, "failed to decode request body")
2✔
759
        }
2✔
760

761
        err = attrs.Validate()
3✔
762
        if err != nil {
5✔
763
                return nil, err
2✔
764
        }
2✔
765

766
        return attrs, nil
3✔
767
}
768

769
func (i *ManagementAPI) GetGroupsHandler(c *gin.Context) {
2✔
770
        var fltr []model.FilterPredicate
2✔
771
        ctx := c.Request.Context()
2✔
772

2✔
773
        query := c.Request.URL.Query()
2✔
774
        status := query.Get("status")
2✔
775
        if status != "" {
3✔
776
                fltr = []model.FilterPredicate{{
1✔
777
                        Attribute: "status",
1✔
778
                        Scope:     "identity",
1✔
779
                        Type:      "$eq",
1✔
780
                        Value:     status,
1✔
781
                }}
1✔
782
        }
1✔
783

784
        groups, err := i.App.ListGroups(ctx, fltr)
2✔
785
        if err != nil {
3✔
786
                rest.RenderInternalError(c, err)
1✔
787
                return
1✔
788
        }
1✔
789

790
        if groups == nil {
3✔
791
                groups = []model.GroupName{}
1✔
792
        }
1✔
793

794
        c.JSON(http.StatusOK, groups)
2✔
795
}
796

797
func (i *ManagementAPI) GetDeviceGroupHandler(c *gin.Context) {
1✔
798
        ctx := c.Request.Context()
1✔
799

1✔
800
        deviceID := c.Param("id")
1✔
801

1✔
802
        group, err := i.App.GetDeviceGroup(ctx, model.DeviceID(deviceID))
1✔
803
        if err != nil {
2✔
804
                if err == store.ErrDevNotFound {
2✔
805
                        rest.RenderError(c,
1✔
806
                                http.StatusNotFound,
1✔
807
                                store.ErrDevNotFound,
1✔
808
                        )
1✔
809
                } else {
2✔
810
                        rest.RenderError(c,
1✔
811
                                http.StatusInternalServerError,
1✔
812
                                errors.New("internal error"),
1✔
813
                        )
1✔
814
                }
1✔
815
                return
1✔
816
        }
817

818
        ret := map[string]*model.GroupName{"group": nil}
1✔
819

1✔
820
        if group != "" {
2✔
821
                ret["group"] = &group
1✔
822
        }
1✔
823

824
        c.JSON(http.StatusOK, ret)
1✔
825
}
826

827
type newTenantRequest struct {
828
        TenantID string `json:"tenant_id" valid:"required"`
829
}
830

831
func (t newTenantRequest) Validate() error {
2✔
832
        return validation.ValidateStruct(&t,
2✔
833
                validation.Field(&t.TenantID, validation.Required),
2✔
834
        )
2✔
835
}
2✔
836
func (i *InternalAPI) CreateTenantHandler(c *gin.Context) {
3✔
837
        ctx := c.Request.Context()
3✔
838

3✔
839
        var newTenant newTenantRequest
3✔
840

3✔
841
        if err := c.ShouldBindJSON(&newTenant); err != nil {
5✔
842
                rest.RenderError(c, http.StatusBadRequest, err)
2✔
843
                return
2✔
844
        }
2✔
845

846
        if err := newTenant.Validate(); err != nil {
4✔
847
                rest.RenderError(c, http.StatusBadRequest, err)
2✔
848
                return
2✔
849
        }
2✔
850

851
        err := i.App.CreateTenant(ctx, model.NewTenant{
2✔
852
                ID: newTenant.TenantID,
2✔
853
        })
2✔
854
        if err != nil {
3✔
855
                rest.RenderInternalError(c, err)
1✔
856
                return
1✔
857
        }
1✔
858

859
        c.Status(http.StatusCreated)
2✔
860
}
861

862
func (i *ManagementAPI) FiltersAttributesHandler(c *gin.Context) {
2✔
863
        ctx := c.Request.Context()
2✔
864

2✔
865
        // query the database
2✔
866
        attributes, err := i.App.GetFiltersAttributes(ctx)
2✔
867
        if err != nil {
3✔
868
                rest.RenderInternalError(c, err)
1✔
869
                return
1✔
870
        }
1✔
871

872
        // in case of nil make sure we return empty list
873
        if attributes == nil {
4✔
874
                attributes = []model.FilterAttribute{}
2✔
875
        }
2✔
876

877
        c.JSON(http.StatusOK, attributes)
2✔
878
}
879

880
func (i *ManagementAPI) FiltersSearchHandler(c *gin.Context) {
2✔
881
        ctx := c.Request.Context()
2✔
882

2✔
883
        //extract attributes from body
2✔
884
        searchParams, err := parseSearchParams(c)
2✔
885
        if err != nil {
4✔
886
                rest.RenderError(c, http.StatusBadRequest, err)
2✔
887
                return
2✔
888
        }
2✔
889

890
        // query the database
891
        devs, totalCount, err := i.App.SearchDevices(ctx, *searchParams)
2✔
892
        if err != nil {
3✔
893
                if strings.Contains(err.Error(), "BadValue") {
2✔
894
                        rest.RenderError(c, http.StatusBadRequest, err)
1✔
895
                } else {
2✔
896
                        rest.RenderError(c,
1✔
897
                                http.StatusInternalServerError,
1✔
898
                                errors.New("internal error"),
1✔
899
                        )
1✔
900
                }
1✔
901
                return
1✔
902
        }
903

904
        // the response writer will ensure the header name is in Kebab-Pascal-Case
905
        c.Writer.Header().Add(hdrTotalCount, strconv.Itoa(totalCount))
2✔
906
        c.JSON(http.StatusOK, devs)
2✔
907
}
908

909
func (i *InternalAPI) InternalFiltersSearchHandler(c *gin.Context) {
2✔
910
        ctx := c.Request.Context()
2✔
911

2✔
912
        tenantId := c.Param("tenant_id")
2✔
913
        if tenantId != "" {
4✔
914
                ctx = getTenantContext(ctx, tenantId)
2✔
915
        }
2✔
916

917
        //extract attributes from body
918
        searchParams, err := parseSearchParams(c)
2✔
919
        if err != nil {
4✔
920
                rest.RenderError(c, http.StatusBadRequest, err)
2✔
921
                return
2✔
922
        }
2✔
923

924
        // query the database
925
        devs, totalCount, err := i.App.SearchDevices(ctx, *searchParams)
2✔
926
        if err != nil {
3✔
927
                if strings.Contains(err.Error(), "BadValue") {
2✔
928
                        rest.RenderError(c, http.StatusBadRequest, err)
1✔
929
                } else {
2✔
930
                        rest.RenderError(c,
1✔
931
                                http.StatusInternalServerError,
1✔
932
                                errors.New("internal error"),
1✔
933
                        )
1✔
934
                }
1✔
935
                return
1✔
936
        }
937

938
        // the response writer will ensure the header name is in Kebab-Pascal-Case
939
        c.Writer.Header().Add(hdrTotalCount, strconv.Itoa(totalCount))
2✔
940
        c.JSON(http.StatusOK, devs)
2✔
941
}
942

943
func getTenantContext(ctx context.Context, tenantId string) context.Context {
2✔
944
        if ctx == nil {
2✔
945
                ctx = context.Background()
×
946
        }
×
947
        if tenantId == "" {
4✔
948
                return ctx
2✔
949
        }
2✔
950
        id := &identity.Identity{
2✔
951
                Tenant: tenantId,
2✔
952
        }
2✔
953

2✔
954
        ctx = identity.WithContext(ctx, id)
2✔
955

2✔
956
        return ctx
2✔
957
}
958

959
func (i *InternalAPI) InternalDevicesStatusHandler(c *gin.Context) {
2✔
960
        const (
2✔
961
                StatusDecommissioned = "decommissioned"
2✔
962
                StatusAccepted       = "accepted"
2✔
963
                StatusRejected       = "rejected"
2✔
964
                StatusPreauthorized  = "preauthorized"
2✔
965
                StatusPending        = "pending"
2✔
966
                StatusNoAuth         = "noauth"
2✔
967
        )
2✔
968
        var (
2✔
969
                devices []model.DeviceUpdate
2✔
970
                result  *model.UpdateResult
2✔
971
        )
2✔
972

2✔
973
        ctx := c.Request.Context()
2✔
974

2✔
975
        tenantID := c.Param("tenant_id")
2✔
976
        ctx = getTenantContext(ctx, tenantID)
2✔
977

2✔
978
        status := c.Param("status")
2✔
979

2✔
980
        err := c.ShouldBindJSON(&devices)
2✔
981
        if err != nil {
4✔
982
                rest.RenderError(c,
2✔
983
                        http.StatusBadRequest,
2✔
984
                        errors.Wrap(err, "cant parse devices"),
2✔
985
                )
2✔
986
                return
2✔
987
        }
2✔
988

989
        switch status {
2✔
990
        case StatusAccepted, StatusPreauthorized,
991
                StatusPending, StatusRejected,
992
                StatusNoAuth:
2✔
993
                // Update statuses
2✔
994
                attrs := model.DeviceAttributes{{
2✔
995
                        Name:  "status",
2✔
996
                        Scope: model.AttrScopeIdentity,
2✔
997
                        Value: status,
2✔
998
                }}
2✔
999
                result, err = i.App.UpsertDevicesStatuses(ctx, devices, attrs)
2✔
1000
        case StatusDecommissioned:
1✔
1001
                // Delete Inventory
1✔
1002
                result, err = i.App.DeleteDevices(ctx, getIdsFromDevices(devices))
1✔
1003
        default:
1✔
1004
                // Unrecognized status
1✔
1005
                rest.RenderError(c,
1✔
1006
                        http.StatusNotFound,
1✔
1007
                        errors.Errorf("unrecognized status: %s", status),
1✔
1008
                )
1✔
1009
                return
1✔
1010
        }
1011
        if err == store.ErrWriteConflict {
3✔
1012
                rest.RenderError(c,
1✔
1013
                        http.StatusConflict,
1✔
1014
                        err,
1✔
1015
                )
1✔
1016
                return
1✔
1017
        } else if err != nil {
4✔
1018
                rest.RenderInternalError(c, err)
1✔
1019
                return
1✔
1020
        }
1✔
1021

1022
        c.JSON(http.StatusOK, result)
2✔
1023
}
1024

1025
func (i *InternalAPI) GetDeviceGroupsInternalHandler(c *gin.Context) {
2✔
1026
        ctx := c.Request.Context()
2✔
1027

2✔
1028
        tenantId := c.Param("tenant_id")
2✔
1029
        ctx = getTenantContext(ctx, tenantId)
2✔
1030

2✔
1031
        deviceID := c.Param("device_id")
2✔
1032
        group, err := i.App.GetDeviceGroup(ctx, model.DeviceID(deviceID))
2✔
1033
        if err != nil {
4✔
1034
                if err == store.ErrDevNotFound {
4✔
1035
                        rest.RenderError(c,
2✔
1036
                                http.StatusNotFound,
2✔
1037
                                store.ErrDevNotFound,
2✔
1038
                        )
2✔
1039
                } else {
3✔
1040
                        rest.RenderError(c,
1✔
1041
                                http.StatusInternalServerError,
1✔
1042
                                errors.New("internal error"),
1✔
1043
                        )
1✔
1044
                }
1✔
1045
                return
2✔
1046
        }
1047

1048
        res := model.DeviceGroups{}
2✔
1049
        if group != "" {
3✔
1050
                res.Groups = append(res.Groups, string(group))
1✔
1051
        }
1✔
1052

1053
        c.JSON(http.StatusOK, res)
2✔
1054
}
1055

1056
func (i *InternalAPI) ReindexDeviceDataHandler(c *gin.Context) {
2✔
1057
        ctx := c.Request.Context()
2✔
1058
        tenantId := c.Param("tenant_id")
2✔
1059
        ctx = getTenantContext(ctx, tenantId)
2✔
1060

2✔
1061
        deviceId := c.Param("device_id")
2✔
1062
        if len(deviceId) < 1 {
3✔
1063
                rest.RenderError(c,
1✔
1064
                        http.StatusBadRequest,
1✔
1065
                        errors.New("device id cannot be empty"),
1✔
1066
                )
1✔
1067
                return
1✔
1068
        }
1✔
1069

1070
        serviceName, err := utils.ParseQueryParmStr(c.Request, "service", false, nil)
2✔
1071
        // inventory service accepts only reindex requests from devicemonitor
2✔
1072
        if err != nil || serviceName != "devicemonitor" {
4✔
1073
                rest.RenderError(c,
2✔
1074
                        http.StatusBadRequest,
2✔
1075
                        errors.New("unsupported service"),
2✔
1076
                )
2✔
1077
                return
2✔
1078
        }
2✔
1079

1080
        // check devicemonitor alerts
1081
        alertsCount, err := i.App.CheckAlerts(ctx, deviceId)
1✔
1082
        if err != nil {
2✔
1083
                rest.RenderInternalError(c, err)
1✔
1084
                return
1✔
1085
        }
1✔
1086

1087
        alertsPresent := false
1✔
1088
        if alertsCount > 0 {
2✔
1089
                alertsPresent = true
1✔
1090
        }
1✔
1091
        attrs := model.DeviceAttributes{
1✔
1092
                model.DeviceAttribute{
1✔
1093
                        Name:  model.AttrNameNumberOfAlerts,
1✔
1094
                        Scope: model.AttrScopeMonitor,
1✔
1095
                        Value: alertsCount,
1✔
1096
                },
1✔
1097
                model.DeviceAttribute{
1✔
1098
                        Name:  model.AttrNameAlerts,
1✔
1099
                        Scope: model.AttrScopeMonitor,
1✔
1100
                        Value: alertsPresent,
1✔
1101
                },
1✔
1102
        }
1✔
1103

1✔
1104
        // upsert monitor attributes
1✔
1105
        err = i.App.UpsertAttributes(ctx, model.DeviceID(deviceId), attrs)
1✔
1106
        cause := errors.Cause(err)
1✔
1107
        switch cause {
1✔
1108
        case store.ErrNoAttrName:
×
UNCOV
1109
                rest.RenderError(c, http.StatusBadRequest, cause)
×
UNCOV
1110
                return
×
1111
        }
1112
        if err != nil {
2✔
1113
                rest.RenderInternalError(c, err)
1✔
1114
                return
1✔
1115
        }
1✔
1116

1117
        c.Status(http.StatusOK)
1✔
1118
}
1119

1120
func getIdsFromDevices(devices []model.DeviceUpdate) []model.DeviceID {
1✔
1121
        ids := make([]model.DeviceID, len(devices))
1✔
1122
        for i, dev := range devices {
2✔
1123
                ids[i] = dev.Id
1✔
1124
        }
1✔
1125
        return ids
1✔
1126
}
1127

1128
func parseSearchParams(c *gin.Context) (*model.SearchParams, error) {
2✔
1129
        var searchParams model.SearchParams
2✔
1130

2✔
1131
        if err := c.ShouldBindJSON(&searchParams); err != nil {
4✔
1132
                return nil, errors.Wrap(err, "failed to decode request body")
2✔
1133
        }
2✔
1134

1135
        if searchParams.Page < 1 {
4✔
1136
                searchParams.Page = utils.PageDefault
2✔
1137
        }
2✔
1138
        if searchParams.PerPage < 1 {
4✔
1139
                searchParams.PerPage = utils.PerPageDefault
2✔
1140
        }
2✔
1141

1142
        if err := searchParams.Validate(); err != nil {
4✔
1143
                return nil, err
2✔
1144
        }
2✔
1145

1146
        return &searchParams, nil
2✔
1147
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc