go.etcd.io/etcd@v3.3.27+incompatible/etcdserver/api/v2http/client_auth.go (about) 1 // 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 15 package v2http 16 17 import ( 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 28 type authHandler struct { 29 sec auth.Store 30 cluster api.Cluster 31 clientCertAuthEnabled bool 32 } 33 34 func 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 41 func 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 60 func 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 78 func 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 109 func 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 151 func 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 164 func 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 171 func 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 179 func (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 227 func (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 243 func (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 318 type userWithRoles struct { 319 User string `json:"user"` 320 Roles []auth.Role `json:"roles,omitempty"` 321 } 322 323 type usersCollections struct { 324 Users []userWithRoles `json:"users"` 325 } 326 327 func (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 383 func (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 399 func (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 508 type enabled struct { 509 Enabled bool `json:"enabled"` 510 } 511 512 func (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 }