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  }