1
2
3
4
5
6
7
8
9
10
11
12
13
14
15 package v2http
16
17 import (
18 "encoding/json"
19 "net/http"
20 "path"
21 "strings"
22
23 "go.etcd.io/etcd/server/v3/etcdserver/api"
24 "go.etcd.io/etcd/server/v3/etcdserver/api/v2auth"
25 "go.etcd.io/etcd/server/v3/etcdserver/api/v2http/httptypes"
26
27 "go.uber.org/zap"
28 )
29
30 type authHandler struct {
31 lg *zap.Logger
32 sec v2auth.Store
33 cluster api.Cluster
34 clientCertAuthEnabled bool
35 }
36
37 func hasWriteRootAccess(lg *zap.Logger, sec v2auth.Store, r *http.Request, clientCertAuthEnabled bool) bool {
38 if r.Method == "GET" || r.Method == "HEAD" {
39 return true
40 }
41 return hasRootAccess(lg, sec, r, clientCertAuthEnabled)
42 }
43
44 func userFromBasicAuth(lg *zap.Logger, sec v2auth.Store, r *http.Request) *v2auth.User {
45 username, password, ok := r.BasicAuth()
46 if !ok {
47 lg.Warn("malformed basic auth encoding")
48 return nil
49 }
50 user, err := sec.GetUser(username)
51 if err != nil {
52 return nil
53 }
54
55 ok = sec.CheckPassword(user, password)
56 if !ok {
57 lg.Warn("incorrect password", zap.String("user-name", username))
58 return nil
59 }
60 return &user
61 }
62
63 func userFromClientCertificate(lg *zap.Logger, sec v2auth.Store, r *http.Request) *v2auth.User {
64 if r.TLS == nil {
65 return nil
66 }
67
68 for _, chains := range r.TLS.VerifiedChains {
69 for _, chain := range chains {
70 lg.Debug("found common name", zap.String("common-name", chain.Subject.CommonName))
71 user, err := sec.GetUser(chain.Subject.CommonName)
72 if err == nil {
73 lg.Debug(
74 "authenticated a user via common name",
75 zap.String("user-name", user.User),
76 zap.String("common-name", chain.Subject.CommonName),
77 )
78 return &user
79 }
80 }
81 }
82 return nil
83 }
84
85 func hasRootAccess(lg *zap.Logger, sec v2auth.Store, r *http.Request, clientCertAuthEnabled bool) bool {
86 if sec == nil {
87
88 return true
89 }
90 if !sec.AuthEnabled() {
91 return true
92 }
93
94 var rootUser *v2auth.User
95 if r.Header.Get("Authorization") == "" && clientCertAuthEnabled {
96 rootUser = userFromClientCertificate(lg, sec, r)
97 if rootUser == nil {
98 return false
99 }
100 } else {
101 rootUser = userFromBasicAuth(lg, sec, r)
102 if rootUser == nil {
103 return false
104 }
105 }
106
107 for _, role := range rootUser.Roles {
108 if role == v2auth.RootRoleName {
109 return true
110 }
111 }
112
113 lg.Warn(
114 "a user does not have root role for resource",
115 zap.String("root-user", rootUser.User),
116 zap.String("root-role-name", v2auth.RootRoleName),
117 zap.String("resource-path", r.URL.Path),
118 )
119 return false
120 }
121
122 func hasKeyPrefixAccess(lg *zap.Logger, sec v2auth.Store, r *http.Request, key string, recursive, clientCertAuthEnabled bool) bool {
123 if sec == nil {
124
125 return true
126 }
127 if !sec.AuthEnabled() {
128 return true
129 }
130
131 var user *v2auth.User
132 if r.Header.Get("Authorization") == "" {
133 if clientCertAuthEnabled {
134 user = userFromClientCertificate(lg, sec, r)
135 }
136 if user == nil {
137 return hasGuestAccess(lg, sec, r, key)
138 }
139 } else {
140 user = userFromBasicAuth(lg, sec, r)
141 if user == nil {
142 return false
143 }
144 }
145
146 writeAccess := r.Method != "GET" && r.Method != "HEAD"
147 for _, roleName := range user.Roles {
148 role, err := sec.GetRole(roleName)
149 if err != nil {
150 continue
151 }
152 if recursive {
153 if role.HasRecursiveAccess(key, writeAccess) {
154 return true
155 }
156 } else if role.HasKeyAccess(key, writeAccess) {
157 return true
158 }
159 }
160
161 lg.Warn(
162 "invalid access for user on key",
163 zap.String("user-name", user.User),
164 zap.String("key", key),
165 )
166 return false
167 }
168
169 func hasGuestAccess(lg *zap.Logger, sec v2auth.Store, r *http.Request, key string) bool {
170 writeAccess := r.Method != "GET" && r.Method != "HEAD"
171 role, err := sec.GetRole(v2auth.GuestRoleName)
172 if err != nil {
173 return false
174 }
175 if role.HasKeyAccess(key, writeAccess) {
176 return true
177 }
178
179 lg.Warn(
180 "invalid access for a guest role on key",
181 zap.String("role-name", v2auth.GuestRoleName),
182 zap.String("key", key),
183 )
184 return false
185 }
186
187 func writeNoAuth(lg *zap.Logger, w http.ResponseWriter, r *http.Request) {
188 herr := httptypes.NewHTTPError(http.StatusUnauthorized, "Insufficient credentials")
189 if err := herr.WriteTo(w); err != nil {
190 lg.Debug(
191 "failed to write v2 HTTP error",
192 zap.String("remote-addr", r.RemoteAddr),
193 zap.Error(err),
194 )
195 }
196 }
197
198 func handleAuth(mux *http.ServeMux, sh *authHandler) {
199 mux.HandleFunc(authPrefix+"/roles", authCapabilityHandler(sh.baseRoles))
200 mux.HandleFunc(authPrefix+"/roles/", authCapabilityHandler(sh.handleRoles))
201 mux.HandleFunc(authPrefix+"/users", authCapabilityHandler(sh.baseUsers))
202 mux.HandleFunc(authPrefix+"/users/", authCapabilityHandler(sh.handleUsers))
203 mux.HandleFunc(authPrefix+"/enable", authCapabilityHandler(sh.enableDisable))
204 }
205
206 func (sh *authHandler) baseRoles(w http.ResponseWriter, r *http.Request) {
207 if !allowMethod(w, r.Method, "GET") {
208 return
209 }
210 if !hasRootAccess(sh.lg, sh.sec, r, sh.clientCertAuthEnabled) {
211 writeNoAuth(sh.lg, w, r)
212 return
213 }
214
215 w.Header().Set("X-Etcd-Cluster-ID", sh.cluster.ID().String())
216 w.Header().Set("Content-Type", "application/json")
217
218 roles, err := sh.sec.AllRoles()
219 if err != nil {
220 writeError(sh.lg, w, r, err)
221 return
222 }
223 if roles == nil {
224 roles = make([]string, 0)
225 }
226
227 err = r.ParseForm()
228 if err != nil {
229 writeError(sh.lg, w, r, err)
230 return
231 }
232
233 var rolesCollections struct {
234 Roles []v2auth.Role `json:"roles"`
235 }
236 for _, roleName := range roles {
237 var role v2auth.Role
238 role, err = sh.sec.GetRole(roleName)
239 if err != nil {
240 writeError(sh.lg, w, r, err)
241 return
242 }
243 rolesCollections.Roles = append(rolesCollections.Roles, role)
244 }
245 err = json.NewEncoder(w).Encode(rolesCollections)
246
247 if err != nil {
248 sh.lg.Warn(
249 "failed to encode base roles",
250 zap.String("url", r.URL.String()),
251 zap.Error(err),
252 )
253 writeError(sh.lg, w, r, err)
254 return
255 }
256 }
257
258 func (sh *authHandler) handleRoles(w http.ResponseWriter, r *http.Request) {
259 subpath := path.Clean(r.URL.Path[len(authPrefix):])
260
261
262 pieces := strings.Split(subpath, "/")
263 if len(pieces) == 2 {
264 sh.baseRoles(w, r)
265 return
266 }
267 if len(pieces) != 3 {
268 writeError(sh.lg, w, r, httptypes.NewHTTPError(http.StatusBadRequest, "Invalid path"))
269 return
270 }
271 sh.forRole(w, r, pieces[2])
272 }
273
274 func (sh *authHandler) forRole(w http.ResponseWriter, r *http.Request, role string) {
275 if !allowMethod(w, r.Method, "GET", "PUT", "DELETE") {
276 return
277 }
278 if !hasRootAccess(sh.lg, sh.sec, r, sh.clientCertAuthEnabled) {
279 writeNoAuth(sh.lg, w, r)
280 return
281 }
282 w.Header().Set("X-Etcd-Cluster-ID", sh.cluster.ID().String())
283 w.Header().Set("Content-Type", "application/json")
284
285 switch r.Method {
286 case "GET":
287 data, err := sh.sec.GetRole(role)
288 if err != nil {
289 writeError(sh.lg, w, r, err)
290 return
291 }
292 err = json.NewEncoder(w).Encode(data)
293 if err != nil {
294 sh.lg.Warn(
295 "failed to encode a role",
296 zap.String("url", r.URL.String()),
297 zap.Error(err),
298 )
299 return
300 }
301 return
302
303 case "PUT":
304 var in v2auth.Role
305 err := json.NewDecoder(r.Body).Decode(&in)
306 if err != nil {
307 writeError(sh.lg, w, r, httptypes.NewHTTPError(http.StatusBadRequest, "Invalid JSON in request body."))
308 return
309 }
310 if in.Role != role {
311 writeError(sh.lg, w, r, httptypes.NewHTTPError(http.StatusBadRequest, "Role JSON name does not match the name in the URL"))
312 return
313 }
314
315 var out v2auth.Role
316
317
318 if in.Grant.IsEmpty() && in.Revoke.IsEmpty() {
319 err = sh.sec.CreateRole(in)
320 if err != nil {
321 writeError(sh.lg, w, r, err)
322 return
323 }
324 w.WriteHeader(http.StatusCreated)
325 out = in
326 } else {
327 if !in.Permissions.IsEmpty() {
328 writeError(sh.lg, w, r, httptypes.NewHTTPError(http.StatusBadRequest, "Role JSON contains both permissions and grant/revoke"))
329 return
330 }
331 out, err = sh.sec.UpdateRole(in)
332 if err != nil {
333 writeError(sh.lg, w, r, err)
334 return
335 }
336 w.WriteHeader(http.StatusOK)
337 }
338
339 err = json.NewEncoder(w).Encode(out)
340 if err != nil {
341 sh.lg.Warn(
342 "failed to encode a role",
343 zap.String("url", r.URL.String()),
344 zap.Error(err),
345 )
346 return
347 }
348 return
349
350 case "DELETE":
351 err := sh.sec.DeleteRole(role)
352 if err != nil {
353 writeError(sh.lg, w, r, err)
354 return
355 }
356 }
357 }
358
359 type userWithRoles struct {
360 User string `json:"user"`
361 Roles []v2auth.Role `json:"roles,omitempty"`
362 }
363
364 type usersCollections struct {
365 Users []userWithRoles `json:"users"`
366 }
367
368 func (sh *authHandler) baseUsers(w http.ResponseWriter, r *http.Request) {
369 if !allowMethod(w, r.Method, "GET") {
370 return
371 }
372 if !hasRootAccess(sh.lg, sh.sec, r, sh.clientCertAuthEnabled) {
373 writeNoAuth(sh.lg, w, r)
374 return
375 }
376 w.Header().Set("X-Etcd-Cluster-ID", sh.cluster.ID().String())
377 w.Header().Set("Content-Type", "application/json")
378
379 users, err := sh.sec.AllUsers()
380 if err != nil {
381 writeError(sh.lg, w, r, err)
382 return
383 }
384 if users == nil {
385 users = make([]string, 0)
386 }
387
388 err = r.ParseForm()
389 if err != nil {
390 writeError(sh.lg, w, r, err)
391 return
392 }
393
394 ucs := usersCollections{}
395 for _, userName := range users {
396 var user v2auth.User
397 user, err = sh.sec.GetUser(userName)
398 if err != nil {
399 writeError(sh.lg, w, r, err)
400 return
401 }
402
403 uwr := userWithRoles{User: user.User}
404 for _, roleName := range user.Roles {
405 var role v2auth.Role
406 role, err = sh.sec.GetRole(roleName)
407 if err != nil {
408 continue
409 }
410 uwr.Roles = append(uwr.Roles, role)
411 }
412
413 ucs.Users = append(ucs.Users, uwr)
414 }
415 err = json.NewEncoder(w).Encode(ucs)
416
417 if err != nil {
418 sh.lg.Warn(
419 "failed to encode users",
420 zap.String("url", r.URL.String()),
421 zap.Error(err),
422 )
423 writeError(sh.lg, w, r, err)
424 return
425 }
426 }
427
428 func (sh *authHandler) handleUsers(w http.ResponseWriter, r *http.Request) {
429 subpath := path.Clean(r.URL.Path[len(authPrefix):])
430
431
432 pieces := strings.Split(subpath, "/")
433 if len(pieces) == 2 {
434 sh.baseUsers(w, r)
435 return
436 }
437 if len(pieces) != 3 {
438 writeError(sh.lg, w, r, httptypes.NewHTTPError(http.StatusBadRequest, "Invalid path"))
439 return
440 }
441 sh.forUser(w, r, pieces[2])
442 }
443
444 func (sh *authHandler) forUser(w http.ResponseWriter, r *http.Request, user string) {
445 if !allowMethod(w, r.Method, "GET", "PUT", "DELETE") {
446 return
447 }
448 if !hasRootAccess(sh.lg, sh.sec, r, sh.clientCertAuthEnabled) {
449 writeNoAuth(sh.lg, w, r)
450 return
451 }
452 w.Header().Set("X-Etcd-Cluster-ID", sh.cluster.ID().String())
453 w.Header().Set("Content-Type", "application/json")
454
455 switch r.Method {
456 case "GET":
457 u, err := sh.sec.GetUser(user)
458 if err != nil {
459 writeError(sh.lg, w, r, err)
460 return
461 }
462
463 err = r.ParseForm()
464 if err != nil {
465 writeError(sh.lg, w, r, err)
466 return
467 }
468
469 uwr := userWithRoles{User: u.User}
470 for _, roleName := range u.Roles {
471 var role v2auth.Role
472 role, err = sh.sec.GetRole(roleName)
473 if err != nil {
474 writeError(sh.lg, w, r, err)
475 return
476 }
477 uwr.Roles = append(uwr.Roles, role)
478 }
479 err = json.NewEncoder(w).Encode(uwr)
480
481 if err != nil {
482 sh.lg.Warn(
483 "failed to encode roles",
484 zap.String("url", r.URL.String()),
485 zap.Error(err),
486 )
487 return
488 }
489 return
490
491 case "PUT":
492 var u v2auth.User
493 err := json.NewDecoder(r.Body).Decode(&u)
494 if err != nil {
495 writeError(sh.lg, w, r, httptypes.NewHTTPError(http.StatusBadRequest, "Invalid JSON in request body."))
496 return
497 }
498 if u.User != user {
499 writeError(sh.lg, w, r, httptypes.NewHTTPError(http.StatusBadRequest, "User JSON name does not match the name in the URL"))
500 return
501 }
502
503 var (
504 out v2auth.User
505 created bool
506 )
507
508 if len(u.Grant) == 0 && len(u.Revoke) == 0 {
509
510 if len(u.Roles) != 0 {
511 out, err = sh.sec.CreateUser(u)
512 } else {
513
514
515 out, created, err = sh.sec.CreateOrUpdateUser(u)
516 }
517
518 if err != nil {
519 writeError(sh.lg, w, r, err)
520 return
521 }
522 } else {
523
524 if len(u.Roles) != 0 {
525 writeError(sh.lg, w, r, httptypes.NewHTTPError(http.StatusBadRequest, "User JSON contains both roles and grant/revoke"))
526 return
527 }
528 out, err = sh.sec.UpdateUser(u)
529 if err != nil {
530 writeError(sh.lg, w, r, err)
531 return
532 }
533 }
534
535 if created {
536 w.WriteHeader(http.StatusCreated)
537 } else {
538 w.WriteHeader(http.StatusOK)
539 }
540
541 out.Password = ""
542
543 err = json.NewEncoder(w).Encode(out)
544 if err != nil {
545 sh.lg.Warn(
546 "failed to encode a user",
547 zap.String("url", r.URL.String()),
548 zap.Error(err),
549 )
550 return
551 }
552 return
553
554 case "DELETE":
555 err := sh.sec.DeleteUser(user)
556 if err != nil {
557 writeError(sh.lg, w, r, err)
558 return
559 }
560 }
561 }
562
563 type enabled struct {
564 Enabled bool `json:"enabled"`
565 }
566
567 func (sh *authHandler) enableDisable(w http.ResponseWriter, r *http.Request) {
568 if !allowMethod(w, r.Method, "GET", "PUT", "DELETE") {
569 return
570 }
571 if !hasWriteRootAccess(sh.lg, sh.sec, r, sh.clientCertAuthEnabled) {
572 writeNoAuth(sh.lg, w, r)
573 return
574 }
575 w.Header().Set("X-Etcd-Cluster-ID", sh.cluster.ID().String())
576 w.Header().Set("Content-Type", "application/json")
577 isEnabled := sh.sec.AuthEnabled()
578 switch r.Method {
579 case "GET":
580 jsonDict := enabled{isEnabled}
581 err := json.NewEncoder(w).Encode(jsonDict)
582 if err != nil {
583 sh.lg.Warn(
584 "failed to encode a auth state",
585 zap.String("url", r.URL.String()),
586 zap.Error(err),
587 )
588 }
589
590 case "PUT":
591 err := sh.sec.EnableAuth()
592 if err != nil {
593 writeError(sh.lg, w, r, err)
594 return
595 }
596
597 case "DELETE":
598 err := sh.sec.DisableAuth()
599 if err != nil {
600 writeError(sh.lg, w, r, err)
601 return
602 }
603 }
604 }
605
View as plain text