github.com/minio/madmin-go/v2@v2.2.1/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/ioutil" 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 := ioutil.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 // UserInfo carries information about long term users. 128 type UserInfo struct { 129 SecretKey string `json:"secretKey,omitempty"` 130 PolicyName string `json:"policyName,omitempty"` 131 Status AccountStatus `json:"status"` 132 MemberOf []string `json:"memberOf,omitempty"` 133 UpdatedAt time.Time `json:"updatedAt,omitempty"` 134 } 135 136 // RemoveUser - remove a user. 137 func (adm *AdminClient) RemoveUser(ctx context.Context, accessKey string) error { 138 queryValues := url.Values{} 139 queryValues.Set("accessKey", accessKey) 140 141 reqData := requestData{ 142 relPath: adminAPIPrefix + "/remove-user", 143 queryValues: queryValues, 144 } 145 146 // Execute DELETE on /minio/admin/v3/remove-user to remove a user. 147 resp, err := adm.executeMethod(ctx, http.MethodDelete, reqData) 148 149 defer closeResponse(resp) 150 if err != nil { 151 return err 152 } 153 154 if resp.StatusCode != http.StatusOK { 155 return httpRespToErrorResponse(resp) 156 } 157 158 return nil 159 } 160 161 // ListUsers - list all users. 162 func (adm *AdminClient) ListUsers(ctx context.Context) (map[string]UserInfo, error) { 163 reqData := requestData{ 164 relPath: adminAPIPrefix + "/list-users", 165 } 166 167 // Execute GET on /minio/admin/v3/list-users 168 resp, err := adm.executeMethod(ctx, http.MethodGet, reqData) 169 170 defer closeResponse(resp) 171 if err != nil { 172 return nil, err 173 } 174 175 if resp.StatusCode != http.StatusOK { 176 return nil, httpRespToErrorResponse(resp) 177 } 178 179 data, err := DecryptData(adm.getSecretKey(), resp.Body) 180 if err != nil { 181 return nil, err 182 } 183 184 users := make(map[string]UserInfo) 185 if err = json.Unmarshal(data, &users); err != nil { 186 return nil, err 187 } 188 189 return users, nil 190 } 191 192 // GetUserInfo - get info on a user 193 func (adm *AdminClient) GetUserInfo(ctx context.Context, name string) (u UserInfo, err error) { 194 queryValues := url.Values{} 195 queryValues.Set("accessKey", name) 196 197 reqData := requestData{ 198 relPath: adminAPIPrefix + "/user-info", 199 queryValues: queryValues, 200 } 201 202 // Execute GET on /minio/admin/v3/user-info 203 resp, err := adm.executeMethod(ctx, http.MethodGet, reqData) 204 205 defer closeResponse(resp) 206 if err != nil { 207 return u, err 208 } 209 210 if resp.StatusCode != http.StatusOK { 211 return u, httpRespToErrorResponse(resp) 212 } 213 214 b, err := ioutil.ReadAll(resp.Body) 215 if err != nil { 216 return u, err 217 } 218 219 if err = json.Unmarshal(b, &u); err != nil { 220 return u, err 221 } 222 223 return u, nil 224 } 225 226 // AddOrUpdateUserReq allows to update 227 // - user details such as secret key 228 // - account status. 229 // - optionally a comma separated list of policies 230 // to be applied for the user. 231 type AddOrUpdateUserReq struct { 232 SecretKey string `json:"secretKey,omitempty"` 233 Policy string `json:"policy,omitempty"` 234 Status AccountStatus `json:"status"` 235 } 236 237 // SetUserReq - update user secret key, account status or policies. 238 func (adm *AdminClient) SetUserReq(ctx context.Context, accessKey string, req AddOrUpdateUserReq) error { 239 data, err := json.Marshal(req) 240 if err != nil { 241 return err 242 } 243 econfigBytes, err := EncryptData(adm.getSecretKey(), data) 244 if err != nil { 245 return err 246 } 247 248 queryValues := url.Values{} 249 queryValues.Set("accessKey", accessKey) 250 251 reqData := requestData{ 252 relPath: adminAPIPrefix + "/add-user", 253 queryValues: queryValues, 254 content: econfigBytes, 255 } 256 257 // Execute PUT on /minio/admin/v3/add-user to set a user. 258 resp, err := adm.executeMethod(ctx, http.MethodPut, reqData) 259 260 defer closeResponse(resp) 261 if err != nil { 262 return err 263 } 264 265 if resp.StatusCode != http.StatusOK { 266 return httpRespToErrorResponse(resp) 267 } 268 269 return nil 270 } 271 272 // SetUser - update user secret key or account status. 273 func (adm *AdminClient) SetUser(ctx context.Context, accessKey, secretKey string, status AccountStatus) error { 274 return adm.SetUserReq(ctx, accessKey, AddOrUpdateUserReq{ 275 SecretKey: secretKey, 276 Status: status, 277 }) 278 } 279 280 // AddUser - adds a user. 281 func (adm *AdminClient) AddUser(ctx context.Context, accessKey, secretKey string) error { 282 return adm.SetUser(ctx, accessKey, secretKey, AccountEnabled) 283 } 284 285 // SetUserStatus - adds a status for a user. 286 func (adm *AdminClient) SetUserStatus(ctx context.Context, accessKey string, status AccountStatus) error { 287 queryValues := url.Values{} 288 queryValues.Set("accessKey", accessKey) 289 queryValues.Set("status", string(status)) 290 291 reqData := requestData{ 292 relPath: adminAPIPrefix + "/set-user-status", 293 queryValues: queryValues, 294 } 295 296 // Execute PUT on /minio/admin/v3/set-user-status to set status. 297 resp, err := adm.executeMethod(ctx, http.MethodPut, reqData) 298 299 defer closeResponse(resp) 300 if err != nil { 301 return err 302 } 303 304 if resp.StatusCode != http.StatusOK { 305 return httpRespToErrorResponse(resp) 306 } 307 308 return nil 309 } 310 311 // AddServiceAccountReq is the request options of the add service account admin call 312 type AddServiceAccountReq struct { 313 Policy json.RawMessage `json:"policy,omitempty"` // Parsed value from iam/policy.Parse() 314 TargetUser string `json:"targetUser,omitempty"` 315 AccessKey string `json:"accessKey,omitempty"` 316 SecretKey string `json:"secretKey,omitempty"` 317 318 // Name for this access key 319 Name string `json:"name,omitempty"` 320 // Description for this access key 321 Description string `json:"description,omitempty"` 322 // Time at which this access key expires 323 Expiration *time.Time `json:"expiration,omitempty"` 324 325 // Deprecated: use description instead 326 Comment string `json:"comment,omitempty"` 327 } 328 329 var serviceAcctValidNameRegex = regexp.MustCompile(`^[a-zA-Z][a-zA-Z0-9_-]*`) 330 331 func validateSAName(name string) error { 332 if name == "" { 333 return nil 334 } 335 if len(name) > 32 { 336 return errors.New("name must not be longer than 32 characters") 337 } 338 if !serviceAcctValidNameRegex.MatchString(name) { 339 return errors.New("name must contain only ASCII letters, digits, underscores and hyphens and must start with a letter") 340 } 341 return nil 342 } 343 344 func validateSADescription(desc string) error { 345 if desc == "" { 346 return nil 347 } 348 if len(desc) > 256 { 349 return errors.New("description must be at most 256 bytes long") 350 } 351 return nil 352 } 353 354 // Validate validates the request parameters. 355 func (r *AddServiceAccountReq) Validate() error { 356 err := validateSAName(r.Name) 357 if err != nil { 358 return err 359 } 360 return validateSADescription(r.Description) 361 } 362 363 // AddServiceAccountResp is the response body of the add service account admin call 364 type AddServiceAccountResp struct { 365 Credentials Credentials `json:"credentials"` 366 } 367 368 // AddServiceAccount - creates a new service account belonging to the user sending 369 // the request while restricting the service account permission by the given policy document. 370 func (adm *AdminClient) AddServiceAccount(ctx context.Context, opts AddServiceAccountReq) (Credentials, error) { 371 if err := opts.Validate(); err != nil { 372 return Credentials{}, err 373 } 374 data, err := json.Marshal(opts) 375 if err != nil { 376 return Credentials{}, err 377 } 378 379 econfigBytes, err := EncryptData(adm.getSecretKey(), data) 380 if err != nil { 381 return Credentials{}, err 382 } 383 384 reqData := requestData{ 385 relPath: adminAPIPrefix + "/add-service-account", 386 content: econfigBytes, 387 } 388 389 // Execute PUT on /minio/admin/v3/add-service-account to set a user. 390 resp, err := adm.executeMethod(ctx, http.MethodPut, reqData) 391 defer closeResponse(resp) 392 if err != nil { 393 return Credentials{}, err 394 } 395 396 if resp.StatusCode != http.StatusOK { 397 return Credentials{}, httpRespToErrorResponse(resp) 398 } 399 400 data, err = DecryptData(adm.getSecretKey(), resp.Body) 401 if err != nil { 402 return Credentials{}, err 403 } 404 405 var serviceAccountResp AddServiceAccountResp 406 if err = json.Unmarshal(data, &serviceAccountResp); err != nil { 407 return Credentials{}, err 408 } 409 return serviceAccountResp.Credentials, nil 410 } 411 412 // UpdateServiceAccountReq is the request options of the edit service account admin call 413 type UpdateServiceAccountReq struct { 414 NewPolicy json.RawMessage `json:"newPolicy,omitempty"` // Parsed policy from iam/policy.Parse 415 NewSecretKey string `json:"newSecretKey,omitempty"` 416 NewStatus string `json:"newStatus,omitempty"` 417 NewName string `json:"newName,omitempty"` 418 NewDescription string `json:"newDescription,omitempty"` 419 NewExpiration *time.Time `json:"newExpiration,omitempty"` 420 } 421 422 func (u *UpdateServiceAccountReq) Validate() error { 423 if err := validateSAName(u.NewName); err != nil { 424 return err 425 } 426 return validateSADescription(u.NewDescription) 427 } 428 429 // UpdateServiceAccount - edit an existing service account 430 func (adm *AdminClient) UpdateServiceAccount(ctx context.Context, accessKey string, opts UpdateServiceAccountReq) error { 431 if err := opts.Validate(); err != nil { 432 return err 433 } 434 data, err := json.Marshal(opts) 435 if err != nil { 436 return err 437 } 438 439 econfigBytes, err := EncryptData(adm.getSecretKey(), data) 440 if err != nil { 441 return err 442 } 443 444 queryValues := url.Values{} 445 queryValues.Set("accessKey", accessKey) 446 447 reqData := requestData{ 448 relPath: adminAPIPrefix + "/update-service-account", 449 content: econfigBytes, 450 queryValues: queryValues, 451 } 452 453 // Execute POST on /minio/admin/v3/update-service-account to edit a service account 454 resp, err := adm.executeMethod(ctx, http.MethodPost, reqData) 455 defer closeResponse(resp) 456 if err != nil { 457 return err 458 } 459 460 if resp.StatusCode != http.StatusNoContent { 461 return httpRespToErrorResponse(resp) 462 } 463 464 return nil 465 } 466 467 type ServiceAccountInfo struct { 468 AccessKey string `json:"accessKey"` 469 Expiration *time.Time `json:"expiration,omitempty"` 470 } 471 472 // ListServiceAccountsResp is the response body of the list service accounts call 473 type ListServiceAccountsResp struct { 474 Accounts []ServiceAccountInfo `json:"accounts"` 475 } 476 477 // ListServiceAccounts - list service accounts belonging to the specified user 478 func (adm *AdminClient) ListServiceAccounts(ctx context.Context, user string) (ListServiceAccountsResp, error) { 479 queryValues := url.Values{} 480 queryValues.Set("user", user) 481 482 reqData := requestData{ 483 relPath: adminAPIPrefix + "/list-service-accounts", 484 queryValues: queryValues, 485 } 486 487 // Execute GET on /minio/admin/v3/list-service-accounts 488 resp, err := adm.executeMethod(ctx, http.MethodGet, reqData) 489 defer closeResponse(resp) 490 if err != nil { 491 return ListServiceAccountsResp{}, err 492 } 493 494 if resp.StatusCode != http.StatusOK { 495 return ListServiceAccountsResp{}, httpRespToErrorResponse(resp) 496 } 497 498 data, err := DecryptData(adm.getSecretKey(), resp.Body) 499 if err != nil { 500 return ListServiceAccountsResp{}, err 501 } 502 503 var listResp ListServiceAccountsResp 504 if err = json.Unmarshal(data, &listResp); err != nil { 505 return ListServiceAccountsResp{}, err 506 } 507 return listResp, nil 508 } 509 510 // InfoServiceAccountResp is the response body of the info service account call 511 type InfoServiceAccountResp struct { 512 ParentUser string `json:"parentUser"` 513 AccountStatus string `json:"accountStatus"` 514 ImpliedPolicy bool `json:"impliedPolicy"` 515 Policy string `json:"policy"` 516 Name string `json:"name,omitempty"` 517 Description string `json:"description,omitempty"` 518 Expiration *time.Time `json:"expiration,omitempty"` 519 } 520 521 // InfoServiceAccount - returns the info of service account belonging to the specified user 522 func (adm *AdminClient) InfoServiceAccount(ctx context.Context, accessKey string) (InfoServiceAccountResp, error) { 523 queryValues := url.Values{} 524 queryValues.Set("accessKey", accessKey) 525 526 reqData := requestData{ 527 relPath: adminAPIPrefix + "/info-service-account", 528 queryValues: queryValues, 529 } 530 531 // Execute GET on /minio/admin/v3/info-service-account 532 resp, err := adm.executeMethod(ctx, http.MethodGet, reqData) 533 defer closeResponse(resp) 534 if err != nil { 535 return InfoServiceAccountResp{}, err 536 } 537 538 if resp.StatusCode != http.StatusOK { 539 return InfoServiceAccountResp{}, httpRespToErrorResponse(resp) 540 } 541 542 data, err := DecryptData(adm.getSecretKey(), resp.Body) 543 if err != nil { 544 return InfoServiceAccountResp{}, err 545 } 546 547 var infoResp InfoServiceAccountResp 548 if err = json.Unmarshal(data, &infoResp); err != nil { 549 return InfoServiceAccountResp{}, err 550 } 551 return infoResp, nil 552 } 553 554 // DeleteServiceAccount - delete a specified service account. The server will reject 555 // the request if the service account does not belong to the user initiating the request 556 func (adm *AdminClient) DeleteServiceAccount(ctx context.Context, serviceAccount string) error { 557 queryValues := url.Values{} 558 queryValues.Set("accessKey", serviceAccount) 559 560 reqData := requestData{ 561 relPath: adminAPIPrefix + "/delete-service-account", 562 queryValues: queryValues, 563 } 564 565 // Execute DELETE on /minio/admin/v3/delete-service-account 566 resp, err := adm.executeMethod(ctx, http.MethodDelete, reqData) 567 defer closeResponse(resp) 568 if err != nil { 569 return err 570 } 571 572 if resp.StatusCode != http.StatusNoContent { 573 return httpRespToErrorResponse(resp) 574 } 575 576 return nil 577 } 578 579 // TemporaryAccountInfoResp is the response body of the info temporary call 580 type TemporaryAccountInfoResp InfoServiceAccountResp 581 582 // TemporaryAccountInfo - returns the info of a temporary account 583 func (adm *AdminClient) TemporaryAccountInfo(ctx context.Context, accessKey string) (TemporaryAccountInfoResp, error) { 584 queryValues := url.Values{} 585 queryValues.Set("accessKey", accessKey) 586 587 reqData := requestData{ 588 relPath: adminAPIPrefix + "/temporary-account-info", 589 queryValues: queryValues, 590 } 591 592 // Execute GET on /minio/admin/v3/temporary-account-info 593 resp, err := adm.executeMethod(ctx, http.MethodGet, reqData) 594 defer closeResponse(resp) 595 if err != nil { 596 return TemporaryAccountInfoResp{}, err 597 } 598 599 if resp.StatusCode != http.StatusOK { 600 return TemporaryAccountInfoResp{}, httpRespToErrorResponse(resp) 601 } 602 603 data, err := DecryptData(adm.getSecretKey(), resp.Body) 604 if err != nil { 605 return TemporaryAccountInfoResp{}, err 606 } 607 608 var infoResp TemporaryAccountInfoResp 609 if err = json.Unmarshal(data, &infoResp); err != nil { 610 return TemporaryAccountInfoResp{}, err 611 } 612 return infoResp, nil 613 }