etcd/etcdserver/etcdhttp/client_security.go
Xiang Li 253f7c4ae1 Merge pull request #2522 from xiang90/user_pw
etcdserver/etcdhttp: do not return back the password of a user
2015-03-30 13:42:41 -07:00

357 lines
8.8 KiB
Go

// Copyright 2015 CoreOS, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package etcdhttp
import (
"encoding/json"
"log"
"net/http"
"path"
"strings"
"github.com/coreos/etcd/etcdserver"
"github.com/coreos/etcd/etcdserver/etcdhttp/httptypes"
"github.com/coreos/etcd/etcdserver/security"
"github.com/coreos/etcd/pkg/netutil"
)
type securityHandler struct {
sec *security.Store
clusterInfo etcdserver.ClusterInfo
}
func hasWriteRootAccess(sec *security.Store, r *http.Request) bool {
if r.Method == "GET" || r.Method == "HEAD" {
return true
}
return hasRootAccess(sec, r)
}
func hasRootAccess(sec *security.Store, r *http.Request) bool {
if sec == nil {
// No store means no security avaliable, eg, tests.
return true
}
if !sec.SecurityEnabled() {
return true
}
username, password, ok := netutil.BasicAuth(r)
if !ok {
return false
}
if username != "root" {
log.Printf("security: Attempting to use user %s for resource that requires root.", username)
return false
}
root, err := sec.GetUser("root")
if err != nil {
return false
}
ok = root.CheckPassword(password)
if !ok {
log.Printf("security: Wrong password for user %s", username)
}
return ok
}
func hasKeyPrefixAccess(sec *security.Store, r *http.Request, key string) bool {
if sec == nil {
// No store means no security avaliable, eg, tests.
return true
}
if !sec.SecurityEnabled() {
return true
}
username, password, ok := netutil.BasicAuth(r)
if !ok {
return false
}
user, err := sec.GetUser(username)
if err != nil {
log.Printf("security: No such user: %s.", username)
return false
}
authAsUser := user.CheckPassword(password)
if !authAsUser {
log.Printf("security: Incorrect password for user: %s.", username)
return false
}
if user.User == "root" {
return true
}
writeAccess := r.Method != "GET" && r.Method != "HEAD"
for _, roleName := range user.Roles {
role, err := sec.GetRole(roleName)
if err != nil {
continue
}
if role.HasKeyAccess(key, writeAccess) {
return true
}
}
log.Printf("security: Invalid access for user %s on key %s.", username, key)
return false
}
func writeNoAuth(w http.ResponseWriter) {
herr := httptypes.NewHTTPError(http.StatusUnauthorized, "Insufficient credentials")
herr.WriteTo(w)
}
func handleSecurity(mux *http.ServeMux, sh *securityHandler) {
mux.HandleFunc(securityPrefix+"/roles", sh.baseRoles)
mux.HandleFunc(securityPrefix+"/roles/", sh.handleRoles)
mux.HandleFunc(securityPrefix+"/users", sh.baseUsers)
mux.HandleFunc(securityPrefix+"/users/", sh.handleUsers)
mux.HandleFunc(securityPrefix+"/enable", sh.enableDisable)
}
func (sh *securityHandler) baseRoles(w http.ResponseWriter, r *http.Request) {
if !allowMethod(w, r.Method, "GET") {
return
}
if !hasRootAccess(sh.sec, r) {
writeNoAuth(w)
return
}
w.Header().Set("X-Etcd-Cluster-ID", sh.clusterInfo.ID().String())
w.Header().Set("Content-Type", "application/json")
var rolesCollections struct {
Roles []string `json:"roles"`
}
roles, err := sh.sec.AllRoles()
if err != nil {
writeError(w, err)
return
}
rolesCollections.Roles = roles
err = json.NewEncoder(w).Encode(rolesCollections)
if err != nil {
log.Println("etcdhttp: baseRoles error encoding on", r.URL)
}
}
func (sh *securityHandler) handleRoles(w http.ResponseWriter, r *http.Request) {
subpath := path.Clean(r.URL.Path[len(securityPrefix):])
// Split "/roles/rolename/command".
// First item is an empty string, second is "roles"
pieces := strings.Split(subpath, "/")
if len(pieces) != 3 {
}
sh.forRole(w, r, pieces[2])
}
func (sh *securityHandler) forRole(w http.ResponseWriter, r *http.Request, role string) {
if !allowMethod(w, r.Method, "GET", "PUT", "DELETE") {
return
}
if !hasRootAccess(sh.sec, r) {
writeNoAuth(w)
return
}
w.Header().Set("X-Etcd-Cluster-ID", sh.clusterInfo.ID().String())
switch r.Method {
case "GET":
w.Header().Set("Content-Type", "application/json")
data, err := sh.sec.GetRole(role)
if err != nil {
writeError(w, err)
return
}
err = json.NewEncoder(w).Encode(data)
if err != nil {
log.Println("etcdhttp: forRole error encoding on", r.URL)
return
}
return
case "PUT":
var in security.Role
err := json.NewDecoder(r.Body).Decode(&in)
if err != nil {
writeError(w, httptypes.NewHTTPError(http.StatusBadRequest, "Invalid JSON in request body."))
return
}
if in.Role != role {
writeError(w, httptypes.NewHTTPError(400, "Role JSON name does not match the name in the URL"))
return
}
newrole, err := sh.sec.CreateOrUpdateRole(in)
if err != nil {
writeError(w, err)
return
}
w.WriteHeader(http.StatusCreated)
err = json.NewEncoder(w).Encode(newrole)
if err != nil {
log.Println("etcdhttp: forRole error encoding on", r.URL)
return
}
return
case "DELETE":
err := sh.sec.DeleteRole(role)
if err != nil {
writeError(w, err)
return
}
}
}
func (sh *securityHandler) baseUsers(w http.ResponseWriter, r *http.Request) {
if !allowMethod(w, r.Method, "GET") {
return
}
if !hasRootAccess(sh.sec, r) {
writeNoAuth(w)
return
}
w.Header().Set("X-Etcd-Cluster-ID", sh.clusterInfo.ID().String())
w.Header().Set("Content-Type", "application/json")
var usersCollections struct {
Users []string `json:"users"`
}
users, err := sh.sec.AllUsers()
if err != nil {
writeError(w, err)
return
}
usersCollections.Users = users
err = json.NewEncoder(w).Encode(usersCollections)
if err != nil {
log.Println("etcdhttp: baseUsers error encoding on", r.URL)
}
}
func (sh *securityHandler) handleUsers(w http.ResponseWriter, r *http.Request) {
subpath := path.Clean(r.URL.Path[len(securityPrefix):])
// Split "/users/username/command".
// First item is an empty string, second is "users"
pieces := strings.Split(subpath, "/")
if len(pieces) != 3 {
writeError(w, httptypes.NewHTTPError(http.StatusBadRequest, "Invalid path"))
return
}
sh.forUser(w, r, pieces[2])
}
func (sh *securityHandler) forUser(w http.ResponseWriter, r *http.Request, user string) {
if !allowMethod(w, r.Method, "GET", "PUT", "DELETE") {
return
}
if !hasRootAccess(sh.sec, r) {
writeNoAuth(w)
return
}
w.Header().Set("X-Etcd-Cluster-ID", sh.clusterInfo.ID().String())
switch r.Method {
case "GET":
w.Header().Set("Content-Type", "application/json")
u, err := sh.sec.GetUser(user)
if err != nil {
writeError(w, err)
return
}
u.Password = ""
err = json.NewEncoder(w).Encode(u)
if err != nil {
log.Println("etcdhttp: forUser error encoding on", r.URL)
return
}
return
case "PUT":
var u security.User
err := json.NewDecoder(r.Body).Decode(&u)
if err != nil {
writeError(w, httptypes.NewHTTPError(http.StatusBadRequest, "Invalid JSON in request body."))
return
}
if u.User != user {
writeError(w, httptypes.NewHTTPError(400, "User JSON name does not match the name in the URL"))
return
}
newuser, err := sh.sec.CreateOrUpdateUser(u)
if err != nil {
writeError(w, err)
return
}
newuser.Password = ""
w.WriteHeader(http.StatusCreated)
err = json.NewEncoder(w).Encode(newuser)
if err != nil {
log.Println("etcdhttp: forUser error encoding on", r.URL)
return
}
return
case "DELETE":
err := sh.sec.DeleteUser(user)
if err != nil {
writeError(w, err)
return
}
}
}
type enabled struct {
Enabled bool `json:"enabled"`
}
func (sh *securityHandler) enableDisable(w http.ResponseWriter, r *http.Request) {
if !allowMethod(w, r.Method, "GET", "PUT", "DELETE") {
return
}
if !hasWriteRootAccess(sh.sec, r) {
writeNoAuth(w)
return
}
w.Header().Set("X-Etcd-Cluster-ID", sh.clusterInfo.ID().String())
w.Header().Set("Content-Type", "application/json")
isEnabled := sh.sec.SecurityEnabled()
switch r.Method {
case "GET":
jsonDict := enabled{isEnabled}
err := json.NewEncoder(w).Encode(jsonDict)
if err != nil {
log.Println("etcdhttp: error encoding security state on", r.URL)
}
case "PUT":
var in security.User
err := json.NewDecoder(r.Body).Decode(&in)
if err != nil {
writeError(w, httptypes.NewHTTPError(http.StatusBadRequest, "Invalid JSON in request body."))
return
}
if in.User != "root" {
writeError(w, httptypes.NewHTTPError(http.StatusBadRequest, "Need to create root user"))
return
}
err = sh.sec.EnableSecurity(in)
if err != nil {
writeError(w, err)
return
}
case "DELETE":
err := sh.sec.DisableSecurity()
if err != nil {
writeError(w, err)
return
}
}
}