From bf9b619d8656371752f8d34652159db9a28b761a Mon Sep 17 00:00:00 2001 From: Aditya Manthramurthy Date: Tue, 13 Aug 2019 13:41:06 -0700 Subject: [PATCH] Set the policy mapping for a user or group (#8036) Add API to set policy mapping for a user or group Contains a breaking Admin APIs change. - Also enforce all applicable policies - Removes the previous /set-user-policy API Bump up peerRESTVersion Add get user info API to show groups of a user --- cmd/admin-handlers.go | 51 +++++++++---- cmd/admin-router.go | 10 ++- cmd/iam-etcd-store.go | 11 +++ cmd/iam.go | 130 +++++++++++++++++++++++++++------- cmd/notification.go | 15 ++++ cmd/peer-rest-client.go | 16 +++++ cmd/peer-rest-common.go | 25 ++++--- cmd/peer-rest-server.go | 30 ++++++++ pkg/madmin/policy-commands.go | 29 ++++++++ pkg/madmin/user-commands.go | 62 +++++++++------- 10 files changed, 301 insertions(+), 78 deletions(-) diff --git a/cmd/admin-handlers.go b/cmd/admin-handlers.go index 46cba6872..a392f9c65 100644 --- a/cmd/admin-handlers.go +++ b/cmd/admin-handlers.go @@ -1020,6 +1020,33 @@ func (a adminAPIHandlers) ListUsers(w http.ResponseWriter, r *http.Request) { writeSuccessResponseJSON(w, econfigData) } +// GetUserInfo - GET /minio/admin/v1/user-info +func (a adminAPIHandlers) GetUserInfo(w http.ResponseWriter, r *http.Request) { + ctx := newContext(r, w, "GetUserInfo") + + objectAPI := validateAdminReq(ctx, w, r) + if objectAPI == nil { + return + } + + vars := mux.Vars(r) + name := vars["accessKey"] + + userInfo, err := globalIAMSys.GetUserInfo(name) + if err != nil { + writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL) + return + } + + data, err := json.Marshal(userInfo) + if err != nil { + writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL) + return + } + + writeSuccessResponseJSON(w, data) +} + // UpdateGroupMembers - PUT /minio/admin/v1/update-group-members func (a adminAPIHandlers) UpdateGroupMembers(w http.ResponseWriter, r *http.Request) { ctx := newContext(r, w, "UpdateGroupMembers") @@ -1353,9 +1380,9 @@ func (a adminAPIHandlers) AddCannedPolicy(w http.ResponseWriter, r *http.Request } } -// 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") +// SetPolicyForUserOrGroup - PUT /minio/admin/v1/set-policy?policy=xxx&user-or-group=?[&is-group] +func (a adminAPIHandlers) SetPolicyForUserOrGroup(w http.ResponseWriter, r *http.Request) { + ctx := newContext(r, w, "SetPolicyForUserOrGroup") objectAPI := validateAdminReq(ctx, w, r) if objectAPI == nil { @@ -1363,8 +1390,9 @@ func (a adminAPIHandlers) SetUserPolicy(w http.ResponseWriter, r *http.Request) } vars := mux.Vars(r) - accessKey := vars["accessKey"] - policyName := vars["name"] + policyName := vars["policyName"] + entityName := vars["userOrGroup"] + isGroup := vars["isGroup"] == "true" // Deny if WORM is enabled if globalWORMEnabled { @@ -1372,18 +1400,13 @@ func (a adminAPIHandlers) SetUserPolicy(w http.ResponseWriter, r *http.Request) return } - // Custom IAM policies not allowed for admin user. - if accessKey == globalServerConfig.GetCredential().AccessKey { - writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrInvalidRequest), r.URL) - return - } - - if err := globalIAMSys.PolicyDBSet(accessKey, policyName, false); err != nil { + if err := globalIAMSys.PolicyDBSet(entityName, policyName, isGroup); err != nil { writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL) + return } - // Notify all other Minio peers to reload user - for _, nerr := range globalNotificationSys.LoadUser(accessKey, false) { + // Notify all other MinIO peers to reload policy + for _, nerr := range globalNotificationSys.LoadPolicyMapping(entityName, isGroup) { if nerr.Err != nil { logger.GetReqInfo(ctx).SetTags("peerAddress", nerr.Host.String()) logger.LogIf(ctx, nerr.Err) diff --git a/cmd/admin-router.go b/cmd/admin-router.go index 9ff9d4eb3..23c216726 100644 --- a/cmd/admin-router.go +++ b/cmd/admin-router.go @@ -96,20 +96,26 @@ func registerAdminRouter(router *mux.Router, enableConfigOps, enableIAMOps bool) // Add user IAM adminV1Router.Methods(http.MethodPut).Path("/add-user").HandlerFunc(httpTraceHdrs(adminAPI.AddUser)).Queries("accessKey", "{accessKey:.*}") - adminV1Router.Methods(http.MethodPut).Path("/set-user-policy").HandlerFunc(httpTraceHdrs(adminAPI.SetUserPolicy)). - Queries("accessKey", "{accessKey:.*}").Queries("name", "{name:.*}") adminV1Router.Methods(http.MethodPut).Path("/set-user-status").HandlerFunc(httpTraceHdrs(adminAPI.SetUserStatus)). Queries("accessKey", "{accessKey:.*}").Queries("status", "{status:.*}") // Remove policy IAM adminV1Router.Methods(http.MethodDelete).Path("/remove-canned-policy").HandlerFunc(httpTraceHdrs(adminAPI.RemoveCannedPolicy)).Queries("name", "{name:.*}") + // Set user or group policy + adminV1Router.Methods(http.MethodPut).Path("/set-user-or-group-policy"). + HandlerFunc(httpTraceHdrs(adminAPI.SetPolicyForUserOrGroup)). + Queries("policyName", "{policyName:.*}", "userOrGroup", "{userOrGroup:.*}", "isGroup", "{isGroup:true|false}") + // Remove user IAM adminV1Router.Methods(http.MethodDelete).Path("/remove-user").HandlerFunc(httpTraceHdrs(adminAPI.RemoveUser)).Queries("accessKey", "{accessKey:.*}") // List users adminV1Router.Methods(http.MethodGet).Path("/list-users").HandlerFunc(httpTraceHdrs(adminAPI.ListUsers)) + // User info + adminV1Router.Methods(http.MethodGet).Path("/user-info").HandlerFunc(httpTraceHdrs(adminAPI.GetUserInfo)).Queries("accessKey", "{accessKey:.*}") + // Add/Remove members from group adminV1Router.Methods(http.MethodPut).Path("/update-group-members").HandlerFunc(httpTraceHdrs(adminAPI.UpdateGroupMembers)) diff --git a/cmd/iam-etcd-store.go b/cmd/iam-etcd-store.go index b04449175..6be1d824d 100644 --- a/cmd/iam-etcd-store.go +++ b/cmd/iam-etcd-store.go @@ -530,6 +530,7 @@ func (ies *IAMEtcdStore) reloadFromEvent(sys *IAMSys, event *etcd.Event) { policyPrefix := strings.HasPrefix(string(event.Kv.Key), iamConfigPoliciesPrefix) policyDBUsersPrefix := strings.HasPrefix(string(event.Kv.Key), iamConfigPolicyDBUsersPrefix) policyDBSTSUsersPrefix := strings.HasPrefix(string(event.Kv.Key), iamConfigPolicyDBSTSUsersPrefix) + policyDBGroupsPrefix := strings.HasPrefix(string(event.Kv.Key), iamConfigPolicyDBGroupsPrefix) switch { case eventCreate: @@ -563,6 +564,11 @@ func (ies *IAMEtcdStore) reloadFromEvent(sys *IAMSys, event *etcd.Event) { iamConfigPolicyDBSTSUsersPrefix) user := strings.TrimSuffix(policyMapFile, ".json") ies.loadMappedPolicy(user, true, false, sys.iamUserPolicyMap) + case policyDBGroupsPrefix: + policyMapFile := strings.TrimPrefix(string(event.Kv.Key), + iamConfigPolicyDBGroupsPrefix) + user := strings.TrimSuffix(policyMapFile, ".json") + ies.loadMappedPolicy(user, false, true, sys.iamGroupPolicyMap) } case eventDelete: switch { @@ -594,6 +600,11 @@ func (ies *IAMEtcdStore) reloadFromEvent(sys *IAMSys, event *etcd.Event) { iamConfigPolicyDBSTSUsersPrefix) user := strings.TrimSuffix(policyMapFile, ".json") delete(sys.iamUserPolicyMap, user) + case policyDBGroupsPrefix: + policyMapFile := strings.TrimPrefix(string(event.Kv.Key), + iamConfigPolicyDBGroupsPrefix) + user := strings.TrimSuffix(policyMapFile, ".json") + delete(sys.iamGroupPolicyMap, user) } } } diff --git a/cmd/iam.go b/cmd/iam.go index 9c35a4bd2..19804b60b 100644 --- a/cmd/iam.go +++ b/cmd/iam.go @@ -259,6 +259,33 @@ func (sys *IAMSys) LoadPolicy(objAPI ObjectLayer, policyName string) error { return nil } +// LoadPolicyMapping - loads the mapped policy for a user or group +// from storage into server memory. +func (sys *IAMSys) LoadPolicyMapping(objAPI ObjectLayer, userOrGroup string, isGroup bool) error { + if objAPI == nil { + return errInvalidArgument + } + + sys.Lock() + defer sys.Unlock() + + if globalEtcdClient == nil { + var err error + if isGroup { + err = sys.store.loadMappedPolicy(userOrGroup, false, isGroup, sys.iamGroupPolicyMap) + } else { + err = sys.store.loadMappedPolicy(userOrGroup, false, isGroup, sys.iamUserPolicyMap) + } + + // Ignore policy not mapped error + if err != nil && err != errConfigNotFound { + return err + } + } + // When etcd is set, we use watch APIs so this code is not needed. + return nil +} + // LoadUser - reloads a specific user from backend disks or etcd. func (sys *IAMSys) LoadUser(objAPI ObjectLayer, accessKey string, isSTS bool) error { if objAPI == nil { @@ -516,6 +543,29 @@ func (sys *IAMSys) ListUsers() (map[string]madmin.UserInfo, error) { return users, nil } +// GetUserInfo - get info on a user. +func (sys *IAMSys) GetUserInfo(name string) (u madmin.UserInfo, err error) { + objectAPI := newObjectLayerFn() + if objectAPI == nil { + return u, errServerNotInitialized + } + + sys.RLock() + defer sys.RUnlock() + + creds, found := sys.iamUsersMap[name] + if !found { + return u, errNoSuchUser + } + + u = madmin.UserInfo{ + PolicyName: sys.iamUserPolicyMap[name].Policy, + Status: madmin.AccountStatus(creds.Status), + MemberOf: sys.iamUserGroupMemberships[name].ToSlice(), + } + return u, nil +} + // SetUserStatus - sets current user status, supports disabled or enabled. func (sys *IAMSys) SetUserStatus(accessKey string, status madmin.AccountStatus) error { objectAPI := newObjectLayerFn() @@ -776,6 +826,16 @@ func (sys *IAMSys) SetGroupStatus(group string, enabled bool) error { // GetGroupDescription - builds up group description func (sys *IAMSys) GetGroupDescription(group string) (gd madmin.GroupDesc, err error) { + ps, err := sys.PolicyDBGet(group, true) + if err != nil { + return gd, err + } + // A group may be mapped to at most one policy. + policy := "" + if len(ps) > 0 { + policy = ps[0] + } + sys.RLock() defer sys.RUnlock() @@ -784,17 +844,6 @@ func (sys *IAMSys) GetGroupDescription(group string) (gd madmin.GroupDesc, err e return gd, errNoSuchGroup } - var p []string - p, err = sys.policyDBGet(group, true) - if err != nil { - return gd, err - } - - policy := "" - if len(p) > 0 { - policy = p[0] - } - return madmin.GroupDesc{ Name: group, Status: gi.Status, @@ -837,21 +886,28 @@ func (sys *IAMSys) policyDBSet(objectAPI ObjectLayer, name, policy string, isSTS if name == "" || policy == "" { return errInvalidArgument } - if _, ok := sys.iamUsersMap[name]; !ok { - return errNoSuchUser - } if _, ok := sys.iamPolicyDocsMap[policy]; !ok { return errNoSuchPolicy } - if _, ok := sys.iamUsersMap[name]; !ok { - return errNoSuchUser + if !isGroup { + if _, ok := sys.iamUsersMap[name]; !ok { + return errNoSuchUser + } + } else { + if _, ok := sys.iamGroupsMap[name]; !ok { + return errNoSuchGroup + } } mp := newMappedPolicy(policy) if err := sys.store.saveMappedPolicy(name, isSTS, isGroup, mp); err != nil { return err } - sys.iamUserPolicyMap[name] = mp + if !isGroup { + sys.iamUserPolicyMap[name] = mp + } else { + sys.iamGroupPolicyMap[name] = mp + } return nil } @@ -991,18 +1047,42 @@ func (sys *IAMSys) IsAllowed(args iampolicy.Args) bool { return sys.IsAllowedSTS(args) } + // Policies don't apply to the owner. + if args.IsOwner { + return true + } + + policies, err := sys.PolicyDBGet(args.AccountName, false) + if err != nil { + logger.LogIf(context.Background(), err) + return false + } + + if len(policies) == 0 { + // No policy found. + return false + } + + // Policies were found, evaluate all of them. sys.RLock() defer sys.RUnlock() - // If policy is available for given user, check the policy. - if mp, found := sys.iamUserPolicyMap[args.AccountName]; found { - p, ok := sys.iamPolicyDocsMap[mp.Policy] - return ok && p.IsAllowed(args) + var availablePolicies []iampolicy.Policy + for _, pname := range policies { + p, found := sys.iamPolicyDocsMap[pname] + if found { + availablePolicies = append(availablePolicies, p) + } } - - // As policy is not available and OPA is not configured, - // return the owner value. - return args.IsOwner + if len(availablePolicies) == 0 { + return false + } + combinedPolicy := availablePolicies[0] + for i := 1; i < len(availablePolicies); i++ { + combinedPolicy.Statements = append(combinedPolicy.Statements, + availablePolicies[i].Statements...) + } + return combinedPolicy.IsAllowed(args) } // Set default canned policies only if not already overridden by users. diff --git a/cmd/notification.go b/cmd/notification.go index a8b1d59e9..7b2952ce7 100644 --- a/cmd/notification.go +++ b/cmd/notification.go @@ -189,6 +189,21 @@ func (sys *NotificationSys) LoadPolicy(policyName string) []NotificationPeerErr return ng.Wait() } +// LoadPolicyMapping - reloads a policy mapping across all peers +func (sys *NotificationSys) LoadPolicyMapping(userOrGroup string, isGroup bool) []NotificationPeerErr { + ng := WithNPeers(len(sys.peerClients)) + for idx, client := range sys.peerClients { + if client == nil { + continue + } + client := client + ng.Go(context.Background(), func() error { + return client.LoadPolicyMapping(userOrGroup, isGroup) + }, idx, *client.host) + } + return ng.Wait() +} + // DeleteUser - deletes a specific user across all peers func (sys *NotificationSys) DeleteUser(accessKey string) []NotificationPeerErr { ng := WithNPeers(len(sys.peerClients)) diff --git a/cmd/peer-rest-client.go b/cmd/peer-rest-client.go index 05ed87190..352171b98 100644 --- a/cmd/peer-rest-client.go +++ b/cmd/peer-rest-client.go @@ -406,6 +406,22 @@ func (client *peerRESTClient) LoadPolicy(policyName string) (err error) { return nil } +// LoadPolicyMapping - reload a specific policy mapping +func (client *peerRESTClient) LoadPolicyMapping(userOrGroup string, isGroup bool) error { + values := make(url.Values) + values.Set(peerRESTUserOrGroup, userOrGroup) + if isGroup { + values.Set(peerRESTIsGroup, "") + } + + respBody, err := client.call(peerRESTMethodLoadPolicyMapping, values, nil, -1) + if err != nil { + return err + } + defer http.DrainBody(respBody) + return nil +} + // DeleteUser - delete a specific user. func (client *peerRESTClient) DeleteUser(accessKey string) (err error) { values := make(url.Values) diff --git a/cmd/peer-rest-common.go b/cmd/peer-rest-common.go index aa1982183..5faba48e6 100644 --- a/cmd/peer-rest-common.go +++ b/cmd/peer-rest-common.go @@ -16,7 +16,7 @@ package cmd -const peerRESTVersion = "v3" +const peerRESTVersion = "v4" const peerRESTPath = minioReservedBucketPath + "/peer/" + peerRESTVersion const ( @@ -33,6 +33,7 @@ const ( peerRESTMethodLoadUser = "loaduser" peerRESTMethodDeleteUser = "deleteuser" peerRESTMethodLoadPolicy = "loadpolicy" + peerRESTMethodLoadPolicyMapping = "loadpolicymapping" peerRESTMethodDeletePolicy = "deletepolicy" peerRESTMethodLoadUsers = "loadusers" peerRESTMethodLoadGroup = "loadgroup" @@ -50,14 +51,16 @@ const ( ) const ( - peerRESTBucket = "bucket" - peerRESTUser = "user" - peerRESTGroup = "group" - peerRESTUserTemp = "user-temp" - peerRESTPolicy = "policy" - peerRESTSignal = "signal" - peerRESTProfiler = "profiler" - peerRESTDryRun = "dry-run" - peerRESTTraceAll = "all" - peerRESTTraceErr = "err" + peerRESTBucket = "bucket" + peerRESTUser = "user" + peerRESTGroup = "group" + peerRESTUserTemp = "user-temp" + peerRESTPolicy = "policy" + peerRESTUserOrGroup = "user-or-group" + peerRESTIsGroup = "is-group" + peerRESTSignal = "signal" + peerRESTProfiler = "profiler" + peerRESTDryRun = "dry-run" + peerRESTTraceAll = "all" + peerRESTTraceErr = "err" ) diff --git a/cmd/peer-rest-server.go b/cmd/peer-rest-server.go index e2e94c41c..5ca851a12 100644 --- a/cmd/peer-rest-server.go +++ b/cmd/peer-rest-server.go @@ -176,6 +176,35 @@ func (s *peerRESTServer) LoadPolicyHandler(w http.ResponseWriter, r *http.Reques w.(http.Flusher).Flush() } +// LoadPolicyMappingHandler - reloads a policy mapping on the server. +func (s *peerRESTServer) LoadPolicyMappingHandler(w http.ResponseWriter, r *http.Request) { + if !s.IsValid(w, r) { + s.writeErrorResponse(w, errors.New("Invalid request")) + return + } + + objAPI := newObjectLayerFn() + if objAPI == nil { + s.writeErrorResponse(w, errServerNotInitialized) + return + } + + vars := mux.Vars(r) + userOrGroup := vars[peerRESTUserOrGroup] + if userOrGroup == "" { + s.writeErrorResponse(w, errors.New("user-or-group is missing")) + return + } + _, isGroup := vars[peerRESTIsGroup] + + if err := globalIAMSys.LoadPolicyMapping(objAPI, userOrGroup, isGroup); err != nil { + s.writeErrorResponse(w, err) + return + } + + w.(http.Flusher).Flush() +} + // DeleteUserHandler - deletes a user on the server. func (s *peerRESTServer) DeleteUserHandler(w http.ResponseWriter, r *http.Request) { if !s.IsValid(w, r) { @@ -831,6 +860,7 @@ func registerPeerRESTHandlers(router *mux.Router) { subrouter.Methods(http.MethodPost).Path(SlashSeparator + peerRESTMethodDeletePolicy).HandlerFunc(httpTraceAll(server.LoadPolicyHandler)).Queries(restQueries(peerRESTPolicy)...) subrouter.Methods(http.MethodPost).Path(SlashSeparator + peerRESTMethodLoadPolicy).HandlerFunc(httpTraceAll(server.LoadPolicyHandler)).Queries(restQueries(peerRESTPolicy)...) + subrouter.Methods(http.MethodPost).Path(SlashSeparator + peerRESTMethodLoadPolicyMapping).HandlerFunc(httpTraceAll(server.LoadPolicyMappingHandler)).Queries(restQueries(peerRESTUserOrGroup)...) subrouter.Methods(http.MethodPost).Path(SlashSeparator + peerRESTMethodDeleteUser).HandlerFunc(httpTraceAll(server.LoadUserHandler)).Queries(restQueries(peerRESTUser)...) subrouter.Methods(http.MethodPost).Path(SlashSeparator + peerRESTMethodLoadUser).HandlerFunc(httpTraceAll(server.LoadUserHandler)).Queries(restQueries(peerRESTUser, peerRESTUserTemp)...) subrouter.Methods(http.MethodPost).Path(SlashSeparator + peerRESTMethodLoadUsers).HandlerFunc(httpTraceAll(server.LoadUsersHandler)) diff --git a/pkg/madmin/policy-commands.go b/pkg/madmin/policy-commands.go index 453be9cdd..36e7e4c93 100644 --- a/pkg/madmin/policy-commands.go +++ b/pkg/madmin/policy-commands.go @@ -105,3 +105,32 @@ func (adm *AdminClient) AddCannedPolicy(policyName, policy string) error { return nil } + +// SetPolicy - sets the policy for a user or a group. +func (adm *AdminClient) SetPolicy(policyName, entityName string, isGroup bool) error { + queryValues := url.Values{} + queryValues.Set("policyName", policyName) + queryValues.Set("userOrGroup", entityName) + groupStr := "false" + if isGroup { + groupStr = "true" + } + queryValues.Set("isGroup", groupStr) + + reqData := requestData{ + relPath: "/v1/set-user-or-group-policy", + queryValues: queryValues, + } + + // Execute PUT on /minio/admin/v1/set-user-or-group-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 677ec6413..471725bd7 100644 --- a/pkg/madmin/user-commands.go +++ b/pkg/madmin/user-commands.go @@ -19,6 +19,7 @@ package madmin import ( "encoding/json" + "io/ioutil" "net/http" "net/url" @@ -39,6 +40,7 @@ type UserInfo struct { SecretKey string `json:"secretKey,omitempty"` PolicyName string `json:"policyName,omitempty"` Status AccountStatus `json:"status"` + MemberOf []string `json:"memberOf,omitempty"` } // RemoveUser - remove a user. @@ -97,6 +99,40 @@ func (adm *AdminClient) ListUsers() (map[string]UserInfo, error) { return users, nil } +// GetUserInfo - get info on a user +func (adm *AdminClient) GetUserInfo(name string) (u UserInfo, err error) { + queryValues := url.Values{} + queryValues.Set("accessKey", name) + + reqData := requestData{ + relPath: "/v1/user-info", + queryValues: queryValues, + } + + // Execute GET on /minio/admin/v1/user-info + resp, err := adm.executeMethod("GET", reqData) + + defer closeResponse(resp) + if err != nil { + return u, err + } + + if resp.StatusCode != http.StatusOK { + return u, httpRespToErrorResponse(resp) + } + + b, err := ioutil.ReadAll(resp.Body) + if err != nil { + return u, err + } + + if err = json.Unmarshal(b, &u); err != nil { + return u, err + } + + return u, nil +} + // SetUser - sets a user info. func (adm *AdminClient) SetUser(accessKey, secretKey string, status AccountStatus) error { @@ -149,32 +185,6 @@ func (adm *AdminClient) AddUser(accessKey, secretKey string) error { return adm.SetUser(accessKey, secretKey, AccountEnabled) } -// 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/set-user-policy", - queryValues: queryValues, - } - - // Execute PUT on /minio/admin/v1/set-user-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 -} - // SetUserStatus - adds a status for a user. func (adm *AdminClient) SetUserStatus(accessKey string, status AccountStatus) error { queryValues := url.Values{}