From 131e3806bb77a2472c9059bacbeb7f0e8f7764e9 Mon Sep 17 00:00:00 2001 From: Hitoshi Mitake Date: Wed, 13 Apr 2016 13:44:53 +0900 Subject: [PATCH] *: support authenticate in v3 auth This commit implements Authenticate() API of the auth package. It does authentication based on its authUsers bucket and generate a token for succeeding RPCs. --- auth/simple_token.go | 71 ++++++++++++ auth/store.go | 39 +++++++ clientv3/auth.go | 9 ++ etcdserver/api/v3rpc/auth.go | 7 +- etcdserver/api/v3rpc/rpctypes/error.go | 1 + etcdserver/api/v3rpc/util.go | 2 + etcdserver/apply.go | 7 ++ etcdserver/etcdserverpb/raft_internal.pb.go | 58 +++++++++- etcdserver/etcdserverpb/raft_internal.proto | 3 +- etcdserver/etcdserverpb/rpc.pb.go | 121 ++++++++++++++++++++ etcdserver/etcdserverpb/rpc.proto | 4 + etcdserver/v3_server.go | 9 ++ 12 files changed, 324 insertions(+), 7 deletions(-) create mode 100644 auth/simple_token.go diff --git a/auth/simple_token.go b/auth/simple_token.go new file mode 100644 index 000000000..984a58772 --- /dev/null +++ b/auth/simple_token.go @@ -0,0 +1,71 @@ +// 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 auth + +// CAUTION: This randum number based token mechanism is only for testing purpose. +// JWT based mechanism will be added in the near future. + +import ( + "crypto/rand" + "math/big" +) + +const ( + letters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" + defaultSimpleTokenLength = 16 +) + +var ( + simpleTokens map[string]string // token -> user ID +) + +func init() { + simpleTokens = make(map[string]string) +} + +func genSimpleToken() (string, error) { + ret := make([]byte, defaultSimpleTokenLength) + + for i := 0; i < defaultSimpleTokenLength; i++ { + bInt, err := rand.Int(rand.Reader, big.NewInt(int64(len(letters)))) + if err != nil { + return "", err + } + + ret[i] = letters[bInt.Int64()] + } + + return string(ret), nil +} + +func genSimpleTokenForUser(userID string) (string, error) { + var token string + var err error + + for { + // generating random numbers in RSM would't a good idea + token, err = genSimpleToken() + if err != nil { + return "", err + } + + if _, ok := simpleTokens[token]; !ok { + break + } + } + + simpleTokens[token] = userID + return token, nil +} diff --git a/auth/store.go b/auth/store.go index 7f1b7262b..a0d0597a5 100644 --- a/auth/store.go +++ b/auth/store.go @@ -40,12 +40,19 @@ var ( ErrUserNotFound = errors.New("auth: user not found") ErrRoleAlreadyExist = errors.New("auth: role already exists") ErrRoleNotFound = errors.New("auth: role not found") + ErrAuthFailed = errors.New("auth: authentication failed, invalid user ID or password") ) type AuthStore interface { // AuthEnable() turns on the authentication feature AuthEnable() + // Authenticate() does authentication based on given user name and password, + // and returns a token for successful case. + // Note that the generated token is valid only for the member the client + // connected to within fixed time duration. Reauth is required after the duration. + Authenticate(name string, password string) (*pb.AuthenticateResponse, error) + // Recover recovers the state of auth store from the given backend Recover(b backend.Backend) @@ -85,6 +92,38 @@ func (as *authStore) AuthEnable() { plog.Noticef("Authentication enabled") } +func (as *authStore) Authenticate(name string, password string) (*pb.AuthenticateResponse, error) { + tx := as.be.BatchTx() + tx.Lock() + defer tx.Unlock() + + _, vs := tx.UnsafeRange(authUsersBucketName, []byte(name), nil, 0) + if len(vs) != 1 { + plog.Noticef("authentication failed, user %s doesn't exist", name) + return &pb.AuthenticateResponse{}, ErrAuthFailed + } + + user := &authpb.User{} + err := user.Unmarshal(vs[0]) + if err != nil { + return nil, err + } + + if bcrypt.CompareHashAndPassword(user.Password, []byte(password)) != nil { + plog.Noticef("authentication failed, invalid password for user %s", name) + return &pb.AuthenticateResponse{}, ErrAuthFailed + } + + token, err := genSimpleTokenForUser(name) + if err != nil { + plog.Errorf("failed to generate simple token: %s", err) + return nil, err + } + + plog.Infof("authorized %s, token is %s", name, token) + return &pb.AuthenticateResponse{Token: token}, nil +} + func (as *authStore) Recover(be backend.Backend) { as.be = be // TODO(mitake): recovery process diff --git a/clientv3/auth.go b/clientv3/auth.go index e44ac0c67..9370295f2 100644 --- a/clientv3/auth.go +++ b/clientv3/auth.go @@ -26,6 +26,7 @@ import ( type ( AuthEnableResponse pb.AuthEnableResponse + AuthenticateResponse pb.AuthenticateResponse AuthUserAddResponse pb.AuthUserAddResponse AuthUserDeleteResponse pb.AuthUserDeleteResponse AuthUserChangePasswordResponse pb.AuthUserChangePasswordResponse @@ -46,6 +47,9 @@ type Auth interface { // AuthEnable enables auth of an etcd cluster. AuthEnable(ctx context.Context) (*AuthEnableResponse, error) + // Authenticate does authenticate with given user name and password. + Authenticate(ctx context.Context, name string, password string) (*AuthenticateResponse, error) + // UserAdd adds a new user to an etcd cluster. UserAdd(ctx context.Context, name string, password string) (*AuthUserAddResponse, error) @@ -86,6 +90,11 @@ func (auth *auth) AuthEnable(ctx context.Context) (*AuthEnableResponse, error) { return (*AuthEnableResponse)(resp), err } +func (auth *auth) Authenticate(ctx context.Context, name string, password string) (*AuthenticateResponse, error) { + resp, err := auth.remote.Authenticate(ctx, &pb.AuthenticateRequest{Name: name, Password: password}) + return (*AuthenticateResponse)(resp), err +} + func (auth *auth) UserAdd(ctx context.Context, name string, password string) (*AuthUserAddResponse, error) { resp, err := auth.remote.UserAdd(ctx, &pb.AuthUserAddRequest{Name: name, Password: password}) return (*AuthUserAddResponse)(resp), err diff --git a/etcdserver/api/v3rpc/auth.go b/etcdserver/api/v3rpc/auth.go index 05a04532a..daed48f1c 100644 --- a/etcdserver/api/v3rpc/auth.go +++ b/etcdserver/api/v3rpc/auth.go @@ -42,8 +42,11 @@ func (as *AuthServer) AuthDisable(ctx context.Context, r *pb.AuthDisableRequest) } func (as *AuthServer) Authenticate(ctx context.Context, r *pb.AuthenticateRequest) (*pb.AuthenticateResponse, error) { - plog.Info("not implemented yet") - return nil, nil + resp, err := as.authenticator.Authenticate(ctx, r) + if err != nil { + return nil, togRPCError(err) + } + return resp, nil } func (as *AuthServer) RoleAdd(ctx context.Context, r *pb.AuthRoleAddRequest) (*pb.AuthRoleAddResponse, error) { diff --git a/etcdserver/api/v3rpc/rpctypes/error.go b/etcdserver/api/v3rpc/rpctypes/error.go index 376a91dcb..4c8982089 100644 --- a/etcdserver/api/v3rpc/rpctypes/error.go +++ b/etcdserver/api/v3rpc/rpctypes/error.go @@ -41,4 +41,5 @@ var ( ErrUserNotFound = grpc.Errorf(codes.FailedPrecondition, "etcdserver: user name not found") ErrRoleAlreadyExist = grpc.Errorf(codes.FailedPrecondition, "etcdserver: role name already exists") ErrRoleNotFound = grpc.Errorf(codes.FailedPrecondition, "etcdserver: role name not found") + ErrAuthFailed = grpc.Errorf(codes.InvalidArgument, "etcdserver: authentication failed, invalid user ID or password") ) diff --git a/etcdserver/api/v3rpc/util.go b/etcdserver/api/v3rpc/util.go index a4fae02cd..4b2d1a8f4 100644 --- a/etcdserver/api/v3rpc/util.go +++ b/etcdserver/api/v3rpc/util.go @@ -45,6 +45,8 @@ func togRPCError(err error) error { return rpctypes.ErrRoleAlreadyExist case auth.ErrRoleNotFound: return rpctypes.ErrRoleNotFound + case auth.ErrAuthFailed: + return rpctypes.ErrAuthFailed default: return grpc.Errorf(codes.Internal, err.Error()) } diff --git a/etcdserver/apply.go b/etcdserver/apply.go index 435e102b7..a55d4d739 100644 --- a/etcdserver/apply.go +++ b/etcdserver/apply.go @@ -54,6 +54,7 @@ type applierV3 interface { LeaseRevoke(lc *pb.LeaseRevokeRequest) (*pb.LeaseRevokeResponse, error) Alarm(*pb.AlarmRequest) (*pb.AlarmResponse, error) AuthEnable() (*pb.AuthEnableResponse, error) + Authenticate(r *pb.AuthenticateRequest) (*pb.AuthenticateResponse, error) UserAdd(ua *pb.AuthUserAddRequest) (*pb.AuthUserAddResponse, error) UserDelete(ua *pb.AuthUserDeleteRequest) (*pb.AuthUserDeleteResponse, error) UserChangePassword(ua *pb.AuthUserChangePasswordRequest) (*pb.AuthUserChangePasswordResponse, error) @@ -87,6 +88,8 @@ func (s *EtcdServer) applyV3Request(r *pb.InternalRaftRequest) *applyResult { ar.resp, ar.err = s.applyV3.Alarm(r.Alarm) case r.AuthEnable != nil: ar.resp, ar.err = s.applyV3.AuthEnable() + case r.Authenticate != nil: + ar.resp, ar.err = s.applyV3.Authenticate(r.Authenticate) case r.AuthUserAdd != nil: ar.resp, ar.err = s.applyV3.UserAdd(r.AuthUserAdd) case r.AuthUserDelete != nil: @@ -490,6 +493,10 @@ func (a *applierV3backend) AuthEnable() (*pb.AuthEnableResponse, error) { return &pb.AuthEnableResponse{}, nil } +func (a *applierV3backend) Authenticate(r *pb.AuthenticateRequest) (*pb.AuthenticateResponse, error) { + return a.s.AuthStore().Authenticate(r.Name, r.Password) +} + func (a *applierV3backend) UserAdd(r *pb.AuthUserAddRequest) (*pb.AuthUserAddResponse, error) { return a.s.AuthStore().UserAdd(r) } diff --git a/etcdserver/etcdserverpb/raft_internal.pb.go b/etcdserver/etcdserverpb/raft_internal.pb.go index 0b3a5fed6..49412f165 100644 --- a/etcdserver/etcdserverpb/raft_internal.pb.go +++ b/etcdserver/etcdserverpb/raft_internal.pb.go @@ -38,7 +38,8 @@ type InternalRaftRequest struct { AuthUserGrant *AuthUserGrantRequest `protobuf:"bytes,14,opt,name=auth_user_grant" json:"auth_user_grant,omitempty"` AuthRoleAdd *AuthRoleAddRequest `protobuf:"bytes,15,opt,name=auth_role_add" json:"auth_role_add,omitempty"` AuthRoleGrant *AuthRoleGrantRequest `protobuf:"bytes,16,opt,name=auth_role_grant" json:"auth_role_grant,omitempty"` - Alarm *AlarmRequest `protobuf:"bytes,17,opt,name=alarm" json:"alarm,omitempty"` + Authenticate *AuthenticateRequest `protobuf:"bytes,17,opt,name=authenticate" json:"authenticate,omitempty"` + Alarm *AlarmRequest `protobuf:"bytes,18,opt,name=alarm" json:"alarm,omitempty"` } func (m *InternalRaftRequest) Reset() { *m = InternalRaftRequest{} } @@ -228,18 +229,30 @@ func (m *InternalRaftRequest) MarshalTo(data []byte) (int, error) { } i += n15 } - if m.Alarm != nil { + if m.Authenticate != nil { data[i] = 0x8a i++ data[i] = 0x1 i++ - i = encodeVarintRaftInternal(data, i, uint64(m.Alarm.Size())) - n16, err := m.Alarm.MarshalTo(data[i:]) + i = encodeVarintRaftInternal(data, i, uint64(m.Authenticate.Size())) + n16, err := m.Authenticate.MarshalTo(data[i:]) if err != nil { return 0, err } i += n16 } + if m.Alarm != nil { + data[i] = 0x92 + i++ + data[i] = 0x1 + i++ + i = encodeVarintRaftInternal(data, i, uint64(m.Alarm.Size())) + n17, err := m.Alarm.MarshalTo(data[i:]) + if err != nil { + return 0, err + } + i += n17 + } return i, nil } @@ -354,6 +367,10 @@ func (m *InternalRaftRequest) Size() (n int) { l = m.AuthRoleGrant.Size() n += 2 + l + sovRaftInternal(uint64(l)) } + if m.Authenticate != nil { + l = m.Authenticate.Size() + n += 2 + l + sovRaftInternal(uint64(l)) + } if m.Alarm != nil { l = m.Alarm.Size() n += 2 + l + sovRaftInternal(uint64(l)) @@ -924,6 +941,39 @@ func (m *InternalRaftRequest) Unmarshal(data []byte) error { } iNdEx = postIndex case 17: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field Authenticate", 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.Authenticate == nil { + m.Authenticate = &AuthenticateRequest{} + } + if err := m.Authenticate.Unmarshal(data[iNdEx:postIndex]); err != nil { + return err + } + iNdEx = postIndex + case 18: 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 2a6c9295d..2ea0c057a 100644 --- a/etcdserver/etcdserverpb/raft_internal.proto +++ b/etcdserver/etcdserverpb/raft_internal.proto @@ -32,8 +32,9 @@ message InternalRaftRequest { AuthUserGrantRequest auth_user_grant = 14; AuthRoleAddRequest auth_role_add = 15; AuthRoleGrantRequest auth_role_grant = 16; + AuthenticateRequest authenticate = 17; - AlarmRequest alarm = 17; + AlarmRequest alarm = 18; } message EmptyResponse { diff --git a/etcdserver/etcdserverpb/rpc.pb.go b/etcdserver/etcdserverpb/rpc.pb.go index 3e2935bf2..3e6ab14f8 100644 --- a/etcdserver/etcdserverpb/rpc.pb.go +++ b/etcdserver/etcdserverpb/rpc.pb.go @@ -1374,6 +1374,8 @@ func (m *AuthDisableRequest) String() string { return proto.CompactTextString(m) func (*AuthDisableRequest) ProtoMessage() {} type AuthenticateRequest 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 *AuthenticateRequest) Reset() { *m = AuthenticateRequest{} } @@ -1514,6 +1516,8 @@ func (m *AuthDisableResponse) GetHeader() *ResponseHeader { type AuthenticateResponse struct { Header *ResponseHeader `protobuf:"bytes,1,opt,name=header" json:"header,omitempty"` + // token is an authorized token that can be used in succeeding RPCs + Token string `protobuf:"bytes,2,opt,name=token,proto3" json:"token,omitempty"` } func (m *AuthenticateResponse) Reset() { *m = AuthenticateResponse{} } @@ -4755,6 +4759,18 @@ func (m *AuthenticateRequest) 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 } @@ -5101,6 +5117,12 @@ func (m *AuthenticateResponse) MarshalTo(data []byte) (int, error) { } i += n35 } + if len(m.Token) > 0 { + data[i] = 0x12 + i++ + i = encodeVarintRpc(data, i, uint64(len(m.Token))) + i += copy(data[i:], m.Token) + } return i, nil } @@ -6187,6 +6209,14 @@ func (m *AuthDisableRequest) Size() (n int) { func (m *AuthenticateRequest) 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 } @@ -6323,6 +6353,10 @@ func (m *AuthenticateResponse) Size() (n int) { l = m.Header.Size() n += 1 + l + sovRpc(uint64(l)) } + l = len(m.Token) + if l > 0 { + n += 1 + l + sovRpc(uint64(l)) + } return n } @@ -11393,6 +11427,64 @@ func (m *AuthenticateRequest) Unmarshal(data []byte) error { return fmt.Errorf("proto: AuthenticateRequest: 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:]) @@ -12486,6 +12578,35 @@ func (m *AuthenticateResponse) Unmarshal(data []byte) error { return err } iNdEx = postIndex + case 2: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field Token", 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.Token = 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 2904049b1..c8fb8b033 100644 --- a/etcdserver/etcdserverpb/rpc.proto +++ b/etcdserver/etcdserverpb/rpc.proto @@ -559,6 +559,8 @@ message AuthDisableRequest { } message AuthenticateRequest { + string name = 1; + string password = 2; } message AuthUserAddRequest { @@ -622,6 +624,8 @@ message AuthDisableResponse { message AuthenticateResponse { ResponseHeader header = 1; + // token is an authorized token that can be used in succeeding RPCs + string token = 2; } message AuthUserAddResponse { diff --git a/etcdserver/v3_server.go b/etcdserver/v3_server.go index 3c16f9f20..d02e06731 100644 --- a/etcdserver/v3_server.go +++ b/etcdserver/v3_server.go @@ -53,6 +53,7 @@ type Lessor interface { type Authenticator interface { AuthEnable(ctx context.Context, r *pb.AuthEnableRequest) (*pb.AuthEnableResponse, error) + Authenticate(ctx context.Context, r *pb.AuthenticateRequest) (*pb.AuthenticateResponse, 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) @@ -212,6 +213,14 @@ func (s *EtcdServer) AuthEnable(ctx context.Context, r *pb.AuthEnableRequest) (* return result.resp.(*pb.AuthEnableResponse), result.err } +func (s *EtcdServer) Authenticate(ctx context.Context, r *pb.AuthenticateRequest) (*pb.AuthenticateResponse, error) { + result, err := s.processInternalRaftRequest(ctx, pb.InternalRaftRequest{Authenticate: r}) + if err != nil { + return nil, err + } + return result.resp.(*pb.AuthenticateResponse), result.err +} + func (s *EtcdServer) UserAdd(ctx context.Context, r *pb.AuthUserAddRequest) (*pb.AuthUserAddResponse, error) { result, err := s.processInternalRaftRequest(ctx, pb.InternalRaftRequest{AuthUserAdd: r}) if err != nil {