From 61af764f8aa4d47c5815f1c19af050f9e47f445b Mon Sep 17 00:00:00 2001 From: Harshavardhana Date: Wed, 10 Aug 2016 21:09:31 -0700 Subject: [PATCH] Add rpc layer authentication. --- cmd/signature-jwt.go | 29 ++++--- cmd/signature-jwt_test.go | 12 +-- cmd/storage-rpc-client.go | 61 ++++++++------ cmd/storage-rpc-server-datatypes.go | 43 ++++++++++ cmd/storage-rpc-server.go | 119 +++++++++++++++++++++++----- cmd/web-handlers.go | 13 ++- 6 files changed, 210 insertions(+), 67 deletions(-) diff --git a/cmd/signature-jwt.go b/cmd/signature-jwt.go index 107c02efd..154aa4036 100644 --- a/cmd/signature-jwt.go +++ b/cmd/signature-jwt.go @@ -17,7 +17,7 @@ package cmd import ( - "fmt" + "errors" "strings" "time" @@ -32,24 +32,27 @@ type JWT struct { credential } -// Default - each token expires in 10hrs. +// Default each token expires in 100yrs. const ( - tokenExpires time.Duration = 10 + defaultTokenExpiry time.Duration = time.Hour * 876000 // 100yrs. ) // newJWT - returns new JWT object. -func newJWT() (*JWT, error) { +func newJWT(expiry time.Duration) (*JWT, error) { if serverConfig == nil { - return nil, fmt.Errorf("server not initialzed") + return nil, errors.New("Server not initialzed") + } + if expiry == 0 { + expiry = defaultTokenExpiry } // Save access, secret keys. cred := serverConfig.GetCredential() if !isValidAccessKey.MatchString(cred.AccessKeyID) { - return nil, fmt.Errorf("Invalid access key") + return nil, errors.New("Invalid access key") } if !isValidSecretKey.MatchString(cred.SecretAccessKey) { - return nil, fmt.Errorf("Invalid secret key") + return nil, errors.New("Invalid secret key") } return &JWT{cred}, nil @@ -61,13 +64,13 @@ func (jwt *JWT) GenerateToken(accessKey string) (string, error) { accessKey = strings.TrimSpace(accessKey) if !isValidAccessKey.MatchString(accessKey) { - return "", fmt.Errorf("Invalid access key") + return "", errors.New("Invalid access key") } tUTCNow := time.Now().UTC() token := jwtgo.NewWithClaims(jwtgo.SigningMethodHS512, jwtgo.MapClaims{ // Token expires in 10hrs. - "exp": tUTCNow.Add(time.Hour * tokenExpires).Unix(), + "exp": tUTCNow.Add(defaultTokenExpiry).Unix(), "iat": tUTCNow.Unix(), "sub": accessKey, }) @@ -80,20 +83,20 @@ func (jwt *JWT) Authenticate(accessKey, secretKey string) error { accessKey = strings.TrimSpace(accessKey) if !isValidAccessKey.MatchString(accessKey) { - return fmt.Errorf("Invalid access key") + return errors.New("Invalid access key") } if !isValidSecretKey.MatchString(secretKey) { - return fmt.Errorf("Invalid secret key") + return errors.New("Invalid secret key") } if accessKey != jwt.AccessKeyID { - return fmt.Errorf("Access key does not match") + return errors.New("Access key does not match") } hashedSecretKey, _ := bcrypt.GenerateFromPassword([]byte(jwt.SecretAccessKey), bcrypt.DefaultCost) if bcrypt.CompareHashAndPassword(hashedSecretKey, []byte(secretKey)) != nil { - return fmt.Errorf("Authentication failed") + return errors.New("Authentication failed") } // Success. diff --git a/cmd/signature-jwt_test.go b/cmd/signature-jwt_test.go index 8137dad3c..0405f7435 100644 --- a/cmd/signature-jwt_test.go +++ b/cmd/signature-jwt_test.go @@ -72,11 +72,11 @@ func TestNewJWT(t *testing.T) { expectedErr error }{ // Test non-existent config directory. - {path.Join(path1, "non-existent-dir"), false, nil, fmt.Errorf("server not initialzed")}, + {path.Join(path1, "non-existent-dir"), false, nil, fmt.Errorf("Server not initialzed")}, // Test empty config directory. - {path2, false, nil, fmt.Errorf("server not initialzed")}, + {path2, false, nil, fmt.Errorf("Server not initialzed")}, // Test empty config file. - {path3, false, nil, fmt.Errorf("server not initialzed")}, + {path3, false, nil, fmt.Errorf("Server not initialzed")}, // Test initialized config file. {path4, true, nil, nil}, // Test to read already created config file. @@ -108,7 +108,7 @@ func TestNewJWT(t *testing.T) { serverConfig.SetCredential(*testCase.cred) } - _, err := newJWT() + _, err := newJWT(defaultWebTokenExpiry) if testCase.expectedErr != nil { if err == nil { @@ -132,7 +132,7 @@ func TestGenerateToken(t *testing.T) { } defer removeAll(testPath) - jwt, err := newJWT() + jwt, err := newJWT(defaultWebTokenExpiry) if err != nil { t.Fatalf("unable get new JWT, %s", err) } @@ -179,7 +179,7 @@ func TestAuthenticate(t *testing.T) { } defer removeAll(testPath) - jwt, err := newJWT() + jwt, err := newJWT(defaultWebTokenExpiry) if err != nil { t.Fatalf("unable get new JWT, %s", err) } diff --git a/cmd/storage-rpc-client.go b/cmd/storage-rpc-client.go index 5e3469228..8bbf503c0 100644 --- a/cmd/storage-rpc-client.go +++ b/cmd/storage-rpc-client.go @@ -17,20 +17,19 @@ package main import ( - "net/http" + "errors" "net/rpc" "path" "strconv" "strings" - "time" ) type networkStorage struct { - netScheme string - netAddr string - netPath string - rpcClient *rpc.Client - httpClient *http.Client + netScheme string + netAddr string + netPath string + rpcClient *rpc.Client + rpcToken string } const ( @@ -70,6 +69,25 @@ func toStorageErr(err error) error { return err } +// Login rpc client makes an authentication request to the rpc server. +// Receives a session token which will be used for subsequent requests. +// FIXME: Currently these tokens expire in 100yrs. +func loginRPCClient(rpcClient *rpc.Client) (tokenStr string, err error) { + cred := serverConfig.GetCredential() + reply := RPCLoginReply{} + if err = rpcClient.Call("Storage.LoginHandler", RPCLoginArgs{ + Username: cred.AccessKeyID, + Password: cred.SecretAccessKey, + }, &reply); err != nil { + return "", err + } + if reply.ServerVersion != minioVersion { + return "", errors.New("Server version mismatch") + } + // Reply back server provided token. + return reply.Token, nil +} + // Initialize new rpc client. func newRPCClient(networkPath string) (StorageAPI, error) { // Input validation. @@ -80,15 +98,6 @@ func newRPCClient(networkPath string) (StorageAPI, error) { // TODO validate netAddr and netPath. netAddr, netPath := splitNetPath(networkPath) - // Initialize http client. - httpClient := &http.Client{ - // Setting a sensible time out of 6minutes to wait for - // response headers. Request is pro-actively cancelled - // after 6minutes if no response was received from server. - Timeout: 6 * time.Minute, - Transport: http.DefaultTransport, - } - // Dial minio rpc storage http path. rpcPath := path.Join(storageRPCPath, netPath) port := getPort(srvConfig.serverAddr) @@ -98,13 +107,18 @@ func newRPCClient(networkPath string) (StorageAPI, error) { return nil, err } + token, err := loginRPCClient(rpcClient) + if err != nil { + return nil, err + } + // Initialize network storage. ndisk := &networkStorage{ - netScheme: "http", // TODO: fix for ssl rpc support. - netAddr: netAddr, - netPath: netPath, - rpcClient: rpcClient, - httpClient: httpClient, + netScheme: "http", // TODO: fix for ssl rpc support. + netAddr: netAddr, + netPath: netPath, + rpcClient: rpcClient, + rpcToken: token, } // Returns successfully here. @@ -117,7 +131,8 @@ func (n networkStorage) MakeVol(volume string) error { return errVolumeBusy } reply := GenericReply{} - if err := n.rpcClient.Call("Storage.MakeVolHandler", volume, &reply); err != nil { + args := GenericVolArgs{n.rpcToken, volume} + if err := n.rpcClient.Call("Storage.MakeVolHandler", args, &reply); err != nil { return toStorageErr(err) } return nil @@ -129,7 +144,7 @@ func (n networkStorage) ListVols() (vols []VolInfo, err error) { return nil, errVolumeBusy } ListVols := ListVolsReply{} - err = n.rpcClient.Call("Storage.ListVolsHandler", "", &ListVols) + err = n.rpcClient.Call("Storage.ListVolsHandler", n.rpcToken, &ListVols) if err != nil { return nil, err } diff --git a/cmd/storage-rpc-server-datatypes.go b/cmd/storage-rpc-server-datatypes.go index 85b501901..ec33935ff 100644 --- a/cmd/storage-rpc-server-datatypes.go +++ b/cmd/storage-rpc-server-datatypes.go @@ -16,12 +16,34 @@ package main +// RPCLoginArgs - login username and password for RPC. +type RPCLoginArgs struct { + Username string + Password string +} + +// RPCLoginReply - login reply provides generated token to be used +// with subsequent requests. +type RPCLoginReply struct { + Token string + ServerVersion string +} + // GenericReply represents any generic RPC reply. type GenericReply struct{} // GenericArgs represents any generic RPC arguments. type GenericArgs struct{} +// GenericVolArgs - generic volume args. +type GenericVolArgs struct { + // Authentication token generated by Login. + Token string + + // Name of the volume. + Vol string +} + // ListVolsReply represents list of vols RPC reply. type ListVolsReply struct { // List of volumes stat information. @@ -30,6 +52,9 @@ type ListVolsReply struct { // ReadAllArgs represents read all RPC arguments. type ReadAllArgs struct { + // Authentication token generated by Login. + Token string + // Name of the volume. Vol string @@ -39,6 +64,9 @@ type ReadAllArgs struct { // ReadFileArgs represents read file RPC arguments. type ReadFileArgs struct { + // Authentication token generated by Login. + Token string + // Name of the volume. Vol string @@ -54,6 +82,9 @@ type ReadFileArgs struct { // AppendFileArgs represents append file RPC arguments. type AppendFileArgs struct { + // Authentication token generated by Login. + Token string + // Name of the volume. Vol string @@ -66,6 +97,9 @@ type AppendFileArgs struct { // StatFileArgs represents stat file RPC arguments. type StatFileArgs struct { + // Authentication token generated by Login. + Token string + // Name of the volume. Vol string @@ -75,6 +109,9 @@ type StatFileArgs struct { // DeleteFileArgs represents delete file RPC arguments. type DeleteFileArgs struct { + // Authentication token generated by Login. + Token string + // Name of the volume. Vol string @@ -84,6 +121,9 @@ type DeleteFileArgs struct { // ListDirArgs represents list contents RPC arguments. type ListDirArgs struct { + // Authentication token generated by Login. + Token string + // Name of the volume. Vol string @@ -93,6 +133,9 @@ type ListDirArgs struct { // RenameFileArgs represents rename file RPC arguments. type RenameFileArgs struct { + // Authentication token generated by Login. + Token string + // Name of source volume. SrcVol string diff --git a/cmd/storage-rpc-server.go b/cmd/storage-rpc-server.go index 5380cafe7..adbd0172c 100644 --- a/cmd/storage-rpc-server.go +++ b/cmd/storage-rpc-server.go @@ -17,10 +17,13 @@ package main import ( + "errors" + "fmt" "net/rpc" "path" "strings" + jwtgo "github.com/dgrijalva/jwt-go" router "github.com/gorilla/mux" ) @@ -31,15 +34,62 @@ type storageServer struct { path string } +// Validates if incoming token is valid. +func isRPCTokenValid(tokenStr string) bool { + jwt, err := newJWT(defaultWebTokenExpiry) // Expiry set to 24Hrs. + if err != nil { + errorIf(err, "Unable to initialize JWT") + return false + } + token, err := jwtgo.Parse(tokenStr, func(token *jwtgo.Token) (interface{}, error) { + if _, ok := token.Method.(*jwtgo.SigningMethodHMAC); !ok { + return nil, fmt.Errorf("Unexpected signing method: %v", token.Header["alg"]) + } + return []byte(jwt.SecretAccessKey), nil + }) + if err != nil { + errorIf(err, "Unable to parse JWT token string") + return false + } + // Return if token is valid. + return token.Valid +} + +/// Auth operations + +// Login - login handler. +func (s *storageServer) LoginHandler(args *RPCLoginArgs, reply *RPCLoginReply) error { + jwt, err := newJWT(defaultTokenExpiry) + if err != nil { + return err + } + if err = jwt.Authenticate(args.Username, args.Password); err != nil { + return err + } + token, err := jwt.GenerateToken(args.Username) + if err != nil { + return err + } + reply.Token = token + reply.ServerVersion = minioVersion + return nil +} + /// Volume operations handlers // MakeVolHandler - make vol handler is rpc wrapper for MakeVol operation. -func (s *storageServer) MakeVolHandler(arg *string, reply *GenericReply) error { - return s.storage.MakeVol(*arg) +func (s *storageServer) MakeVolHandler(args *GenericVolArgs, reply *GenericReply) error { + if !isRPCTokenValid(args.Token) { + return errors.New("Invalid token") + } + return s.storage.MakeVol(args.Vol) } // ListVolsHandler - list vols handler is rpc wrapper for ListVols operation. -func (s *storageServer) ListVolsHandler(arg *string, reply *ListVolsReply) error { +func (s *storageServer) ListVolsHandler(token *string, reply *ListVolsReply) error { + if !isRPCTokenValid(*token) { + return errors.New("Invalid token") + } vols, err := s.storage.ListVols() if err != nil { return err @@ -49,8 +99,11 @@ func (s *storageServer) ListVolsHandler(arg *string, reply *ListVolsReply) error } // StatVolHandler - stat vol handler is a rpc wrapper for StatVol operation. -func (s *storageServer) StatVolHandler(arg *string, reply *VolInfo) error { - volInfo, err := s.storage.StatVol(*arg) +func (s *storageServer) StatVolHandler(args *GenericVolArgs, reply *VolInfo) error { + if !isRPCTokenValid(args.Token) { + return errors.New("Invalid token") + } + volInfo, err := s.storage.StatVol(args.Vol) if err != nil { return err } @@ -60,15 +113,21 @@ func (s *storageServer) StatVolHandler(arg *string, reply *VolInfo) error { // DeleteVolHandler - delete vol handler is a rpc wrapper for // DeleteVol operation. -func (s *storageServer) DeleteVolHandler(arg *string, reply *GenericReply) error { - return s.storage.DeleteVol(*arg) +func (s *storageServer) DeleteVolHandler(args *GenericVolArgs, reply *GenericReply) error { + if !isRPCTokenValid(args.Token) { + return errors.New("Invalid token") + } + return s.storage.DeleteVol(args.Vol) } /// File operations // StatFileHandler - stat file handler is rpc wrapper to stat file. -func (s *storageServer) StatFileHandler(arg *StatFileArgs, reply *FileInfo) error { - fileInfo, err := s.storage.StatFile(arg.Vol, arg.Path) +func (s *storageServer) StatFileHandler(args *StatFileArgs, reply *FileInfo) error { + if !isRPCTokenValid(args.Token) { + return errors.New("Invalid token") + } + fileInfo, err := s.storage.StatFile(args.Vol, args.Path) if err != nil { return err } @@ -77,8 +136,11 @@ func (s *storageServer) StatFileHandler(arg *StatFileArgs, reply *FileInfo) erro } // ListDirHandler - list directory handler is rpc wrapper to list dir. -func (s *storageServer) ListDirHandler(arg *ListDirArgs, reply *[]string) error { - entries, err := s.storage.ListDir(arg.Vol, arg.Path) +func (s *storageServer) ListDirHandler(args *ListDirArgs, reply *[]string) error { + if !isRPCTokenValid(args.Token) { + return errors.New("Invalid token") + } + entries, err := s.storage.ListDir(args.Vol, args.Path) if err != nil { return err } @@ -87,8 +149,11 @@ func (s *storageServer) ListDirHandler(arg *ListDirArgs, reply *[]string) error } // ReadAllHandler - read all handler is rpc wrapper to read all storage API. -func (s *storageServer) ReadAllHandler(arg *ReadFileArgs, reply *[]byte) error { - buf, err := s.storage.ReadAll(arg.Vol, arg.Path) +func (s *storageServer) ReadAllHandler(args *ReadFileArgs, reply *[]byte) error { + if !isRPCTokenValid(args.Token) { + return errors.New("Invalid token") + } + buf, err := s.storage.ReadAll(args.Vol, args.Path) if err != nil { return err } @@ -97,8 +162,11 @@ func (s *storageServer) ReadAllHandler(arg *ReadFileArgs, reply *[]byte) error { } // ReadFileHandler - read file handler is rpc wrapper to read file. -func (s *storageServer) ReadFileHandler(arg *ReadFileArgs, reply *int64) error { - n, err := s.storage.ReadFile(arg.Vol, arg.Path, arg.Offset, arg.Buffer) +func (s *storageServer) ReadFileHandler(args *ReadFileArgs, reply *int64) error { + if !isRPCTokenValid(args.Token) { + return errors.New("Invalid token") + } + n, err := s.storage.ReadFile(args.Vol, args.Path, args.Offset, args.Buffer) if err != nil { return err } @@ -107,18 +175,27 @@ func (s *storageServer) ReadFileHandler(arg *ReadFileArgs, reply *int64) error { } // AppendFileHandler - append file handler is rpc wrapper to append file. -func (s *storageServer) AppendFileHandler(arg *AppendFileArgs, reply *GenericReply) error { - return s.storage.AppendFile(arg.Vol, arg.Path, arg.Buffer) +func (s *storageServer) AppendFileHandler(args *AppendFileArgs, reply *GenericReply) error { + if !isRPCTokenValid(args.Token) { + return errors.New("Invalid token") + } + return s.storage.AppendFile(args.Vol, args.Path, args.Buffer) } // DeleteFileHandler - delete file handler is rpc wrapper to delete file. -func (s *storageServer) DeleteFileHandler(arg *DeleteFileArgs, reply *GenericReply) error { - return s.storage.DeleteFile(arg.Vol, arg.Path) +func (s *storageServer) DeleteFileHandler(args *DeleteFileArgs, reply *GenericReply) error { + if !isRPCTokenValid(args.Token) { + return errors.New("Invalid token") + } + return s.storage.DeleteFile(args.Vol, args.Path) } // RenameFileHandler - rename file handler is rpc wrapper to rename file. -func (s *storageServer) RenameFileHandler(arg *RenameFileArgs, reply *GenericReply) error { - return s.storage.RenameFile(arg.SrcVol, arg.SrcPath, arg.DstVol, arg.DstPath) +func (s *storageServer) RenameFileHandler(args *RenameFileArgs, reply *GenericReply) error { + if !isRPCTokenValid(args.Token) { + return errors.New("Invalid token") + } + return s.storage.RenameFile(args.SrcVol, args.SrcPath, args.DstVol, args.DstPath) } // Initialize new storage rpc. diff --git a/cmd/web-handlers.go b/cmd/web-handlers.go index 8456636d6..3bebfc157 100644 --- a/cmd/web-handlers.go +++ b/cmd/web-handlers.go @@ -41,7 +41,7 @@ import ( // isJWTReqAuthenticated validates if any incoming request to be a // valid JWT authenticated request. func isJWTReqAuthenticated(req *http.Request) bool { - jwt, err := newJWT() + jwt, err := newJWT(defaultWebTokenExpiry) if err != nil { errorIf(err, "unable to initialize a new JWT") return false @@ -289,9 +289,14 @@ type LoginRep struct { UIVersion string `json:"uiVersion"` } +// Default JWT for minio browser expires in 24hrs. +const ( + defaultWebTokenExpiry time.Duration = time.Hour * 24 // 24Hrs. +) + // Login - user login handler. func (web *webAPIHandlers) Login(r *http.Request, args *LoginArgs, reply *LoginRep) error { - jwt, err := newJWT() + jwt, err := newJWT(defaultWebTokenExpiry) if err != nil { return &json2.Error{Message: err.Error()} } @@ -356,7 +361,7 @@ func (web *webAPIHandlers) SetAuth(r *http.Request, args *SetAuthArgs, reply *Se return &json2.Error{Message: err.Error()} } - jwt, err := newJWT() + jwt, err := newJWT(defaultWebTokenExpiry) // JWT Expiry set to 24Hrs. if err != nil { return &json2.Error{Message: err.Error()} } @@ -442,7 +447,7 @@ func (web *webAPIHandlers) Download(w http.ResponseWriter, r *http.Request) { object := vars["object"] tokenStr := r.URL.Query().Get("token") - jwt, err := newJWT() + jwt, err := newJWT(defaultWebTokenExpiry) // Expiry set to 24Hrs. if err != nil { errorIf(err, "error in getting new JWT") return