diff --git a/cmd/admin-bucket-handlers.go b/cmd/admin-bucket-handlers.go index bc1ceefd2..867f4b8a7 100644 --- a/cmd/admin-bucket-handlers.go +++ b/cmd/admin-bucket-handlers.go @@ -804,7 +804,7 @@ func (a adminAPIHandlers) ImportBucketMetadataHandler(w http.ResponseWriter, r * for _, file := range zr.File { reader, err := file.Open() if err != nil { - writeErrorResponse(ctx, w, importError(ctx, err, file.Name, bucket), r.URL) + writeErrorResponse(ctx, w, importError(ctx, err, file.Name, ""), r.URL) return } sz := file.FileInfo().Size() diff --git a/cmd/admin-handler-utils.go b/cmd/admin-handler-utils.go index 1c62afb0d..2d7491443 100644 --- a/cmd/admin-handler-utils.go +++ b/cmd/admin-handler-utils.go @@ -240,3 +240,11 @@ func importError(ctx context.Context, err error, fname, entity string) APIError } return toAPIError(ctx, fmt.Errorf("error importing %s from %s with: %w", entity, fname, err)) } + +// wraps import error for more context +func importErrorWithAPIErr(ctx context.Context, apiErr APIErrorCode, err error, fname, entity string) APIError { + if entity == "" { + return errorCodes.ToAPIErrWithErr(apiErr, fmt.Errorf("error importing %s with: %w", fname, err)) + } + return errorCodes.ToAPIErrWithErr(apiErr, fmt.Errorf("error importing %s from %s with: %w", entity, fname, err)) +} diff --git a/cmd/admin-handlers-users.go b/cmd/admin-handlers-users.go index 5890ebb8a..3c9ff2618 100644 --- a/cmd/admin-handlers-users.go +++ b/cmd/admin-handlers-users.go @@ -24,9 +24,12 @@ import ( "io" "io/ioutil" "net/http" + "os" "sort" + "time" "github.com/gorilla/mux" + "github.com/klauspost/compress/zip" "github.com/minio/madmin-go" "github.com/minio/minio/internal/auth" "github.com/minio/minio/internal/config/dns" @@ -629,7 +632,7 @@ func (a adminAPIHandlers) AddServiceAccount(w http.ResponseWriter, r *http.Reque opts.claims[k] = v } } else { - // Need permission if we are creating a service acccount for a + // Need permission if we are creating a service account for a // user <> to the request sender if !globalIAMSys.IsAllowed(iampolicy.Args{ AccountName: requestorUser, @@ -1512,3 +1515,677 @@ func (a adminAPIHandlers) SetPolicyForUserOrGroup(w http.ResponseWriter, r *http return } } + +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" + stsGroupPolicyMappingsFile = "stsgroup_mappings.json" +) + +// ExportIAMHandler - exports all iam info as a zipped file +func (a adminAPIHandlers) ExportIAM(w http.ResponseWriter, r *http.Request) { + ctx := newContext(r, w, "ExportIAM") + defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r)) + + // Get current object layer instance. + objectAPI, _ := validateAdminReq(ctx, w, r, iampolicy.ExportIAMAction) + if objectAPI == nil { + writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrServerNotInitialized), r.URL) + 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 { + logger.LogIf(ctx, zerr) + return nil + } + header.Method = zip.Deflate + zwriter, zerr := zipWriter.CreateHeader(header) + if zerr != nil { + logger.LogIf(ctx, zerr) + return nil + } + if _, err := io.Copy(zwriter, r); err != nil { + logger.LogIf(ctx, err) + } + return nil + } + + iamFiles := []string{ + allPoliciesFile, + allUsersFile, + allGroupsFile, + allSvcAcctsFile, + userPolicyMappingsFile, + groupPolicyMappingsFile, + stsUserPolicyMappingsFile, + stsGroupPolicyMappingsFile, + } + for _, iamFile := range iamFiles { + switch iamFile { + case allPoliciesFile: + allPolicies, err := globalIAMSys.ListPolicies(ctx, "") + if err != nil { + logger.LogIf(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: + userCreds := make(map[string]auth.Credentials) + globalIAMSys.store.rlock() + err := globalIAMSys.store.loadUsers(ctx, regUser, userCreds) + globalIAMSys.store.runlock() + if err != nil { + writeErrorResponse(ctx, w, exportError(ctx, err, iamFile, ""), r.URL) + return + } + userAccounts := make(map[string]madmin.AddOrUpdateUserReq) + for u, cred := range userCreds { + status := madmin.AccountDisabled + if cred.IsValid() { + status = madmin.AccountEnabled + } + userAccounts[u] = madmin.AddOrUpdateUserReq{ + SecretKey: cred.SecretKey, + Status: status, + } + } + 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) + globalIAMSys.store.rlock() + err := globalIAMSys.store.loadGroups(ctx, groups) + globalIAMSys.store.runlock() + 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]auth.Credentials) + globalIAMSys.store.rlock() + err := globalIAMSys.store.loadUsers(ctx, svcUser, serviceAccounts) + globalIAMSys.store.runlock() + 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 the site replicate svc account as it should be created automatically if + // site replication is enabled. + continue + } + claims, err := globalIAMSys.GetClaimsForSvcAcc(ctx, acc.AccessKey) + if err != nil { + writeErrorResponse(ctx, w, exportError(ctx, err, iamFile, ""), r.URL) + return + } + _, policy, err := globalIAMSys.GetServiceAccount(ctx, acc.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.ParentUser, + AccessKey: user, + SecretKey: acc.SecretKey, + Groups: acc.Groups, + Claims: claims, + SessionPolicy: json.RawMessage(policyJSON), + Status: acc.Status, + } + } + + 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 := make(map[string]MappedPolicy) + globalIAMSys.store.rlock() + err := globalIAMSys.store.loadMappedPolicies(ctx, regUser, false, userPolicyMap) + globalIAMSys.store.runlock() + if err != nil { + writeErrorResponse(ctx, w, exportError(ctx, err, iamFile, ""), r.URL) + return + } + userPolData, err := json.Marshal(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 := make(map[string]MappedPolicy) + globalIAMSys.store.rlock() + err := globalIAMSys.store.loadMappedPolicies(ctx, regUser, true, groupPolicyMap) + globalIAMSys.store.runlock() + if err != nil { + writeErrorResponse(ctx, w, exportError(ctx, err, iamFile, ""), r.URL) + return + } + grpPolData, err := json.Marshal(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 := make(map[string]MappedPolicy) + globalIAMSys.store.rlock() + err := globalIAMSys.store.loadMappedPolicies(ctx, stsUser, false, userPolicyMap) + globalIAMSys.store.runlock() + if err != nil { + writeErrorResponse(ctx, w, exportError(ctx, err, iamFile, ""), r.URL) + return + } + userPolData, err := json.Marshal(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 stsGroupPolicyMappingsFile: + groupPolicyMap := make(map[string]MappedPolicy) + globalIAMSys.store.rlock() + err := globalIAMSys.store.loadMappedPolicies(ctx, stsUser, true, groupPolicyMap) + globalIAMSys.store.runlock() + if err != nil { + writeErrorResponse(ctx, w, exportError(ctx, err, iamFile, ""), r.URL) + return + } + grpPolData, err := json.Marshal(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 + } + } + } +} + +// ImportIAM - imports all IAM info into MinIO +func (a adminAPIHandlers) ImportIAM(w http.ResponseWriter, r *http.Request) { + ctx := newContext(r, w, "ImportIAM") + + defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r)) + + // Get current object layer instance. + objectAPI := newObjectLayerFn() + if objectAPI == nil || globalNotificationSys == nil { + writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrServerNotInitialized), r.URL) + return + } + cred, claims, owner, s3Err := validateAdminSignature(ctx, r, "") + if s3Err != ErrNone { + writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(s3Err), r.URL) + return + } + data, err := ioutil.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 + } + // import policies first + { + f, err := zr.Open(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]iampolicy.Policy + data, err = ioutil.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) + } else { + err = globalIAMSys.SetPolicy(ctx, policyName, policy) + } + if err != nil { + writeErrorResponseJSON(ctx, w, importError(ctx, err, allPoliciesFile, policyName), r.URL) + return + } + } + } + } + + // import users + { + f, err := zr.Open(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 := ioutil.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 owner && accessKey == cred.AccessKey { + writeErrorResponseJSON(ctx, w, importErrorWithAPIErr(ctx, ErrAddUserInvalidArgument, err, allUsersFile, accessKey), r.URL) + return + } + + userCred, exists := globalIAMSys.GetUser(ctx, accessKey) + if exists && (userCred.IsTemp() || userCred.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(iampolicy.Args{ + AccountName: cred.AccessKey, + Groups: cred.Groups, + Action: iampolicy.CreateUserAdminAction, + ConditionValues: getConditionValues(r, "", cred.AccessKey, claims), + IsOwner: owner, + Claims: claims, + DenyOnly: checkDenyOnly, + }) { + writeErrorResponseJSON(ctx, w, importErrorWithAPIErr(ctx, ErrAccessDenied, err, allUsersFile, accessKey), r.URL) + return + } + if err = globalIAMSys.CreateUser(ctx, accessKey, ureq); err != nil { + writeErrorResponseJSON(ctx, w, importErrorWithAPIErr(ctx, toAdminAPIErrCode(ctx, err), err, allUsersFile, accessKey), r.URL) + return + } + + } + } + } + + // import groups + { + f, err := zr.Open(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 := ioutil.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, err, allGroupsFile, group), r.URL) + return + } + } + if gerr := globalIAMSys.AddUsersToGroup(ctx, group, grpInfo.Members); gerr != nil { + writeErrorResponseJSON(ctx, w, importError(ctx, err, allGroupsFile, group), r.URL) + return + } + } + } + } + + // import service accounts + { + f, err := zr.Open(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 := ioutil.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 + } + for user, svcAcctReq := range serviceAcctReqs { + var sp *iampolicy.Policy + var err error + if len(svcAcctReq.SessionPolicy) > 0 { + sp, err = iampolicy.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(iampolicy.Args{ + AccountName: svcAcctReq.AccessKey, + Groups: svcAcctReq.Groups, + Action: iampolicy.CreateServiceAccountAdminAction, + ConditionValues: getConditionValues(r, "", cred.AccessKey, claims), + IsOwner: owner, + Claims: 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 { + opts := updateServiceAccountOpts{ + secretKey: svcAcctReq.SecretKey, + status: svcAcctReq.Status, + sessionPolicy: sp, + } + err = globalIAMSys.UpdateServiceAccount(ctx, svcAcctReq.AccessKey, opts) + if err != nil { + writeErrorResponseJSON(ctx, w, importError(ctx, err, allSvcAcctsFile, user), r.URL) + return + } + continue + } + opts := newServiceAccountOpts{ + accessKey: user, + secretKey: svcAcctReq.SecretKey, + sessionPolicy: sp, + claims: svcAcctReq.Claims, + } + + // In case of LDAP we need to resolve the targetUser to a DN and + // query their groups: + if globalLDAPConfig.Enabled { + opts.claims[ldapUserN] = svcAcctReq.AccessKey // simple username + targetUser, _, err := globalLDAPConfig.LookupUserDN(svcAcctReq.AccessKey) + if err != nil { + writeErrorResponseJSON(ctx, w, importError(ctx, err, allSvcAcctsFile, user), r.URL) + return + } + opts.claims[ldapUser] = targetUser // username DN + } + + if _, err = globalIAMSys.NewServiceAccount(ctx, svcAcctReq.Parent, svcAcctReq.Groups, opts); err != nil { + writeErrorResponseJSON(ctx, w, importError(ctx, err, allSvcAcctsFile, user), r.URL) + return + } + + } + } + } + + // import user policy mappings + { + f, err := zr.Open(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 := ioutil.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, false); err != nil { + writeErrorResponseJSON(ctx, w, importError(ctx, err, userPolicyMappingsFile, u), r.URL) + return + } + } + } + } + + // import group policy mappings + { + f, err := zr.Open(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 := ioutil.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 + } + for g, pm := range grpPolicyMap { + if err := globalIAMSys.PolicyDBSet(ctx, g, pm.Policies, true); err != nil { + writeErrorResponseJSON(ctx, w, importError(ctx, err, groupPolicyMappingsFile, g), r.URL) + return + } + } + } + } + + // import sts user policy mappings + { + f, err := zr.Open(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 := ioutil.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 + } + 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, 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, false); err != nil { + writeErrorResponseJSON(ctx, w, importError(ctx, err, stsUserPolicyMappingsFile, u), r.URL) + return + } + } + } + } + + // import sts group policy mappings + { + f, err := zr.Open(stsGroupPolicyMappingsFile) + switch { + case errors.Is(err, os.ErrNotExist): + case err != nil: + writeErrorResponseJSON(ctx, w, importErrorWithAPIErr(ctx, ErrInvalidRequest, err, stsGroupPolicyMappingsFile, ""), r.URL) + return + default: + defer f.Close() + var grpPolicyMap map[string]MappedPolicy + data, err := ioutil.ReadAll(f) + if err != nil { + writeErrorResponseJSON(ctx, w, importErrorWithAPIErr(ctx, ErrInvalidRequest, err, stsGroupPolicyMappingsFile, ""), r.URL) + return + } + if err = json.Unmarshal(data, &grpPolicyMap); err != nil { + writeErrorResponseJSON(ctx, w, importErrorWithAPIErr(ctx, ErrAdminConfigBadJSON, err, stsGroupPolicyMappingsFile, ""), r.URL) + return + } + for g, pm := range grpPolicyMap { + if err := globalIAMSys.PolicyDBSet(ctx, g, pm.Policies, true); err != nil { + writeErrorResponseJSON(ctx, w, importError(ctx, err, stsGroupPolicyMappingsFile, g), r.URL) + return + } + } + } + } +} diff --git a/cmd/admin-router.go b/cmd/admin-router.go index c5232400f..d2364bd32 100644 --- a/cmd/admin-router.go +++ b/cmd/admin-router.go @@ -170,6 +170,12 @@ func registerAdminRouter(router *mux.Router, enableConfigOps bool) { // Set Group Status adminRouter.Methods(http.MethodPut).Path(adminVersion+"/set-group-status").HandlerFunc(gz(httpTraceHdrs(adminAPI.SetGroupStatus))).Queries("group", "{group:.*}").Queries("status", "{status:.*}") + // Export IAM info to zipped file + adminRouter.Methods(http.MethodGet).Path(adminVersion + "/export-iam").HandlerFunc(httpTraceHdrs(adminAPI.ExportIAM)) + + // Import IAM info + adminRouter.Methods(http.MethodPut).Path(adminVersion + "/import-iam").HandlerFunc(httpTraceHdrs(adminAPI.ImportIAM)) + // GetBucketQuotaConfig adminRouter.Methods(http.MethodGet).Path(adminVersion+"/get-bucket-quota").HandlerFunc( gz(httpTraceHdrs(adminAPI.GetBucketQuotaConfigHandler))).Queries("bucket", "{bucket:.*}") diff --git a/go.mod b/go.mod index 6384ddf1d..20f3f9cad 100644 --- a/go.mod +++ b/go.mod @@ -50,7 +50,7 @@ require ( github.com/minio/kes v0.19.2 github.com/minio/madmin-go v1.3.14 github.com/minio/minio-go/v7 v7.0.29 - github.com/minio/pkg v1.1.25 + github.com/minio/pkg v1.1.26 github.com/minio/selfupdate v0.4.0 github.com/minio/sha256-simd v1.0.0 github.com/minio/simdjson-go v0.4.2 diff --git a/go.sum b/go.sum index 919dd32cb..8a17bb502 100644 --- a/go.sum +++ b/go.sum @@ -636,8 +636,8 @@ github.com/minio/minio-go/v7 v7.0.23/go.mod h1:ei5JjmxwHaMrgsMrn4U/+Nmg+d8MKS1U2 github.com/minio/minio-go/v7 v7.0.29 h1:7md6lIq1s6zPzUiDRX1BVLHolA4pDM8RMQqIszaJbY0= github.com/minio/minio-go/v7 v7.0.29/go.mod h1:x81+AX5gHSfCSqw7jxRKHvxUXMlE5uKX0Vb75Xk5yYg= github.com/minio/pkg v1.1.20/go.mod h1:Xo7LQshlxGa9shKwJ7NzQbgW4s8T/Wc1cOStR/eUiMY= -github.com/minio/pkg v1.1.25 h1:QYLzmTFUV5D3bY9qXKzDj7eW2C+HOPcdtIZft9q2Azo= -github.com/minio/pkg v1.1.25/go.mod h1:z9PfmEI804KFkF6eY4LoGe8IDVvTCsYGVuaf58Dr0WI= +github.com/minio/pkg v1.1.26 h1:a8x4sHNBxCiHEkxZ/0EBTLqvV3nMtM2G/A6lXNfXN3U= +github.com/minio/pkg v1.1.26/go.mod h1:z9PfmEI804KFkF6eY4LoGe8IDVvTCsYGVuaf58Dr0WI= github.com/minio/selfupdate v0.4.0 h1:A7t07pN4Ch1tBTIRStW0KhUVyykz+2muCqFsITQeEW8= github.com/minio/selfupdate v0.4.0/go.mod h1:mcDkzMgq8PRcpCRJo/NlPY7U45O5dfYl2Y0Rg7IustY= github.com/minio/sha256-simd v0.1.1/go.mod h1:B5e1o+1/KgNmWrSQK08Y6Z1Vb5pwIktudl0J58iy0KM=