diff --git a/server/etcdserver/api/etcdhttp/base.go b/server/etcdserver/api/etcdhttp/base.go index dcfa3f069..06067cc44 100644 --- a/server/etcdserver/api/etcdhttp/base.go +++ b/server/etcdserver/api/etcdhttp/base.go @@ -29,7 +29,6 @@ import ( ) const ( - configPath = "/config" varsPath = "/debug/vars" versionPath = "/version" ) diff --git a/server/etcdserver/api/etcdhttp/metrics_test.go b/server/etcdserver/api/etcdhttp/metrics_test.go index 859bdf495..1a0605b51 100644 --- a/server/etcdserver/api/etcdhttp/metrics_test.go +++ b/server/etcdserver/api/etcdhttp/metrics_test.go @@ -14,7 +14,6 @@ import ( "go.etcd.io/etcd/client/pkg/v3/types" "go.etcd.io/etcd/raft/v3" "go.etcd.io/etcd/server/v3/etcdserver" - stats "go.etcd.io/etcd/server/v3/etcdserver/api/v2stats" "go.uber.org/zap" ) @@ -26,7 +25,6 @@ func (s *fakeStats) StoreStats() []byte { return nil } type fakeServerV2 struct { fakeServer - stats.Stats health string } @@ -102,7 +100,6 @@ func TestHealthHandler(t *testing.T) { mux := http.NewServeMux() HandleMetricsHealth(zap.NewExample(), mux, &fakeServerV2{ fakeServer: fakeServer{alarms: tt.alarms}, - Stats: &fakeStats{}, health: tt.health, }) ts := httptest.NewServer(mux) diff --git a/server/etcdserver/api/v2auth/auth.go b/server/etcdserver/api/v2auth/auth.go deleted file mode 100644 index e76ee8b2a..000000000 --- a/server/etcdserver/api/v2auth/auth.go +++ /dev/null @@ -1,670 +0,0 @@ -// Copyright 2015 The etcd Authors -// -// 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 v2auth implements etcd authentication. -package v2auth - -import ( - "context" - "encoding/json" - "fmt" - "net/http" - "path" - "reflect" - "sort" - "strings" - "time" - - "go.etcd.io/etcd/api/v3/etcdserverpb" - "go.etcd.io/etcd/client/pkg/v3/types" - "go.etcd.io/etcd/server/v3/etcdserver" - "go.etcd.io/etcd/server/v3/etcdserver/api/v2error" - - "go.uber.org/zap" - "golang.org/x/crypto/bcrypt" -) - -const ( - // StorePermsPrefix is the internal prefix of the storage layer dedicated to storing user data. - StorePermsPrefix = "/2" - - // RootRoleName is the name of the ROOT role, with privileges to manage the cluster. - RootRoleName = "root" - - // GuestRoleName is the name of the role that defines the privileges of an unauthenticated user. - GuestRoleName = "guest" -) - -var rootRole = Role{ - Role: RootRoleName, - Permissions: Permissions{ - KV: RWPermission{ - Read: []string{"/*"}, - Write: []string{"/*"}, - }, - }, -} - -var guestRole = Role{ - Role: GuestRoleName, - Permissions: Permissions{ - KV: RWPermission{ - Read: []string{"/*"}, - Write: []string{"/*"}, - }, - }, -} - -type doer interface { - Do(context.Context, etcdserverpb.Request) (etcdserver.Response, error) -} - -type Store interface { - AllUsers() ([]string, error) - GetUser(name string) (User, error) - CreateOrUpdateUser(user User) (out User, created bool, err error) - CreateUser(user User) (User, error) - DeleteUser(name string) error - UpdateUser(user User) (User, error) - AllRoles() ([]string, error) - GetRole(name string) (Role, error) - CreateRole(role Role) error - DeleteRole(name string) error - UpdateRole(role Role) (Role, error) - AuthEnabled() bool - EnableAuth() error - DisableAuth() error - PasswordStore -} - -type PasswordStore interface { - CheckPassword(user User, password string) bool - HashPassword(password string) (string, error) -} - -type store struct { - lg *zap.Logger - server doer - timeout time.Duration - ensuredOnce bool - - PasswordStore -} - -type User struct { - User string `json:"user"` - Password string `json:"password,omitempty"` - Roles []string `json:"roles"` - Grant []string `json:"grant,omitempty"` - Revoke []string `json:"revoke,omitempty"` -} - -type Role struct { - Role string `json:"role"` - Permissions Permissions `json:"permissions"` - Grant *Permissions `json:"grant,omitempty"` - Revoke *Permissions `json:"revoke,omitempty"` -} - -type Permissions struct { - KV RWPermission `json:"kv"` -} - -func (p *Permissions) IsEmpty() bool { - return p == nil || (len(p.KV.Read) == 0 && len(p.KV.Write) == 0) -} - -type RWPermission struct { - Read []string `json:"read"` - Write []string `json:"write"` -} - -type Error struct { - Status int - Errmsg string -} - -func (ae Error) Error() string { return ae.Errmsg } -func (ae Error) HTTPStatus() int { return ae.Status } - -func authErr(hs int, s string, v ...interface{}) Error { - return Error{Status: hs, Errmsg: fmt.Sprintf("auth: "+s, v...)} -} - -func NewStore(lg *zap.Logger, server doer, timeout time.Duration) Store { - if lg == nil { - lg = zap.NewNop() - } - s := &store{ - lg: lg, - server: server, - timeout: timeout, - PasswordStore: passwordStore{}, - } - return s -} - -// passwordStore implements PasswordStore using bcrypt to hash user passwords -type passwordStore struct{} - -func (passwordStore) CheckPassword(user User, password string) bool { - err := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(password)) - return err == nil -} - -func (passwordStore) HashPassword(password string) (string, error) { - hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) - return string(hash), err -} - -func (s *store) AllUsers() ([]string, error) { - resp, err := s.requestResource("/users/", false) - if err != nil { - if e, ok := err.(*v2error.Error); ok { - if e.ErrorCode == v2error.EcodeKeyNotFound { - return []string{}, nil - } - } - return nil, err - } - var nodes []string - for _, n := range resp.Event.Node.Nodes { - _, user := path.Split(n.Key) - nodes = append(nodes, user) - } - sort.Strings(nodes) - return nodes, nil -} - -func (s *store) GetUser(name string) (User, error) { return s.getUser(name, false) } - -// CreateOrUpdateUser should be only used for creating the new user or when you are not -// sure if it is a create or update. (When only password is passed in, we are not sure -// if it is a update or create) -func (s *store) CreateOrUpdateUser(user User) (out User, created bool, err error) { - _, err = s.getUser(user.User, true) - if err == nil { - out, err = s.UpdateUser(user) - return out, false, err - } - u, err := s.CreateUser(user) - return u, true, err -} - -func (s *store) CreateUser(user User) (User, error) { - // Attach root role to root user. - if user.User == "root" { - user = attachRootRole(user) - } - u, err := s.createUserInternal(user) - if err == nil { - s.lg.Info("created a user", zap.String("user-name", user.User)) - } - return u, err -} - -func (s *store) createUserInternal(user User) (User, error) { - if user.Password == "" { - return user, authErr(http.StatusBadRequest, "Cannot create user %s with an empty password", user.User) - } - hash, err := s.HashPassword(user.Password) - if err != nil { - return user, err - } - user.Password = hash - - _, err = s.createResource("/users/"+user.User, user) - if err != nil { - if e, ok := err.(*v2error.Error); ok { - if e.ErrorCode == v2error.EcodeNodeExist { - return user, authErr(http.StatusConflict, "User %s already exists.", user.User) - } - } - } - return user, err -} - -func (s *store) DeleteUser(name string) error { - if s.AuthEnabled() && name == "root" { - return authErr(http.StatusForbidden, "Cannot delete root user while auth is enabled.") - } - err := s.deleteResource("/users/" + name) - if err != nil { - if e, ok := err.(*v2error.Error); ok { - if e.ErrorCode == v2error.EcodeKeyNotFound { - return authErr(http.StatusNotFound, "User %s does not exist", name) - } - } - return err - } - s.lg.Info("deleted a user", zap.String("user-name", name)) - return nil -} - -func (s *store) UpdateUser(user User) (User, error) { - old, err := s.getUser(user.User, true) - if err != nil { - if e, ok := err.(*v2error.Error); ok { - if e.ErrorCode == v2error.EcodeKeyNotFound { - return user, authErr(http.StatusNotFound, "User %s doesn't exist.", user.User) - } - } - return old, err - } - - newUser, err := old.merge(s.lg, user, s.PasswordStore) - if err != nil { - return old, err - } - if reflect.DeepEqual(old, newUser) { - return old, authErr(http.StatusBadRequest, "User not updated. Use grant/revoke/password to update the user.") - } - _, err = s.updateResource("/users/"+user.User, newUser) - if err == nil { - s.lg.Info("updated a user", zap.String("user-name", user.User)) - } - return newUser, err -} - -func (s *store) AllRoles() ([]string, error) { - nodes := []string{RootRoleName} - resp, err := s.requestResource("/roles/", false) - if err != nil { - if e, ok := err.(*v2error.Error); ok { - if e.ErrorCode == v2error.EcodeKeyNotFound { - return nodes, nil - } - } - return nil, err - } - for _, n := range resp.Event.Node.Nodes { - _, role := path.Split(n.Key) - nodes = append(nodes, role) - } - sort.Strings(nodes) - return nodes, nil -} - -func (s *store) GetRole(name string) (Role, error) { return s.getRole(name, false) } - -func (s *store) CreateRole(role Role) error { - if role.Role == RootRoleName { - return authErr(http.StatusForbidden, "Cannot modify role %s: is root role.", role.Role) - } - _, err := s.createResource("/roles/"+role.Role, role) - if err != nil { - if e, ok := err.(*v2error.Error); ok { - if e.ErrorCode == v2error.EcodeNodeExist { - return authErr(http.StatusConflict, "Role %s already exists.", role.Role) - } - } - } - if err == nil { - s.lg.Info("created a new role", zap.String("role-name", role.Role)) - } - return err -} - -func (s *store) DeleteRole(name string) error { - if name == RootRoleName { - return authErr(http.StatusForbidden, "Cannot modify role %s: is root role.", name) - } - err := s.deleteResource("/roles/" + name) - if err != nil { - if e, ok := err.(*v2error.Error); ok { - if e.ErrorCode == v2error.EcodeKeyNotFound { - return authErr(http.StatusNotFound, "Role %s doesn't exist.", name) - } - } - } - if err == nil { - s.lg.Info("delete a new role", zap.String("role-name", name)) - } - return err -} - -func (s *store) UpdateRole(role Role) (Role, error) { - if role.Role == RootRoleName { - return Role{}, authErr(http.StatusForbidden, "Cannot modify role %s: is root role.", role.Role) - } - old, err := s.getRole(role.Role, true) - if err != nil { - if e, ok := err.(*v2error.Error); ok { - if e.ErrorCode == v2error.EcodeKeyNotFound { - return role, authErr(http.StatusNotFound, "Role %s doesn't exist.", role.Role) - } - } - return old, err - } - newRole, err := old.merge(s.lg, role) - if err != nil { - return old, err - } - if reflect.DeepEqual(old, newRole) { - return old, authErr(http.StatusBadRequest, "Role not updated. Use grant/revoke to update the role.") - } - _, err = s.updateResource("/roles/"+role.Role, newRole) - if err == nil { - s.lg.Info("updated a new role", zap.String("role-name", role.Role)) - } - return newRole, err -} - -func (s *store) AuthEnabled() bool { - return s.detectAuth() -} - -func (s *store) EnableAuth() error { - if s.AuthEnabled() { - return authErr(http.StatusConflict, "already enabled") - } - - if _, err := s.getUser("root", true); err != nil { - return authErr(http.StatusConflict, "No root user available, please create one") - } - if _, err := s.getRole(GuestRoleName, true); err != nil { - s.lg.Info( - "no guest role access found; creating default", - zap.String("role-name", GuestRoleName), - ) - if err := s.CreateRole(guestRole); err != nil { - s.lg.Warn( - "failed to create a guest role; aborting auth enable", - zap.String("role-name", GuestRoleName), - zap.Error(err), - ) - return err - } - } - - if err := s.enableAuth(); err != nil { - s.lg.Warn("failed to enable auth", zap.Error(err)) - return err - } - - s.lg.Info("enabled auth") - return nil -} - -func (s *store) DisableAuth() error { - if !s.AuthEnabled() { - return authErr(http.StatusConflict, "already disabled") - } - - err := s.disableAuth() - if err == nil { - s.lg.Info("disabled auth") - } else { - s.lg.Warn("failed to disable auth", zap.Error(err)) - } - return err -} - -// merge applies the properties of the passed-in User to the User on which it -// is called and returns a new User with these modifications applied. Think of -// all Users as immutable sets of data. Merge allows you to perform the set -// operations (desired grants and revokes) atomically -func (ou User) merge(lg *zap.Logger, nu User, s PasswordStore) (User, error) { - var out User - if ou.User != nu.User { - return out, authErr(http.StatusConflict, "Merging user data with conflicting usernames: %s %s", ou.User, nu.User) - } - out.User = ou.User - if nu.Password != "" { - hash, err := s.HashPassword(nu.Password) - if err != nil { - return ou, err - } - out.Password = hash - } else { - out.Password = ou.Password - } - currentRoles := types.NewUnsafeSet(ou.Roles...) - for _, g := range nu.Grant { - if currentRoles.Contains(g) { - lg.Warn( - "attempted to grant a duplicate role for a user", - zap.String("user-name", nu.User), - zap.String("role-name", g), - ) - return User{}, authErr(http.StatusConflict, fmt.Sprintf("Granting duplicate role %s for user %s", g, nu.User)) - } - currentRoles.Add(g) - } - for _, r := range nu.Revoke { - if !currentRoles.Contains(r) { - lg.Warn( - "attempted to revoke a ungranted role for a user", - zap.String("user-name", nu.User), - zap.String("role-name", r), - ) - return User{}, authErr(http.StatusConflict, fmt.Sprintf("Revoking ungranted role %s for user %s", r, nu.User)) - } - currentRoles.Remove(r) - } - out.Roles = currentRoles.Values() - sort.Strings(out.Roles) - return out, nil -} - -// merge for a role works the same as User above -- atomic Role application to -// each of the substructures. -func (r Role) merge(lg *zap.Logger, n Role) (Role, error) { - var out Role - var err error - if r.Role != n.Role { - return out, authErr(http.StatusConflict, "Merging role with conflicting names: %s %s", r.Role, n.Role) - } - out.Role = r.Role - out.Permissions, err = r.Permissions.Grant(n.Grant) - if err != nil { - return out, err - } - out.Permissions, err = out.Permissions.Revoke(lg, n.Revoke) - return out, err -} - -func (r Role) HasKeyAccess(key string, write bool) bool { - if r.Role == RootRoleName { - return true - } - return r.Permissions.KV.HasAccess(key, write) -} - -func (r Role) HasRecursiveAccess(key string, write bool) bool { - if r.Role == RootRoleName { - return true - } - return r.Permissions.KV.HasRecursiveAccess(key, write) -} - -// Grant adds a set of permissions to the permission object on which it is called, -// returning a new permission object. -func (p Permissions) Grant(n *Permissions) (Permissions, error) { - var out Permissions - var err error - if n == nil { - return p, nil - } - out.KV, err = p.KV.Grant(n.KV) - return out, err -} - -// Revoke removes a set of permissions to the permission object on which it is called, -// returning a new permission object. -func (p Permissions) Revoke(lg *zap.Logger, n *Permissions) (Permissions, error) { - var out Permissions - var err error - if n == nil { - return p, nil - } - out.KV, err = p.KV.Revoke(lg, n.KV) - return out, err -} - -// Grant adds a set of permissions to the permission object on which it is called, -// returning a new permission object. -func (rw RWPermission) Grant(n RWPermission) (RWPermission, error) { - var out RWPermission - currentRead := types.NewUnsafeSet(rw.Read...) - for _, r := range n.Read { - if currentRead.Contains(r) { - return out, authErr(http.StatusConflict, "Granting duplicate read permission %s", r) - } - currentRead.Add(r) - } - currentWrite := types.NewUnsafeSet(rw.Write...) - for _, w := range n.Write { - if currentWrite.Contains(w) { - return out, authErr(http.StatusConflict, "Granting duplicate write permission %s", w) - } - currentWrite.Add(w) - } - out.Read = currentRead.Values() - out.Write = currentWrite.Values() - sort.Strings(out.Read) - sort.Strings(out.Write) - return out, nil -} - -// Revoke removes a set of permissions to the permission object on which it is called, -// returning a new permission object. -func (rw RWPermission) Revoke(lg *zap.Logger, n RWPermission) (RWPermission, error) { - var out RWPermission - currentRead := types.NewUnsafeSet(rw.Read...) - for _, r := range n.Read { - if !currentRead.Contains(r) { - lg.Info( - "revoking ungranted read permission", - zap.String("read-permission", r), - ) - continue - } - currentRead.Remove(r) - } - currentWrite := types.NewUnsafeSet(rw.Write...) - for _, w := range n.Write { - if !currentWrite.Contains(w) { - lg.Info( - "revoking ungranted write permission", - zap.String("write-permission", w), - ) - continue - } - currentWrite.Remove(w) - } - out.Read = currentRead.Values() - out.Write = currentWrite.Values() - sort.Strings(out.Read) - sort.Strings(out.Write) - return out, nil -} - -func (rw RWPermission) HasAccess(key string, write bool) bool { - var list []string - if write { - list = rw.Write - } else { - list = rw.Read - } - for _, pat := range list { - match, err := simpleMatch(pat, key) - if err == nil && match { - return true - } - } - return false -} - -func (rw RWPermission) HasRecursiveAccess(key string, write bool) bool { - list := rw.Read - if write { - list = rw.Write - } - for _, pat := range list { - match, err := prefixMatch(pat, key) - if err == nil && match { - return true - } - } - return false -} - -func simpleMatch(pattern string, key string) (match bool, err error) { - if pattern[len(pattern)-1] == '*' { - return strings.HasPrefix(key, pattern[:len(pattern)-1]), nil - } - return key == pattern, nil -} - -func prefixMatch(pattern string, key string) (match bool, err error) { - if pattern[len(pattern)-1] != '*' { - return false, nil - } - return strings.HasPrefix(key, pattern[:len(pattern)-1]), nil -} - -func attachRootRole(u User) User { - inRoles := false - for _, r := range u.Roles { - if r == RootRoleName { - inRoles = true - break - } - } - if !inRoles { - u.Roles = append(u.Roles, RootRoleName) - } - return u -} - -func (s *store) getUser(name string, quorum bool) (User, error) { - resp, err := s.requestResource("/users/"+name, quorum) - if err != nil { - if e, ok := err.(*v2error.Error); ok { - if e.ErrorCode == v2error.EcodeKeyNotFound { - return User{}, authErr(http.StatusNotFound, "User %s does not exist.", name) - } - } - return User{}, err - } - var u User - err = json.Unmarshal([]byte(*resp.Event.Node.Value), &u) - if err != nil { - return u, err - } - // Attach root role to root user. - if u.User == "root" { - u = attachRootRole(u) - } - return u, nil -} - -func (s *store) getRole(name string, quorum bool) (Role, error) { - if name == RootRoleName { - return rootRole, nil - } - resp, err := s.requestResource("/roles/"+name, quorum) - if err != nil { - if e, ok := err.(*v2error.Error); ok { - if e.ErrorCode == v2error.EcodeKeyNotFound { - return Role{}, authErr(http.StatusNotFound, "Role %s does not exist.", name) - } - } - return Role{}, err - } - var r Role - err = json.Unmarshal([]byte(*resp.Event.Node.Value), &r) - return r, err -} diff --git a/server/etcdserver/api/v2auth/auth_requests.go b/server/etcdserver/api/v2auth/auth_requests.go deleted file mode 100644 index 6c8c50c8c..000000000 --- a/server/etcdserver/api/v2auth/auth_requests.go +++ /dev/null @@ -1,177 +0,0 @@ -// Copyright 2015 The etcd Authors -// -// 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 v2auth - -import ( - "context" - "encoding/json" - "path" - - "go.etcd.io/etcd/api/v3/etcdserverpb" - "go.etcd.io/etcd/server/v3/etcdserver" - "go.etcd.io/etcd/server/v3/etcdserver/api/v2error" - - "go.uber.org/zap" -) - -func (s *store) ensureAuthDirectories() error { - if s.ensuredOnce { - return nil - } - for _, res := range []string{StorePermsPrefix, StorePermsPrefix + "/users/", StorePermsPrefix + "/roles/"} { - ctx, cancel := context.WithTimeout(context.Background(), s.timeout) - pe := false - rr := etcdserverpb.Request{ - Method: "PUT", - Path: res, - Dir: true, - PrevExist: &pe, - } - _, err := s.server.Do(ctx, rr) - cancel() - if err != nil { - if e, ok := err.(*v2error.Error); ok { - if e.ErrorCode == v2error.EcodeNodeExist { - continue - } - } - s.lg.Warn( - "failed to create auth directories", - zap.Error(err), - ) - return err - } - } - ctx, cancel := context.WithTimeout(context.Background(), s.timeout) - defer cancel() - pe := false - rr := etcdserverpb.Request{ - Method: "PUT", - Path: StorePermsPrefix + "/enabled", - Val: "false", - PrevExist: &pe, - } - _, err := s.server.Do(ctx, rr) - if err != nil { - if e, ok := err.(*v2error.Error); ok { - if e.ErrorCode == v2error.EcodeNodeExist { - s.ensuredOnce = true - return nil - } - } - return err - } - s.ensuredOnce = true - return nil -} - -func (s *store) enableAuth() error { - _, err := s.updateResource("/enabled", true) - return err -} -func (s *store) disableAuth() error { - _, err := s.updateResource("/enabled", false) - return err -} - -func (s *store) detectAuth() bool { - if s.server == nil { - return false - } - value, err := s.requestResource("/enabled", false) - if err != nil { - if e, ok := err.(*v2error.Error); ok { - if e.ErrorCode == v2error.EcodeKeyNotFound { - return false - } - } - s.lg.Warn( - "failed to detect auth settings", - zap.Error(err), - ) - return false - } - - var u bool - err = json.Unmarshal([]byte(*value.Event.Node.Value), &u) - if err != nil { - s.lg.Warn( - "internal bookkeeping value for enabled isn't valid JSON", - zap.Error(err), - ) - return false - } - return u -} - -func (s *store) requestResource(res string, quorum bool) (etcdserver.Response, error) { - ctx, cancel := context.WithTimeout(context.Background(), s.timeout) - defer cancel() - p := path.Join(StorePermsPrefix, res) - method := "GET" - if quorum { - method = "QGET" - } - rr := etcdserverpb.Request{ - Method: method, - Path: p, - Dir: false, // TODO: always false? - } - return s.server.Do(ctx, rr) -} - -func (s *store) updateResource(res string, value interface{}) (etcdserver.Response, error) { - return s.setResource(res, value, true) -} -func (s *store) createResource(res string, value interface{}) (etcdserver.Response, error) { - return s.setResource(res, value, false) -} -func (s *store) setResource(res string, value interface{}, prevexist bool) (etcdserver.Response, error) { - err := s.ensureAuthDirectories() - if err != nil { - return etcdserver.Response{}, err - } - ctx, cancel := context.WithTimeout(context.Background(), s.timeout) - defer cancel() - data, err := json.Marshal(value) - if err != nil { - return etcdserver.Response{}, err - } - p := path.Join(StorePermsPrefix, res) - rr := etcdserverpb.Request{ - Method: "PUT", - Path: p, - Val: string(data), - PrevExist: &prevexist, - } - return s.server.Do(ctx, rr) -} - -func (s *store) deleteResource(res string) error { - err := s.ensureAuthDirectories() - if err != nil { - return err - } - ctx, cancel := context.WithTimeout(context.Background(), s.timeout) - defer cancel() - pex := true - p := path.Join(StorePermsPrefix, res) - _, err = s.server.Do(ctx, etcdserverpb.Request{ - Method: "DELETE", - Path: p, - PrevExist: &pex, - }) - return err -} diff --git a/server/etcdserver/api/v2auth/auth_test.go b/server/etcdserver/api/v2auth/auth_test.go deleted file mode 100644 index cd9c66667..000000000 --- a/server/etcdserver/api/v2auth/auth_test.go +++ /dev/null @@ -1,688 +0,0 @@ -// Copyright 2015 The etcd Authors -// -// 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 v2auth - -import ( - "context" - "reflect" - "testing" - "time" - - "go.etcd.io/etcd/api/v3/etcdserverpb" - "go.etcd.io/etcd/server/v3/etcdserver" - "go.etcd.io/etcd/server/v3/etcdserver/api/v2error" - "go.etcd.io/etcd/server/v3/etcdserver/api/v2store" - - "go.uber.org/zap" -) - -type fakeDoer struct{} - -func (fakeDoer) Do(context.Context, etcdserverpb.Request) (etcdserver.Response, error) { - return etcdserver.Response{}, nil -} - -func TestCheckPassword(t *testing.T) { - st := NewStore(zap.NewExample(), fakeDoer{}, 5*time.Second) - u := User{Password: "$2a$10$I3iddh1D..EIOXXQtsra4u8AjOtgEa2ERxVvYGfXFBJDo1omXwP.q"} - matched := st.CheckPassword(u, "foo") - if matched { - t.Fatalf("expected false, got %v", matched) - } -} - -const testTimeout = time.Millisecond - -func TestMergeUser(t *testing.T) { - tbl := []struct { - input User - merge User - expect User - iserr bool - }{ - { - User{User: "foo"}, - User{User: "bar"}, - User{}, - true, - }, - { - User{User: "foo"}, - User{User: "foo"}, - User{User: "foo", Roles: []string{}}, - false, - }, - { - User{User: "foo"}, - User{User: "foo", Grant: []string{"role1"}}, - User{User: "foo", Roles: []string{"role1"}}, - false, - }, - { - User{User: "foo", Roles: []string{"role1"}}, - User{User: "foo", Grant: []string{"role1"}}, - User{}, - true, - }, - { - User{User: "foo", Roles: []string{"role1"}}, - User{User: "foo", Revoke: []string{"role2"}}, - User{}, - true, - }, - { - User{User: "foo", Roles: []string{"role1"}}, - User{User: "foo", Grant: []string{"role2"}}, - User{User: "foo", Roles: []string{"role1", "role2"}}, - false, - }, - { // empty password will not overwrite the previous password - User{User: "foo", Password: "foo", Roles: []string{}}, - User{User: "foo", Password: ""}, - User{User: "foo", Password: "foo", Roles: []string{}}, - false, - }, - } - - for i, tt := range tbl { - out, err := tt.input.merge(zap.NewExample(), tt.merge, passwordStore{}) - if err != nil && !tt.iserr { - t.Fatalf("Got unexpected error on item %d", i) - } - if !tt.iserr { - if !reflect.DeepEqual(out, tt.expect) { - t.Errorf("Unequal merge expectation on item %d: got: %#v, expect: %#v", i, out, tt.expect) - } - } - } -} - -func TestMergeRole(t *testing.T) { - tbl := []struct { - input Role - merge Role - expect Role - iserr bool - }{ - { - Role{Role: "foo"}, - Role{Role: "bar"}, - Role{}, - true, - }, - { - Role{Role: "foo"}, - Role{Role: "foo", Grant: &Permissions{KV: RWPermission{Read: []string{"/foodir"}, Write: []string{"/foodir"}}}}, - Role{Role: "foo", Permissions: Permissions{KV: RWPermission{Read: []string{"/foodir"}, Write: []string{"/foodir"}}}}, - false, - }, - { - Role{Role: "foo", Permissions: Permissions{KV: RWPermission{Read: []string{"/foodir"}, Write: []string{"/foodir"}}}}, - Role{Role: "foo", Revoke: &Permissions{KV: RWPermission{Read: []string{"/foodir"}, Write: []string{"/foodir"}}}}, - Role{Role: "foo", Permissions: Permissions{KV: RWPermission{Read: []string{}, Write: []string{}}}}, - false, - }, - { - Role{Role: "foo", Permissions: Permissions{KV: RWPermission{Read: []string{"/bardir"}}}}, - Role{Role: "foo", Revoke: &Permissions{KV: RWPermission{Read: []string{"/foodir"}}}}, - Role{}, - true, - }, - } - for i, tt := range tbl { - out, err := tt.input.merge(zap.NewExample(), tt.merge) - if err != nil && !tt.iserr { - t.Fatalf("Got unexpected error on item %d", i) - } - if !tt.iserr { - if !reflect.DeepEqual(out, tt.expect) { - t.Errorf("Unequal merge expectation on item %d: got: %#v, expect: %#v", i, out, tt.expect) - } - } - } -} - -type testDoer struct { - get []etcdserver.Response - put []etcdserver.Response - getindex int - putindex int - explicitlyEnabled bool -} - -func (td *testDoer) Do(_ context.Context, req etcdserverpb.Request) (etcdserver.Response, error) { - if td.explicitlyEnabled && (req.Path == StorePermsPrefix+"/enabled") { - t := "true" - return etcdserver.Response{ - Event: &v2store.Event{ - Action: v2store.Get, - Node: &v2store.NodeExtern{ - Key: StorePermsPrefix + "/users/cat", - Value: &t, - }, - }, - }, nil - } - if (req.Method == "GET" || req.Method == "QGET") && td.get != nil { - if td.getindex >= len(td.get) { - return etcdserver.Response{}, &v2error.Error{ - ErrorCode: v2error.EcodeKeyNotFound, - } - } - res := td.get[td.getindex] - if res.Event == nil { - td.getindex++ - return etcdserver.Response{}, &v2error.Error{ - ErrorCode: v2error.EcodeKeyNotFound, - } - } - td.getindex++ - return res, nil - } - if req.Method == "PUT" && td.put != nil { - if td.putindex >= len(td.put) { - return etcdserver.Response{}, &v2error.Error{ - ErrorCode: v2error.EcodeKeyNotFound, - } - } - res := td.put[td.putindex] - if res.Event == nil { - td.putindex++ - return etcdserver.Response{}, &v2error.Error{ - ErrorCode: v2error.EcodeNodeExist, - } - } - td.putindex++ - return res, nil - } - return etcdserver.Response{}, nil -} - -func TestAllUsers(t *testing.T) { - d := &testDoer{ - get: []etcdserver.Response{ - { - Event: &v2store.Event{ - Action: v2store.Get, - Node: &v2store.NodeExtern{ - Nodes: v2store.NodeExterns([]*v2store.NodeExtern{ - { - Key: StorePermsPrefix + "/users/cat", - }, - { - Key: StorePermsPrefix + "/users/dog", - }, - }), - }, - }, - }, - }, - } - expected := []string{"cat", "dog"} - - s := store{lg: zap.NewExample(), server: d, timeout: testTimeout, ensuredOnce: false} - users, err := s.AllUsers() - if err != nil { - t.Error("Unexpected error", err) - } - if !reflect.DeepEqual(users, expected) { - t.Error("AllUsers doesn't match given store. Got", users, "expected", expected) - } -} - -func TestGetAndDeleteUser(t *testing.T) { - data := `{"user": "cat", "roles" : ["animal"]}` - d := &testDoer{ - get: []etcdserver.Response{ - { - Event: &v2store.Event{ - Action: v2store.Get, - Node: &v2store.NodeExtern{ - Key: StorePermsPrefix + "/users/cat", - Value: &data, - }, - }, - }, - }, - explicitlyEnabled: true, - } - expected := User{User: "cat", Roles: []string{"animal"}} - - s := store{lg: zap.NewExample(), server: d, timeout: testTimeout, ensuredOnce: false} - out, err := s.GetUser("cat") - if err != nil { - t.Error("Unexpected error", err) - } - if !reflect.DeepEqual(out, expected) { - t.Error("GetUser doesn't match given store. Got", out, "expected", expected) - } - err = s.DeleteUser("cat") - if err != nil { - t.Error("Unexpected error", err) - } -} - -func TestAllRoles(t *testing.T) { - d := &testDoer{ - get: []etcdserver.Response{ - { - Event: &v2store.Event{ - Action: v2store.Get, - Node: &v2store.NodeExtern{ - Nodes: v2store.NodeExterns([]*v2store.NodeExtern{ - { - Key: StorePermsPrefix + "/roles/animal", - }, - { - Key: StorePermsPrefix + "/roles/human", - }, - }), - }, - }, - }, - }, - explicitlyEnabled: true, - } - expected := []string{"animal", "human", "root"} - - s := store{lg: zap.NewExample(), server: d, timeout: testTimeout, ensuredOnce: false} - out, err := s.AllRoles() - if err != nil { - t.Error("Unexpected error", err) - } - if !reflect.DeepEqual(out, expected) { - t.Error("AllRoles doesn't match given store. Got", out, "expected", expected) - } -} - -func TestGetAndDeleteRole(t *testing.T) { - data := `{"role": "animal"}` - d := &testDoer{ - get: []etcdserver.Response{ - { - Event: &v2store.Event{ - Action: v2store.Get, - Node: &v2store.NodeExtern{ - Key: StorePermsPrefix + "/roles/animal", - Value: &data, - }, - }, - }, - }, - explicitlyEnabled: true, - } - expected := Role{Role: "animal"} - - s := store{lg: zap.NewExample(), server: d, timeout: testTimeout, ensuredOnce: false} - out, err := s.GetRole("animal") - if err != nil { - t.Error("Unexpected error", err) - } - if !reflect.DeepEqual(out, expected) { - t.Error("GetRole doesn't match given store. Got", out, "expected", expected) - } - err = s.DeleteRole("animal") - if err != nil { - t.Error("Unexpected error", err) - } -} - -func TestEnsure(t *testing.T) { - d := &testDoer{ - get: []etcdserver.Response{ - { - Event: &v2store.Event{ - Action: v2store.Set, - Node: &v2store.NodeExtern{ - Key: StorePermsPrefix, - Dir: true, - }, - }, - }, - { - Event: &v2store.Event{ - Action: v2store.Set, - Node: &v2store.NodeExtern{ - Key: StorePermsPrefix + "/users/", - Dir: true, - }, - }, - }, - { - Event: &v2store.Event{ - Action: v2store.Set, - Node: &v2store.NodeExtern{ - Key: StorePermsPrefix + "/roles/", - Dir: true, - }, - }, - }, - }, - } - - s := store{lg: zap.NewExample(), server: d, timeout: testTimeout, ensuredOnce: false} - err := s.ensureAuthDirectories() - if err != nil { - t.Error("Unexpected error", err) - } -} - -type fastPasswordStore struct { -} - -func (fastPasswordStore) CheckPassword(user User, password string) bool { - return user.Password == password -} - -func (fastPasswordStore) HashPassword(password string) (string, error) { return password, nil } - -func TestCreateAndUpdateUser(t *testing.T) { - olduser := `{"user": "cat", "roles" : ["animal"]}` - newuser := `{"user": "cat", "roles" : ["animal", "pet"]}` - d := &testDoer{ - get: []etcdserver.Response{ - { - Event: nil, - }, - { - Event: &v2store.Event{ - Action: v2store.Get, - Node: &v2store.NodeExtern{ - Key: StorePermsPrefix + "/users/cat", - Value: &olduser, - }, - }, - }, - { - Event: &v2store.Event{ - Action: v2store.Get, - Node: &v2store.NodeExtern{ - Key: StorePermsPrefix + "/users/cat", - Value: &olduser, - }, - }, - }, - }, - put: []etcdserver.Response{ - { - Event: &v2store.Event{ - Action: v2store.Update, - Node: &v2store.NodeExtern{ - Key: StorePermsPrefix + "/users/cat", - Value: &olduser, - }, - }, - }, - { - Event: &v2store.Event{ - Action: v2store.Update, - Node: &v2store.NodeExtern{ - Key: StorePermsPrefix + "/users/cat", - Value: &newuser, - }, - }, - }, - }, - explicitlyEnabled: true, - } - user := User{User: "cat", Password: "meow", Roles: []string{"animal"}} - update := User{User: "cat", Grant: []string{"pet"}} - expected := User{User: "cat", Roles: []string{"animal", "pet"}} - - s := store{lg: zap.NewExample(), server: d, timeout: testTimeout, ensuredOnce: true, PasswordStore: fastPasswordStore{}} - out, created, err := s.CreateOrUpdateUser(user) - if !created { - t.Error("Should have created user, instead updated?") - } - if err != nil { - t.Error("Unexpected error", err) - } - out.Password = "meow" - if !reflect.DeepEqual(out, user) { - t.Error("UpdateUser doesn't match given update. Got", out, "expected", expected) - } - out, created, err = s.CreateOrUpdateUser(update) - if created { - t.Error("Should have updated user, instead created?") - } - if err != nil { - t.Error("Unexpected error", err) - } - if !reflect.DeepEqual(out, expected) { - t.Error("UpdateUser doesn't match given update. Got", out, "expected", expected) - } -} - -func TestUpdateRole(t *testing.T) { - oldrole := `{"role": "animal", "permissions" : {"kv": {"read": ["/animal"], "write": []}}}` - newrole := `{"role": "animal", "permissions" : {"kv": {"read": ["/animal"], "write": ["/animal"]}}}` - d := &testDoer{ - get: []etcdserver.Response{ - { - Event: &v2store.Event{ - Action: v2store.Get, - Node: &v2store.NodeExtern{ - Key: StorePermsPrefix + "/roles/animal", - Value: &oldrole, - }, - }, - }, - }, - put: []etcdserver.Response{ - { - Event: &v2store.Event{ - Action: v2store.Update, - Node: &v2store.NodeExtern{ - Key: StorePermsPrefix + "/roles/animal", - Value: &newrole, - }, - }, - }, - }, - explicitlyEnabled: true, - } - update := Role{Role: "animal", Grant: &Permissions{KV: RWPermission{Read: []string{}, Write: []string{"/animal"}}}} - expected := Role{Role: "animal", Permissions: Permissions{KV: RWPermission{Read: []string{"/animal"}, Write: []string{"/animal"}}}} - - s := store{lg: zap.NewExample(), server: d, timeout: testTimeout, ensuredOnce: true} - out, err := s.UpdateRole(update) - if err != nil { - t.Error("Unexpected error", err) - } - if !reflect.DeepEqual(out, expected) { - t.Error("UpdateRole doesn't match given update. Got", out, "expected", expected) - } -} - -func TestCreateRole(t *testing.T) { - role := `{"role": "animal", "permissions" : {"kv": {"read": ["/animal"], "write": []}}}` - d := &testDoer{ - put: []etcdserver.Response{ - { - Event: &v2store.Event{ - Action: v2store.Create, - Node: &v2store.NodeExtern{ - Key: StorePermsPrefix + "/roles/animal", - Value: &role, - }, - }, - }, - { - Event: nil, - }, - }, - explicitlyEnabled: true, - } - r := Role{Role: "animal", Permissions: Permissions{KV: RWPermission{Read: []string{"/animal"}, Write: []string{}}}} - - s := store{lg: zap.NewExample(), server: d, timeout: testTimeout, ensuredOnce: true} - err := s.CreateRole(Role{Role: "root"}) - if err == nil { - t.Error("Should error creating root role") - } - err = s.CreateRole(r) - if err != nil { - t.Error("Unexpected error", err) - } - err = s.CreateRole(r) - if err == nil { - t.Error("Creating duplicate role, should error") - } -} - -func TestEnableAuth(t *testing.T) { - rootUser := `{"user": "root", "password": ""}` - guestRole := `{"role": "guest", "permissions" : {"kv": {"read": ["*"], "write": ["*"]}}}` - trueval := "true" - falseval := "false" - d := &testDoer{ - get: []etcdserver.Response{ - { - Event: &v2store.Event{ - Action: v2store.Get, - Node: &v2store.NodeExtern{ - Key: StorePermsPrefix + "/enabled", - Value: &falseval, - }, - }, - }, - { - Event: &v2store.Event{ - Action: v2store.Get, - Node: &v2store.NodeExtern{ - Key: StorePermsPrefix + "/user/root", - Value: &rootUser, - }, - }, - }, - { - Event: nil, - }, - }, - put: []etcdserver.Response{ - { - Event: &v2store.Event{ - Action: v2store.Create, - Node: &v2store.NodeExtern{ - Key: StorePermsPrefix + "/roles/guest", - Value: &guestRole, - }, - }, - }, - { - Event: &v2store.Event{ - Action: v2store.Update, - Node: &v2store.NodeExtern{ - Key: StorePermsPrefix + "/enabled", - Value: &trueval, - }, - }, - }, - }, - explicitlyEnabled: false, - } - s := store{lg: zap.NewExample(), server: d, timeout: testTimeout, ensuredOnce: true} - err := s.EnableAuth() - if err != nil { - t.Error("Unexpected error", err) - } -} - -func TestDisableAuth(t *testing.T) { - trueval := "true" - falseval := "false" - d := &testDoer{ - get: []etcdserver.Response{ - { - Event: &v2store.Event{ - Action: v2store.Get, - Node: &v2store.NodeExtern{ - Key: StorePermsPrefix + "/enabled", - Value: &falseval, - }, - }, - }, - { - Event: &v2store.Event{ - Action: v2store.Get, - Node: &v2store.NodeExtern{ - Key: StorePermsPrefix + "/enabled", - Value: &trueval, - }, - }, - }, - }, - put: []etcdserver.Response{ - { - Event: &v2store.Event{ - Action: v2store.Update, - Node: &v2store.NodeExtern{ - Key: StorePermsPrefix + "/enabled", - Value: &falseval, - }, - }, - }, - }, - explicitlyEnabled: false, - } - s := store{lg: zap.NewExample(), server: d, timeout: testTimeout, ensuredOnce: true} - err := s.DisableAuth() - if err == nil { - t.Error("Expected error; already disabled") - } - - err = s.DisableAuth() - if err != nil { - t.Error("Unexpected error", err) - } -} - -func TestSimpleMatch(t *testing.T) { - role := Role{Role: "foo", Permissions: Permissions{KV: RWPermission{Read: []string{"/foodir/*", "/fookey"}, Write: []string{"/bardir/*", "/barkey"}}}} - if !role.HasKeyAccess("/foodir/foo/bar", false) { - t.Fatal("role lacks expected access") - } - if !role.HasKeyAccess("/fookey", false) { - t.Fatal("role lacks expected access") - } - if !role.HasRecursiveAccess("/foodir/*", false) { - t.Fatal("role lacks expected access") - } - if !role.HasRecursiveAccess("/foodir/foo*", false) { - t.Fatal("role lacks expected access") - } - if !role.HasRecursiveAccess("/bardir/*", true) { - t.Fatal("role lacks expected access") - } - if !role.HasKeyAccess("/bardir/bar/foo", true) { - t.Fatal("role lacks expected access") - } - if !role.HasKeyAccess("/barkey", true) { - t.Fatal("role lacks expected access") - } - - if role.HasKeyAccess("/bardir/bar/foo", false) { - t.Fatal("role has unexpected access") - } - if role.HasKeyAccess("/barkey", false) { - t.Fatal("role has unexpected access") - } - if role.HasKeyAccess("/foodir/foo/bar", true) { - t.Fatal("role has unexpected access") - } - if role.HasKeyAccess("/fookey", true) { - t.Fatal("role has unexpected access") - } -} diff --git a/server/etcdserver/api/v2error/error.go b/server/etcdserver/api/v2error/error.go index 623863254..ab24757d7 100644 --- a/server/etcdserver/api/v2error/error.go +++ b/server/etcdserver/api/v2error/error.go @@ -125,10 +125,6 @@ type Error struct { Index uint64 `json:"index"` } -func NewRequestError(errorCode int, cause string) *Error { - return NewError(errorCode, cause, 0) -} - func NewError(errorCode int, cause string, index uint64) *Error { return &Error{ ErrorCode: errorCode, diff --git a/server/etcdserver/api/v2http/capability.go b/server/etcdserver/api/v2http/capability.go deleted file mode 100644 index 718b5ed50..000000000 --- a/server/etcdserver/api/v2http/capability.go +++ /dev/null @@ -1,41 +0,0 @@ -// Copyright 2015 The etcd Authors -// -// 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 v2http - -import ( - "fmt" - "net/http" - - "go.etcd.io/etcd/server/v3/etcdserver/api" - "go.etcd.io/etcd/server/v3/etcdserver/api/v2http/httptypes" -) - -func authCapabilityHandler(fn func(http.ResponseWriter, *http.Request)) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - if !api.IsCapabilityEnabled(api.AuthCapability) { - notCapable(w, r, api.AuthCapability) - return - } - fn(w, r) - } -} - -func notCapable(w http.ResponseWriter, r *http.Request, c api.Capability) { - herr := httptypes.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("Not capable of accessing %s feature during rolling upgrades.", c)) - if err := herr.WriteTo(w); err != nil { - // TODO: the following plog was removed, add the logging back if possible - // plog.Debugf("error writing HTTPError (%v) to %s", err, r.RemoteAddr) - } -} diff --git a/server/etcdserver/api/v2http/client.go b/server/etcdserver/api/v2http/client.go index fcd1fda6f..7b56da357 100644 --- a/server/etcdserver/api/v2http/client.go +++ b/server/etcdserver/api/v2http/client.go @@ -12,45 +12,18 @@ // See the License for the specific language governing permissions and // limitations under the License. +// Package v2http provides etcd client and server implementations. package v2http import ( - "context" - "encoding/json" - "errors" - "fmt" - "io" "net/http" - "net/url" - "path" - "strconv" - "strings" "time" - "go.etcd.io/etcd/api/v3/etcdserverpb" - "go.etcd.io/etcd/client/pkg/v3/types" "go.etcd.io/etcd/server/v3/etcdserver" - "go.etcd.io/etcd/server/v3/etcdserver/api" "go.etcd.io/etcd/server/v3/etcdserver/api/etcdhttp" - "go.etcd.io/etcd/server/v3/etcdserver/api/membership" - "go.etcd.io/etcd/server/v3/etcdserver/api/v2auth" - "go.etcd.io/etcd/server/v3/etcdserver/api/v2error" - "go.etcd.io/etcd/server/v3/etcdserver/api/v2http/httptypes" - stats "go.etcd.io/etcd/server/v3/etcdserver/api/v2stats" - "go.etcd.io/etcd/server/v3/etcdserver/api/v2store" - - "github.com/jonboulle/clockwork" "go.uber.org/zap" ) -const ( - authPrefix = "/v2/auth" - keysPrefix = "/v2/keys" - machinesPrefix = "/v2/machines" - membersPrefix = "/v2/members" - statsPrefix = "/v2/stats" -) - // NewClientHandler generates a muxed http.Handler with the given parameters to serve etcd client requests. func NewClientHandler(lg *zap.Logger, server etcdserver.ServerPeer, timeout time.Duration) http.Handler { if lg == nil { @@ -59,698 +32,19 @@ func NewClientHandler(lg *zap.Logger, server etcdserver.ServerPeer, timeout time mux := http.NewServeMux() etcdhttp.HandleBasic(lg, mux, server) etcdhttp.HandleMetricsHealth(lg, mux, server) - handleV2(lg, mux, server, timeout) return requestLogger(lg, mux) } -func handleV2(lg *zap.Logger, mux *http.ServeMux, server etcdserver.ServerV2, timeout time.Duration) { - sec := v2auth.NewStore(lg, server, timeout) - kh := &keysHandler{ - lg: lg, - sec: sec, - server: server, - cluster: server.Cluster(), - timeout: timeout, - clientCertAuthEnabled: server.ClientCertAuthEnabled(), - } - - sh := &statsHandler{ - lg: lg, - stats: server, - } - - mh := &membersHandler{ - lg: lg, - sec: sec, - server: server, - cluster: server.Cluster(), - timeout: timeout, - clock: clockwork.NewRealClock(), - clientCertAuthEnabled: server.ClientCertAuthEnabled(), - } - - mah := &machinesHandler{cluster: server.Cluster()} - - sech := &authHandler{ - lg: lg, - sec: sec, - cluster: server.Cluster(), - clientCertAuthEnabled: server.ClientCertAuthEnabled(), - } - mux.HandleFunc("/", http.NotFound) - mux.Handle(keysPrefix, kh) - mux.Handle(keysPrefix+"/", kh) - mux.HandleFunc(statsPrefix+"/store", sh.serveStore) - mux.HandleFunc(statsPrefix+"/self", sh.serveSelf) - mux.HandleFunc(statsPrefix+"/leader", sh.serveLeader) - mux.Handle(membersPrefix, mh) - mux.Handle(membersPrefix+"/", mh) - mux.Handle(machinesPrefix, mah) - handleAuth(mux, sech) -} - -type keysHandler struct { - lg *zap.Logger - sec v2auth.Store - server etcdserver.ServerV2 - cluster api.Cluster - timeout time.Duration - clientCertAuthEnabled bool -} - -func (h *keysHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { - if !allowMethod(w, r.Method, "HEAD", "GET", "PUT", "POST", "DELETE") { - return - } - - w.Header().Set("X-Etcd-Cluster-ID", h.cluster.ID().String()) - - ctx, cancel := context.WithTimeout(context.Background(), h.timeout) - defer cancel() - clock := clockwork.NewRealClock() - startTime := clock.Now() - rr, noValueOnSuccess, err := parseKeyRequest(r, clock) - if err != nil { - writeKeyError(h.lg, w, err) - return - } - // The path must be valid at this point (we've parsed the request successfully). - if !hasKeyPrefixAccess(h.lg, h.sec, r, r.URL.Path[len(keysPrefix):], rr.Recursive, h.clientCertAuthEnabled) { - writeKeyNoAuth(w) - return - } - if !rr.Wait { - reportRequestReceived(rr) - } - resp, err := h.server.Do(ctx, rr) - if err != nil { - err = trimErrorPrefix(err, etcdserver.StoreKeysPrefix) - writeKeyError(h.lg, w, err) - reportRequestFailed(rr, err) - return - } - switch { - case resp.Event != nil: - if err := writeKeyEvent(w, resp, noValueOnSuccess); err != nil { - // Should never be reached - h.lg.Warn("failed to write key event", zap.Error(err)) - } - reportRequestCompleted(rr, startTime) - case resp.Watcher != nil: - ctx, cancel := context.WithTimeout(context.Background(), defaultWatchTimeout) - defer cancel() - handleKeyWatch(ctx, h.lg, w, resp, rr.Stream) - default: - writeKeyError(h.lg, w, errors.New("received response with no Event/Watcher")) - } -} - -type machinesHandler struct { - cluster api.Cluster -} - -func (h *machinesHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { - if !allowMethod(w, r.Method, "GET", "HEAD") { - return - } - endpoints := h.cluster.ClientURLs() - w.Write([]byte(strings.Join(endpoints, ", "))) -} - -type membersHandler struct { - lg *zap.Logger - sec v2auth.Store - server etcdserver.ServerV2 - cluster api.Cluster - timeout time.Duration - clock clockwork.Clock - clientCertAuthEnabled bool -} - -func (h *membersHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { - if !allowMethod(w, r.Method, "GET", "POST", "DELETE", "PUT") { - return - } - if !hasWriteRootAccess(h.lg, h.sec, r, h.clientCertAuthEnabled) { - writeNoAuth(h.lg, w, r) - return - } - w.Header().Set("X-Etcd-Cluster-ID", h.cluster.ID().String()) - - ctx, cancel := context.WithTimeout(context.Background(), h.timeout) - defer cancel() - - switch r.Method { - case "GET": - switch trimPrefix(r.URL.Path, membersPrefix) { - case "": - mc := newMemberCollection(h.cluster.Members()) - w.Header().Set("Content-Type", "application/json") - if err := json.NewEncoder(w).Encode(mc); err != nil { - h.lg.Warn("failed to encode members response", zap.Error(err)) - } - case "leader": - id := h.server.Leader() - if id == 0 { - writeError(h.lg, w, r, httptypes.NewHTTPError(http.StatusServiceUnavailable, "During election")) - return - } - m := newMember(h.cluster.Member(id)) - w.Header().Set("Content-Type", "application/json") - if err := json.NewEncoder(w).Encode(m); err != nil { - h.lg.Warn("failed to encode members response", zap.Error(err)) - } - default: - writeError(h.lg, w, r, httptypes.NewHTTPError(http.StatusNotFound, "Not found")) - } - - case "POST": - req := httptypes.MemberCreateRequest{} - if ok := unmarshalRequest(h.lg, r, &req, w); !ok { - return - } - now := h.clock.Now() - m := membership.NewMember("", req.PeerURLs, "", &now) - _, err := h.server.AddMember(ctx, *m) - switch { - case err == membership.ErrIDExists || err == membership.ErrPeerURLexists: - writeError(h.lg, w, r, httptypes.NewHTTPError(http.StatusConflict, err.Error())) - return - case err != nil: - h.lg.Warn( - "failed to add a member", - zap.String("member-id", m.ID.String()), - zap.Error(err), - ) - writeError(h.lg, w, r, err) - return - } - res := newMember(m) - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(http.StatusCreated) - if err := json.NewEncoder(w).Encode(res); err != nil { - h.lg.Warn("failed to encode members response", zap.Error(err)) - } - - case "DELETE": - id, ok := getID(h.lg, r.URL.Path, w) - if !ok { - return - } - _, err := h.server.RemoveMember(ctx, uint64(id)) - switch { - case err == membership.ErrIDRemoved: - writeError(h.lg, w, r, httptypes.NewHTTPError(http.StatusGone, fmt.Sprintf("Member permanently removed: %s", id))) - case err == membership.ErrIDNotFound: - writeError(h.lg, w, r, httptypes.NewHTTPError(http.StatusNotFound, fmt.Sprintf("No such member: %s", id))) - case err != nil: - h.lg.Warn( - "failed to remove a member", - zap.String("member-id", id.String()), - zap.Error(err), - ) - writeError(h.lg, w, r, err) - default: - w.WriteHeader(http.StatusNoContent) - } - - case "PUT": - id, ok := getID(h.lg, r.URL.Path, w) - if !ok { - return - } - req := httptypes.MemberUpdateRequest{} - if ok := unmarshalRequest(h.lg, r, &req, w); !ok { - return - } - m := membership.Member{ - ID: id, - RaftAttributes: membership.RaftAttributes{PeerURLs: req.PeerURLs.StringSlice()}, - } - _, err := h.server.UpdateMember(ctx, m) - switch { - case err == membership.ErrPeerURLexists: - writeError(h.lg, w, r, httptypes.NewHTTPError(http.StatusConflict, err.Error())) - case err == membership.ErrIDNotFound: - writeError(h.lg, w, r, httptypes.NewHTTPError(http.StatusNotFound, fmt.Sprintf("No such member: %s", id))) - case err != nil: - h.lg.Warn( - "failed to update a member", - zap.String("member-id", m.ID.String()), - zap.Error(err), - ) - writeError(h.lg, w, r, err) - default: - w.WriteHeader(http.StatusNoContent) - } - } -} - -type statsHandler struct { - lg *zap.Logger - stats stats.Stats -} - -func (h *statsHandler) serveStore(w http.ResponseWriter, r *http.Request) { - if !allowMethod(w, r.Method, "GET") { - return - } - w.Header().Set("Content-Type", "application/json") - w.Write(h.stats.StoreStats()) -} - -func (h *statsHandler) serveSelf(w http.ResponseWriter, r *http.Request) { - if !allowMethod(w, r.Method, "GET") { - return - } - w.Header().Set("Content-Type", "application/json") - w.Write(h.stats.SelfStats()) -} - -func (h *statsHandler) serveLeader(w http.ResponseWriter, r *http.Request) { - if !allowMethod(w, r.Method, "GET") { - return - } - stats := h.stats.LeaderStats() - if stats == nil { - etcdhttp.WriteError(h.lg, w, r, httptypes.NewHTTPError(http.StatusForbidden, "not current leader")) - return - } - w.Header().Set("Content-Type", "application/json") - w.Write(stats) -} - -// parseKeyRequest converts a received http.Request on keysPrefix to -// a server Request, performing validation of supplied fields as appropriate. -// If any validation fails, an empty Request and non-nil error is returned. -func parseKeyRequest(r *http.Request, clock clockwork.Clock) (etcdserverpb.Request, bool, error) { - var noValueOnSuccess bool - emptyReq := etcdserverpb.Request{} - - err := r.ParseForm() - if err != nil { - return emptyReq, false, v2error.NewRequestError( - v2error.EcodeInvalidForm, - err.Error(), - ) - } - - if !strings.HasPrefix(r.URL.Path, keysPrefix) { - return emptyReq, false, v2error.NewRequestError( - v2error.EcodeInvalidForm, - "incorrect key prefix", - ) - } - p := path.Join(etcdserver.StoreKeysPrefix, r.URL.Path[len(keysPrefix):]) - - var pIdx, wIdx uint64 - if pIdx, err = getUint64(r.Form, "prevIndex"); err != nil { - return emptyReq, false, v2error.NewRequestError( - v2error.EcodeIndexNaN, - `invalid value for "prevIndex"`, - ) - } - if wIdx, err = getUint64(r.Form, "waitIndex"); err != nil { - return emptyReq, false, v2error.NewRequestError( - v2error.EcodeIndexNaN, - `invalid value for "waitIndex"`, - ) - } - - var rec, sort, wait, dir, quorum, stream bool - if rec, err = getBool(r.Form, "recursive"); err != nil { - return emptyReq, false, v2error.NewRequestError( - v2error.EcodeInvalidField, - `invalid value for "recursive"`, - ) - } - if sort, err = getBool(r.Form, "sorted"); err != nil { - return emptyReq, false, v2error.NewRequestError( - v2error.EcodeInvalidField, - `invalid value for "sorted"`, - ) - } - if wait, err = getBool(r.Form, "wait"); err != nil { - return emptyReq, false, v2error.NewRequestError( - v2error.EcodeInvalidField, - `invalid value for "wait"`, - ) - } - // TODO(jonboulle): define what parameters dir is/isn't compatible with? - if dir, err = getBool(r.Form, "dir"); err != nil { - return emptyReq, false, v2error.NewRequestError( - v2error.EcodeInvalidField, - `invalid value for "dir"`, - ) - } - if quorum, err = getBool(r.Form, "quorum"); err != nil { - return emptyReq, false, v2error.NewRequestError( - v2error.EcodeInvalidField, - `invalid value for "quorum"`, - ) - } - if stream, err = getBool(r.Form, "stream"); err != nil { - return emptyReq, false, v2error.NewRequestError( - v2error.EcodeInvalidField, - `invalid value for "stream"`, - ) - } - - if wait && r.Method != "GET" { - return emptyReq, false, v2error.NewRequestError( - v2error.EcodeInvalidField, - `"wait" can only be used with GET requests`, - ) - } - - pV := r.FormValue("prevValue") - if _, ok := r.Form["prevValue"]; ok && pV == "" { - return emptyReq, false, v2error.NewRequestError( - v2error.EcodePrevValueRequired, - `"prevValue" cannot be empty`, - ) - } - - if noValueOnSuccess, err = getBool(r.Form, "noValueOnSuccess"); err != nil { - return emptyReq, false, v2error.NewRequestError( - v2error.EcodeInvalidField, - `invalid value for "noValueOnSuccess"`, - ) - } - - // TTL is nullable, so leave it null if not specified - // or an empty string - var ttl *uint64 - if len(r.FormValue("ttl")) > 0 { - i, err := getUint64(r.Form, "ttl") - if err != nil { - return emptyReq, false, v2error.NewRequestError( - v2error.EcodeTTLNaN, - `invalid value for "ttl"`, +func requestLogger(lg *zap.Logger, handler http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if lg != nil { + lg.Debug( + "handling HTTP request", + zap.String("method", r.Method), + zap.String("request-uri", r.RequestURI), + zap.String("remote-addr", r.RemoteAddr), ) } - ttl = &i - } - - // prevExist is nullable, so leave it null if not specified - var pe *bool - if _, ok := r.Form["prevExist"]; ok { - bv, err := getBool(r.Form, "prevExist") - if err != nil { - return emptyReq, false, v2error.NewRequestError( - v2error.EcodeInvalidField, - "invalid value for prevExist", - ) - } - pe = &bv - } - - // refresh is nullable, so leave it null if not specified - var refresh *bool - if _, ok := r.Form["refresh"]; ok { - bv, err := getBool(r.Form, "refresh") - if err != nil { - return emptyReq, false, v2error.NewRequestError( - v2error.EcodeInvalidField, - "invalid value for refresh", - ) - } - refresh = &bv - if refresh != nil && *refresh { - val := r.FormValue("value") - if _, ok := r.Form["value"]; ok && val != "" { - return emptyReq, false, v2error.NewRequestError( - v2error.EcodeRefreshValue, - `A value was provided on a refresh`, - ) - } - if ttl == nil { - return emptyReq, false, v2error.NewRequestError( - v2error.EcodeRefreshTTLRequired, - `No TTL value set`, - ) - } - } - } - - rr := etcdserverpb.Request{ - Method: r.Method, - Path: p, - Val: r.FormValue("value"), - Dir: dir, - PrevValue: pV, - PrevIndex: pIdx, - PrevExist: pe, - Wait: wait, - Since: wIdx, - Recursive: rec, - Sorted: sort, - Quorum: quorum, - Stream: stream, - } - - if pe != nil { - rr.PrevExist = pe - } - - if refresh != nil { - rr.Refresh = refresh - } - - // Null TTL is equivalent to unset Expiration - if ttl != nil { - expr := time.Duration(*ttl) * time.Second - rr.Expiration = clock.Now().Add(expr).UnixNano() - } - - return rr, noValueOnSuccess, nil -} - -// writeKeyEvent trims the prefix of key path in a single Event under -// StoreKeysPrefix, serializes it and writes the resulting JSON to the given -// ResponseWriter, along with the appropriate headers. -func writeKeyEvent(w http.ResponseWriter, resp etcdserver.Response, noValueOnSuccess bool) error { - ev := resp.Event - if ev == nil { - return errors.New("cannot write empty Event") - } - w.Header().Set("Content-Type", "application/json") - w.Header().Set("X-Etcd-Index", fmt.Sprint(ev.EtcdIndex)) - w.Header().Set("X-Raft-Index", fmt.Sprint(resp.Index)) - w.Header().Set("X-Raft-Term", fmt.Sprint(resp.Term)) - - if ev.IsCreated() { - w.WriteHeader(http.StatusCreated) - } - - ev = trimEventPrefix(ev, etcdserver.StoreKeysPrefix) - if noValueOnSuccess && - (ev.Action == v2store.Set || ev.Action == v2store.CompareAndSwap || - ev.Action == v2store.Create || ev.Action == v2store.Update) { - ev.Node = nil - ev.PrevNode = nil - } - return json.NewEncoder(w).Encode(ev) -} - -func writeKeyNoAuth(w http.ResponseWriter) { - e := v2error.NewError(v2error.EcodeUnauthorized, "Insufficient credentials", 0) - e.WriteTo(w) -} - -// writeKeyError logs and writes the given Error to the ResponseWriter. -// If Error is not an etcdErr, the error will be converted to an etcd error. -func writeKeyError(lg *zap.Logger, w http.ResponseWriter, err error) { - if err == nil { - return - } - switch e := err.(type) { - case *v2error.Error: - e.WriteTo(w) - default: - switch err { - case etcdserver.ErrTimeoutDueToLeaderFail, etcdserver.ErrTimeoutDueToConnectionLost: - if lg != nil { - lg.Warn( - "v2 response error", - zap.String("internal-server-error", err.Error()), - ) - } - default: - if lg != nil { - lg.Warn( - "unexpected v2 response error", - zap.String("internal-server-error", err.Error()), - ) - } - } - ee := v2error.NewError(v2error.EcodeRaftInternal, err.Error(), 0) - ee.WriteTo(w) - } -} - -func handleKeyWatch(ctx context.Context, lg *zap.Logger, w http.ResponseWriter, resp etcdserver.Response, stream bool) { - wa := resp.Watcher - defer wa.Remove() - ech := wa.EventChan() - var nch <-chan bool - if x, ok := w.(http.CloseNotifier); ok { - nch = x.CloseNotify() - } - - w.Header().Set("Content-Type", "application/json") - w.Header().Set("X-Etcd-Index", fmt.Sprint(wa.StartIndex())) - w.Header().Set("X-Raft-Index", fmt.Sprint(resp.Index)) - w.Header().Set("X-Raft-Term", fmt.Sprint(resp.Term)) - w.WriteHeader(http.StatusOK) - - // Ensure headers are flushed early, in case of long polling - w.(http.Flusher).Flush() - - for { - select { - case <-nch: - // Client closed connection. Nothing to do. - return - case <-ctx.Done(): - // Timed out. net/http will close the connection for us, so nothing to do. - return - case ev, ok := <-ech: - if !ok { - // If the channel is closed this may be an indication of - // that notifications are much more than we are able to - // send to the client in time. Then we simply end streaming. - return - } - ev = trimEventPrefix(ev, etcdserver.StoreKeysPrefix) - if err := json.NewEncoder(w).Encode(ev); err != nil { - // Should never be reached - lg.Warn("failed to encode event", zap.Error(err)) - return - } - if !stream { - return - } - w.(http.Flusher).Flush() - } - } -} - -func trimEventPrefix(ev *v2store.Event, prefix string) *v2store.Event { - if ev == nil { - return nil - } - // Since the *Event may reference one in the store history - // history, we must copy it before modifying - e := ev.Clone() - trimNodeExternPrefix(e.Node, prefix) - trimNodeExternPrefix(e.PrevNode, prefix) - return e -} - -func trimNodeExternPrefix(n *v2store.NodeExtern, prefix string) { - if n == nil { - return - } - n.Key = strings.TrimPrefix(n.Key, prefix) - for _, nn := range n.Nodes { - trimNodeExternPrefix(nn, prefix) - } -} - -func trimErrorPrefix(err error, prefix string) error { - if e, ok := err.(*v2error.Error); ok { - e.Cause = strings.TrimPrefix(e.Cause, prefix) - } - return err -} - -func unmarshalRequest(lg *zap.Logger, r *http.Request, req json.Unmarshaler, w http.ResponseWriter) bool { - ctype := r.Header.Get("Content-Type") - semicolonPosition := strings.Index(ctype, ";") - if semicolonPosition != -1 { - ctype = strings.TrimSpace(strings.ToLower(ctype[0:semicolonPosition])) - } - if ctype != "application/json" { - writeError(lg, w, r, httptypes.NewHTTPError(http.StatusUnsupportedMediaType, fmt.Sprintf("Bad Content-Type %s, accept application/json", ctype))) - return false - } - b, err := io.ReadAll(r.Body) - if err != nil { - writeError(lg, w, r, httptypes.NewHTTPError(http.StatusBadRequest, err.Error())) - return false - } - if err := req.UnmarshalJSON(b); err != nil { - writeError(lg, w, r, httptypes.NewHTTPError(http.StatusBadRequest, err.Error())) - return false - } - return true -} - -func getID(lg *zap.Logger, p string, w http.ResponseWriter) (types.ID, bool) { - idStr := trimPrefix(p, membersPrefix) - if idStr == "" { - http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed) - return 0, false - } - id, err := types.IDFromString(idStr) - if err != nil { - writeError(lg, w, nil, httptypes.NewHTTPError(http.StatusNotFound, fmt.Sprintf("No such member: %s", idStr))) - return 0, false - } - return id, true -} - -// getUint64 extracts a uint64 by the given key from a Form. If the key does -// not exist in the form, 0 is returned. If the key exists but the value is -// badly formed, an error is returned. If multiple values are present only the -// first is considered. -func getUint64(form url.Values, key string) (i uint64, err error) { - if vals, ok := form[key]; ok { - i, err = strconv.ParseUint(vals[0], 10, 64) - } - return -} - -// getBool extracts a bool by the given key from a Form. If the key does not -// exist in the form, false is returned. If the key exists but the value is -// badly formed, an error is returned. If multiple values are present only the -// first is considered. -func getBool(form url.Values, key string) (b bool, err error) { - if vals, ok := form[key]; ok { - b, err = strconv.ParseBool(vals[0]) - } - return -} - -// trimPrefix removes a given prefix and any slash following the prefix -// e.g.: trimPrefix("foo", "foo") == trimPrefix("foo/", "foo") == "" -func trimPrefix(p, prefix string) (s string) { - s = strings.TrimPrefix(p, prefix) - s = strings.TrimPrefix(s, "/") - return -} - -func newMemberCollection(ms []*membership.Member) *httptypes.MemberCollection { - c := httptypes.MemberCollection(make([]httptypes.Member, len(ms))) - - for i, m := range ms { - c[i] = newMember(m) - } - - return &c -} - -func newMember(m *membership.Member) httptypes.Member { - tm := httptypes.Member{ - ID: m.ID.String(), - Name: m.Name, - PeerURLs: make([]string, len(m.PeerURLs)), - ClientURLs: make([]string, len(m.ClientURLs)), - } - - copy(tm.PeerURLs, m.PeerURLs) - copy(tm.ClientURLs, m.ClientURLs) - - return tm + handler.ServeHTTP(w, r) + }) } diff --git a/server/etcdserver/api/v2http/client_auth.go b/server/etcdserver/api/v2http/client_auth.go deleted file mode 100644 index 2c6e7744e..000000000 --- a/server/etcdserver/api/v2http/client_auth.go +++ /dev/null @@ -1,604 +0,0 @@ -// Copyright 2015 The etcd Authors -// -// 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 v2http - -import ( - "encoding/json" - "net/http" - "path" - "strings" - - "go.etcd.io/etcd/server/v3/etcdserver/api" - "go.etcd.io/etcd/server/v3/etcdserver/api/v2auth" - "go.etcd.io/etcd/server/v3/etcdserver/api/v2http/httptypes" - - "go.uber.org/zap" -) - -type authHandler struct { - lg *zap.Logger - sec v2auth.Store - cluster api.Cluster - clientCertAuthEnabled bool -} - -func hasWriteRootAccess(lg *zap.Logger, sec v2auth.Store, r *http.Request, clientCertAuthEnabled bool) bool { - if r.Method == "GET" || r.Method == "HEAD" { - return true - } - return hasRootAccess(lg, sec, r, clientCertAuthEnabled) -} - -func userFromBasicAuth(lg *zap.Logger, sec v2auth.Store, r *http.Request) *v2auth.User { - username, password, ok := r.BasicAuth() - if !ok { - lg.Warn("malformed basic auth encoding") - return nil - } - user, err := sec.GetUser(username) - if err != nil { - return nil - } - - ok = sec.CheckPassword(user, password) - if !ok { - lg.Warn("incorrect password", zap.String("user-name", username)) - return nil - } - return &user -} - -func userFromClientCertificate(lg *zap.Logger, sec v2auth.Store, r *http.Request) *v2auth.User { - if r.TLS == nil { - return nil - } - - for _, chains := range r.TLS.VerifiedChains { - for _, chain := range chains { - lg.Debug("found common name", zap.String("common-name", chain.Subject.CommonName)) - user, err := sec.GetUser(chain.Subject.CommonName) - if err == nil { - lg.Debug( - "authenticated a user via common name", - zap.String("user-name", user.User), - zap.String("common-name", chain.Subject.CommonName), - ) - return &user - } - } - } - return nil -} - -func hasRootAccess(lg *zap.Logger, sec v2auth.Store, r *http.Request, clientCertAuthEnabled bool) bool { - if sec == nil { - // No store means no auth available, eg, tests. - return true - } - if !sec.AuthEnabled() { - return true - } - - var rootUser *v2auth.User - if r.Header.Get("Authorization") == "" && clientCertAuthEnabled { - rootUser = userFromClientCertificate(lg, sec, r) - if rootUser == nil { - return false - } - } else { - rootUser = userFromBasicAuth(lg, sec, r) - if rootUser == nil { - return false - } - } - - for _, role := range rootUser.Roles { - if role == v2auth.RootRoleName { - return true - } - } - - lg.Warn( - "a user does not have root role for resource", - zap.String("root-user", rootUser.User), - zap.String("root-role-name", v2auth.RootRoleName), - zap.String("resource-path", r.URL.Path), - ) - return false -} - -func hasKeyPrefixAccess(lg *zap.Logger, sec v2auth.Store, r *http.Request, key string, recursive, clientCertAuthEnabled bool) bool { - if sec == nil { - // No store means no auth available, eg, tests. - return true - } - if !sec.AuthEnabled() { - return true - } - - var user *v2auth.User - if r.Header.Get("Authorization") == "" { - if clientCertAuthEnabled { - user = userFromClientCertificate(lg, sec, r) - } - if user == nil { - return hasGuestAccess(lg, sec, r, key) - } - } else { - user = userFromBasicAuth(lg, sec, r) - if user == nil { - return false - } - } - - writeAccess := r.Method != "GET" && r.Method != "HEAD" - for _, roleName := range user.Roles { - role, err := sec.GetRole(roleName) - if err != nil { - continue - } - if recursive { - if role.HasRecursiveAccess(key, writeAccess) { - return true - } - } else if role.HasKeyAccess(key, writeAccess) { - return true - } - } - - lg.Warn( - "invalid access for user on key", - zap.String("user-name", user.User), - zap.String("key", key), - ) - return false -} - -func hasGuestAccess(lg *zap.Logger, sec v2auth.Store, r *http.Request, key string) bool { - writeAccess := r.Method != "GET" && r.Method != "HEAD" - role, err := sec.GetRole(v2auth.GuestRoleName) - if err != nil { - return false - } - if role.HasKeyAccess(key, writeAccess) { - return true - } - - lg.Warn( - "invalid access for a guest role on key", - zap.String("role-name", v2auth.GuestRoleName), - zap.String("key", key), - ) - return false -} - -func writeNoAuth(lg *zap.Logger, w http.ResponseWriter, r *http.Request) { - herr := httptypes.NewHTTPError(http.StatusUnauthorized, "Insufficient credentials") - if err := herr.WriteTo(w); err != nil { - lg.Debug( - "failed to write v2 HTTP error", - zap.String("remote-addr", r.RemoteAddr), - zap.Error(err), - ) - } -} - -func handleAuth(mux *http.ServeMux, sh *authHandler) { - mux.HandleFunc(authPrefix+"/roles", authCapabilityHandler(sh.baseRoles)) - mux.HandleFunc(authPrefix+"/roles/", authCapabilityHandler(sh.handleRoles)) - mux.HandleFunc(authPrefix+"/users", authCapabilityHandler(sh.baseUsers)) - mux.HandleFunc(authPrefix+"/users/", authCapabilityHandler(sh.handleUsers)) - mux.HandleFunc(authPrefix+"/enable", authCapabilityHandler(sh.enableDisable)) -} - -func (sh *authHandler) baseRoles(w http.ResponseWriter, r *http.Request) { - if !allowMethod(w, r.Method, "GET") { - return - } - if !hasRootAccess(sh.lg, sh.sec, r, sh.clientCertAuthEnabled) { - writeNoAuth(sh.lg, w, r) - return - } - - w.Header().Set("X-Etcd-Cluster-ID", sh.cluster.ID().String()) - w.Header().Set("Content-Type", "application/json") - - roles, err := sh.sec.AllRoles() - if err != nil { - writeError(sh.lg, w, r, err) - return - } - if roles == nil { - roles = make([]string, 0) - } - - err = r.ParseForm() - if err != nil { - writeError(sh.lg, w, r, err) - return - } - - var rolesCollections struct { - Roles []v2auth.Role `json:"roles"` - } - for _, roleName := range roles { - var role v2auth.Role - role, err = sh.sec.GetRole(roleName) - if err != nil { - writeError(sh.lg, w, r, err) - return - } - rolesCollections.Roles = append(rolesCollections.Roles, role) - } - err = json.NewEncoder(w).Encode(rolesCollections) - - if err != nil { - sh.lg.Warn( - "failed to encode base roles", - zap.String("url", r.URL.String()), - zap.Error(err), - ) - writeError(sh.lg, w, r, err) - return - } -} - -func (sh *authHandler) handleRoles(w http.ResponseWriter, r *http.Request) { - subpath := path.Clean(r.URL.Path[len(authPrefix):]) - // Split "/roles/rolename/command". - // First item is an empty string, second is "roles" - pieces := strings.Split(subpath, "/") - if len(pieces) == 2 { - sh.baseRoles(w, r) - return - } - if len(pieces) != 3 { - writeError(sh.lg, w, r, httptypes.NewHTTPError(http.StatusBadRequest, "Invalid path")) - return - } - sh.forRole(w, r, pieces[2]) -} - -func (sh *authHandler) forRole(w http.ResponseWriter, r *http.Request, role string) { - if !allowMethod(w, r.Method, "GET", "PUT", "DELETE") { - return - } - if !hasRootAccess(sh.lg, sh.sec, r, sh.clientCertAuthEnabled) { - writeNoAuth(sh.lg, w, r) - return - } - w.Header().Set("X-Etcd-Cluster-ID", sh.cluster.ID().String()) - w.Header().Set("Content-Type", "application/json") - - switch r.Method { - case "GET": - data, err := sh.sec.GetRole(role) - if err != nil { - writeError(sh.lg, w, r, err) - return - } - err = json.NewEncoder(w).Encode(data) - if err != nil { - sh.lg.Warn( - "failed to encode a role", - zap.String("url", r.URL.String()), - zap.Error(err), - ) - return - } - return - - case "PUT": - var in v2auth.Role - err := json.NewDecoder(r.Body).Decode(&in) - if err != nil { - writeError(sh.lg, w, r, httptypes.NewHTTPError(http.StatusBadRequest, "Invalid JSON in request body.")) - return - } - if in.Role != role { - writeError(sh.lg, w, r, httptypes.NewHTTPError(http.StatusBadRequest, "Role JSON name does not match the name in the URL")) - return - } - - var out v2auth.Role - - // create - if in.Grant.IsEmpty() && in.Revoke.IsEmpty() { - err = sh.sec.CreateRole(in) - if err != nil { - writeError(sh.lg, w, r, err) - return - } - w.WriteHeader(http.StatusCreated) - out = in - } else { - if !in.Permissions.IsEmpty() { - writeError(sh.lg, w, r, httptypes.NewHTTPError(http.StatusBadRequest, "Role JSON contains both permissions and grant/revoke")) - return - } - out, err = sh.sec.UpdateRole(in) - if err != nil { - writeError(sh.lg, w, r, err) - return - } - w.WriteHeader(http.StatusOK) - } - - err = json.NewEncoder(w).Encode(out) - if err != nil { - sh.lg.Warn( - "failed to encode a role", - zap.String("url", r.URL.String()), - zap.Error(err), - ) - return - } - return - - case "DELETE": - err := sh.sec.DeleteRole(role) - if err != nil { - writeError(sh.lg, w, r, err) - return - } - } -} - -type userWithRoles struct { - User string `json:"user"` - Roles []v2auth.Role `json:"roles,omitempty"` -} - -type usersCollections struct { - Users []userWithRoles `json:"users"` -} - -func (sh *authHandler) baseUsers(w http.ResponseWriter, r *http.Request) { - if !allowMethod(w, r.Method, "GET") { - return - } - if !hasRootAccess(sh.lg, sh.sec, r, sh.clientCertAuthEnabled) { - writeNoAuth(sh.lg, w, r) - return - } - w.Header().Set("X-Etcd-Cluster-ID", sh.cluster.ID().String()) - w.Header().Set("Content-Type", "application/json") - - users, err := sh.sec.AllUsers() - if err != nil { - writeError(sh.lg, w, r, err) - return - } - if users == nil { - users = make([]string, 0) - } - - err = r.ParseForm() - if err != nil { - writeError(sh.lg, w, r, err) - return - } - - ucs := usersCollections{} - for _, userName := range users { - var user v2auth.User - user, err = sh.sec.GetUser(userName) - if err != nil { - writeError(sh.lg, w, r, err) - return - } - - uwr := userWithRoles{User: user.User} - for _, roleName := range user.Roles { - var role v2auth.Role - role, err = sh.sec.GetRole(roleName) - if err != nil { - continue - } - uwr.Roles = append(uwr.Roles, role) - } - - ucs.Users = append(ucs.Users, uwr) - } - err = json.NewEncoder(w).Encode(ucs) - - if err != nil { - sh.lg.Warn( - "failed to encode users", - zap.String("url", r.URL.String()), - zap.Error(err), - ) - writeError(sh.lg, w, r, err) - return - } -} - -func (sh *authHandler) handleUsers(w http.ResponseWriter, r *http.Request) { - subpath := path.Clean(r.URL.Path[len(authPrefix):]) - // Split "/users/username". - // First item is an empty string, second is "users" - pieces := strings.Split(subpath, "/") - if len(pieces) == 2 { - sh.baseUsers(w, r) - return - } - if len(pieces) != 3 { - writeError(sh.lg, w, r, httptypes.NewHTTPError(http.StatusBadRequest, "Invalid path")) - return - } - sh.forUser(w, r, pieces[2]) -} - -func (sh *authHandler) forUser(w http.ResponseWriter, r *http.Request, user string) { - if !allowMethod(w, r.Method, "GET", "PUT", "DELETE") { - return - } - if !hasRootAccess(sh.lg, sh.sec, r, sh.clientCertAuthEnabled) { - writeNoAuth(sh.lg, w, r) - return - } - w.Header().Set("X-Etcd-Cluster-ID", sh.cluster.ID().String()) - w.Header().Set("Content-Type", "application/json") - - switch r.Method { - case "GET": - u, err := sh.sec.GetUser(user) - if err != nil { - writeError(sh.lg, w, r, err) - return - } - - err = r.ParseForm() - if err != nil { - writeError(sh.lg, w, r, err) - return - } - - uwr := userWithRoles{User: u.User} - for _, roleName := range u.Roles { - var role v2auth.Role - role, err = sh.sec.GetRole(roleName) - if err != nil { - writeError(sh.lg, w, r, err) - return - } - uwr.Roles = append(uwr.Roles, role) - } - err = json.NewEncoder(w).Encode(uwr) - - if err != nil { - sh.lg.Warn( - "failed to encode roles", - zap.String("url", r.URL.String()), - zap.Error(err), - ) - return - } - return - - case "PUT": - var u v2auth.User - err := json.NewDecoder(r.Body).Decode(&u) - if err != nil { - writeError(sh.lg, w, r, httptypes.NewHTTPError(http.StatusBadRequest, "Invalid JSON in request body.")) - return - } - if u.User != user { - writeError(sh.lg, w, r, httptypes.NewHTTPError(http.StatusBadRequest, "User JSON name does not match the name in the URL")) - return - } - - var ( - out v2auth.User - created bool - ) - - if len(u.Grant) == 0 && len(u.Revoke) == 0 { - // create or update - if len(u.Roles) != 0 { - out, err = sh.sec.CreateUser(u) - } else { - // if user passes in both password and roles, we are unsure about his/her - // intention. - out, created, err = sh.sec.CreateOrUpdateUser(u) - } - - if err != nil { - writeError(sh.lg, w, r, err) - return - } - } else { - // update case - if len(u.Roles) != 0 { - writeError(sh.lg, w, r, httptypes.NewHTTPError(http.StatusBadRequest, "User JSON contains both roles and grant/revoke")) - return - } - out, err = sh.sec.UpdateUser(u) - if err != nil { - writeError(sh.lg, w, r, err) - return - } - } - - if created { - w.WriteHeader(http.StatusCreated) - } else { - w.WriteHeader(http.StatusOK) - } - - out.Password = "" - - err = json.NewEncoder(w).Encode(out) - if err != nil { - sh.lg.Warn( - "failed to encode a user", - zap.String("url", r.URL.String()), - zap.Error(err), - ) - return - } - return - - case "DELETE": - err := sh.sec.DeleteUser(user) - if err != nil { - writeError(sh.lg, w, r, err) - return - } - } -} - -type enabled struct { - Enabled bool `json:"enabled"` -} - -func (sh *authHandler) enableDisable(w http.ResponseWriter, r *http.Request) { - if !allowMethod(w, r.Method, "GET", "PUT", "DELETE") { - return - } - if !hasWriteRootAccess(sh.lg, sh.sec, r, sh.clientCertAuthEnabled) { - writeNoAuth(sh.lg, w, r) - return - } - w.Header().Set("X-Etcd-Cluster-ID", sh.cluster.ID().String()) - w.Header().Set("Content-Type", "application/json") - isEnabled := sh.sec.AuthEnabled() - switch r.Method { - case "GET": - jsonDict := enabled{isEnabled} - err := json.NewEncoder(w).Encode(jsonDict) - if err != nil { - sh.lg.Warn( - "failed to encode a auth state", - zap.String("url", r.URL.String()), - zap.Error(err), - ) - } - - case "PUT": - err := sh.sec.EnableAuth() - if err != nil { - writeError(sh.lg, w, r, err) - return - } - - case "DELETE": - err := sh.sec.DisableAuth() - if err != nil { - writeError(sh.lg, w, r, err) - return - } - } -} diff --git a/server/etcdserver/api/v2http/client_auth_test.go b/server/etcdserver/api/v2http/client_auth_test.go deleted file mode 100644 index 9a5dd2c88..000000000 --- a/server/etcdserver/api/v2http/client_auth_test.go +++ /dev/null @@ -1,913 +0,0 @@ -// Copyright 2015 The etcd Authors -// -// 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 v2http - -import ( - "crypto/tls" - "crypto/x509" - "encoding/json" - "encoding/pem" - "errors" - "fmt" - "net/http" - "net/http/httptest" - "net/url" - "os" - "path" - "sort" - "strings" - "testing" - - "go.etcd.io/etcd/server/v3/etcdserver/api" - "go.etcd.io/etcd/server/v3/etcdserver/api/v2auth" - - "go.uber.org/zap" -) - -const goodPassword = "good" - -func mustJSONRequest(t *testing.T, method string, p string, body string) *http.Request { - req, err := http.NewRequest(method, path.Join(authPrefix, p), strings.NewReader(body)) - if err != nil { - t.Fatalf("Error making JSON request: %s %s %s\n", method, p, body) - } - req.Header.Set("Content-Type", "application/json") - return req -} - -type mockAuthStore struct { - users map[string]*v2auth.User - roles map[string]*v2auth.Role - err error - enabled bool -} - -func (s *mockAuthStore) AllUsers() ([]string, error) { - var us []string - for u := range s.users { - us = append(us, u) - } - sort.Strings(us) - return us, s.err -} -func (s *mockAuthStore) GetUser(name string) (v2auth.User, error) { - u, ok := s.users[name] - if !ok { - return v2auth.User{}, s.err - } - return *u, s.err -} -func (s *mockAuthStore) CreateOrUpdateUser(user v2auth.User) (out v2auth.User, created bool, err error) { - if s.users == nil { - out, err = s.CreateUser(user) - return out, true, err - } - out, err = s.UpdateUser(user) - return out, false, err -} -func (s *mockAuthStore) CreateUser(user v2auth.User) (v2auth.User, error) { return user, s.err } -func (s *mockAuthStore) DeleteUser(name string) error { return s.err } -func (s *mockAuthStore) UpdateUser(user v2auth.User) (v2auth.User, error) { - return *s.users[user.User], s.err -} -func (s *mockAuthStore) AllRoles() ([]string, error) { - return []string{"awesome", "guest", "root"}, s.err -} -func (s *mockAuthStore) GetRole(name string) (v2auth.Role, error) { - r, ok := s.roles[name] - if ok { - return *r, s.err - } - return v2auth.Role{}, fmt.Errorf("%q does not exist (%v)", name, s.err) -} -func (s *mockAuthStore) CreateRole(role v2auth.Role) error { return s.err } -func (s *mockAuthStore) DeleteRole(name string) error { return s.err } -func (s *mockAuthStore) UpdateRole(role v2auth.Role) (v2auth.Role, error) { - return *s.roles[role.Role], s.err -} -func (s *mockAuthStore) AuthEnabled() bool { return s.enabled } -func (s *mockAuthStore) EnableAuth() error { return s.err } -func (s *mockAuthStore) DisableAuth() error { return s.err } - -func (s *mockAuthStore) CheckPassword(user v2auth.User, password string) bool { - return user.Password == password -} - -func (s *mockAuthStore) HashPassword(password string) (string, error) { - return password, nil -} - -func TestAuthFlow(t *testing.T) { - api.EnableCapability(api.AuthCapability) - var testCases = []struct { - req *http.Request - store mockAuthStore - - wcode int - wbody string - }{ - { - req: mustJSONRequest(t, "PUT", "users/alice", `{{{{{{{`), - store: mockAuthStore{}, - wcode: http.StatusBadRequest, - wbody: `{"message":"Invalid JSON in request body."}`, - }, - { - req: mustJSONRequest(t, "PUT", "users/alice", `{"user": "alice", "password": "goodpassword"}`), - store: mockAuthStore{enabled: true}, - wcode: http.StatusUnauthorized, - wbody: `{"message":"Insufficient credentials"}`, - }, - // Users - { - req: mustJSONRequest(t, "GET", "users", ""), - store: mockAuthStore{ - users: map[string]*v2auth.User{ - "alice": { - User: "alice", - Roles: []string{"alicerole", "guest"}, - Password: "wheeee", - }, - "bob": { - User: "bob", - Roles: []string{"guest"}, - Password: "wheeee", - }, - "root": { - User: "root", - Roles: []string{"root"}, - Password: "wheeee", - }, - }, - roles: map[string]*v2auth.Role{ - "alicerole": { - Role: "alicerole", - }, - "guest": { - Role: "guest", - }, - "root": { - Role: "root", - }, - }, - }, - wcode: http.StatusOK, - wbody: `{"users":[` + - `{"user":"alice","roles":[` + - `{"role":"alicerole","permissions":{"kv":{"read":null,"write":null}}},` + - `{"role":"guest","permissions":{"kv":{"read":null,"write":null}}}` + - `]},` + - `{"user":"bob","roles":[{"role":"guest","permissions":{"kv":{"read":null,"write":null}}}]},` + - `{"user":"root","roles":[{"role":"root","permissions":{"kv":{"read":null,"write":null}}}]}]}`, - }, - { - req: mustJSONRequest(t, "GET", "users/alice", ""), - store: mockAuthStore{ - users: map[string]*v2auth.User{ - "alice": { - User: "alice", - Roles: []string{"alicerole"}, - Password: "wheeee", - }, - }, - roles: map[string]*v2auth.Role{ - "alicerole": { - Role: "alicerole", - }, - }, - }, - wcode: http.StatusOK, - wbody: `{"user":"alice","roles":[{"role":"alicerole","permissions":{"kv":{"read":null,"write":null}}}]}`, - }, - { - req: mustJSONRequest(t, "PUT", "users/alice", `{"user": "alice", "password": "goodpassword"}`), - store: mockAuthStore{}, - wcode: http.StatusCreated, - wbody: `{"user":"alice","roles":null}`, - }, - { - req: mustJSONRequest(t, "DELETE", "users/alice", ``), - store: mockAuthStore{}, - wcode: http.StatusOK, - wbody: ``, - }, - { - req: mustJSONRequest(t, "PUT", "users/alice", `{"user": "alice", "password": "goodpassword"}`), - store: mockAuthStore{ - users: map[string]*v2auth.User{ - "alice": { - User: "alice", - Roles: []string{"alicerole", "guest"}, - Password: "wheeee", - }, - }, - }, - wcode: http.StatusOK, - wbody: `{"user":"alice","roles":["alicerole","guest"]}`, - }, - { - req: mustJSONRequest(t, "PUT", "users/alice", `{"user": "alice", "grant": ["alicerole"]}`), - store: mockAuthStore{ - users: map[string]*v2auth.User{ - "alice": { - User: "alice", - Roles: []string{"alicerole", "guest"}, - Password: "wheeee", - }, - }, - }, - wcode: http.StatusOK, - wbody: `{"user":"alice","roles":["alicerole","guest"]}`, - }, - { - req: mustJSONRequest(t, "GET", "users/alice", ``), - store: mockAuthStore{ - users: map[string]*v2auth.User{}, - err: v2auth.Error{Status: http.StatusNotFound, Errmsg: "auth: User alice doesn't exist."}, - }, - wcode: http.StatusNotFound, - wbody: `{"message":"auth: User alice doesn't exist."}`, - }, - { - req: mustJSONRequest(t, "GET", "roles/manager", ""), - store: mockAuthStore{ - roles: map[string]*v2auth.Role{ - "manager": { - Role: "manager", - }, - }, - }, - wcode: http.StatusOK, - wbody: `{"role":"manager","permissions":{"kv":{"read":null,"write":null}}}`, - }, - { - req: mustJSONRequest(t, "DELETE", "roles/manager", ``), - store: mockAuthStore{}, - wcode: http.StatusOK, - wbody: ``, - }, - { - req: mustJSONRequest(t, "PUT", "roles/manager", `{"role":"manager","permissions":{"kv":{"read":[],"write":[]}}}`), - store: mockAuthStore{}, - wcode: http.StatusCreated, - wbody: `{"role":"manager","permissions":{"kv":{"read":[],"write":[]}}}`, - }, - { - req: mustJSONRequest(t, "PUT", "roles/manager", `{"role":"manager","revoke":{"kv":{"read":["foo"],"write":[]}}}`), - store: mockAuthStore{ - roles: map[string]*v2auth.Role{ - "manager": { - Role: "manager", - }, - }, - }, - wcode: http.StatusOK, - wbody: `{"role":"manager","permissions":{"kv":{"read":null,"write":null}}}`, - }, - { - req: mustJSONRequest(t, "GET", "roles", ""), - store: mockAuthStore{ - roles: map[string]*v2auth.Role{ - "awesome": { - Role: "awesome", - }, - "guest": { - Role: "guest", - }, - "root": { - Role: "root", - }, - }, - }, - wcode: http.StatusOK, - wbody: `{"roles":[{"role":"awesome","permissions":{"kv":{"read":null,"write":null}}},` + - `{"role":"guest","permissions":{"kv":{"read":null,"write":null}}},` + - `{"role":"root","permissions":{"kv":{"read":null,"write":null}}}]}`, - }, - { - req: mustJSONRequest(t, "GET", "enable", ""), - store: mockAuthStore{ - enabled: true, - }, - wcode: http.StatusOK, - wbody: `{"enabled":true}`, - }, - { - req: mustJSONRequest(t, "PUT", "enable", ""), - store: mockAuthStore{ - enabled: false, - }, - wcode: http.StatusOK, - wbody: ``, - }, - { - req: (func() *http.Request { - req := mustJSONRequest(t, "DELETE", "enable", "") - req.SetBasicAuth("root", "good") - return req - })(), - store: mockAuthStore{ - enabled: true, - users: map[string]*v2auth.User{ - "root": { - User: "root", - Password: goodPassword, - Roles: []string{"root"}, - }, - }, - roles: map[string]*v2auth.Role{ - "root": { - Role: "root", - }, - }, - }, - wcode: http.StatusOK, - wbody: ``, - }, - { - req: (func() *http.Request { - req := mustJSONRequest(t, "DELETE", "enable", "") - req.SetBasicAuth("root", "bad") - return req - })(), - store: mockAuthStore{ - enabled: true, - users: map[string]*v2auth.User{ - "root": { - User: "root", - Password: goodPassword, - Roles: []string{"root"}, - }, - }, - roles: map[string]*v2auth.Role{ - "root": { - Role: "guest", - }, - }, - }, - wcode: http.StatusUnauthorized, - wbody: `{"message":"Insufficient credentials"}`, - }, - } - - for i, tt := range testCases { - mux := http.NewServeMux() - h := &authHandler{ - lg: zap.NewExample(), - sec: &tt.store, - cluster: &fakeCluster{id: 1}, - } - handleAuth(mux, h) - rw := httptest.NewRecorder() - mux.ServeHTTP(rw, tt.req) - if rw.Code != tt.wcode { - t.Errorf("#%d: got code=%d, want %d", i, rw.Code, tt.wcode) - } - g := rw.Body.String() - g = strings.TrimSpace(g) - if g != tt.wbody { - t.Errorf("#%d: got body=%s, want %s", i, g, tt.wbody) - } - } -} - -func TestGetUserGrantedWithNonexistingRole(t *testing.T) { - sh := &authHandler{ - sec: &mockAuthStore{ - users: map[string]*v2auth.User{ - "root": { - User: "root", - Roles: []string{"root", "foo"}, - }, - }, - roles: map[string]*v2auth.Role{ - "root": { - Role: "root", - }, - }, - }, - cluster: &fakeCluster{id: 1}, - } - srv := httptest.NewServer(http.HandlerFunc(sh.baseUsers)) - defer srv.Close() - - req, err := http.NewRequest("GET", "", nil) - if err != nil { - t.Fatal(err) - } - req.URL, err = url.Parse(srv.URL) - if err != nil { - t.Fatal(err) - } - req.Header.Set("Content-Type", "application/json") - - cli := http.DefaultClient - resp, err := cli.Do(req) - if err != nil { - t.Fatal(err) - } - defer resp.Body.Close() - - var uc usersCollections - if err := json.NewDecoder(resp.Body).Decode(&uc); err != nil { - t.Fatal(err) - } - if len(uc.Users) != 1 { - t.Fatalf("expected 1 user, got %+v", uc.Users) - } - if uc.Users[0].User != "root" { - t.Fatalf("expected 'root', got %q", uc.Users[0].User) - } - if len(uc.Users[0].Roles) != 1 { - t.Fatalf("expected 1 role, got %+v", uc.Users[0].Roles) - } - if uc.Users[0].Roles[0].Role != "root" { - t.Fatalf("expected 'root', got %q", uc.Users[0].Roles[0].Role) - } -} - -func mustAuthRequest(username, password string) *http.Request { - req, err := http.NewRequest(http.MethodGet, "path", strings.NewReader("")) - if err != nil { - panic("Cannot make auth request: " + err.Error()) - } - req.SetBasicAuth(username, password) - return req -} - -func unauthedRequest() *http.Request { - req, err := http.NewRequest(http.MethodGet, "path", strings.NewReader("")) - if err != nil { - panic("Cannot make request: " + err.Error()) - } - return req -} - -func tlsAuthedRequest(req *http.Request, certname string) *http.Request { - bytes, err := os.ReadFile(fmt.Sprintf("testdata/%s.pem", certname)) - if err != nil { - panic(err) - } - - block, _ := pem.Decode(bytes) - cert, err := x509.ParseCertificate(block.Bytes) - if err != nil { - panic(err) - } - - req.TLS = &tls.ConnectionState{ - VerifiedChains: [][]*x509.Certificate{{cert}}, - } - return req -} - -func TestPrefixAccess(t *testing.T) { - var table = []struct { - key string - req *http.Request - store *mockAuthStore - hasRoot bool - hasKeyPrefixAccess bool - hasRecursiveAccess bool - }{ - { - key: "/foo", - req: mustAuthRequest("root", "good"), - store: &mockAuthStore{ - users: map[string]*v2auth.User{ - "root": { - User: "root", - Password: goodPassword, - Roles: []string{"root"}, - }, - }, - roles: map[string]*v2auth.Role{ - "root": { - Role: "root", - }, - }, - enabled: true, - }, - hasRoot: true, - hasKeyPrefixAccess: true, - hasRecursiveAccess: true, - }, - { - key: "/foo", - req: mustAuthRequest("user", "good"), - store: &mockAuthStore{ - users: map[string]*v2auth.User{ - "user": { - User: "user", - Password: goodPassword, - Roles: []string{"foorole"}, - }, - }, - roles: map[string]*v2auth.Role{ - "foorole": { - Role: "foorole", - Permissions: v2auth.Permissions{ - KV: v2auth.RWPermission{ - Read: []string{"/foo"}, - Write: []string{"/foo"}, - }, - }, - }, - }, - enabled: true, - }, - hasRoot: false, - hasKeyPrefixAccess: true, - hasRecursiveAccess: false, - }, - { - key: "/foo", - req: mustAuthRequest("user", "good"), - store: &mockAuthStore{ - users: map[string]*v2auth.User{ - "user": { - User: "user", - Password: goodPassword, - Roles: []string{"foorole"}, - }, - }, - roles: map[string]*v2auth.Role{ - "foorole": { - Role: "foorole", - Permissions: v2auth.Permissions{ - KV: v2auth.RWPermission{ - Read: []string{"/foo*"}, - Write: []string{"/foo*"}, - }, - }, - }, - }, - enabled: true, - }, - hasRoot: false, - hasKeyPrefixAccess: true, - hasRecursiveAccess: true, - }, - { - key: "/foo", - req: mustAuthRequest("user", "bad"), - store: &mockAuthStore{ - users: map[string]*v2auth.User{ - "user": { - User: "user", - Password: goodPassword, - Roles: []string{"foorole"}, - }, - }, - roles: map[string]*v2auth.Role{ - "foorole": { - Role: "foorole", - Permissions: v2auth.Permissions{ - KV: v2auth.RWPermission{ - Read: []string{"/foo*"}, - Write: []string{"/foo*"}, - }, - }, - }, - }, - enabled: true, - }, - hasRoot: false, - hasKeyPrefixAccess: false, - hasRecursiveAccess: false, - }, - { - key: "/foo", - req: mustAuthRequest("user", "good"), - store: &mockAuthStore{ - users: map[string]*v2auth.User{}, - err: errors.New("Not the user"), - enabled: true, - }, - hasRoot: false, - hasKeyPrefixAccess: false, - hasRecursiveAccess: false, - }, - { - key: "/foo", - req: mustJSONRequest(t, "GET", "somepath", ""), - store: &mockAuthStore{ - users: map[string]*v2auth.User{ - "user": { - User: "user", - Password: goodPassword, - Roles: []string{"foorole"}, - }, - }, - roles: map[string]*v2auth.Role{ - "guest": { - Role: "guest", - Permissions: v2auth.Permissions{ - KV: v2auth.RWPermission{ - Read: []string{"/foo*"}, - Write: []string{"/foo*"}, - }, - }, - }, - }, - enabled: true, - }, - hasRoot: false, - hasKeyPrefixAccess: true, - hasRecursiveAccess: true, - }, - { - key: "/bar", - req: mustJSONRequest(t, "GET", "somepath", ""), - store: &mockAuthStore{ - users: map[string]*v2auth.User{ - "user": { - User: "user", - Password: goodPassword, - Roles: []string{"foorole"}, - }, - }, - roles: map[string]*v2auth.Role{ - "guest": { - Role: "guest", - Permissions: v2auth.Permissions{ - KV: v2auth.RWPermission{ - Read: []string{"/foo*"}, - Write: []string{"/foo*"}, - }, - }, - }, - }, - enabled: true, - }, - hasRoot: false, - hasKeyPrefixAccess: false, - hasRecursiveAccess: false, - }, - // check access for multiple roles - { - key: "/foo", - req: mustAuthRequest("user", "good"), - store: &mockAuthStore{ - users: map[string]*v2auth.User{ - "user": { - User: "user", - Password: goodPassword, - Roles: []string{"role1", "role2"}, - }, - }, - roles: map[string]*v2auth.Role{ - "role1": { - Role: "role1", - }, - "role2": { - Role: "role2", - Permissions: v2auth.Permissions{ - KV: v2auth.RWPermission{ - Read: []string{"/foo"}, - Write: []string{"/foo"}, - }, - }, - }, - }, - enabled: true, - }, - hasRoot: false, - hasKeyPrefixAccess: true, - hasRecursiveAccess: false, - }, - { - key: "/foo", - req: (func() *http.Request { - req := mustJSONRequest(t, "GET", "somepath", "") - req.Header.Set("Authorization", "malformedencoding") - return req - })(), - store: &mockAuthStore{ - enabled: true, - users: map[string]*v2auth.User{ - "root": { - User: "root", - Password: goodPassword, - Roles: []string{"root"}, - }, - }, - roles: map[string]*v2auth.Role{ - "guest": { - Role: "guest", - Permissions: v2auth.Permissions{ - KV: v2auth.RWPermission{ - Read: []string{"/foo*"}, - Write: []string{"/foo*"}, - }, - }, - }, - }, - }, - hasRoot: false, - hasKeyPrefixAccess: false, - hasRecursiveAccess: false, - }, - { // guest access in non-TLS mode - key: "/foo", - req: (func() *http.Request { - return mustJSONRequest(t, "GET", "somepath", "") - })(), - store: &mockAuthStore{ - enabled: true, - users: map[string]*v2auth.User{ - "root": { - User: "root", - Password: goodPassword, - Roles: []string{"root"}, - }, - }, - roles: map[string]*v2auth.Role{ - "guest": { - Role: "guest", - Permissions: v2auth.Permissions{ - KV: v2auth.RWPermission{ - Read: []string{"/foo*"}, - Write: []string{"/foo*"}, - }, - }, - }, - }, - }, - hasRoot: false, - hasKeyPrefixAccess: true, - hasRecursiveAccess: true, - }, - } - - for i, tt := range table { - if tt.hasRoot != hasRootAccess(zap.NewExample(), tt.store, tt.req, true) { - t.Errorf("#%d: hasRoot doesn't match (expected %v)", i, tt.hasRoot) - } - if tt.hasKeyPrefixAccess != hasKeyPrefixAccess(zap.NewExample(), tt.store, tt.req, tt.key, false, true) { - t.Errorf("#%d: hasKeyPrefixAccess doesn't match (expected %v)", i, tt.hasRoot) - } - if tt.hasRecursiveAccess != hasKeyPrefixAccess(zap.NewExample(), tt.store, tt.req, tt.key, true, true) { - t.Errorf("#%d: hasRecursiveAccess doesn't match (expected %v)", i, tt.hasRoot) - } - } -} - -func TestUserFromClientCertificate(t *testing.T) { - witherror := &mockAuthStore{ - users: map[string]*v2auth.User{ - "user": { - User: "user", - Roles: []string{"root"}, - Password: "password", - }, - "basicauth": { - User: "basicauth", - Roles: []string{"root"}, - Password: "password", - }, - }, - roles: map[string]*v2auth.Role{ - "root": { - Role: "root", - }, - }, - err: errors.New(""), - } - - noerror := &mockAuthStore{ - users: map[string]*v2auth.User{ - "user": { - User: "user", - Roles: []string{"root"}, - Password: "password", - }, - "basicauth": { - User: "basicauth", - Roles: []string{"root"}, - Password: "password", - }, - }, - roles: map[string]*v2auth.Role{ - "root": { - Role: "root", - }, - }, - } - - var table = []struct { - req *http.Request - userExists bool - store v2auth.Store - username string - }{ - { - // non tls request - req: unauthedRequest(), - userExists: false, - store: witherror, - }, - { - // cert with cn of existing user - req: tlsAuthedRequest(unauthedRequest(), "user"), - userExists: true, - username: "user", - store: noerror, - }, - { - // cert with cn of non-existing user - req: tlsAuthedRequest(unauthedRequest(), "otheruser"), - userExists: false, - store: witherror, - }, - } - - for i, tt := range table { - user := userFromClientCertificate(zap.NewExample(), tt.store, tt.req) - userExists := user != nil - - if tt.userExists != userExists { - t.Errorf("#%d: userFromClientCertificate doesn't match (expected %v)", i, tt.userExists) - } - if user != nil && (tt.username != user.User) { - t.Errorf("#%d: userFromClientCertificate username doesn't match (expected %s, got %s)", i, tt.username, user.User) - } - } -} - -func TestUserFromBasicAuth(t *testing.T) { - sec := &mockAuthStore{ - users: map[string]*v2auth.User{ - "user": { - User: "user", - Roles: []string{"root"}, - Password: "password", - }, - }, - roles: map[string]*v2auth.Role{ - "root": { - Role: "root", - }, - }, - } - - var table = []struct { - username string - req *http.Request - userExists bool - }{ - { - // valid user, valid pass - username: "user", - req: mustAuthRequest("user", "password"), - userExists: true, - }, - { - // valid user, bad pass - username: "user", - req: mustAuthRequest("user", "badpass"), - userExists: false, - }, - { - // valid user, no pass - username: "user", - req: mustAuthRequest("user", ""), - userExists: false, - }, - { - // missing user - username: "missing", - req: mustAuthRequest("missing", "badpass"), - userExists: false, - }, - { - // no basic auth - req: unauthedRequest(), - userExists: false, - }, - } - - for i, tt := range table { - user := userFromBasicAuth(zap.NewExample(), sec, tt.req) - userExists := user != nil - - if tt.userExists != userExists { - t.Errorf("#%d: userFromBasicAuth doesn't match (expected %v)", i, tt.userExists) - } - if user != nil && (tt.username != user.User) { - t.Errorf("#%d: userFromBasicAuth username doesn't match (expected %s, got %s)", i, tt.username, user.User) - } - } -} diff --git a/server/etcdserver/api/v2http/client_test.go b/server/etcdserver/api/v2http/client_test.go deleted file mode 100644 index 49a5da007..000000000 --- a/server/etcdserver/api/v2http/client_test.go +++ /dev/null @@ -1,2080 +0,0 @@ -// Copyright 2015 The etcd Authors -// -// 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 v2http - -import ( - "bytes" - "context" - "encoding/json" - "errors" - "io" - "net/http" - "net/http/httptest" - "net/url" - "path" - "reflect" - "strings" - "testing" - "time" - - "go.etcd.io/etcd/api/v3/etcdserverpb" - "go.etcd.io/etcd/client/pkg/v3/testutil" - "go.etcd.io/etcd/client/pkg/v3/types" - "go.etcd.io/etcd/raft/v3/raftpb" - "go.etcd.io/etcd/server/v3/etcdserver" - "go.etcd.io/etcd/server/v3/etcdserver/api" - "go.etcd.io/etcd/server/v3/etcdserver/api/membership" - "go.etcd.io/etcd/server/v3/etcdserver/api/v2error" - "go.etcd.io/etcd/server/v3/etcdserver/api/v2http/httptypes" - "go.etcd.io/etcd/server/v3/etcdserver/api/v2store" - - "github.com/coreos/go-semver/semver" - "github.com/jonboulle/clockwork" - "go.uber.org/zap" -) - -func mustMarshalEvent(t *testing.T, ev *v2store.Event) string { - b := new(bytes.Buffer) - if err := json.NewEncoder(b).Encode(ev); err != nil { - t.Fatalf("error marshalling event %#v: %v", ev, err) - } - return b.String() -} - -// mustNewForm takes a set of Values and constructs a PUT *http.Request, -// with a URL constructed from appending the given path to the standard keysPrefix -func mustNewForm(t *testing.T, p string, vals url.Values) *http.Request { - u := testutil.MustNewURL(t, path.Join(keysPrefix, p)) - req, err := http.NewRequest("PUT", u.String(), strings.NewReader(vals.Encode())) - req.Header.Set("Content-Type", "application/x-www-form-urlencoded") - if err != nil { - t.Fatalf("error creating new request: %v", err) - } - return req -} - -// mustNewPostForm takes a set of Values and constructs a POST *http.Request, -// with a URL constructed from appending the given path to the standard keysPrefix -func mustNewPostForm(t *testing.T, p string, vals url.Values) *http.Request { - u := testutil.MustNewURL(t, path.Join(keysPrefix, p)) - req, err := http.NewRequest("POST", u.String(), strings.NewReader(vals.Encode())) - req.Header.Set("Content-Type", "application/x-www-form-urlencoded") - if err != nil { - t.Fatalf("error creating new request: %v", err) - } - return req -} - -// mustNewRequest takes a path, appends it to the standard keysPrefix, and constructs -// a GET *http.Request referencing the resulting URL -func mustNewRequest(t *testing.T, p string) *http.Request { - return mustNewMethodRequest(t, "GET", p) -} - -func mustNewMethodRequest(t *testing.T, m, p string) *http.Request { - return &http.Request{ - Method: m, - URL: testutil.MustNewURL(t, path.Join(keysPrefix, p)), - } -} - -type fakeServer struct { - dummyRaftTimer - dummyStats -} - -func (s *fakeServer) Leader() types.ID { return types.ID(1) } -func (s *fakeServer) Alarms() []*etcdserverpb.AlarmMember { return nil } -func (s *fakeServer) LeaderChangedNotify() <-chan struct{} { return nil } -func (s *fakeServer) Cluster() api.Cluster { return nil } -func (s *fakeServer) ClusterVersion() *semver.Version { return nil } -func (s *fakeServer) RaftHandler() http.Handler { return nil } -func (s *fakeServer) Do(ctx context.Context, r etcdserverpb.Request) (rr etcdserver.Response, err error) { - return -} -func (s *fakeServer) ClientCertAuthEnabled() bool { return false } - -type serverRecorder struct { - fakeServer - actions []action -} - -func (s *serverRecorder) Do(_ context.Context, r etcdserverpb.Request) (etcdserver.Response, error) { - s.actions = append(s.actions, action{name: "Do", params: []interface{}{r}}) - return etcdserver.Response{}, nil -} -func (s *serverRecorder) Process(_ context.Context, m raftpb.Message) error { - s.actions = append(s.actions, action{name: "Process", params: []interface{}{m}}) - return nil -} -func (s *serverRecorder) AddMember(_ context.Context, m membership.Member) ([]*membership.Member, error) { - s.actions = append(s.actions, action{name: "AddMember", params: []interface{}{m}}) - return nil, nil -} -func (s *serverRecorder) RemoveMember(_ context.Context, id uint64) ([]*membership.Member, error) { - s.actions = append(s.actions, action{name: "RemoveMember", params: []interface{}{id}}) - return nil, nil -} - -func (s *serverRecorder) UpdateMember(_ context.Context, m membership.Member) ([]*membership.Member, error) { - s.actions = append(s.actions, action{name: "UpdateMember", params: []interface{}{m}}) - return nil, nil -} - -func (s *serverRecorder) PromoteMember(_ context.Context, id uint64) ([]*membership.Member, error) { - s.actions = append(s.actions, action{name: "PromoteMember", params: []interface{}{id}}) - return nil, nil -} - -type action struct { - name string - params []interface{} -} - -// flushingRecorder provides a channel to allow users to block until the Recorder is Flushed() -type flushingRecorder struct { - *httptest.ResponseRecorder - ch chan struct{} -} - -func (fr *flushingRecorder) Flush() { - fr.ResponseRecorder.Flush() - fr.ch <- struct{}{} -} - -// resServer implements the etcd.Server interface for testing. -// It returns the given response from any Do calls, and nil error -type resServer struct { - fakeServer - res etcdserver.Response -} - -func (rs *resServer) Do(_ context.Context, _ etcdserverpb.Request) (etcdserver.Response, error) { - return rs.res, nil -} -func (rs *resServer) Process(_ context.Context, _ raftpb.Message) error { return nil } -func (rs *resServer) AddMember(_ context.Context, _ membership.Member) ([]*membership.Member, error) { - return nil, nil -} -func (rs *resServer) RemoveMember(_ context.Context, _ uint64) ([]*membership.Member, error) { - return nil, nil -} -func (rs *resServer) UpdateMember(_ context.Context, _ membership.Member) ([]*membership.Member, error) { - return nil, nil -} -func (rs *resServer) PromoteMember(_ context.Context, _ uint64) ([]*membership.Member, error) { - return nil, nil -} - -func boolp(b bool) *bool { return &b } - -type dummyRaftTimer struct{} - -func (drt dummyRaftTimer) Index() uint64 { return uint64(100) } -func (drt dummyRaftTimer) Term() uint64 { return uint64(5) } - -type dummyWatcher struct { - echan chan *v2store.Event - sidx uint64 -} - -func (w *dummyWatcher) EventChan() chan *v2store.Event { - return w.echan -} -func (w *dummyWatcher) StartIndex() uint64 { return w.sidx } -func (w *dummyWatcher) Remove() {} - -func TestBadRefreshRequest(t *testing.T) { - tests := []struct { - in *http.Request - wcode int - }{ - { - mustNewRequest(t, "foo?refresh=true&value=test"), - v2error.EcodeRefreshValue, - }, - { - mustNewRequest(t, "foo?refresh=true&value=10"), - v2error.EcodeRefreshValue, - }, - { - mustNewRequest(t, "foo?refresh=true"), - v2error.EcodeRefreshTTLRequired, - }, - { - mustNewRequest(t, "foo?refresh=true&ttl="), - v2error.EcodeRefreshTTLRequired, - }, - } - for i, tt := range tests { - got, _, err := parseKeyRequest(tt.in, clockwork.NewFakeClock()) - if err == nil { - t.Errorf("#%d: unexpected nil error!", i) - continue - } - ee, ok := err.(*v2error.Error) - if !ok { - t.Errorf("#%d: err is not etcd.Error!", i) - continue - } - if ee.ErrorCode != tt.wcode { - t.Errorf("#%d: code=%d, want %v", i, ee.ErrorCode, tt.wcode) - t.Logf("cause: %#v", ee.Cause) - } - if !reflect.DeepEqual(got, etcdserverpb.Request{}) { - t.Errorf("#%d: unexpected non-empty Request: %#v", i, got) - } - } -} - -func TestBadParseRequest(t *testing.T) { - tests := []struct { - in *http.Request - wcode int - }{ - { - // parseForm failure - &http.Request{ - Body: nil, - Method: "PUT", - }, - v2error.EcodeInvalidForm, - }, - { - // bad key prefix - &http.Request{ - URL: testutil.MustNewURL(t, "/badprefix/"), - }, - v2error.EcodeInvalidForm, - }, - // bad values for prevIndex, waitIndex, ttl - { - mustNewForm(t, "foo", url.Values{"prevIndex": []string{"garbage"}}), - v2error.EcodeIndexNaN, - }, - { - mustNewForm(t, "foo", url.Values{"prevIndex": []string{"1.5"}}), - v2error.EcodeIndexNaN, - }, - { - mustNewForm(t, "foo", url.Values{"prevIndex": []string{"-1"}}), - v2error.EcodeIndexNaN, - }, - { - mustNewForm(t, "foo", url.Values{"waitIndex": []string{"garbage"}}), - v2error.EcodeIndexNaN, - }, - { - mustNewForm(t, "foo", url.Values{"waitIndex": []string{"??"}}), - v2error.EcodeIndexNaN, - }, - { - mustNewForm(t, "foo", url.Values{"ttl": []string{"-1"}}), - v2error.EcodeTTLNaN, - }, - // bad values for recursive, sorted, wait, prevExist, dir, stream - { - mustNewForm(t, "foo", url.Values{"recursive": []string{"hahaha"}}), - v2error.EcodeInvalidField, - }, - { - mustNewForm(t, "foo", url.Values{"recursive": []string{"1234"}}), - v2error.EcodeInvalidField, - }, - { - mustNewForm(t, "foo", url.Values{"recursive": []string{"?"}}), - v2error.EcodeInvalidField, - }, - { - mustNewForm(t, "foo", url.Values{"sorted": []string{"?"}}), - v2error.EcodeInvalidField, - }, - { - mustNewForm(t, "foo", url.Values{"sorted": []string{"x"}}), - v2error.EcodeInvalidField, - }, - { - mustNewForm(t, "foo", url.Values{"wait": []string{"?!"}}), - v2error.EcodeInvalidField, - }, - { - mustNewForm(t, "foo", url.Values{"wait": []string{"yes"}}), - v2error.EcodeInvalidField, - }, - { - mustNewForm(t, "foo", url.Values{"prevExist": []string{"yes"}}), - v2error.EcodeInvalidField, - }, - { - mustNewForm(t, "foo", url.Values{"prevExist": []string{"#2"}}), - v2error.EcodeInvalidField, - }, - { - mustNewForm(t, "foo", url.Values{"dir": []string{"no"}}), - v2error.EcodeInvalidField, - }, - { - mustNewForm(t, "foo", url.Values{"dir": []string{"file"}}), - v2error.EcodeInvalidField, - }, - { - mustNewForm(t, "foo", url.Values{"quorum": []string{"no"}}), - v2error.EcodeInvalidField, - }, - { - mustNewForm(t, "foo", url.Values{"quorum": []string{"file"}}), - v2error.EcodeInvalidField, - }, - { - mustNewForm(t, "foo", url.Values{"stream": []string{"zzz"}}), - v2error.EcodeInvalidField, - }, - { - mustNewForm(t, "foo", url.Values{"stream": []string{"something"}}), - v2error.EcodeInvalidField, - }, - // prevValue cannot be empty - { - mustNewForm(t, "foo", url.Values{"prevValue": []string{""}}), - v2error.EcodePrevValueRequired, - }, - // wait is only valid with GET requests - { - mustNewMethodRequest(t, "HEAD", "foo?wait=true"), - v2error.EcodeInvalidField, - }, - // query values are considered - { - mustNewRequest(t, "foo?prevExist=wrong"), - v2error.EcodeInvalidField, - }, - { - mustNewRequest(t, "foo?ttl=wrong"), - v2error.EcodeTTLNaN, - }, - // but body takes precedence if both are specified - { - mustNewForm( - t, - "foo?ttl=12", - url.Values{"ttl": []string{"garbage"}}, - ), - v2error.EcodeTTLNaN, - }, - { - mustNewForm( - t, - "foo?prevExist=false", - url.Values{"prevExist": []string{"yes"}}, - ), - v2error.EcodeInvalidField, - }, - } - for i, tt := range tests { - got, _, err := parseKeyRequest(tt.in, clockwork.NewFakeClock()) - if err == nil { - t.Errorf("#%d: unexpected nil error!", i) - continue - } - ee, ok := err.(*v2error.Error) - if !ok { - t.Errorf("#%d: err is not etcd.Error!", i) - continue - } - if ee.ErrorCode != tt.wcode { - t.Errorf("#%d: code=%d, want %v", i, ee.ErrorCode, tt.wcode) - t.Logf("cause: %#v", ee.Cause) - } - if !reflect.DeepEqual(got, etcdserverpb.Request{}) { - t.Errorf("#%d: unexpected non-empty Request: %#v", i, got) - } - } -} - -func TestGoodParseRequest(t *testing.T) { - fc := clockwork.NewFakeClock() - fc.Advance(1111) - tests := []struct { - in *http.Request - w etcdserverpb.Request - noValue bool - }{ - { - // good prefix, all other values default - mustNewRequest(t, "foo"), - etcdserverpb.Request{ - Method: "GET", - Path: path.Join(etcdserver.StoreKeysPrefix, "/foo"), - }, - false, - }, - { - // value specified - mustNewForm( - t, - "foo", - url.Values{"value": []string{"some_value"}}, - ), - etcdserverpb.Request{ - Method: "PUT", - Val: "some_value", - Path: path.Join(etcdserver.StoreKeysPrefix, "/foo"), - }, - false, - }, - { - // prevIndex specified - mustNewForm( - t, - "foo", - url.Values{"prevIndex": []string{"98765"}}, - ), - etcdserverpb.Request{ - Method: "PUT", - PrevIndex: 98765, - Path: path.Join(etcdserver.StoreKeysPrefix, "/foo"), - }, - false, - }, - { - // recursive specified - mustNewForm( - t, - "foo", - url.Values{"recursive": []string{"true"}}, - ), - etcdserverpb.Request{ - Method: "PUT", - Recursive: true, - Path: path.Join(etcdserver.StoreKeysPrefix, "/foo"), - }, - false, - }, - { - // sorted specified - mustNewForm( - t, - "foo", - url.Values{"sorted": []string{"true"}}, - ), - etcdserverpb.Request{ - Method: "PUT", - Sorted: true, - Path: path.Join(etcdserver.StoreKeysPrefix, "/foo"), - }, - false, - }, - { - // quorum specified - mustNewForm( - t, - "foo", - url.Values{"quorum": []string{"true"}}, - ), - etcdserverpb.Request{ - Method: "PUT", - Quorum: true, - Path: path.Join(etcdserver.StoreKeysPrefix, "/foo"), - }, - false, - }, - { - // wait specified - mustNewRequest(t, "foo?wait=true"), - etcdserverpb.Request{ - Method: "GET", - Wait: true, - Path: path.Join(etcdserver.StoreKeysPrefix, "/foo"), - }, - false, - }, - { - // empty TTL specified - mustNewRequest(t, "foo?ttl="), - etcdserverpb.Request{ - Method: "GET", - Path: path.Join(etcdserver.StoreKeysPrefix, "/foo"), - Expiration: 0, - }, - false, - }, - { - // non-empty TTL specified - mustNewRequest(t, "foo?ttl=5678"), - etcdserverpb.Request{ - Method: "GET", - Path: path.Join(etcdserver.StoreKeysPrefix, "/foo"), - Expiration: fc.Now().Add(5678 * time.Second).UnixNano(), - }, - false, - }, - { - // zero TTL specified - mustNewRequest(t, "foo?ttl=0"), - etcdserverpb.Request{ - Method: "GET", - Path: path.Join(etcdserver.StoreKeysPrefix, "/foo"), - Expiration: fc.Now().UnixNano(), - }, - false, - }, - { - // dir specified - mustNewRequest(t, "foo?dir=true"), - etcdserverpb.Request{ - Method: "GET", - Dir: true, - Path: path.Join(etcdserver.StoreKeysPrefix, "/foo"), - }, - false, - }, - { - // dir specified negatively - mustNewRequest(t, "foo?dir=false"), - etcdserverpb.Request{ - Method: "GET", - Dir: false, - Path: path.Join(etcdserver.StoreKeysPrefix, "/foo"), - }, - false, - }, - { - // prevExist should be non-null if specified - mustNewForm( - t, - "foo", - url.Values{"prevExist": []string{"true"}}, - ), - etcdserverpb.Request{ - Method: "PUT", - PrevExist: boolp(true), - Path: path.Join(etcdserver.StoreKeysPrefix, "/foo"), - }, - false, - }, - { - // prevExist should be non-null if specified - mustNewForm( - t, - "foo", - url.Values{"prevExist": []string{"false"}}, - ), - etcdserverpb.Request{ - Method: "PUT", - PrevExist: boolp(false), - Path: path.Join(etcdserver.StoreKeysPrefix, "/foo"), - }, - false, - }, - // mix various fields - { - mustNewForm( - t, - "foo", - url.Values{ - "value": []string{"some value"}, - "prevExist": []string{"true"}, - "prevValue": []string{"previous value"}, - }, - ), - etcdserverpb.Request{ - Method: "PUT", - PrevExist: boolp(true), - PrevValue: "previous value", - Val: "some value", - Path: path.Join(etcdserver.StoreKeysPrefix, "/foo"), - }, - false, - }, - // query parameters should be used if given - { - mustNewForm( - t, - "foo?prevValue=woof", - url.Values{}, - ), - etcdserverpb.Request{ - Method: "PUT", - PrevValue: "woof", - Path: path.Join(etcdserver.StoreKeysPrefix, "/foo"), - }, - false, - }, - // but form values should take precedence over query parameters - { - mustNewForm( - t, - "foo?prevValue=woof", - url.Values{ - "prevValue": []string{"miaow"}, - }, - ), - etcdserverpb.Request{ - Method: "PUT", - PrevValue: "miaow", - Path: path.Join(etcdserver.StoreKeysPrefix, "/foo"), - }, - false, - }, - { - // noValueOnSuccess specified - mustNewForm( - t, - "foo", - url.Values{"noValueOnSuccess": []string{"true"}}, - ), - etcdserverpb.Request{ - Method: "PUT", - Path: path.Join(etcdserver.StoreKeysPrefix, "/foo"), - }, - true, - }, - } - - for i, tt := range tests { - got, noValueOnSuccess, err := parseKeyRequest(tt.in, fc) - if err != nil { - t.Errorf("#%d: err = %v, want %v", i, err, nil) - } - - if noValueOnSuccess != tt.noValue { - t.Errorf("#%d: noValue=%t, want %t", i, noValueOnSuccess, tt.noValue) - } - - if !reflect.DeepEqual(got, tt.w) { - t.Errorf("#%d: request=%#v, want %#v", i, got, tt.w) - } - } -} - -func TestServeMembers(t *testing.T) { - memb1 := membership.Member{ID: 12, Attributes: membership.Attributes{ClientURLs: []string{"http://localhost:8080"}}} - memb2 := membership.Member{ID: 13, Attributes: membership.Attributes{ClientURLs: []string{"http://localhost:8081"}}} - cluster := &fakeCluster{ - id: 1, - members: map[uint64]*membership.Member{1: &memb1, 2: &memb2}, - } - h := &membersHandler{ - lg: zap.NewExample(), - server: &serverRecorder{}, - clock: clockwork.NewFakeClock(), - cluster: cluster, - } - - wmc := string(`{"members":[{"id":"c","name":"","peerURLs":[],"clientURLs":["http://localhost:8080"]},{"id":"d","name":"","peerURLs":[],"clientURLs":["http://localhost:8081"]}]}`) - - tests := []struct { - path string - wcode int - wct string - wbody string - }{ - {membersPrefix, http.StatusOK, "application/json", wmc + "\n"}, - {membersPrefix + "/", http.StatusOK, "application/json", wmc + "\n"}, - {path.Join(membersPrefix, "100"), http.StatusNotFound, "application/json", `{"message":"Not found"}`}, - {path.Join(membersPrefix, "foobar"), http.StatusNotFound, "application/json", `{"message":"Not found"}`}, - } - - for i, tt := range tests { - req, err := http.NewRequest("GET", testutil.MustNewURL(t, tt.path).String(), nil) - if err != nil { - t.Fatal(err) - } - rw := httptest.NewRecorder() - h.ServeHTTP(rw, req) - - if rw.Code != tt.wcode { - t.Errorf("#%d: code=%d, want %d", i, rw.Code, tt.wcode) - } - if gct := rw.Header().Get("Content-Type"); gct != tt.wct { - t.Errorf("#%d: content-type = %s, want %s", i, gct, tt.wct) - } - gcid := rw.Header().Get("X-Etcd-Cluster-ID") - wcid := cluster.ID().String() - if gcid != wcid { - t.Errorf("#%d: cid = %s, want %s", i, gcid, wcid) - } - if rw.Body.String() != tt.wbody { - t.Errorf("#%d: body = %q, want %q", i, rw.Body.String(), tt.wbody) - } - } -} - -// TODO: consolidate **ALL** fake server implementations and add no leader test case. -func TestServeLeader(t *testing.T) { - memb1 := membership.Member{ID: 1, Attributes: membership.Attributes{ClientURLs: []string{"http://localhost:8080"}}} - memb2 := membership.Member{ID: 2, Attributes: membership.Attributes{ClientURLs: []string{"http://localhost:8081"}}} - cluster := &fakeCluster{ - id: 1, - members: map[uint64]*membership.Member{1: &memb1, 2: &memb2}, - } - h := &membersHandler{ - lg: zap.NewExample(), - server: &serverRecorder{}, - clock: clockwork.NewFakeClock(), - cluster: cluster, - } - - wmc := string(`{"id":"1","name":"","peerURLs":[],"clientURLs":["http://localhost:8080"]}`) - - tests := []struct { - path string - wcode int - wct string - wbody string - }{ - {membersPrefix + "leader", http.StatusOK, "application/json", wmc + "\n"}, - // TODO: add no leader case - } - - for i, tt := range tests { - req, err := http.NewRequest("GET", testutil.MustNewURL(t, tt.path).String(), nil) - if err != nil { - t.Fatal(err) - } - rw := httptest.NewRecorder() - h.ServeHTTP(rw, req) - - if rw.Code != tt.wcode { - t.Errorf("#%d: code=%d, want %d", i, rw.Code, tt.wcode) - } - if gct := rw.Header().Get("Content-Type"); gct != tt.wct { - t.Errorf("#%d: content-type = %s, want %s", i, gct, tt.wct) - } - gcid := rw.Header().Get("X-Etcd-Cluster-ID") - wcid := cluster.ID().String() - if gcid != wcid { - t.Errorf("#%d: cid = %s, want %s", i, gcid, wcid) - } - if rw.Body.String() != tt.wbody { - t.Errorf("#%d: body = %q, want %q", i, rw.Body.String(), tt.wbody) - } - } -} - -func TestServeMembersCreate(t *testing.T) { - u := testutil.MustNewURL(t, membersPrefix) - b := []byte(`{"peerURLs":["http://127.0.0.1:1"]}`) - req, err := http.NewRequest("POST", u.String(), bytes.NewReader(b)) - if err != nil { - t.Fatal(err) - } - req.Header.Set("Content-Type", "application/json") - s := &serverRecorder{} - h := &membersHandler{ - lg: zap.NewExample(), - server: s, - clock: clockwork.NewFakeClock(), - cluster: &fakeCluster{id: 1}, - } - rw := httptest.NewRecorder() - - h.ServeHTTP(rw, req) - - wcode := http.StatusCreated - if rw.Code != wcode { - t.Errorf("code=%d, want %d", rw.Code, wcode) - } - - wct := "application/json" - if gct := rw.Header().Get("Content-Type"); gct != wct { - t.Errorf("content-type = %s, want %s", gct, wct) - } - gcid := rw.Header().Get("X-Etcd-Cluster-ID") - wcid := h.cluster.ID().String() - if gcid != wcid { - t.Errorf("cid = %s, want %s", gcid, wcid) - } - - wb := `{"id":"c29b431f04be0bc7","name":"","peerURLs":["http://127.0.0.1:1"],"clientURLs":[]}` + "\n" - g := rw.Body.String() - if g != wb { - t.Errorf("got body=%q, want %q", g, wb) - } - - wm := membership.Member{ - ID: 14022875665250782151, - RaftAttributes: membership.RaftAttributes{ - PeerURLs: []string{"http://127.0.0.1:1"}, - }, - } - - wactions := []action{{name: "AddMember", params: []interface{}{wm}}} - if !reflect.DeepEqual(s.actions, wactions) { - t.Errorf("actions = %+v, want %+v", s.actions, wactions) - } -} - -func TestServeMembersDelete(t *testing.T) { - req := &http.Request{ - Method: "DELETE", - URL: testutil.MustNewURL(t, path.Join(membersPrefix, "BEEF")), - } - s := &serverRecorder{} - h := &membersHandler{ - lg: zap.NewExample(), - server: s, - cluster: &fakeCluster{id: 1}, - } - rw := httptest.NewRecorder() - - h.ServeHTTP(rw, req) - - wcode := http.StatusNoContent - if rw.Code != wcode { - t.Errorf("code=%d, want %d", rw.Code, wcode) - } - gcid := rw.Header().Get("X-Etcd-Cluster-ID") - wcid := h.cluster.ID().String() - if gcid != wcid { - t.Errorf("cid = %s, want %s", gcid, wcid) - } - g := rw.Body.String() - if g != "" { - t.Errorf("got body=%q, want %q", g, "") - } - wactions := []action{{name: "RemoveMember", params: []interface{}{uint64(0xBEEF)}}} - if !reflect.DeepEqual(s.actions, wactions) { - t.Errorf("actions = %+v, want %+v", s.actions, wactions) - } -} - -func TestServeMembersUpdate(t *testing.T) { - u := testutil.MustNewURL(t, path.Join(membersPrefix, "1")) - b := []byte(`{"peerURLs":["http://127.0.0.1:1"]}`) - req, err := http.NewRequest("PUT", u.String(), bytes.NewReader(b)) - if err != nil { - t.Fatal(err) - } - req.Header.Set("Content-Type", "application/json") - s := &serverRecorder{} - h := &membersHandler{ - lg: zap.NewExample(), - server: s, - clock: clockwork.NewFakeClock(), - cluster: &fakeCluster{id: 1}, - } - rw := httptest.NewRecorder() - - h.ServeHTTP(rw, req) - - wcode := http.StatusNoContent - if rw.Code != wcode { - t.Errorf("code=%d, want %d", rw.Code, wcode) - } - - gcid := rw.Header().Get("X-Etcd-Cluster-ID") - wcid := h.cluster.ID().String() - if gcid != wcid { - t.Errorf("cid = %s, want %s", gcid, wcid) - } - - wm := membership.Member{ - ID: 1, - RaftAttributes: membership.RaftAttributes{ - PeerURLs: []string{"http://127.0.0.1:1"}, - }, - } - - wactions := []action{{name: "UpdateMember", params: []interface{}{wm}}} - if !reflect.DeepEqual(s.actions, wactions) { - t.Errorf("actions = %+v, want %+v", s.actions, wactions) - } -} - -func TestServeMembersFail(t *testing.T) { - tests := []struct { - req *http.Request - server etcdserver.ServerV2 - - wcode int - }{ - { - // bad method - &http.Request{ - Method: "CONNECT", - }, - &resServer{}, - - http.StatusMethodNotAllowed, - }, - { - // bad method - &http.Request{ - Method: "TRACE", - }, - &resServer{}, - - http.StatusMethodNotAllowed, - }, - { - // parse body error - &http.Request{ - URL: testutil.MustNewURL(t, membersPrefix), - Method: "POST", - Body: io.NopCloser(strings.NewReader("bad json")), - Header: map[string][]string{"Content-Type": {"application/json"}}, - }, - &resServer{}, - - http.StatusBadRequest, - }, - { - // bad content type - &http.Request{ - URL: testutil.MustNewURL(t, membersPrefix), - Method: "POST", - Body: io.NopCloser(strings.NewReader(`{"PeerURLs": ["http://127.0.0.1:1"]}`)), - Header: map[string][]string{"Content-Type": {"application/bad"}}, - }, - &errServer{}, - - http.StatusUnsupportedMediaType, - }, - { - // bad url - &http.Request{ - URL: testutil.MustNewURL(t, membersPrefix), - Method: "POST", - Body: io.NopCloser(strings.NewReader(`{"PeerURLs": ["http://a"]}`)), - Header: map[string][]string{"Content-Type": {"application/json"}}, - }, - &errServer{}, - - http.StatusBadRequest, - }, - { - // etcdserver.AddMember error - &http.Request{ - URL: testutil.MustNewURL(t, membersPrefix), - Method: "POST", - Body: io.NopCloser(strings.NewReader(`{"PeerURLs": ["http://127.0.0.1:1"]}`)), - Header: map[string][]string{"Content-Type": {"application/json"}}, - }, - &errServer{ - err: errors.New("Error while adding a member"), - }, - - http.StatusInternalServerError, - }, - { - // etcdserver.AddMember error - &http.Request{ - URL: testutil.MustNewURL(t, membersPrefix), - Method: "POST", - Body: io.NopCloser(strings.NewReader(`{"PeerURLs": ["http://127.0.0.1:1"]}`)), - Header: map[string][]string{"Content-Type": {"application/json"}}, - }, - &errServer{ - err: membership.ErrIDExists, - }, - - http.StatusConflict, - }, - { - // etcdserver.AddMember error - &http.Request{ - URL: testutil.MustNewURL(t, membersPrefix), - Method: "POST", - Body: io.NopCloser(strings.NewReader(`{"PeerURLs": ["http://127.0.0.1:1"]}`)), - Header: map[string][]string{"Content-Type": {"application/json"}}, - }, - &errServer{ - err: membership.ErrPeerURLexists, - }, - - http.StatusConflict, - }, - { - // etcdserver.RemoveMember error with arbitrary server error - &http.Request{ - URL: testutil.MustNewURL(t, path.Join(membersPrefix, "1")), - Method: "DELETE", - }, - &errServer{ - err: errors.New("Error while removing member"), - }, - - http.StatusInternalServerError, - }, - { - // etcdserver.RemoveMember error with previously removed ID - &http.Request{ - URL: testutil.MustNewURL(t, path.Join(membersPrefix, "0")), - Method: "DELETE", - }, - &errServer{ - err: membership.ErrIDRemoved, - }, - - http.StatusGone, - }, - { - // etcdserver.RemoveMember error with nonexistent ID - &http.Request{ - URL: testutil.MustNewURL(t, path.Join(membersPrefix, "0")), - Method: "DELETE", - }, - &errServer{ - err: membership.ErrIDNotFound, - }, - - http.StatusNotFound, - }, - { - // etcdserver.RemoveMember error with badly formed ID - &http.Request{ - URL: testutil.MustNewURL(t, path.Join(membersPrefix, "bad_id")), - Method: "DELETE", - }, - nil, - - http.StatusNotFound, - }, - { - // etcdserver.RemoveMember with no ID - &http.Request{ - URL: testutil.MustNewURL(t, membersPrefix), - Method: "DELETE", - }, - nil, - - http.StatusMethodNotAllowed, - }, - { - // parse body error - &http.Request{ - URL: testutil.MustNewURL(t, path.Join(membersPrefix, "0")), - Method: "PUT", - Body: io.NopCloser(strings.NewReader("bad json")), - Header: map[string][]string{"Content-Type": {"application/json"}}, - }, - &resServer{}, - - http.StatusBadRequest, - }, - { - // bad content type - &http.Request{ - URL: testutil.MustNewURL(t, path.Join(membersPrefix, "0")), - Method: "PUT", - Body: io.NopCloser(strings.NewReader(`{"PeerURLs": ["http://127.0.0.1:1"]}`)), - Header: map[string][]string{"Content-Type": {"application/bad"}}, - }, - &errServer{}, - - http.StatusUnsupportedMediaType, - }, - { - // bad url - &http.Request{ - URL: testutil.MustNewURL(t, path.Join(membersPrefix, "0")), - Method: "PUT", - Body: io.NopCloser(strings.NewReader(`{"PeerURLs": ["http://a"]}`)), - Header: map[string][]string{"Content-Type": {"application/json"}}, - }, - &errServer{}, - - http.StatusBadRequest, - }, - { - // etcdserver.UpdateMember error - &http.Request{ - URL: testutil.MustNewURL(t, path.Join(membersPrefix, "0")), - Method: "PUT", - Body: io.NopCloser(strings.NewReader(`{"PeerURLs": ["http://127.0.0.1:1"]}`)), - Header: map[string][]string{"Content-Type": {"application/json"}}, - }, - &errServer{ - err: errors.New("blah"), - }, - - http.StatusInternalServerError, - }, - { - // etcdserver.UpdateMember error - &http.Request{ - URL: testutil.MustNewURL(t, path.Join(membersPrefix, "0")), - Method: "PUT", - Body: io.NopCloser(strings.NewReader(`{"PeerURLs": ["http://127.0.0.1:1"]}`)), - Header: map[string][]string{"Content-Type": {"application/json"}}, - }, - &errServer{ - err: membership.ErrPeerURLexists, - }, - - http.StatusConflict, - }, - { - // etcdserver.UpdateMember error - &http.Request{ - URL: testutil.MustNewURL(t, path.Join(membersPrefix, "0")), - Method: "PUT", - Body: io.NopCloser(strings.NewReader(`{"PeerURLs": ["http://127.0.0.1:1"]}`)), - Header: map[string][]string{"Content-Type": {"application/json"}}, - }, - &errServer{ - err: membership.ErrIDNotFound, - }, - - http.StatusNotFound, - }, - { - // etcdserver.UpdateMember error with badly formed ID - &http.Request{ - URL: testutil.MustNewURL(t, path.Join(membersPrefix, "bad_id")), - Method: "PUT", - }, - nil, - - http.StatusNotFound, - }, - { - // etcdserver.UpdateMember with no ID - &http.Request{ - URL: testutil.MustNewURL(t, membersPrefix), - Method: "PUT", - }, - nil, - - http.StatusMethodNotAllowed, - }, - } - for i, tt := range tests { - h := &membersHandler{ - lg: zap.NewExample(), - server: tt.server, - cluster: &fakeCluster{id: 1}, - clock: clockwork.NewFakeClock(), - } - rw := httptest.NewRecorder() - h.ServeHTTP(rw, tt.req) - if rw.Code != tt.wcode { - t.Errorf("#%d: code=%d, want %d", i, rw.Code, tt.wcode) - } - if rw.Code != http.StatusMethodNotAllowed { - gcid := rw.Header().Get("X-Etcd-Cluster-ID") - wcid := h.cluster.ID().String() - if gcid != wcid { - t.Errorf("#%d: cid = %s, want %s", i, gcid, wcid) - } - } - } -} - -func TestWriteEvent(t *testing.T) { - // nil event should not panic - rec := httptest.NewRecorder() - writeKeyEvent(rec, etcdserver.Response{}, false) - h := rec.Header() - if len(h) > 0 { - t.Fatalf("unexpected non-empty headers: %#v", h) - } - b := rec.Body.String() - if len(b) > 0 { - t.Fatalf("unexpected non-empty body: %q", b) - } - - tests := []struct { - ev *v2store.Event - noValue bool - idx string - // TODO(jonboulle): check body as well as just status code - code int - err error - }{ - // standard case, standard 200 response - { - &v2store.Event{ - Action: v2store.Get, - Node: &v2store.NodeExtern{}, - PrevNode: &v2store.NodeExtern{}, - }, - false, - "0", - http.StatusOK, - nil, - }, - // check new nodes return StatusCreated - { - &v2store.Event{ - Action: v2store.Create, - Node: &v2store.NodeExtern{}, - PrevNode: &v2store.NodeExtern{}, - }, - false, - "0", - http.StatusCreated, - nil, - }, - } - - for i, tt := range tests { - rw := httptest.NewRecorder() - resp := etcdserver.Response{Event: tt.ev, Term: 5, Index: 100} - writeKeyEvent(rw, resp, tt.noValue) - if gct := rw.Header().Get("Content-Type"); gct != "application/json" { - t.Errorf("case %d: bad Content-Type: got %q, want application/json", i, gct) - } - if gri := rw.Header().Get("X-Raft-Index"); gri != "100" { - t.Errorf("case %d: bad X-Raft-Index header: got %s, want %s", i, gri, "100") - } - if grt := rw.Header().Get("X-Raft-Term"); grt != "5" { - t.Errorf("case %d: bad X-Raft-Term header: got %s, want %s", i, grt, "5") - } - if gei := rw.Header().Get("X-Etcd-Index"); gei != tt.idx { - t.Errorf("case %d: bad X-Etcd-Index header: got %s, want %s", i, gei, tt.idx) - } - if rw.Code != tt.code { - t.Errorf("case %d: bad response code: got %d, want %v", i, rw.Code, tt.code) - } - - } -} - -func TestV2DMachinesEndpoint(t *testing.T) { - tests := []struct { - method string - wcode int - }{ - {"GET", http.StatusOK}, - {"HEAD", http.StatusOK}, - {"POST", http.StatusMethodNotAllowed}, - } - - m := &machinesHandler{cluster: &fakeCluster{}} - s := httptest.NewServer(m) - defer s.Close() - - for _, tt := range tests { - req, err := http.NewRequest(tt.method, s.URL+machinesPrefix, nil) - if err != nil { - t.Fatal(err) - } - resp, err := http.DefaultClient.Do(req) - if err != nil { - t.Fatal(err) - } - - if resp.StatusCode != tt.wcode { - t.Errorf("StatusCode = %d, expected %d", resp.StatusCode, tt.wcode) - } - } -} - -func TestServeMachines(t *testing.T) { - cluster := &fakeCluster{ - clientURLs: []string{"http://localhost:8080", "http://localhost:8081", "http://localhost:8082"}, - } - writer := httptest.NewRecorder() - req, err := http.NewRequest("GET", "", nil) - if err != nil { - t.Fatal(err) - } - h := &machinesHandler{cluster: cluster} - h.ServeHTTP(writer, req) - w := "http://localhost:8080, http://localhost:8081, http://localhost:8082" - if g := writer.Body.String(); g != w { - t.Errorf("body = %s, want %s", g, w) - } - if writer.Code != http.StatusOK { - t.Errorf("code = %d, want %d", writer.Code, http.StatusOK) - } -} - -func TestGetID(t *testing.T) { - tests := []struct { - path string - - wok bool - wid types.ID - wcode int - }{ - { - "123", - true, 0x123, http.StatusOK, - }, - { - "bad_id", - false, 0, http.StatusNotFound, - }, - { - "", - false, 0, http.StatusMethodNotAllowed, - }, - } - - for i, tt := range tests { - w := httptest.NewRecorder() - id, ok := getID(zap.NewExample(), tt.path, w) - if id != tt.wid { - t.Errorf("#%d: id = %d, want %d", i, id, tt.wid) - } - if ok != tt.wok { - t.Errorf("#%d: ok = %t, want %t", i, ok, tt.wok) - } - if w.Code != tt.wcode { - t.Errorf("#%d code = %d, want %d", i, w.Code, tt.wcode) - } - } -} - -type dummyStats struct { - data []byte -} - -func (ds *dummyStats) SelfStats() []byte { return ds.data } -func (ds *dummyStats) LeaderStats() []byte { return ds.data } -func (ds *dummyStats) StoreStats() []byte { return ds.data } -func (ds *dummyStats) UpdateRecvApp(_ types.ID, _ int64) {} - -func TestServeSelfStats(t *testing.T) { - wb := []byte("some statistics") - w := string(wb) - sh := &statsHandler{ - lg: zap.NewExample(), - stats: &dummyStats{data: wb}, - } - rw := httptest.NewRecorder() - sh.serveSelf(rw, &http.Request{Method: "GET"}) - if rw.Code != http.StatusOK { - t.Errorf("code = %d, want %d", rw.Code, http.StatusOK) - } - wct := "application/json" - if gct := rw.Header().Get("Content-Type"); gct != wct { - t.Errorf("Content-Type = %q, want %q", gct, wct) - } - if g := rw.Body.String(); g != w { - t.Errorf("body = %s, want %s", g, w) - } -} - -func TestSelfServeStatsBad(t *testing.T) { - for _, m := range []string{"PUT", "POST", "DELETE"} { - sh := &statsHandler{lg: zap.NewExample()} - rw := httptest.NewRecorder() - sh.serveSelf( - rw, - &http.Request{ - Method: m, - }, - ) - if rw.Code != http.StatusMethodNotAllowed { - t.Errorf("method %s: code=%d, want %d", m, rw.Code, http.StatusMethodNotAllowed) - } - } -} - -func TestLeaderServeStatsBad(t *testing.T) { - for _, m := range []string{"PUT", "POST", "DELETE"} { - sh := &statsHandler{lg: zap.NewExample()} - rw := httptest.NewRecorder() - sh.serveLeader( - rw, - &http.Request{ - Method: m, - }, - ) - if rw.Code != http.StatusMethodNotAllowed { - t.Errorf("method %s: code=%d, want %d", m, rw.Code, http.StatusMethodNotAllowed) - } - } -} - -func TestServeLeaderStats(t *testing.T) { - wb := []byte("some statistics") - w := string(wb) - sh := &statsHandler{ - lg: zap.NewExample(), - stats: &dummyStats{data: wb}, - } - rw := httptest.NewRecorder() - sh.serveLeader(rw, &http.Request{Method: "GET"}) - if rw.Code != http.StatusOK { - t.Errorf("code = %d, want %d", rw.Code, http.StatusOK) - } - wct := "application/json" - if gct := rw.Header().Get("Content-Type"); gct != wct { - t.Errorf("Content-Type = %q, want %q", gct, wct) - } - if g := rw.Body.String(); g != w { - t.Errorf("body = %s, want %s", g, w) - } -} - -func TestServeStoreStats(t *testing.T) { - wb := []byte("some statistics") - w := string(wb) - sh := &statsHandler{ - lg: zap.NewExample(), - stats: &dummyStats{data: wb}, - } - rw := httptest.NewRecorder() - sh.serveStore(rw, &http.Request{Method: "GET"}) - if rw.Code != http.StatusOK { - t.Errorf("code = %d, want %d", rw.Code, http.StatusOK) - } - wct := "application/json" - if gct := rw.Header().Get("Content-Type"); gct != wct { - t.Errorf("Content-Type = %q, want %q", gct, wct) - } - if g := rw.Body.String(); g != w { - t.Errorf("body = %s, want %s", g, w) - } - -} - -func TestBadServeKeys(t *testing.T) { - testBadCases := []struct { - req *http.Request - server etcdserver.ServerV2 - - wcode int - wbody string - }{ - { - // bad method - &http.Request{ - Method: "CONNECT", - }, - &resServer{}, - - http.StatusMethodNotAllowed, - "Method Not Allowed", - }, - { - // bad method - &http.Request{ - Method: "TRACE", - }, - &resServer{}, - - http.StatusMethodNotAllowed, - "Method Not Allowed", - }, - { - // parseRequest error - &http.Request{ - Body: nil, - Method: "PUT", - }, - &resServer{}, - - http.StatusBadRequest, - `{"errorCode":210,"message":"Invalid POST form","cause":"missing form body","index":0}`, - }, - { - // etcdserver.Server error - mustNewRequest(t, "foo"), - &errServer{ - err: errors.New("Internal Server Error"), - }, - - http.StatusInternalServerError, - `{"errorCode":300,"message":"Raft Internal Error","cause":"Internal Server Error","index":0}`, - }, - { - // etcdserver.Server etcd error - mustNewRequest(t, "foo"), - &errServer{ - err: v2error.NewError(v2error.EcodeKeyNotFound, "/1/pant", 0), - }, - - http.StatusNotFound, - `{"errorCode":100,"message":"Key not found","cause":"/pant","index":0}`, - }, - { - // non-event/watcher response from etcdserver.Server - mustNewRequest(t, "foo"), - &resServer{ - res: etcdserver.Response{}, - }, - - http.StatusInternalServerError, - `{"errorCode":300,"message":"Raft Internal Error","cause":"received response with no Event/Watcher","index":0}`, - }, - } - for i, tt := range testBadCases { - h := &keysHandler{ - lg: zap.NewExample(), - timeout: 0, // context times out immediately - server: tt.server, - cluster: &fakeCluster{id: 1}, - } - rw := httptest.NewRecorder() - h.ServeHTTP(rw, tt.req) - if rw.Code != tt.wcode { - t.Errorf("#%d: got code=%d, want %d", i, rw.Code, tt.wcode) - } - if rw.Code != http.StatusMethodNotAllowed { - gcid := rw.Header().Get("X-Etcd-Cluster-ID") - wcid := h.cluster.ID().String() - if gcid != wcid { - t.Errorf("#%d: cid = %s, want %s", i, gcid, wcid) - } - } - if g := strings.TrimSuffix(rw.Body.String(), "\n"); g != tt.wbody { - t.Errorf("#%d: body = %s, want %s", i, g, tt.wbody) - } - } -} - -func TestServeKeysGood(t *testing.T) { - tests := []struct { - req *http.Request - wcode int - }{ - { - mustNewMethodRequest(t, "HEAD", "foo"), - http.StatusOK, - }, - { - mustNewMethodRequest(t, "GET", "foo"), - http.StatusOK, - }, - { - mustNewForm(t, "foo", url.Values{"value": []string{"bar"}}), - http.StatusOK, - }, - { - mustNewMethodRequest(t, "DELETE", "foo"), - http.StatusOK, - }, - { - mustNewPostForm(t, "foo", url.Values{"value": []string{"bar"}}), - http.StatusOK, - }, - } - server := &resServer{ - res: etcdserver.Response{ - Event: &v2store.Event{ - Action: v2store.Get, - Node: &v2store.NodeExtern{}, - }, - }, - } - for i, tt := range tests { - h := &keysHandler{ - lg: zap.NewExample(), - timeout: time.Hour, - server: server, - cluster: &fakeCluster{id: 1}, - } - rw := httptest.NewRecorder() - h.ServeHTTP(rw, tt.req) - if rw.Code != tt.wcode { - t.Errorf("#%d: got code=%d, want %d", i, rw.Code, tt.wcode) - } - } -} - -func TestServeKeysEvent(t *testing.T) { - tests := []struct { - req *http.Request - rsp etcdserver.Response - wcode int - event *v2store.Event - }{ - { - mustNewRequest(t, "foo"), - etcdserver.Response{ - Event: &v2store.Event{ - Action: v2store.Get, - Node: &v2store.NodeExtern{}, - }, - }, - http.StatusOK, - &v2store.Event{ - Action: v2store.Get, - Node: &v2store.NodeExtern{}, - }, - }, - { - mustNewForm( - t, - "foo", - url.Values{"noValueOnSuccess": []string{"true"}}, - ), - etcdserver.Response{ - Event: &v2store.Event{ - Action: v2store.CompareAndSwap, - Node: &v2store.NodeExtern{}, - }, - }, - http.StatusOK, - &v2store.Event{ - Action: v2store.CompareAndSwap, - Node: nil, - }, - }, - } - - server := &resServer{} - h := &keysHandler{ - lg: zap.NewExample(), - timeout: time.Hour, - server: server, - cluster: &fakeCluster{id: 1}, - } - - for _, tt := range tests { - server.res = tt.rsp - rw := httptest.NewRecorder() - h.ServeHTTP(rw, tt.req) - - wbody := mustMarshalEvent( - t, - tt.event, - ) - - if rw.Code != tt.wcode { - t.Errorf("got code=%d, want %d", rw.Code, tt.wcode) - } - gcid := rw.Header().Get("X-Etcd-Cluster-ID") - wcid := h.cluster.ID().String() - if gcid != wcid { - t.Errorf("cid = %s, want %s", gcid, wcid) - } - g := rw.Body.String() - if g != wbody { - t.Errorf("got body=%#v, want %#v", g, wbody) - } - } -} - -func TestServeKeysWatch(t *testing.T) { - req := mustNewRequest(t, "/foo/bar") - ec := make(chan *v2store.Event) - dw := &dummyWatcher{ - echan: ec, - } - server := &resServer{ - res: etcdserver.Response{ - Watcher: dw, - }, - } - h := &keysHandler{ - lg: zap.NewExample(), - timeout: time.Hour, - server: server, - cluster: &fakeCluster{id: 1}, - } - go func() { - ec <- &v2store.Event{ - Action: v2store.Get, - Node: &v2store.NodeExtern{}, - } - }() - rw := httptest.NewRecorder() - - h.ServeHTTP(rw, req) - - wcode := http.StatusOK - wbody := mustMarshalEvent( - t, - &v2store.Event{ - Action: v2store.Get, - Node: &v2store.NodeExtern{}, - }, - ) - - if rw.Code != wcode { - t.Errorf("got code=%d, want %d", rw.Code, wcode) - } - gcid := rw.Header().Get("X-Etcd-Cluster-ID") - wcid := h.cluster.ID().String() - if gcid != wcid { - t.Errorf("cid = %s, want %s", gcid, wcid) - } - g := rw.Body.String() - if g != wbody { - t.Errorf("got body=%#v, want %#v", g, wbody) - } -} - -type recordingCloseNotifier struct { - *httptest.ResponseRecorder - cn chan bool -} - -func (rcn *recordingCloseNotifier) CloseNotify() <-chan bool { - return rcn.cn -} - -func TestHandleWatch(t *testing.T) { - defaultRwRr := func() (http.ResponseWriter, *httptest.ResponseRecorder) { - r := httptest.NewRecorder() - return r, r - } - noopEv := func(chan *v2store.Event) {} - - tests := []struct { - getCtx func() context.Context - getRwRr func() (http.ResponseWriter, *httptest.ResponseRecorder) - doToChan func(chan *v2store.Event) - - wbody string - }{ - { - // Normal case: one event - context.Background, - defaultRwRr, - func(ch chan *v2store.Event) { - ch <- &v2store.Event{ - Action: v2store.Get, - Node: &v2store.NodeExtern{}, - } - }, - - mustMarshalEvent( - t, - &v2store.Event{ - Action: v2store.Get, - Node: &v2store.NodeExtern{}, - }, - ), - }, - { - // Channel is closed, no event - context.Background, - defaultRwRr, - func(ch chan *v2store.Event) { - close(ch) - }, - - "", - }, - { - // Simulate a timed-out context - func() context.Context { - ctx, cancel := context.WithCancel(context.Background()) - cancel() - return ctx - }, - defaultRwRr, - noopEv, - - "", - }, - { - // Close-notifying request - context.Background, - func() (http.ResponseWriter, *httptest.ResponseRecorder) { - rw := &recordingCloseNotifier{ - ResponseRecorder: httptest.NewRecorder(), - cn: make(chan bool, 1), - } - rw.cn <- true - return rw, rw.ResponseRecorder - }, - noopEv, - - "", - }, - } - - for i, tt := range tests { - rw, rr := tt.getRwRr() - wa := &dummyWatcher{ - echan: make(chan *v2store.Event, 1), - sidx: 10, - } - tt.doToChan(wa.echan) - - resp := etcdserver.Response{Term: 5, Index: 100, Watcher: wa} - handleKeyWatch(tt.getCtx(), zap.NewExample(), rw, resp, false) - - wcode := http.StatusOK - wct := "application/json" - wei := "10" - wri := "100" - wrt := "5" - - if rr.Code != wcode { - t.Errorf("#%d: got code=%d, want %d", i, rr.Code, wcode) - } - h := rr.Header() - if ct := h.Get("Content-Type"); ct != wct { - t.Errorf("#%d: Content-Type=%q, want %q", i, ct, wct) - } - if ei := h.Get("X-Etcd-Index"); ei != wei { - t.Errorf("#%d: X-Etcd-Index=%q, want %q", i, ei, wei) - } - if ri := h.Get("X-Raft-Index"); ri != wri { - t.Errorf("#%d: X-Raft-Index=%q, want %q", i, ri, wri) - } - if rt := h.Get("X-Raft-Term"); rt != wrt { - t.Errorf("#%d: X-Raft-Term=%q, want %q", i, rt, wrt) - } - g := rr.Body.String() - if g != tt.wbody { - t.Errorf("#%d: got body=%#v, want %#v", i, g, tt.wbody) - } - } -} - -func TestHandleWatchStreaming(t *testing.T) { - rw := &flushingRecorder{ - httptest.NewRecorder(), - make(chan struct{}, 1), - } - wa := &dummyWatcher{ - echan: make(chan *v2store.Event), - } - - // Launch the streaming handler in the background with a cancellable context - ctx, cancel := context.WithCancel(context.Background()) - done := make(chan struct{}) - go func() { - resp := etcdserver.Response{Watcher: wa} - handleKeyWatch(ctx, zap.NewExample(), rw, resp, true) - close(done) - }() - - // Expect one Flush for the headers etc. - select { - case <-rw.ch: - case <-time.After(time.Second): - t.Fatalf("timed out waiting for flush") - } - - // Expect headers but no body - wcode := http.StatusOK - wct := "application/json" - wbody := "" - - if rw.Code != wcode { - t.Errorf("got code=%d, want %d", rw.Code, wcode) - } - h := rw.Header() - if ct := h.Get("Content-Type"); ct != wct { - t.Errorf("Content-Type=%q, want %q", ct, wct) - } - g := rw.Body.String() - if g != wbody { - t.Errorf("got body=%#v, want %#v", g, wbody) - } - - // Now send the first event - select { - case wa.echan <- &v2store.Event{ - Action: v2store.Get, - Node: &v2store.NodeExtern{}, - }: - case <-time.After(time.Second): - t.Fatal("timed out waiting for send") - } - - // Wait for it to be flushed... - select { - case <-rw.ch: - case <-time.After(time.Second): - t.Fatalf("timed out waiting for flush") - } - - // And check the body is as expected - wbody = mustMarshalEvent( - t, - &v2store.Event{ - Action: v2store.Get, - Node: &v2store.NodeExtern{}, - }, - ) - g = rw.Body.String() - if g != wbody { - t.Errorf("got body=%#v, want %#v", g, wbody) - } - - // Rinse and repeat - select { - case wa.echan <- &v2store.Event{ - Action: v2store.Get, - Node: &v2store.NodeExtern{}, - }: - case <-time.After(time.Second): - t.Fatal("timed out waiting for send") - } - - select { - case <-rw.ch: - case <-time.After(time.Second): - t.Fatalf("timed out waiting for flush") - } - - // This time, we expect to see both events - wbody = wbody + wbody - g = rw.Body.String() - if g != wbody { - t.Errorf("got body=%#v, want %#v", g, wbody) - } - - // Finally, time out the connection and ensure the serving goroutine returns - cancel() - - select { - case <-done: - case <-time.After(time.Second): - t.Fatalf("timed out waiting for done") - } -} - -func TestTrimEventPrefix(t *testing.T) { - pre := "/abc" - tests := []struct { - ev *v2store.Event - wev *v2store.Event - }{ - { - nil, - nil, - }, - { - &v2store.Event{}, - &v2store.Event{}, - }, - { - &v2store.Event{Node: &v2store.NodeExtern{Key: "/abc/def"}}, - &v2store.Event{Node: &v2store.NodeExtern{Key: "/def"}}, - }, - { - &v2store.Event{PrevNode: &v2store.NodeExtern{Key: "/abc/ghi"}}, - &v2store.Event{PrevNode: &v2store.NodeExtern{Key: "/ghi"}}, - }, - { - &v2store.Event{ - Node: &v2store.NodeExtern{Key: "/abc/def"}, - PrevNode: &v2store.NodeExtern{Key: "/abc/ghi"}, - }, - &v2store.Event{ - Node: &v2store.NodeExtern{Key: "/def"}, - PrevNode: &v2store.NodeExtern{Key: "/ghi"}, - }, - }, - } - for i, tt := range tests { - ev := trimEventPrefix(tt.ev, pre) - if !reflect.DeepEqual(ev, tt.wev) { - t.Errorf("#%d: event = %+v, want %+v", i, ev, tt.wev) - } - } -} - -func TestTrimNodeExternPrefix(t *testing.T) { - pre := "/abc" - tests := []struct { - n *v2store.NodeExtern - wn *v2store.NodeExtern - }{ - { - nil, - nil, - }, - { - &v2store.NodeExtern{Key: "/abc/def"}, - &v2store.NodeExtern{Key: "/def"}, - }, - { - &v2store.NodeExtern{ - Key: "/abc/def", - Nodes: []*v2store.NodeExtern{ - {Key: "/abc/def/1"}, - {Key: "/abc/def/2"}, - }, - }, - &v2store.NodeExtern{ - Key: "/def", - Nodes: []*v2store.NodeExtern{ - {Key: "/def/1"}, - {Key: "/def/2"}, - }, - }, - }, - } - for i, tt := range tests { - trimNodeExternPrefix(tt.n, pre) - if !reflect.DeepEqual(tt.n, tt.wn) { - t.Errorf("#%d: node = %+v, want %+v", i, tt.n, tt.wn) - } - } -} - -func TestTrimPrefix(t *testing.T) { - tests := []struct { - in string - prefix string - w string - }{ - {"/v2/members", "/v2/members", ""}, - {"/v2/members/", "/v2/members", ""}, - {"/v2/members/foo", "/v2/members", "foo"}, - } - for i, tt := range tests { - if g := trimPrefix(tt.in, tt.prefix); g != tt.w { - t.Errorf("#%d: trimPrefix = %q, want %q", i, g, tt.w) - } - } -} - -func TestNewMemberCollection(t *testing.T) { - fixture := []*membership.Member{ - { - ID: 12, - Attributes: membership.Attributes{ClientURLs: []string{"http://localhost:8080", "http://localhost:8081"}}, - RaftAttributes: membership.RaftAttributes{PeerURLs: []string{"http://localhost:8082", "http://localhost:8083"}}, - }, - { - ID: 13, - Attributes: membership.Attributes{ClientURLs: []string{"http://localhost:9090", "http://localhost:9091"}}, - RaftAttributes: membership.RaftAttributes{PeerURLs: []string{"http://localhost:9092", "http://localhost:9093"}}, - }, - } - got := newMemberCollection(fixture) - - want := httptypes.MemberCollection([]httptypes.Member{ - { - ID: "c", - ClientURLs: []string{"http://localhost:8080", "http://localhost:8081"}, - PeerURLs: []string{"http://localhost:8082", "http://localhost:8083"}, - }, - { - ID: "d", - ClientURLs: []string{"http://localhost:9090", "http://localhost:9091"}, - PeerURLs: []string{"http://localhost:9092", "http://localhost:9093"}, - }, - }) - - if !reflect.DeepEqual(&want, got) { - t.Fatalf("newMemberCollection failure: want=%#v, got=%#v", &want, got) - } -} - -func TestNewMember(t *testing.T) { - fixture := &membership.Member{ - ID: 12, - Attributes: membership.Attributes{ClientURLs: []string{"http://localhost:8080", "http://localhost:8081"}}, - RaftAttributes: membership.RaftAttributes{PeerURLs: []string{"http://localhost:8082", "http://localhost:8083"}}, - } - got := newMember(fixture) - - want := httptypes.Member{ - ID: "c", - ClientURLs: []string{"http://localhost:8080", "http://localhost:8081"}, - PeerURLs: []string{"http://localhost:8082", "http://localhost:8083"}, - } - - if !reflect.DeepEqual(want, got) { - t.Fatalf("newMember failure: want=%#v, got=%#v", want, got) - } -} diff --git a/server/etcdserver/api/v2http/doc.go b/server/etcdserver/api/v2http/doc.go deleted file mode 100644 index 475c4b1f9..000000000 --- a/server/etcdserver/api/v2http/doc.go +++ /dev/null @@ -1,16 +0,0 @@ -// Copyright 2015 The etcd Authors -// -// 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 v2http provides etcd client and server implementations. -package v2http diff --git a/server/etcdserver/api/v2http/http.go b/server/etcdserver/api/v2http/http.go deleted file mode 100644 index 88138b80a..000000000 --- a/server/etcdserver/api/v2http/http.go +++ /dev/null @@ -1,82 +0,0 @@ -// Copyright 2015 The etcd Authors -// -// 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 v2http - -import ( - "math" - "net/http" - "strings" - "time" - - "go.etcd.io/etcd/server/v3/etcdserver/api/etcdhttp" - "go.etcd.io/etcd/server/v3/etcdserver/api/v2auth" - "go.etcd.io/etcd/server/v3/etcdserver/api/v2http/httptypes" - - "go.uber.org/zap" -) - -const ( - // time to wait for a Watch request - defaultWatchTimeout = time.Duration(math.MaxInt64) -) - -func writeError(lg *zap.Logger, w http.ResponseWriter, r *http.Request, err error) { - if err == nil { - return - } - if e, ok := err.(v2auth.Error); ok { - herr := httptypes.NewHTTPError(e.HTTPStatus(), e.Error()) - if et := herr.WriteTo(w); et != nil { - if lg != nil { - lg.Debug( - "failed to write v2 HTTP error", - zap.String("remote-addr", r.RemoteAddr), - zap.String("v2auth-error", e.Error()), - zap.Error(et), - ) - } - } - return - } - etcdhttp.WriteError(lg, w, r, err) -} - -// allowMethod verifies that the given method is one of the allowed methods, -// and if not, it writes an error to w. A boolean is returned indicating -// whether or not the method is allowed. -func allowMethod(w http.ResponseWriter, m string, ms ...string) bool { - for _, meth := range ms { - if m == meth { - return true - } - } - w.Header().Set("Allow", strings.Join(ms, ",")) - http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed) - return false -} - -func requestLogger(lg *zap.Logger, handler http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - if lg != nil { - lg.Debug( - "handling HTTP request", - zap.String("method", r.Method), - zap.String("request-uri", r.RequestURI), - zap.String("remote-addr", r.RemoteAddr), - ) - } - handler.ServeHTTP(w, r) - }) -} diff --git a/server/etcdserver/api/v2http/http_test.go b/server/etcdserver/api/v2http/http_test.go deleted file mode 100644 index 2fcd6b26b..000000000 --- a/server/etcdserver/api/v2http/http_test.go +++ /dev/null @@ -1,192 +0,0 @@ -// Copyright 2015 The etcd Authors -// -// 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 v2http - -import ( - "context" - "errors" - "net/http" - "net/http/httptest" - "sort" - "testing" - - "go.etcd.io/etcd/api/v3/etcdserverpb" - "go.etcd.io/etcd/client/pkg/v3/types" - "go.etcd.io/etcd/raft/v3/raftpb" - "go.etcd.io/etcd/server/v3/etcdserver" - "go.etcd.io/etcd/server/v3/etcdserver/api/membership" - "go.etcd.io/etcd/server/v3/etcdserver/api/v2error" - - "github.com/coreos/go-semver/semver" - "go.uber.org/zap" -) - -type fakeCluster struct { - id uint64 - clientURLs []string - members map[uint64]*membership.Member -} - -func (c *fakeCluster) ID() types.ID { return types.ID(c.id) } -func (c *fakeCluster) ClientURLs() []string { return c.clientURLs } -func (c *fakeCluster) Members() []*membership.Member { - var ms membership.MembersByID - for _, m := range c.members { - ms = append(ms, m) - } - sort.Sort(ms) - return []*membership.Member(ms) -} -func (c *fakeCluster) Member(id types.ID) *membership.Member { return c.members[uint64(id)] } -func (c *fakeCluster) Version() *semver.Version { return nil } - -// errServer implements the etcd.Server interface for testing. -// It returns the given error from any Do/Process/AddMember/RemoveMember calls. -type errServer struct { - err error - fakeServer -} - -func (fs *errServer) Do(ctx context.Context, r etcdserverpb.Request) (etcdserver.Response, error) { - return etcdserver.Response{}, fs.err -} -func (fs *errServer) Process(ctx context.Context, m raftpb.Message) error { - return fs.err -} -func (fs *errServer) AddMember(ctx context.Context, m membership.Member) ([]*membership.Member, error) { - return nil, fs.err -} -func (fs *errServer) RemoveMember(ctx context.Context, id uint64) ([]*membership.Member, error) { - return nil, fs.err -} -func (fs *errServer) UpdateMember(ctx context.Context, m membership.Member) ([]*membership.Member, error) { - return nil, fs.err -} -func (fs *errServer) PromoteMember(ctx context.Context, id uint64) ([]*membership.Member, error) { - return nil, fs.err -} - -func TestWriteError(t *testing.T) { - // nil error should not panic - rec := httptest.NewRecorder() - r := new(http.Request) - writeError(zap.NewExample(), rec, r, nil) - h := rec.Header() - if len(h) > 0 { - t.Fatalf("unexpected non-empty headers: %#v", h) - } - b := rec.Body.String() - if len(b) > 0 { - t.Fatalf("unexpected non-empty body: %q", b) - } - - tests := []struct { - err error - wcode int - wi string - }{ - { - v2error.NewError(v2error.EcodeKeyNotFound, "/foo/bar", 123), - http.StatusNotFound, - "123", - }, - { - v2error.NewError(v2error.EcodeTestFailed, "/foo/bar", 456), - http.StatusPreconditionFailed, - "456", - }, - { - err: errors.New("something went wrong"), - wcode: http.StatusInternalServerError, - }, - } - - for i, tt := range tests { - rw := httptest.NewRecorder() - writeError(zap.NewExample(), rw, r, tt.err) - if code := rw.Code; code != tt.wcode { - t.Errorf("#%d: code=%d, want %d", i, code, tt.wcode) - } - if idx := rw.Header().Get("X-Etcd-Index"); idx != tt.wi { - t.Errorf("#%d: X-Etcd-Index=%q, want %q", i, idx, tt.wi) - } - } -} - -func TestAllowMethod(t *testing.T) { - tests := []struct { - m string - ms []string - w bool - wh string - }{ - // Accepted methods - { - m: "GET", - ms: []string{"GET", "POST", "PUT"}, - w: true, - }, - { - m: "POST", - ms: []string{"POST"}, - w: true, - }, - // Made-up methods no good - { - m: "FAKE", - ms: []string{"GET", "POST", "PUT"}, - w: false, - wh: "GET,POST,PUT", - }, - // Empty methods no good - { - m: "", - ms: []string{"GET", "POST"}, - w: false, - wh: "GET,POST", - }, - // Empty accepted methods no good - { - m: "GET", - ms: []string{""}, - w: false, - wh: "", - }, - // No methods accepted - { - m: "GET", - ms: []string{}, - w: false, - wh: "", - }, - } - - for i, tt := range tests { - rw := httptest.NewRecorder() - g := allowMethod(rw, tt.m, tt.ms...) - if g != tt.w { - t.Errorf("#%d: got allowMethod()=%t, want %t", i, g, tt.w) - } - if !tt.w { - if rw.Code != http.StatusMethodNotAllowed { - t.Errorf("#%d: code=%d, want %d", i, rw.Code, http.StatusMethodNotAllowed) - } - gh := rw.Header().Get("Allow") - if gh != tt.wh { - t.Errorf("#%d: Allow header=%q, want %q", i, gh, tt.wh) - } - } - } -} diff --git a/server/etcdserver/api/v2http/httptypes/member.go b/server/etcdserver/api/v2http/httptypes/member.go deleted file mode 100644 index a5467be91..000000000 --- a/server/etcdserver/api/v2http/httptypes/member.go +++ /dev/null @@ -1,69 +0,0 @@ -// Copyright 2015 The etcd Authors -// -// 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 httptypes defines how etcd's HTTP API entities are serialized to and -// deserialized from JSON. -package httptypes - -import ( - "encoding/json" - - "go.etcd.io/etcd/client/pkg/v3/types" -) - -type Member struct { - ID string `json:"id"` - Name string `json:"name"` - PeerURLs []string `json:"peerURLs"` - ClientURLs []string `json:"clientURLs"` -} - -type MemberCreateRequest struct { - PeerURLs types.URLs -} - -type MemberUpdateRequest struct { - MemberCreateRequest -} - -func (m *MemberCreateRequest) UnmarshalJSON(data []byte) error { - s := struct { - PeerURLs []string `json:"peerURLs"` - }{} - - err := json.Unmarshal(data, &s) - if err != nil { - return err - } - - urls, err := types.NewURLs(s.PeerURLs) - if err != nil { - return err - } - - m.PeerURLs = urls - return nil -} - -type MemberCollection []Member - -func (c *MemberCollection) MarshalJSON() ([]byte, error) { - d := struct { - Members []Member `json:"members"` - }{ - Members: []Member(*c), - } - - return json.Marshal(d) -} diff --git a/server/etcdserver/api/v2http/httptypes/member_test.go b/server/etcdserver/api/v2http/httptypes/member_test.go deleted file mode 100644 index 6b8056abd..000000000 --- a/server/etcdserver/api/v2http/httptypes/member_test.go +++ /dev/null @@ -1,135 +0,0 @@ -// Copyright 2015 The etcd Authors -// -// 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 httptypes - -import ( - "encoding/json" - "net/url" - "reflect" - "testing" - - "go.etcd.io/etcd/client/pkg/v3/types" -) - -func TestMemberUnmarshal(t *testing.T) { - tests := []struct { - body []byte - wantMember Member - wantError bool - }{ - // no URLs, just check ID & Name - { - body: []byte(`{"id": "c", "name": "dungarees"}`), - wantMember: Member{ID: "c", Name: "dungarees", PeerURLs: nil, ClientURLs: nil}, - }, - - // both client and peer URLs - { - body: []byte(`{"peerURLs": ["http://127.0.0.1:2379"], "clientURLs": ["http://127.0.0.1:2379"]}`), - wantMember: Member{ - PeerURLs: []string{ - "http://127.0.0.1:2379", - }, - ClientURLs: []string{ - "http://127.0.0.1:2379", - }, - }, - }, - - // multiple peer URLs - { - body: []byte(`{"peerURLs": ["http://127.0.0.1:2379", "https://example.com"]}`), - wantMember: Member{ - PeerURLs: []string{ - "http://127.0.0.1:2379", - "https://example.com", - }, - ClientURLs: nil, - }, - }, - - // multiple client URLs - { - body: []byte(`{"clientURLs": ["http://127.0.0.1:2379", "https://example.com"]}`), - wantMember: Member{ - PeerURLs: nil, - ClientURLs: []string{ - "http://127.0.0.1:2379", - "https://example.com", - }, - }, - }, - - // invalid JSON - { - body: []byte(`{"peerU`), - wantError: true, - }, - } - - for i, tt := range tests { - got := Member{} - err := json.Unmarshal(tt.body, &got) - if tt.wantError != (err != nil) { - t.Errorf("#%d: want error %t, got %v", i, tt.wantError, err) - continue - } - - if !reflect.DeepEqual(tt.wantMember, got) { - t.Errorf("#%d: incorrect output: want=%#v, got=%#v", i, tt.wantMember, got) - } - } -} - -func TestMemberCreateRequestUnmarshal(t *testing.T) { - body := []byte(`{"peerURLs": ["http://127.0.0.1:8081", "https://127.0.0.1:8080"]}`) - want := MemberCreateRequest{ - PeerURLs: types.URLs([]url.URL{ - {Scheme: "http", Host: "127.0.0.1:8081"}, - {Scheme: "https", Host: "127.0.0.1:8080"}, - }), - } - - var req MemberCreateRequest - if err := json.Unmarshal(body, &req); err != nil { - t.Fatalf("Unmarshal returned unexpected err=%v", err) - } - - if !reflect.DeepEqual(want, req) { - t.Fatalf("Failed to unmarshal MemberCreateRequest: want=%#v, got=%#v", want, req) - } -} - -func TestMemberCreateRequestUnmarshalFail(t *testing.T) { - tests := [][]byte{ - // invalid JSON - []byte(``), - []byte(`{`), - - // spot-check validation done in types.NewURLs - []byte(`{"peerURLs": "foo"}`), - []byte(`{"peerURLs": ["."]}`), - []byte(`{"peerURLs": []}`), - []byte(`{"peerURLs": ["http://127.0.0.1:2379/foo"]}`), - []byte(`{"peerURLs": ["http://127.0.0.1"]}`), - } - - for i, tt := range tests { - var req MemberCreateRequest - if err := json.Unmarshal(tt, &req); err == nil { - t.Errorf("#%d: expected err, got nil", i) - } - } -} diff --git a/server/etcdserver/api/v2http/metrics.go b/server/etcdserver/api/v2http/metrics.go deleted file mode 100644 index bdbd8c71c..000000000 --- a/server/etcdserver/api/v2http/metrics.go +++ /dev/null @@ -1,98 +0,0 @@ -// Copyright 2015 The etcd Authors -// -// 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 v2http - -import ( - "net/http" - "strconv" - "time" - - "go.etcd.io/etcd/api/v3/etcdserverpb" - "go.etcd.io/etcd/server/v3/etcdserver/api/v2error" - "go.etcd.io/etcd/server/v3/etcdserver/api/v2http/httptypes" - - "github.com/prometheus/client_golang/prometheus" -) - -var ( - incomingEvents = prometheus.NewCounterVec( - prometheus.CounterOpts{ - Namespace: "etcd", - Subsystem: "http", - Name: "received_total", - Help: "Counter of requests received into the system (successfully parsed and authd).", - }, []string{"method"}) - - failedEvents = prometheus.NewCounterVec( - prometheus.CounterOpts{ - Namespace: "etcd", - Subsystem: "http", - Name: "failed_total", - Help: "Counter of handle failures of requests (non-watches), by method (GET/PUT etc.) and code (400, 500 etc.).", - }, []string{"method", "code"}) - - successfulEventsHandlingSec = prometheus.NewHistogramVec( - prometheus.HistogramOpts{ - Namespace: "etcd", - Subsystem: "http", - Name: "successful_duration_seconds", - Help: "Bucketed histogram of processing time (s) of successfully handled requests (non-watches), by method (GET/PUT etc.).", - - // lowest bucket start of upper bound 0.0005 sec (0.5 ms) with factor 2 - // highest bucket start of 0.0005 sec * 2^12 == 2.048 sec - Buckets: prometheus.ExponentialBuckets(0.0005, 2, 13), - }, []string{"method"}) -) - -func init() { - prometheus.MustRegister(incomingEvents) - prometheus.MustRegister(failedEvents) - prometheus.MustRegister(successfulEventsHandlingSec) -} - -func reportRequestReceived(request etcdserverpb.Request) { - incomingEvents.WithLabelValues(methodFromRequest(request)).Inc() -} - -func reportRequestCompleted(request etcdserverpb.Request, startTime time.Time) { - method := methodFromRequest(request) - successfulEventsHandlingSec.WithLabelValues(method).Observe(time.Since(startTime).Seconds()) -} - -func reportRequestFailed(request etcdserverpb.Request, err error) { - method := methodFromRequest(request) - failedEvents.WithLabelValues(method, strconv.Itoa(codeFromError(err))).Inc() -} - -func methodFromRequest(request etcdserverpb.Request) string { - if request.Method == "GET" && request.Quorum { - return "QGET" - } - return request.Method -} - -func codeFromError(err error) int { - if err == nil { - return http.StatusInternalServerError - } - switch e := err.(type) { - case *v2error.Error: - return e.StatusCode() - case *httptypes.HTTPError: - return e.Code - default: - return http.StatusInternalServerError - } -} diff --git a/server/etcdserver/api/v2http/testdata/ca.pem b/server/etcdserver/api/v2http/testdata/ca.pem deleted file mode 100644 index 60cbee3bb..000000000 --- a/server/etcdserver/api/v2http/testdata/ca.pem +++ /dev/null @@ -1,19 +0,0 @@ ------BEGIN CERTIFICATE----- -MIIDEjCCAfqgAwIBAgIIYpX+8HgWGfkwDQYJKoZIhvcNAQELBQAwFTETMBEGA1UE -AxMKZXRjZCB0ZXN0czAeFw0xNTExMjQwMzA1MDBaFw0yMDExMjIwMzA1MDBaMBUx -EzARBgNVBAMTCmV0Y2QgdGVzdHMwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEK -AoIBAQDa9PkwEwiBD8mB+VIKz5r5gRHnNF4Icj6T6R/RsdatecQe6vU0EU4FXtKZ -drWnCGlATyrQooqHpb+rDc7CUt3mXrIxrNkcGTMaesF7P0GWxVkyOGSjJMxGBv3e -bAZknBe4eLMi68L1aT/uYmxcp/B3L2mfdFtc1Gd6mYJpNm1PgilRyIrO0mY5ysIX -4WHCa3yudAv8HrFbQcw7l7OyKA6uSWg6h07lE3d5jw5YOly+hz0iaRtzhb4tJrYD -Lm1tehb0nnoLuW6yYblRSoyBVDT50MFVlyvW40Po5WkOXw/wnsnyxWRR4yqU23wq -quQU0HXJEBLFnT+KbLOQ0EAE35vXAgMBAAGjZjBkMA4GA1UdDwEB/wQEAwIBBjAS -BgNVHRMBAf8ECDAGAQH/AgECMB0GA1UdDgQWBBSbUCGB95ochDrbEZlzGGYuA7xu -xjAfBgNVHSMEGDAWgBSbUCGB95ochDrbEZlzGGYuA7xuxjANBgkqhkiG9w0BAQsF -AAOCAQEAardO/SGCu7Snz3YRBUinzpZEUFTFend+FJtBkxBXCao1RvTXg8PBMkza -LUsaR4mLsGoXLIbNCoIinvVG0QULYCZe11N3l1L0G2g5uhEM4MfJ2rwrMD0o17i+ -nwNRRE3tfKAlWhYQg+4ye36kQVxASPniHjdQgjKYUFTNXdyG6DzuAclaVte9iVw6 -cWl61fB2CZya3+uMtih8t/Kgl2KbMO2PvNByfnDjKmW+v58qHbXyoJZqnpvDn14+ -p2Ox+AvvxYiEiUIvFdWy101QB7NJMCtdwq6oG6OvIOgXzLgitTFSq4kfWDfupQjW -iFoQ+vWmYhK5ld0nBaiz+JmHuemK7A== ------END CERTIFICATE----- diff --git a/server/etcdserver/api/v2http/testdata/otheruser.pem b/server/etcdserver/api/v2http/testdata/otheruser.pem deleted file mode 100644 index d0c74eb9f..000000000 --- a/server/etcdserver/api/v2http/testdata/otheruser.pem +++ /dev/null @@ -1,20 +0,0 @@ ------BEGIN CERTIFICATE----- -MIIDOTCCAiGgAwIBAgIINYpsso1f3SswDQYJKoZIhvcNAQELBQAwFTETMBEGA1UE -AxMKZXRjZCB0ZXN0czAeFw0xNTExMjQwMzA4MDBaFw0xNjExMjMwMzA4MDBaMBQx -EjAQBgNVBAMTCW90aGVydXNlcjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC -ggEBAPOAUa5GblwIjHTEnox2c/Am9jV1TMvzBuVXxnp2UnNHMNwstAooFrEs/Z+d -ft5AOsooP6zVuM3eBQa4i9huJbVNDfPU2H94yA89jYfJYUgo7C838V6NjGsCCptQ -WzkKPNlDbT9xA/7XpIUJ2WltuYDRrjWq8pXQONqTjcg5n4l0JO8xdHJHRUkFQ76F -1npXeLndgGaP11lqzpYlglEGi5URhzAT1xxQ0hLSe8WNmiCxxkq++C8Gx4sPg9mX -M94aoJDzZSnoaqDxckbP/7Q0ZKe/fVdCFkd5+jqT4Mt7hwmz9jTCHcVnAz4EKI+t -rbWgbCfMK6013GotXz7InStVe+MCAwEAAaOBjTCBijAOBgNVHQ8BAf8EBAMCBaAw -HQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMCMAwGA1UdEwEB/wQCMAAwHQYD -VR0OBBYEFFwMmf+pnaejmri6y1T+lfU+MBq/MB8GA1UdIwQYMBaAFJtQIYH3mhyE -OtsRmXMYZi4DvG7GMAsGA1UdEQQEMAKCADANBgkqhkiG9w0BAQsFAAOCAQEACOn6 -mec29MTMGPt/EPOmSyhvTKSwH+5YWjCbyUFeoB8puxrJlIphK4mvT+sXp2wzno89 -FVCliO/rJurdErKvyOjlK1QrVGPYIt7Wz9ssAfvlwCyBM8PqgEG8dJN9aAkf2h4r -Ye+hBh1y6Nnataf7lxe9mqAOvD/7wVIgzjCnMD1q5QSY2Mln3HwVQXtbZFbY363Z -X9Fk3PUpjJSX9jbEz9kIlT8AJAdxl6GB8Z9B8PrA8qf4Bhk15ICRHxb67EhDrGWV -8q7ArU2XBqs/+GWpUIMoGKNZv+K+/SksZK1KnzaUvApUCJzt+ac+p8HOgMdvDRgr -GfVVJqcZgyEmeczy0A== ------END CERTIFICATE----- diff --git a/server/etcdserver/api/v2http/testdata/user.pem b/server/etcdserver/api/v2http/testdata/user.pem deleted file mode 100644 index 0fc210865..000000000 --- a/server/etcdserver/api/v2http/testdata/user.pem +++ /dev/null @@ -1,20 +0,0 @@ ------BEGIN CERTIFICATE----- -MIIDNDCCAhygAwIBAgIIcQ0DAfgevocwDQYJKoZIhvcNAQELBQAwFTETMBEGA1UE -AxMKZXRjZCB0ZXN0czAeFw0xNTExMjQwMzA4MDBaFw0xNjExMjMwMzA4MDBaMA8x -DTALBgNVBAMTBHVzZXIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQD0 -+3Lm1SmUJJLufaFTYz+e5qyQEshNRyeAhXIeZ1aw+yBjslXGZQ3/uGOwnOnGqUeA -Nidc9ty4NsK6RVppHlezUrBnpl4hws8vHWFKZpU2R6kKL8EYLmg+iVqEBj7XqfAp -8bJqqZI3KOqLXpRH55mA69KP7VEK9ngTVR/tERSrUPT8jcjwbvhSOqD8Qk07BUDR -6RpDr94Mnaf+fMGG36Sh7iUl+i4Oh6FFar+7+b0+5Bhs2/6uVsK4A1Z3jqqfSQH8 -q8Wf5h9Ka4aqGSw4ia5G3Uw7Jsl2aDgpJ7uwJo1k8SclbMYnYdhZuo+U+esY/Fai -YdbjG+AroZ+y9TB8bMlHAgMBAAGjgY0wgYowDgYDVR0PAQH/BAQDAgWgMB0GA1Ud -JQQWMBQGCCsGAQUFBwMBBggrBgEFBQcDAjAMBgNVHRMBAf8EAjAAMB0GA1UdDgQW -BBRuTt0lJIVKYaz76aSxl/MQOLRwfDAfBgNVHSMEGDAWgBSbUCGB95ochDrbEZlz -GGYuA7xuxjALBgNVHREEBDACggAwDQYJKoZIhvcNAQELBQADggEBABLRWZm+Lgjs -c5qDXbgOJW2pR630syY8ixR9c6HvzPVJim8mFioMX+xrlbOC6BmOUlOb9j83bTKn -aOg/0xlpxNbd8QYzgRxZmHZLULPdiNeeRvIzsrzrH88+inrmZhRXRVcHjdO6CG6t -hCdDdRiNU6GkF7dPna0xNcEOKe2wUfzd1ZtKOqzi1w+fKjSeMplZomeWgP4WRvkh -JJ/0ujlMMckgyTxRh8EEaJ35OnpXX7EdipoWhOMmiUnlPqye2icC8Y+CMdZsrod6 -nkoEQnXDCLf/Iv0qj7B9iKbxn7t3QDVxY4UILUReDuD8yrGULlGOl//aY/T3pkZ6 -R5trduZhI3o= ------END CERTIFICATE----- diff --git a/server/etcdserver/api/v2stats/stats.go b/server/etcdserver/api/v2stats/stats.go deleted file mode 100644 index cbf60215a..000000000 --- a/server/etcdserver/api/v2stats/stats.go +++ /dev/null @@ -1,26 +0,0 @@ -// Copyright 2015 The etcd Authors -// -// 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 v2stats defines a standard interface for etcd cluster statistics. -package v2stats - -type Stats interface { - // SelfStats returns the struct representing statistics of this server - SelfStats() []byte - // LeaderStats returns the statistics of all followers in the cluster - // if this server is leader. Otherwise, nil is returned. - LeaderStats() []byte - // StoreStats returns statistics of the store backing this EtcdServer - StoreStats() []byte -} diff --git a/server/etcdserver/server.go b/server/etcdserver/server.go index 1828cfd2c..d3a7981bc 100644 --- a/server/etcdserver/server.go +++ b/server/etcdserver/server.go @@ -142,7 +142,6 @@ type ServerV2 interface { // Do takes a V2 request and attempts to fulfill it, returning a Response. Do(ctx context.Context, r pb.Request) (Response, error) - stats.Stats ClientCertAuthEnabled() bool } @@ -1224,18 +1223,6 @@ func (s *EtcdServer) StopNotify() <-chan struct{} { return s.done } // when the server is being stopped. func (s *EtcdServer) StoppingNotify() <-chan struct{} { return s.stopping } -func (s *EtcdServer) SelfStats() []byte { return s.stats.JSON() } - -func (s *EtcdServer) LeaderStats() []byte { - lead := s.getLead() - if lead != uint64(s.id) { - return nil - } - return s.lstats.JSON() -} - -func (s *EtcdServer) StoreStats() []byte { return s.v2store.JsonStats() } - func (s *EtcdServer) checkMembershipOperationPermission(ctx context.Context) error { if s.authStore == nil { // In the context of ordinary etcd process, s.authStore will never be nil. diff --git a/tests/e2e/discovery_test.go b/tests/e2e/discovery_test.go index 6f6a5e150..8ace067d5 100644 --- a/tests/e2e/discovery_test.go +++ b/tests/e2e/discovery_test.go @@ -17,11 +17,16 @@ package e2e import ( "context" "fmt" + "net/http" "strings" "testing" + "time" "go.etcd.io/etcd/client/pkg/v3/fileutil" + "go.etcd.io/etcd/client/pkg/v3/testutil" + "go.etcd.io/etcd/client/pkg/v3/transport" "go.etcd.io/etcd/client/v2" + "go.etcd.io/etcd/server/v3/etcdserver/api/rafthttp" "go.etcd.io/etcd/tests/v3/framework/e2e" "go.etcd.io/etcd/tests/v3/framework/integration" ) @@ -49,7 +54,7 @@ func testClusterUsingDiscovery(t *testing.T, size int, peerTLS bool) { } defer dc.Close() - dcc := integration.MustNewHTTPClient(t, dc.EndpointsV2(), nil) + dcc := MustNewHTTPClient(t, dc.EndpointsV2(), nil) dkapi := client.NewKeysAPI(dcc) ctx, cancel := context.WithTimeout(context.Background(), integration.RequestTimeout) if _, err := dkapi.Create(ctx, "/_config/size", fmt.Sprintf("%d", size)); err != nil { @@ -76,3 +81,25 @@ func testClusterUsingDiscovery(t *testing.T, size int, peerTLS bool) { t.Fatal(err) } } + +func MustNewHTTPClient(t testutil.TB, eps []string, tls *transport.TLSInfo) client.Client { + cfgtls := transport.TLSInfo{} + if tls != nil { + cfgtls = *tls + } + cfg := client.Config{Transport: mustNewTransport(t, cfgtls), Endpoints: eps} + c, err := client.New(cfg) + if err != nil { + t.Fatal(err) + } + return c +} + +func mustNewTransport(t testutil.TB, tlsInfo transport.TLSInfo) *http.Transport { + // tick in integration test is short, so 1s dial timeout could play well. + tr, err := transport.NewTimeoutTransport(tlsInfo, time.Second, rafthttp.ConnReadTimeout, rafthttp.ConnWriteTimeout) + if err != nil { + t.Fatal(err) + } + return tr +} diff --git a/tests/framework/integration/cluster.go b/tests/framework/integration/cluster.go index c5687a486..a32e63471 100644 --- a/tests/framework/integration/cluster.go +++ b/tests/framework/integration/cluster.go @@ -38,7 +38,6 @@ import ( "go.etcd.io/etcd/client/pkg/v3/tlsutil" "go.etcd.io/etcd/client/pkg/v3/transport" "go.etcd.io/etcd/client/pkg/v3/types" - "go.etcd.io/etcd/client/v2" "go.etcd.io/etcd/client/v3" "go.etcd.io/etcd/pkg/v3/grpc_testing" "go.etcd.io/etcd/raft/v3" @@ -1305,28 +1304,6 @@ func (m *Member) ReadyNotify() <-chan struct{} { return m.Server.ReadyNotify() } -func MustNewHTTPClient(t testutil.TB, eps []string, tls *transport.TLSInfo) client.Client { - cfgtls := transport.TLSInfo{} - if tls != nil { - cfgtls = *tls - } - cfg := client.Config{Transport: mustNewTransport(t, cfgtls), Endpoints: eps} - c, err := client.New(cfg) - if err != nil { - t.Fatal(err) - } - return c -} - -func mustNewTransport(t testutil.TB, tlsInfo transport.TLSInfo) *http.Transport { - // tick in integration test is short, so 1s dial timeout could play well. - tr, err := transport.NewTimeoutTransport(tlsInfo, time.Second, rafthttp.ConnReadTimeout, rafthttp.ConnWriteTimeout) - if err != nil { - t.Fatal(err) - } - return tr -} - type SortableMemberSliceByPeerURLs []*pb.Member func (p SortableMemberSliceByPeerURLs) Len() int { return len(p) }