/* * Minio Cloud Storage, (C) 2016, 2017, 2018 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 cmd import ( "archive/zip" "bytes" "context" "encoding/base64" "encoding/json" "errors" "fmt" "io" "net/http" "os" "strconv" "strings" "sync" "time" "github.com/gorilla/mux" "github.com/minio/minio/cmd/logger" "github.com/minio/minio/pkg/auth" "github.com/minio/minio/pkg/handlers" "github.com/minio/minio/pkg/iam/policy" "github.com/minio/minio/pkg/madmin" "github.com/minio/minio/pkg/quick" "github.com/tidwall/gjson" "github.com/tidwall/sjson" ) const ( maxEConfigJSONSize = 262272 ) // Type-safe query params. type mgmtQueryKey string // Only valid query params for mgmt admin APIs. const ( mgmtBucket mgmtQueryKey = "bucket" mgmtPrefix = "prefix" mgmtClientToken = "clientToken" mgmtForceStart = "forceStart" mgmtForceStop = "forceStop" ) var ( // This struct literal represents the Admin API version that // the server uses. adminAPIVersionInfo = madmin.AdminAPIVersionInfo{ Version: "1", } ) // VersionHandler - GET /minio/admin/version // ----------- // Returns Administration API version func (a adminAPIHandlers) VersionHandler(w http.ResponseWriter, r *http.Request) { adminAPIErr := checkAdminRequestAuthType(r, "") if adminAPIErr != ErrNone { writeErrorResponseJSON(w, adminAPIErr, r.URL) return } jsonBytes, err := json.Marshal(adminAPIVersionInfo) if err != nil { writeErrorResponseJSON(w, ErrInternalError, r.URL) logger.LogIf(context.Background(), err) return } writeSuccessResponseJSON(w, jsonBytes) } // ServiceStatusHandler - GET /minio/admin/v1/service // ---------- // Returns server version and uptime. func (a adminAPIHandlers) ServiceStatusHandler(w http.ResponseWriter, r *http.Request) { adminAPIErr := checkAdminRequestAuthType(r, "") if adminAPIErr != ErrNone { writeErrorResponseJSON(w, adminAPIErr, r.URL) return } // Fetch server version serverVersion := madmin.ServerVersion{ Version: Version, CommitID: CommitID, } // Fetch uptimes from all peers. This may fail to due to lack // of read-quorum availability. uptime, err := getPeerUptimes(globalAdminPeers) if err != nil { writeErrorResponseJSON(w, toAdminAPIErrCode(err), r.URL) logger.LogIf(context.Background(), err) return } // Create API response serverStatus := madmin.ServiceStatus{ ServerVersion: serverVersion, Uptime: uptime, } // Marshal API response jsonBytes, err := json.Marshal(serverStatus) if err != nil { writeErrorResponseJSON(w, ErrInternalError, r.URL) logger.LogIf(context.Background(), err) return } // Reply with storage information (across nodes in a // distributed setup) as json. writeSuccessResponseJSON(w, jsonBytes) } // ServiceStopNRestartHandler - POST /minio/admin/v1/service // Body: {"action": } // ---------- // Restarts/Stops minio server gracefully. In a distributed setup, // restarts all the servers in the cluster. func (a adminAPIHandlers) ServiceStopNRestartHandler(w http.ResponseWriter, r *http.Request) { adminAPIErr := checkAdminRequestAuthType(r, "") if adminAPIErr != ErrNone { writeErrorResponseJSON(w, adminAPIErr, r.URL) return } var sa madmin.ServiceAction err := json.NewDecoder(r.Body).Decode(&sa) if err != nil { logger.LogIf(context.Background(), err) writeErrorResponseJSON(w, ErrRequestBodyParse, r.URL) return } var serviceSig serviceSignal switch sa.Action { case madmin.ServiceActionValueRestart: serviceSig = serviceRestart case madmin.ServiceActionValueStop: serviceSig = serviceStop default: writeErrorResponseJSON(w, ErrMalformedPOSTRequest, r.URL) logger.LogIf(context.Background(), errors.New("Invalid service action received")) return } // Reply to the client before restarting minio server. writeSuccessResponseHeadersOnly(w) sendServiceCmd(globalAdminPeers, serviceSig) } // ServerProperties holds some server information such as, version, region // uptime, etc.. type ServerProperties struct { Uptime time.Duration `json:"uptime"` Version string `json:"version"` CommitID string `json:"commitID"` Region string `json:"region"` SQSARN []string `json:"sqsARN"` } // ServerConnStats holds transferred bytes from/to the server type ServerConnStats struct { TotalInputBytes uint64 `json:"transferred"` TotalOutputBytes uint64 `json:"received"` Throughput uint64 `json:"throughput,omitempty"` } // ServerHTTPMethodStats holds total number of HTTP operations from/to the server, // including the average duration the call was spent. type ServerHTTPMethodStats struct { Count uint64 `json:"count"` AvgDuration string `json:"avgDuration"` } // ServerHTTPStats holds all type of http operations performed to/from the server // including their average execution time. type ServerHTTPStats struct { TotalHEADStats ServerHTTPMethodStats `json:"totalHEADs"` SuccessHEADStats ServerHTTPMethodStats `json:"successHEADs"` TotalGETStats ServerHTTPMethodStats `json:"totalGETs"` SuccessGETStats ServerHTTPMethodStats `json:"successGETs"` TotalPUTStats ServerHTTPMethodStats `json:"totalPUTs"` SuccessPUTStats ServerHTTPMethodStats `json:"successPUTs"` TotalPOSTStats ServerHTTPMethodStats `json:"totalPOSTs"` SuccessPOSTStats ServerHTTPMethodStats `json:"successPOSTs"` TotalDELETEStats ServerHTTPMethodStats `json:"totalDELETEs"` SuccessDELETEStats ServerHTTPMethodStats `json:"successDELETEs"` } // ServerInfoData holds storage, connections and other // information of a given server. type ServerInfoData struct { StorageInfo StorageInfo `json:"storage"` ConnStats ServerConnStats `json:"network"` HTTPStats ServerHTTPStats `json:"http"` Properties ServerProperties `json:"server"` } // ServerInfo holds server information result of one node type ServerInfo struct { Error string `json:"error"` Addr string `json:"addr"` Data *ServerInfoData `json:"data"` } // ServerInfoHandler - GET /minio/admin/v1/info // ---------- // Get server information func (a adminAPIHandlers) ServerInfoHandler(w http.ResponseWriter, r *http.Request) { // Authenticate request // Setting the region as empty so as the mc server info command is irrespective to the region. adminAPIErr := checkAdminRequestAuthType(r, "") if adminAPIErr != ErrNone { writeErrorResponseJSON(w, adminAPIErr, r.URL) return } // Web service response reply := make([]ServerInfo, len(globalAdminPeers)) var wg sync.WaitGroup // Gather server information for all nodes for i, p := range globalAdminPeers { wg.Add(1) // Gather information from a peer in a goroutine go func(idx int, peer adminPeer) { defer wg.Done() // Initialize server info at index reply[idx] = ServerInfo{Addr: peer.addr} serverInfoData, err := peer.cmdRunner.ServerInfo() if err != nil { reqInfo := (&logger.ReqInfo{}).AppendTags("peerAddress", peer.addr) ctx := logger.SetReqInfo(context.Background(), reqInfo) logger.LogIf(ctx, err) reply[idx].Error = err.Error() return } reply[idx].Data = &serverInfoData }(i, p) } wg.Wait() // Marshal API response jsonBytes, err := json.Marshal(reply) if err != nil { writeErrorResponseJSON(w, ErrInternalError, r.URL) logger.LogIf(context.Background(), err) return } // Reply with storage information (across nodes in a // distributed setup) as json. writeSuccessResponseJSON(w, jsonBytes) } // StartProfilingResult contains the status of the starting // profiling action in a given server type StartProfilingResult struct { NodeName string `json:"nodeName"` Success bool `json:"success"` Error string `json:"error"` } // StartProfilingHandler - POST /minio/admin/v1/profiling/start?profilerType={profilerType} // ---------- // Enable server profiling func (a adminAPIHandlers) StartProfilingHandler(w http.ResponseWriter, r *http.Request) { adminAPIErr := checkAdminRequestAuthType(r, "") if adminAPIErr != ErrNone { writeErrorResponseJSON(w, adminAPIErr, r.URL) return } vars := mux.Vars(r) profiler := vars["profilerType"] startProfilingResult := make([]StartProfilingResult, len(globalAdminPeers)) // Call StartProfiling function on all nodes and save results wg := sync.WaitGroup{} for i, peer := range globalAdminPeers { wg.Add(1) go func(idx int, peer adminPeer) { defer wg.Done() result := StartProfilingResult{NodeName: peer.addr} if err := peer.cmdRunner.StartProfiling(profiler); err != nil { result.Error = err.Error() return } result.Success = true startProfilingResult[idx] = result }(i, peer) } wg.Wait() // Create JSON result and send it to the client startProfilingResultInBytes, err := json.Marshal(startProfilingResult) if err != nil { writeCustomErrorResponseJSON(w, http.StatusInternalServerError, err.Error(), r.URL) return } writeSuccessResponseJSON(w, []byte(startProfilingResultInBytes)) } // dummyFileInfo represents a dummy representation of a profile data file // present only in memory, it helps to generate the zip stream. type dummyFileInfo struct { name string size int64 mode os.FileMode modTime time.Time isDir bool sys interface{} } func (f dummyFileInfo) Name() string { return f.name } func (f dummyFileInfo) Size() int64 { return f.size } func (f dummyFileInfo) Mode() os.FileMode { return f.mode } func (f dummyFileInfo) ModTime() time.Time { return f.modTime } func (f dummyFileInfo) IsDir() bool { return f.isDir } func (f dummyFileInfo) Sys() interface{} { return f.sys } // DownloadProfilingHandler - POST /minio/admin/v1/profiling/download // ---------- // Download profiling information of all nodes in a zip format func (a adminAPIHandlers) DownloadProfilingHandler(w http.ResponseWriter, r *http.Request) { adminAPIErr := checkAdminRequestAuthType(r, "") if adminAPIErr != ErrNone { writeErrorResponseJSON(w, adminAPIErr, r.URL) return } profilingDataFound := false // Initialize a zip writer which will provide a zipped content // of profiling data of all nodes zipWriter := zip.NewWriter(w) defer zipWriter.Close() for i, peer := range globalAdminPeers { // Get profiling data from a node data, err := peer.cmdRunner.DownloadProfilingData() if err != nil { logger.LogIf(context.Background(), fmt.Errorf("Unable to download profiling data from node `%s`, reason: %s", peer.addr, err.Error())) continue } profilingDataFound = true // Send profiling data to zip as file header, err := zip.FileInfoHeader(dummyFileInfo{ name: fmt.Sprintf("profiling-%d", i), size: int64(len(data)), mode: 0600, modTime: time.Now().UTC(), isDir: false, sys: nil, }) if err != nil { continue } writer, err := zipWriter.CreateHeader(header) if err != nil { continue } if _, err = io.Copy(writer, bytes.NewBuffer(data)); err != nil { return } } if !profilingDataFound { writeErrorResponseJSON(w, ErrAdminProfilerNotEnabled, r.URL) return } } // extractHealInitParams - Validates params for heal init API. func extractHealInitParams(r *http.Request) (bucket, objPrefix string, hs madmin.HealOpts, clientToken string, forceStart bool, forceStop bool, err APIErrorCode) { vars := mux.Vars(r) bucket = vars[string(mgmtBucket)] objPrefix = vars[string(mgmtPrefix)] if bucket == "" { if objPrefix != "" { // Bucket is required if object-prefix is given err = ErrHealMissingBucket return } } else if !IsValidBucketName(bucket) { err = ErrInvalidBucketName return } // empty prefix is valid. if !IsValidObjectPrefix(objPrefix) { err = ErrInvalidObjectName return } qParms := r.URL.Query() if len(qParms[string(mgmtClientToken)]) > 0 { clientToken = qParms[string(mgmtClientToken)][0] } if _, ok := qParms[string(mgmtForceStart)]; ok { forceStart = true } if _, ok := qParms[string(mgmtForceStop)]; ok { forceStop = true } // ignore body if clientToken is provided if clientToken == "" { jerr := json.NewDecoder(r.Body).Decode(&hs) if jerr != nil { logger.LogIf(context.Background(), jerr) err = ErrRequestBodyParse return } } err = ErrNone return } // HealHandler - POST /minio/admin/v1/heal/ // ----------- // Start heal processing and return heal status items. // // On a successful heal sequence start, a unique client token is // returned. Subsequent requests to this endpoint providing the client // token will receive heal status records from the running heal // sequence. // // If no client token is provided, and a heal sequence is in progress // an error is returned with information about the running heal // sequence. However, if the force-start flag is provided, the server // aborts the running heal sequence and starts a new one. func (a adminAPIHandlers) HealHandler(w http.ResponseWriter, r *http.Request) { ctx := newContext(r, w, "Heal") // Get object layer instance. objLayer := newObjectLayerFn() if objLayer == nil { writeErrorResponseJSON(w, ErrServerNotInitialized, r.URL) return } // Validate request signature. adminAPIErr := checkAdminRequestAuthType(r, "") if adminAPIErr != ErrNone { writeErrorResponseJSON(w, adminAPIErr, r.URL) return } // Check if this setup has an erasure coded backend. if !globalIsXL { writeErrorResponseJSON(w, ErrHealNotImplemented, r.URL) return } bucket, objPrefix, hs, clientToken, forceStart, forceStop, apiErr := extractHealInitParams(r) if apiErr != ErrNone { writeErrorResponseJSON(w, apiErr, r.URL) return } type healResp struct { respBytes []byte errCode APIErrorCode errBody string } // Define a closure to start sending whitespace to client // after 10s unless a response item comes in keepConnLive := func(w http.ResponseWriter, respCh chan healResp) { ticker := time.NewTicker(time.Second * 10) defer ticker.Stop() started := false forLoop: for { select { case <-ticker.C: if !started { // Start writing response to client started = true setCommonHeaders(w) w.Header().Set("Content-Type", string(mimeJSON)) // Set 200 OK status w.WriteHeader(200) } // Send whitespace and keep connection open w.Write([]byte("\n\r")) w.(http.Flusher).Flush() case hr := <-respCh: switch hr.errCode { case ErrNone: if started { w.Write(hr.respBytes) w.(http.Flusher).Flush() } else { writeSuccessResponseJSON(w, hr.respBytes) } default: apiError := getAPIError(hr.errCode) var errorRespJSON []byte if hr.errBody == "" { errorRespJSON = encodeResponseJSON(getAPIErrorResponse(apiError, r.URL.Path, w.Header().Get(responseRequestIDKey))) } else { errorRespJSON = encodeResponseJSON(APIErrorResponse{ Code: apiError.Code, Message: hr.errBody, Resource: r.URL.Path, RequestID: w.Header().Get(responseRequestIDKey), HostID: "3L137", }) } if !started { setCommonHeaders(w) w.Header().Set("Content-Type", string(mimeJSON)) w.WriteHeader(apiError.HTTPStatusCode) } w.Write(errorRespJSON) w.(http.Flusher).Flush() } break forLoop } } } // find number of disks in the setup info := objLayer.StorageInfo(ctx) numDisks := info.Backend.OfflineDisks + info.Backend.OnlineDisks healPath := pathJoin(bucket, objPrefix) if clientToken == "" && !forceStart && !forceStop { nh, exists := globalAllHealState.getHealSequence(healPath) if exists && !nh.hasEnded() && len(nh.currentStatus.Items) > 0 { b, err := json.Marshal(madmin.HealStartSuccess{ ClientToken: nh.clientToken, ClientAddress: nh.clientAddress, StartTime: nh.startTime, }) if err != nil { logger.LogIf(context.Background(), err) writeErrorResponse(w, toAPIErrorCode(err), r.URL) return } // Client token not specified but a heal sequence exists on a path, // Send the token back to client. writeSuccessResponseJSON(w, b) return } } if clientToken != "" && !forceStart && !forceStop { // Since clientToken is given, fetch heal status from running // heal sequence. respBytes, errCode := globalAllHealState.PopHealStatusJSON( healPath, clientToken) if errCode != ErrNone { writeErrorResponseJSON(w, errCode, r.URL) } else { writeSuccessResponseJSON(w, respBytes) } return } respCh := make(chan healResp) switch { case forceStop: go func() { respBytes, errCode := globalAllHealState.stopHealSequence(healPath) hr := healResp{respBytes: respBytes, errCode: errCode} respCh <- hr }() case clientToken == "": nh := newHealSequence(bucket, objPrefix, handlers.GetSourceIP(r), numDisks, hs, forceStart) go func() { respBytes, errCode, errMsg := globalAllHealState.LaunchNewHealSequence(nh) hr := healResp{respBytes, errCode, errMsg} respCh <- hr }() } // Due to the force-starting functionality, the Launch // call above can take a long time - to keep the // connection alive, we start sending whitespace keepConnLive(w, respCh) } // GetConfigHandler - GET /minio/admin/v1/config // Get config.json of this minio setup. func (a adminAPIHandlers) GetConfigHandler(w http.ResponseWriter, r *http.Request) { ctx := newContext(r, w, "GetConfigHandler") // Get current object layer instance. objectAPI := newObjectLayerFn() if objectAPI == nil { writeErrorResponseJSON(w, ErrServerNotInitialized, r.URL) return } // Validate request signature. adminAPIErr := checkAdminRequestAuthType(r, "") if adminAPIErr != ErrNone { writeErrorResponseJSON(w, adminAPIErr, r.URL) return } config, err := readServerConfig(ctx, objectAPI) if err != nil { writeErrorResponseJSON(w, toAdminAPIErrCode(err), r.URL) return } configData, err := json.MarshalIndent(config, "", "\t") if err != nil { logger.LogIf(ctx, err) writeErrorResponseJSON(w, toAdminAPIErrCode(err), r.URL) return } password := config.GetCredential().SecretKey econfigData, err := madmin.EncryptData(password, configData) if err != nil { logger.LogIf(ctx, err) writeErrorResponseJSON(w, toAdminAPIErrCode(err), r.URL) return } writeSuccessResponseJSON(w, econfigData) } // Disable tidwall json array notation in JSON key path so // users can set json with a key as a number. // In tidwall json, notify.webhook.0 = val means { "notify" : { "webhook" : [val] }} // In Minio, notify.webhook.0 = val means { "notify" : { "webhook" : {"0" : val}}} func normalizeJSONKey(input string) (key string) { subKeys := strings.Split(input, ".") for i, k := range subKeys { if i > 0 { key += "." } if _, err := strconv.Atoi(k); err == nil { key += ":" + k } else { key += k } } return } // GetConfigHandler - GET /minio/admin/v1/config-keys // Get some keys in config.json of this minio setup. func (a adminAPIHandlers) GetConfigKeysHandler(w http.ResponseWriter, r *http.Request) { ctx := newContext(r, w, "GetConfigKeysHandler") // Get current object layer instance. objectAPI := newObjectLayerFn() if objectAPI == nil { writeErrorResponseJSON(w, ErrServerNotInitialized, r.URL) return } // Validate request signature. adminAPIErr := checkAdminRequestAuthType(r, "") if adminAPIErr != ErrNone { writeErrorResponseJSON(w, adminAPIErr, r.URL) return } var keys []string queries := r.URL.Query() for k := range queries { keys = append(keys, k) } config, err := readServerConfig(ctx, objectAPI) if err != nil { writeErrorResponseJSON(w, toAdminAPIErrCode(err), r.URL) return } configData, err := json.Marshal(config) if err != nil { logger.LogIf(ctx, err) writeErrorResponseJSON(w, toAdminAPIErrCode(err), r.URL) return } configStr := string(configData) newConfigStr := `{}` for _, key := range keys { // sjson.Set does not return an error if key is empty // we should check by ourselves here if key == "" { continue } val := gjson.Get(configStr, key) if j, ierr := sjson.Set(newConfigStr, normalizeJSONKey(key), val.Value()); ierr == nil { newConfigStr = j } } password := config.GetCredential().SecretKey econfigData, err := madmin.EncryptData(password, []byte(newConfigStr)) if err != nil { logger.LogIf(ctx, err) writeErrorResponseJSON(w, toAdminAPIErrCode(err), r.URL) return } writeSuccessResponseJSON(w, []byte(econfigData)) } // toAdminAPIErrCode - converts errXLWriteQuorum error to admin API // specific error. func toAdminAPIErrCode(err error) APIErrorCode { switch err { case errXLWriteQuorum: return ErrAdminConfigNoQuorum default: return toAPIErrorCode(err) } } // RemoveUser - DELETE /minio/admin/v1/remove-user?accessKey= func (a adminAPIHandlers) RemoveUser(w http.ResponseWriter, r *http.Request) { ctx := newContext(r, w, "RemoveUser") // Get current object layer instance. objectAPI := newObjectLayerFn() if objectAPI == nil { writeErrorResponseJSON(w, ErrServerNotInitialized, r.URL) return } // Validate request signature. adminAPIErr := checkAdminRequestAuthType(r, "") if adminAPIErr != ErrNone { writeErrorResponseJSON(w, adminAPIErr, r.URL) return } // Deny if WORM is enabled if globalWORMEnabled { writeErrorResponseJSON(w, ErrMethodNotAllowed, r.URL) return } vars := mux.Vars(r) accessKey := vars["accessKey"] if err := globalIAMSys.DeleteUser(accessKey); err != nil { logger.LogIf(ctx, err) writeErrorResponseJSON(w, toAdminAPIErrCode(err), r.URL) } } // ListUsers - GET /minio/admin/v1/list-users func (a adminAPIHandlers) ListUsers(w http.ResponseWriter, r *http.Request) { ctx := newContext(r, w, "ListUsers") // Get current object layer instance. objectAPI := newObjectLayerFn() if objectAPI == nil { writeErrorResponseJSON(w, ErrServerNotInitialized, r.URL) return } // Validate request signature. adminAPIErr := checkAdminRequestAuthType(r, "") if adminAPIErr != ErrNone { writeErrorResponseJSON(w, adminAPIErr, r.URL) return } allCredentials, err := globalIAMSys.ListUsers() if err != nil { writeErrorResponseJSON(w, toAdminAPIErrCode(err), r.URL) return } data, err := json.Marshal(allCredentials) if err != nil { logger.LogIf(ctx, err) writeErrorResponseJSON(w, ErrInternalError, r.URL) return } password := globalServerConfig.GetCredential().SecretKey econfigData, err := madmin.EncryptData(password, data) if err != nil { logger.LogIf(ctx, err) writeErrorResponseJSON(w, ErrInternalError, r.URL) return } writeSuccessResponseJSON(w, econfigData) } // SetUserStatus - PUT /minio/admin/v1/set-user-status?accessKey=&status=[enabled|disabled] func (a adminAPIHandlers) SetUserStatus(w http.ResponseWriter, r *http.Request) { ctx := newContext(r, w, "SetUserStatus") // Get current object layer instance. objectAPI := newObjectLayerFn() if objectAPI == nil { writeErrorResponseJSON(w, ErrServerNotInitialized, r.URL) return } // Validate request signature. adminAPIErr := checkAdminRequestAuthType(r, "") if adminAPIErr != ErrNone { writeErrorResponseJSON(w, adminAPIErr, r.URL) return } // Deny if WORM is enabled if globalWORMEnabled { writeErrorResponseJSON(w, ErrMethodNotAllowed, r.URL) return } vars := mux.Vars(r) accessKey := vars["accessKey"] status := vars["status"] // Custom IAM policies not allowed for admin user. if accessKey == globalServerConfig.GetCredential().AccessKey { writeErrorResponse(w, ErrInvalidRequest, r.URL) return } if err := globalIAMSys.SetUserStatus(accessKey, madmin.AccountStatus(status)); err != nil { logger.LogIf(ctx, err) writeErrorResponseJSON(w, toAdminAPIErrCode(err), r.URL) return } } // AddUser - PUT /minio/admin/v1/add-user?accessKey= func (a adminAPIHandlers) AddUser(w http.ResponseWriter, r *http.Request) { ctx := newContext(r, w, "AddUser") // Get current object layer instance. objectAPI := newObjectLayerFn() if objectAPI == nil { writeErrorResponseJSON(w, ErrServerNotInitialized, r.URL) return } // Validate request signature. adminAPIErr := checkAdminRequestAuthType(r, "") if adminAPIErr != ErrNone { writeErrorResponseJSON(w, adminAPIErr, r.URL) return } // Deny if WORM is enabled if globalWORMEnabled { writeErrorResponseJSON(w, ErrMethodNotAllowed, r.URL) return } vars := mux.Vars(r) accessKey := vars["accessKey"] // Custom IAM policies not allowed for admin user. if accessKey == globalServerConfig.GetCredential().AccessKey { writeErrorResponse(w, ErrInvalidRequest, r.URL) return } if r.ContentLength > maxEConfigJSONSize || r.ContentLength == -1 { // More than maxConfigSize bytes were available writeErrorResponseJSON(w, ErrAdminConfigTooLarge, r.URL) return } password := globalServerConfig.GetCredential().SecretKey configBytes, err := madmin.DecryptData(password, io.LimitReader(r.Body, r.ContentLength)) if err != nil { logger.LogIf(ctx, err) writeErrorResponseJSON(w, ErrAdminConfigBadJSON, r.URL) return } var uinfo madmin.UserInfo if err = json.Unmarshal(configBytes, &uinfo); err != nil { logger.LogIf(ctx, err) writeErrorResponseJSON(w, ErrAdminConfigBadJSON, r.URL) return } if err = globalIAMSys.SetUser(accessKey, uinfo); err != nil { logger.LogIf(ctx, err) writeErrorResponseJSON(w, toAdminAPIErrCode(err), r.URL) return } } // ListCannedPolicies - GET /minio/admin/v1/list-canned-policies func (a adminAPIHandlers) ListCannedPolicies(w http.ResponseWriter, r *http.Request) { ctx := newContext(r, w, "ListCannedPolicies") // Get current object layer instance. objectAPI := newObjectLayerFn() if objectAPI == nil { writeErrorResponseJSON(w, ErrServerNotInitialized, r.URL) return } // Validate request signature. adminAPIErr := checkAdminRequestAuthType(r, "") if adminAPIErr != ErrNone { writeErrorResponseJSON(w, adminAPIErr, r.URL) return } policies, err := globalIAMSys.ListCannedPolicies() if err != nil { logger.LogIf(ctx, err) writeErrorResponseJSON(w, toAdminAPIErrCode(err), r.URL) return } if err = json.NewEncoder(w).Encode(policies); err != nil { logger.LogIf(ctx, err) writeErrorResponseJSON(w, toAdminAPIErrCode(err), r.URL) return } w.(http.Flusher).Flush() } // RemoveCannedPolicy - DELETE /minio/admin/v1/remove-canned-policy?name= func (a adminAPIHandlers) RemoveCannedPolicy(w http.ResponseWriter, r *http.Request) { ctx := newContext(r, w, "RemoveCannedPolicy") // Get current object layer instance. objectAPI := newObjectLayerFn() if objectAPI == nil { writeErrorResponseJSON(w, ErrServerNotInitialized, r.URL) return } vars := mux.Vars(r) policyName := vars["name"] // Validate request signature. adminAPIErr := checkAdminRequestAuthType(r, "") if adminAPIErr != ErrNone { writeErrorResponseJSON(w, adminAPIErr, r.URL) return } // Deny if WORM is enabled if globalWORMEnabled { writeErrorResponseJSON(w, ErrMethodNotAllowed, r.URL) return } if err := globalIAMSys.DeleteCannedPolicy(policyName); err != nil { logger.LogIf(ctx, err) writeErrorResponseJSON(w, toAdminAPIErrCode(err), r.URL) return } } // AddCannedPolicy - PUT /minio/admin/v1/add-canned-policy?name= func (a adminAPIHandlers) AddCannedPolicy(w http.ResponseWriter, r *http.Request) { ctx := newContext(r, w, "AddCannedPolicy") // Get current object layer instance. objectAPI := newObjectLayerFn() if objectAPI == nil { writeErrorResponseJSON(w, ErrServerNotInitialized, r.URL) return } vars := mux.Vars(r) policyName := vars["name"] // Validate request signature. adminAPIErr := checkAdminRequestAuthType(r, "") if adminAPIErr != ErrNone { writeErrorResponseJSON(w, adminAPIErr, r.URL) return } // Deny if WORM is enabled if globalWORMEnabled { writeErrorResponseJSON(w, ErrMethodNotAllowed, r.URL) return } // Error out if Content-Length is missing. if r.ContentLength <= 0 { writeErrorResponseJSON(w, ErrMissingContentLength, r.URL) return } // Error out if Content-Length is beyond allowed size. if r.ContentLength > maxBucketPolicySize { writeErrorResponseJSON(w, ErrEntityTooLarge, r.URL) return } iamPolicy, err := iampolicy.ParseConfig(io.LimitReader(r.Body, r.ContentLength)) if err != nil { writeErrorResponseJSON(w, ErrMalformedPolicy, r.URL) return } // Version in policy must not be empty if iamPolicy.Version == "" { writeErrorResponseJSON(w, ErrMalformedPolicy, r.URL) return } if err = globalIAMSys.SetCannedPolicy(policyName, *iamPolicy); err != nil { logger.LogIf(ctx, err) writeErrorResponseJSON(w, toAdminAPIErrCode(err), r.URL) return } } // SetUserPolicy - PUT /minio/admin/v1/set-user-policy?accessKey=&name= func (a adminAPIHandlers) SetUserPolicy(w http.ResponseWriter, r *http.Request) { ctx := newContext(r, w, "SetUserPolicy") // Get current object layer instance. objectAPI := newObjectLayerFn() if objectAPI == nil { writeErrorResponseJSON(w, ErrServerNotInitialized, r.URL) return } vars := mux.Vars(r) accessKey := vars["accessKey"] policyName := vars["name"] // Validate request signature. adminAPIErr := checkAdminRequestAuthType(r, "") if adminAPIErr != ErrNone { writeErrorResponseJSON(w, adminAPIErr, r.URL) return } // Deny if WORM is enabled if globalWORMEnabled { writeErrorResponseJSON(w, ErrMethodNotAllowed, r.URL) return } // Custom IAM policies not allowed for admin user. if accessKey == globalServerConfig.GetCredential().AccessKey { writeErrorResponseJSON(w, ErrInvalidRequest, r.URL) return } if err := globalIAMSys.SetUserPolicy(accessKey, policyName); err != nil { logger.LogIf(ctx, err) writeErrorResponseJSON(w, toAdminAPIErrCode(err), r.URL) } } // SetConfigHandler - PUT /minio/admin/v1/config func (a adminAPIHandlers) SetConfigHandler(w http.ResponseWriter, r *http.Request) { ctx := newContext(r, w, "SetConfigHandler") // Get current object layer instance. objectAPI := newObjectLayerFn() if objectAPI == nil { writeErrorResponseJSON(w, ErrServerNotInitialized, r.URL) return } // Validate request signature. adminAPIErr := checkAdminRequestAuthType(r, "") if adminAPIErr != ErrNone { writeErrorResponseJSON(w, adminAPIErr, r.URL) return } // Deny if WORM is enabled if globalWORMEnabled { writeErrorResponseJSON(w, ErrMethodNotAllowed, r.URL) return } if r.ContentLength > maxEConfigJSONSize || r.ContentLength == -1 { // More than maxConfigSize bytes were available writeErrorResponseJSON(w, ErrAdminConfigTooLarge, r.URL) return } password := globalServerConfig.GetCredential().SecretKey configBytes, err := madmin.DecryptData(password, io.LimitReader(r.Body, r.ContentLength)) if err != nil { logger.LogIf(ctx, err) writeErrorResponseJSON(w, ErrAdminConfigBadJSON, r.URL) return } // Validate JSON provided in the request body: check the // client has not sent JSON objects with duplicate keys. if err = quick.CheckDuplicateKeys(string(configBytes)); err != nil { logger.LogIf(ctx, err) writeErrorResponseJSON(w, ErrAdminConfigBadJSON, r.URL) return } var config serverConfig if err = json.Unmarshal(configBytes, &config); err != nil { logger.LogIf(ctx, err) writeCustomErrorResponseJSON(w, ErrAdminConfigBadJSON, err.Error(), r.URL) return } // If credentials for the server are provided via environment, // then credentials in the provided configuration must match. if globalIsEnvCreds { creds := globalServerConfig.GetCredential() if config.Credential.AccessKey != creds.AccessKey || config.Credential.SecretKey != creds.SecretKey { writeErrorResponseJSON(w, ErrAdminCredentialsMismatch, r.URL) return } } if err = config.Validate(); err != nil { writeCustomErrorResponseJSON(w, ErrAdminConfigBadJSON, err.Error(), r.URL) return } if err = config.TestNotificationTargets(); err != nil { writeCustomErrorResponseJSON(w, ErrAdminConfigBadJSON, err.Error(), r.URL) return } if err = saveServerConfig(ctx, objectAPI, &config); err != nil { writeErrorResponseJSON(w, toAdminAPIErrCode(err), r.URL) return } // Reply to the client before restarting minio server. writeSuccessResponseHeadersOnly(w) } func convertValueType(elem []byte, jsonType gjson.Type) (interface{}, error) { str := string(elem) switch jsonType { case gjson.False, gjson.True: return strconv.ParseBool(str) case gjson.JSON: return gjson.Parse(str).Value(), nil case gjson.String: return str, nil case gjson.Number: return strconv.ParseFloat(str, 64) default: return nil, nil } } // SetConfigKeysHandler - PUT /minio/admin/v1/config-keys func (a adminAPIHandlers) SetConfigKeysHandler(w http.ResponseWriter, r *http.Request) { ctx := newContext(r, w, "SetConfigKeysHandler") // Get current object layer instance. objectAPI := newObjectLayerFn() if objectAPI == nil { writeErrorResponseJSON(w, ErrServerNotInitialized, r.URL) return } // Deny if WORM is enabled if globalWORMEnabled { writeErrorResponseJSON(w, ErrMethodNotAllowed, r.URL) return } // Validate request signature. adminAPIErr := checkAdminRequestAuthType(r, "") if adminAPIErr != ErrNone { writeErrorResponseJSON(w, adminAPIErr, r.URL) return } // Load config configStruct, err := readServerConfig(ctx, objectAPI) if err != nil { writeErrorResponseJSON(w, toAdminAPIErrCode(err), r.URL) return } // Convert config to json bytes configBytes, err := json.Marshal(configStruct) if err != nil { logger.LogIf(ctx, err) writeErrorResponseJSON(w, toAdminAPIErrCode(err), r.URL) return } configStr := string(configBytes) queries := r.URL.Query() password := globalServerConfig.GetCredential().SecretKey // Set key values in the JSON config for k := range queries { // Decode encrypted data associated to the current key encryptedElem, dErr := base64.StdEncoding.DecodeString(queries.Get(k)) if dErr != nil { reqInfo := (&logger.ReqInfo{}).AppendTags("key", k) ctx = logger.SetReqInfo(ctx, reqInfo) logger.LogIf(ctx, dErr) writeErrorResponseJSON(w, ErrAdminConfigBadJSON, r.URL) return } elem, dErr := madmin.DecryptData(password, bytes.NewBuffer([]byte(encryptedElem))) if dErr != nil { logger.LogIf(ctx, dErr) writeErrorResponseJSON(w, ErrAdminConfigBadJSON, r.URL) return } // Calculate the type of the current key from the // original config json jsonFieldType := gjson.Get(configStr, k).Type // Convert passed value to json filed type val, cErr := convertValueType(elem, jsonFieldType) if cErr != nil { writeCustomErrorResponseJSON(w, ErrAdminConfigBadJSON, cErr.Error(), r.URL) return } // Set the key/value in the new json document if s, sErr := sjson.Set(configStr, normalizeJSONKey(k), val); sErr == nil { configStr = s } } configBytes = []byte(configStr) // Validate config var config serverConfig if err = json.Unmarshal(configBytes, &config); err != nil { writeCustomErrorResponseJSON(w, ErrAdminConfigBadJSON, err.Error(), r.URL) return } if err = config.Validate(); err != nil { writeCustomErrorResponseJSON(w, ErrAdminConfigBadJSON, err.Error(), r.URL) return } if err = config.TestNotificationTargets(); err != nil { writeCustomErrorResponseJSON(w, ErrAdminConfigBadJSON, err.Error(), r.URL) return } // If credentials for the server are provided via environment, // then credentials in the provided configuration must match. if globalIsEnvCreds { creds := globalServerConfig.GetCredential() if config.Credential.AccessKey != creds.AccessKey || config.Credential.SecretKey != creds.SecretKey { writeErrorResponseJSON(w, ErrAdminCredentialsMismatch, r.URL) return } } if err = saveServerConfig(ctx, objectAPI, &config); err != nil { writeErrorResponseJSON(w, toAdminAPIErrCode(err), r.URL) return } // Send success response writeSuccessResponseHeadersOnly(w) } // UpdateAdminCredsHandler - POST /minio/admin/v1/config/credential // ---------- // Update admin credentials in a minio server func (a adminAPIHandlers) UpdateAdminCredentialsHandler(w http.ResponseWriter, r *http.Request) { ctx := newContext(r, w, "UpdateCredentialsHandler") // Get current object layer instance. objectAPI := newObjectLayerFn() if objectAPI == nil { writeErrorResponseJSON(w, ErrServerNotInitialized, r.URL) return } // Avoid setting new credentials when they are already passed // by the environment. Deny if WORM is enabled. if globalIsEnvCreds || globalWORMEnabled { writeErrorResponseJSON(w, ErrMethodNotAllowed, r.URL) return } // Authenticate request adminAPIErr := checkAdminRequestAuthType(r, "") if adminAPIErr != ErrNone { writeErrorResponseJSON(w, adminAPIErr, r.URL) return } if r.ContentLength > maxEConfigJSONSize || r.ContentLength == -1 { // More than maxConfigSize bytes were available writeErrorResponseJSON(w, ErrAdminConfigTooLarge, r.URL) return } password := globalServerConfig.GetCredential().SecretKey configBytes, err := madmin.DecryptData(password, io.LimitReader(r.Body, r.ContentLength)) if err != nil { logger.LogIf(ctx, err) writeErrorResponseJSON(w, ErrAdminConfigBadJSON, r.URL) return } // Decode request body var req madmin.SetCredsReq if err = json.Unmarshal(configBytes, &req); err != nil { logger.LogIf(ctx, err) writeErrorResponseJSON(w, ErrRequestBodyParse, r.URL) return } creds, err := auth.CreateCredentials(req.AccessKey, req.SecretKey) if err != nil { writeErrorResponseJSON(w, toAdminAPIErrCode(err), r.URL) return } // Acquire lock before updating global configuration. globalServerConfigMu.Lock() defer globalServerConfigMu.Unlock() // Update local credentials in memory. globalServerConfig.SetCredential(creds) // Set active creds. globalActiveCred = creds if err = saveServerConfig(ctx, objectAPI, globalServerConfig); err != nil { writeErrorResponseJSON(w, toAdminAPIErrCode(err), r.URL) return } // Notify all other Minio peers to update credentials for host, err := range globalNotificationSys.LoadCredentials() { if err != nil { logger.GetReqInfo(ctx).SetTags("peerAddress", host.String()) logger.LogIf(ctx, err) } } // Reply to the client before restarting minio server. writeSuccessResponseHeadersOnly(w) }