소스 검색

Merge pull request #4845 from mitake/auth-user

*: support adding user in v3 auth
Xiang Li 9 년 전
부모
커밋
fa98d8d337

+ 379 - 0
auth/authpb/auth.pb.go

@@ -0,0 +1,379 @@
+// Code generated by protoc-gen-gogo.
+// source: auth.proto
+// DO NOT EDIT!
+
+/*
+	Package authpb is a generated protocol buffer package.
+
+	It is generated from these files:
+		auth.proto
+
+	It has these top-level messages:
+		User
+*/
+package authpb
+
+import (
+	"fmt"
+
+	proto "github.com/gogo/protobuf/proto"
+)
+
+import math "math"
+
+import io "io"
+
+// Reference imports to suppress errors if they are not otherwise used.
+var _ = proto.Marshal
+var _ = fmt.Errorf
+var _ = math.Inf
+
+// User is a single entry in the bucket authUsers
+type User struct {
+	Name      []byte `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"`
+	Password  []byte `protobuf:"bytes,2,opt,name=password,proto3" json:"password,omitempty"`
+	Tombstone int64  `protobuf:"varint,3,opt,name=tombstone,proto3" json:"tombstone,omitempty"`
+}
+
+func (m *User) Reset()         { *m = User{} }
+func (m *User) String() string { return proto.CompactTextString(m) }
+func (*User) ProtoMessage()    {}
+
+func init() {
+	proto.RegisterType((*User)(nil), "authpb.User")
+}
+func (m *User) Marshal() (data []byte, err error) {
+	size := m.Size()
+	data = make([]byte, size)
+	n, err := m.MarshalTo(data)
+	if err != nil {
+		return nil, err
+	}
+	return data[:n], nil
+}
+
+func (m *User) MarshalTo(data []byte) (int, error) {
+	var i int
+	_ = i
+	var l int
+	_ = l
+	if m.Name != nil {
+		if len(m.Name) > 0 {
+			data[i] = 0xa
+			i++
+			i = encodeVarintAuth(data, i, uint64(len(m.Name)))
+			i += copy(data[i:], m.Name)
+		}
+	}
+	if m.Password != nil {
+		if len(m.Password) > 0 {
+			data[i] = 0x12
+			i++
+			i = encodeVarintAuth(data, i, uint64(len(m.Password)))
+			i += copy(data[i:], m.Password)
+		}
+	}
+	if m.Tombstone != 0 {
+		data[i] = 0x18
+		i++
+		i = encodeVarintAuth(data, i, uint64(m.Tombstone))
+	}
+	return i, nil
+}
+
+func encodeFixed64Auth(data []byte, offset int, v uint64) int {
+	data[offset] = uint8(v)
+	data[offset+1] = uint8(v >> 8)
+	data[offset+2] = uint8(v >> 16)
+	data[offset+3] = uint8(v >> 24)
+	data[offset+4] = uint8(v >> 32)
+	data[offset+5] = uint8(v >> 40)
+	data[offset+6] = uint8(v >> 48)
+	data[offset+7] = uint8(v >> 56)
+	return offset + 8
+}
+func encodeFixed32Auth(data []byte, offset int, v uint32) int {
+	data[offset] = uint8(v)
+	data[offset+1] = uint8(v >> 8)
+	data[offset+2] = uint8(v >> 16)
+	data[offset+3] = uint8(v >> 24)
+	return offset + 4
+}
+func encodeVarintAuth(data []byte, offset int, v uint64) int {
+	for v >= 1<<7 {
+		data[offset] = uint8(v&0x7f | 0x80)
+		v >>= 7
+		offset++
+	}
+	data[offset] = uint8(v)
+	return offset + 1
+}
+func (m *User) Size() (n int) {
+	var l int
+	_ = l
+	if m.Name != nil {
+		l = len(m.Name)
+		if l > 0 {
+			n += 1 + l + sovAuth(uint64(l))
+		}
+	}
+	if m.Password != nil {
+		l = len(m.Password)
+		if l > 0 {
+			n += 1 + l + sovAuth(uint64(l))
+		}
+	}
+	if m.Tombstone != 0 {
+		n += 1 + sovAuth(uint64(m.Tombstone))
+	}
+	return n
+}
+
+func sovAuth(x uint64) (n int) {
+	for {
+		n++
+		x >>= 7
+		if x == 0 {
+			break
+		}
+	}
+	return n
+}
+func sozAuth(x uint64) (n int) {
+	return sovAuth(uint64((x << 1) ^ uint64((int64(x) >> 63))))
+}
+func (m *User) Unmarshal(data []byte) error {
+	l := len(data)
+	iNdEx := 0
+	for iNdEx < l {
+		preIndex := iNdEx
+		var wire uint64
+		for shift := uint(0); ; shift += 7 {
+			if shift >= 64 {
+				return ErrIntOverflowAuth
+			}
+			if iNdEx >= l {
+				return io.ErrUnexpectedEOF
+			}
+			b := data[iNdEx]
+			iNdEx++
+			wire |= (uint64(b) & 0x7F) << shift
+			if b < 0x80 {
+				break
+			}
+		}
+		fieldNum := int32(wire >> 3)
+		wireType := int(wire & 0x7)
+		if wireType == 4 {
+			return fmt.Errorf("proto: User: wiretype end group for non-group")
+		}
+		if fieldNum <= 0 {
+			return fmt.Errorf("proto: User: illegal tag %d (wire type %d)", fieldNum, wire)
+		}
+		switch fieldNum {
+		case 1:
+			if wireType != 2 {
+				return fmt.Errorf("proto: wrong wireType = %d for field Name", wireType)
+			}
+			var byteLen int
+			for shift := uint(0); ; shift += 7 {
+				if shift >= 64 {
+					return ErrIntOverflowAuth
+				}
+				if iNdEx >= l {
+					return io.ErrUnexpectedEOF
+				}
+				b := data[iNdEx]
+				iNdEx++
+				byteLen |= (int(b) & 0x7F) << shift
+				if b < 0x80 {
+					break
+				}
+			}
+			if byteLen < 0 {
+				return ErrInvalidLengthAuth
+			}
+			postIndex := iNdEx + byteLen
+			if postIndex > l {
+				return io.ErrUnexpectedEOF
+			}
+			m.Name = append(m.Name[:0], data[iNdEx:postIndex]...)
+			if m.Name == nil {
+				m.Name = []byte{}
+			}
+			iNdEx = postIndex
+		case 2:
+			if wireType != 2 {
+				return fmt.Errorf("proto: wrong wireType = %d for field Password", wireType)
+			}
+			var byteLen int
+			for shift := uint(0); ; shift += 7 {
+				if shift >= 64 {
+					return ErrIntOverflowAuth
+				}
+				if iNdEx >= l {
+					return io.ErrUnexpectedEOF
+				}
+				b := data[iNdEx]
+				iNdEx++
+				byteLen |= (int(b) & 0x7F) << shift
+				if b < 0x80 {
+					break
+				}
+			}
+			if byteLen < 0 {
+				return ErrInvalidLengthAuth
+			}
+			postIndex := iNdEx + byteLen
+			if postIndex > l {
+				return io.ErrUnexpectedEOF
+			}
+			m.Password = append(m.Password[:0], data[iNdEx:postIndex]...)
+			if m.Password == nil {
+				m.Password = []byte{}
+			}
+			iNdEx = postIndex
+		case 3:
+			if wireType != 0 {
+				return fmt.Errorf("proto: wrong wireType = %d for field Tombstone", wireType)
+			}
+			m.Tombstone = 0
+			for shift := uint(0); ; shift += 7 {
+				if shift >= 64 {
+					return ErrIntOverflowAuth
+				}
+				if iNdEx >= l {
+					return io.ErrUnexpectedEOF
+				}
+				b := data[iNdEx]
+				iNdEx++
+				m.Tombstone |= (int64(b) & 0x7F) << shift
+				if b < 0x80 {
+					break
+				}
+			}
+		default:
+			iNdEx = preIndex
+			skippy, err := skipAuth(data[iNdEx:])
+			if err != nil {
+				return err
+			}
+			if skippy < 0 {
+				return ErrInvalidLengthAuth
+			}
+			if (iNdEx + skippy) > l {
+				return io.ErrUnexpectedEOF
+			}
+			iNdEx += skippy
+		}
+	}
+
+	if iNdEx > l {
+		return io.ErrUnexpectedEOF
+	}
+	return nil
+}
+func skipAuth(data []byte) (n int, err error) {
+	l := len(data)
+	iNdEx := 0
+	for iNdEx < l {
+		var wire uint64
+		for shift := uint(0); ; shift += 7 {
+			if shift >= 64 {
+				return 0, ErrIntOverflowAuth
+			}
+			if iNdEx >= l {
+				return 0, io.ErrUnexpectedEOF
+			}
+			b := data[iNdEx]
+			iNdEx++
+			wire |= (uint64(b) & 0x7F) << shift
+			if b < 0x80 {
+				break
+			}
+		}
+		wireType := int(wire & 0x7)
+		switch wireType {
+		case 0:
+			for shift := uint(0); ; shift += 7 {
+				if shift >= 64 {
+					return 0, ErrIntOverflowAuth
+				}
+				if iNdEx >= l {
+					return 0, io.ErrUnexpectedEOF
+				}
+				iNdEx++
+				if data[iNdEx-1] < 0x80 {
+					break
+				}
+			}
+			return iNdEx, nil
+		case 1:
+			iNdEx += 8
+			return iNdEx, nil
+		case 2:
+			var length int
+			for shift := uint(0); ; shift += 7 {
+				if shift >= 64 {
+					return 0, ErrIntOverflowAuth
+				}
+				if iNdEx >= l {
+					return 0, io.ErrUnexpectedEOF
+				}
+				b := data[iNdEx]
+				iNdEx++
+				length |= (int(b) & 0x7F) << shift
+				if b < 0x80 {
+					break
+				}
+			}
+			iNdEx += length
+			if length < 0 {
+				return 0, ErrInvalidLengthAuth
+			}
+			return iNdEx, nil
+		case 3:
+			for {
+				var innerWire uint64
+				var start int = iNdEx
+				for shift := uint(0); ; shift += 7 {
+					if shift >= 64 {
+						return 0, ErrIntOverflowAuth
+					}
+					if iNdEx >= l {
+						return 0, io.ErrUnexpectedEOF
+					}
+					b := data[iNdEx]
+					iNdEx++
+					innerWire |= (uint64(b) & 0x7F) << shift
+					if b < 0x80 {
+						break
+					}
+				}
+				innerWireType := int(innerWire & 0x7)
+				if innerWireType == 4 {
+					break
+				}
+				next, err := skipAuth(data[start:])
+				if err != nil {
+					return 0, err
+				}
+				iNdEx = start + next
+			}
+			return iNdEx, nil
+		case 4:
+			return iNdEx, nil
+		case 5:
+			iNdEx += 4
+			return iNdEx, nil
+		default:
+			return 0, fmt.Errorf("proto: illegal wireType %d", wireType)
+		}
+	}
+	panic("unreachable")
+}
+
+var (
+	ErrInvalidLengthAuth = fmt.Errorf("proto: negative length found during unmarshaling")
+	ErrIntOverflowAuth   = fmt.Errorf("proto: integer overflow")
+)

+ 17 - 0
auth/authpb/auth.proto

@@ -0,0 +1,17 @@
+syntax = "proto3";
+package authpb;
+
+import "gogoproto/gogo.proto";
+
+option (gogoproto.marshaler_all) = true;
+option (gogoproto.sizer_all) = true;
+option (gogoproto.unmarshaler_all) = true;
+option (gogoproto.goproto_getters_all) = false;
+option (gogoproto.goproto_enum_prefix_all) = false;
+
+// User is a single entry in the bucket authUsers
+message User {
+  bytes name = 1;
+  bytes password = 2;
+  int64 tombstone = 3;
+}

+ 49 - 2
auth/store.go

@@ -15,13 +15,18 @@
 package auth
 
 import (
+	"github.com/coreos/etcd/auth/authpb"
+	"github.com/coreos/etcd/etcdserver/api/v3rpc/rpctypes"
+	pb "github.com/coreos/etcd/etcdserver/etcdserverpb"
 	"github.com/coreos/etcd/storage/backend"
 	"github.com/coreos/pkg/capnslog"
+	"golang.org/x/crypto/bcrypt"
 )
 
 var (
-	enableFlagKey  = []byte("authEnabled")
-	authBucketName = []byte("auth")
+	enableFlagKey       = []byte("authEnabled")
+	authBucketName      = []byte("auth")
+	authUsersBucketName = []byte("authUsers")
 
 	plog = capnslog.NewPackageLogger("github.com/coreos/etcd", "auth")
 )
@@ -32,6 +37,9 @@ type AuthStore interface {
 
 	// Recover recovers the state of auth store from the given backend
 	Recover(b backend.Backend)
+
+	// UserAdd adds a new user
+	UserAdd(r *pb.UserAddRequest) (*pb.UserAddResponse, error)
 }
 
 type authStore struct {
@@ -56,10 +64,49 @@ func (as *authStore) Recover(be backend.Backend) {
 	// TODO(mitake): recovery process
 }
 
+func (as *authStore) UserAdd(r *pb.UserAddRequest) (*pb.UserAddResponse, error) {
+	plog.Noticef("adding a new user: %s", r.Name)
+
+	hashed, err := bcrypt.GenerateFromPassword([]byte(r.Password), bcrypt.DefaultCost)
+	if err != nil {
+		plog.Errorf("failed to hash password: %s", err)
+		return nil, err
+	}
+
+	tx := as.be.BatchTx()
+	tx.Lock()
+	defer tx.Unlock()
+
+	_, vs := tx.UnsafeRange(authUsersBucketName, []byte(r.Name), nil, 0)
+	if len(vs) != 0 {
+		return &pb.UserAddResponse{}, rpctypes.ErrUserAlreadyExist
+	}
+
+	newUser := authpb.User{
+		Name:     []byte(r.Name),
+		Password: hashed,
+	}
+
+	marshaledUser, merr := newUser.Marshal()
+	if merr != nil {
+		plog.Errorf("failed to marshal a new user data: %s", merr)
+		return nil, merr
+	}
+
+	tx.UnsafePut(authUsersBucketName, []byte(r.Name), marshaledUser)
+
+	plog.Noticef("added a new user: %s", r.Name)
+
+	return &pb.UserAddResponse{}, nil
+}
+
 func NewAuthStore(be backend.Backend) *authStore {
 	tx := be.BatchTx()
 	tx.Lock()
+
 	tx.UnsafeCreateBucket(authBucketName)
+	tx.UnsafeCreateBucket(authUsersBucketName)
+
 	tx.Unlock()
 	be.ForceCommit()
 

+ 10 - 1
clientv3/auth.go

@@ -22,11 +22,15 @@ import (
 
 type (
 	AuthEnableResponse pb.AuthEnableResponse
+	UserAddResponse    pb.UserAddResponse
 )
 
 type Auth interface {
-	// AuthEnable enables auth of a etcd cluster.
+	// AuthEnable enables auth of an etcd cluster.
 	AuthEnable(ctx context.Context) (*AuthEnableResponse, error)
+
+	// UserAdd adds a new user to an etcd cluster.
+	UserAdd(ctx context.Context, name string, password string) (*UserAddResponse, error)
 }
 
 type auth struct {
@@ -49,3 +53,8 @@ func (auth *auth) AuthEnable(ctx context.Context) (*AuthEnableResponse, error) {
 	resp, err := auth.remote.AuthEnable(ctx, &pb.AuthEnableRequest{})
 	return (*AuthEnableResponse)(resp), err
 }
+
+func (auth *auth) UserAdd(ctx context.Context, name string, password string) (*UserAddResponse, error) {
+	resp, err := auth.remote.UserAdd(ctx, &pb.UserAddRequest{Name: name, Password: password})
+	return (*UserAddResponse)(resp), err
+}

+ 91 - 0
etcdctlv3/command/user_command.go

@@ -0,0 +1,91 @@
+// Copyright 2016 Nippon Telegraph and Telephone Corporation.
+//
+// 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 command
+
+import (
+	"fmt"
+	"strings"
+
+	"github.com/bgentry/speakeasy"
+	"github.com/spf13/cobra"
+	"golang.org/x/net/context"
+)
+
+// NewUserCommand returns the cobra command for "user".
+func NewUserCommand() *cobra.Command {
+	ac := &cobra.Command{
+		Use:   "user <subcommand>",
+		Short: "user related command",
+	}
+
+	ac.AddCommand(NewUserAddCommand())
+
+	return ac
+}
+
+var (
+	passwordInteractive bool
+)
+
+func NewUserAddCommand() *cobra.Command {
+	cmd := cobra.Command{
+		Use:   "add <user name>",
+		Short: "add a new user",
+		Run:   userAddCommandFunc,
+	}
+
+	cmd.Flags().BoolVar(&passwordInteractive, "interactive", true, "read password from stdin instead of interactive terminal")
+
+	return &cmd
+}
+
+// userAddCommandFunc executes the "user add" command.
+func userAddCommandFunc(cmd *cobra.Command, args []string) {
+	if len(args) != 1 {
+		ExitWithError(ExitBadArgs, fmt.Errorf("user add command requires user name as its argument."))
+	}
+
+	var password string
+
+	if !passwordInteractive {
+		fmt.Scanf("%s", &password)
+	} else {
+		prompt1 := fmt.Sprintf("Password of %s: ", args[0])
+		password1, err1 := speakeasy.Ask(prompt1)
+		if err1 != nil {
+			ExitWithError(ExitBadArgs, fmt.Errorf("failed to ask password: %s.", err1))
+		}
+
+		if len(password1) == 0 {
+			ExitWithError(ExitBadArgs, fmt.Errorf("empty password"))
+		}
+
+		prompt2 := fmt.Sprintf("Type password of %s again for confirmation: ", args[0])
+		password2, err2 := speakeasy.Ask(prompt2)
+		if err2 != nil {
+			ExitWithError(ExitBadArgs, fmt.Errorf("failed to ask password: %s.", err2))
+		}
+
+		if strings.Compare(password1, password2) != 0 {
+			ExitWithError(ExitBadArgs, fmt.Errorf("given passwords are different."))
+		}
+		password = password1
+	}
+
+	_, err := mustClientFromCmd(cmd).Auth.UserAdd(context.TODO(), args[0], password)
+	if err != nil {
+		ExitWithError(ExitError, err)
+	}
+}

+ 1 - 0
etcdctlv3/main.go

@@ -77,6 +77,7 @@ func init() {
 		command.NewLockCommand(),
 		command.NewAuthCommand(),
 		command.NewElectCommand(),
+		command.NewUserCommand(),
 	)
 }
 

+ 1 - 2
etcdserver/api/v3rpc/auth.go

@@ -68,8 +68,7 @@ func (as *AuthServer) RoleGrant(ctx context.Context, r *pb.RoleGrantRequest) (*p
 }
 
 func (as *AuthServer) UserAdd(ctx context.Context, r *pb.UserAddRequest) (*pb.UserAddResponse, error) {
-	plog.Info("not implemented yet")
-	return nil, nil
+	return as.authenticator.UserAdd(ctx, r)
 }
 
 func (as *AuthServer) UserDelete(ctx context.Context, r *pb.UserDeleteRequest) (*pb.UserDeleteResponse, error) {

+ 2 - 0
etcdserver/api/v3rpc/rpctypes/error.go

@@ -35,4 +35,6 @@ var (
 	ErrMemberNotFound = grpc.Errorf(codes.NotFound, "etcdserver: member not found")
 
 	ErrRequestTooLarge = grpc.Errorf(codes.InvalidArgument, "etcdserver: request is too large")
+
+	ErrUserAlreadyExist = grpc.Errorf(codes.FailedPrecondition, "etcdserver: user name already exists")
 )

+ 48 - 0
etcdserver/etcdserverpb/raft_internal.pb.go

@@ -32,6 +32,7 @@ type InternalRaftRequest struct {
 	LeaseCreate *LeaseCreateRequest `protobuf:"bytes,8,opt,name=lease_create" json:"lease_create,omitempty"`
 	LeaseRevoke *LeaseRevokeRequest `protobuf:"bytes,9,opt,name=lease_revoke" json:"lease_revoke,omitempty"`
 	AuthEnable  *AuthEnableRequest  `protobuf:"bytes,10,opt,name=auth_enable" json:"auth_enable,omitempty"`
+	UserAdd     *UserAddRequest     `protobuf:"bytes,11,opt,name=user_add" json:"user_add,omitempty"`
 }
 
 func (m *InternalRaftRequest) Reset()         { *m = InternalRaftRequest{} }
@@ -159,6 +160,16 @@ func (m *InternalRaftRequest) MarshalTo(data []byte) (int, error) {
 		}
 		i += n9
 	}
+	if m.UserAdd != nil {
+		data[i] = 0x5a
+		i++
+		i = encodeVarintRaftInternal(data, i, uint64(m.UserAdd.Size()))
+		n10, err := m.UserAdd.MarshalTo(data[i:])
+		if err != nil {
+			return 0, err
+		}
+		i += n10
+	}
 	return i, nil
 }
 
@@ -249,6 +260,10 @@ func (m *InternalRaftRequest) Size() (n int) {
 		l = m.AuthEnable.Size()
 		n += 1 + l + sovRaftInternal(uint64(l))
 	}
+	if m.UserAdd != nil {
+		l = m.UserAdd.Size()
+		n += 1 + l + sovRaftInternal(uint64(l))
+	}
 	return n
 }
 
@@ -616,6 +631,39 @@ func (m *InternalRaftRequest) Unmarshal(data []byte) error {
 				return err
 			}
 			iNdEx = postIndex
+		case 11:
+			if wireType != 2 {
+				return fmt.Errorf("proto: wrong wireType = %d for field UserAdd", wireType)
+			}
+			var msglen int
+			for shift := uint(0); ; shift += 7 {
+				if shift >= 64 {
+					return ErrIntOverflowRaftInternal
+				}
+				if iNdEx >= l {
+					return io.ErrUnexpectedEOF
+				}
+				b := data[iNdEx]
+				iNdEx++
+				msglen |= (int(b) & 0x7F) << shift
+				if b < 0x80 {
+					break
+				}
+			}
+			if msglen < 0 {
+				return ErrInvalidLengthRaftInternal
+			}
+			postIndex := iNdEx + msglen
+			if postIndex > l {
+				return io.ErrUnexpectedEOF
+			}
+			if m.UserAdd == nil {
+				m.UserAdd = &UserAddRequest{}
+			}
+			if err := m.UserAdd.Unmarshal(data[iNdEx:postIndex]); err != nil {
+				return err
+			}
+			iNdEx = postIndex
 		default:
 			iNdEx = preIndex
 			skippy, err := skipRaftInternal(data[iNdEx:])

+ 1 - 0
etcdserver/etcdserverpb/raft_internal.proto

@@ -26,6 +26,7 @@ message InternalRaftRequest {
   LeaseRevokeRequest lease_revoke = 9;
 
   AuthEnableRequest auth_enable = 10;
+  UserAddRequest user_add = 11;
 }
 
 message EmptyResponse {

+ 80 - 0
etcdserver/etcdserverpb/rpc.pb.go

@@ -1181,6 +1181,8 @@ func (m *AuthenticateRequest) String() string { return proto.CompactTextString(m
 func (*AuthenticateRequest) ProtoMessage()    {}
 
 type UserAddRequest struct {
+	Name     string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"`
+	Password string `protobuf:"bytes,2,opt,name=password,proto3" json:"password,omitempty"`
 }
 
 func (m *UserAddRequest) Reset()         { *m = UserAddRequest{} }
@@ -4182,6 +4184,18 @@ func (m *UserAddRequest) MarshalTo(data []byte) (int, error) {
 	_ = i
 	var l int
 	_ = l
+	if len(m.Name) > 0 {
+		data[i] = 0xa
+		i++
+		i = encodeVarintRpc(data, i, uint64(len(m.Name)))
+		i += copy(data[i:], m.Name)
+	}
+	if len(m.Password) > 0 {
+		data[i] = 0x12
+		i++
+		i = encodeVarintRpc(data, i, uint64(len(m.Password)))
+		i += copy(data[i:], m.Password)
+	}
 	return i, nil
 }
 
@@ -5438,6 +5452,14 @@ func (m *AuthenticateRequest) Size() (n int) {
 func (m *UserAddRequest) Size() (n int) {
 	var l int
 	_ = l
+	l = len(m.Name)
+	if l > 0 {
+		n += 1 + l + sovRpc(uint64(l))
+	}
+	l = len(m.Password)
+	if l > 0 {
+		n += 1 + l + sovRpc(uint64(l))
+	}
 	return n
 }
 
@@ -9918,6 +9940,64 @@ func (m *UserAddRequest) Unmarshal(data []byte) error {
 			return fmt.Errorf("proto: UserAddRequest: illegal tag %d (wire type %d)", fieldNum, wire)
 		}
 		switch fieldNum {
+		case 1:
+			if wireType != 2 {
+				return fmt.Errorf("proto: wrong wireType = %d for field Name", wireType)
+			}
+			var stringLen uint64
+			for shift := uint(0); ; shift += 7 {
+				if shift >= 64 {
+					return ErrIntOverflowRpc
+				}
+				if iNdEx >= l {
+					return io.ErrUnexpectedEOF
+				}
+				b := data[iNdEx]
+				iNdEx++
+				stringLen |= (uint64(b) & 0x7F) << shift
+				if b < 0x80 {
+					break
+				}
+			}
+			intStringLen := int(stringLen)
+			if intStringLen < 0 {
+				return ErrInvalidLengthRpc
+			}
+			postIndex := iNdEx + intStringLen
+			if postIndex > l {
+				return io.ErrUnexpectedEOF
+			}
+			m.Name = string(data[iNdEx:postIndex])
+			iNdEx = postIndex
+		case 2:
+			if wireType != 2 {
+				return fmt.Errorf("proto: wrong wireType = %d for field Password", wireType)
+			}
+			var stringLen uint64
+			for shift := uint(0); ; shift += 7 {
+				if shift >= 64 {
+					return ErrIntOverflowRpc
+				}
+				if iNdEx >= l {
+					return io.ErrUnexpectedEOF
+				}
+				b := data[iNdEx]
+				iNdEx++
+				stringLen |= (uint64(b) & 0x7F) << shift
+				if b < 0x80 {
+					break
+				}
+			}
+			intStringLen := int(stringLen)
+			if intStringLen < 0 {
+				return ErrInvalidLengthRpc
+			}
+			postIndex := iNdEx + intStringLen
+			if postIndex > l {
+				return io.ErrUnexpectedEOF
+			}
+			m.Password = string(data[iNdEx:postIndex])
+			iNdEx = postIndex
 		default:
 			iNdEx = preIndex
 			skippy, err := skipRpc(data[iNdEx:])

+ 2 - 0
etcdserver/etcdserverpb/rpc.proto

@@ -448,6 +448,8 @@ message AuthenticateRequest {
 }
 
 message UserAddRequest {
+  string name = 1;
+  string password = 2;
 }
 
 message UserGetRequest {

+ 15 - 0
etcdserver/v3demo_server.go

@@ -59,6 +59,7 @@ type Lessor interface {
 
 type Authenticator interface {
 	AuthEnable(ctx context.Context, r *pb.AuthEnableRequest) (*pb.AuthEnableResponse, error)
+	UserAdd(ctx context.Context, r *pb.UserAddRequest) (*pb.UserAddResponse, error)
 }
 
 func (s *EtcdServer) Range(ctx context.Context, r *pb.RangeRequest) (*pb.RangeResponse, error) {
@@ -185,6 +186,14 @@ func (s *EtcdServer) AuthEnable(ctx context.Context, r *pb.AuthEnableRequest) (*
 	return result.resp.(*pb.AuthEnableResponse), result.err
 }
 
+func (s *EtcdServer) UserAdd(ctx context.Context, r *pb.UserAddRequest) (*pb.UserAddResponse, error) {
+	result, err := s.processInternalRaftRequest(ctx, pb.InternalRaftRequest{UserAdd: r})
+	if err != nil {
+		return nil, err
+	}
+	return result.resp.(*pb.UserAddResponse), result.err
+}
+
 type applyResult struct {
 	resp proto.Message
 	err  error
@@ -252,6 +261,8 @@ func (s *EtcdServer) applyV3Request(r *pb.InternalRaftRequest) interface{} {
 		ar.resp, ar.err = applyLeaseRevoke(le, r.LeaseRevoke)
 	case r.AuthEnable != nil:
 		ar.resp, ar.err = applyAuthEnable(s)
+	case r.UserAdd != nil:
+		ar.resp, ar.err = applyUserAdd(s, r.UserAdd)
 	default:
 		panic("not implemented")
 	}
@@ -660,3 +671,7 @@ func applyAuthEnable(s *EtcdServer) (*pb.AuthEnableResponse, error) {
 	s.AuthStore().AuthEnable()
 	return &pb.AuthEnableResponse{}, nil
 }
+
+func applyUserAdd(s *EtcdServer, r *pb.UserAddRequest) (*pb.UserAddResponse, error) {
+	return s.AuthStore().UserAdd(r)
+}

+ 1 - 1
scripts/genproto.sh

@@ -17,7 +17,7 @@ if ! [[ $(protoc --version) =~ "3.0.0" ]]; then
 fi
 
 # directories containing protos to be built
-DIRS="./wal/walpb ./etcdserver/etcdserverpb ./snap/snappb ./raft/raftpb ./storage/storagepb ./lease/leasepb"
+DIRS="./wal/walpb ./etcdserver/etcdserverpb ./snap/snappb ./raft/raftpb ./storage/storagepb ./lease/leasepb ./auth/authpb"
 
 # exact version of protoc-gen-gogo to build
 SHA="c57e439bad574c2e0877ff18d514badcfced004d"