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  }