diff --git a/appveyor.yml b/appveyor.yml index 66afd4cb2..7488c103a 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -35,8 +35,8 @@ test_script: # Unit tests - ps: Add-AppveyorTest "Unit Tests" -Outcome Running - mkdir build\coverage - - go test -race github.com/minio/minio/cmd... - - go test -race github.com/minio/minio/pkg... + - go test -timeout 15m -v -race github.com/minio/minio/cmd... + - go test -v -race github.com/minio/minio/pkg... - go test -coverprofile=build\coverage\coverage.txt -covermode=atomic github.com/minio/minio/cmd - ps: Update-AppveyorTest "Unit Tests" -Outcome Passed diff --git a/cmd/admin-handlers.go b/cmd/admin-handlers.go index cebfcd3b0..8d93f605d 100644 --- a/cmd/admin-handlers.go +++ b/cmd/admin-handlers.go @@ -18,6 +18,8 @@ package cmd import ( "encoding/json" + "encoding/xml" + "io/ioutil" "net/http" "net/url" "strconv" @@ -84,6 +86,76 @@ func (adminAPI adminAPIHandlers) ServiceRestartHandler(w http.ResponseWriter, r sendServiceCmd(globalAdminPeers, serviceRestart) } +// setCredsReq request +type setCredsReq struct { + Username string `xml:"username"` + Password string `xml:"password"` +} + +// ServiceCredsHandler - POST /?service +// HTTP header x-minio-operation: creds +// ---------- +// Update credentials in a minio server. In a distributed setup, update all the servers +// in the cluster. +func (adminAPI adminAPIHandlers) ServiceCredentialsHandler(w http.ResponseWriter, r *http.Request) { + // Authenticate request + adminAPIErr := checkRequestAuthType(r, "", "", "") + if adminAPIErr != ErrNone { + writeErrorResponse(w, adminAPIErr, r.URL) + return + } + + // Avoid setting new credentials when they are already passed + // by the environnement + if globalEnvAccessKey != "" || globalEnvSecretKey != "" { + writeErrorResponse(w, ErrMethodNotAllowed, r.URL) + return + } + + // Load request body + inputData, err := ioutil.ReadAll(r.Body) + if err != nil { + writeErrorResponse(w, ErrInternalError, r.URL) + return + } + + // Unmarshal request body + var req setCredsReq + err = xml.Unmarshal(inputData, &req) + if err != nil { + errorIf(err, "Cannot unmarshal credentials request") + writeErrorResponse(w, ErrMalformedXML, r.URL) + return + } + + // Check passed credentials + cred, err := getCredential(req.Username, req.Password) + switch err { + case errInvalidAccessKeyLength: + writeErrorResponse(w, ErrAdminInvalidAccessKey, r.URL) + return + case errInvalidSecretKeyLength: + writeErrorResponse(w, ErrAdminInvalidSecretKey, r.URL) + return + } + + // Notify all other Minio peers to update credentials + updateErrs := updateCredsOnPeers(cred) + for peer, err := range updateErrs { + errorIf(err, "Unable to update credentials on peer %s.", peer) + } + + // Update local credentials + serverConfig.SetCredential(cred) + if err = serverConfig.Save(); err != nil { + writeErrorResponse(w, ErrInternalError, r.URL) + return + } + + // At this stage, the operation is successful, return 200 OK + w.WriteHeader(http.StatusOK) +} + // validateLockQueryParams - Validates query params for list/clear locks management APIs. func validateLockQueryParams(vars url.Values) (string, string, time.Duration, APIErrorCode) { bucket := vars.Get(string(mgmtBucket)) diff --git a/cmd/admin-handlers_test.go b/cmd/admin-handlers_test.go index 0f0c39c66..15b620623 100644 --- a/cmd/admin-handlers_test.go +++ b/cmd/admin-handlers_test.go @@ -19,6 +19,8 @@ package cmd import ( "bytes" "encoding/json" + "encoding/xml" + "io/ioutil" "net/http" "net/http/httptest" "net/url" @@ -33,8 +35,8 @@ type cmdType int const ( statusCmd cmdType = iota - stopCmd restartCmd + setCreds ) // String - String representation for cmdType @@ -42,10 +44,10 @@ func (c cmdType) String() string { switch c { case statusCmd: return "status" - case stopCmd: - return "stop" case restartCmd: return "restart" + case setCreds: + return "set-credentials" } return "" } @@ -58,6 +60,8 @@ func (c cmdType) apiMethod() string { return "GET" case restartCmd: return "POST" + case setCreds: + return "POST" } return "GET" } @@ -86,15 +90,19 @@ func testServiceSignalReceiver(cmd cmdType, t *testing.T) { // getServiceCmdRequest - Constructs a management REST API request for service // subcommands for a given cmdType value. -func getServiceCmdRequest(cmd cmdType, cred credential) (*http.Request, error) { +func getServiceCmdRequest(cmd cmdType, cred credential, body []byte) (*http.Request, error) { req, err := newTestRequest(cmd.apiMethod(), "/?service", 0, nil) if err != nil { return nil, err } + // Set body + req.Body = ioutil.NopCloser(bytes.NewReader(body)) + // minioAdminOpHeader is to identify the request as a // management REST API request. req.Header.Set(minioAdminOpHeader, cmd.String()) + req.Header.Set("X-Amz-Content-Sha256", getSHA256Hash(body)) // management REST API uses signature V4 for authentication. err = signRequestV4(req, cred.AccessKey, cred.SecretKey) @@ -106,7 +114,7 @@ func getServiceCmdRequest(cmd cmdType, cred credential) (*http.Request, error) { // testServicesCmdHandler - parametrizes service subcommand tests on // cmdType value. -func testServicesCmdHandler(cmd cmdType, t *testing.T) { +func testServicesCmdHandler(cmd cmdType, args map[string]interface{}, t *testing.T) { // reset globals. // this is to make sure that the tests are not affected by modified value. resetTestGlobals() @@ -147,19 +155,25 @@ func testServicesCmdHandler(cmd cmdType, t *testing.T) { // Setting up a go routine to simulate ServerMux's // handleServiceSignals for stop and restart commands. - switch cmd { - case stopCmd, restartCmd: + if cmd == restartCmd { go testServiceSignalReceiver(cmd, t) } credentials := serverConfig.GetCredential() adminRouter := router.NewRouter() registerAdminRouter(adminRouter) - rec := httptest.NewRecorder() - req, err := getServiceCmdRequest(cmd, credentials) + var body []byte + + if cmd == setCreds { + body, _ = xml.Marshal(setCredsReq{Username: args["username"].(string), Password: args["password"].(string)}) + } + + req, err := getServiceCmdRequest(cmd, credentials, body) if err != nil { t.Fatalf("Failed to build service status request %v", err) } + + rec := httptest.NewRecorder() adminRouter.ServeHTTP(rec, req) if cmd == statusCmd { @@ -173,20 +187,37 @@ func testServicesCmdHandler(cmd cmdType, t *testing.T) { } } + if cmd == setCreds { + // Check if new credentials are set + cred := serverConfig.GetCredential() + if cred.AccessKey != args["username"].(string) { + t.Errorf("Wrong access key, expected = %s, found = %s", args["username"].(string), cred.AccessKey) + } + if cred.SecretKey != args["password"].(string) { + t.Errorf("Wrong secret key, expected = %s, found = %s", args["password"].(string), cred.SecretKey) + } + + } + if rec.Code != http.StatusOK { - t.Errorf("Expected to receive %d status code but received %d", - http.StatusOK, rec.Code) + resp, _ := ioutil.ReadAll(rec.Body) + t.Errorf("Expected to receive %d status code but received %d. Body (%s)", + http.StatusOK, rec.Code, string(resp)) } } // Test for service status management REST API. func TestServiceStatusHandler(t *testing.T) { - testServicesCmdHandler(statusCmd, t) + testServicesCmdHandler(statusCmd, nil, t) } // Test for service restart management REST API. func TestServiceRestartHandler(t *testing.T) { - testServicesCmdHandler(restartCmd, t) + testServicesCmdHandler(restartCmd, nil, t) +} + +func TestServiceSetCreds(t *testing.T) { + testServicesCmdHandler(setCreds, map[string]interface{}{"username": "minio", "password": "minio123"}, t) } // mkLockQueryVal - helper function to build lock query param. diff --git a/cmd/admin-router.go b/cmd/admin-router.go index dde7faea8..3c5f63a96 100644 --- a/cmd/admin-router.go +++ b/cmd/admin-router.go @@ -36,6 +36,8 @@ func registerAdminRouter(mux *router.Router) { // Service restart adminRouter.Methods("POST").Queries("service", "").Headers(minioAdminOpHeader, "restart").HandlerFunc(adminAPI.ServiceRestartHandler) + // Service update credentials + adminRouter.Methods("POST").Queries("service", "").Headers(minioAdminOpHeader, "set-credentials").HandlerFunc(adminAPI.ServiceCredentialsHandler) /// Lock operations diff --git a/cmd/api-errors.go b/cmd/api-errors.go index a23a7e7d6..99b18ccd6 100644 --- a/cmd/api-errors.go +++ b/cmd/api-errors.go @@ -140,6 +140,9 @@ const ( // Add new extended error codes here. // Please open a https://github.com/minio/minio/issues before adding // new error codes here. + + ErrAdminInvalidAccessKey + ErrAdminInvalidSecretKey ) // error code to APIError structure, these fields carry respective @@ -574,6 +577,17 @@ var errorCodeResponse = map[APIErrorCode]APIError{ Description: "Server not initialized, please try again.", HTTPStatusCode: http.StatusServiceUnavailable, }, + ErrAdminInvalidAccessKey: { + Code: "XMinioAdminInvalidAccessKey", + Description: "The access key is invalid.", + HTTPStatusCode: http.StatusBadRequest, + }, + ErrAdminInvalidSecretKey: { + Code: "XMinioAdminInvalidSecretKey", + Description: "The secret key is invalid.", + HTTPStatusCode: http.StatusBadRequest, + }, + // Add your error structure here. } diff --git a/cmd/globals.go b/cmd/globals.go index bae69452a..ce3a0a983 100644 --- a/cmd/globals.go +++ b/cmd/globals.go @@ -101,6 +101,12 @@ var ( // Minio server user agent string. globalServerUserAgent = "Minio/" + ReleaseTag + " (" + runtime.GOOS + "; " + runtime.GOARCH + ")" + // Access key passed from the environment + globalEnvAccessKey = os.Getenv("MINIO_ACCESS_KEY") + + // Secret key passed from the environment + globalEnvSecretKey = os.Getenv("MINIO_SECRET_KEY") + // Add new variable global values here. ) diff --git a/cmd/main.go b/cmd/main.go index bb2a47a71..db214201e 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -190,13 +190,11 @@ func minioInit(ctx *cli.Context) { enableLoggers() // Fetch access keys from environment variables and update the config. - accessKey := os.Getenv("MINIO_ACCESS_KEY") - secretKey := os.Getenv("MINIO_SECRET_KEY") - if accessKey != "" && secretKey != "" { + if globalEnvAccessKey != "" && globalEnvSecretKey != "" { // Set new credentials. serverConfig.SetCredential(credential{ - AccessKey: accessKey, - SecretKey: secretKey, + AccessKey: globalEnvAccessKey, + SecretKey: globalEnvSecretKey, }) } if !isAccessKeyValid(serverConfig.GetCredential().AccessKey) { diff --git a/docs/admin-api/management-api.md b/docs/admin-api/management-api.md index 2655c1698..b8ed45489 100644 --- a/docs/admin-api/management-api.md +++ b/docs/admin-api/management-api.md @@ -6,9 +6,9 @@ ## List of management APIs - Service - - Stop - Restart - Status + - SetCredentials - Locks - List @@ -17,11 +17,6 @@ - Healing ### Service Management APIs -* Stop - - POST /?service - - x-minio-operation: stop - - Response: On success 200 - * Restart - POST /?service - x-minio-operation: restart @@ -32,6 +27,43 @@ - x-minio-operation: status - Response: On success 200, return json formatted StorageInfo object. +* SetCredentials + - GET /?service + - x-minio-operation: set-credentials + - Response: Success 200 + - Possible error responses + - ErrMethodNotAllowed + + MethodNotAllowed + The specified method is not allowed against this resource. + + + / + 3L137 + 3L137 + + - ErrAdminBadCred + + XMinioBadCred + XMinioBadCred + + + / + 3L137 + 3L137 + + - ErrInternalError + + InternalError + We encountered an internal error, please try again. + + + / + 3L137 + 3L137 + + + ### Lock Management APIs * ListLocks - GET /?lock&bucket=mybucket&prefix=myprefix&older-than=rel_time diff --git a/pkg/madmin/README.md b/pkg/madmin/README.md index 73aba84a3..e2d16f1f8 100644 --- a/pkg/madmin/README.md +++ b/pkg/madmin/README.md @@ -106,6 +106,7 @@ go run service-status.go * [`ServiceStatus`](./API.md#ServiceStatus) * [`ServiceRestart`](./API.md#ServiceRestart) +* [`ServiceSetCredentials`](./API.md#ServiceSetCredentials) ## Full Examples @@ -113,6 +114,7 @@ go run service-status.go * [service-status.go](https://github.com/minio/minio/blob/master/pkg/madmin/examples/service-status.go) * [service-restart.go](https://github.com/minio/minio/blob/master/pkg/madmin/examples/service-restart.go) +* [service-set-credentials.go](https://github.com/minio/minio/blob/master/pkg/madmin/examples/service-set-credentials.go) ## Contribute diff --git a/pkg/madmin/api-error-response.go b/pkg/madmin/api-error-response.go index 49abf7e70..d5a14f656 100644 --- a/pkg/madmin/api-error-response.go +++ b/pkg/madmin/api-error-response.go @@ -16,7 +16,10 @@ package madmin -import "encoding/xml" +import ( + "encoding/xml" + "net/http" +) /* **** SAMPLE ERROR RESPONSE **** @@ -50,6 +53,29 @@ func (e ErrorResponse) Error() string { return e.Message } +const ( + reportIssue = "Please report this issue at https://github.com/minio/minio-go/issues." +) + +// httpRespToErrorResponse returns a new encoded ErrorResponse +// structure as error. +func httpRespToErrorResponse(resp *http.Response) error { + if resp == nil { + msg := "Response is empty. " + reportIssue + return ErrInvalidArgument(msg) + } + var errResp ErrorResponse + // Decode the xml error + err := xmlDecoder(resp.Body, &errResp) + if err != nil { + return ErrorResponse{ + Code: resp.Status, + Message: "Failed to parse server response.", + } + } + return errResp +} + // ErrInvalidArgument - Invalid argument response. func ErrInvalidArgument(message string) error { return ErrorResponse{ diff --git a/pkg/madmin/examples/service-set-credentials.go b/pkg/madmin/examples/service-set-credentials.go new file mode 100644 index 000000000..81f0dc203 --- /dev/null +++ b/pkg/madmin/examples/service-set-credentials.go @@ -0,0 +1,44 @@ +// +build ignore + +/* + * Minio Cloud Storage, (C) 2016 Minio, Inc. + * + * 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 main + +import ( + "log" + + "github.com/minio/minio/pkg/madmin" +) + +func main() { + // Note: YOUR-ACCESSKEYID, YOUR-SECRETACCESSKEY and my-bucketname are + // dummy values, please replace them with original values. + + // API requests are secure (HTTPS) if secure=true and insecure (HTTPS) otherwise. + // New returns an Minio Admin client object. + madmClnt, err := madmin.New("your-minio.example.com:9000", "YOUR-ACCESSKEYID", "YOUR-SECRETACCESSKEY", true) + if err != nil { + log.Fatalln(err) + } + + err = madmClnt.ServiceSetCredentials("YOUR-NEW-ACCESSKEY", "YOUR-NEW-SECRETKEY") + if err != nil { + log.Fatalln(err) + } + log.Println("New credentials successfully set.") +} diff --git a/pkg/madmin/service.go b/pkg/madmin/service.go index d49213037..442ab3ffb 100644 --- a/pkg/madmin/service.go +++ b/pkg/madmin/service.go @@ -18,7 +18,9 @@ package madmin import ( + "bytes" "encoding/json" + "encoding/xml" "errors" "io/ioutil" "net/http" @@ -117,3 +119,49 @@ func (adm *AdminClient) ServiceRestart() error { } return nil } + +// setCredsReq - xml to send to the server to set new credentials +type setCredsReq struct { + Username string `xml:"username"` + Password string `xml:"password"` +} + +// ServiceSetCredentials - Call Service Set Credentials API to set new access and secret keys in the specified Minio server +func (adm *AdminClient) ServiceSetCredentials(access, secret string) error { + + // Disallow sending with the server if the connection is not secure + if !adm.secure { + return errors.New("setting new credentials requires HTTPS connection to the server") + } + + // Setup new request + reqData := requestData{} + reqData.queryValues = make(url.Values) + reqData.queryValues.Set("service", "") + reqData.customHeaders = make(http.Header) + reqData.customHeaders.Set(minioAdminOpHeader, "set-credentials") + + // Setup request's body + body, err := xml.Marshal(setCredsReq{Username: access, Password: secret}) + if err != nil { + return err + } + reqData.contentBody = bytes.NewReader(body) + reqData.contentLength = int64(len(body)) + reqData.contentMD5Bytes = sumMD5(body) + reqData.contentSHA256Bytes = sum256(body) + + // Execute GET on bucket to list objects. + resp, err := adm.executeMethod("POST", reqData) + + defer closeResponse(resp) + if err != nil { + return err + } + + // Return error to the caller if http response code is different from 200 + if resp.StatusCode != http.StatusOK { + return httpRespToErrorResponse(resp) + } + return nil +} diff --git a/pkg/madmin/utils.go b/pkg/madmin/utils.go index 1661c2f8c..34aa80618 100644 --- a/pkg/madmin/utils.go +++ b/pkg/madmin/utils.go @@ -17,7 +17,9 @@ package madmin import ( + "crypto/md5" "crypto/sha256" + "encoding/xml" "io" "io/ioutil" "net" @@ -35,6 +37,19 @@ func sum256(data []byte) []byte { return hash.Sum(nil) } +// sumMD5 calculate sumMD5 sum for an input byte array. +func sumMD5(data []byte) []byte { + hash := md5.New() + hash.Write(data) + return hash.Sum(nil) +} + +// xmlDecoder provide decoded value in xml. +func xmlDecoder(body io.Reader, v interface{}) error { + d := xml.NewDecoder(body) + return d.Decode(v) +} + // getEndpointURL - construct a new endpoint. func getEndpointURL(endpoint string, secure bool) (*url.URL, error) { if strings.Contains(endpoint, ":") {