...

Source file src/go.etcd.io/etcd/server/v3/etcdserver/api/v2http/client_auth.go

Documentation: go.etcd.io/etcd/server/v3/etcdserver/api/v2http

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

View as plain text