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

mendersoftware / mender-server / 1495380963

14 Oct 2024 03:35PM UTC coverage: 70.373% (-2.5%) from 72.904%
1495380963

Pull #101

gitlab-ci

mineralsfree
feat: tenant list added

Ticket: MEN-7568
Changelog: None

Signed-off-by: Mikita Pilinka <mikita.pilinka@northern.tech>
Pull Request #101: feat: tenant list added

4406 of 6391 branches covered (68.94%)

Branch coverage included in aggregate %.

88 of 183 new or added lines in 10 files covered. (48.09%)

2623 existing lines in 65 files now uncovered.

36673 of 51982 relevant lines covered (70.55%)

31.07 hits per line

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

78.86
/backend/services/iot-manager/model/connection_string.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

15
package model
16

17
import (
18
        "crypto/hmac"
19
        "crypto/sha256"
20
        "encoding/base64"
21
        "fmt"
22
        "net"
23
        "net/url"
24
        "reflect"
25
        "strings"
26
        "time"
27

28
        validation "github.com/go-ozzo/ozzo-validation/v4"
29
        "github.com/pkg/errors"
30

31
        "github.com/mendersoftware/mender-server/services/iot-manager/crypto"
32
)
33

34
const (
35
        csDelimiter    = ";"
36
        csVarSeparator = "="
37
        omitted        = "...<omitted>"
38

39
        csKeyHostName              = "HostName"
40
        csKeySharedAccessKey       = "SharedAccessKey"
41
        csKeySharedAccessKeyName   = "SharedAccessKeyName"
42
        csKeySharedAccessSignature = "SharedAccessSignature"
43
        csKeyDeviceId              = "DeviceId"
44
        csKeyModuleId              = "ModuleId"
45
)
46

47
var (
48
        ErrConnectionStringTooLong = errors.New(
49
                "connection string can be no longer than 4096 characters",
50
        )
51
        ErrHostnameTrust = errors.New(
52
                "hostname does not refer to a trusted domain",
53
        )
54

55
        trustedHostnames hostnameValidator
56
)
57

58
type ResolveError string
59
type lookupHost func(hostname string) error
60

UNCOV
61
var lookupHostFunc lookupHost = func(hostname string) error {
×
UNCOV
62
        _, err := net.LookupHost(hostname)
×
UNCOV
63
        return err
×
UNCOV
64
}
×
65

66
func (err ResolveError) Error() string {
×
67
        return fmt.Sprintf("failed to lookup host with name '%s'", string(err))
×
68
}
×
69

70
func SetTrustedHostnames(hostnames []string) {
1✔
71
        trustedHostnames = newHostnameValidator(hostnames)
1✔
72
}
1✔
73

74
// ConnectionString implements the Azure connection string format and the
75
// SharedAccessSignature authz algorithm.
76
// The implementation is based on the official python SDK.
77
// https://github.com/Azure/azure-iot-sdk-python
78
type ConnectionString struct {
79
        HostName        string        `cs:"HostName" bson:"hostname"`
80
        GatewayHostName string        `cs:"GatewayHostName" bson:"gateway_hostname,omitempty"`
81
        Name            string        `cs:"SharedAccessKeyName" bson:"name,omitempty"`
82
        DeviceID        string        `cs:"DeviceId" bson:"device_id,omitempty"`
83
        ModuleID        string        `cs:"ModuleId" bson:"module_id,omitempty"`
84
        Key             crypto.String `cs:"SharedAccessKey" bson:"access_key"`
85
        Signature       string        `cs:"SharedAccessSignature" bson:"-"`
86
}
87

88
func ParseConnectionString(connection string) (*ConnectionString, error) {
1✔
89
        cs := new(ConnectionString)
1✔
90
        csArgs := strings.Split(connection, csDelimiter)
1✔
91
        for _, arg := range csArgs {
2✔
92
                kv := strings.SplitN(arg, csVarSeparator, 2)
1✔
93
                if len(kv) != 2 {
2✔
94
                        return nil, errors.New("invalid connectionstring format")
1✔
95
                }
1✔
96
                switch kv[0] {
1✔
97
                case csKeyHostName:
1✔
98
                        cs.HostName = kv[1]
1✔
99
                case csKeySharedAccessKey:
1✔
100
                        key, err := base64.StdEncoding.DecodeString(kv[1])
1✔
101
                        if err != nil {
1✔
102
                                return nil, errors.Wrap(err, "shared access key format")
×
103
                        }
×
104
                        cs.Key = crypto.String(key)
1✔
UNCOV
105
                case csKeySharedAccessKeyName:
×
UNCOV
106
                        cs.Name = kv[1]
×
107
                case csKeySharedAccessSignature:
×
108
                        cs.Signature = kv[1]
×
109
                case csKeyDeviceId:
1✔
110
                        cs.DeviceID = kv[1]
1✔
111
                case csKeyModuleId:
×
112
                        cs.ModuleID = kv[1]
×
113
                default:
×
114
                        return nil, fmt.Errorf("invalid connection string key: %s", kv[0])
×
115
                }
116
        }
117
        return cs, errors.Wrap(cs.Validate(), "connection string invalid")
1✔
118
}
119

120
func (cs ConnectionString) IsZero() bool {
1✔
121
        rVal := reflect.ValueOf(cs)
1✔
122
        n := rVal.NumField()
1✔
123
        for i := 0; i < n; i++ {
2✔
124
                if !rVal.Field(i).IsZero() {
2✔
125
                        return false
1✔
126
                }
1✔
127
        }
128
        return true
×
129
}
130

131
func (cs ConnectionString) Validate() error {
1✔
132
        if cs.IsZero() {
1✔
133
                return nil
×
134
        }
×
135
        err := validation.ValidateStruct(&cs,
1✔
136
                validation.Field(&cs.HostName, validation.Required, trustedHostnames),
1✔
137
                validation.Field(&cs.Key, validation.Required),
1✔
138
                validation.Field(&cs.GatewayHostName, validation.When(
1✔
139
                        cs.GatewayHostName != "", trustedHostnames,
1✔
140
                )),
1✔
141
        )
1✔
142
        if err != nil {
2✔
143
                return err
1✔
144
        }
1✔
145
        if cs.DeviceID == "" && cs.Name == "" {
1✔
146
                return errors.New("one of 'DeviceId' or 'SharedAccessKeyName' must be set")
×
147
        }
×
148
        if len(cs.String()) > 4096 {
1✔
149
                return ErrConnectionStringTooLong
×
150
        }
×
151
        return nil
1✔
152
}
153

154
func (cs ConnectionString) Authorization(expireAt time.Time) string {
1✔
155
        qURI := url.QueryEscape(cs.HostName)
1✔
156
        msg := fmt.Sprintf("%s\n%d", qURI, expireAt.Unix())
1✔
157
        signer := hmac.New(sha256.New, []byte(cs.Key))
1✔
158
        _, _ = signer.Write([]byte(msg))
1✔
159
        sign := signer.Sum(nil)
1✔
160
        sign64 := base64.StdEncoding.EncodeToString(sign)
1✔
161
        token := fmt.Sprintf("SharedAccessSignature sr=%s&sig=%s&se=%d",
1✔
162
                qURI,
1✔
163
                url.QueryEscape(sign64),
1✔
164
                expireAt.Unix(),
1✔
165
        )
1✔
166
        if cs.Name != "" {
1✔
UNCOV
167
                token += "&skn=" + cs.Name
×
UNCOV
168
        }
×
169
        return token
1✔
170
}
171

172
func (cs ConnectionString) string(omit bool) string {
1✔
173
        val := reflect.ValueOf(cs)
1✔
174
        typ := val.Type()
1✔
175
        n := typ.NumField()
1✔
176
        var res = make([]string, 0, n)
1✔
177
        for i := 0; i < n; i++ {
2✔
178
                field := typ.Field(i)
1✔
179
                tag := field.Tag.Get("cs")
1✔
180
                if tag == "" {
1✔
181
                        continue
×
182
                }
183
                fieldVal := val.Field(i)
1✔
184
                if fieldVal.Len() == 0 {
2✔
185
                        continue
1✔
186
                }
187
                switch typ := fieldVal.Interface().(type) {
1✔
188
                case []byte:
×
189
                        res = append(res, tag+"="+base64.StdEncoding.EncodeToString(typ))
×
190
                case crypto.String:
1✔
191
                        var value string
1✔
192
                        if omit {
2✔
193
                                value = base64.StdEncoding.EncodeToString([]byte(typ[:3])) + omitted
1✔
194
                        } else {
2✔
195
                                value = base64.StdEncoding.EncodeToString([]byte(typ))
1✔
196
                        }
1✔
197
                        res = append(res, tag+"="+value)
1✔
198
                case string:
1✔
199
                        res = append(res, tag+"="+typ)
1✔
200
                default:
×
201
                        continue
×
202
                }
203
        }
204
        txt := strings.Join(res, csDelimiter)
1✔
205
        return txt
1✔
206
}
207

208
func (cs ConnectionString) String() string {
1✔
209
        return cs.string(false)
1✔
210
}
1✔
211

212
func (cs ConnectionString) MarshalText() ([]byte, error) {
1✔
213
        return []byte(cs.string(true)), nil
1✔
214
}
1✔
215

216
func (cs *ConnectionString) UnmarshalText(b []byte) error {
1✔
217
        if len(b) == 0 {
1✔
218
                return nil
×
219
        }
×
220
        connStr, err := ParseConnectionString(string(b))
1✔
221
        if err != nil {
1✔
222
                return err
×
223
        }
×
224
        *cs = *connStr
1✔
225
        return nil
1✔
226
}
227

228
type hostnameValidator [][]string
229

230
func newHostnameValidator(hostnames []string) hostnameValidator {
1✔
231
        // Compile the list of hostnames into a set of hostnames split by separator '.'
1✔
232
        var ret = make(hostnameValidator, len(hostnames))
1✔
233
        var j int
1✔
234
        for i := range hostnames {
2✔
235
                if hostnames[i] == "" {
2✔
236
                        continue
1✔
237
                }
238
                ret[i] = strings.Split(hostnames[i], ".")
1✔
239
                j++
1✔
240
        }
241
        ret = ret[:j]
1✔
242
        return ret
1✔
243
}
244

245
func (patterns hostnameValidator) matchHostname(hostname string) bool {
1✔
246
        hostname = strings.ToLower(strings.TrimSuffix(hostname, "."))
1✔
247
        if len(hostname) == 0 {
2✔
248
                return false
1✔
249
        }
1✔
250
        hostParts := strings.Split(hostname, ".")
1✔
251
        for _, patternParts := range patterns {
2✔
252
                if len(patternParts) != len(hostParts) {
2✔
253
                        continue
1✔
254
                }
255
                allMatch := true
1✔
256
                for i, patternPart := range patternParts {
2✔
257
                        if patternPart == "*" {
2✔
258
                                continue
1✔
259
                        }
260
                        if patternPart != hostParts[i] {
2✔
261
                                allMatch = false
1✔
262
                                break
1✔
263
                        }
264
                }
265
                if allMatch {
2✔
266
                        return true
1✔
267
                }
1✔
268
        }
269
        return false
1✔
270
}
271

272
func (patterns hostnameValidator) Validate(v interface{}) error {
1✔
273
        if len(patterns) == 0 {
2✔
274
                return errors.New("[PROG ERR(hostnameValidator)] no trusted hostnames configured")
1✔
275
        }
1✔
276
        hostname, ok := v.(string)
1✔
277
        if !ok {
2✔
278
                return errors.New("[PROG ERR(hostnameValidator)] validating non-string hostname")
1✔
279
        }
1✔
280
        hostname = strings.SplitN(hostname, ":", 2)[0]
1✔
281
        if !patterns.matchHostname(hostname) {
2✔
282
                return ErrHostnameTrust
1✔
283
        } else if err := lookupHostFunc(hostname); err != nil {
2✔
284
                return ResolveError(hostname)
×
285
        }
×
286
        return nil
1✔
287
}
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