// Copyright (c) 2015-2023 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import ( "bytes" "context" "encoding/json" "errors" "fmt" "io" "net/http" "os" "slices" "sort" "strconv" "strings" "time" "unicode/utf8" "github.com/klauspost/compress/zip" "github.com/minio/madmin-go/v3" "github.com/minio/minio/internal/auth" "github.com/minio/minio/internal/config/dns" "github.com/minio/mux" xldap "github.com/minio/pkg/v3/ldap" "github.com/minio/pkg/v3/policy" "github.com/puzpuzpuz/xsync/v3" ) // RemoveUser - DELETE /minio/admin/v3/remove-user?accessKey= func (a adminAPIHandlers) RemoveUser(w http.ResponseWriter, r *http.Request) { ctx := r.Context() objectAPI, cred := validateAdminReq(ctx, w, r, policy.DeleteUserAdminAction) if objectAPI == nil { return } vars := mux.Vars(r) accessKey := vars["accessKey"] ok, _, err := globalIAMSys.IsTempUser(accessKey) if err != nil { writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL) return } if ok { writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, errIAMActionNotAllowed), r.URL) return } // When the user is root credential you are not allowed to // remove the root user. Also you cannot delete yourself. if accessKey == globalActiveCred.AccessKey || accessKey == cred.AccessKey { writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, errIAMActionNotAllowed), r.URL) return } if err := globalIAMSys.DeleteUser(ctx, accessKey, true); err != nil { writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL) return } replLogIf(ctx, globalSiteReplicationSys.IAMChangeHook(ctx, madmin.SRIAMItem{ Type: madmin.SRIAMItemIAMUser, IAMUser: &madmin.SRIAMUser{ AccessKey: accessKey, IsDeleteReq: true, }, UpdatedAt: UTCNow(), })) } // ListBucketUsers - GET /minio/admin/v3/list-users?bucket={bucket} func (a adminAPIHandlers) ListBucketUsers(w http.ResponseWriter, r *http.Request) { ctx := r.Context() objectAPI, cred := validateAdminReq(ctx, w, r, policy.ListUsersAdminAction) if objectAPI == nil { return } bucket := mux.Vars(r)["bucket"] password := cred.SecretKey allCredentials, err := globalIAMSys.ListBucketUsers(ctx, bucket) if err != nil { writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL) return } data, err := json.Marshal(allCredentials) if err != nil { writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL) return } econfigData, err := madmin.EncryptData(password, data) if err != nil { writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL) return } writeSuccessResponseJSON(w, econfigData) } // ListUsers - GET /minio/admin/v3/list-users func (a adminAPIHandlers) ListUsers(w http.ResponseWriter, r *http.Request) { ctx := r.Context() objectAPI, cred := validateAdminReq(ctx, w, r, policy.ListUsersAdminAction) if objectAPI == nil { return } password := cred.SecretKey allCredentials, err := globalIAMSys.ListUsers(ctx) if err != nil { writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL) return } // Add ldap users which have mapped policies if in LDAP mode // FIXME(vadmeste): move this to policy info in the future ldapUsers, err := globalIAMSys.ListLDAPUsers(ctx) if err != nil && err != errIAMActionNotAllowed { writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL) return } for k, v := range ldapUsers { allCredentials[k] = v } // Marshal the response data, err := json.Marshal(allCredentials) if err != nil { writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL) return } econfigData, err := madmin.EncryptData(password, data) if err != nil { writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL) return } writeSuccessResponseJSON(w, econfigData) } // GetUserInfo - GET /minio/admin/v3/user-info func (a adminAPIHandlers) GetUserInfo(w http.ResponseWriter, r *http.Request) { ctx := r.Context() vars := mux.Vars(r) name := vars["accessKey"] // Get current object layer instance. objectAPI := newObjectLayerFn() if objectAPI == nil || globalNotificationSys == 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 } checkDenyOnly := false if name == cred.AccessKey { // Check that there is no explicit deny - otherwise it's allowed // to view one's own info. checkDenyOnly = true } if !globalIAMSys.IsAllowed(policy.Args{ AccountName: cred.AccessKey, Groups: cred.Groups, Action: policy.GetUserAdminAction, ConditionValues: getConditionValues(r, "", cred), IsOwner: owner, Claims: cred.Claims, DenyOnly: checkDenyOnly, }) { writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAccessDenied), r.URL) return } userInfo, err := globalIAMSys.GetUserInfo(ctx, name) if err != nil { writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL) return } data, err := json.Marshal(userInfo) if err != nil { writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL) return } writeSuccessResponseJSON(w, data) } // UpdateGroupMembers - PUT /minio/admin/v3/update-group-members func (a adminAPIHandlers) UpdateGroupMembers(w http.ResponseWriter, r *http.Request) { ctx := r.Context() objectAPI, _ := validateAdminReq(ctx, w, r, policy.AddUserToGroupAdminAction) if objectAPI == nil { return } data, err := io.ReadAll(r.Body) if err != nil { writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrInvalidRequest), r.URL) return } var updReq madmin.GroupAddRemove err = json.Unmarshal(data, &updReq) if err != nil { writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrInvalidRequest), r.URL) return } // Reject if the group add and remove are temporary credentials, or root credential. for _, member := range updReq.Members { ok, _, err := globalIAMSys.IsTempUser(member) if err != nil && err != errNoSuchUser { writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL) return } if ok { writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, errIAMActionNotAllowed), r.URL) return } // When the user is root credential you are not allowed to // add policies for root user. if member == globalActiveCred.AccessKey { writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, errIAMActionNotAllowed), r.URL) return } } var updatedAt time.Time if updReq.IsRemove { updatedAt, err = globalIAMSys.RemoveUsersFromGroup(ctx, updReq.Group, updReq.Members) } else { // Check if group already exists if _, gerr := globalIAMSys.GetGroupDescription(updReq.Group); gerr != nil { // If group does not exist, then check if the group has beginning and end space characters // we will reject such group names. if errors.Is(gerr, errNoSuchGroup) && hasSpaceBE(updReq.Group) { writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAdminResourceInvalidArgument), r.URL) return } } if globalIAMSys.LDAPConfig.Enabled() { // We don't allow internal group manipulation in this API when LDAP // is enabled for now. err = errIAMActionNotAllowed } else { updatedAt, err = globalIAMSys.AddUsersToGroup(ctx, updReq.Group, updReq.Members) } } if err != nil { writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL) return } replLogIf(ctx, globalSiteReplicationSys.IAMChangeHook(ctx, madmin.SRIAMItem{ Type: madmin.SRIAMItemGroupInfo, GroupInfo: &madmin.SRGroupInfo{ UpdateReq: updReq, }, UpdatedAt: updatedAt, })) } // GetGroup - /minio/admin/v3/group?group=mygroup1 func (a adminAPIHandlers) GetGroup(w http.ResponseWriter, r *http.Request) { ctx := r.Context() objectAPI, _ := validateAdminReq(ctx, w, r, policy.GetGroupAdminAction) if objectAPI == nil { return } vars := mux.Vars(r) group := vars["group"] gdesc, err := globalIAMSys.GetGroupDescription(group) if err != nil { writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL) return } body, err := json.Marshal(gdesc) if err != nil { writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL) return } writeSuccessResponseJSON(w, body) } // ListGroups - GET /minio/admin/v3/groups func (a adminAPIHandlers) ListGroups(w http.ResponseWriter, r *http.Request) { ctx := r.Context() objectAPI, _ := validateAdminReq(ctx, w, r, policy.ListGroupsAdminAction) if objectAPI == nil { return } groups, err := globalIAMSys.ListGroups(ctx) if err != nil { writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL) return } body, err := json.Marshal(groups) if err != nil { writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL) return } writeSuccessResponseJSON(w, body) } // SetGroupStatus - PUT /minio/admin/v3/set-group-status?group=mygroup1&status=enabled func (a adminAPIHandlers) SetGroupStatus(w http.ResponseWriter, r *http.Request) { ctx := r.Context() objectAPI, _ := validateAdminReq(ctx, w, r, policy.EnableGroupAdminAction) if objectAPI == nil { return } vars := mux.Vars(r) group := vars["group"] status := vars["status"] var ( err error updatedAt time.Time ) switch status { case statusEnabled: updatedAt, err = globalIAMSys.SetGroupStatus(ctx, group, true) case statusDisabled: updatedAt, err = globalIAMSys.SetGroupStatus(ctx, group, false) default: err = errInvalidArgument } if err != nil { writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL) return } replLogIf(ctx, globalSiteReplicationSys.IAMChangeHook(ctx, madmin.SRIAMItem{ Type: madmin.SRIAMItemGroupInfo, GroupInfo: &madmin.SRGroupInfo{ UpdateReq: madmin.GroupAddRemove{ Group: group, Status: madmin.GroupStatus(status), IsRemove: false, }, }, UpdatedAt: updatedAt, })) } // SetUserStatus - PUT /minio/admin/v3/set-user-status?accessKey=&status=[enabled|disabled] func (a adminAPIHandlers) SetUserStatus(w http.ResponseWriter, r *http.Request) { ctx := r.Context() objectAPI, creds := validateAdminReq(ctx, w, r, policy.EnableUserAdminAction) if objectAPI == nil { return } vars := mux.Vars(r) accessKey := vars["accessKey"] status := vars["status"] // you cannot enable or disable yourself. if accessKey == creds.AccessKey { writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, errInvalidArgument), r.URL) return } updatedAt, err := globalIAMSys.SetUserStatus(ctx, accessKey, madmin.AccountStatus(status)) if err != nil { writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL) return } replLogIf(ctx, globalSiteReplicationSys.IAMChangeHook(ctx, madmin.SRIAMItem{ Type: madmin.SRIAMItemIAMUser, IAMUser: &madmin.SRIAMUser{ AccessKey: accessKey, IsDeleteReq: false, UserReq: &madmin.AddOrUpdateUserReq{ Status: madmin.AccountStatus(status), }, }, UpdatedAt: updatedAt, })) } // AddUser - PUT /minio/admin/v3/add-user?accessKey= func (a adminAPIHandlers) AddUser(w http.ResponseWriter, r *http.Request) { ctx := r.Context() vars := mux.Vars(r) accessKey := vars["accessKey"] // Get current object layer instance. objectAPI := newObjectLayerFn() if objectAPI == nil || globalNotificationSys == 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 } // Not allowed to add a user with same access key as root credential if accessKey == globalActiveCred.AccessKey { writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAddUserInvalidArgument), r.URL) return } user, exists := globalIAMSys.GetUser(ctx, accessKey) if exists && (user.Credentials.IsTemp() || user.Credentials.IsServiceAccount()) { // Updating STS credential is not allowed, and this API does not // support updating service accounts. writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAddUserInvalidArgument), r.URL) return } if (cred.IsTemp() || cred.IsServiceAccount()) && cred.ParentUser == accessKey { // Incoming access key matches parent user then we should // reject password change requests. writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAddUserInvalidArgument), r.URL) return } // Check if accessKey has beginning and end space characters, this only applies to new users. if !exists && hasSpaceBE(accessKey) { writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAdminResourceInvalidArgument), r.URL) return } if !utf8.ValidString(accessKey) { writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAddUserValidUTF), r.URL) return } checkDenyOnly := false if accessKey == cred.AccessKey { // Check that there is no explicit deny - otherwise it's allowed // to change one's own password. checkDenyOnly = true } if !globalIAMSys.IsAllowed(policy.Args{ AccountName: cred.AccessKey, Groups: cred.Groups, Action: policy.CreateUserAdminAction, ConditionValues: getConditionValues(r, "", cred), IsOwner: owner, Claims: cred.Claims, DenyOnly: checkDenyOnly, }) { writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAccessDenied), r.URL) return } if r.ContentLength > maxEConfigJSONSize || r.ContentLength == -1 { // More than maxConfigSize bytes were available writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAdminConfigTooLarge), r.URL) return } password := cred.SecretKey configBytes, err := madmin.DecryptData(password, io.LimitReader(r.Body, r.ContentLength)) if err != nil { adminLogIf(ctx, err) writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAdminConfigBadJSON), r.URL) return } var ureq madmin.AddOrUpdateUserReq if err = json.Unmarshal(configBytes, &ureq); err != nil { adminLogIf(ctx, err) writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAdminConfigBadJSON), r.URL) return } // We don't allow internal user creation with LDAP enabled for now. if globalIAMSys.LDAPConfig.Enabled() { writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, errIAMActionNotAllowed), r.URL) return } updatedAt, err := globalIAMSys.CreateUser(ctx, accessKey, ureq) if err != nil { writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL) return } replLogIf(ctx, globalSiteReplicationSys.IAMChangeHook(ctx, madmin.SRIAMItem{ Type: madmin.SRIAMItemIAMUser, IAMUser: &madmin.SRIAMUser{ AccessKey: accessKey, IsDeleteReq: false, UserReq: &ureq, }, UpdatedAt: updatedAt, })) } // TemporaryAccountInfo - GET /minio/admin/v3/temporary-account-info func (a adminAPIHandlers) TemporaryAccountInfo(w http.ResponseWriter, r *http.Request) { ctx := r.Context() // Get current object layer instance. objectAPI := newObjectLayerFn() if objectAPI == nil || globalNotificationSys == 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 } accessKey := mux.Vars(r)["accessKey"] if accessKey == "" { writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrInvalidRequest), r.URL) return } args := policy.Args{ AccountName: cred.AccessKey, Groups: cred.Groups, Action: policy.ListTemporaryAccountsAdminAction, ConditionValues: getConditionValues(r, "", cred), IsOwner: owner, Claims: cred.Claims, } if !globalIAMSys.IsAllowed(args) { writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAccessDenied), r.URL) return } stsAccount, sessionPolicy, err := globalIAMSys.GetTemporaryAccount(ctx, accessKey) if err != nil { writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL) return } var stsAccountPolicy policy.Policy if sessionPolicy != nil { stsAccountPolicy = *sessionPolicy } else { policiesNames, err := globalIAMSys.PolicyDBGet(stsAccount.ParentUser, stsAccount.Groups...) if err != nil { writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL) return } if len(policiesNames) == 0 { policySet, _ := args.GetPolicies(iamPolicyClaimNameOpenID()) policiesNames = policySet.ToSlice() } stsAccountPolicy = globalIAMSys.GetCombinedPolicy(policiesNames...) } policyJSON, err := json.MarshalIndent(stsAccountPolicy, "", " ") if err != nil { writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL) return } infoResp := madmin.TemporaryAccountInfoResp{ ParentUser: stsAccount.ParentUser, AccountStatus: stsAccount.Status, ImpliedPolicy: sessionPolicy == nil, Policy: string(policyJSON), Expiration: &stsAccount.Expiration, } data, err := json.Marshal(infoResp) 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) } // AddServiceAccount - PUT /minio/admin/v3/add-service-account func (a adminAPIHandlers) AddServiceAccount(w http.ResponseWriter, r *http.Request) { ctx, cred, opts, createReq, targetUser, APIError := commonAddServiceAccount(r, false) if APIError.Code != "" { writeErrorResponseJSON(ctx, w, APIError, r.URL) return } if createReq.AccessKey == globalActiveCred.AccessKey { writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAddUserInvalidArgument), r.URL) return } var ( targetGroups []string err error ) // Find the user for the request sender (as it may be sent via a service // account or STS account): requestorUser := cred.AccessKey requestorParentUser := cred.AccessKey requestorGroups := cred.Groups requestorIsDerivedCredential := false if cred.IsServiceAccount() || cred.IsTemp() { requestorParentUser = cred.ParentUser requestorIsDerivedCredential = true } if globalIAMSys.GetUsersSysType() == MinIOUsersSysType && targetUser != cred.AccessKey { // For internal IDP, ensure that the targetUser's parent account exists. // It could be a regular user account or the root account. _, isRegularUser := globalIAMSys.GetUser(ctx, targetUser) if !isRegularUser && targetUser != globalActiveCred.AccessKey { apiErr := toAdminAPIErr(ctx, errNoSuchUser) apiErr.Description = fmt.Sprintf("Specified target user %s does not exist", targetUser) writeErrorResponseJSON(ctx, w, apiErr, r.URL) return } } // Check if we are creating svc account for request sender. isSvcAccForRequestor := false if targetUser == requestorUser || targetUser == requestorParentUser { isSvcAccForRequestor = true } // If we are creating svc account for request sender, ensure // that targetUser is a real user (i.e. not derived // credentials). if isSvcAccForRequestor { if requestorIsDerivedCredential { if requestorParentUser == "" { writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, errors.New("service accounts cannot be generated for temporary credentials without parent")), r.URL) return } targetUser = requestorParentUser } targetGroups = requestorGroups // In case of LDAP/OIDC we need to set `opts.claims` to ensure // it is associated with the LDAP/OIDC user properly. for k, v := range cred.Claims { if k == expClaim { continue } opts.claims[k] = v } } else if globalIAMSys.LDAPConfig.Enabled() { // In case of LDAP we need to resolve the targetUser to a DN and // query their groups: opts.claims[ldapUserN] = targetUser // simple username var lookupResult *xldap.DNSearchResult lookupResult, targetGroups, err = globalIAMSys.LDAPConfig.LookupUserDN(targetUser) if err != nil { writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL) return } targetUser = lookupResult.NormDN opts.claims[ldapUser] = targetUser // username DN opts.claims[ldapActualUser] = lookupResult.ActualDN // Add LDAP attributes that were looked up into the claims. for attribKey, attribValue := range lookupResult.Attributes { opts.claims[ldapAttribPrefix+attribKey] = attribValue } // NOTE: if not using LDAP, then internal IDP or open ID is // being used - in the former, group info is enforced when // generated credentials are used to make requests, and in the // latter, a group notion is not supported. } newCred, updatedAt, err := globalIAMSys.NewServiceAccount(ctx, targetUser, targetGroups, opts) if err != nil { writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL) return } createResp := madmin.AddServiceAccountResp{ Credentials: madmin.Credentials{ AccessKey: newCred.AccessKey, SecretKey: newCred.SecretKey, Expiration: newCred.Expiration, }, } data, err := json.Marshal(createResp) 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) // Call hook for cluster-replication if the service account is not for a // root user. if newCred.ParentUser != globalActiveCred.AccessKey { replLogIf(ctx, globalSiteReplicationSys.IAMChangeHook(ctx, madmin.SRIAMItem{ Type: madmin.SRIAMItemSvcAcc, SvcAccChange: &madmin.SRSvcAccChange{ Create: &madmin.SRSvcAccCreate{ Parent: newCred.ParentUser, AccessKey: newCred.AccessKey, SecretKey: newCred.SecretKey, Groups: newCred.Groups, Name: newCred.Name, Description: newCred.Description, Claims: opts.claims, SessionPolicy: createReq.Policy, Status: auth.AccountOn, Expiration: createReq.Expiration, }, }, UpdatedAt: updatedAt, })) } } // UpdateServiceAccount - POST /minio/admin/v3/update-service-account func (a adminAPIHandlers) UpdateServiceAccount(w http.ResponseWriter, r *http.Request) { ctx := r.Context() // Get current object layer instance. objectAPI := newObjectLayerFn() if objectAPI == nil || globalNotificationSys == 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 } accessKey := mux.Vars(r)["accessKey"] if accessKey == "" { writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrInvalidRequest), r.URL) return } svcAccount, _, err := globalIAMSys.GetServiceAccount(ctx, accessKey) if err != nil { writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL) return } password := cred.SecretKey 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 updateReq madmin.UpdateServiceAccountReq if err = json.Unmarshal(reqBytes, &updateReq); err != nil { writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErrWithErr(ErrAdminConfigBadJSON, err), r.URL) return } if err := updateReq.Validate(); err != nil { // Since this validation would happen client side as well, we only send // a generic error message here. writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAdminResourceInvalidArgument), r.URL) return } condValues := getConditionValues(r, "", cred) err = addExpirationToCondValues(updateReq.NewExpiration, condValues) if err != nil { writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL) return } // Permission checks: // // 1. Any type of account (i.e. access keys (previously/still called service // accounts), STS accounts, internal IDP accounts, etc) with the // policy.UpdateServiceAccountAdminAction permission can update any service // account. // // 2. We would like to let a user update their own access keys, however it // is currently blocked pending a re-design. Users are still able to delete // and re-create them. if !globalIAMSys.IsAllowed(policy.Args{ AccountName: cred.AccessKey, Groups: cred.Groups, Action: policy.UpdateServiceAccountAdminAction, ConditionValues: condValues, IsOwner: owner, Claims: cred.Claims, }) { writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAccessDenied), r.URL) return } var sp *policy.Policy if len(updateReq.NewPolicy) > 0 { sp, err = policy.ParseConfig(bytes.NewReader(updateReq.NewPolicy)) if err != nil { writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL) return } if sp.Version == "" && len(sp.Statements) == 0 { sp = nil } } opts := updateServiceAccountOpts{ secretKey: updateReq.NewSecretKey, status: updateReq.NewStatus, name: updateReq.NewName, description: updateReq.NewDescription, expiration: updateReq.NewExpiration, sessionPolicy: sp, } updatedAt, err := globalIAMSys.UpdateServiceAccount(ctx, accessKey, opts) if err != nil { writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL) return } // Call site replication hook - non-root user accounts are replicated. if svcAccount.ParentUser != globalActiveCred.AccessKey { replLogIf(ctx, globalSiteReplicationSys.IAMChangeHook(ctx, madmin.SRIAMItem{ Type: madmin.SRIAMItemSvcAcc, SvcAccChange: &madmin.SRSvcAccChange{ Update: &madmin.SRSvcAccUpdate{ AccessKey: accessKey, SecretKey: opts.secretKey, Status: opts.status, Name: opts.name, Description: opts.description, SessionPolicy: updateReq.NewPolicy, Expiration: updateReq.NewExpiration, }, }, UpdatedAt: updatedAt, })) } writeSuccessNoContent(w) } // InfoServiceAccount - GET /minio/admin/v3/info-service-account func (a adminAPIHandlers) InfoServiceAccount(w http.ResponseWriter, r *http.Request) { ctx := r.Context() // Get current object layer instance. objectAPI := newObjectLayerFn() if objectAPI == nil || globalNotificationSys == 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 } accessKey := mux.Vars(r)["accessKey"] if accessKey == "" { writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrInvalidRequest), r.URL) return } svcAccount, sessionPolicy, err := globalIAMSys.GetServiceAccount(ctx, accessKey) if err != nil { writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL) return } if !globalIAMSys.IsAllowed(policy.Args{ AccountName: cred.AccessKey, Groups: cred.Groups, Action: policy.ListServiceAccountsAdminAction, ConditionValues: getConditionValues(r, "", cred), IsOwner: owner, Claims: cred.Claims, }) { requestUser := cred.AccessKey if cred.ParentUser != "" { requestUser = cred.ParentUser } if requestUser != svcAccount.ParentUser { writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAccessDenied), r.URL) return } } // if session policy is nil or empty, then it is implied policy impliedPolicy := sessionPolicy == nil || (sessionPolicy.Version == "" && len(sessionPolicy.Statements) == 0) var svcAccountPolicy policy.Policy if !impliedPolicy { svcAccountPolicy = *sessionPolicy } else { policiesNames, err := globalIAMSys.PolicyDBGet(svcAccount.ParentUser, svcAccount.Groups...) if err != nil { writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL) return } svcAccountPolicy = globalIAMSys.GetCombinedPolicy(policiesNames...) } policyJSON, err := json.MarshalIndent(svcAccountPolicy, "", " ") if err != nil { writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL) return } var expiration *time.Time if !svcAccount.Expiration.IsZero() && !svcAccount.Expiration.Equal(timeSentinel) { expiration = &svcAccount.Expiration } infoResp := madmin.InfoServiceAccountResp{ ParentUser: svcAccount.ParentUser, Name: svcAccount.Name, Description: svcAccount.Description, AccountStatus: svcAccount.Status, ImpliedPolicy: impliedPolicy, Policy: string(policyJSON), Expiration: expiration, } data, err := json.Marshal(infoResp) 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) } // ListServiceAccounts - GET /minio/admin/v3/list-service-accounts func (a adminAPIHandlers) ListServiceAccounts(w http.ResponseWriter, r *http.Request) { ctx := r.Context() // Get current object layer instance. objectAPI := newObjectLayerFn() if objectAPI == nil || globalNotificationSys == 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 } var targetAccount string // If listing is requested for a specific user (who is not the request // sender), check that the user has permissions. user := r.Form.Get("user") if user != "" && user != cred.AccessKey { if !globalIAMSys.IsAllowed(policy.Args{ AccountName: cred.AccessKey, Groups: cred.Groups, Action: policy.ListServiceAccountsAdminAction, ConditionValues: getConditionValues(r, "", cred), IsOwner: owner, Claims: cred.Claims, }) { writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAccessDenied), r.URL) return } targetAccount = user } else { targetAccount = cred.AccessKey if cred.ParentUser != "" { targetAccount = cred.ParentUser } } serviceAccounts, err := globalIAMSys.ListServiceAccounts(ctx, targetAccount) if err != nil { writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL) return } var serviceAccountList []madmin.ServiceAccountInfo for _, svc := range serviceAccounts { expiryTime := svc.Expiration serviceAccountList = append(serviceAccountList, madmin.ServiceAccountInfo{ Description: svc.Description, ParentUser: svc.ParentUser, Name: svc.Name, AccountStatus: svc.Status, AccessKey: svc.AccessKey, ImpliedPolicy: svc.IsImpliedPolicy(), Expiration: &expiryTime, }) } listResp := madmin.ListServiceAccountsResp{ Accounts: serviceAccountList, } 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 := r.Context() // Get current object layer instance. objectAPI := newObjectLayerFn() if objectAPI == nil || globalNotificationSys == 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 } serviceAccount := mux.Vars(r)["accessKey"] if serviceAccount == "" { writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAdminInvalidArgument), r.URL) return } if serviceAccount == siteReplicatorSvcAcc && globalSiteReplicationSys.isEnabled() { writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrInvalidArgument), r.URL) return } // We do not care if service account is readable or not at this point, // since this is a delete call we shall allow it to be deleted if possible. svcAccount, _, err := globalIAMSys.GetServiceAccount(ctx, serviceAccount) if errors.Is(err, errNoSuchServiceAccount) { writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAdminServiceAccountNotFound), r.URL) return } adminPrivilege := globalIAMSys.IsAllowed(policy.Args{ AccountName: cred.AccessKey, Groups: cred.Groups, Action: policy.RemoveServiceAccountAdminAction, ConditionValues: getConditionValues(r, "", cred), IsOwner: owner, Claims: cred.Claims, }) if !adminPrivilege { parentUser := cred.AccessKey if cred.ParentUser != "" { parentUser = cred.ParentUser } if svcAccount.ParentUser != "" && parentUser != svcAccount.ParentUser { // The service account belongs to another user but return not // found error to mitigate brute force attacks. or the // serviceAccount doesn't exist. writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAdminServiceAccountNotFound), r.URL) return } } if err := globalIAMSys.DeleteServiceAccount(ctx, serviceAccount, true); err != nil { writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL) return } // Call site replication hook - non-root user accounts are replicated. if svcAccount.ParentUser != "" && svcAccount.ParentUser != globalActiveCred.AccessKey { replLogIf(ctx, globalSiteReplicationSys.IAMChangeHook(ctx, madmin.SRIAMItem{ Type: madmin.SRIAMItemSvcAcc, SvcAccChange: &madmin.SRSvcAccChange{ Delete: &madmin.SRSvcAccDelete{ AccessKey: serviceAccount, }, }, UpdatedAt: UTCNow(), })) } writeSuccessNoContent(w) } // ListAccessKeysBulk - GET /minio/admin/v3/list-access-keys-bulk func (a adminAPIHandlers) ListAccessKeysBulk(w http.ResponseWriter, r *http.Request) { ctx := r.Context() // Get current object layer instance. objectAPI := newObjectLayerFn() if objectAPI == nil || globalNotificationSys == 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 } users := r.Form["users"] isAll := r.Form.Get("all") == "true" selfOnly := !isAll && len(users) == 0 if isAll && len(users) > 0 { // This should be checked on client side, so return generic error writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrInvalidRequest), r.URL) return } // Empty user list and not self, list access keys for all users if isAll { if !globalIAMSys.IsAllowed(policy.Args{ AccountName: cred.AccessKey, Groups: cred.Groups, Action: policy.ListUsersAdminAction, ConditionValues: getConditionValues(r, "", cred), IsOwner: owner, Claims: cred.Claims, }) { writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAccessDenied), r.URL) return } } else if len(users) == 1 { if users[0] == cred.AccessKey || users[0] == cred.ParentUser { selfOnly = true } } if !globalIAMSys.IsAllowed(policy.Args{ AccountName: cred.AccessKey, Groups: cred.Groups, Action: policy.ListServiceAccountsAdminAction, ConditionValues: getConditionValues(r, "", cred), IsOwner: owner, Claims: cred.Claims, DenyOnly: selfOnly, }) { writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAccessDenied), r.URL) return } if selfOnly && len(users) == 0 { selfUser := cred.AccessKey if cred.ParentUser != "" { selfUser = cred.ParentUser } users = append(users, selfUser) } var checkedUserList []string if isAll { users, err := globalIAMSys.ListUsers(ctx) if err != nil { writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL) return } for user := range users { checkedUserList = append(checkedUserList, user) } checkedUserList = append(checkedUserList, globalActiveCred.AccessKey) } else { for _, user := range users { // Validate the user _, ok := globalIAMSys.GetUser(ctx, user) if !ok { continue } checkedUserList = append(checkedUserList, user) } } listType := r.Form.Get("listType") var listSTSKeys, listServiceAccounts bool switch listType { case madmin.AccessKeyListUsersOnly: listSTSKeys = false listServiceAccounts = false case madmin.AccessKeyListSTSOnly: listSTSKeys = true listServiceAccounts = false case madmin.AccessKeyListSvcaccOnly: listSTSKeys = false listServiceAccounts = true case madmin.AccessKeyListAll: listSTSKeys = true listServiceAccounts = true default: err := errors.New("invalid list type") writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErrWithErr(ErrInvalidRequest, err), r.URL) return } accessKeyMap := make(map[string]madmin.ListAccessKeysResp) for _, user := range checkedUserList { accessKeys := madmin.ListAccessKeysResp{} if listSTSKeys { stsKeys, err := globalIAMSys.ListSTSAccounts(ctx, user) if err != nil { writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL) return } for _, sts := range stsKeys { accessKeys.STSKeys = append(accessKeys.STSKeys, madmin.ServiceAccountInfo{ AccessKey: sts.AccessKey, Expiration: &sts.Expiration, }) } // if only STS keys, skip if user has no STS keys if !listServiceAccounts && len(stsKeys) == 0 { continue } } if listServiceAccounts { serviceAccounts, err := globalIAMSys.ListServiceAccounts(ctx, user) if err != nil { writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL) return } for _, svc := range serviceAccounts { accessKeys.ServiceAccounts = append(accessKeys.ServiceAccounts, madmin.ServiceAccountInfo{ AccessKey: svc.AccessKey, Expiration: &svc.Expiration, }) } // if only service accounts, skip if user has no service accounts if !listSTSKeys && len(serviceAccounts) == 0 { continue } } accessKeyMap[user] = accessKeys } data, err := json.Marshal(accessKeyMap) 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) } // AccountInfoHandler returns usage, permissions and other bucket metadata for incoming us func (a adminAPIHandlers) AccountInfoHandler(w http.ResponseWriter, r *http.Request) { ctx := r.Context() // Get current object layer instance. objectAPI := newObjectLayerFn() if objectAPI == nil || globalNotificationSys == 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 } // Set prefix value for "s3:prefix" policy conditionals. r.Header.Set("prefix", "") // Set delimiter value for "s3:delimiter" policy conditionals. r.Header.Set("delimiter", SlashSeparator) // Check if we are asked to return prefix usage enablePrefixUsage := r.Form.Get("prefix-usage") == "true" isAllowedAccess := func(bucketName string) (rd, wr bool) { if globalIAMSys.IsAllowed(policy.Args{ AccountName: cred.AccessKey, Groups: cred.Groups, Action: policy.ListBucketAction, BucketName: bucketName, ConditionValues: getConditionValues(r, "", cred), IsOwner: owner, ObjectName: "", Claims: cred.Claims, }) { rd = true } if globalIAMSys.IsAllowed(policy.Args{ AccountName: cred.AccessKey, Groups: cred.Groups, Action: policy.GetBucketLocationAction, BucketName: bucketName, ConditionValues: getConditionValues(r, "", cred), IsOwner: owner, ObjectName: "", Claims: cred.Claims, }) { rd = true } if globalIAMSys.IsAllowed(policy.Args{ AccountName: cred.AccessKey, Groups: cred.Groups, Action: policy.PutObjectAction, BucketName: bucketName, ConditionValues: getConditionValues(r, "", cred), IsOwner: owner, ObjectName: "", Claims: cred.Claims, }) { wr = true } return rd, wr } // If etcd, dns federation configured list buckets from etcd. var err error var buckets []BucketInfo if globalDNSConfig != nil && globalBucketFederation { dnsBuckets, err := globalDNSConfig.List() if err != nil && !IsErrIgnored(err, dns.ErrNoEntriesFound, dns.ErrDomainMissing) { writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL) return } for _, dnsRecords := range dnsBuckets { buckets = append(buckets, BucketInfo{ Name: dnsRecords[0].Key, Created: dnsRecords[0].CreationDate, }) } sort.Slice(buckets, func(i, j int) bool { return buckets[i].Name < buckets[j].Name }) } else { buckets, err = objectAPI.ListBuckets(ctx, BucketOptions{Cached: true}) if err != nil { writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL) return } } accountName := cred.AccessKey if cred.IsTemp() || cred.IsServiceAccount() { // For derived credentials, check the parent user's permissions. accountName = cred.ParentUser } roleArn := policy.Args{Claims: cred.Claims}.GetRoleArn() policySetFromClaims, hasPolicyClaim := policy.GetPoliciesFromClaims(cred.Claims, iamPolicyClaimNameOpenID()) var effectivePolicy policy.Policy var buf []byte switch { case accountName == globalActiveCred.AccessKey || newGlobalAuthZPluginFn() != nil: // For owner account and when plugin authZ is configured always set // effective policy as `consoleAdmin`. // // In the latter case, we let the UI render everything, but individual // actions would fail if not permitted by the external authZ service. for _, policy := range policy.DefaultPolicies { if policy.Name == "consoleAdmin" { effectivePolicy = policy.Definition break } } case roleArn != "": _, policy, err := globalIAMSys.GetRolePolicy(roleArn) if err != nil { writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL) return } policySlice := newMappedPolicy(policy).toSlice() effectivePolicy = globalIAMSys.GetCombinedPolicy(policySlice...) case hasPolicyClaim: effectivePolicy = globalIAMSys.GetCombinedPolicy(policySetFromClaims.ToSlice()...) default: policies, err := globalIAMSys.PolicyDBGet(accountName, cred.Groups...) if err != nil { adminLogIf(ctx, err) writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL) return } effectivePolicy = globalIAMSys.GetCombinedPolicy(policies...) } buf, err = json.MarshalIndent(effectivePolicy, "", " ") if err != nil { writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL) return } acctInfo := madmin.AccountInfo{ AccountName: accountName, Server: objectAPI.BackendInfo(), Policy: buf, } for _, bucket := range buckets { rd, wr := isAllowedAccess(bucket.Name) if rd || wr { // Fetch the data usage of the current bucket bui := globalBucketQuotaSys.GetBucketUsageInfo(ctx, bucket.Name) size := bui.Size objectsCount := bui.ObjectsCount objectsHist := bui.ObjectSizesHistogram versionsHist := bui.ObjectVersionsHistogram // Fetch the prefix usage of the current bucket var prefixUsage map[string]uint64 if enablePrefixUsage { prefixUsage, _ = loadPrefixUsageFromBackend(ctx, objectAPI, bucket.Name) } lcfg, _ := globalBucketObjectLockSys.Get(bucket.Name) quota, _ := globalBucketQuotaSys.Get(ctx, bucket.Name) rcfg, _, _ := globalBucketMetadataSys.GetReplicationConfig(ctx, bucket.Name) tcfg, _, _ := globalBucketMetadataSys.GetTaggingConfig(bucket.Name) acctInfo.Buckets = append(acctInfo.Buckets, madmin.BucketAccessInfo{ Name: bucket.Name, Created: bucket.Created, Size: size, Objects: objectsCount, ObjectSizesHistogram: objectsHist, ObjectVersionsHistogram: versionsHist, PrefixUsage: prefixUsage, Details: &madmin.BucketDetails{ Versioning: globalBucketVersioningSys.Enabled(bucket.Name), VersioningSuspended: globalBucketVersioningSys.Suspended(bucket.Name), Replication: rcfg != nil, Locking: lcfg.LockEnabled, Quota: quota, Tagging: tcfg, }, Access: madmin.AccountAccess{ Read: rd, Write: wr, }, }) } } usageInfoJSON, err := json.Marshal(acctInfo) if err != nil { writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL) return } writeSuccessResponseJSON(w, usageInfoJSON) } // InfoCannedPolicy - GET /minio/admin/v3/info-canned-policy?name={policyName} // // Newer API response with policy timestamps is returned with query parameter // `v=2` like: // // GET /minio/admin/v3/info-canned-policy?name={policyName}&v=2 // // The newer API will eventually become the default (and only) one. The older // response is to return only the policy JSON. The newer response returns // timestamps along with the policy JSON. Both versions are supported for now, // for smooth transition to new API. func (a adminAPIHandlers) InfoCannedPolicy(w http.ResponseWriter, r *http.Request) { ctx := r.Context() objectAPI, _ := validateAdminReq(ctx, w, r, policy.GetPolicyAdminAction) if objectAPI == nil { return } name := mux.Vars(r)["name"] policies := newMappedPolicy(name).toSlice() if len(policies) != 1 { writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, errTooManyPolicies), r.URL) return } policyDoc, err := globalIAMSys.InfoPolicy(name) if err != nil { writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL) return } // Is the new API version being requested? infoPolicyAPIVersion := r.Form.Get("v") if infoPolicyAPIVersion == "2" { buf, err := json.MarshalIndent(policyDoc, "", " ") if err != nil { writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL) return } w.Write(buf) return } else if infoPolicyAPIVersion != "" { writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, errors.New("invalid version parameter 'v' supplied")), r.URL) return } // Return the older API response value of just the policy json. buf, err := json.MarshalIndent(policyDoc.Policy, "", " ") if err != nil { writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL) return } w.Write(buf) } // ListBucketPolicies - GET /minio/admin/v3/list-canned-policies?bucket={bucket} func (a adminAPIHandlers) ListBucketPolicies(w http.ResponseWriter, r *http.Request) { ctx := r.Context() objectAPI, _ := validateAdminReq(ctx, w, r, policy.ListUserPoliciesAdminAction) if objectAPI == nil { return } bucket := mux.Vars(r)["bucket"] policies, err := globalIAMSys.ListPolicies(ctx, bucket) if err != nil { writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL) return } newPolicies := make(map[string]policy.Policy) for name, p := range policies { _, err = json.Marshal(p) if err != nil { adminLogIf(ctx, err) continue } newPolicies[name] = p } if err = json.NewEncoder(w).Encode(newPolicies); err != nil { writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL) return } } // ListCannedPolicies - GET /minio/admin/v3/list-canned-policies func (a adminAPIHandlers) ListCannedPolicies(w http.ResponseWriter, r *http.Request) { ctx := r.Context() objectAPI, _ := validateAdminReq(ctx, w, r, policy.ListUserPoliciesAdminAction) if objectAPI == nil { return } policies, err := globalIAMSys.ListPolicies(ctx, "") if err != nil { writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL) return } newPolicies := make(map[string]policy.Policy) for name, p := range policies { _, err = json.Marshal(p) if err != nil { adminLogIf(ctx, err) continue } newPolicies[name] = p } if err = json.NewEncoder(w).Encode(newPolicies); err != nil { writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL) return } } // RemoveCannedPolicy - DELETE /minio/admin/v3/remove-canned-policy?name= func (a adminAPIHandlers) RemoveCannedPolicy(w http.ResponseWriter, r *http.Request) { ctx := r.Context() objectAPI, _ := validateAdminReq(ctx, w, r, policy.DeletePolicyAdminAction) if objectAPI == nil { return } vars := mux.Vars(r) policyName := vars["name"] if err := globalIAMSys.DeletePolicy(ctx, policyName, true); err != nil { writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL) return } // Call cluster-replication policy creation hook to replicate policy deletion to // other minio clusters. replLogIf(ctx, globalSiteReplicationSys.IAMChangeHook(ctx, madmin.SRIAMItem{ Type: madmin.SRIAMItemPolicy, Name: policyName, UpdatedAt: UTCNow(), })) } // AddCannedPolicy - PUT /minio/admin/v3/add-canned-policy?name= func (a adminAPIHandlers) AddCannedPolicy(w http.ResponseWriter, r *http.Request) { ctx := r.Context() objectAPI, _ := validateAdminReq(ctx, w, r, policy.CreatePolicyAdminAction) if objectAPI == nil { return } vars := mux.Vars(r) policyName := vars["name"] // Policy has space characters in begin and end reject such inputs. if hasSpaceBE(policyName) { writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAdminResourceInvalidArgument), r.URL) return } // Error out if Content-Length is missing. if r.ContentLength <= 0 { writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrMissingContentLength), r.URL) return } // Error out if Content-Length is beyond allowed size. if r.ContentLength > maxBucketPolicySize { writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrEntityTooLarge), r.URL) return } iamPolicyBytes, err := io.ReadAll(io.LimitReader(r.Body, r.ContentLength)) if err != nil { writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL) return } iamPolicy, err := policy.ParseConfig(bytes.NewReader(iamPolicyBytes)) if err != nil { writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL) return } // Version in policy must not be empty if iamPolicy.Version == "" { writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrPolicyInvalidVersion), r.URL) return } updatedAt, err := globalIAMSys.SetPolicy(ctx, policyName, *iamPolicy) if err != nil { writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL) return } // Call cluster-replication policy creation hook to replicate policy to // other minio clusters. replLogIf(ctx, globalSiteReplicationSys.IAMChangeHook(ctx, madmin.SRIAMItem{ Type: madmin.SRIAMItemPolicy, Name: policyName, Policy: iamPolicyBytes, UpdatedAt: updatedAt, })) } // SetPolicyForUserOrGroup - sets a policy on a user or a group. // // PUT /minio/admin/v3/set-policy?policy=xxx&user-or-group=?[&is-group] // // Deprecated: This API is replaced by attach/detach policy APIs for specific // type of users (builtin or LDAP). func (a adminAPIHandlers) SetPolicyForUserOrGroup(w http.ResponseWriter, r *http.Request) { ctx := r.Context() objectAPI, _ := validateAdminReq(ctx, w, r, policy.AttachPolicyAdminAction) if objectAPI == nil { return } vars := mux.Vars(r) policyName := vars["policyName"] entityName := vars["userOrGroup"] isGroup := vars["isGroup"] == "true" if !isGroup { ok, _, err := globalIAMSys.IsTempUser(entityName) if err != nil && err != errNoSuchUser { writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL) return } if ok { writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, errIAMActionNotAllowed), r.URL) return } // When the user is root credential you are not allowed to // add policies for root user. if entityName == globalActiveCred.AccessKey { writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, errIAMActionNotAllowed), r.URL) return } } // Validate that user or group exists. if !isGroup { if globalIAMSys.GetUsersSysType() == MinIOUsersSysType { _, ok := globalIAMSys.GetUser(ctx, entityName) if !ok { writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, errNoSuchUser), r.URL) return } } } else { _, err := globalIAMSys.GetGroupDescription(entityName) if err != nil { writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL) return } } userType := regUser if globalIAMSys.GetUsersSysType() == LDAPUsersSysType { userType = stsUser // Validate that the user or group exists in LDAP and use the normalized // form of the entityName (which will be an LDAP DN). var err error if isGroup { var foundGroupDN *xldap.DNSearchResult var underBaseDN bool if foundGroupDN, underBaseDN, err = globalIAMSys.LDAPConfig.GetValidatedGroupDN(nil, entityName); err != nil { iamLogIf(ctx, err) } else if foundGroupDN == nil || !underBaseDN { err = errNoSuchGroup } entityName = foundGroupDN.NormDN } else { var foundUserDN *xldap.DNSearchResult if foundUserDN, err = globalIAMSys.LDAPConfig.GetValidatedDNForUsername(entityName); err != nil { iamLogIf(ctx, err) } else if foundUserDN == nil { err = errNoSuchUser } entityName = foundUserDN.NormDN } if err != nil { writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL) return } } updatedAt, err := globalIAMSys.PolicyDBSet(ctx, entityName, policyName, userType, isGroup) if err != nil { writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL) return } replLogIf(ctx, globalSiteReplicationSys.IAMChangeHook(ctx, madmin.SRIAMItem{ Type: madmin.SRIAMItemPolicyMapping, PolicyMapping: &madmin.SRPolicyMapping{ UserOrGroup: entityName, UserType: int(userType), IsGroup: isGroup, Policy: policyName, }, UpdatedAt: updatedAt, })) } // ListPolicyMappingEntities - GET /minio/admin/v3/idp/builtin/polciy-entities?policy=xxx&user=xxx&group=xxx func (a adminAPIHandlers) ListPolicyMappingEntities(w http.ResponseWriter, r *http.Request) { ctx := r.Context() // Check authorization. objectAPI, cred := validateAdminReq(ctx, w, r, policy.ListGroupsAdminAction, policy.ListUsersAdminAction, policy.ListUserPoliciesAdminAction) if objectAPI == nil { return } // Validate API arguments. q := madmin.PolicyEntitiesQuery{ Users: r.Form["user"], Groups: r.Form["group"], Policy: r.Form["policy"], } // Query IAM res, err := globalIAMSys.QueryPolicyEntities(r.Context(), q) if err != nil { writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL) return } // Encode result and send response. data, err := json.Marshal(res) if err != nil { writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL) return } password := cred.SecretKey econfigData, err := madmin.EncryptData(password, data) if err != nil { writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL) return } writeSuccessResponseJSON(w, econfigData) } // AttachDetachPolicyBuiltin - POST /minio/admin/v3/idp/builtin/policy/{operation} func (a adminAPIHandlers) AttachDetachPolicyBuiltin(w http.ResponseWriter, r *http.Request) { ctx := r.Context() objectAPI, cred := validateAdminReq(ctx, w, r, policy.UpdatePolicyAssociationAction, policy.AttachPolicyAdminAction) if objectAPI == nil { return } if r.ContentLength > maxEConfigJSONSize || r.ContentLength == -1 { // More than maxConfigSize bytes were available writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAdminConfigTooLarge), r.URL) return } // Ensure body content type is opaque to ensure that request body has not // been interpreted as form data. contentType := r.Header.Get("Content-Type") if contentType != "application/octet-stream" { writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrBadRequest), r.URL) return } operation := mux.Vars(r)["operation"] if operation != "attach" && operation != "detach" { writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAdminInvalidArgument), r.URL) return } isAttach := operation == "attach" password := cred.SecretKey reqBytes, err := madmin.DecryptData(password, io.LimitReader(r.Body, r.ContentLength)) if err != nil { writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL) return } var par madmin.PolicyAssociationReq if err = json.Unmarshal(reqBytes, &par); err != nil { writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL) return } if err = par.IsValid(); err != nil { writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL) return } updatedAt, addedOrRemoved, _, err := globalIAMSys.PolicyDBUpdateBuiltin(ctx, isAttach, par) if err != nil { if err == errNoSuchUser || err == errNoSuchGroup { if globalIAMSys.LDAPConfig.Enabled() { // When LDAP is enabled, warn user that they are using the wrong // API. FIXME: error can be no such group as well - fix errNoSuchUserLDAPWarn writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, errNoSuchUserLDAPWarn), r.URL) return } } writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL) return } respBody := madmin.PolicyAssociationResp{ UpdatedAt: updatedAt, } if isAttach { respBody.PoliciesAttached = addedOrRemoved } else { respBody.PoliciesDetached = addedOrRemoved } data, err := json.Marshal(respBody) if err != nil { writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL) return } encryptedData, err := madmin.EncryptData(password, data) if err != nil { writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL) return } writeSuccessResponseJSON(w, encryptedData) } const ( allPoliciesFile = "policies.json" allUsersFile = "users.json" allGroupsFile = "groups.json" allSvcAcctsFile = "svcaccts.json" userPolicyMappingsFile = "user_mappings.json" groupPolicyMappingsFile = "group_mappings.json" stsUserPolicyMappingsFile = "stsuser_mappings.json" iamAssetsDir = "iam-assets" ) var iamExportFiles = []string{ allPoliciesFile, allUsersFile, allGroupsFile, allSvcAcctsFile, userPolicyMappingsFile, groupPolicyMappingsFile, stsUserPolicyMappingsFile, } // ExportIAMHandler - exports all iam info as a zipped file func (a adminAPIHandlers) ExportIAM(w http.ResponseWriter, r *http.Request) { ctx := r.Context() // Get current object layer instance. objectAPI, _ := validateAdminReq(ctx, w, r, policy.ExportIAMAction) if objectAPI == nil { return } // Initialize a zip writer which will provide a zipped content // of bucket metadata zipWriter := zip.NewWriter(w) defer zipWriter.Close() rawDataFn := func(r io.Reader, filename string, sz int) error { header, zerr := zip.FileInfoHeader(dummyFileInfo{ name: filename, size: int64(sz), mode: 0o600, modTime: time.Now(), isDir: false, sys: nil, }) if zerr != nil { adminLogIf(ctx, zerr) return nil } header.Method = zip.Deflate zwriter, zerr := zipWriter.CreateHeader(header) if zerr != nil { adminLogIf(ctx, zerr) return nil } if _, err := io.Copy(zwriter, r); err != nil { adminLogIf(ctx, err) } return nil } for _, f := range iamExportFiles { iamFile := pathJoin(iamAssetsDir, f) switch f { case allPoliciesFile: allPolicies, err := globalIAMSys.ListPolicies(ctx, "") if err != nil { adminLogIf(ctx, err) writeErrorResponse(ctx, w, exportError(ctx, err, iamFile, ""), r.URL) return } policiesData, err := json.Marshal(allPolicies) if err != nil { writeErrorResponse(ctx, w, exportError(ctx, err, iamFile, ""), r.URL) return } if err = rawDataFn(bytes.NewReader(policiesData), iamFile, len(policiesData)); err != nil { writeErrorResponse(ctx, w, exportError(ctx, err, iamFile, ""), r.URL) return } case allUsersFile: userIdentities := make(map[string]UserIdentity) err := globalIAMSys.store.loadUsers(ctx, regUser, userIdentities) if err != nil { writeErrorResponse(ctx, w, exportError(ctx, err, iamFile, ""), r.URL) return } userAccounts := make(map[string]madmin.AddOrUpdateUserReq) for u, uid := range userIdentities { userAccounts[u] = madmin.AddOrUpdateUserReq{ SecretKey: uid.Credentials.SecretKey, Status: func() madmin.AccountStatus { // Export current credential status if uid.Credentials.Status == auth.AccountOff { return madmin.AccountDisabled } return madmin.AccountEnabled }(), } } userData, err := json.Marshal(userAccounts) if err != nil { writeErrorResponse(ctx, w, exportError(ctx, err, iamFile, ""), r.URL) return } if err = rawDataFn(bytes.NewReader(userData), iamFile, len(userData)); err != nil { writeErrorResponse(ctx, w, exportError(ctx, err, iamFile, ""), r.URL) return } case allGroupsFile: groups := make(map[string]GroupInfo) err := globalIAMSys.store.loadGroups(ctx, groups) if err != nil { writeErrorResponse(ctx, w, exportError(ctx, err, iamFile, ""), r.URL) return } groupData, err := json.Marshal(groups) if err != nil { writeErrorResponse(ctx, w, exportError(ctx, err, iamFile, ""), r.URL) return } if err = rawDataFn(bytes.NewReader(groupData), iamFile, len(groupData)); err != nil { writeErrorResponse(ctx, w, exportError(ctx, err, iamFile, ""), r.URL) return } case allSvcAcctsFile: serviceAccounts := make(map[string]UserIdentity) err := globalIAMSys.store.loadUsers(ctx, svcUser, serviceAccounts) if err != nil { writeErrorResponse(ctx, w, exportError(ctx, err, iamFile, ""), r.URL) return } svcAccts := make(map[string]madmin.SRSvcAccCreate) for user, acc := range serviceAccounts { if user == siteReplicatorSvcAcc { // skip site-replication service account. continue } claims, err := globalIAMSys.GetClaimsForSvcAcc(ctx, acc.Credentials.AccessKey) if err != nil { writeErrorResponse(ctx, w, exportError(ctx, err, iamFile, ""), r.URL) return } sa, policy, err := globalIAMSys.GetServiceAccount(ctx, acc.Credentials.AccessKey) if err != nil { writeErrorResponse(ctx, w, exportError(ctx, err, iamFile, ""), r.URL) return } var policyJSON []byte if policy != nil { policyJSON, err = json.Marshal(policy) if err != nil { writeErrorResponse(ctx, w, exportError(ctx, err, iamFile, ""), r.URL) return } } svcAccts[user] = madmin.SRSvcAccCreate{ Parent: acc.Credentials.ParentUser, AccessKey: user, SecretKey: acc.Credentials.SecretKey, Groups: acc.Credentials.Groups, Claims: claims, SessionPolicy: json.RawMessage(policyJSON), Status: acc.Credentials.Status, Name: sa.Name, Description: sa.Description, Expiration: &sa.Expiration, } } svcAccData, err := json.Marshal(svcAccts) if err != nil { writeErrorResponse(ctx, w, exportError(ctx, err, iamFile, ""), r.URL) return } if err = rawDataFn(bytes.NewReader(svcAccData), iamFile, len(svcAccData)); err != nil { writeErrorResponse(ctx, w, exportError(ctx, err, iamFile, ""), r.URL) return } case userPolicyMappingsFile: userPolicyMap := xsync.NewMapOf[string, MappedPolicy]() err := globalIAMSys.store.loadMappedPolicies(ctx, regUser, false, userPolicyMap) if err != nil { writeErrorResponse(ctx, w, exportError(ctx, err, iamFile, ""), r.URL) return } userPolData, err := json.Marshal(mappedPoliciesToMap(userPolicyMap)) if err != nil { writeErrorResponse(ctx, w, exportError(ctx, err, iamFile, ""), r.URL) return } if err = rawDataFn(bytes.NewReader(userPolData), iamFile, len(userPolData)); err != nil { writeErrorResponse(ctx, w, exportError(ctx, err, iamFile, ""), r.URL) return } case groupPolicyMappingsFile: groupPolicyMap := xsync.NewMapOf[string, MappedPolicy]() err := globalIAMSys.store.loadMappedPolicies(ctx, regUser, true, groupPolicyMap) if err != nil { writeErrorResponse(ctx, w, exportError(ctx, err, iamFile, ""), r.URL) return } grpPolData, err := json.Marshal(mappedPoliciesToMap(groupPolicyMap)) if err != nil { writeErrorResponse(ctx, w, exportError(ctx, err, iamFile, ""), r.URL) return } if err = rawDataFn(bytes.NewReader(grpPolData), iamFile, len(grpPolData)); err != nil { writeErrorResponse(ctx, w, exportError(ctx, err, iamFile, ""), r.URL) return } case stsUserPolicyMappingsFile: userPolicyMap := xsync.NewMapOf[string, MappedPolicy]() err := globalIAMSys.store.loadMappedPolicies(ctx, stsUser, false, userPolicyMap) if err != nil { writeErrorResponse(ctx, w, exportError(ctx, err, iamFile, ""), r.URL) return } userPolData, err := json.Marshal(mappedPoliciesToMap(userPolicyMap)) if err != nil { writeErrorResponse(ctx, w, exportError(ctx, err, iamFile, ""), r.URL) return } if err = rawDataFn(bytes.NewReader(userPolData), iamFile, len(userPolData)); err != nil { writeErrorResponse(ctx, w, exportError(ctx, err, iamFile, ""), r.URL) return } } } } // ImportIAM - imports all IAM info into MinIO func (a adminAPIHandlers) ImportIAM(w http.ResponseWriter, r *http.Request) { a.importIAM(w, r, "") } // ImportIAMV2 - imports all IAM info into MinIO func (a adminAPIHandlers) ImportIAMV2(w http.ResponseWriter, r *http.Request) { a.importIAM(w, r, "v2") } // ImportIAM - imports all IAM info into MinIO func (a adminAPIHandlers) importIAM(w http.ResponseWriter, r *http.Request, apiVer string) { ctx := r.Context() // Get current object layer instance. objectAPI := newObjectLayerFn() if objectAPI == nil || globalNotificationSys == 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 } data, err := io.ReadAll(r.Body) if err != nil { writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrInvalidRequest), r.URL) return } reader := bytes.NewReader(data) zr, err := zip.NewReader(reader, int64(len(data))) if err != nil { writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrInvalidRequest), r.URL) return } var skipped, removed, added madmin.IAMEntities var failed madmin.IAMErrEntities // import policies first { f, err := zr.Open(pathJoin(iamAssetsDir, allPoliciesFile)) switch { case errors.Is(err, os.ErrNotExist): case err != nil: writeErrorResponseJSON(ctx, w, importErrorWithAPIErr(ctx, ErrInvalidRequest, err, allPoliciesFile, ""), r.URL) return default: defer f.Close() var allPolicies map[string]policy.Policy data, err = io.ReadAll(f) if err != nil { writeErrorResponseJSON(ctx, w, importErrorWithAPIErr(ctx, ErrInvalidRequest, err, allPoliciesFile, ""), r.URL) return } err = json.Unmarshal(data, &allPolicies) if err != nil { writeErrorResponseJSON(ctx, w, importErrorWithAPIErr(ctx, ErrAdminConfigBadJSON, err, allPoliciesFile, ""), r.URL) return } for policyName, policy := range allPolicies { if policy.IsEmpty() { err = globalIAMSys.DeletePolicy(ctx, policyName, true) removed.Policies = append(removed.Policies, policyName) } else { _, err = globalIAMSys.SetPolicy(ctx, policyName, policy) added.Policies = append(added.Policies, policyName) } if err != nil { writeErrorResponseJSON(ctx, w, importError(ctx, err, allPoliciesFile, policyName), r.URL) return } } } } // import users { f, err := zr.Open(pathJoin(iamAssetsDir, allUsersFile)) switch { case errors.Is(err, os.ErrNotExist): case err != nil: writeErrorResponseJSON(ctx, w, importErrorWithAPIErr(ctx, ErrInvalidRequest, err, allUsersFile, ""), r.URL) return default: defer f.Close() var userAccts map[string]madmin.AddOrUpdateUserReq data, err := io.ReadAll(f) if err != nil { writeErrorResponseJSON(ctx, w, importErrorWithAPIErr(ctx, ErrInvalidRequest, err, allUsersFile, ""), r.URL) return } err = json.Unmarshal(data, &userAccts) if err != nil { writeErrorResponseJSON(ctx, w, importErrorWithAPIErr(ctx, ErrAdminConfigBadJSON, err, allUsersFile, ""), r.URL) return } for accessKey, ureq := range userAccts { // Not allowed to add a user with same access key as root credential if accessKey == globalActiveCred.AccessKey { writeErrorResponseJSON(ctx, w, importErrorWithAPIErr(ctx, ErrAddUserInvalidArgument, err, allUsersFile, accessKey), r.URL) return } user, exists := globalIAMSys.GetUser(ctx, accessKey) if exists && (user.Credentials.IsTemp() || user.Credentials.IsServiceAccount()) { // Updating STS credential is not allowed, and this API does not // support updating service accounts. writeErrorResponseJSON(ctx, w, importErrorWithAPIErr(ctx, ErrAddUserInvalidArgument, err, allUsersFile, accessKey), r.URL) return } if (cred.IsTemp() || cred.IsServiceAccount()) && cred.ParentUser == accessKey { // Incoming access key matches parent user then we should // reject password change requests. writeErrorResponseJSON(ctx, w, importErrorWithAPIErr(ctx, ErrAddUserInvalidArgument, err, allUsersFile, accessKey), r.URL) return } // Check if accessKey has beginning and end space characters, this only applies to new users. if !exists && hasSpaceBE(accessKey) { writeErrorResponseJSON(ctx, w, importErrorWithAPIErr(ctx, ErrAdminResourceInvalidArgument, err, allUsersFile, accessKey), r.URL) return } checkDenyOnly := false if accessKey == cred.AccessKey { // Check that there is no explicit deny - otherwise it's allowed // to change one's own password. checkDenyOnly = true } if !globalIAMSys.IsAllowed(policy.Args{ AccountName: cred.AccessKey, Groups: cred.Groups, Action: policy.CreateUserAdminAction, ConditionValues: getConditionValues(r, "", cred), IsOwner: owner, Claims: cred.Claims, DenyOnly: checkDenyOnly, }) { writeErrorResponseJSON(ctx, w, importErrorWithAPIErr(ctx, ErrAccessDenied, err, allUsersFile, accessKey), r.URL) return } if _, err = globalIAMSys.CreateUser(ctx, accessKey, ureq); err != nil { failed.Users = append(failed.Users, madmin.IAMErrEntity{Name: accessKey, Error: err}) } else { added.Users = append(added.Users, accessKey) } } } } // import groups { f, err := zr.Open(pathJoin(iamAssetsDir, allGroupsFile)) switch { case errors.Is(err, os.ErrNotExist): case err != nil: writeErrorResponseJSON(ctx, w, importErrorWithAPIErr(ctx, ErrInvalidRequest, err, allGroupsFile, ""), r.URL) return default: defer f.Close() var grpInfos map[string]GroupInfo data, err := io.ReadAll(f) if err != nil { writeErrorResponseJSON(ctx, w, importErrorWithAPIErr(ctx, ErrInvalidRequest, err, allGroupsFile, ""), r.URL) return } if err = json.Unmarshal(data, &grpInfos); err != nil { writeErrorResponseJSON(ctx, w, importErrorWithAPIErr(ctx, ErrAdminConfigBadJSON, err, allGroupsFile, ""), r.URL) return } for group, grpInfo := range grpInfos { // Check if group already exists if _, gerr := globalIAMSys.GetGroupDescription(group); gerr != nil { // If group does not exist, then check if the group has beginning and end space characters // we will reject such group names. if errors.Is(gerr, errNoSuchGroup) && hasSpaceBE(group) { writeErrorResponseJSON(ctx, w, importErrorWithAPIErr(ctx, ErrAdminResourceInvalidArgument, gerr, allGroupsFile, group), r.URL) return } } if _, gerr := globalIAMSys.AddUsersToGroup(ctx, group, grpInfo.Members); gerr != nil { failed.Groups = append(failed.Groups, madmin.IAMErrEntity{Name: group, Error: err}) } else { added.Groups = append(added.Groups, group) } } } } // import service accounts { f, err := zr.Open(pathJoin(iamAssetsDir, allSvcAcctsFile)) switch { case errors.Is(err, os.ErrNotExist): case err != nil: writeErrorResponseJSON(ctx, w, importErrorWithAPIErr(ctx, ErrInvalidRequest, err, allSvcAcctsFile, ""), r.URL) return default: defer f.Close() var serviceAcctReqs map[string]madmin.SRSvcAccCreate data, err := io.ReadAll(f) if err != nil { writeErrorResponseJSON(ctx, w, importErrorWithAPIErr(ctx, ErrInvalidRequest, err, allSvcAcctsFile, ""), r.URL) return } if err = json.Unmarshal(data, &serviceAcctReqs); err != nil { writeErrorResponseJSON(ctx, w, importErrorWithAPIErr(ctx, ErrAdminConfigBadJSON, err, allSvcAcctsFile, ""), r.URL) return } // Validations for LDAP enabled deployments. if globalIAMSys.LDAPConfig.Enabled() { skippedAccessKeys, err := globalIAMSys.NormalizeLDAPAccessKeypairs(ctx, serviceAcctReqs) skipped.ServiceAccounts = append(skipped.ServiceAccounts, skippedAccessKeys...) if err != nil { writeErrorResponseJSON(ctx, w, importError(ctx, err, allSvcAcctsFile, ""), r.URL) return } } for user, svcAcctReq := range serviceAcctReqs { if slices.Contains(skipped.ServiceAccounts, user) { continue } var sp *policy.Policy var err error if len(svcAcctReq.SessionPolicy) > 0 { sp, err = policy.ParseConfig(bytes.NewReader(svcAcctReq.SessionPolicy)) if err != nil { writeErrorResponseJSON(ctx, w, importError(ctx, err, allSvcAcctsFile, user), r.URL) return } } // service account access key cannot have space characters // beginning and end of the string. if hasSpaceBE(svcAcctReq.AccessKey) { writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAdminResourceInvalidArgument), r.URL) return } if !globalIAMSys.IsAllowed(policy.Args{ AccountName: cred.AccessKey, Groups: cred.Groups, Action: policy.CreateServiceAccountAdminAction, ConditionValues: getConditionValues(r, "", cred), IsOwner: owner, Claims: cred.Claims, }) { writeErrorResponseJSON(ctx, w, importErrorWithAPIErr(ctx, ErrAccessDenied, err, allSvcAcctsFile, user), r.URL) return } updateReq := true _, _, err = globalIAMSys.GetServiceAccount(ctx, svcAcctReq.AccessKey) if err != nil { if !errors.Is(err, errNoSuchServiceAccount) { writeErrorResponseJSON(ctx, w, importError(ctx, err, allSvcAcctsFile, user), r.URL) return } updateReq = false } if updateReq { // If the service account exists, we remove it to ensure a // clean import. err := globalIAMSys.DeleteServiceAccount(ctx, svcAcctReq.AccessKey, true) if err != nil { delErr := fmt.Errorf("failed to delete existing service account (%s) before importing it: %w", svcAcctReq.AccessKey, err) writeErrorResponseJSON(ctx, w, importError(ctx, delErr, allSvcAcctsFile, user), r.URL) return } } opts := newServiceAccountOpts{ accessKey: user, secretKey: svcAcctReq.SecretKey, sessionPolicy: sp, claims: svcAcctReq.Claims, name: svcAcctReq.Name, description: svcAcctReq.Description, expiration: svcAcctReq.Expiration, allowSiteReplicatorAccount: false, } if _, _, err = globalIAMSys.NewServiceAccount(ctx, svcAcctReq.Parent, svcAcctReq.Groups, opts); err != nil { failed.ServiceAccounts = append(failed.ServiceAccounts, madmin.IAMErrEntity{Name: user, Error: err}) } else { added.ServiceAccounts = append(added.ServiceAccounts, user) } } } } // import user policy mappings { f, err := zr.Open(pathJoin(iamAssetsDir, userPolicyMappingsFile)) switch { case errors.Is(err, os.ErrNotExist): case err != nil: writeErrorResponseJSON(ctx, w, importErrorWithAPIErr(ctx, ErrInvalidRequest, err, userPolicyMappingsFile, ""), r.URL) return default: defer f.Close() var userPolicyMap map[string]MappedPolicy data, err := io.ReadAll(f) if err != nil { writeErrorResponseJSON(ctx, w, importErrorWithAPIErr(ctx, ErrInvalidRequest, err, userPolicyMappingsFile, ""), r.URL) return } if err = json.Unmarshal(data, &userPolicyMap); err != nil { writeErrorResponseJSON(ctx, w, importErrorWithAPIErr(ctx, ErrAdminConfigBadJSON, err, userPolicyMappingsFile, ""), r.URL) return } for u, pm := range userPolicyMap { // disallow setting policy mapping if user is a temporary user ok, _, err := globalIAMSys.IsTempUser(u) if err != nil && err != errNoSuchUser { writeErrorResponseJSON(ctx, w, importError(ctx, err, userPolicyMappingsFile, u), r.URL) return } if ok { writeErrorResponseJSON(ctx, w, importError(ctx, errIAMActionNotAllowed, userPolicyMappingsFile, u), r.URL) return } if _, err := globalIAMSys.PolicyDBSet(ctx, u, pm.Policies, regUser, false); err != nil { failed.UserPolicies = append( failed.UserPolicies, madmin.IAMErrPolicyEntity{ Name: u, Policies: strings.Split(pm.Policies, ","), Error: err, }) } else { added.UserPolicies = append(added.UserPolicies, map[string][]string{u: strings.Split(pm.Policies, ",")}) } } } } // import group policy mappings { f, err := zr.Open(pathJoin(iamAssetsDir, groupPolicyMappingsFile)) switch { case errors.Is(err, os.ErrNotExist): case err != nil: writeErrorResponseJSON(ctx, w, importErrorWithAPIErr(ctx, ErrInvalidRequest, err, groupPolicyMappingsFile, ""), r.URL) return default: defer f.Close() var grpPolicyMap map[string]MappedPolicy data, err := io.ReadAll(f) if err != nil { writeErrorResponseJSON(ctx, w, importErrorWithAPIErr(ctx, ErrInvalidRequest, err, groupPolicyMappingsFile, ""), r.URL) return } if err = json.Unmarshal(data, &grpPolicyMap); err != nil { writeErrorResponseJSON(ctx, w, importErrorWithAPIErr(ctx, ErrAdminConfigBadJSON, err, groupPolicyMappingsFile, ""), r.URL) return } // Validations for LDAP enabled deployments. if globalIAMSys.LDAPConfig.Enabled() { isGroup := true skippedDN, err := globalIAMSys.NormalizeLDAPMappingImport(ctx, isGroup, grpPolicyMap) skipped.Groups = append(skipped.Groups, skippedDN...) if err != nil { writeErrorResponseJSON(ctx, w, importError(ctx, err, groupPolicyMappingsFile, ""), r.URL) return } } for g, pm := range grpPolicyMap { if slices.Contains(skipped.Groups, g) { continue } if _, err := globalIAMSys.PolicyDBSet(ctx, g, pm.Policies, unknownIAMUserType, true); err != nil { failed.GroupPolicies = append( failed.GroupPolicies, madmin.IAMErrPolicyEntity{ Name: g, Policies: strings.Split(pm.Policies, ","), Error: err, }) } else { added.GroupPolicies = append(added.GroupPolicies, map[string][]string{g: strings.Split(pm.Policies, ",")}) } } } } // import sts user policy mappings { f, err := zr.Open(pathJoin(iamAssetsDir, stsUserPolicyMappingsFile)) switch { case errors.Is(err, os.ErrNotExist): case err != nil: writeErrorResponseJSON(ctx, w, importErrorWithAPIErr(ctx, ErrInvalidRequest, err, stsUserPolicyMappingsFile, ""), r.URL) return default: defer f.Close() var userPolicyMap map[string]MappedPolicy data, err := io.ReadAll(f) if err != nil { writeErrorResponseJSON(ctx, w, importErrorWithAPIErr(ctx, ErrInvalidRequest, err, stsUserPolicyMappingsFile, ""), r.URL) return } if err = json.Unmarshal(data, &userPolicyMap); err != nil { writeErrorResponseJSON(ctx, w, importErrorWithAPIErr(ctx, ErrAdminConfigBadJSON, err, stsUserPolicyMappingsFile, ""), r.URL) return } // Validations for LDAP enabled deployments. if globalIAMSys.LDAPConfig.Enabled() { isGroup := true skippedDN, err := globalIAMSys.NormalizeLDAPMappingImport(ctx, !isGroup, userPolicyMap) skipped.Users = append(skipped.Users, skippedDN...) if err != nil { writeErrorResponseJSON(ctx, w, importError(ctx, err, stsUserPolicyMappingsFile, ""), r.URL) return } } for u, pm := range userPolicyMap { if slices.Contains(skipped.Users, u) { continue } // disallow setting policy mapping if user is a temporary user ok, _, err := globalIAMSys.IsTempUser(u) if err != nil && err != errNoSuchUser { writeErrorResponseJSON(ctx, w, importError(ctx, err, stsUserPolicyMappingsFile, u), r.URL) return } if ok { writeErrorResponseJSON(ctx, w, importError(ctx, errIAMActionNotAllowed, stsUserPolicyMappingsFile, u), r.URL) return } if _, err := globalIAMSys.PolicyDBSet(ctx, u, pm.Policies, stsUser, false); err != nil { failed.STSPolicies = append( failed.STSPolicies, madmin.IAMErrPolicyEntity{ Name: u, Policies: strings.Split(pm.Policies, ","), Error: err, }) } else { added.STSPolicies = append(added.STSPolicies, map[string][]string{u: strings.Split(pm.Policies, ",")}) } } } } if apiVer == "v2" { iamr := madmin.ImportIAMResult{ Skipped: skipped, Removed: removed, Added: added, Failed: failed, } b, err := json.Marshal(iamr) if err != nil { writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL) return } writeSuccessResponseJSON(w, b) } } func addExpirationToCondValues(exp *time.Time, condValues map[string][]string) error { if exp == nil || exp.IsZero() || exp.Equal(timeSentinel) { return nil } dur := exp.Sub(time.Now()) if dur <= 0 { return errors.New("unsupported expiration time") } condValues["DurationSeconds"] = []string{strconv.FormatInt(int64(dur.Seconds()), 10)} return nil } func commonAddServiceAccount(r *http.Request, ldap bool) (context.Context, auth.Credentials, newServiceAccountOpts, madmin.AddServiceAccountReq, string, APIError) { ctx := r.Context() // Get current object layer instance. objectAPI := newObjectLayerFn() if objectAPI == nil || globalNotificationSys == nil { return ctx, auth.Credentials{}, newServiceAccountOpts{}, madmin.AddServiceAccountReq{}, "", errorCodes.ToAPIErr(ErrServerNotInitialized) } cred, owner, s3Err := validateAdminSignature(ctx, r, "") if s3Err != ErrNone { return ctx, auth.Credentials{}, newServiceAccountOpts{}, madmin.AddServiceAccountReq{}, "", errorCodes.ToAPIErr(s3Err) } password := cred.SecretKey reqBytes, err := madmin.DecryptData(password, io.LimitReader(r.Body, r.ContentLength)) if err != nil { return ctx, auth.Credentials{}, newServiceAccountOpts{}, madmin.AddServiceAccountReq{}, "", errorCodes.ToAPIErrWithErr(ErrAdminConfigBadJSON, err) } var createReq madmin.AddServiceAccountReq if err = json.Unmarshal(reqBytes, &createReq); err != nil { return ctx, auth.Credentials{}, newServiceAccountOpts{}, madmin.AddServiceAccountReq{}, "", errorCodes.ToAPIErrWithErr(ErrAdminConfigBadJSON, err) } if createReq.Expiration != nil && !createReq.Expiration.IsZero() { // truncate expiration at the second. truncateTime := createReq.Expiration.Truncate(time.Second) createReq.Expiration = &truncateTime } // service account access key cannot have space characters beginning and end of the string. if hasSpaceBE(createReq.AccessKey) { return ctx, auth.Credentials{}, newServiceAccountOpts{}, madmin.AddServiceAccountReq{}, "", errorCodes.ToAPIErr(ErrAdminResourceInvalidArgument) } if err := createReq.Validate(); err != nil { // Since this validation would happen client side as well, we only send // a generic error message here. return ctx, auth.Credentials{}, newServiceAccountOpts{}, madmin.AddServiceAccountReq{}, "", errorCodes.ToAPIErr(ErrAdminResourceInvalidArgument) } // If the request did not set a TargetUser, the service account is // created for the request sender. targetUser := createReq.TargetUser if targetUser == "" { targetUser = cred.AccessKey } description := createReq.Description if description == "" { description = createReq.Comment } opts := newServiceAccountOpts{ accessKey: createReq.AccessKey, secretKey: createReq.SecretKey, name: createReq.Name, description: description, expiration: createReq.Expiration, claims: make(map[string]interface{}), } condValues := getConditionValues(r, "", cred) err = addExpirationToCondValues(createReq.Expiration, condValues) if err != nil { return ctx, auth.Credentials{}, newServiceAccountOpts{}, madmin.AddServiceAccountReq{}, "", toAdminAPIErr(ctx, err) } denyOnly := (targetUser == cred.AccessKey || targetUser == cred.ParentUser) if ldap && !denyOnly { res, _ := globalIAMSys.LDAPConfig.GetValidatedDNForUsername(targetUser) if res.NormDN == cred.ParentUser { denyOnly = true } } // Check if action is allowed if creating access key for another user // Check if action is explicitly denied if for self if !globalIAMSys.IsAllowed(policy.Args{ AccountName: cred.AccessKey, Groups: cred.Groups, Action: policy.CreateServiceAccountAdminAction, ConditionValues: condValues, IsOwner: owner, Claims: cred.Claims, DenyOnly: denyOnly, }) { return ctx, auth.Credentials{}, newServiceAccountOpts{}, madmin.AddServiceAccountReq{}, "", errorCodes.ToAPIErr(ErrAccessDenied) } var sp *policy.Policy if len(createReq.Policy) > 0 { sp, err = policy.ParseConfig(bytes.NewReader(createReq.Policy)) if err != nil { return ctx, auth.Credentials{}, newServiceAccountOpts{}, madmin.AddServiceAccountReq{}, "", toAdminAPIErr(ctx, err) } } opts.sessionPolicy = sp return ctx, cred, opts, createReq, targetUser, APIError{} }