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

mendersoftware / useradm / 1088469980

28 Nov 2023 09:52PM UTC coverage: 87.17%. First build
1088469980

Pull #394

gitlab-ci

merlin-northern
chore: private keys and key ids: tests

Changelog: Title
Ticket: MEN-6804
Signed-off-by: Peter Grzybowski <peter@northern.tech>
Pull Request #394: Men 6804 key rotation support

166 of 232 new or added lines in 9 files covered. (71.55%)

2874 of 3297 relevant lines covered (87.17%)

130.99 hits per line

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

72.14
/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
        "net/http"
18
        "os"
19
        "path"
20
        "path/filepath"
21
        "regexp"
22

23
        "github.com/ant0ine/go-json-rest/rest"
24
        "github.com/mendersoftware/go-lib-micro/config"
25
        "github.com/mendersoftware/go-lib-micro/log"
26
        "github.com/pkg/errors"
27

28
        api_http "github.com/mendersoftware/useradm/api/http"
29
        "github.com/mendersoftware/useradm/authz"
30
        "github.com/mendersoftware/useradm/client/tenant"
31
        "github.com/mendersoftware/useradm/common"
32
        . "github.com/mendersoftware/useradm/config"
33
        "github.com/mendersoftware/useradm/jwt"
34
        "github.com/mendersoftware/useradm/store/mongo"
35
        useradm "github.com/mendersoftware/useradm/user"
36
)
37

38
func SetupAPI(stacktype string, authz authz.Authorizer, jwth map[int]jwt.Handler,
39
        jwthFallback jwt.Handler) (*rest.Api, error) {
4✔
40
        api := rest.NewApi()
4✔
41
        if err := SetupMiddleware(api, stacktype, authz, jwth, jwthFallback); err != nil {
5✔
42
                return nil, errors.Wrap(err, "failed to setup middleware")
1✔
43
        }
1✔
44

45
        //this will override the framework's error resp to the desired one:
46
        // {"error": "msg"}
47
        // instead of:
48
        // {"Error": "msg"}
49
        rest.ErrorFieldName = "error"
3✔
50

3✔
51
        return api, nil
3✔
52
}
53

54
func RunServer(c config.Reader) error {
2✔
55

2✔
56
        l := log.New(log.Ctx{})
2✔
57

2✔
58
        authorizer := &SimpleAuthz{}
2✔
59

2✔
60
        // let's now go through all the existing keys and load them
2✔
61
        jwtHandlers, err := addPrivateKeys(
2✔
62
                l,
2✔
63
                filepath.Dir(c.GetString(SettingServerPrivKeyPath)),
2✔
64
                c.GetString(SettingServerPrivKeyFileNamePattern),
2✔
65
        )
2✔
66
        if err != nil {
2✔
NEW
67
                return err
×
NEW
68
        }
×
69

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

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

125
        var jwtFallbackHandler jwt.Handler
2✔
126
        fallback := c.GetString(SettingServerFallbackPrivKeyPath)
2✔
127
        if err == nil && fallback != "" {
2✔
128
                jwtFallbackHandler, err = jwt.NewJWTHandler(
×
129
                        fallback,
×
NEW
130
                        c.GetString(SettingServerPrivKeyFileNamePattern),
×
131
                )
×
132
        }
×
133
        if err != nil {
2✔
134
                return err
×
135
        }
×
136

137
        db, err := mongo.GetDataStoreMongo(dataStoreMongoConfigFromAppConfig(c))
2✔
138
        if err != nil {
2✔
139
                return errors.Wrap(err, "database connection failed")
×
140
        }
×
141

142
        ua := useradm.NewUserAdm(jwtHandlers, db,
2✔
143
                useradm.Config{
2✔
144
                        Issuer:                         c.GetString(SettingJWTIssuer),
2✔
145
                        ExpirationTimeSeconds:          int64(c.GetInt(SettingJWTExpirationTimeout)),
2✔
146
                        LimitSessionsPerUser:           c.GetInt(SettingLimitSessionsPerUser),
2✔
147
                        LimitTokensPerUser:             c.GetInt(SettingLimitTokensPerUser),
2✔
148
                        TokenLastUsedUpdateFreqMinutes: c.GetInt(SettingTokenLastUsedUpdateFreqMinutes),
2✔
149
                        PrivateKeyPath:                 c.GetString(SettingServerPrivKeyPath),
2✔
150
                        PrivateKeyFileNamePattern:      c.GetString(SettingServerPrivKeyFileNamePattern),
2✔
151
                })
2✔
152

2✔
153
        if tadmAddr := c.GetString(SettingTenantAdmAddr); tadmAddr != "" {
3✔
154
                l.Infof("settting up tenant verification")
1✔
155

1✔
156
                tc := tenant.NewClient(tenant.Config{
1✔
157
                        TenantAdmAddr: tadmAddr,
1✔
158
                })
1✔
159

1✔
160
                ua = ua.WithTenantVerification(tc)
1✔
161
        }
1✔
162

163
        useradmapi := api_http.NewUserAdmApiHandlers(ua, db, jwtHandlers,
2✔
164
                api_http.Config{
2✔
165
                        TokenMaxExpSeconds: c.GetInt(SettingTokenMaxExpirationSeconds),
2✔
166
                })
2✔
167

2✔
168
        api, err := SetupAPI(
2✔
169
                c.GetString(SettingMiddleware),
2✔
170
                authorizer,
2✔
171
                jwtHandlers,
2✔
172
                jwtFallbackHandler,
2✔
173
        )
2✔
174
        if err != nil {
2✔
175
                return errors.Wrap(err, "API setup failed")
×
176
        }
×
177

178
        apph, err := useradmapi.GetApp()
2✔
179
        if err != nil {
2✔
180
                return errors.Wrap(err, "useradm API handlers setup failed")
×
181
        }
×
182
        api.SetApp(apph)
2✔
183

2✔
184
        addr := c.GetString(SettingListen)
2✔
185
        l.Printf("listening on %s", addr)
2✔
186

2✔
187
        return http.ListenAndServe(addr, api.MakeHandler())
2✔
188
}
189

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

200
        r, err := regexp.Compile(privateKeyPattern)
3✔
201
        if err != nil {
3✔
NEW
202
                return
×
NEW
203
        }
×
204

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