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

mendersoftware / mender-server / 1550767116

19 Nov 2024 11:58AM UTC coverage: 72.563% (-0.2%) from 72.771%
1550767116

Pull #202

gitlab-ci

alfrunes
test: Fix deviceauth.cache tests after changing client initialization

Signed-off-by: Alf-Rune Siqveland <alf.rune@northern.tech>
Pull Request #202: MEN-7733: Rate limits for devices APIs

4174 of 6072 branches covered (68.74%)

Branch coverage included in aggregate %.

135 of 380 new or added lines in 8 files covered. (35.53%)

84 existing lines in 3 files now uncovered.

42593 of 58378 relevant lines covered (72.96%)

15.45 hits per line

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

34.97
/backend/services/deviceauth/server.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 main
16

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

26
        "github.com/pkg/errors"
27

28
        "github.com/mendersoftware/mender-server/pkg/config"
29
        "github.com/mendersoftware/mender-server/pkg/log"
30
        "github.com/mendersoftware/mender-server/pkg/redis"
31

32
        api_http "github.com/mendersoftware/mender-server/services/deviceauth/api/http"
33
        "github.com/mendersoftware/mender-server/services/deviceauth/cache"
34
        "github.com/mendersoftware/mender-server/services/deviceauth/client/orchestrator"
35
        "github.com/mendersoftware/mender-server/services/deviceauth/client/tenant"
36
        dconfig "github.com/mendersoftware/mender-server/services/deviceauth/config"
37
        "github.com/mendersoftware/mender-server/services/deviceauth/devauth"
38
        "github.com/mendersoftware/mender-server/services/deviceauth/jwt"
39
        "github.com/mendersoftware/mender-server/services/deviceauth/store/mongo"
40
)
41

42
func RunServer(c config.Reader) error {
×
43
        var tenantadmAddr = c.GetString(dconfig.SettingTenantAdmAddr)
×
44

×
45
        l := log.New(log.Ctx{})
×
46

×
47
        db, err := mongo.NewDataStoreMongo(
×
48
                mongo.DataStoreMongoConfig{
×
49
                        ConnectionString: c.GetString(dconfig.SettingDb),
×
50

×
51
                        SSL:           c.GetBool(dconfig.SettingDbSSL),
×
52
                        SSLSkipVerify: c.GetBool(dconfig.SettingDbSSLSkipVerify),
×
53

×
54
                        Username: c.GetString(dconfig.SettingDbUsername),
×
55
                        Password: c.GetString(dconfig.SettingDbPassword),
×
56
                })
×
57
        if err != nil {
×
58
                return errors.Wrap(err, "database connection failed")
×
59
        }
×
60

61
        jwtHandler, err := jwt.NewJWTHandler(
×
62
                c.GetString(dconfig.SettingServerPrivKeyPath),
×
63
        )
×
64
        var jwtFallbackHandler jwt.Handler
×
65
        fallback := c.GetString(dconfig.SettingServerFallbackPrivKeyPath)
×
66
        if err == nil && fallback != "" {
×
67
                jwtFallbackHandler, err = jwt.NewJWTHandler(
×
68
                        fallback,
×
69
                )
×
70
        }
×
71
        if err != nil {
×
72
                return err
×
73
        }
×
74

75
        orchClientConf := orchestrator.Config{
×
76
                OrchestratorAddr: c.GetString(dconfig.SettingOrchestratorAddr),
×
77
                Timeout:          time.Duration(30) * time.Second,
×
78
        }
×
79

×
80
        devauth := devauth.NewDevAuth(db,
×
81
                orchestrator.NewClient(orchClientConf),
×
82
                jwtHandler,
×
83
                devauth.Config{
×
84
                        Issuer:             c.GetString(dconfig.SettingJWTIssuer),
×
85
                        ExpirationTime:     int64(c.GetInt(dconfig.SettingJWTExpirationTimeout)),
×
86
                        DefaultTenantToken: c.GetString(dconfig.SettingDefaultTenantToken),
×
87
                        InventoryAddr:      config.Config.GetString(dconfig.SettingInventoryAddr),
×
88

×
89
                        EnableReporting: config.Config.GetBool(dconfig.SettingEnableReporting),
×
90
                        HaveAddons: config.Config.GetBool(dconfig.SettingHaveAddons) &&
×
91
                                tenantadmAddr != "",
×
92
                })
×
93

×
94
        if jwtFallbackHandler != nil {
×
95
                devauth = devauth.WithJWTFallbackHandler(jwtFallbackHandler)
×
96
        }
×
97

98
        if tenantadmAddr != "" {
×
99
                tc := tenant.NewClient(tenant.Config{
×
100
                        TenantAdmAddr: tenantadmAddr,
×
101
                })
×
102
                devauth = devauth.WithTenantVerification(tc)
×
103
        }
×
104

105
        cacheConnStr := c.GetString(dconfig.SettingRedisConnectionString)
×
106
        if cacheConnStr == "" {
×
107
                // for backward compatibility check old redis_addr setting
×
108
                cacheConnStr = c.GetString(dconfig.SettingRedisAddr)
×
109
        }
×
110
        if cacheConnStr != "" {
×
111
                l.Infof("setting up redis cache")
×
112

×
NEW
113
                ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
×
NEW
114
                redisClient, err := redis.ClientFromConnectionString(ctx, cacheConnStr)
×
NEW
115
                cancel()
×
116
                if err != nil {
×
NEW
117
                        return fmt.Errorf("failed to initialize redis client: %w", err)
×
118
                }
×
119

NEW
120
                redisKeyPrefix := c.GetString(dconfig.SettingRedisKeyPrefix)
×
NEW
UNCOV
121
                cache := cache.NewRedisCache(
×
NEW
UNCOV
122
                        redisClient,
×
NEW
123
                        redisKeyPrefix,
×
NEW
124
                        c.GetInt(dconfig.SettingRedisLimitsExpSec),
×
NEW
125
                )
×
126
                devauth = devauth.WithCache(cache)
×
NEW
127
                err = setupRatelimits(c, devauth, redisKeyPrefix, redisClient)
×
NEW
128
                if err != nil {
×
NEW
129
                        return fmt.Errorf("error configuring rate limits: %w", err)
×
NEW
130
                }
×
131
        }
132

133
        devauthapi := api_http.NewDevAuthApiHandlers(devauth, db)
×
134

×
135
        apiHandler, err := devauthapi.Build()
×
136
        if err != nil {
×
137
                return errors.Wrap(err, "device authentication API handlers setup failed")
×
138
        }
×
139

140
        addr := c.GetString(dconfig.SettingListen)
×
141
        l.Printf("listening on %s", addr)
×
142

×
143
        return http.ListenAndServe(addr, apiHandler)
×
144
}
145

146
func setupRatelimits(
147
        c config.Reader,
148
        devauth *devauth.DevAuth,
149
        redisKeyPrefix string,
150
        redisClient redis.Client,
151
) error {
1✔
152
        if !c.IsSet(dconfig.SettingRatelimitsQuotas) {
2✔
153
                return nil
1✔
154
        }
1✔
155
        quotas := make(map[string]float64)
1✔
156
        // quotas can be given as either "plan=quota plan2=quota2"
1✔
157
        // or as a map of string -> float64
1✔
158
        // Only the former can be backed by environment variables
1✔
159
        quotaSlice := c.GetStringSlice(dconfig.SettingRatelimitsQuotas)
1✔
160
        if len(quotaSlice) > 0 {
2✔
161
                for i, keyValue := range quotaSlice {
2✔
162
                        key, value, ok := strings.Cut(keyValue, "=")
1✔
163
                        if !ok {
2✔
164
                                return fmt.Errorf(
1✔
165
                                        `invalid config %s: value %v item #%d: missing key/value separator '='`,
1✔
166
                                        dconfig.SettingRatelimitsQuotas, quotaSlice, i+1,
1✔
167
                                )
1✔
168
                        }
1✔
169
                        valueF64, err := strconv.ParseFloat(value, 64)
1✔
170
                        if err != nil {
2✔
171
                                return fmt.Errorf("error parsing quota value: %w", err)
1✔
172
                        }
1✔
173
                        quotas[key] = valueF64
1✔
174
                }
175
        } else {
1✔
176
                // Check for map in config file
1✔
177
                quotaMap := c.GetStringMap(dconfig.SettingRatelimitsQuotas)
1✔
178
                if len(quotaMap) == 0 {
2✔
179
                        return fmt.Errorf(
1✔
180
                                "invalid config value %s: cannot be empty",
1✔
181
                                dconfig.SettingRatelimitsQuotas)
1✔
182
                }
1✔
183
                for key, valueAny := range quotaMap {
2✔
184
                        rVal := reflect.ValueOf(valueAny)
1✔
185
                        if rVal.CanFloat() {
2✔
186
                                quotas[key] = rVal.Float()
1✔
187
                        } else if rVal.CanInt() {
3✔
188
                                quotas[key] = float64(rVal.Int())
1✔
189
                        } else if rVal.CanUint() {
3✔
190
                                quotas[key] = float64(rVal.Uint())
1✔
191
                        } else {
2✔
192
                                return fmt.Errorf(
1✔
193
                                        "invalid config value %s[%s]: not a numeric value",
1✔
194
                                        dconfig.SettingRatelimitsQuotas, key,
1✔
195
                                )
1✔
196
                        }
1✔
197
                }
198
        }
199
        for key := range quotas {
2✔
200
                if quotas[key] < 0.0 {
2✔
201
                        return fmt.Errorf("invalid config value %s[%s]: value must be a positive value",
1✔
202
                                dconfig.SettingRatelimitsQuotas, key)
1✔
203
                }
1✔
204
        }
205
        log.NewEmpty().Infof("using rate limit quotas: %v", quotas)
1✔
206

1✔
207
        interval := c.GetDuration(dconfig.SettingRatelimitsInterval)
1✔
208
        rateLimiter := redis.NewFixedWindowRateLimiter(redisClient,
1✔
209
                func(ctx context.Context) (*redis.RatelimitParams, error) {
1✔
NEW
210
                        limit, eventID, err := devauth.RateLimitsFromContext(ctx)
×
NEW
211
                        if err != nil {
×
NEW
212
                                return nil, err
×
NEW
213
                        } else if limit < 0 {
×
NEW
214
                                return nil, nil
×
NEW
215
                        }
×
NEW
216
                        keyPrefix := redisKeyPrefix + ":" + eventID
×
NEW
217
                        return &redis.RatelimitParams{
×
NEW
218
                                Burst:     uint64(limit),
×
NEW
219
                                Interval:  interval,
×
NEW
220
                                KeyPrefix: keyPrefix,
×
NEW
221
                        }, nil
×
222
                },
223
        )
224
        devauth.WithRatelimits(rateLimiter, quotas)
1✔
225
        return nil
1✔
226
}
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