mirror of
https://github.com/etcd-io/etcd.git
synced 2024-09-27 06:25:44 +00:00
server: Remove most of V2 API
This commit is contained in:
parent
37879eb5f9
commit
692b3c4cd7
@ -29,7 +29,6 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
configPath = "/config"
|
||||
varsPath = "/debug/vars"
|
||||
versionPath = "/version"
|
||||
)
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
}
|
@ -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
|
||||
}
|
@ -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")
|
||||
}
|
||||
}
|
@ -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,
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
@ -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)
|
||||
})
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
@ -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
@ -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
|
@ -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)
|
||||
})
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
19
server/etcdserver/api/v2http/testdata/ca.pem
vendored
19
server/etcdserver/api/v2http/testdata/ca.pem
vendored
@ -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-----
|
@ -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-----
|
20
server/etcdserver/api/v2http/testdata/user.pem
vendored
20
server/etcdserver/api/v2http/testdata/user.pem
vendored
@ -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-----
|
@ -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
|
||||
}
|
@ -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.
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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) }
|
||||
|
Loading…
x
Reference in New Issue
Block a user