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

mendersoftware / mender-server / 1915348700

09 Jul 2025 07:07AM UTC coverage: 65.635% (+0.2%) from 65.484%
1915348700

Pull #781

gitlab-ci

alfrunes
test(pkg): Serialize test execution of packages for pkg/...

Signed-off-by: Alf-Rune Siqveland <alf.rune@northern.tech>
Pull Request #781: Created interface and implementation of distributed locks

34 of 42 new or added lines in 1 file covered. (80.95%)

75 existing lines in 5 files now uncovered.

32330 of 49257 relevant lines covered (65.64%)

1.39 hits per line

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

68.49
/backend/services/useradm/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
package main
15

16
import (
17
        "context"
18
        "net/http"
19
        "os"
20
        "os/signal"
21
        "path"
22
        "path/filepath"
23
        "regexp"
24
        "time"
25

26
        "github.com/pkg/errors"
27
        "golang.org/x/sys/unix"
28

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

32
        api_http "github.com/mendersoftware/mender-server/services/useradm/api/http"
33
        "github.com/mendersoftware/mender-server/services/useradm/client/tenant"
34
        "github.com/mendersoftware/mender-server/services/useradm/common"
35
        . "github.com/mendersoftware/mender-server/services/useradm/config"
36
        "github.com/mendersoftware/mender-server/services/useradm/jwt"
37
        "github.com/mendersoftware/mender-server/services/useradm/store/mongo"
38
        useradm "github.com/mendersoftware/mender-server/services/useradm/user"
39
)
40

41
func RunServer(c config.Reader) error {
2✔
42

2✔
43
        l := log.New(log.Ctx{})
2✔
44

2✔
45
        authorizer := &SimpleAuthz{}
2✔
46

2✔
47
        // let's now go through all the existing keys and load them
2✔
48
        jwtHandlers, err := addPrivateKeys(
2✔
49
                l,
2✔
50
                filepath.Dir(c.GetString(SettingServerPrivKeyPath)),
2✔
51
                c.GetString(SettingServerPrivKeyFileNamePattern),
2✔
52
        )
2✔
53
        if err != nil {
2✔
54
                return err
×
55
        }
×
56

57
        // the handler for keyId equal 0 is the one associated with the
58
        // SettingServerPrivKeyPathDefault key. it is the one serving all the previously
59
        // issued tokens (before the kid introduction in the JWTs)
60
        defaultHandler, err := jwt.NewJWTHandler(
2✔
61
                SettingServerPrivKeyPathDefault,
2✔
62
                c.GetString(SettingServerPrivKeyFileNamePattern),
2✔
63
        )
2✔
64
        if err == nil && defaultHandler != nil {
2✔
65
                // the key with id 0 is by default the default one. this allows
×
66
                // to support tokens without "kid" in the header
×
67
                // it is possible, that you rotated the default key, in which case you have to
×
68
                // set USERADM_SERVER_PRIV_KEY_PATH=/etc/useradm/rsa/private.id.2048.pem
×
69
                // where private.id.2048.pem is the new key, with new id. the new one will by default
×
70
                // be used to issue new tokens, while any other token which has id that we have
×
71
                // will be authorized against its matching key (by id from "kid" in JWT header)
×
72
                // or which does not have "kid" will be authorized against the key with id 0.
×
73
                // in other words: the key with id 0 (if not present as private.id.0.pem)
×
74
                // is the default one, and all the JWT with no "kid" in headers are being
×
75
                // checked against it.
×
76
                jwtHandlers[common.KeyIdZero] = defaultHandler
×
77
        }
×
78

79
        // if the default path is different from the currently set key path
80
        // we still have not loaded this key. this happens when the key rotation took place,
81
        // someone exported USERADM_SERVER_PRIV_KEY_PATH=path-to-a-new-key and this key
82
        // now will serve all. if we do not have this, the Login will fall back to the keyId
83
        // from the filename and either use the KeyIdZero key or fail to find the key to issue a
84
        // token if the one set in USERADM_SERVER_PRIV_KEY_PATH does have id in the filename
85
        // (but does not exist because we have not loaded it)
86
        // this also means that careless setting of USERADM_SERVER_PRIV_KEY_PATH to a key that does
87
        // not match the SettingServerPrivKeyFileNamePattern will result in
88
        // KeyIdZero handler overwrite and lack of back support for tokens signed by it.
89
        if c.GetString(SettingServerPrivKeyPath) != SettingServerPrivKeyPathDefault {
4✔
90
                defaultHandler, err = jwt.NewJWTHandler(
2✔
91
                        c.GetString(SettingServerPrivKeyPath),
2✔
92
                        c.GetString(SettingServerPrivKeyFileNamePattern),
2✔
93
                )
2✔
94
                if err == nil && defaultHandler != nil {
4✔
95
                        keyId := common.KeyIdFromPath(
2✔
96
                                c.GetString(SettingServerPrivKeyPath),
2✔
97
                                c.GetString(SettingServerPrivKeyFileNamePattern),
2✔
98
                        )
2✔
99
                        if keyId == common.KeyIdZero {
4✔
100
                                l.Warnf(
2✔
101
                                        "currently set private key %s either does not match %s pattern"+
2✔
102
                                                " or has explicitly set id=0. we are overridding the default"+
2✔
103
                                                " private key handler with id=0",
2✔
104
                                        c.GetString(SettingServerPrivKeyPath),
2✔
105
                                        c.GetString(SettingServerPrivKeyFileNamePattern),
2✔
106
                                )
2✔
107
                        }
2✔
108
                        jwtHandlers[keyId] = defaultHandler
2✔
109
                }
110
        }
111

112
        var jwtFallbackHandler jwt.Handler
2✔
113
        fallback := c.GetString(SettingServerFallbackPrivKeyPath)
2✔
114
        if err == nil && fallback != "" {
2✔
115
                jwtFallbackHandler, err = jwt.NewJWTHandler(
×
116
                        fallback,
×
117
                        c.GetString(SettingServerPrivKeyFileNamePattern),
×
118
                )
×
119
        }
×
120
        if err != nil {
2✔
121
                return err
×
122
        }
×
123

124
        db, err := mongo.GetDataStoreMongo(dataStoreMongoConfigFromAppConfig(c))
2✔
125
        if err != nil {
2✔
126
                return errors.Wrap(err, "database connection failed")
×
127
        }
×
128

129
        ua := useradm.NewUserAdm(jwtHandlers, db,
2✔
130
                useradm.Config{
2✔
131
                        Issuer:                         c.GetString(SettingJWTIssuer),
2✔
132
                        ExpirationTimeSeconds:          int64(c.GetInt(SettingJWTExpirationTimeout)),
2✔
133
                        LimitSessionsPerUser:           c.GetInt(SettingLimitSessionsPerUser),
2✔
134
                        LimitTokensPerUser:             c.GetInt(SettingLimitTokensPerUser),
2✔
135
                        TokenLastUsedUpdateFreqMinutes: c.GetInt(SettingTokenLastUsedUpdateFreqMinutes),
2✔
136
                        PrivateKeyPath:                 c.GetString(SettingServerPrivKeyPath),
2✔
137
                        PrivateKeyFileNamePattern:      c.GetString(SettingServerPrivKeyFileNamePattern),
2✔
138
                })
2✔
139

2✔
140
        if tadmAddr := c.GetString(SettingTenantAdmAddr); tadmAddr != "" {
2✔
141
                l.Infof("settting up tenant verification")
×
142

×
143
                tc := tenant.NewClient(tenant.Config{
×
144
                        TenantAdmAddr: tadmAddr,
×
145
                })
×
146

×
147
                ua = ua.WithTenantVerification(tc)
×
148
        }
×
149

150
        useradmapi := api_http.NewUserAdmApiHandlers(ua, db, jwtHandlers,
2✔
151
                api_http.Config{
2✔
152
                        TokenMaxExpSeconds: c.GetInt(SettingTokenMaxExpirationSeconds),
2✔
153
                        JWTFallback:        jwtFallbackHandler,
2✔
154
                })
2✔
155

2✔
156
        handler, err := useradmapi.Build(authorizer)
2✔
157
        if err != nil {
2✔
UNCOV
158
                return errors.Wrap(err, "useradm API handlers setup failed")
×
UNCOV
159
        }
×
160

161
        addr := c.GetString(SettingListen)
2✔
162
        l.Printf("listening on %s", addr)
2✔
163

2✔
164
        srv := &http.Server{
2✔
165
                Addr:    addr,
2✔
166
                Handler: handler,
2✔
167
        }
2✔
168

2✔
169
        errChan := make(chan error, 1)
2✔
170
        go func() {
4✔
171
                if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
2✔
UNCOV
172
                        errChan <- err
×
UNCOV
173
                }
×
174
        }()
175
        quit := make(chan os.Signal, 1)
2✔
176
        signal.Notify(quit, unix.SIGINT, unix.SIGTERM)
2✔
177
        select {
2✔
178
        case sig := <-quit:
2✔
179
                l.Infof("received signal %s: terminating", sig)
2✔
UNCOV
180
        case err := <-errChan:
×
UNCOV
181
                l.Errorf("server terminated unexpectedly: %s", err.Error())
×
UNCOV
182
                return err
×
183
        }
184
        l.Info("server shutdown")
2✔
185
        ctxWithTimeout, cancel := context.WithTimeout(context.Background(), 5*time.Second)
2✔
186
        defer cancel()
2✔
187
        if err := srv.Shutdown(ctxWithTimeout); err != nil {
2✔
UNCOV
188
                l.Error("error when shutting down the server ", err)
×
UNCOV
189
        }
×
190
        return nil
2✔
191
}
192

193
func addPrivateKeys(
194
        l *log.Logger,
195
        privateKeysDirectory string,
196
        privateKeyPattern string,
197
) (handlers map[int]jwt.Handler, err error) {
3✔
198
        files, err := os.ReadDir(privateKeysDirectory)
3✔
199
        if err != nil {
3✔
UNCOV
200
                return
×
UNCOV
201
        }
×
202

203
        r, err := regexp.Compile(privateKeyPattern)
3✔
204
        if err != nil {
3✔
UNCOV
205
                return
×
UNCOV
206
        }
×
207

208
        handlers = make(map[int]jwt.Handler, len(files))
3✔
209
        for _, fileEntry := range files {
6✔
210
                if r.MatchString(fileEntry.Name()) {
4✔
211
                        keyPath := path.Join(privateKeysDirectory, fileEntry.Name())
1✔
212
                        handler, err := jwt.NewJWTHandler(keyPath, privateKeyPattern)
1✔
213
                        if err != nil {
1✔
UNCOV
214
                                continue
×
215
                        }
216
                        keyId := common.KeyIdFromPath(keyPath, privateKeyPattern)
1✔
217
                        l.Infof("loaded private key id=%d from %s", keyId, keyPath)
1✔
218
                        handlers[keyId] = handler
1✔
219
                }
220
        }
221
        return handlers, nil
3✔
222
}
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