From 73166b41e9e512b27ab080b439bfcea3ea5ffde1 Mon Sep 17 00:00:00 2001 From: Hitoshi Mitake Date: Thu, 31 Mar 2016 14:31:07 +0900 Subject: [PATCH] *: support changing password in v3 auth This commit adds a functionality for updating password of existing users. --- auth/store.go | 39 ++++++++++ clientv3/auth.go | 15 +++- etcdctl/ctlv3/command/user_command.go | 80 +++++++++++++++------ etcdserver/api/v3rpc/auth.go | 7 +- etcdserver/apply.go | 7 ++ etcdserver/etcdserverpb/raft_internal.pb.go | 80 ++++++++++++++++----- etcdserver/etcdserverpb/raft_internal.proto | 3 +- etcdserver/etcdserverpb/rpc.pb.go | 80 +++++++++++++++++++++ etcdserver/etcdserverpb/rpc.proto | 2 + etcdserver/v3demo_server.go | 9 +++ 10 files changed, 280 insertions(+), 42 deletions(-) diff --git a/auth/store.go b/auth/store.go index af6c039a8..42f6ab5dc 100644 --- a/auth/store.go +++ b/auth/store.go @@ -47,6 +47,9 @@ type AuthStore interface { // UserDelete deletes a user UserDelete(r *pb.AuthUserDeleteRequest) (*pb.AuthUserDeleteResponse, error) + + // UserChangePassword changes a password of a user + UserChangePassword(r *pb.AuthUserChangePasswordRequest) (*pb.AuthUserChangePasswordResponse, error) } type authStore struct { @@ -124,6 +127,42 @@ func (as *authStore) UserDelete(r *pb.AuthUserDeleteRequest) (*pb.AuthUserDelete return &pb.AuthUserDeleteResponse{}, nil } +func (as *authStore) UserChangePassword(r *pb.AuthUserChangePasswordRequest) (*pb.AuthUserChangePasswordResponse, error) { + // TODO(mitake): measure the cost of bcrypt.GenerateFromPassword() + // If the cost is too high, we should move the encryption to outside of the raft + 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) != 1 { + return &pb.AuthUserChangePasswordResponse{}, ErrUserNotFound + } + + updatedUser := authpb.User{ + Name: []byte(r.Name), + Password: hashed, + } + + marshaledUser, merr := updatedUser.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("changed a password of a user: %s", r.Name) + + return &pb.AuthUserChangePasswordResponse{}, nil +} + func NewAuthStore(be backend.Backend) *authStore { tx := be.BatchTx() tx.Lock() diff --git a/clientv3/auth.go b/clientv3/auth.go index ff7851117..c4e57809a 100644 --- a/clientv3/auth.go +++ b/clientv3/auth.go @@ -21,9 +21,10 @@ import ( ) type ( - AuthEnableResponse pb.AuthEnableResponse - AuthUserAddResponse pb.AuthUserAddResponse - AuthUserDeleteResponse pb.AuthUserDeleteResponse + AuthEnableResponse pb.AuthEnableResponse + AuthUserAddResponse pb.AuthUserAddResponse + AuthUserDeleteResponse pb.AuthUserDeleteResponse + AuthUserChangePasswordResponse pb.AuthUserChangePasswordResponse ) type Auth interface { @@ -35,6 +36,9 @@ type Auth interface { // UserDelete deletes a user from an etcd cluster. UserDelete(ctx context.Context, name string) (*AuthUserDeleteResponse, error) + + // UserChangePassword changes a password of a user. + UserChangePassword(ctx context.Context, name string, password string) (*AuthUserChangePasswordResponse, error) } type auth struct { @@ -67,3 +71,8 @@ func (auth *auth) UserDelete(ctx context.Context, name string) (*AuthUserDeleteR resp, err := auth.remote.UserDelete(ctx, &pb.AuthUserDeleteRequest{Name: name}) return (*AuthUserDeleteResponse)(resp), err } + +func (auth *auth) UserChangePassword(ctx context.Context, name string, password string) (*AuthUserChangePasswordResponse, error) { + resp, err := auth.remote.UserChangePassword(ctx, &pb.AuthUserChangePasswordRequest{Name: name, Password: password}) + return (*AuthUserChangePasswordResponse)(resp), err +} diff --git a/etcdctl/ctlv3/command/user_command.go b/etcdctl/ctlv3/command/user_command.go index b9a1df467..770802d56 100644 --- a/etcdctl/ctlv3/command/user_command.go +++ b/etcdctl/ctlv3/command/user_command.go @@ -32,6 +32,7 @@ func NewUserCommand() *cobra.Command { ac.AddCommand(NewUserAddCommand()) ac.AddCommand(NewUserDeleteCommand()) + ac.AddCommand(NewUserChangePasswordCommand()) return ac } @@ -60,6 +61,18 @@ func NewUserDeleteCommand() *cobra.Command { } } +func NewUserChangePasswordCommand() *cobra.Command { + cmd := cobra.Command{ + Use: "passwd ", + Short: "change password of user", + Run: userChangePasswordCommandFunc, + } + + 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 { @@ -71,26 +84,7 @@ func userAddCommandFunc(cmd *cobra.Command, args []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 + password = readPasswordInteractive(args[0]) } _, err := mustClientFromCmd(cmd).Auth.UserAdd(context.TODO(), args[0], password) @@ -114,3 +108,49 @@ func userDeleteCommandFunc(cmd *cobra.Command, args []string) { fmt.Printf("User %s deleted\n", args[0]) } + +// userChangePasswordCommandFunc executes the "user passwd" command. +func userChangePasswordCommandFunc(cmd *cobra.Command, args []string) { + if len(args) != 1 { + ExitWithError(ExitBadArgs, fmt.Errorf("user passwd command requires user name as its argument.")) + } + + var password string + + if !passwordInteractive { + fmt.Scanf("%s", &password) + } else { + password = readPasswordInteractive(args[0]) + } + + _, err := mustClientFromCmd(cmd).Auth.UserChangePassword(context.TODO(), args[0], password) + if err != nil { + ExitWithError(ExitError, err) + } + + fmt.Println("Password updated") +} + +func readPasswordInteractive(name string) string { + prompt1 := fmt.Sprintf("Password of %s: ", name) + 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: ", name) + 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.")) + } + + return password1 +} diff --git a/etcdserver/api/v3rpc/auth.go b/etcdserver/api/v3rpc/auth.go index e1b1c2794..fa3bdf964 100644 --- a/etcdserver/api/v3rpc/auth.go +++ b/etcdserver/api/v3rpc/auth.go @@ -103,6 +103,9 @@ func (as *AuthServer) UserRevoke(ctx context.Context, r *pb.AuthUserRevokeReques } func (as *AuthServer) UserChangePassword(ctx context.Context, r *pb.AuthUserChangePasswordRequest) (*pb.AuthUserChangePasswordResponse, error) { - plog.Info("not implemented yet") - return nil, nil + resp, err := as.authenticator.UserChangePassword(ctx, r) + if err != nil { + return nil, togRPCError(err) + } + return resp, nil } diff --git a/etcdserver/apply.go b/etcdserver/apply.go index abf6d1db3..86f7f8b43 100644 --- a/etcdserver/apply.go +++ b/etcdserver/apply.go @@ -56,6 +56,7 @@ type applierV3 interface { AuthEnable() (*pb.AuthEnableResponse, error) UserAdd(ua *pb.AuthUserAddRequest) (*pb.AuthUserAddResponse, error) UserDelete(ua *pb.AuthUserDeleteRequest) (*pb.AuthUserDeleteResponse, error) + UserChangePassword(ua *pb.AuthUserChangePasswordRequest) (*pb.AuthUserChangePasswordResponse, error) } type applierV3backend struct { @@ -87,6 +88,8 @@ func (s *EtcdServer) applyV3Request(r *pb.InternalRaftRequest) *applyResult { ar.resp, ar.err = s.applyV3.UserAdd(r.AuthUserAdd) case r.AuthUserDelete != nil: ar.resp, ar.err = s.applyV3.UserDelete(r.AuthUserDelete) + case r.AuthUserChangePassword != nil: + ar.resp, ar.err = s.applyV3.UserChangePassword(r.AuthUserChangePassword) default: panic("not implemented") } @@ -482,6 +485,10 @@ func (a *applierV3backend) UserDelete(r *pb.AuthUserDeleteRequest) (*pb.AuthUser return a.s.AuthStore().UserDelete(r) } +func (a *applierV3backend) UserChangePassword(r *pb.AuthUserChangePasswordRequest) (*pb.AuthUserChangePasswordResponse, error) { + return a.s.AuthStore().UserChangePassword(r) +} + type quotaApplierV3 struct { applierV3 q Quota diff --git a/etcdserver/etcdserverpb/raft_internal.pb.go b/etcdserver/etcdserverpb/raft_internal.pb.go index d0d2852fe..1368692b6 100644 --- a/etcdserver/etcdserverpb/raft_internal.pb.go +++ b/etcdserver/etcdserverpb/raft_internal.pb.go @@ -22,19 +22,20 @@ var _ = math.Inf // An InternalRaftRequest is the union of all requests which can be // sent via raft. type InternalRaftRequest struct { - ID uint64 `protobuf:"varint,1,opt,name=ID,proto3" json:"ID,omitempty"` - V2 *Request `protobuf:"bytes,2,opt,name=v2" json:"v2,omitempty"` - Range *RangeRequest `protobuf:"bytes,3,opt,name=range" json:"range,omitempty"` - Put *PutRequest `protobuf:"bytes,4,opt,name=put" json:"put,omitempty"` - DeleteRange *DeleteRangeRequest `protobuf:"bytes,5,opt,name=delete_range" json:"delete_range,omitempty"` - Txn *TxnRequest `protobuf:"bytes,6,opt,name=txn" json:"txn,omitempty"` - Compaction *CompactionRequest `protobuf:"bytes,7,opt,name=compaction" json:"compaction,omitempty"` - 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"` - AuthUserAdd *AuthUserAddRequest `protobuf:"bytes,11,opt,name=auth_user_add" json:"auth_user_add,omitempty"` - AuthUserDelete *AuthUserDeleteRequest `protobuf:"bytes,12,opt,name=auth_user_delete" json:"auth_user_delete,omitempty"` - Alarm *AlarmRequest `protobuf:"bytes,13,opt,name=alarm" json:"alarm,omitempty"` + ID uint64 `protobuf:"varint,1,opt,name=ID,proto3" json:"ID,omitempty"` + V2 *Request `protobuf:"bytes,2,opt,name=v2" json:"v2,omitempty"` + Range *RangeRequest `protobuf:"bytes,3,opt,name=range" json:"range,omitempty"` + Put *PutRequest `protobuf:"bytes,4,opt,name=put" json:"put,omitempty"` + DeleteRange *DeleteRangeRequest `protobuf:"bytes,5,opt,name=delete_range" json:"delete_range,omitempty"` + Txn *TxnRequest `protobuf:"bytes,6,opt,name=txn" json:"txn,omitempty"` + Compaction *CompactionRequest `protobuf:"bytes,7,opt,name=compaction" json:"compaction,omitempty"` + 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"` + AuthUserAdd *AuthUserAddRequest `protobuf:"bytes,11,opt,name=auth_user_add" json:"auth_user_add,omitempty"` + AuthUserDelete *AuthUserDeleteRequest `protobuf:"bytes,12,opt,name=auth_user_delete" json:"auth_user_delete,omitempty"` + AuthUserChangePassword *AuthUserChangePasswordRequest `protobuf:"bytes,13,opt,name=auth_user_change_password" json:"auth_user_change_password,omitempty"` + Alarm *AlarmRequest `protobuf:"bytes,14,opt,name=alarm" json:"alarm,omitempty"` } func (m *InternalRaftRequest) Reset() { *m = InternalRaftRequest{} } @@ -182,16 +183,26 @@ func (m *InternalRaftRequest) MarshalTo(data []byte) (int, error) { } i += n11 } - if m.Alarm != nil { + if m.AuthUserChangePassword != nil { data[i] = 0x6a i++ - i = encodeVarintRaftInternal(data, i, uint64(m.Alarm.Size())) - n12, err := m.Alarm.MarshalTo(data[i:]) + i = encodeVarintRaftInternal(data, i, uint64(m.AuthUserChangePassword.Size())) + n12, err := m.AuthUserChangePassword.MarshalTo(data[i:]) if err != nil { return 0, err } i += n12 } + if m.Alarm != nil { + data[i] = 0x72 + i++ + i = encodeVarintRaftInternal(data, i, uint64(m.Alarm.Size())) + n13, err := m.Alarm.MarshalTo(data[i:]) + if err != nil { + return 0, err + } + i += n13 + } return i, nil } @@ -290,6 +301,10 @@ func (m *InternalRaftRequest) Size() (n int) { l = m.AuthUserDelete.Size() n += 1 + l + sovRaftInternal(uint64(l)) } + if m.AuthUserChangePassword != nil { + l = m.AuthUserChangePassword.Size() + n += 1 + l + sovRaftInternal(uint64(l)) + } if m.Alarm != nil { l = m.Alarm.Size() n += 1 + l + sovRaftInternal(uint64(l)) @@ -728,6 +743,39 @@ func (m *InternalRaftRequest) Unmarshal(data []byte) error { } iNdEx = postIndex case 13: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field AuthUserChangePassword", 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.AuthUserChangePassword == nil { + m.AuthUserChangePassword = &AuthUserChangePasswordRequest{} + } + if err := m.AuthUserChangePassword.Unmarshal(data[iNdEx:postIndex]); err != nil { + return err + } + iNdEx = postIndex + case 14: if wireType != 2 { return fmt.Errorf("proto: wrong wireType = %d for field Alarm", wireType) } diff --git a/etcdserver/etcdserverpb/raft_internal.proto b/etcdserver/etcdserverpb/raft_internal.proto index 7965639bc..097791b84 100644 --- a/etcdserver/etcdserverpb/raft_internal.proto +++ b/etcdserver/etcdserverpb/raft_internal.proto @@ -28,8 +28,9 @@ message InternalRaftRequest { AuthEnableRequest auth_enable = 10; AuthUserAddRequest auth_user_add = 11; AuthUserDeleteRequest auth_user_delete = 12; + AuthUserChangePasswordRequest auth_user_change_password = 13; - AlarmRequest alarm = 13; + AlarmRequest alarm = 14; } message EmptyResponse { diff --git a/etcdserver/etcdserverpb/rpc.pb.go b/etcdserver/etcdserverpb/rpc.pb.go index 80e8dfd8f..0628a6a45 100644 --- a/etcdserver/etcdserverpb/rpc.pb.go +++ b/etcdserver/etcdserverpb/rpc.pb.go @@ -1295,6 +1295,8 @@ func (m *AuthUserDeleteRequest) String() string { return proto.CompactTextString func (*AuthUserDeleteRequest) ProtoMessage() {} type AuthUserChangePasswordRequest 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 *AuthUserChangePasswordRequest) Reset() { *m = AuthUserChangePasswordRequest{} } @@ -4490,6 +4492,18 @@ func (m *AuthUserChangePasswordRequest) 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 } @@ -5768,6 +5782,14 @@ func (m *AuthUserDeleteRequest) Size() (n int) { func (m *AuthUserChangePasswordRequest) 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 } @@ -10796,6 +10818,64 @@ func (m *AuthUserChangePasswordRequest) Unmarshal(data []byte) error { return fmt.Errorf("proto: AuthUserChangePasswordRequest: 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:]) diff --git a/etcdserver/etcdserverpb/rpc.proto b/etcdserver/etcdserverpb/rpc.proto index 4098fe619..82fb6c1a8 100644 --- a/etcdserver/etcdserverpb/rpc.proto +++ b/etcdserver/etcdserverpb/rpc.proto @@ -493,6 +493,8 @@ message AuthUserDeleteRequest { } message AuthUserChangePasswordRequest { + string name = 1; + string password = 2; } message AuthUserGrantRequest { diff --git a/etcdserver/v3demo_server.go b/etcdserver/v3demo_server.go index 8646fead6..2c16c1129 100644 --- a/etcdserver/v3demo_server.go +++ b/etcdserver/v3demo_server.go @@ -55,6 +55,7 @@ type Authenticator interface { AuthEnable(ctx context.Context, r *pb.AuthEnableRequest) (*pb.AuthEnableResponse, error) UserAdd(ctx context.Context, r *pb.AuthUserAddRequest) (*pb.AuthUserAddResponse, error) UserDelete(ctx context.Context, r *pb.AuthUserDeleteRequest) (*pb.AuthUserDeleteResponse, error) + UserChangePassword(ctx context.Context, r *pb.AuthUserChangePasswordRequest) (*pb.AuthUserChangePasswordResponse, error) } func (s *EtcdServer) Range(ctx context.Context, r *pb.RangeRequest) (*pb.RangeResponse, error) { @@ -200,6 +201,14 @@ func (s *EtcdServer) UserDelete(ctx context.Context, r *pb.AuthUserDeleteRequest return result.resp.(*pb.AuthUserDeleteResponse), result.err } +func (s *EtcdServer) UserChangePassword(ctx context.Context, r *pb.AuthUserChangePasswordRequest) (*pb.AuthUserChangePasswordResponse, error) { + result, err := s.processInternalRaftRequest(ctx, pb.InternalRaftRequest{AuthUserChangePassword: r}) + if err != nil { + return nil, err + } + return result.resp.(*pb.AuthUserChangePasswordResponse), result.err +} + func (s *EtcdServer) processInternalRaftRequest(ctx context.Context, r pb.InternalRaftRequest) (*applyResult, error) { r.ID = s.reqIDGen.Next()