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