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