server: Remove most of V2 API

This commit is contained in:
Marek Siarkowicz 2022-01-25 14:46:21 +01:00
parent 37879eb5f9
commit 692b3c4cd7
24 changed files with 39 additions and 6612 deletions

View File

@ -29,7 +29,6 @@ import (
)
const (
configPath = "/config"
varsPath = "/debug/vars"
versionPath = "/version"
)

View File

@ -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)

View File

@ -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
}

View File

@ -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
}

View File

@ -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")
}
}

View File

@ -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,

View File

@ -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)
}
}

View File

@ -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)
})
}

View File

@ -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
}
}
}

View File

@ -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)
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -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

View File

@ -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)
})
}

View File

@ -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)
}
}
}
}

View File

@ -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)
}

View File

@ -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)
}
}
}

View File

@ -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
}
}

View File

@ -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-----

View File

@ -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-----

View File

@ -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-----

View File

@ -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
}

View File

@ -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.

View File

@ -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
}

View File

@ -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) }