diff --git a/cmd/admin-handlers.go b/cmd/admin-handlers.go index dc1c58629..746c16dd5 100644 --- a/cmd/admin-handlers.go +++ b/cmd/admin-handlers.go @@ -106,7 +106,7 @@ func (a adminAPIHandlers) ServiceStatusHandler(w http.ResponseWriter, r *http.Re // of read-quorum availability. uptime, err := getPeerUptimes(globalAdminPeers) if err != nil { - writeErrorResponseJSON(w, toAPIErrorCode(err), r.URL) + writeErrorResponseJSON(w, toAdminAPIErrCode(err), r.URL) logger.LogIf(context.Background(), err) return } @@ -731,41 +731,7 @@ func (a adminAPIHandlers) RemoveUser(w http.ResponseWriter, r *http.Request) { accessKey := vars["accessKey"] if err := globalIAMSys.DeleteUser(accessKey); err != nil { logger.LogIf(ctx, err) - writeErrorResponseJSON(w, ErrInternalError, r.URL) - return - } -} - -// RemoveUserPolicy - DELETE /minio/admin/v1/remove-user-policy?accessKey= -func (a adminAPIHandlers) RemoveUserPolicy(w http.ResponseWriter, r *http.Request) { - ctx := newContext(r, w, "RemoveUserPolicy") - - // 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.DeletePolicy(accessKey); err != nil { - logger.LogIf(ctx, err) - writeErrorResponseJSON(w, ErrInternalError, r.URL) - return + writeErrorResponseJSON(w, toAdminAPIErrCode(err), r.URL) } } @@ -867,14 +833,140 @@ func (a adminAPIHandlers) AddUser(w http.ResponseWriter, r *http.Request) { if err = globalIAMSys.SetUser(accessKey, uinfo); err != nil { logger.LogIf(ctx, err) - writeErrorResponseJSON(w, ErrInternalError, r.URL) + writeErrorResponseJSON(w, toAdminAPIErrCode(err), r.URL) return } } -// AddUserPolicy - PUT /minio/admin/v1/add-user-policy?accessKey= -func (a adminAPIHandlers) AddUserPolicy(w http.ResponseWriter, r *http.Request) { - ctx := newContext(r, w, "AddUserPolicy") +// 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() @@ -885,6 +977,7 @@ func (a adminAPIHandlers) AddUserPolicy(w http.ResponseWriter, r *http.Request) vars := mux.Vars(r) accessKey := vars["accessKey"] + policyName := vars["name"] // Validate request signature. adminAPIErr := checkAdminRequestAuthType(r, "") @@ -901,38 +994,13 @@ func (a adminAPIHandlers) AddUserPolicy(w http.ResponseWriter, r *http.Request) // Custom IAM policies not allowed for admin user. if accessKey == globalServerConfig.GetCredential().AccessKey { - writeErrorResponse(w, ErrInvalidRequest, r.URL) + writeErrorResponseJSON(w, ErrInvalidRequest, r.URL) return } - // Error out if Content-Length is missing. - if r.ContentLength <= 0 { - writeErrorResponse(w, ErrMissingContentLength, r.URL) - return - } - - // Error out if Content-Length is beyond allowed size. - if r.ContentLength > maxBucketPolicySize { - writeErrorResponse(w, ErrEntityTooLarge, r.URL) - return - } - - iamPolicy, err := iampolicy.ParseConfig(io.LimitReader(r.Body, r.ContentLength)) - if err != nil { - writeErrorResponse(w, ErrMalformedPolicy, r.URL) - return - } - - // Version in policy must not be empty - if iamPolicy.Version == "" { - writeErrorResponse(w, ErrMalformedPolicy, r.URL) - return - } - - if err = globalIAMSys.SetPolicy(accessKey, *iamPolicy); err != nil { + if err := globalIAMSys.SetUserPolicy(accessKey, policyName); err != nil { logger.LogIf(ctx, err) - writeErrorResponse(w, ErrInternalError, r.URL) - return + writeErrorResponseJSON(w, toAdminAPIErrCode(err), r.URL) } } @@ -1198,7 +1266,7 @@ func (a adminAPIHandlers) UpdateAdminCredentialsHandler(w http.ResponseWriter, creds, err := auth.CreateCredentials(req.AccessKey, req.SecretKey) if err != nil { - writeErrorResponseJSON(w, toAPIErrorCode(err), r.URL) + writeErrorResponseJSON(w, toAdminAPIErrCode(err), r.URL) return } diff --git a/cmd/admin-router.go b/cmd/admin-router.go index c91454a97..f9cb38190 100644 --- a/cmd/admin-router.go +++ b/cmd/admin-router.go @@ -81,15 +81,23 @@ func registerAdminRouter(router *mux.Router) { // -- IAM APIs -- + // Add policy IAM + adminV1Router.Methods(http.MethodPut).Path("/add-canned-policy").HandlerFunc(httpTraceHdrs(adminAPI.AddCannedPolicy)).Queries("name", "{name:.*}") + // Add user IAM adminV1Router.Methods(http.MethodPut).Path("/add-user").HandlerFunc(httpTraceHdrs(adminAPI.AddUser)).Queries("accessKey", "{accessKey:.*}") - adminV1Router.Methods(http.MethodPut).Path("/add-user-policy").HandlerFunc(httpTraceHdrs(adminAPI.AddUserPolicy)).Queries("accessKey", "{accessKey:.*}") + adminV1Router.Methods(http.MethodPut).Path("/set-user-policy").HandlerFunc(httpTraceHdrs(adminAPI.SetUserPolicy)). + Queries("accessKey", "{accessKey:.*}").Queries("name", "{name:.*}") + + // Remove policy IAM + adminV1Router.Methods(http.MethodDelete).Path("/remove-canned-policy").HandlerFunc(httpTraceHdrs(adminAPI.RemoveCannedPolicy)).Queries("name", "{name:.*}") // Remove user IAM adminV1Router.Methods(http.MethodDelete).Path("/remove-user").HandlerFunc(httpTraceHdrs(adminAPI.RemoveUser)).Queries("accessKey", "{accessKey:.*}") - adminV1Router.Methods(http.MethodDelete).Path("/remove-user-policy").HandlerFunc(httpTraceHdrs(adminAPI.RemoveUserPolicy)).Queries("accessKey", "{accessKey:.*}") // List users adminV1Router.Methods(http.MethodGet).Path("/list-users").HandlerFunc(httpTraceHdrs(adminAPI.ListUsers)) + // List policies + adminV1Router.Methods(http.MethodGet).Path("/list-canned-policies").HandlerFunc(httpTraceHdrs(adminAPI.ListCannedPolicies)) } diff --git a/cmd/api-errors.go b/cmd/api-errors.go index 79532f20e..89946cf43 100644 --- a/cmd/api-errors.go +++ b/cmd/api-errors.go @@ -187,6 +187,9 @@ const ( // new error codes here. ErrMalformedJSON + ErrAdminNoSuchUser + ErrAdminNoSuchPolicy + ErrAdminInvalidArgument ErrAdminInvalidAccessKey ErrAdminInvalidSecretKey ErrAdminConfigNoQuorum @@ -864,6 +867,21 @@ var errorCodeResponse = map[APIErrorCode]APIError{ Description: "The JSON you provided was not well-formed or did not validate against our published format.", HTTPStatusCode: http.StatusBadRequest, }, + ErrAdminNoSuchUser: { + Code: "XMinioAdminNoSuchUser", + Description: "The specified user does not exist.", + HTTPStatusCode: http.StatusNotFound, + }, + ErrAdminNoSuchPolicy: { + Code: "XMinioAdminNoSuchPolicy", + Description: "The canned policy does not exist.", + HTTPStatusCode: http.StatusNotFound, + }, + ErrAdminInvalidArgument: { + Code: "XMinioAdminInvalidArgument", + Description: "Invalid arguments specified.", + HTTPStatusCode: http.StatusBadRequest, + }, ErrAdminInvalidAccessKey: { Code: "XMinioAdminInvalidAccessKey", Description: "The access key is invalid.", @@ -1428,6 +1446,12 @@ func toAPIErrorCode(err error) (apiErr APIErrorCode) { // Verify if the underlying error is signature mismatch. switch err { + case errInvalidArgument: + apiErr = ErrAdminInvalidArgument + case errNoSuchUser: + apiErr = ErrAdminNoSuchUser + case errNoSuchPolicy: + apiErr = ErrAdminNoSuchPolicy case errSignatureMismatch: apiErr = ErrSignatureDoesNotMatch case errInvalidRange: diff --git a/cmd/iam.go b/cmd/iam.go index 738b7f7e9..066f8a868 100644 --- a/cmd/iam.go +++ b/cmd/iam.go @@ -39,6 +39,9 @@ const ( // IAM users directory. iamConfigUsersPrefix = iamConfigPrefix + "/users/" + // IAM policies directory. + iamConfigPoliciesPrefix = iamConfigPrefix + "/policies/" + // IAM sts directory. iamConfigSTSPrefix = iamConfigPrefix + "/sts/" @@ -52,8 +55,9 @@ const ( // IAMSys - config system. type IAMSys struct { sync.RWMutex - iamUsersMap map[string]auth.Credentials - iamPolicyMap map[string]iampolicy.Policy + iamUsersMap map[string]auth.Credentials + iamPolicyMap map[string]string + iamCannedPolicyMap map[string]iampolicy.Policy } // Load - load iam.json @@ -109,87 +113,19 @@ func (sys *IAMSys) Init(objAPI ObjectLayer) error { } } -// SetPolicy - sets policy to given user name. If policy is empty, -// existing policy is removed. -func (sys *IAMSys) SetPolicy(accessKey string, p iampolicy.Policy) error { +// DeleteCannedPolicy - deletes a canned policy. +func (sys *IAMSys) DeleteCannedPolicy(policyName string) error { objectAPI := newObjectLayerFn() if objectAPI == nil { return errServerNotInitialized } - configFile := pathJoin(iamConfigUsersPrefix, accessKey, iamPolicyFile) - data, err := json.Marshal(p) - if err != nil { - return err - } - - if globalEtcdClient != nil { - if err = saveConfigEtcd(context.Background(), globalEtcdClient, configFile, data); err != nil { - return err - } - } else { - if err = saveConfig(context.Background(), objectAPI, configFile, data); err != nil { - return err - } - } - - sys.Lock() - defer sys.Unlock() - - if p.IsEmpty() { - delete(sys.iamPolicyMap, accessKey) - } else { - sys.iamPolicyMap[accessKey] = p - } - - return nil -} - -// SaveTempPolicy - this is used for temporary credentials only. -func (sys *IAMSys) SaveTempPolicy(accessKey string, p iampolicy.Policy) error { - objectAPI := newObjectLayerFn() - if objectAPI == nil { - return errServerNotInitialized - } - - configFile := pathJoin(iamConfigSTSPrefix, accessKey, iamPolicyFile) - data, err := json.Marshal(p) - if err != nil { - return err - } - - if globalEtcdClient != nil { - if err = saveConfigEtcd(context.Background(), globalEtcdClient, configFile, data); err != nil { - return err - } - } else { - if err = saveConfig(context.Background(), objectAPI, configFile, data); err != nil { - return err - } - } - - sys.Lock() - defer sys.Unlock() - - if p.IsEmpty() { - delete(sys.iamPolicyMap, accessKey) - } else { - sys.iamPolicyMap[accessKey] = p - } - - return nil -} - -// DeletePolicy - sets policy to given user name. If policy is empty, -// existing policy is removed. -func (sys *IAMSys) DeletePolicy(accessKey string) error { - objectAPI := newObjectLayerFn() - if objectAPI == nil { - return errServerNotInitialized + if policyName == "" { + return errInvalidArgument } var err error - configFile := pathJoin(iamConfigUsersPrefix, accessKey, iamPolicyFile) + configFile := pathJoin(iamConfigPoliciesPrefix, policyName, iamPolicyFile) if globalEtcdClient != nil { err = deleteConfigEtcd(context.Background(), globalEtcdClient, configFile) } else { @@ -199,11 +135,104 @@ func (sys *IAMSys) DeletePolicy(accessKey string) error { sys.Lock() defer sys.Unlock() - delete(sys.iamPolicyMap, accessKey) - + delete(sys.iamCannedPolicyMap, policyName) return err } +// ListCannedPolicies - lists all canned policies. +func (sys *IAMSys) ListCannedPolicies() (map[string][]byte, error) { + objectAPI := newObjectLayerFn() + if objectAPI == nil { + return nil, errServerNotInitialized + } + + var cannedPolicyMap = make(map[string][]byte) + + sys.RLock() + defer sys.RUnlock() + + for k, v := range sys.iamCannedPolicyMap { + data, err := json.Marshal(v) + if err != nil { + return nil, err + } + cannedPolicyMap[k] = data + } + + return cannedPolicyMap, nil +} + +// SetCannedPolicy - sets a new canned policy. +func (sys *IAMSys) SetCannedPolicy(policyName string, p iampolicy.Policy) error { + objectAPI := newObjectLayerFn() + if objectAPI == nil { + return errServerNotInitialized + } + + if p.IsEmpty() || policyName == "" { + return errInvalidArgument + } + + configFile := pathJoin(iamConfigPoliciesPrefix, policyName, iamPolicyFile) + data, err := json.Marshal(p) + if err != nil { + return err + } + + if globalEtcdClient != nil { + err = saveConfigEtcd(context.Background(), globalEtcdClient, configFile, data) + } else { + err = saveConfig(context.Background(), objectAPI, configFile, data) + } + if err != nil { + return err + } + + sys.Lock() + defer sys.Unlock() + + sys.iamCannedPolicyMap[policyName] = p + + return nil +} + +// SetUserPolicy - sets policy to given user name. +func (sys *IAMSys) SetUserPolicy(accessKey, policyName string) error { + objectAPI := newObjectLayerFn() + if objectAPI == nil { + return errServerNotInitialized + } + + sys.Lock() + defer sys.Unlock() + + if _, ok := sys.iamUsersMap[accessKey]; !ok { + return errNoSuchUser + } + + if _, ok := sys.iamCannedPolicyMap[policyName]; !ok { + return errNoSuchPolicy + } + + data, err := json.Marshal(policyName) + if err != nil { + return err + } + + configFile := pathJoin(iamConfigUsersPrefix, accessKey, iamPolicyFile) + if globalEtcdClient != nil { + err = saveConfigEtcd(context.Background(), globalEtcdClient, configFile, data) + } else { + err = saveConfig(context.Background(), objectAPI, configFile, data) + } + if err != nil { + return err + } + + sys.iamPolicyMap[accessKey] = policyName + return nil +} + // DeleteUser - set user credentials. func (sys *IAMSys) DeleteUser(accessKey string) error { objectAPI := newObjectLayerFn() @@ -212,17 +241,30 @@ func (sys *IAMSys) DeleteUser(accessKey string) error { } var err error - configFile := pathJoin(iamConfigUsersPrefix, accessKey, iamPolicyFile) + pFile := pathJoin(iamConfigUsersPrefix, accessKey, iamPolicyFile) + iFile := pathJoin(iamConfigUsersPrefix, accessKey, iamIdentityFile) if globalEtcdClient != nil { - err = deleteConfigEtcd(context.Background(), globalEtcdClient, configFile) + // It is okay to ingnore errors when deleting policy.json for the user. + _ = deleteConfigEtcd(context.Background(), globalEtcdClient, pFile) + err = deleteConfigEtcd(context.Background(), globalEtcdClient, iFile) } else { - err = deleteConfig(context.Background(), objectAPI, configFile) + // It is okay to ingnore errors when deleting policy.json for the user. + _ = deleteConfig(context.Background(), objectAPI, pFile) + err = deleteConfig(context.Background(), objectAPI, iFile) + } + + // + switch err.(type) { + case ObjectNotFound: + err = errNoSuchUser } sys.Lock() defer sys.Unlock() delete(sys.iamUsersMap, accessKey) + delete(sys.iamPolicyMap, accessKey) + return err } @@ -240,13 +282,13 @@ func (sys *IAMSys) SetTempUser(accessKey string, cred auth.Credentials) error { } if globalEtcdClient != nil { - if err = saveConfigEtcd(context.Background(), globalEtcdClient, configFile, data); err != nil { - return err - } + err = saveConfigEtcd(context.Background(), globalEtcdClient, configFile, data) } else { - if err = saveConfig(context.Background(), objectAPI, configFile, data); err != nil { - return err - } + err = saveConfig(context.Background(), objectAPI, configFile, data) + } + + if err != nil { + return err } sys.Lock() @@ -270,7 +312,8 @@ func (sys *IAMSys) ListUsers() (map[string]madmin.UserInfo, error) { for k, v := range sys.iamUsersMap { users[k] = madmin.UserInfo{ - Status: madmin.AccountStatus(v.Status), + PolicyName: sys.iamPolicyMap[k], + Status: madmin.AccountStatus(v.Status), } } @@ -291,13 +334,13 @@ func (sys *IAMSys) SetUser(accessKey string, uinfo madmin.UserInfo) error { } if globalEtcdClient != nil { - if err = saveConfigEtcd(context.Background(), globalEtcdClient, configFile, data); err != nil { - return err - } + err = saveConfigEtcd(context.Background(), globalEtcdClient, configFile, data) } else { - if err = saveConfig(context.Background(), objectAPI, configFile, data); err != nil { - return err - } + err = saveConfig(context.Background(), objectAPI, configFile, data) + } + + if err != nil { + return err } sys.Lock() @@ -332,8 +375,9 @@ func (sys *IAMSys) IsAllowed(args iampolicy.Args) bool { } // If policy is available for given user, check the policy. - if p, found := sys.iamPolicyMap[args.AccountName]; found { - return p.IsAllowed(args) + if name, found := sys.iamPolicyMap[args.AccountName]; found { + p, ok := sys.iamCannedPolicyMap[name] + return ok && p.IsAllowed(args) } // As policy is not available and OPA is not configured, return the owner value. @@ -343,7 +387,7 @@ func (sys *IAMSys) IsAllowed(args iampolicy.Args) bool { var defaultContextTimeout = 5 * time.Minute // Similar to reloadUsers but updates users, policies maps from etcd server, -func reloadEtcdUsers(prefix string, usersMap map[string]auth.Credentials, policyMap map[string]iampolicy.Policy) error { +func reloadEtcdUsers(prefix string, usersMap map[string]auth.Credentials, policyMap map[string]string) error { ctx, cancel := context.WithTimeout(context.Background(), defaultContextTimeout) r, err := globalEtcdClient.Get(ctx, prefix, etcd.WithPrefix(), etcd.WithKeysOnly()) defer cancel() @@ -400,18 +444,92 @@ func reloadEtcdUsers(prefix string, usersMap map[string]auth.Credentials, policy usersMap[cred.AccessKey] = cred } if perr == nil { - var p iampolicy.Policy - if err = json.Unmarshal(pdata, &p); err != nil { + var policyName string + if err = json.Unmarshal(pdata, &policyName); err != nil { return err } - policyMap[path.Base(prefix)] = p + policyMap[path.Base(prefix)] = policyName } } return nil } +func reloadEtcdPolicies(prefix string, cannedPolicyMap map[string]iampolicy.Policy) error { + ctx, cancel := context.WithTimeout(context.Background(), defaultContextTimeout) + r, err := globalEtcdClient.Get(ctx, prefix, etcd.WithPrefix(), etcd.WithKeysOnly()) + defer cancel() + if err != nil { + return err + } + // No users are created yet. + if r.Count == 0 { + return nil + } + + policies := set.NewStringSet() + for _, kv := range r.Kvs { + // Extract policy by stripping off the `prefix` value as suffix, + // then strip off the remaining basename to obtain the prefix + // value, usually in the following form. + // + // key := "config/iam/policys/newpolicy/identity.json" + // prefix := "config/iam/policys/" + // v := trim(trim(key, prefix), base(key)) == "newpolicy" + // + policyName := strings.TrimSuffix(strings.TrimSuffix(string(kv.Key), prefix), path.Base(string(kv.Key))) + if !policies.Contains(policyName) { + policies.Add(policyName) + } + } + + // Reload config and policies for all policys. + for _, policyName := range policies.ToSlice() { + pFile := pathJoin(prefix, policyName, iamPolicyFile) + pdata, perr := readConfigEtcd(ctx, globalEtcdClient, pFile) + if perr != nil { + return perr + } + var p iampolicy.Policy + if err = json.Unmarshal(pdata, &p); err != nil { + return err + } + cannedPolicyMap[path.Base(prefix)] = p + } + return nil +} + +func reloadPolicies(objectAPI ObjectLayer, prefix string, cannedPolicyMap map[string]iampolicy.Policy) error { + marker := "" + for { + var lo ListObjectsInfo + var err error + lo, err = objectAPI.ListObjects(context.Background(), minioMetaBucket, prefix, marker, "/", 1000) + if err != nil { + return err + } + marker = lo.NextMarker + for _, prefix := range lo.Prefixes { + pFile := pathJoin(prefix, iamPolicyFile) + pdata, perr := readConfig(context.Background(), objectAPI, pFile) + if perr != nil { + return perr + } + var p iampolicy.Policy + if err = json.Unmarshal(pdata, &p); err != nil { + return err + } + cannedPolicyMap[path.Base(prefix)] = p + } + if !lo.IsTruncated { + break + } + } + return nil + +} + // reloadUsers reads an updates users, policies from object layer into user and policy maps. -func reloadUsers(objectAPI ObjectLayer, prefix string, usersMap map[string]auth.Credentials, policyMap map[string]iampolicy.Policy) error { +func reloadUsers(objectAPI ObjectLayer, prefix string, usersMap map[string]auth.Credentials, policyMap map[string]string) error { marker := "" for { var lo ListObjectsInfo @@ -451,11 +569,11 @@ func reloadUsers(objectAPI ObjectLayer, prefix string, usersMap map[string]auth. usersMap[cred.AccessKey] = cred } if perr == nil { - var p iampolicy.Policy - if err = json.Unmarshal(pdata, &p); err != nil { + var policyName string + if err = json.Unmarshal(pdata, &policyName); err != nil { return err } - policyMap[path.Base(prefix)] = p + policyMap[path.Base(prefix)] = policyName } } if !lo.IsTruncated { @@ -468,9 +586,13 @@ func reloadUsers(objectAPI ObjectLayer, prefix string, usersMap map[string]auth. // Refresh IAMSys. func (sys *IAMSys) refresh(objAPI ObjectLayer) error { iamUsersMap := make(map[string]auth.Credentials) - iamPolicyMap := make(map[string]iampolicy.Policy) + iamPolicyMap := make(map[string]string) + iamCannedPolicyMap := make(map[string]iampolicy.Policy) if globalEtcdClient != nil { + if err := reloadEtcdPolicies(iamConfigPoliciesPrefix, iamCannedPolicyMap); err != nil { + return err + } if err := reloadEtcdUsers(iamConfigUsersPrefix, iamUsersMap, iamPolicyMap); err != nil { return err } @@ -478,6 +600,9 @@ func (sys *IAMSys) refresh(objAPI ObjectLayer) error { return err } } else { + if err := reloadPolicies(objAPI, iamConfigPoliciesPrefix, iamCannedPolicyMap); err != nil { + return err + } if err := reloadUsers(objAPI, iamConfigUsersPrefix, iamUsersMap, iamPolicyMap); err != nil { return err } @@ -491,6 +616,7 @@ func (sys *IAMSys) refresh(objAPI ObjectLayer) error { sys.iamUsersMap = iamUsersMap sys.iamPolicyMap = iamPolicyMap + sys.iamCannedPolicyMap = iamCannedPolicyMap return nil } @@ -498,7 +624,8 @@ func (sys *IAMSys) refresh(objAPI ObjectLayer) error { // NewIAMSys - creates new config system object. func NewIAMSys() *IAMSys { return &IAMSys{ - iamUsersMap: make(map[string]auth.Credentials), - iamPolicyMap: make(map[string]iampolicy.Policy), + iamUsersMap: make(map[string]auth.Credentials), + iamPolicyMap: make(map[string]string), + iamCannedPolicyMap: make(map[string]iampolicy.Policy), } } diff --git a/cmd/typed-errors.go b/cmd/typed-errors.go index 6eb8c1b74..e1f9368dd 100644 --- a/cmd/typed-errors.go +++ b/cmd/typed-errors.go @@ -76,3 +76,9 @@ var errBucketAlreadyExists = errors.New("Your previous request to create the nam // error returned for a negative actual size. var errInvalidDecompressedSize = errors.New("Invalid Decompressed Size") + +// error returned in IAM subsystem when user doesn't exist. +var errNoSuchUser = errors.New("Specified user does not exist") + +// error returned in IAM subsystem when policy doesn't exist. +var errNoSuchPolicy = errors.New("Specified canned policy does not exist") diff --git a/docs/multi-user/README.md b/docs/multi-user/README.md index 365480d6c..bb13d74ab 100644 --- a/docs/multi-user/README.md +++ b/docs/multi-user/README.md @@ -9,13 +9,9 @@ In this document we will explain in detail on how to configure multiple users. - Install Minio - [Minio Quickstart Guide](https://docs.minio.io/docs/minio-quickstart-guide) ### 2. Create a new user and policy -Create a new user `newuser` on Minio use `mc admin users`, with a `newuser.json`. -``` -mc admin users add myminio newuser newuser123 /tmp/newuser.json -``` - -An example user policy, enables `newuser` to download all objects in my-bucketname. +Create new canned policy `getonly` with `newuser.json` use `mc admin policies`. This policy enables users to download all objects in my-bucketname. ```json +cat > getonly.json << EOF { "Version": "2012-10-17", "Statement": [ @@ -31,6 +27,14 @@ An example user policy, enables `newuser` to download all objects in my-bucketna } ] } +EOF + +mc admin policies add myminio getonly getonly.json +``` + +Create a new user `newuser` on Minio use `mc admin users`, additionally specify `getonly` canned policy for this `newuser`. +``` +mc admin users add myminio newuser newuser123 getonly ``` ### 3. Revoke user diff --git a/pkg/madmin/API.md b/pkg/madmin/API.md index 94ef5481a..75469df5c 100644 --- a/pkg/madmin/API.md +++ b/pkg/madmin/API.md @@ -38,10 +38,10 @@ func main() { | Service operations | Info operations | Healing operations | Config operations | IAM operations | Misc | |:----------------------------|:----------------------------|:--------------------------------------|:--------------------------|:------------------------------------|:------------------------------------| -| [`ServiceStatus`](#ServiceStatus) | [`ServerInfo`](#ServerInfo) | [`Heal`](#Heal) | [`GetConfig`](#GetConfig) | [`AddUser()`](#AddUser) | [`SetAdminCredentials`](#SetAdminCredentials) | -| [`ServiceSendAction`](#ServiceSendAction) | | | [`SetConfig`](#SetConfig) | [`AddUserPolicy`](#AddUserPolicy) | [`StartProfiling`](#StartProfiling) | +| [`ServiceStatus`](#ServiceStatus) | [`ServerInfo`](#ServerInfo) | [`Heal`](#Heal) | [`GetConfig`](#GetConfig) | [`AddUser`](#AddUser) | [`SetAdminCredentials`](#SetAdminCredentials) | +| [`ServiceSendAction`](#ServiceSendAction) | | | [`SetConfig`](#SetConfig) | [`SetUserPolicy`](#SetUserPolicy) | [`StartProfiling`](#StartProfiling) | | | | | [`GetConfigKeys`](#GetConfigKeys) | [`ListUsers`](#ListUsers) | [`DownloadProfilingData`](#DownloadProfilingData) | -| | | | [`SetConfigKeys`](#SetConfigKeys) | | +| | | | [`SetConfigKeys`](#SetConfigKeys) | [`AddCannedPolicy`](#AddCannedPolicy) | | ## 1. Constructor @@ -349,6 +349,20 @@ __Example__ ## 8. IAM operations + +### AddCannedPolicy(policyName string, policy string) error +Create a new canned policy on Minio server. + +__Example__ + +``` + policy := `{"Version": "2012-10-17","Statement": [{"Action": ["s3:GetObject"],"Effect": "Allow","Resource": ["arn:aws:s3:::my-bucketname/*"],"Sid": ""}]}` + + if err = madmClnt.AddCannedPolicy("get-only", policy); err != nil { + log.Fatalln(err) + } +``` + ### AddUser(user string, secret string) error Add a new user on a Minio server. @@ -361,16 +375,14 @@ __Example__ } ``` - -### AddUserPolicy(user string, policy string) error -Set a new policy for a given user on Minio server. + +### SetUserPolicy(user string, policyName string) error +Enable a canned policy `get-only` for a given user on Minio server. __Example__ ``` go - policy := `{"Version": "2012-10-17","Statement": [{"Action": ["s3:GetObject"],"Effect": "Allow","Resource": ["arn:aws:s3:::my-bucketname/*"],"Sid": ""}]}` - - if err = madmClnt.AddUserPolicy("newuser", policy); err != nil { + if err = madmClnt.SetUserPolicy("newuser", "get-only"); err != nil { log.Fatalln(err) } ``` diff --git a/pkg/madmin/examples/add-user-and-policy.go b/pkg/madmin/examples/add-user-and-policy.go index be0e50891..e0d2b2b20 100644 --- a/pkg/madmin/examples/add-user-and-policy.go +++ b/pkg/madmin/examples/add-user-and-policy.go @@ -46,7 +46,11 @@ func main() { // Create policy policy := `{"Version": "2012-10-17","Statement": [{"Action": ["s3:GetObject"],"Effect": "Allow","Resource": ["arn:aws:s3:::my-bucketname/*"],"Sid": ""}]}` - if err = madmClnt.AddUserPolicy("newuser", policy); err != nil { + if err = madmClnt.AddCannedPolicy("get-only", policy); err != nil { + log.Fatalln(err) + } + + if err = madmClnt.SetUserPolicy("newuser", "get-only"); err != nil { log.Fatalln(err) } } diff --git a/pkg/madmin/policy-commands.go b/pkg/madmin/policy-commands.go new file mode 100644 index 000000000..210a994c7 --- /dev/null +++ b/pkg/madmin/policy-commands.go @@ -0,0 +1,107 @@ +/* + * Minio Cloud Storage, (C) 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 madmin + +import ( + "encoding/json" + "io/ioutil" + "net/http" + "net/url" +) + +// ListCannedPolicies - list all configured canned policies. +func (adm *AdminClient) ListCannedPolicies() (map[string][]byte, error) { + reqData := requestData{ + relPath: "/v1/list-canned-policies", + } + + // Execute GET on /minio/admin/v1/list-canned-policies + resp, err := adm.executeMethod("GET", reqData) + + defer closeResponse(resp) + if err != nil { + return nil, err + } + + if resp.StatusCode != http.StatusOK { + return nil, httpRespToErrorResponse(resp) + } + + respBytes, err := ioutil.ReadAll(resp.Body) + if err != nil { + return nil, err + } + + var policies = make(map[string][]byte) + if err = json.Unmarshal(respBytes, &policies); err != nil { + return nil, err + } + + return policies, nil +} + +// RemoveCannedPolicy - remove a policy for a canned. +func (adm *AdminClient) RemoveCannedPolicy(policyName string) error { + queryValues := url.Values{} + queryValues.Set("name", policyName) + + reqData := requestData{ + relPath: "/v1/remove-canned-policy", + queryValues: queryValues, + } + + // Execute DELETE on /minio/admin/v1/remove-canned-policy to remove policy. + resp, err := adm.executeMethod("DELETE", reqData) + + defer closeResponse(resp) + if err != nil { + return err + } + + if resp.StatusCode != http.StatusOK { + return httpRespToErrorResponse(resp) + } + + return nil +} + +// AddCannedPolicy - adds a policy for a canned. +func (adm *AdminClient) AddCannedPolicy(policyName, policy string) error { + queryValues := url.Values{} + queryValues.Set("name", policyName) + + reqData := requestData{ + relPath: "/v1/add-canned-policy", + queryValues: queryValues, + content: []byte(policy), + } + + // Execute PUT on /minio/admin/v1/add-canned-policy to set policy. + resp, err := adm.executeMethod("PUT", reqData) + + defer closeResponse(resp) + if err != nil { + return err + } + + if resp.StatusCode != http.StatusOK { + return httpRespToErrorResponse(resp) + } + + return nil +} diff --git a/pkg/madmin/user-commands.go b/pkg/madmin/user-commands.go index a31df5193..e0337b709 100644 --- a/pkg/madmin/user-commands.go +++ b/pkg/madmin/user-commands.go @@ -34,8 +34,9 @@ const ( // UserInfo carries information about long term users. type UserInfo struct { - SecretKey string `json:"secretKey,omitempty"` - Status AccountStatus `json:"status"` + SecretKey string `json:"secretKey,omitempty"` + PolicyName string `json:"policyName,omitempty"` + Status AccountStatus `json:"status"` } // RemoveUser - remove a user. @@ -137,43 +138,18 @@ func (adm *AdminClient) AddUser(accessKey, secretKey string) error { return adm.SetUser(accessKey, secretKey, AccountEnabled) } -// RemoveUserPolicy - remove a policy for a user. -func (adm *AdminClient) RemoveUserPolicy(accessKey string) error { +// SetUserPolicy - adds a policy for a user. +func (adm *AdminClient) SetUserPolicy(accessKey, policyName string) error { queryValues := url.Values{} queryValues.Set("accessKey", accessKey) + queryValues.Set("name", policyName) reqData := requestData{ - relPath: "/v1/remove-user-policy", + relPath: "/v1/set-user-policy", queryValues: queryValues, } - // Execute DELETE on /minio/admin/v1/remove-user-policy to remove policy. - resp, err := adm.executeMethod("DELETE", reqData) - - defer closeResponse(resp) - if err != nil { - return err - } - - if resp.StatusCode != http.StatusOK { - return httpRespToErrorResponse(resp) - } - - return nil -} - -// AddUserPolicy - adds a policy for a user. -func (adm *AdminClient) AddUserPolicy(accessKey, policy string) error { - queryValues := url.Values{} - queryValues.Set("accessKey", accessKey) - - reqData := requestData{ - relPath: "/v1/add-user-policy", - queryValues: queryValues, - content: []byte(policy), - } - - // Execute PUT on /minio/admin/v1/add-user-policy to set policy. + // Execute PUT on /minio/admin/v1/set-user-policy to set policy. resp, err := adm.executeMethod("PUT", reqData) defer closeResponse(resp)