github.com/minio/console@v1.4.1/api/service_accounts_handlers.go (about) 1 // This file is part of MinIO Console Server 2 // Copyright (c) 2021 MinIO, Inc. 3 // 4 // This program is free software: you can redistribute it and/or modify 5 // it under the terms of the GNU Affero General Public License as published by 6 // the Free Software Foundation, either version 3 of the License, or 7 // (at your option) any later version. 8 // 9 // This program is distributed in the hope that it will be useful, 10 // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 // GNU Affero General Public License for more details. 13 // 14 // You should have received a copy of the GNU Affero General Public License 15 // along with this program. If not, see <http://www.gnu.org/licenses/>. 16 17 package api 18 19 import ( 20 "context" 21 "encoding/json" 22 "errors" 23 "time" 24 25 "github.com/go-openapi/runtime/middleware" 26 "github.com/minio/console/api/operations" 27 saApi "github.com/minio/console/api/operations/service_account" 28 userApi "github.com/minio/console/api/operations/user" 29 "github.com/minio/console/models" 30 "github.com/minio/console/pkg/utils" 31 "github.com/minio/madmin-go/v3" 32 iampolicy "github.com/minio/pkg/v3/policy" 33 ) 34 35 func registerServiceAccountsHandlers(api *operations.ConsoleAPI) { 36 // Create Service Account 37 api.ServiceAccountCreateServiceAccountHandler = saApi.CreateServiceAccountHandlerFunc(func(params saApi.CreateServiceAccountParams, session *models.Principal) middleware.Responder { 38 creds, err := getCreateServiceAccountResponse(session, params) 39 if err != nil { 40 return saApi.NewCreateServiceAccountDefault(err.Code).WithPayload(err.APIError) 41 } 42 return saApi.NewCreateServiceAccountCreated().WithPayload(creds) 43 }) 44 // Create User Service Account 45 api.UserCreateAUserServiceAccountHandler = userApi.CreateAUserServiceAccountHandlerFunc(func(params userApi.CreateAUserServiceAccountParams, session *models.Principal) middleware.Responder { 46 creds, err := getCreateAUserServiceAccountResponse(session, params) 47 if err != nil { 48 return saApi.NewCreateServiceAccountDefault(err.Code).WithPayload(err.APIError) 49 } 50 return userApi.NewCreateAUserServiceAccountCreated().WithPayload(creds) 51 }) 52 // Create User Service Account 53 api.UserCreateServiceAccountCredentialsHandler = userApi.CreateServiceAccountCredentialsHandlerFunc(func(params userApi.CreateServiceAccountCredentialsParams, session *models.Principal) middleware.Responder { 54 creds, err := getCreateAUserServiceAccountCredsResponse(session, params) 55 if err != nil { 56 return saApi.NewCreateServiceAccountDefault(err.Code).WithPayload(err.APIError) 57 } 58 return userApi.NewCreateServiceAccountCredentialsCreated().WithPayload(creds) 59 }) 60 api.ServiceAccountCreateServiceAccountCredsHandler = saApi.CreateServiceAccountCredsHandlerFunc(func(params saApi.CreateServiceAccountCredsParams, session *models.Principal) middleware.Responder { 61 creds, err := getCreateServiceAccountCredsResponse(session, params) 62 if err != nil { 63 return saApi.NewCreateServiceAccountDefault(err.Code).WithPayload(err.APIError) 64 } 65 return userApi.NewCreateServiceAccountCredentialsCreated().WithPayload(creds) 66 }) 67 // List Service Accounts for User 68 api.ServiceAccountListUserServiceAccountsHandler = saApi.ListUserServiceAccountsHandlerFunc(func(params saApi.ListUserServiceAccountsParams, session *models.Principal) middleware.Responder { 69 ctx, cancel := context.WithCancel(params.HTTPRequest.Context()) 70 defer cancel() 71 serviceAccounts, err := getUserServiceAccountsResponse(ctx, session, "") 72 if err != nil { 73 return saApi.NewListUserServiceAccountsDefault(err.Code).WithPayload(err.APIError) 74 } 75 return saApi.NewListUserServiceAccountsOK().WithPayload(serviceAccounts) 76 }) 77 78 // Delete a User's service account 79 api.ServiceAccountDeleteServiceAccountHandler = saApi.DeleteServiceAccountHandlerFunc(func(params saApi.DeleteServiceAccountParams, session *models.Principal) middleware.Responder { 80 if err := getDeleteServiceAccountResponse(session, params); err != nil { 81 return saApi.NewDeleteServiceAccountDefault(err.Code).WithPayload(err.APIError) 82 } 83 return saApi.NewDeleteServiceAccountNoContent() 84 }) 85 86 // List Service Accounts for User 87 api.UserListAUserServiceAccountsHandler = userApi.ListAUserServiceAccountsHandlerFunc(func(params userApi.ListAUserServiceAccountsParams, session *models.Principal) middleware.Responder { 88 ctx, cancel := context.WithCancel(params.HTTPRequest.Context()) 89 defer cancel() 90 serviceAccounts, err := getUserServiceAccountsResponse(ctx, session, params.Name) 91 if err != nil { 92 return saApi.NewListUserServiceAccountsDefault(err.Code).WithPayload(err.APIError) 93 } 94 return saApi.NewListUserServiceAccountsOK().WithPayload(serviceAccounts) 95 }) 96 97 api.ServiceAccountGetServiceAccountHandler = saApi.GetServiceAccountHandlerFunc(func(params saApi.GetServiceAccountParams, session *models.Principal) middleware.Responder { 98 serviceAccounts, err := getServiceAccountInfo(session, params) 99 if err != nil { 100 return saApi.NewGetServiceAccountDefault(err.Code).WithPayload(err.APIError) 101 } 102 return saApi.NewGetServiceAccountOK().WithPayload(serviceAccounts) 103 }) 104 105 api.ServiceAccountUpdateServiceAccountHandler = saApi.UpdateServiceAccountHandlerFunc(func(params saApi.UpdateServiceAccountParams, session *models.Principal) middleware.Responder { 106 err := updateSetServiceAccountResponse(session, params) 107 if err != nil { 108 return saApi.NewUpdateServiceAccountDefault(err.Code).WithPayload(err.APIError) 109 } 110 return saApi.NewUpdateServiceAccountOK() 111 }) 112 113 // Delete multiple service accounts 114 api.ServiceAccountDeleteMultipleServiceAccountsHandler = saApi.DeleteMultipleServiceAccountsHandlerFunc(func(params saApi.DeleteMultipleServiceAccountsParams, session *models.Principal) middleware.Responder { 115 if err := getDeleteMultipleServiceAccountsResponse(session, params); err != nil { 116 return saApi.NewDeleteMultipleServiceAccountsDefault(err.Code).WithPayload(err.APIError) 117 } 118 return saApi.NewDeleteMultipleServiceAccountsNoContent() 119 }) 120 } 121 122 // createServiceAccount adds a service account to the userClient and assigns a policy to him if defined. 123 func createServiceAccount(ctx context.Context, userClient MinioAdmin, policy string, name string, description string, expiry *time.Time, comment string) (*models.ServiceAccountCreds, error) { 124 creds, err := userClient.addServiceAccount(ctx, policy, "", "", "", name, description, expiry, comment) 125 if err != nil { 126 return nil, err 127 } 128 return &models.ServiceAccountCreds{AccessKey: creds.AccessKey, SecretKey: creds.SecretKey, URL: getMinIOServer()}, nil 129 } 130 131 // createServiceAccount adds a service account with the given credentials to the 132 // userClient and assigns a policy to him if defined. 133 func createServiceAccountCreds(ctx context.Context, userClient MinioAdmin, policy string, accessKey string, secretKey string, name string, description string, expiry *time.Time, comment string) (*models.ServiceAccountCreds, error) { 134 creds, err := userClient.addServiceAccount(ctx, policy, "", accessKey, secretKey, name, description, expiry, comment) 135 if err != nil { 136 return nil, err 137 } 138 return &models.ServiceAccountCreds{AccessKey: creds.AccessKey, SecretKey: creds.SecretKey, URL: getMinIOServer()}, nil 139 } 140 141 // getCreateServiceAccountResponse creates a service account with the defined policy for the user that 142 // is requesting, it first gets the credentials of the user and creates a client which is going to 143 // make the call to create the Service Account 144 func getCreateServiceAccountResponse(session *models.Principal, params saApi.CreateServiceAccountParams) (*models.ServiceAccountCreds, *CodedAPIError) { 145 ctx, cancel := context.WithCancel(params.HTTPRequest.Context()) 146 defer cancel() 147 148 userAdmin, err := NewMinioAdminClient(params.HTTPRequest.Context(), session) 149 if err != nil { 150 return nil, ErrorWithContext(ctx, err) 151 } 152 // create a MinIO user Admin Client interface implementation 153 // defining the client to be used 154 userAdminClient := AdminClient{Client: userAdmin} 155 156 var expiry *time.Time 157 if params.Body.Expiry != "" { 158 parsedExpiry, err := time.Parse(time.RFC3339, params.Body.Expiry) 159 if err != nil { 160 return nil, ErrorWithContext(ctx, err) 161 } 162 expiry = &parsedExpiry 163 } 164 saCreds, err := createServiceAccount(ctx, userAdminClient, params.Body.Policy, params.Body.Name, params.Body.Description, expiry, params.Body.Comment) 165 if err != nil { 166 return nil, ErrorWithContext(ctx, err) 167 } 168 return saCreds, nil 169 } 170 171 // createServiceAccount adds a service account to a given user and assigns a policy to him if defined. 172 func createAUserServiceAccount(ctx context.Context, userClient MinioAdmin, policy string, user string, name string, description string, expiry *time.Time, comment string) (*models.ServiceAccountCreds, error) { 173 creds, err := userClient.addServiceAccount(ctx, policy, user, "", "", name, description, expiry, comment) 174 if err != nil { 175 return nil, err 176 } 177 return &models.ServiceAccountCreds{AccessKey: creds.AccessKey, SecretKey: creds.SecretKey, URL: getMinIOServer()}, nil 178 } 179 180 func createAUserServiceAccountCreds(ctx context.Context, userClient MinioAdmin, policy string, user string, accessKey string, secretKey string, name string, description string, expiry *time.Time, comment string) (*models.ServiceAccountCreds, error) { 181 creds, err := userClient.addServiceAccount(ctx, policy, user, accessKey, secretKey, name, description, expiry, comment) 182 if err != nil { 183 return nil, err 184 } 185 return &models.ServiceAccountCreds{AccessKey: creds.AccessKey, SecretKey: creds.SecretKey, URL: getMinIOServer()}, nil 186 } 187 188 // getCreateServiceAccountResponse creates a service account with the defined policy for the user that 189 // is requesting it ,it first gets the credentials of the user and creates a client which is going to 190 // make the call to create the Service Account 191 func getCreateAUserServiceAccountResponse(session *models.Principal, params userApi.CreateAUserServiceAccountParams) (*models.ServiceAccountCreds, *CodedAPIError) { 192 ctx, cancel := context.WithCancel(params.HTTPRequest.Context()) 193 defer cancel() 194 195 userAdmin, err := NewMinioAdminClient(params.HTTPRequest.Context(), session) 196 if err != nil { 197 return nil, ErrorWithContext(ctx, err) 198 } 199 // create a MinIO user Admin Client interface implementation 200 // defining the client to be used 201 userAdminClient := AdminClient{Client: userAdmin} 202 name, err := utils.DecodeBase64(params.Name) 203 if err != nil { 204 return nil, ErrorWithContext(ctx, err) 205 } 206 207 var expiry *time.Time 208 if params.Body.Expiry != "" { 209 parsedExpiry, err := time.Parse(time.RFC3339, params.Body.Expiry) 210 if err != nil { 211 return nil, ErrorWithContext(ctx, err) 212 } 213 expiry = &parsedExpiry 214 } 215 saCreds, err := createAUserServiceAccount(ctx, userAdminClient, params.Body.Policy, name, params.Body.Name, params.Body.Description, expiry, params.Body.Comment) 216 if err != nil { 217 return nil, ErrorWithContext(ctx, err) 218 } 219 return saCreds, nil 220 } 221 222 // getCreateServiceAccountCredsResponse creates a service account with the defined policy for the user that 223 // is requesting it, and with the credentials provided 224 func getCreateAUserServiceAccountCredsResponse(session *models.Principal, params userApi.CreateServiceAccountCredentialsParams) (*models.ServiceAccountCreds, *CodedAPIError) { 225 ctx, cancel := context.WithCancel(params.HTTPRequest.Context()) 226 defer cancel() 227 228 userAdmin, err := NewMinioAdminClient(params.HTTPRequest.Context(), session) 229 if err != nil { 230 return nil, ErrorWithContext(ctx, err) 231 } 232 // create a MinIO user Admin Client interface implementation 233 // defining the client to be used 234 userAdminClient := AdminClient{Client: userAdmin} 235 serviceAccount := params.Body 236 user, err := utils.DecodeBase64(params.Name) 237 if err != nil { 238 return nil, ErrorWithContext(ctx, err) 239 } 240 if user == serviceAccount.AccessKey { 241 return nil, ErrorWithContext(ctx, errors.New("Access Key already in use")) 242 } 243 accounts, err := userAdminClient.listServiceAccounts(ctx, user) 244 if err != nil { 245 return nil, ErrorWithContext(ctx, err) 246 } 247 for i := 0; i < len(accounts.Accounts); i++ { 248 if accounts.Accounts[i].AccessKey == serviceAccount.AccessKey { 249 return nil, ErrorWithContext(ctx, errors.New("Access Key already in use")) 250 } 251 } 252 253 var expiry *time.Time 254 if serviceAccount.Expiry != "" { 255 parsedExpiry, err := time.Parse(time.RFC3339, serviceAccount.Expiry) 256 if err != nil { 257 return nil, ErrorWithContext(ctx, err) 258 } 259 expiry = &parsedExpiry 260 } 261 saCreds, err := createAUserServiceAccountCreds(ctx, userAdminClient, serviceAccount.Policy, user, serviceAccount.AccessKey, serviceAccount.SecretKey, serviceAccount.Name, serviceAccount.Description, expiry, serviceAccount.Comment) 262 if err != nil { 263 return nil, ErrorWithContext(ctx, err) 264 } 265 return saCreds, nil 266 } 267 268 func getCreateServiceAccountCredsResponse(session *models.Principal, params saApi.CreateServiceAccountCredsParams) (*models.ServiceAccountCreds, *CodedAPIError) { 269 ctx, cancel := context.WithCancel(params.HTTPRequest.Context()) 270 defer cancel() 271 serviceAccount := params.Body 272 userAdmin, err := NewMinioAdminClient(params.HTTPRequest.Context(), session) 273 if err != nil { 274 return nil, ErrorWithContext(ctx, err) 275 } 276 // create a MinIO user Admin Client interface implementation 277 // defining the client to be used 278 userAdminClient := AdminClient{Client: userAdmin} 279 280 if session.AccountAccessKey == serviceAccount.AccessKey { 281 return nil, ErrorWithContext(ctx, errors.New("Access Key already in use")) 282 } 283 284 accounts, err := userAdminClient.listServiceAccounts(ctx, "") 285 if err != nil { 286 return nil, ErrorWithContext(ctx, err) 287 } 288 289 for i := 0; i < len(accounts.Accounts); i++ { 290 if accounts.Accounts[i].AccessKey == serviceAccount.AccessKey { 291 return nil, ErrorWithContext(ctx, errors.New("Access Key already in use")) 292 } 293 } 294 295 var expiry *time.Time 296 if params.Body.Expiry != "" { 297 parsedExpiry, err := time.Parse(time.RFC3339, params.Body.Expiry) 298 if err != nil { 299 return nil, ErrorWithContext(ctx, err) 300 } 301 expiry = &parsedExpiry 302 } 303 304 saCreds, err := createServiceAccountCreds(ctx, userAdminClient, serviceAccount.Policy, serviceAccount.AccessKey, serviceAccount.SecretKey, params.Body.Name, params.Body.Description, expiry, params.Body.Comment) 305 if err != nil { 306 return nil, ErrorWithContext(ctx, err) 307 } 308 return saCreds, nil 309 } 310 311 // getUserServiceAccount gets list of the user's service accounts 312 func getUserServiceAccounts(ctx context.Context, userClient MinioAdmin, user string) (models.ServiceAccounts, error) { 313 listServAccs, err := userClient.listServiceAccounts(ctx, user) 314 if err != nil { 315 return nil, err 316 } 317 saList := models.ServiceAccounts{} 318 319 for _, acc := range listServAccs.Accounts { 320 if acc.AccountStatus != "" { 321 // Newer releases of MinIO would support enhanced listServiceAccounts() 322 // we can avoid infoServiceAccount() at that point, this scales well 323 // for 100's of service accounts. 324 expiry := "" 325 if acc.Expiration != nil { 326 expiry = acc.Expiration.Format(time.RFC3339) 327 } 328 329 saList = append(saList, &models.ServiceAccountsItems0{ 330 AccountStatus: acc.AccountStatus, 331 Description: acc.Description, 332 Expiration: expiry, 333 Name: acc.Name, 334 AccessKey: acc.AccessKey, 335 }) 336 continue 337 } 338 339 aInfo, err := userClient.infoServiceAccount(ctx, acc.AccessKey) 340 if err != nil { 341 continue 342 } 343 expiry := "" 344 if aInfo.Expiration != nil { 345 expiry = aInfo.Expiration.Format(time.RFC3339) 346 } 347 348 saList = append(saList, &models.ServiceAccountsItems0{ 349 AccountStatus: aInfo.AccountStatus, 350 Description: aInfo.Description, 351 Expiration: expiry, 352 Name: aInfo.Name, 353 AccessKey: acc.AccessKey, 354 }) 355 } 356 return saList, nil 357 } 358 359 // getUserServiceAccountsResponse authenticates the user and calls 360 // getUserServiceAccounts to list the user's service accounts 361 func getUserServiceAccountsResponse(ctx context.Context, session *models.Principal, user string) (models.ServiceAccounts, *CodedAPIError) { 362 userAdmin, err := NewMinioAdminClient(ctx, session) 363 if err != nil { 364 return nil, ErrorWithContext(ctx, err) 365 } 366 // create a MinIO user Admin Client interface implementation 367 // defining the client to be used 368 userAdminClient := AdminClient{Client: userAdmin} 369 user, err = utils.DecodeBase64(user) 370 if err != nil { 371 return nil, ErrorWithContext(ctx, err) 372 } 373 serviceAccounts, err := getUserServiceAccounts(ctx, userAdminClient, user) 374 if err != nil { 375 return nil, ErrorWithContext(ctx, err) 376 } 377 return serviceAccounts, nil 378 } 379 380 // deleteServiceAccount calls delete service account api 381 func deleteServiceAccount(ctx context.Context, userClient MinioAdmin, accessKey string) error { 382 return userClient.deleteServiceAccount(ctx, accessKey) 383 } 384 385 // getDeleteServiceAccountResponse authenticates the user and calls deleteServiceAccount 386 func getDeleteServiceAccountResponse(session *models.Principal, params saApi.DeleteServiceAccountParams) *CodedAPIError { 387 ctx, cancel := context.WithCancel(params.HTTPRequest.Context()) 388 defer cancel() 389 accessKey, err := utils.DecodeBase64(params.AccessKey) 390 if err != nil { 391 return ErrorWithContext(ctx, err) 392 } 393 userAdmin, err := NewMinioAdminClient(params.HTTPRequest.Context(), session) 394 if err != nil { 395 return ErrorWithContext(ctx, err) 396 } 397 // create a MinIO user Admin Client interface implementation 398 // defining the client to be used 399 userAdminClient := AdminClient{Client: userAdmin} 400 if err := deleteServiceAccount(ctx, userAdminClient, accessKey); err != nil { 401 return ErrorWithContext(ctx, err) 402 } 403 return nil 404 } 405 406 // getServiceAccountDetails gets policy for a service account 407 func getServiceAccountDetails(ctx context.Context, userClient MinioAdmin, accessKey string) (*models.ServiceAccount, error) { 408 saInfo, err := userClient.infoServiceAccount(ctx, accessKey) 409 if err != nil { 410 return nil, err 411 } 412 413 var policyJSON string 414 var policy iampolicy.Policy 415 json.Unmarshal([]byte(saInfo.Policy), &policy) 416 if policy.Statements == nil { 417 policyJSON = "" 418 } else { 419 policyJSON = saInfo.Policy 420 } 421 422 expiry := "" 423 if saInfo.Expiration != nil { 424 expiry = saInfo.Expiration.Format(time.RFC3339) 425 } 426 427 sa := models.ServiceAccount{ 428 AccountStatus: saInfo.AccountStatus, 429 Description: saInfo.Description, 430 Expiration: expiry, 431 ImpliedPolicy: saInfo.ImpliedPolicy, 432 Name: saInfo.Name, 433 ParentUser: saInfo.ParentUser, 434 Policy: policyJSON, 435 } 436 return &sa, nil 437 } 438 439 // getServiceAccountInfo authenticates the user and calls 440 // getServiceAccountInfo to get the policy for a service account 441 func getServiceAccountInfo(session *models.Principal, params saApi.GetServiceAccountParams) (*models.ServiceAccount, *CodedAPIError) { 442 ctx, cancel := context.WithCancel(params.HTTPRequest.Context()) 443 defer cancel() 444 accessKey, err := utils.DecodeBase64(params.AccessKey) 445 if err != nil { 446 return nil, ErrorWithContext(ctx, err) 447 } 448 userAdmin, err := NewMinioAdminClient(params.HTTPRequest.Context(), session) 449 if err != nil { 450 return nil, ErrorWithContext(ctx, err) 451 } 452 // create a MinIO user Admin Client interface implementation 453 // defining the client to be used 454 userAdminClient := AdminClient{Client: userAdmin} 455 456 serviceAccount, err := getServiceAccountDetails(ctx, userAdminClient, accessKey) 457 if err != nil { 458 return nil, ErrorWithContext(ctx, err) 459 } 460 461 return serviceAccount, nil 462 } 463 464 // setServiceAccountPolicy sets policy for a service account 465 func updateServiceAccountDetails(ctx context.Context, userClient MinioAdmin, accessKey string, policy string, expiry *time.Time, name string, description string, status string, secretKey string) error { 466 req := madmin.UpdateServiceAccountReq{ 467 NewPolicy: json.RawMessage(policy), 468 NewSecretKey: secretKey, 469 NewStatus: status, 470 NewName: name, 471 NewDescription: description, 472 NewExpiration: expiry, 473 } 474 475 err := userClient.updateServiceAccount(ctx, accessKey, req) 476 return err 477 } 478 479 // updateSetServiceAccountResponse authenticates the user and calls 480 // getSetServiceAccountPolicy to set the policy for a service account 481 func updateSetServiceAccountResponse(session *models.Principal, params saApi.UpdateServiceAccountParams) *CodedAPIError { 482 ctx, cancel := context.WithCancel(params.HTTPRequest.Context()) 483 defer cancel() 484 accessKey, err := utils.DecodeBase64(params.AccessKey) 485 if err != nil { 486 return ErrorWithContext(ctx, err) 487 } 488 policy := *params.Body.Policy 489 userAdmin, err := NewMinioAdminClient(params.HTTPRequest.Context(), session) 490 if err != nil { 491 return ErrorWithContext(ctx, err) 492 } 493 // create a MinIO user Admin Client interface implementation 494 // defining the client to be used 495 userAdminClient := AdminClient{Client: userAdmin} 496 497 var expiry *time.Time 498 if params.Body.Expiry != "" { 499 parsedExpiry, err := time.Parse(time.RFC3339, params.Body.Expiry) 500 if err != nil { 501 return ErrorWithContext(ctx, err) 502 } 503 expiry = &parsedExpiry 504 } 505 err = updateServiceAccountDetails(ctx, userAdminClient, accessKey, policy, expiry, params.Body.Name, params.Body.Description, params.Body.Status, params.Body.SecretKey) 506 if err != nil { 507 return ErrorWithContext(ctx, err) 508 } 509 return nil 510 } 511 512 // getDeleteMultipleServiceAccountsResponse authenticates the user and calls deleteServiceAccount for each account listed in selectedSAs 513 func getDeleteMultipleServiceAccountsResponse(session *models.Principal, params saApi.DeleteMultipleServiceAccountsParams) *CodedAPIError { 514 ctx, cancel := context.WithCancel(params.HTTPRequest.Context()) 515 defer cancel() 516 selectedSAs := params.SelectedSA 517 userAdmin, err := NewMinioAdminClient(params.HTTPRequest.Context(), session) 518 if err != nil { 519 return ErrorWithContext(ctx, err) 520 } 521 // create a MinIO user Admin Client interface implementation 522 // defining the client to be used 523 userAdminClient := AdminClient{Client: userAdmin} 524 for _, sa := range selectedSAs { 525 if err := deleteServiceAccount(ctx, userAdminClient, sa); err != nil { 526 return ErrorWithContext(ctx, err) 527 } 528 } 529 return nil 530 }