From 20766069a81408381d3039c78e414607988e6d0b Mon Sep 17 00:00:00 2001 From: Anis Elleuch Date: Fri, 24 Apr 2020 20:10:09 +0100 Subject: [PATCH] add list/delete API service accounts admin API (#9402) --- cmd/admin-handlers-users.go | 145 ++++++++++++++---- cmd/admin-router.go | 2 + cmd/api-errors.go | 17 +- cmd/iam.go | 99 ++++++++++++ cmd/notification.go | 30 ++++ cmd/peer-rest-client.go | 26 ++++ cmd/peer-rest-common.go | 2 + cmd/peer-rest-server.go | 68 ++++++++ cmd/typed-errors.go | 3 + ...ount-and-policy.go => service-accounts.go} | 19 ++- pkg/madmin/user-commands.go | 75 +++++++-- 11 files changed, 436 insertions(+), 50 deletions(-) rename pkg/madmin/examples/{add-service-account-and-policy.go => service-accounts.go} (82%) diff --git a/cmd/admin-handlers-users.go b/cmd/admin-handlers-users.go index a7454c842..691e337f3 100644 --- a/cmd/admin-handlers-users.go +++ b/cmd/admin-handlers-users.go @@ -397,38 +397,21 @@ func (a adminAPIHandlers) AddServiceAccount(w http.ResponseWriter, r *http.Reque } password := cred.SecretKey - configBytes, err := madmin.DecryptData(password, io.LimitReader(r.Body, r.ContentLength)) + reqBytes, err := madmin.DecryptData(password, io.LimitReader(r.Body, r.ContentLength)) if err != nil { writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErrWithErr(ErrAdminConfigBadJSON, err), r.URL) return } var createReq madmin.AddServiceAccountReq - if err = json.Unmarshal(configBytes, &createReq); err != nil { + if err = json.Unmarshal(reqBytes, &createReq); err != nil { writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErrWithErr(ErrAdminConfigBadJSON, err), r.URL) return } - if createReq.Parent == "" { - apiErr := APIError{ - Code: "XMinioAdminInvalidArgument", - Description: "Service account parent cannot be empty", - HTTPStatusCode: http.StatusBadRequest, - } - writeErrorResponseJSON(ctx, w, apiErr, r.URL) - return - } - - // Disallow creating service accounts by root user as well. - if createReq.Parent == globalActiveCred.AccessKey { - writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAddServiceAccountInvalidArgument), r.URL) - return - } - - // Disallow creating service accounts by users who are not the requested parent. - // this restriction is not required for Owner account i.e root user. - if createReq.Parent != cred.AccessKey && !owner { - writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAddServiceAccountInvalidParent), r.URL) + // Disallow creating service accounts by root user. + if owner { + writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAdminAccountNotEligible), r.URL) return } @@ -438,14 +421,14 @@ func (a adminAPIHandlers) AddServiceAccount(w http.ResponseWriter, r *http.Reque return } - creds, err := globalIAMSys.NewServiceAccount(ctx, createReq.Parent, createReq.Policy) + newCred, err := globalIAMSys.NewServiceAccount(ctx, cred.AccessKey, createReq.Policy) if err != nil { writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL) return } - // Notify all other Minio peers to reload user - for _, nerr := range globalNotificationSys.LoadUser(creds.AccessKey, false) { + // Notify all other Minio peers to reload user the service account + for _, nerr := range globalNotificationSys.LoadServiceAccount(newCred.AccessKey) { if nerr.Err != nil { logger.GetReqInfo(ctx).SetTags("peerAddress", nerr.Host.String()) logger.LogIf(ctx, nerr.Err) @@ -454,8 +437,8 @@ func (a adminAPIHandlers) AddServiceAccount(w http.ResponseWriter, r *http.Reque var createResp = madmin.AddServiceAccountResp{ Credentials: auth.Credentials{ - AccessKey: creds.AccessKey, - SecretKey: creds.SecretKey, + AccessKey: newCred.AccessKey, + SecretKey: newCred.SecretKey, }, } @@ -465,13 +448,117 @@ func (a adminAPIHandlers) AddServiceAccount(w http.ResponseWriter, r *http.Reque return } - econfigData, err := madmin.EncryptData(password, data) + encryptedData, err := madmin.EncryptData(password, data) if err != nil { writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL) return } - writeSuccessResponseJSON(w, econfigData) + writeSuccessResponseJSON(w, encryptedData) +} + +// ListServiceAccounts - GET /minio/admin/v3/list-service-accounts +func (a adminAPIHandlers) ListServiceAccounts(w http.ResponseWriter, r *http.Request) { + ctx := newContext(r, w, "ListServiceAccounts") + + // Get current object layer instance. + objectAPI := newObjectLayerWithoutSafeModeFn() + if objectAPI == nil || globalNotificationSys == nil || globalIAMSys == nil { + writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrServerNotInitialized), r.URL) + return + } + + cred, _, owner, s3Err := validateAdminSignature(ctx, r, "") + if s3Err != ErrNone { + writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(s3Err), r.URL) + return + } + + // Disallow creating service accounts by root user. + if owner { + writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAdminAccountNotEligible), r.URL) + return + } + + serviceAccounts, err := globalIAMSys.ListServiceAccounts(ctx, cred.AccessKey) + if err != nil { + writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL) + return + } + + var listResp = madmin.ListServiceAccountsResp{ + Accounts: serviceAccounts, + } + + data, err := json.Marshal(listResp) + if err != nil { + writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL) + return + } + + encryptedData, err := madmin.EncryptData(cred.SecretKey, data) + if err != nil { + writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL) + return + } + + writeSuccessResponseJSON(w, encryptedData) +} + +// DeleteServiceAccount - DELETE /minio/admin/v3/delete-service-account +func (a adminAPIHandlers) DeleteServiceAccount(w http.ResponseWriter, r *http.Request) { + ctx := newContext(r, w, "DeleteServiceAccount") + + // Get current object layer instance. + objectAPI := newObjectLayerWithoutSafeModeFn() + if objectAPI == nil || globalNotificationSys == nil || globalIAMSys == nil { + writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrServerNotInitialized), r.URL) + return + } + + cred, _, owner, s3Err := validateAdminSignature(ctx, r, "") + if s3Err != ErrNone { + writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(s3Err), r.URL) + return + } + + // Disallow creating service accounts by root user. + if owner { + writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAdminAccountNotEligible), r.URL) + return + } + + // Deny if WORM is enabled + if globalWORMEnabled { + writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrMethodNotAllowed), r.URL) + return + } + + serviceAccount := mux.Vars(r)["accessKey"] + if serviceAccount == "" { + writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAdminInvalidArgument), r.URL) + return + } + + user, err := globalIAMSys.GetServiceAccountParent(ctx, serviceAccount) + if err != nil { + writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL) + return + } + + if cred.AccessKey != user { + // The service account belongs to another user but return not found error to mitigate brute force attacks. + writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrServiceAccountNotFound), r.URL) + return + } + + err = globalIAMSys.DeleteServiceAccount(ctx, serviceAccount) + if err != nil { + writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL) + return + } + + writeSuccessNoContent(w) } // InfoCannedPolicyV2 - GET /minio/admin/v2/info-canned-policy?name={policyName} diff --git a/cmd/admin-router.go b/cmd/admin-router.go index e45d57e2d..5119300b4 100644 --- a/cmd/admin-router.go +++ b/cmd/admin-router.go @@ -121,6 +121,8 @@ func registerAdminRouter(router *mux.Router, enableConfigOps, enableIAMOps bool) // Service accounts ops adminRouter.Methods(http.MethodPut).Path(adminVersion + "/add-service-account").HandlerFunc(httpTraceHdrs(adminAPI.AddServiceAccount)) + adminRouter.Methods(http.MethodGet).Path(adminVersion + "/list-service-accounts").HandlerFunc(httpTraceHdrs(adminAPI.ListServiceAccounts)) + adminRouter.Methods(http.MethodDelete).Path(adminVersion+"/delete-service-account").HandlerFunc(httpTraceHdrs(adminAPI.DeleteServiceAccount)).Queries("accessKey", "{accessKey:.*}") if adminVersion == adminAPIVersionV2Prefix { // Info policy IAM v2 diff --git a/cmd/api-errors.go b/cmd/api-errors.go index fea254e09..204f6b90f 100644 --- a/cmd/api-errors.go +++ b/cmd/api-errors.go @@ -334,8 +334,8 @@ const ( ErrAdminProfilerNotEnabled ErrInvalidDecompressedSize ErrAddUserInvalidArgument - ErrAddServiceAccountInvalidArgument - ErrAddServiceAccountInvalidParent + ErrAdminAccountNotEligible + ErrServiceAccountNotFound ErrPostPolicyConditionInvalidFormat ) @@ -1599,17 +1599,16 @@ var errorCodes = errorCodeMap{ Description: "User is not allowed to be same as admin access key", HTTPStatusCode: http.StatusConflict, }, - ErrAddServiceAccountInvalidArgument: { + ErrAdminAccountNotEligible: { Code: "XMinioInvalidIAMCredentials", - Description: "Creating service accounts for admin access key is not allowed", + Description: "The administrator key is not eligible for this operation", HTTPStatusCode: http.StatusConflict, }, - ErrAddServiceAccountInvalidParent: { - Code: "XMinioInvalidIAMCredentialsParent", - Description: "Creating service accounts for other users is not allowed", - HTTPStatusCode: http.StatusConflict, + ErrServiceAccountNotFound: { + Code: "XMinioInvalidIAMCredentials", + Description: "The specified service account is not found", + HTTPStatusCode: http.StatusNotFound, }, - ErrPostPolicyConditionInvalidFormat: { Code: "PostPolicyInvalidKeyName", Description: "Invalid according to Policy: Policy Condition failed", diff --git a/cmd/iam.go b/cmd/iam.go index 03136ee6c..c5e2088c0 100644 --- a/cmd/iam.go +++ b/cmd/iam.go @@ -354,6 +354,25 @@ func (sys *IAMSys) LoadUser(objAPI ObjectLayer, accessKey string, userType IAMUs return nil } +// LoadServiceAccount - reloads a specific service account from backend disks or etcd. +func (sys *IAMSys) LoadServiceAccount(accessKey string) error { + if sys == nil || sys.store == nil { + return errServerNotInitialized + } + + sys.store.lock() + defer sys.store.unlock() + + if globalEtcdClient == nil { + err := sys.store.loadUser(accessKey, srvAccUser, sys.iamUsersMap) + if err != nil { + return err + } + } + // When etcd is set, we use watch APIs so this code is not needed. + return nil +} + // Perform IAM configuration migration. func (sys *IAMSys) doIAMConfigMigration(ctx context.Context) error { return sys.store.migrateBackendFormat(ctx) @@ -832,6 +851,86 @@ func (sys *IAMSys) NewServiceAccount(ctx context.Context, parentUser string, ses return cred, nil } +// ListServiceAccounts - lists all services accounts associated to a specific user +func (sys *IAMSys) ListServiceAccounts(ctx context.Context, accessKey string) ([]string, error) { + objectAPI := newObjectLayerWithoutSafeModeFn() + if objectAPI == nil || sys == nil || sys.store == nil { + return nil, errServerNotInitialized + } + + sys.store.rlock() + defer sys.store.runlock() + + if sys.usersSysType != MinIOUsersSysType { + return nil, errIAMActionNotAllowed + } + + var serviceAccounts []string + + for k, v := range sys.iamUsersMap { + if v.IsServiceAccount() && v.ParentUser == accessKey { + serviceAccounts = append(serviceAccounts, k) + } + } + + return serviceAccounts, nil +} + +// GetServiceAccountParent - gets information about a service account +func (sys *IAMSys) GetServiceAccountParent(ctx context.Context, accessKey string) (string, error) { + objectAPI := newObjectLayerWithoutSafeModeFn() + if objectAPI == nil || sys == nil || sys.store == nil { + return "", errServerNotInitialized + } + + sys.store.rlock() + defer sys.store.runlock() + + if sys.usersSysType != MinIOUsersSysType { + return "", errIAMActionNotAllowed + } + + sa, ok := sys.iamUsersMap[accessKey] + if !ok || !sa.IsServiceAccount() { + return "", errNoSuchServiceAccount + } + + return sa.ParentUser, nil +} + +// DeleteServiceAccount - delete a service account +func (sys *IAMSys) DeleteServiceAccount(ctx context.Context, accessKey string) error { + objectAPI := newObjectLayerWithoutSafeModeFn() + if objectAPI == nil || sys == nil || sys.store == nil { + return errServerNotInitialized + } + + sys.store.lock() + defer sys.store.unlock() + + if sys.usersSysType != MinIOUsersSysType { + return errIAMActionNotAllowed + } + + sa, ok := sys.iamUsersMap[accessKey] + if !ok || !sa.IsServiceAccount() { + return errNoSuchServiceAccount + } + + // It is ok to ignore deletion error on the mapped policy + err := sys.store.deleteUserIdentity(accessKey, srvAccUser) + if err != nil { + // ignore if user is already deleted. + if err == errNoSuchUser { + return nil + } + return err + } + + delete(sys.iamUsersMap, accessKey) + return nil +} + // SetUser - set user credentials and policy. func (sys *IAMSys) SetUser(accessKey string, uinfo madmin.UserInfo) error { objectAPI := newObjectLayerWithoutSafeModeFn() diff --git a/cmd/notification.go b/cmd/notification.go index d1bd6764d..b288ec82d 100644 --- a/cmd/notification.go +++ b/cmd/notification.go @@ -243,6 +243,36 @@ func (sys *NotificationSys) LoadGroup(group string) []NotificationPeerErr { return ng.Wait() } +// DeleteServiceAccount - deletes a specific service account across all peers +func (sys *NotificationSys) DeleteServiceAccount(accessKey string) []NotificationPeerErr { + ng := WithNPeers(len(sys.peerClients)) + for idx, client := range sys.peerClients { + if client == nil { + continue + } + client := client + ng.Go(GlobalContext, func() error { + return client.DeleteServiceAccount(accessKey) + }, idx, *client.host) + } + return ng.Wait() +} + +// LoadServiceAccount - reloads a specific service account across all peers +func (sys *NotificationSys) LoadServiceAccount(accessKey string) []NotificationPeerErr { + ng := WithNPeers(len(sys.peerClients)) + for idx, client := range sys.peerClients { + if client == nil { + continue + } + client := client + ng.Go(GlobalContext, func() error { + return client.LoadServiceAccount(accessKey) + }, idx, *client.host) + } + return ng.Wait() +} + // BackgroundHealStatus - returns background heal status of all peers func (sys *NotificationSys) BackgroundHealStatus() []madmin.BgHealState { states := make([]madmin.BgHealState, len(sys.peerClients)) diff --git a/cmd/peer-rest-client.go b/cmd/peer-rest-client.go index 47db71cb5..65578569a 100644 --- a/cmd/peer-rest-client.go +++ b/cmd/peer-rest-client.go @@ -770,6 +770,19 @@ func (client *peerRESTClient) DeleteUser(accessKey string) (err error) { return nil } +// DeleteServiceAccount - delete a specific service account. +func (client *peerRESTClient) DeleteServiceAccount(accessKey string) (err error) { + values := make(url.Values) + values.Set(peerRESTUser, accessKey) + + respBody, err := client.call(peerRESTMethodDeleteServiceAccount, values, nil, -1) + if err != nil { + return + } + defer http.DrainBody(respBody) + return nil +} + // LoadUser - reload a specific user. func (client *peerRESTClient) LoadUser(accessKey string, temp bool) (err error) { values := make(url.Values) @@ -784,6 +797,19 @@ func (client *peerRESTClient) LoadUser(accessKey string, temp bool) (err error) return nil } +// LoadServiceAccount - reload a specific service account. +func (client *peerRESTClient) LoadServiceAccount(accessKey string) (err error) { + values := make(url.Values) + values.Set(peerRESTUser, accessKey) + + respBody, err := client.call(peerRESTMethodLoadServiceAccount, values, nil, -1) + if err != nil { + return + } + defer http.DrainBody(respBody) + return nil +} + // LoadGroup - send load group command to peers. func (client *peerRESTClient) LoadGroup(group string) error { values := make(url.Values) diff --git a/cmd/peer-rest-common.go b/cmd/peer-rest-common.go index 077b6dec5..5e0ba17e9 100644 --- a/cmd/peer-rest-common.go +++ b/cmd/peer-rest-common.go @@ -40,7 +40,9 @@ const ( peerRESTMethodGetLocks = "/getlocks" peerRESTMethodBucketPolicyRemove = "/removebucketpolicy" peerRESTMethodLoadUser = "/loaduser" + peerRESTMethodLoadServiceAccount = "/loadserviceaccount" peerRESTMethodDeleteUser = "/deleteuser" + peerRESTMethodDeleteServiceAccount = "/deleteserviceaccount" peerRESTMethodLoadPolicy = "/loadpolicy" peerRESTMethodLoadPolicyMapping = "/loadpolicymapping" peerRESTMethodDeletePolicy = "/deletepolicy" diff --git a/cmd/peer-rest-server.go b/cmd/peer-rest-server.go index 5e0df29dc..db89f087a 100644 --- a/cmd/peer-rest-server.go +++ b/cmd/peer-rest-server.go @@ -184,6 +184,72 @@ func (s *peerRESTServer) LoadPolicyMappingHandler(w http.ResponseWriter, r *http w.(http.Flusher).Flush() } +// DeleteServiceAccountHandler - deletes a service account on the server. +func (s *peerRESTServer) DeleteServiceAccountHandler(w http.ResponseWriter, r *http.Request) { + if !s.IsValid(w, r) { + s.writeErrorResponse(w, errors.New("Invalid request")) + return + } + + objAPI := newObjectLayerWithoutSafeModeFn() + if objAPI == nil { + s.writeErrorResponse(w, errServerNotInitialized) + return + } + + if globalIAMSys == nil { + s.writeErrorResponse(w, errServerNotInitialized) + return + } + + vars := mux.Vars(r) + accessKey := vars[peerRESTUser] + if accessKey == "" { + s.writeErrorResponse(w, errors.New("service account name is missing")) + return + } + + if err := globalIAMSys.DeleteServiceAccount(context.Background(), accessKey); err != nil { + s.writeErrorResponse(w, err) + return + } + + w.(http.Flusher).Flush() +} + +// LoadServiceAccountHandler - reloads a service account on the server. +func (s *peerRESTServer) LoadServiceAccountHandler(w http.ResponseWriter, r *http.Request) { + if !s.IsValid(w, r) { + s.writeErrorResponse(w, errors.New("Invalid request")) + return + } + + objAPI := newObjectLayerWithoutSafeModeFn() + if objAPI == nil { + s.writeErrorResponse(w, errServerNotInitialized) + return + } + + if globalIAMSys == nil { + s.writeErrorResponse(w, errServerNotInitialized) + return + } + + vars := mux.Vars(r) + accessKey := vars[peerRESTUser] + if accessKey == "" { + s.writeErrorResponse(w, errors.New("service account parameter is missing")) + return + } + + if err := globalIAMSys.LoadServiceAccount(accessKey); 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) { @@ -1165,7 +1231,9 @@ func registerPeerRESTHandlers(router *mux.Router) { subrouter.Methods(http.MethodPost).Path(peerRESTVersionPrefix + peerRESTMethodLoadPolicy).HandlerFunc(httpTraceAll(server.LoadPolicyHandler)).Queries(restQueries(peerRESTPolicy)...) subrouter.Methods(http.MethodPost).Path(peerRESTVersionPrefix + peerRESTMethodLoadPolicyMapping).HandlerFunc(httpTraceAll(server.LoadPolicyMappingHandler)).Queries(restQueries(peerRESTUserOrGroup)...) subrouter.Methods(http.MethodPost).Path(peerRESTVersionPrefix + peerRESTMethodDeleteUser).HandlerFunc(httpTraceAll(server.DeleteUserHandler)).Queries(restQueries(peerRESTUser)...) + subrouter.Methods(http.MethodPost).Path(peerRESTVersionPrefix + peerRESTMethodDeleteServiceAccount).HandlerFunc(httpTraceAll(server.DeleteServiceAccountHandler)).Queries(restQueries(peerRESTUser)...) subrouter.Methods(http.MethodPost).Path(peerRESTVersionPrefix + peerRESTMethodLoadUser).HandlerFunc(httpTraceAll(server.LoadUserHandler)).Queries(restQueries(peerRESTUser, peerRESTUserTemp)...) + subrouter.Methods(http.MethodPost).Path(peerRESTVersionPrefix + peerRESTMethodLoadServiceAccount).HandlerFunc(httpTraceAll(server.LoadServiceAccountHandler)).Queries(restQueries(peerRESTUser)...) subrouter.Methods(http.MethodPost).Path(peerRESTVersionPrefix + peerRESTMethodLoadGroup).HandlerFunc(httpTraceAll(server.LoadGroupHandler)).Queries(restQueries(peerRESTGroup)...) subrouter.Methods(http.MethodPost).Path(peerRESTVersionPrefix + peerRESTMethodStartProfiling).HandlerFunc(httpTraceAll(server.StartProfilingHandler)).Queries(restQueries(peerRESTProfiler)...) diff --git a/cmd/typed-errors.go b/cmd/typed-errors.go index 254a4641b..1d77c86be 100644 --- a/cmd/typed-errors.go +++ b/cmd/typed-errors.go @@ -77,6 +77,9 @@ 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 the service account doesn't exist. +var errNoSuchServiceAccount = errors.New("Specified service account does not exist") + // error returned in IAM subsystem when groups doesn't exist. var errNoSuchGroup = errors.New("Specified group does not exist") diff --git a/pkg/madmin/examples/add-service-account-and-policy.go b/pkg/madmin/examples/service-accounts.go similarity index 82% rename from pkg/madmin/examples/add-service-account-and-policy.go rename to pkg/madmin/examples/service-accounts.go index 1a7a14e8e..28315954a 100644 --- a/pkg/madmin/examples/add-service-account-and-policy.go +++ b/pkg/madmin/examples/service-accounts.go @@ -46,7 +46,7 @@ func main() { p := iampolicy.Policy{ Version: iampolicy.DefaultVersion, - Statements: []Statement{ + Statements: []iampolicy.Statement{ iampolicy.NewStatement( policy.Allow, iampolicy.NewActionSet(iampolicy.GetObjectAction), @@ -55,10 +55,23 @@ func main() { )}, } - creds, err := madmClnt.AddServiceAccount(context.Background(), "parentuser", &p) + // Create a new service account + creds, err := madmClnt.AddServiceAccount(context.Background(), &p) if err != nil { log.Fatalln(err) } - fmt.Println(creds) + + // List all services accounts + list, err := madmClnt.ListServiceAccounts(context.Background()) + if err != nil { + log.Fatalln(err) + } + fmt.Println(list) + + // Delete a service account + err = madmClnt.DeleteServiceAccount(context.Background(), list.Accounts[0]) + if err != nil { + log.Fatalln(err) + } } diff --git a/pkg/madmin/user-commands.go b/pkg/madmin/user-commands.go index 06d541b85..a65b1e3dd 100644 --- a/pkg/madmin/user-commands.go +++ b/pkg/madmin/user-commands.go @@ -215,7 +215,6 @@ func (adm *AdminClient) SetUserStatus(ctx context.Context, accessKey string, sta // AddServiceAccountReq is the request body of the add service account admin call type AddServiceAccountReq struct { - Parent string `json:"parent"` Policy *iampolicy.Policy `json:"policy,omitempty"` } @@ -224,13 +223,9 @@ type AddServiceAccountResp struct { Credentials auth.Credentials `json:"credentials"` } -// AddServiceAccount - creates a new service account belonging to the given parent user -// while restricting the service account permission by the given policy document. -func (adm *AdminClient) AddServiceAccount(ctx context.Context, parentUser string, policy *iampolicy.Policy) (auth.Credentials, error) { - if !auth.IsAccessKeyValid(parentUser) { - return auth.Credentials{}, auth.ErrInvalidAccessKeyLength - } - +// AddServiceAccount - creates a new service account belonging to the user sending +// the request while restricting the service account permission by the given policy document. +func (adm *AdminClient) AddServiceAccount(ctx context.Context, policy *iampolicy.Policy) (auth.Credentials, error) { if policy != nil { if err := policy.Validate(); err != nil { return auth.Credentials{}, err @@ -238,7 +233,6 @@ func (adm *AdminClient) AddServiceAccount(ctx context.Context, parentUser string } data, err := json.Marshal(AddServiceAccountReq{ - Parent: parentUser, Policy: policy, }) if err != nil { @@ -277,3 +271,66 @@ func (adm *AdminClient) AddServiceAccount(ctx context.Context, parentUser string } return serviceAccountResp.Credentials, nil } + +// ListServiceAccountsResp is the response body of the list service accounts call +type ListServiceAccountsResp struct { + Accounts []string `json:"accounts"` +} + +// ListServiceAccounts - list service accounts belonging to the specified user +func (adm *AdminClient) ListServiceAccounts(ctx context.Context) (ListServiceAccountsResp, error) { + reqData := requestData{ + relPath: adminAPIPrefix + "/list-service-accounts", + } + + // Execute GET on /minio/admin/v3/list-service-accounts + resp, err := adm.executeMethod(ctx, http.MethodGet, reqData) + defer closeResponse(resp) + if err != nil { + return ListServiceAccountsResp{}, err + } + + if resp.StatusCode != http.StatusOK { + return ListServiceAccountsResp{}, httpRespToErrorResponse(resp) + } + + data, err := DecryptData(adm.getSecretKey(), resp.Body) + if err != nil { + return ListServiceAccountsResp{}, err + } + + var listResp ListServiceAccountsResp + if err = json.Unmarshal(data, &listResp); err != nil { + return ListServiceAccountsResp{}, err + } + return listResp, nil +} + +// DeleteServiceAccount - delete a specified service account. The server will reject +// the request if the service account does not belong to the user initiating the request +func (adm *AdminClient) DeleteServiceAccount(ctx context.Context, serviceAccount string) error { + if !auth.IsAccessKeyValid(serviceAccount) { + return auth.ErrInvalidAccessKeyLength + } + + queryValues := url.Values{} + queryValues.Set("accessKey", serviceAccount) + + reqData := requestData{ + relPath: adminAPIPrefix + "/delete-service-account", + queryValues: queryValues, + } + + // Execute DELETE on /minio/admin/v3/delete-service-account + resp, err := adm.executeMethod(ctx, http.MethodDelete, reqData) + defer closeResponse(resp) + if err != nil { + return err + } + + if resp.StatusCode != http.StatusNoContent { + return httpRespToErrorResponse(resp) + } + + return nil +}