github.com/minio/madmin-go/v3@v3.0.51/user-commands.go (about) 1 // 2 // Copyright (c) 2015-2022 MinIO, Inc. 3 // 4 // This file is part of MinIO Object Storage stack 5 // 6 // This program is free software: you can redistribute it and/or modify 7 // it under the terms of the GNU Affero General Public License as 8 // published by the Free Software Foundation, either version 3 of the 9 // License, or (at your option) any later version. 10 // 11 // This program is distributed in the hope that it will be useful, 12 // but WITHOUT ANY WARRANTY; without even the implied warranty of 13 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 // GNU Affero General Public License for more details. 15 // 16 // You should have received a copy of the GNU Affero General Public License 17 // along with this program. If not, see <http://www.gnu.org/licenses/>. 18 // 19 20 package madmin 21 22 import ( 23 "context" 24 "encoding/json" 25 "errors" 26 "io" 27 "net/http" 28 "net/url" 29 "regexp" 30 "time" 31 32 "github.com/minio/minio-go/v7/pkg/tags" 33 ) 34 35 // AccountAccess contains information about 36 type AccountAccess struct { 37 Read bool `json:"read"` 38 Write bool `json:"write"` 39 } 40 41 // BucketDetails provides information about features currently 42 // turned-on per bucket. 43 type BucketDetails struct { 44 Versioning bool `json:"versioning"` 45 VersioningSuspended bool `json:"versioningSuspended"` 46 Locking bool `json:"locking"` 47 Replication bool `json:"replication"` 48 Tagging *tags.Tags `json:"tags"` 49 Quota *BucketQuota `json:"quota"` 50 } 51 52 // BucketAccessInfo represents bucket usage of a bucket, and its relevant 53 // access type for an account 54 type BucketAccessInfo struct { 55 Name string `json:"name"` 56 Size uint64 `json:"size"` 57 Objects uint64 `json:"objects"` 58 ObjectSizesHistogram map[string]uint64 `json:"objectHistogram"` 59 ObjectVersionsHistogram map[string]uint64 `json:"objectsVersionsHistogram"` 60 Details *BucketDetails `json:"details"` 61 PrefixUsage map[string]uint64 `json:"prefixUsage"` 62 Created time.Time `json:"created"` 63 Access AccountAccess `json:"access"` 64 } 65 66 // AccountInfo represents the account usage info of an 67 // account across buckets. 68 type AccountInfo struct { 69 AccountName string 70 Server BackendInfo 71 Policy json.RawMessage // Use iam/policy.Parse to parse the result, to be done by the caller. 72 Buckets []BucketAccessInfo 73 } 74 75 // AccountOpts allows for configurable behavior with "prefix-usage" 76 type AccountOpts struct { 77 PrefixUsage bool 78 } 79 80 // AccountInfo returns the usage info for the authenticating account. 81 func (adm *AdminClient) AccountInfo(ctx context.Context, opts AccountOpts) (AccountInfo, error) { 82 q := make(url.Values) 83 if opts.PrefixUsage { 84 q.Set("prefix-usage", "true") 85 } 86 resp, err := adm.executeMethod(ctx, http.MethodGet, 87 requestData{ 88 relPath: adminAPIPrefix + "/accountinfo", 89 queryValues: q, 90 }, 91 ) 92 defer closeResponse(resp) 93 if err != nil { 94 return AccountInfo{}, err 95 } 96 97 // Check response http status code 98 if resp.StatusCode != http.StatusOK { 99 return AccountInfo{}, httpRespToErrorResponse(resp) 100 } 101 102 // Unmarshal the server's json response 103 var accountInfo AccountInfo 104 105 respBytes, err := io.ReadAll(resp.Body) 106 if err != nil { 107 return AccountInfo{}, err 108 } 109 110 err = json.Unmarshal(respBytes, &accountInfo) 111 if err != nil { 112 return AccountInfo{}, err 113 } 114 115 return accountInfo, nil 116 } 117 118 // AccountStatus - account status. 119 type AccountStatus string 120 121 // Account status per user. 122 const ( 123 AccountEnabled AccountStatus = "enabled" 124 AccountDisabled AccountStatus = "disabled" 125 ) 126 127 // UserAuthType indicates the type of authentication for the user. 128 type UserAuthType string 129 130 // Valid values for UserAuthType. 131 const ( 132 BuiltinUserAuthType UserAuthType = "builtin" 133 LDAPUserAuthType = "ldap" 134 ) 135 136 // UserAuthInfo contains info about how the user is authenticated. 137 type UserAuthInfo struct { 138 Type UserAuthType `json:"type"` 139 140 // Specifies the external server that authenticated the server (empty for 141 // builtin IDP) 142 AuthServer string `json:"authServer,omitempty"` 143 144 // Specifies the user ID as present in the external auth server (e.g. in 145 // OIDC could be the email of the user). For builtin, this would be the same 146 // as the access key. 147 AuthServerUserID string `json:"authServerUserID,omitempty"` 148 } 149 150 // UserInfo carries information about long term users. 151 type UserInfo struct { 152 AuthInfo *UserAuthInfo `json:"userAuthInfo,omitempty"` 153 SecretKey string `json:"secretKey,omitempty"` 154 PolicyName string `json:"policyName,omitempty"` 155 Status AccountStatus `json:"status"` 156 MemberOf []string `json:"memberOf,omitempty"` 157 UpdatedAt time.Time `json:"updatedAt,omitempty"` 158 } 159 160 // RemoveUser - remove a user. 161 func (adm *AdminClient) RemoveUser(ctx context.Context, accessKey string) error { 162 queryValues := url.Values{} 163 queryValues.Set("accessKey", accessKey) 164 165 reqData := requestData{ 166 relPath: adminAPIPrefix + "/remove-user", 167 queryValues: queryValues, 168 } 169 170 // Execute DELETE on /minio/admin/v3/remove-user to remove a user. 171 resp, err := adm.executeMethod(ctx, http.MethodDelete, reqData) 172 173 defer closeResponse(resp) 174 if err != nil { 175 return err 176 } 177 178 if resp.StatusCode != http.StatusOK { 179 return httpRespToErrorResponse(resp) 180 } 181 182 return nil 183 } 184 185 // ListUsers - list all users. 186 func (adm *AdminClient) ListUsers(ctx context.Context) (map[string]UserInfo, error) { 187 reqData := requestData{ 188 relPath: adminAPIPrefix + "/list-users", 189 } 190 191 // Execute GET on /minio/admin/v3/list-users 192 resp, err := adm.executeMethod(ctx, http.MethodGet, reqData) 193 194 defer closeResponse(resp) 195 if err != nil { 196 return nil, err 197 } 198 199 if resp.StatusCode != http.StatusOK { 200 return nil, httpRespToErrorResponse(resp) 201 } 202 203 data, err := DecryptData(adm.getSecretKey(), resp.Body) 204 if err != nil { 205 return nil, err 206 } 207 208 users := make(map[string]UserInfo) 209 if err = json.Unmarshal(data, &users); err != nil { 210 return nil, err 211 } 212 213 return users, nil 214 } 215 216 // GetUserInfo - get info on a user 217 func (adm *AdminClient) GetUserInfo(ctx context.Context, name string) (u UserInfo, err error) { 218 queryValues := url.Values{} 219 queryValues.Set("accessKey", name) 220 221 reqData := requestData{ 222 relPath: adminAPIPrefix + "/user-info", 223 queryValues: queryValues, 224 } 225 226 // Execute GET on /minio/admin/v3/user-info 227 resp, err := adm.executeMethod(ctx, http.MethodGet, reqData) 228 229 defer closeResponse(resp) 230 if err != nil { 231 return u, err 232 } 233 234 if resp.StatusCode != http.StatusOK { 235 return u, httpRespToErrorResponse(resp) 236 } 237 238 b, err := io.ReadAll(resp.Body) 239 if err != nil { 240 return u, err 241 } 242 243 if err = json.Unmarshal(b, &u); err != nil { 244 return u, err 245 } 246 247 return u, nil 248 } 249 250 // AddOrUpdateUserReq allows to update 251 // - user details such as secret key 252 // - account status. 253 // - optionally a comma separated list of policies 254 // to be applied for the user. 255 type AddOrUpdateUserReq struct { 256 SecretKey string `json:"secretKey,omitempty"` 257 Policy string `json:"policy,omitempty"` 258 Status AccountStatus `json:"status"` 259 } 260 261 // SetUserReq - update user secret key, account status or policies. 262 func (adm *AdminClient) SetUserReq(ctx context.Context, accessKey string, req AddOrUpdateUserReq) error { 263 data, err := json.Marshal(req) 264 if err != nil { 265 return err 266 } 267 econfigBytes, err := EncryptData(adm.getSecretKey(), data) 268 if err != nil { 269 return err 270 } 271 272 queryValues := url.Values{} 273 queryValues.Set("accessKey", accessKey) 274 275 reqData := requestData{ 276 relPath: adminAPIPrefix + "/add-user", 277 queryValues: queryValues, 278 content: econfigBytes, 279 } 280 281 // Execute PUT on /minio/admin/v3/add-user to set a user. 282 resp, err := adm.executeMethod(ctx, http.MethodPut, reqData) 283 284 defer closeResponse(resp) 285 if err != nil { 286 return err 287 } 288 289 if resp.StatusCode != http.StatusOK { 290 return httpRespToErrorResponse(resp) 291 } 292 293 return nil 294 } 295 296 // SetUser - update user secret key or account status. 297 func (adm *AdminClient) SetUser(ctx context.Context, accessKey, secretKey string, status AccountStatus) error { 298 return adm.SetUserReq(ctx, accessKey, AddOrUpdateUserReq{ 299 SecretKey: secretKey, 300 Status: status, 301 }) 302 } 303 304 // AddUser - adds a user. 305 func (adm *AdminClient) AddUser(ctx context.Context, accessKey, secretKey string) error { 306 return adm.SetUser(ctx, accessKey, secretKey, AccountEnabled) 307 } 308 309 // SetUserStatus - adds a status for a user. 310 func (adm *AdminClient) SetUserStatus(ctx context.Context, accessKey string, status AccountStatus) error { 311 queryValues := url.Values{} 312 queryValues.Set("accessKey", accessKey) 313 queryValues.Set("status", string(status)) 314 315 reqData := requestData{ 316 relPath: adminAPIPrefix + "/set-user-status", 317 queryValues: queryValues, 318 } 319 320 // Execute PUT on /minio/admin/v3/set-user-status to set status. 321 resp, err := adm.executeMethod(ctx, http.MethodPut, reqData) 322 323 defer closeResponse(resp) 324 if err != nil { 325 return err 326 } 327 328 if resp.StatusCode != http.StatusOK { 329 return httpRespToErrorResponse(resp) 330 } 331 332 return nil 333 } 334 335 // AddServiceAccountReq is the request options of the add service account admin call 336 type AddServiceAccountReq struct { 337 Policy json.RawMessage `json:"policy,omitempty"` // Parsed value from iam/policy.Parse() 338 TargetUser string `json:"targetUser,omitempty"` 339 AccessKey string `json:"accessKey,omitempty"` 340 SecretKey string `json:"secretKey,omitempty"` 341 342 // Name for this access key 343 Name string `json:"name,omitempty"` 344 // Description for this access key 345 Description string `json:"description,omitempty"` 346 // Time at which this access key expires 347 Expiration *time.Time `json:"expiration,omitempty"` 348 349 // Deprecated: use description instead 350 Comment string `json:"comment,omitempty"` 351 } 352 353 var serviceAcctValidNameRegex = regexp.MustCompile(`^[a-zA-Z][a-zA-Z0-9_-]*`) 354 355 func validateSAName(name string) error { 356 if name == "" { 357 return nil 358 } 359 if len(name) > 32 { 360 return errors.New("name must not be longer than 32 characters") 361 } 362 if !serviceAcctValidNameRegex.MatchString(name) { 363 return errors.New("name must contain only ASCII letters, digits, underscores and hyphens and must start with a letter") 364 } 365 return nil 366 } 367 368 func validateSADescription(desc string) error { 369 if desc == "" { 370 return nil 371 } 372 if len(desc) > 256 { 373 return errors.New("description must be at most 256 bytes long") 374 } 375 return nil 376 } 377 378 // Validate validates the request parameters. 379 func (r *AddServiceAccountReq) Validate() error { 380 err := validateSAName(r.Name) 381 if err != nil { 382 return err 383 } 384 if r.Expiration != nil && r.Expiration.Before(time.Now()) { 385 return errors.New("the expiration time should be in the future") 386 } 387 return validateSADescription(r.Description) 388 } 389 390 // AddServiceAccountResp is the response body of the add service account admin call 391 type AddServiceAccountResp struct { 392 Credentials Credentials `json:"credentials"` 393 } 394 395 // AddServiceAccount - creates a new service account belonging to the user sending 396 // the request while restricting the service account permission by the given policy document. 397 func (adm *AdminClient) AddServiceAccount(ctx context.Context, opts AddServiceAccountReq) (Credentials, error) { 398 if err := opts.Validate(); err != nil { 399 return Credentials{}, err 400 } 401 data, err := json.Marshal(opts) 402 if err != nil { 403 return Credentials{}, err 404 } 405 406 econfigBytes, err := EncryptData(adm.getSecretKey(), data) 407 if err != nil { 408 return Credentials{}, err 409 } 410 411 reqData := requestData{ 412 relPath: adminAPIPrefix + "/add-service-account", 413 content: econfigBytes, 414 } 415 416 // Execute PUT on /minio/admin/v3/add-service-account to set a user. 417 resp, err := adm.executeMethod(ctx, http.MethodPut, reqData) 418 defer closeResponse(resp) 419 if err != nil { 420 return Credentials{}, err 421 } 422 423 if resp.StatusCode != http.StatusOK { 424 return Credentials{}, httpRespToErrorResponse(resp) 425 } 426 427 data, err = DecryptData(adm.getSecretKey(), resp.Body) 428 if err != nil { 429 return Credentials{}, err 430 } 431 432 var serviceAccountResp AddServiceAccountResp 433 if err = json.Unmarshal(data, &serviceAccountResp); err != nil { 434 return Credentials{}, err 435 } 436 return serviceAccountResp.Credentials, nil 437 } 438 439 // AddServiceAccountLDAP - AddServiceAccount with extra features, restricted to LDAP users. 440 func (adm *AdminClient) AddServiceAccountLDAP(ctx context.Context, opts AddServiceAccountReq) (Credentials, error) { 441 if err := opts.Validate(); err != nil { 442 return Credentials{}, err 443 } 444 data, err := json.Marshal(opts) 445 if err != nil { 446 return Credentials{}, err 447 } 448 449 econfigBytes, err := EncryptData(adm.getSecretKey(), data) 450 if err != nil { 451 return Credentials{}, err 452 } 453 454 reqData := requestData{ 455 relPath: adminAPIPrefix + "/idp/ldap/add-service-account", 456 content: econfigBytes, 457 } 458 resp, err := adm.executeMethod(ctx, http.MethodPut, reqData) 459 defer closeResponse(resp) 460 if err != nil { 461 return Credentials{}, err 462 } 463 464 if resp.StatusCode != http.StatusOK { 465 return Credentials{}, httpRespToErrorResponse(resp) 466 } 467 468 data, err = DecryptData(adm.getSecretKey(), resp.Body) 469 if err != nil { 470 return Credentials{}, err 471 } 472 473 var serviceAccountResp AddServiceAccountResp 474 if err = json.Unmarshal(data, &serviceAccountResp); err != nil { 475 return Credentials{}, err 476 } 477 return serviceAccountResp.Credentials, nil 478 } 479 480 // UpdateServiceAccountReq is the request options of the edit service account admin call 481 type UpdateServiceAccountReq struct { 482 NewPolicy json.RawMessage `json:"newPolicy,omitempty"` // Parsed policy from iam/policy.Parse 483 NewSecretKey string `json:"newSecretKey,omitempty"` 484 NewStatus string `json:"newStatus,omitempty"` 485 NewName string `json:"newName,omitempty"` 486 NewDescription string `json:"newDescription,omitempty"` 487 NewExpiration *time.Time `json:"newExpiration,omitempty"` 488 } 489 490 func (u *UpdateServiceAccountReq) Validate() error { 491 if err := validateSAName(u.NewName); err != nil { 492 return err 493 } 494 495 if u.NewExpiration != nil && u.NewExpiration.Before(time.Now()) { 496 return errors.New("the expiration time should be in the future") 497 } 498 return validateSADescription(u.NewDescription) 499 } 500 501 // UpdateServiceAccount - edit an existing service account 502 func (adm *AdminClient) UpdateServiceAccount(ctx context.Context, accessKey string, opts UpdateServiceAccountReq) error { 503 if err := opts.Validate(); err != nil { 504 return err 505 } 506 data, err := json.Marshal(opts) 507 if err != nil { 508 return err 509 } 510 511 econfigBytes, err := EncryptData(adm.getSecretKey(), data) 512 if err != nil { 513 return err 514 } 515 516 queryValues := url.Values{} 517 queryValues.Set("accessKey", accessKey) 518 519 reqData := requestData{ 520 relPath: adminAPIPrefix + "/update-service-account", 521 content: econfigBytes, 522 queryValues: queryValues, 523 } 524 525 // Execute POST on /minio/admin/v3/update-service-account to edit a service account 526 resp, err := adm.executeMethod(ctx, http.MethodPost, reqData) 527 defer closeResponse(resp) 528 if err != nil { 529 return err 530 } 531 532 if resp.StatusCode != http.StatusNoContent { 533 return httpRespToErrorResponse(resp) 534 } 535 536 return nil 537 } 538 539 type ServiceAccountInfo struct { 540 ParentUser string `json:"parentUser"` 541 AccountStatus string `json:"accountStatus"` 542 ImpliedPolicy bool `json:"impliedPolicy"` 543 AccessKey string `json:"accessKey"` 544 Name string `json:"name,omitempty"` 545 Description string `json:"description,omitempty"` 546 Expiration *time.Time `json:"expiration,omitempty"` 547 } 548 549 // ListServiceAccountsResp is the response body of the list service accounts call 550 type ListServiceAccountsResp struct { 551 Accounts []ServiceAccountInfo `json:"accounts"` 552 } 553 554 // ListServiceAccounts - list service accounts belonging to the specified user 555 func (adm *AdminClient) ListServiceAccounts(ctx context.Context, user string) (ListServiceAccountsResp, error) { 556 queryValues := url.Values{} 557 queryValues.Set("user", user) 558 559 reqData := requestData{ 560 relPath: adminAPIPrefix + "/list-service-accounts", 561 queryValues: queryValues, 562 } 563 564 // Execute GET on /minio/admin/v3/list-service-accounts 565 resp, err := adm.executeMethod(ctx, http.MethodGet, reqData) 566 defer closeResponse(resp) 567 if err != nil { 568 return ListServiceAccountsResp{}, err 569 } 570 571 if resp.StatusCode != http.StatusOK { 572 return ListServiceAccountsResp{}, httpRespToErrorResponse(resp) 573 } 574 575 data, err := DecryptData(adm.getSecretKey(), resp.Body) 576 if err != nil { 577 return ListServiceAccountsResp{}, err 578 } 579 580 var listResp ListServiceAccountsResp 581 if err = json.Unmarshal(data, &listResp); err != nil { 582 return ListServiceAccountsResp{}, err 583 } 584 return listResp, nil 585 } 586 587 // ListAccessKeysLDAPResp is the response body of the list service accounts call 588 type ListAccessKeysLDAPResp struct { 589 ServiceAccounts []ServiceAccountInfo `json:"serviceAccounts"` 590 STSKeys []ServiceAccountInfo `json:"stsKeys"` 591 } 592 593 // ListAccessKeysLDAP - list service accounts belonging to the specified user 594 func (adm *AdminClient) ListAccessKeysLDAP(ctx context.Context, userDN string, listType string) (ListAccessKeysLDAPResp, error) { 595 queryValues := url.Values{} 596 queryValues.Set("listType", listType) 597 queryValues.Set("userDN", userDN) 598 599 reqData := requestData{ 600 relPath: adminAPIPrefix + "/idp/ldap/list-access-keys", 601 queryValues: queryValues, 602 } 603 604 // Execute GET on /minio/admin/v3/list-service-accounts 605 resp, err := adm.executeMethod(ctx, http.MethodGet, reqData) 606 defer closeResponse(resp) 607 if err != nil { 608 return ListAccessKeysLDAPResp{}, err 609 } 610 611 if resp.StatusCode != http.StatusOK { 612 return ListAccessKeysLDAPResp{}, httpRespToErrorResponse(resp) 613 } 614 615 data, err := DecryptData(adm.getSecretKey(), resp.Body) 616 if err != nil { 617 return ListAccessKeysLDAPResp{}, err 618 } 619 620 var listResp ListAccessKeysLDAPResp 621 if err = json.Unmarshal(data, &listResp); err != nil { 622 return ListAccessKeysLDAPResp{}, err 623 } 624 return listResp, nil 625 } 626 627 // InfoServiceAccountResp is the response body of the info service account call 628 type InfoServiceAccountResp struct { 629 ParentUser string `json:"parentUser"` 630 AccountStatus string `json:"accountStatus"` 631 ImpliedPolicy bool `json:"impliedPolicy"` 632 Policy string `json:"policy"` 633 Name string `json:"name,omitempty"` 634 Description string `json:"description,omitempty"` 635 Expiration *time.Time `json:"expiration,omitempty"` 636 } 637 638 // InfoServiceAccount - returns the info of service account belonging to the specified user 639 func (adm *AdminClient) InfoServiceAccount(ctx context.Context, accessKey string) (InfoServiceAccountResp, error) { 640 queryValues := url.Values{} 641 queryValues.Set("accessKey", accessKey) 642 643 reqData := requestData{ 644 relPath: adminAPIPrefix + "/info-service-account", 645 queryValues: queryValues, 646 } 647 648 // Execute GET on /minio/admin/v3/info-service-account 649 resp, err := adm.executeMethod(ctx, http.MethodGet, reqData) 650 defer closeResponse(resp) 651 if err != nil { 652 return InfoServiceAccountResp{}, err 653 } 654 655 if resp.StatusCode != http.StatusOK { 656 return InfoServiceAccountResp{}, httpRespToErrorResponse(resp) 657 } 658 659 data, err := DecryptData(adm.getSecretKey(), resp.Body) 660 if err != nil { 661 return InfoServiceAccountResp{}, err 662 } 663 664 var infoResp InfoServiceAccountResp 665 if err = json.Unmarshal(data, &infoResp); err != nil { 666 return InfoServiceAccountResp{}, err 667 } 668 return infoResp, nil 669 } 670 671 // DeleteServiceAccount - delete a specified service account. The server will reject 672 // the request if the service account does not belong to the user initiating the request 673 func (adm *AdminClient) DeleteServiceAccount(ctx context.Context, serviceAccount string) error { 674 queryValues := url.Values{} 675 queryValues.Set("accessKey", serviceAccount) 676 677 reqData := requestData{ 678 relPath: adminAPIPrefix + "/delete-service-account", 679 queryValues: queryValues, 680 } 681 682 // Execute DELETE on /minio/admin/v3/delete-service-account 683 resp, err := adm.executeMethod(ctx, http.MethodDelete, reqData) 684 defer closeResponse(resp) 685 if err != nil { 686 return err 687 } 688 689 if resp.StatusCode != http.StatusNoContent { 690 return httpRespToErrorResponse(resp) 691 } 692 693 return nil 694 } 695 696 // TemporaryAccountInfoResp is the response body of the info temporary call 697 type TemporaryAccountInfoResp InfoServiceAccountResp 698 699 // TemporaryAccountInfo - returns the info of a temporary account 700 func (adm *AdminClient) TemporaryAccountInfo(ctx context.Context, accessKey string) (TemporaryAccountInfoResp, error) { 701 queryValues := url.Values{} 702 queryValues.Set("accessKey", accessKey) 703 704 reqData := requestData{ 705 relPath: adminAPIPrefix + "/temporary-account-info", 706 queryValues: queryValues, 707 } 708 709 // Execute GET on /minio/admin/v3/temporary-account-info 710 resp, err := adm.executeMethod(ctx, http.MethodGet, reqData) 711 defer closeResponse(resp) 712 if err != nil { 713 return TemporaryAccountInfoResp{}, err 714 } 715 716 if resp.StatusCode != http.StatusOK { 717 return TemporaryAccountInfoResp{}, httpRespToErrorResponse(resp) 718 } 719 720 data, err := DecryptData(adm.getSecretKey(), resp.Body) 721 if err != nil { 722 return TemporaryAccountInfoResp{}, err 723 } 724 725 var infoResp TemporaryAccountInfoResp 726 if err = json.Unmarshal(data, &infoResp); err != nil { 727 return TemporaryAccountInfoResp{}, err 728 } 729 return infoResp, nil 730 }