-
Notifications
You must be signed in to change notification settings - Fork 0
/
devicegroup.go
316 lines (284 loc) · 10.4 KB
/
devicegroup.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
/*
* Copyright 2019 InfAI (CC SES)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package controller
import (
"context"
"errors"
"fmt"
"github.com/SENERGY-Platform/device-repository/lib/model"
"github.com/SENERGY-Platform/models/go/models"
"log"
"net/http"
"slices"
)
/////////////////////////
// api
/////////////////////////
const FilterDevicesOfGroupByAccess = true
func (this *Controller) ReadDeviceGroup(id string, token string, filterGenericDuplicateCriteria bool) (result models.DeviceGroup, err error, errCode int) {
ctx, _ := getTimeoutContext()
result, exists, err := this.db.GetDeviceGroup(ctx, id)
if err != nil {
return result, err, http.StatusInternalServerError
}
if !exists {
return result, errors.New("not found"), http.StatusNotFound
}
ok, err := this.security.CheckBool(token, this.config.DeviceGroupTopic, id, model.READ)
if err != nil {
result = models.DeviceGroup{}
return result, err, http.StatusInternalServerError
}
if !ok {
result = models.DeviceGroup{}
return result, errors.New("access denied"), http.StatusForbidden
}
//ref https://bitnify.atlassian.net/browse/SNRGY-3027
if filterGenericDuplicateCriteria {
result, err = DeviceGroupFilterGenericDuplicateCriteria(result, this.db)
if err != nil {
result = models.DeviceGroup{}
return result, err, http.StatusInternalServerError
}
}
if FilterDevicesOfGroupByAccess {
return this.FilterDevicesOfGroupByAccess(token, result)
} else {
return result, nil, http.StatusOK
}
}
func (this *Controller) FilterDevicesOfGroupByAccess(token string, group models.DeviceGroup) (result models.DeviceGroup, err error, code int) {
if len(group.DeviceIds) == 0 {
return group, nil, http.StatusOK
}
access, err := this.security.CheckMultiple(token, this.config.DeviceTopic, group.DeviceIds, model.EXECUTE)
if err != nil {
return result, err, http.StatusInternalServerError
}
result = group
result.DeviceIds = []string{}
for _, id := range group.DeviceIds {
if access[id] {
result.DeviceIds = append(result.DeviceIds, id)
} else if this.config.Debug {
log.Println("DEBUG: filtered " + id + " from result, because user lost execution access to the device")
}
}
return result, nil, http.StatusOK
}
// only the first element of group.Devices is checked.
// this should be enough because every used device should be referenced in each element of group.Devices
// use ValidateDeviceGroup() to ensure that this constraint is adhered to
func (this *Controller) CheckAccessToDevicesOfGroup(token string, group models.DeviceGroup) (err error, code int) {
if len(group.DeviceIds) == 0 {
return nil, http.StatusOK
}
access, err := this.security.CheckMultiple(token, this.config.DeviceTopic, group.DeviceIds, model.EXECUTE)
if err != nil {
return err, http.StatusInternalServerError
}
//looping one element of group.Devices is enough because ValidateDeviceGroup() ensures that every used device is referenced in each group.Devices element
for _, id := range group.DeviceIds {
if !access[id] {
return errors.New("no execution access to device " + id), http.StatusBadRequest
}
}
return nil, http.StatusOK
}
func (this *Controller) ValidateDeviceGroup(group models.DeviceGroup) (err error, code int) {
if group.Id == "" {
return errors.New("missing device-group id"), http.StatusBadRequest
}
if group.Name == "" {
return errors.New("missing device-group name"), http.StatusBadRequest
}
return this.ValidateDeviceGroupSelection(group.Criteria, group.DeviceIds)
}
func (this *Controller) ValidateDeviceGroupSelection(criteria []models.DeviceGroupFilterCriteria, devices []string) (error, int) {
deviceCache := map[string]models.Device{}
deviceTypeCache := map[string]models.DeviceType{}
deviceUsageCount := map[string]int{}
for _, c := range criteria {
deviceUsedInMapping := map[string]bool{}
for _, deviceId := range devices {
if deviceUsedInMapping[deviceId] {
return errors.New("multiple uses of device-id " + deviceId + " for the same filter-criteria"), http.StatusBadRequest
}
deviceUsedInMapping[deviceId] = true
deviceUsageCount[deviceId] = deviceUsageCount[deviceId] + 1
err, code := this.selectionMatchesCriteria(&deviceCache, &deviceTypeCache, c, deviceId)
if err != nil {
return err, code
}
}
}
return nil, http.StatusOK
}
type AspectNodeProvider interface {
ListAspectNodesByIdList(ctx context.Context, ids []string) ([]models.AspectNode, error)
}
// DeviceGroupFilterGenericDuplicateCriteria removes criteria without aspect, that are already present with an aspect
// ref: https://bitnify.atlassian.net/browse/SNRGY-3027
func DeviceGroupFilterGenericDuplicateCriteria(dg models.DeviceGroup, aspectNodeProvider AspectNodeProvider) (result models.DeviceGroup, err error) {
result = dg
//get used aspect ids
aspectIds := []string{}
for _, criteria := range result.Criteria {
if criteria.AspectId != "" && !slices.Contains(aspectIds, criteria.AspectId) {
aspectIds = append(aspectIds, criteria.AspectId)
}
}
//get used aspect nodes
aspectNodes, err := aspectNodeProvider.ListAspectNodesByIdList(context.Background(), aspectIds)
if err != nil {
return result, err
}
//prepare index of descendents of aspects
descendents := map[string][]string{}
for _, aspect := range aspectNodes {
descendents[aspect.Id] = append(descendents[aspect.Id], aspect.DescendentIds...)
}
//function to check if candidate aspect is descendent of criteria aspect
candidateUsesDescendentAspect := func(criteria models.DeviceGroupFilterCriteria, candidate models.DeviceGroupFilterCriteria) bool {
if criteria.AspectId == "" && candidate.AspectId != "" {
return true
}
if criteria.AspectId == candidate.AspectId {
return false
}
return slices.Contains(descendents[criteria.AspectId], candidate.AspectId)
}
//function to check if the candidate is a more specialized variant of criteria
isDuplicateCriteriaWithDescendentAspect := func(criteria models.DeviceGroupFilterCriteria, candidate models.DeviceGroupFilterCriteria) bool {
return candidateUsesDescendentAspect(criteria, candidate) &&
candidate.FunctionId == criteria.FunctionId &&
candidate.DeviceClassId == criteria.DeviceClassId &&
candidate.Interaction == criteria.Interaction
}
//filter criteria where more specialized variants exist
newCriteriaList := []models.DeviceGroupFilterCriteria{}
for _, criteria := range result.Criteria {
duplicateWithAspectExists := slices.ContainsFunc(result.Criteria, func(element models.DeviceGroupFilterCriteria) bool {
return isDuplicateCriteriaWithDescendentAspect(criteria, element)
})
if !duplicateWithAspectExists {
newCriteriaList = append(newCriteriaList, criteria)
continue
}
}
result.Criteria = newCriteriaList
result.SetShortCriteria()
return result, nil
}
func (this *Controller) selectionMatchesCriteria(
dcache *map[string]models.Device,
dtcache *map[string]models.DeviceType,
criteria models.DeviceGroupFilterCriteria,
deviceId string) (err error, code int) {
ctx, _ := getTimeoutContext()
var exists bool
var aspectNode models.AspectNode
if criteria.AspectId != "" {
aspectNode, exists, err = this.db.GetAspectNode(ctx, criteria.AspectId)
if err != nil {
return err, http.StatusInternalServerError
}
if !exists {
return errors.New("unknown aspect-node-id: " + criteria.AspectId), http.StatusBadRequest
}
}
device, ok := (*dcache)[deviceId]
if !ok {
device, err, code = this.readDevice(deviceId)
if err != nil {
return fmt.Errorf("unable to read device %v: %w", deviceId, err), code
}
(*dcache)[deviceId] = device
}
deviceType, ok := (*dtcache)[device.DeviceTypeId]
if !ok {
deviceType, err, code = this.readDeviceType(device.DeviceTypeId)
if err != nil {
return fmt.Errorf("unable to read device-type %v: %w", device.DeviceTypeId, err), code
}
(*dtcache)[device.DeviceTypeId] = deviceType
}
deviceClassMatches := criteria.DeviceClassId == "" || criteria.DeviceClassId == deviceType.DeviceClassId
if !deviceClassMatches {
return errors.New("device " + deviceId + " does not match device-class of filter-criteria"), http.StatusBadRequest
}
serviceMatches := false
for _, service := range deviceType.Services {
interactionMatches := service.Interaction == criteria.Interaction
if service.Interaction == models.EVENT_AND_REQUEST {
interactionMatches = true
}
contentMatches := false
for _, content := range service.Inputs {
if contentVariableContainsCriteria(content.ContentVariable, criteria, aspectNode) {
contentMatches = true
break
}
}
for _, content := range service.Outputs {
if contentVariableContainsCriteria(content.ContentVariable, criteria, aspectNode) {
contentMatches = true
break
}
}
if interactionMatches && contentMatches {
serviceMatches = true
break
}
}
if !serviceMatches {
return errors.New("no service of the device " + deviceId + " matches filter-criteria"), http.StatusBadRequest
}
return nil, http.StatusOK
}
func contentVariableContainsCriteria(variable models.ContentVariable, criteria models.DeviceGroupFilterCriteria, aspectNode models.AspectNode) bool {
if variable.FunctionId == criteria.FunctionId &&
(criteria.AspectId == "" ||
variable.AspectId == criteria.AspectId ||
listContains(aspectNode.DescendentIds, variable.AspectId)) {
return true
}
for _, sub := range variable.SubContentVariables {
if contentVariableContainsCriteria(sub, criteria, aspectNode) {
return true
}
}
return false
}
func listContains(list []string, search string) bool {
for _, element := range list {
if element == search {
return true
}
}
return false
}
/////////////////////////
// source
/////////////////////////
func (this *Controller) SetDeviceGroup(deviceGroup models.DeviceGroup, owner string) (err error) {
ctx, _ := getTimeoutContext()
return this.db.SetDeviceGroup(ctx, deviceGroup)
}
func (this *Controller) DeleteDeviceGroup(id string) error {
ctx, _ := getTimeoutContext()
return this.db.RemoveDeviceGroup(ctx, id)
}