mirror of https://github.com/minio/minio.git
				
				
				
			
		
			
				
	
	
		
			1468 lines
		
	
	
		
			42 KiB
		
	
	
	
		
			Go
		
	
	
	
			
		
		
	
	
			1468 lines
		
	
	
		
			42 KiB
		
	
	
	
		
			Go
		
	
	
	
| // Copyright (c) 2015-2021 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 <http://www.gnu.org/licenses/>.
 | |
| 
 | |
| package cmd
 | |
| 
 | |
| import (
 | |
| 	"bytes"
 | |
| 	"encoding/json"
 | |
| 	"errors"
 | |
| 	"io"
 | |
| 	"io/ioutil"
 | |
| 	"net/http"
 | |
| 	"sort"
 | |
| 
 | |
| 	"github.com/gorilla/mux"
 | |
| 	"github.com/minio/madmin-go"
 | |
| 	"github.com/minio/minio/internal/auth"
 | |
| 	"github.com/minio/minio/internal/config/dns"
 | |
| 	"github.com/minio/minio/internal/logger"
 | |
| 	iampolicy "github.com/minio/pkg/iam/policy"
 | |
| )
 | |
| 
 | |
| // RemoveUser - DELETE /minio/admin/v3/remove-user?accessKey=<access_key>
 | |
| func (a adminAPIHandlers) RemoveUser(w http.ResponseWriter, r *http.Request) {
 | |
| 	ctx := newContext(r, w, "RemoveUser")
 | |
| 
 | |
| 	defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r))
 | |
| 
 | |
| 	objectAPI, _ := validateAdminReq(ctx, w, r, iampolicy.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
 | |
| 	}
 | |
| 
 | |
| 	if err := globalIAMSys.DeleteUser(ctx, accessKey, true); err != nil {
 | |
| 		writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
 | |
| 		return
 | |
| 	}
 | |
| 
 | |
| 	if err := globalSiteReplicationSys.IAMChangeHook(ctx, madmin.SRIAMItem{
 | |
| 		Type: madmin.SRIAMItemIAMUser,
 | |
| 		IAMUser: &madmin.SRIAMUser{
 | |
| 			AccessKey:   accessKey,
 | |
| 			IsDeleteReq: true,
 | |
| 		},
 | |
| 	}); err != nil {
 | |
| 		writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
 | |
| 		return
 | |
| 	}
 | |
| }
 | |
| 
 | |
| // ListBucketUsers - GET /minio/admin/v3/list-users?bucket={bucket}
 | |
| func (a adminAPIHandlers) ListBucketUsers(w http.ResponseWriter, r *http.Request) {
 | |
| 	ctx := newContext(r, w, "ListBucketUsers")
 | |
| 
 | |
| 	defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r))
 | |
| 
 | |
| 	objectAPI, cred := validateAdminReq(ctx, w, r, iampolicy.ListUsersAdminAction)
 | |
| 	if objectAPI == nil {
 | |
| 		return
 | |
| 	}
 | |
| 
 | |
| 	bucket := mux.Vars(r)["bucket"]
 | |
| 
 | |
| 	password := cred.SecretKey
 | |
| 
 | |
| 	allCredentials, err := globalIAMSys.ListBucketUsers(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 := newContext(r, w, "ListUsers")
 | |
| 
 | |
| 	defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r))
 | |
| 
 | |
| 	objectAPI, cred := validateAdminReq(ctx, w, r, iampolicy.ListUsersAdminAction)
 | |
| 	if objectAPI == nil {
 | |
| 		return
 | |
| 	}
 | |
| 
 | |
| 	password := cred.SecretKey
 | |
| 
 | |
| 	allCredentials, err := globalIAMSys.ListUsers()
 | |
| 	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)
 | |
| }
 | |
| 
 | |
| // GetUserInfo - GET /minio/admin/v3/user-info
 | |
| func (a adminAPIHandlers) GetUserInfo(w http.ResponseWriter, r *http.Request) {
 | |
| 	ctx := newContext(r, w, "GetUserInfo")
 | |
| 
 | |
| 	defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r))
 | |
| 
 | |
| 	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, claims, 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(iampolicy.Args{
 | |
| 		AccountName:     cred.AccessKey,
 | |
| 		Groups:          cred.Groups,
 | |
| 		Action:          iampolicy.GetUserAdminAction,
 | |
| 		ConditionValues: getConditionValues(r, "", cred.AccessKey, claims),
 | |
| 		IsOwner:         owner,
 | |
| 		Claims:          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 := newContext(r, w, "UpdateGroupMembers")
 | |
| 
 | |
| 	defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r))
 | |
| 
 | |
| 	objectAPI, _ := validateAdminReq(ctx, w, r, iampolicy.AddUserToGroupAdminAction)
 | |
| 	if objectAPI == nil {
 | |
| 		return
 | |
| 	}
 | |
| 
 | |
| 	data, err := ioutil.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
 | |
| 	}
 | |
| 
 | |
| 	if updReq.IsRemove {
 | |
| 		err = globalIAMSys.RemoveUsersFromGroup(ctx, updReq.Group, updReq.Members)
 | |
| 	} else {
 | |
| 		err = globalIAMSys.AddUsersToGroup(ctx, updReq.Group, updReq.Members)
 | |
| 	}
 | |
| 	if err != nil {
 | |
| 		writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
 | |
| 		return
 | |
| 	}
 | |
| 
 | |
| 	if err := globalSiteReplicationSys.IAMChangeHook(ctx, madmin.SRIAMItem{
 | |
| 		Type: madmin.SRIAMItemGroupInfo,
 | |
| 		GroupInfo: &madmin.SRGroupInfo{
 | |
| 			UpdateReq: updReq,
 | |
| 		},
 | |
| 	}); err != nil {
 | |
| 		writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
 | |
| 		return
 | |
| 	}
 | |
| }
 | |
| 
 | |
| // GetGroup - /minio/admin/v3/group?group=mygroup1
 | |
| func (a adminAPIHandlers) GetGroup(w http.ResponseWriter, r *http.Request) {
 | |
| 	ctx := newContext(r, w, "GetGroup")
 | |
| 
 | |
| 	defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r))
 | |
| 
 | |
| 	objectAPI, _ := validateAdminReq(ctx, w, r, iampolicy.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 := newContext(r, w, "ListGroups")
 | |
| 
 | |
| 	defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r))
 | |
| 
 | |
| 	objectAPI, _ := validateAdminReq(ctx, w, r, iampolicy.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 := newContext(r, w, "SetGroupStatus")
 | |
| 
 | |
| 	defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r))
 | |
| 
 | |
| 	objectAPI, _ := validateAdminReq(ctx, w, r, iampolicy.EnableGroupAdminAction)
 | |
| 	if objectAPI == nil {
 | |
| 		return
 | |
| 	}
 | |
| 
 | |
| 	vars := mux.Vars(r)
 | |
| 	group := vars["group"]
 | |
| 	status := vars["status"]
 | |
| 
 | |
| 	var err error
 | |
| 	switch status {
 | |
| 	case statusEnabled:
 | |
| 		err = globalIAMSys.SetGroupStatus(ctx, group, true)
 | |
| 	case statusDisabled:
 | |
| 		err = globalIAMSys.SetGroupStatus(ctx, group, false)
 | |
| 	default:
 | |
| 		err = errInvalidArgument
 | |
| 	}
 | |
| 	if err != nil {
 | |
| 		writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
 | |
| 		return
 | |
| 	}
 | |
| 
 | |
| 	if err := globalSiteReplicationSys.IAMChangeHook(ctx, madmin.SRIAMItem{
 | |
| 		Type: madmin.SRIAMItemGroupInfo,
 | |
| 		GroupInfo: &madmin.SRGroupInfo{
 | |
| 			UpdateReq: madmin.GroupAddRemove{
 | |
| 				Group:    group,
 | |
| 				Status:   madmin.GroupStatus(status),
 | |
| 				IsRemove: false,
 | |
| 			},
 | |
| 		},
 | |
| 	}); err != nil {
 | |
| 		writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
 | |
| 		return
 | |
| 	}
 | |
| }
 | |
| 
 | |
| // SetUserStatus - PUT /minio/admin/v3/set-user-status?accessKey=<access_key>&status=[enabled|disabled]
 | |
| func (a adminAPIHandlers) SetUserStatus(w http.ResponseWriter, r *http.Request) {
 | |
| 	ctx := newContext(r, w, "SetUserStatus")
 | |
| 
 | |
| 	defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r))
 | |
| 
 | |
| 	objectAPI, _ := validateAdminReq(ctx, w, r, iampolicy.EnableUserAdminAction)
 | |
| 	if objectAPI == nil {
 | |
| 		return
 | |
| 	}
 | |
| 
 | |
| 	vars := mux.Vars(r)
 | |
| 	accessKey := vars["accessKey"]
 | |
| 	status := vars["status"]
 | |
| 
 | |
| 	// This API is not allowed to lookup master access key user status
 | |
| 	if accessKey == globalActiveCred.AccessKey {
 | |
| 		writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrInvalidRequest), r.URL)
 | |
| 		return
 | |
| 	}
 | |
| 
 | |
| 	if err := globalIAMSys.SetUserStatus(ctx, accessKey, madmin.AccountStatus(status)); err != nil {
 | |
| 		writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
 | |
| 		return
 | |
| 	}
 | |
| 
 | |
| 	if err := globalSiteReplicationSys.IAMChangeHook(ctx, madmin.SRIAMItem{
 | |
| 		Type: madmin.SRIAMItemIAMUser,
 | |
| 		IAMUser: &madmin.SRIAMUser{
 | |
| 			AccessKey:   accessKey,
 | |
| 			IsDeleteReq: false,
 | |
| 			UserReq: &madmin.AddOrUpdateUserReq{
 | |
| 				Status: madmin.AccountStatus(status),
 | |
| 			},
 | |
| 		},
 | |
| 	}); err != nil {
 | |
| 		writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
 | |
| 		return
 | |
| 	}
 | |
| }
 | |
| 
 | |
| // AddUser - PUT /minio/admin/v3/add-user?accessKey=<access_key>
 | |
| func (a adminAPIHandlers) AddUser(w http.ResponseWriter, r *http.Request) {
 | |
| 	ctx := newContext(r, w, "AddUser")
 | |
| 
 | |
| 	defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r))
 | |
| 
 | |
| 	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, claims, 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 owner && accessKey == cred.AccessKey {
 | |
| 		writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAddUserInvalidArgument), 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, 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
 | |
| 	}
 | |
| 
 | |
| 	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, 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 {
 | |
| 		logger.LogIf(ctx, err)
 | |
| 		writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAdminConfigBadJSON), r.URL)
 | |
| 		return
 | |
| 	}
 | |
| 
 | |
| 	var ureq madmin.AddOrUpdateUserReq
 | |
| 	if err = json.Unmarshal(configBytes, &ureq); err != nil {
 | |
| 		logger.LogIf(ctx, err)
 | |
| 		writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAdminConfigBadJSON), r.URL)
 | |
| 		return
 | |
| 	}
 | |
| 
 | |
| 	if err = globalIAMSys.CreateUser(ctx, accessKey, ureq); err != nil {
 | |
| 		writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
 | |
| 		return
 | |
| 	}
 | |
| 
 | |
| 	if err := globalSiteReplicationSys.IAMChangeHook(ctx, madmin.SRIAMItem{
 | |
| 		Type: madmin.SRIAMItemIAMUser,
 | |
| 		IAMUser: &madmin.SRIAMUser{
 | |
| 			AccessKey:   accessKey,
 | |
| 			IsDeleteReq: false,
 | |
| 			UserReq:     &ureq,
 | |
| 		},
 | |
| 	}); err != nil {
 | |
| 		writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
 | |
| 		return
 | |
| 	}
 | |
| }
 | |
| 
 | |
| // AddServiceAccount - PUT /minio/admin/v3/add-service-account
 | |
| func (a adminAPIHandlers) AddServiceAccount(w http.ResponseWriter, r *http.Request) {
 | |
| 	ctx := newContext(r, w, "AddServiceAccount")
 | |
| 
 | |
| 	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
 | |
| 	}
 | |
| 
 | |
| 	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 createReq madmin.AddServiceAccountReq
 | |
| 	if err = json.Unmarshal(reqBytes, &createReq); err != nil {
 | |
| 		writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErrWithErr(ErrAdminConfigBadJSON, err), r.URL)
 | |
| 		return
 | |
| 	}
 | |
| 
 | |
| 	var (
 | |
| 		targetUser   string
 | |
| 		targetGroups []string
 | |
| 	)
 | |
| 
 | |
| 	// 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
 | |
| 	}
 | |
| 
 | |
| 	opts := newServiceAccountOpts{
 | |
| 		accessKey: createReq.AccessKey,
 | |
| 		secretKey: createReq.SecretKey,
 | |
| 		claims:    make(map[string]interface{}),
 | |
| 	}
 | |
| 
 | |
| 	// 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
 | |
| 	}
 | |
| 
 | |
| 	// 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 {
 | |
| 		// Check if adding service account is explicitly denied.
 | |
| 		//
 | |
| 		// This allows turning off service accounts for request sender,
 | |
| 		// if there is no deny statement this call is implicitly enabled.
 | |
| 		if !globalIAMSys.IsAllowed(iampolicy.Args{
 | |
| 			AccountName:     requestorUser,
 | |
| 			Groups:          requestorGroups,
 | |
| 			Action:          iampolicy.CreateServiceAccountAdminAction,
 | |
| 			ConditionValues: getConditionValues(r, "", cred.AccessKey, claims),
 | |
| 			IsOwner:         owner,
 | |
| 			Claims:          claims,
 | |
| 			DenyOnly:        true,
 | |
| 		}) {
 | |
| 			writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAccessDenied), r.URL)
 | |
| 			return
 | |
| 		}
 | |
| 
 | |
| 		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 {
 | |
| 		// Need permission if we are creating a service acccount for a
 | |
| 		// user <> to the request sender
 | |
| 		if !globalIAMSys.IsAllowed(iampolicy.Args{
 | |
| 			AccountName:     requestorUser,
 | |
| 			Groups:          requestorGroups,
 | |
| 			Action:          iampolicy.CreateServiceAccountAdminAction,
 | |
| 			ConditionValues: getConditionValues(r, "", cred.AccessKey, claims),
 | |
| 			IsOwner:         owner,
 | |
| 			Claims:          claims,
 | |
| 			DenyOnly:        true,
 | |
| 		}) {
 | |
| 			writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAccessDenied), r.URL)
 | |
| 			return
 | |
| 		}
 | |
| 
 | |
| 		// In case of LDAP we need to resolve the targetUser to a DN and
 | |
| 		// query their groups:
 | |
| 		if globalLDAPConfig.Enabled {
 | |
| 			opts.claims[ldapUserN] = targetUser // simple username
 | |
| 			targetUser, targetGroups, err = globalLDAPConfig.LookupUserDN(targetUser)
 | |
| 			if err != nil {
 | |
| 				writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
 | |
| 				return
 | |
| 			}
 | |
| 			opts.claims[ldapUser] = targetUser // username DN
 | |
| 		}
 | |
| 
 | |
| 		// 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.
 | |
| 	}
 | |
| 
 | |
| 	var sp *iampolicy.Policy
 | |
| 	if len(createReq.Policy) > 0 {
 | |
| 		sp, err = iampolicy.ParseConfig(bytes.NewReader(createReq.Policy))
 | |
| 		if err != nil {
 | |
| 			writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
 | |
| 			return
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	opts.sessionPolicy = sp
 | |
| 	newCred, err := globalIAMSys.NewServiceAccount(ctx, targetUser, targetGroups, opts)
 | |
| 	if err != nil {
 | |
| 		writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
 | |
| 		return
 | |
| 	}
 | |
| 
 | |
| 	// Call hook for cluster-replication if the service account is not for a
 | |
| 	// root user.
 | |
| 	if newCred.ParentUser != globalActiveCred.AccessKey {
 | |
| 		err = 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,
 | |
| 					Claims:        opts.claims,
 | |
| 					SessionPolicy: createReq.Policy,
 | |
| 					Status:        auth.AccountOn,
 | |
| 				},
 | |
| 			},
 | |
| 		})
 | |
| 		if err != nil {
 | |
| 			writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
 | |
| 			return
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	createResp := madmin.AddServiceAccountResp{
 | |
| 		Credentials: madmin.Credentials{
 | |
| 			AccessKey: newCred.AccessKey,
 | |
| 			SecretKey: newCred.SecretKey,
 | |
| 		},
 | |
| 	}
 | |
| 
 | |
| 	data, err := json.Marshal(createResp)
 | |
| 	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)
 | |
| }
 | |
| 
 | |
| // UpdateServiceAccount - POST /minio/admin/v3/update-service-account
 | |
| func (a adminAPIHandlers) UpdateServiceAccount(w http.ResponseWriter, r *http.Request) {
 | |
| 	ctx := newContext(r, w, "UpdateServiceAccount")
 | |
| 
 | |
| 	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
 | |
| 	}
 | |
| 
 | |
| 	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
 | |
| 	}
 | |
| 
 | |
| 	if !globalIAMSys.IsAllowed(iampolicy.Args{
 | |
| 		AccountName:     cred.AccessKey,
 | |
| 		Action:          iampolicy.UpdateServiceAccountAdminAction,
 | |
| 		ConditionValues: getConditionValues(r, "", cred.AccessKey, claims),
 | |
| 		IsOwner:         owner,
 | |
| 		Claims:          claims,
 | |
| 	}) {
 | |
| 		requestUser := cred.AccessKey
 | |
| 		if cred.ParentUser != "" {
 | |
| 			requestUser = cred.ParentUser
 | |
| 		}
 | |
| 
 | |
| 		if requestUser != svcAccount.ParentUser {
 | |
| 			writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAccessDenied), 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
 | |
| 	}
 | |
| 
 | |
| 	var sp *iampolicy.Policy
 | |
| 	if len(updateReq.NewPolicy) > 0 {
 | |
| 		sp, err = iampolicy.ParseConfig(bytes.NewReader(updateReq.NewPolicy))
 | |
| 		if err != nil {
 | |
| 			writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
 | |
| 			return
 | |
| 		}
 | |
| 	}
 | |
| 	opts := updateServiceAccountOpts{
 | |
| 		secretKey:     updateReq.NewSecretKey,
 | |
| 		status:        updateReq.NewStatus,
 | |
| 		sessionPolicy: sp,
 | |
| 	}
 | |
| 	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 {
 | |
| 		err = globalSiteReplicationSys.IAMChangeHook(ctx, madmin.SRIAMItem{
 | |
| 			Type: madmin.SRIAMItemSvcAcc,
 | |
| 			SvcAccChange: &madmin.SRSvcAccChange{
 | |
| 				Update: &madmin.SRSvcAccUpdate{
 | |
| 					AccessKey:     accessKey,
 | |
| 					SecretKey:     opts.secretKey,
 | |
| 					Status:        opts.status,
 | |
| 					SessionPolicy: updateReq.NewPolicy,
 | |
| 				},
 | |
| 			},
 | |
| 		})
 | |
| 		if err != nil {
 | |
| 			writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
 | |
| 			return
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	writeSuccessNoContent(w)
 | |
| }
 | |
| 
 | |
| // InfoServiceAccount - GET /minio/admin/v3/info-service-account
 | |
| func (a adminAPIHandlers) InfoServiceAccount(w http.ResponseWriter, r *http.Request) {
 | |
| 	ctx := newContext(r, w, "InfoServiceAccount")
 | |
| 
 | |
| 	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
 | |
| 	}
 | |
| 
 | |
| 	accessKey := mux.Vars(r)["accessKey"]
 | |
| 	if accessKey == "" {
 | |
| 		writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrInvalidRequest), r.URL)
 | |
| 		return
 | |
| 	}
 | |
| 
 | |
| 	svcAccount, policy, err := globalIAMSys.GetServiceAccount(ctx, accessKey)
 | |
| 	if err != nil {
 | |
| 		writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
 | |
| 		return
 | |
| 	}
 | |
| 
 | |
| 	if !globalIAMSys.IsAllowed(iampolicy.Args{
 | |
| 		AccountName:     cred.AccessKey,
 | |
| 		Action:          iampolicy.ListServiceAccountsAdminAction,
 | |
| 		ConditionValues: getConditionValues(r, "", cred.AccessKey, claims),
 | |
| 		IsOwner:         owner,
 | |
| 		Claims:          claims,
 | |
| 	}) {
 | |
| 		requestUser := cred.AccessKey
 | |
| 		if cred.ParentUser != "" {
 | |
| 			requestUser = cred.ParentUser
 | |
| 		}
 | |
| 
 | |
| 		if requestUser != svcAccount.ParentUser {
 | |
| 			writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAccessDenied), r.URL)
 | |
| 			return
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	var svcAccountPolicy iampolicy.Policy
 | |
| 
 | |
| 	if policy != nil {
 | |
| 		svcAccountPolicy = *policy
 | |
| 	} else {
 | |
| 		policiesNames, err := globalIAMSys.PolicyDBGet(svcAccount.ParentUser, false)
 | |
| 		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
 | |
| 	}
 | |
| 
 | |
| 	infoResp := madmin.InfoServiceAccountResp{
 | |
| 		ParentUser:    svcAccount.ParentUser,
 | |
| 		AccountStatus: svcAccount.Status,
 | |
| 		ImpliedPolicy: policy == nil,
 | |
| 		Policy:        string(policyJSON),
 | |
| 	}
 | |
| 
 | |
| 	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 := newContext(r, w, "ListServiceAccounts")
 | |
| 
 | |
| 	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
 | |
| 	}
 | |
| 
 | |
| 	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(iampolicy.Args{
 | |
| 			AccountName:     cred.AccessKey,
 | |
| 			Action:          iampolicy.ListServiceAccountsAdminAction,
 | |
| 			ConditionValues: getConditionValues(r, "", cred.AccessKey, claims),
 | |
| 			IsOwner:         owner,
 | |
| 			Claims:          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 serviceAccountsNames []string
 | |
| 
 | |
| 	for _, svc := range serviceAccounts {
 | |
| 		serviceAccountsNames = append(serviceAccountsNames, svc.AccessKey)
 | |
| 	}
 | |
| 
 | |
| 	listResp := madmin.ListServiceAccountsResp{
 | |
| 		Accounts: serviceAccountsNames,
 | |
| 	}
 | |
| 
 | |
| 	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 := newContext(r, w, "DeleteServiceAccount")
 | |
| 
 | |
| 	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
 | |
| 	}
 | |
| 
 | |
| 	serviceAccount := mux.Vars(r)["accessKey"]
 | |
| 	if serviceAccount == "" {
 | |
| 		writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAdminInvalidArgument), r.URL)
 | |
| 		return
 | |
| 	}
 | |
| 
 | |
| 	svcAccount, _, err := globalIAMSys.GetServiceAccount(ctx, serviceAccount)
 | |
| 	if err != nil {
 | |
| 		writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
 | |
| 		return
 | |
| 	}
 | |
| 
 | |
| 	adminPrivilege := globalIAMSys.IsAllowed(iampolicy.Args{
 | |
| 		AccountName:     cred.AccessKey,
 | |
| 		Action:          iampolicy.RemoveServiceAccountAdminAction,
 | |
| 		ConditionValues: getConditionValues(r, "", cred.AccessKey, claims),
 | |
| 		IsOwner:         owner,
 | |
| 		Claims:          claims,
 | |
| 	})
 | |
| 
 | |
| 	if !adminPrivilege {
 | |
| 		parentUser := cred.AccessKey
 | |
| 		if cred.ParentUser != "" {
 | |
| 			parentUser = cred.ParentUser
 | |
| 		}
 | |
| 		if 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
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	err = globalIAMSys.DeleteServiceAccount(ctx, serviceAccount, true)
 | |
| 	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 {
 | |
| 		err = globalSiteReplicationSys.IAMChangeHook(ctx, madmin.SRIAMItem{
 | |
| 			Type: madmin.SRIAMItemSvcAcc,
 | |
| 			SvcAccChange: &madmin.SRSvcAccChange{
 | |
| 				Delete: &madmin.SRSvcAccDelete{
 | |
| 					AccessKey: serviceAccount,
 | |
| 				},
 | |
| 			},
 | |
| 		})
 | |
| 		if err != nil {
 | |
| 			writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
 | |
| 			return
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	writeSuccessNoContent(w)
 | |
| }
 | |
| 
 | |
| // AccountInfoHandler returns usage
 | |
| func (a adminAPIHandlers) AccountInfoHandler(w http.ResponseWriter, r *http.Request) {
 | |
| 	ctx := newContext(r, w, "AccountInfo")
 | |
| 
 | |
| 	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
 | |
| 	}
 | |
| 
 | |
| 	// 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(iampolicy.Args{
 | |
| 			AccountName:     cred.AccessKey,
 | |
| 			Groups:          cred.Groups,
 | |
| 			Action:          iampolicy.ListBucketAction,
 | |
| 			BucketName:      bucketName,
 | |
| 			ConditionValues: getConditionValues(r, "", cred.AccessKey, claims),
 | |
| 			IsOwner:         owner,
 | |
| 			ObjectName:      "",
 | |
| 			Claims:          claims,
 | |
| 		}) {
 | |
| 			rd = true
 | |
| 		}
 | |
| 
 | |
| 		if globalIAMSys.IsAllowed(iampolicy.Args{
 | |
| 			AccountName:     cred.AccessKey,
 | |
| 			Groups:          cred.Groups,
 | |
| 			Action:          iampolicy.PutObjectAction,
 | |
| 			BucketName:      bucketName,
 | |
| 			ConditionValues: getConditionValues(r, "", cred.AccessKey, claims),
 | |
| 			IsOwner:         owner,
 | |
| 			ObjectName:      "",
 | |
| 			Claims:          claims,
 | |
| 		}) {
 | |
| 			wr = true
 | |
| 		}
 | |
| 
 | |
| 		return rd, wr
 | |
| 	}
 | |
| 
 | |
| 	var dataUsageInfo DataUsageInfo
 | |
| 	var err error
 | |
| 	if !globalIsGateway {
 | |
| 		// Load the latest calculated data usage
 | |
| 		dataUsageInfo, _ = loadDataUsageFromBackend(ctx, objectAPI)
 | |
| 	}
 | |
| 
 | |
| 	// If etcd, dns federation configured list buckets from etcd.
 | |
| 	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)
 | |
| 		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
 | |
| 	}
 | |
| 	policies, err := globalIAMSys.PolicyDBGet(accountName, false, cred.Groups...)
 | |
| 	if err != nil {
 | |
| 		logger.LogIf(ctx, err)
 | |
| 		writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
 | |
| 		return
 | |
| 	}
 | |
| 
 | |
| 	buf, err := json.MarshalIndent(globalIAMSys.GetCombinedPolicy(policies...), "", " ")
 | |
| 	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
 | |
| 			var size uint64
 | |
| 			var objectsCount uint64
 | |
| 			var objectsHist map[string]uint64
 | |
| 			if !dataUsageInfo.LastUpdate.IsZero() {
 | |
| 				size = dataUsageInfo.BucketsUsage[bucket.Name].Size
 | |
| 				objectsCount = dataUsageInfo.BucketsUsage[bucket.Name].ObjectsCount
 | |
| 				objectsHist = dataUsageInfo.BucketsUsage[bucket.Name].ObjectSizesHistogram
 | |
| 			}
 | |
| 			// 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,
 | |
| 				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 := newContext(r, w, "InfoCannedPolicy")
 | |
| 
 | |
| 	defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r))
 | |
| 
 | |
| 	objectAPI, _ := validateAdminReq(ctx, w, r, iampolicy.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 := newContext(r, w, "ListBucketPolicies")
 | |
| 
 | |
| 	defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r))
 | |
| 
 | |
| 	objectAPI, _ := validateAdminReq(ctx, w, r, iampolicy.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]iampolicy.Policy)
 | |
| 	for name, p := range policies {
 | |
| 		_, err = json.Marshal(p)
 | |
| 		if err != nil {
 | |
| 			logger.LogIf(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 := newContext(r, w, "ListCannedPolicies")
 | |
| 
 | |
| 	defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r))
 | |
| 
 | |
| 	objectAPI, _ := validateAdminReq(ctx, w, r, iampolicy.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]iampolicy.Policy)
 | |
| 	for name, p := range policies {
 | |
| 		_, err = json.Marshal(p)
 | |
| 		if err != nil {
 | |
| 			logger.LogIf(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=<policy_name>
 | |
| func (a adminAPIHandlers) RemoveCannedPolicy(w http.ResponseWriter, r *http.Request) {
 | |
| 	ctx := newContext(r, w, "RemoveCannedPolicy")
 | |
| 
 | |
| 	defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r))
 | |
| 
 | |
| 	objectAPI, _ := validateAdminReq(ctx, w, r, iampolicy.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.
 | |
| 	if err := globalSiteReplicationSys.IAMChangeHook(ctx, madmin.SRIAMItem{
 | |
| 		Type: madmin.SRIAMItemPolicy,
 | |
| 		Name: policyName,
 | |
| 	}); err != nil {
 | |
| 		writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
 | |
| 		return
 | |
| 	}
 | |
| }
 | |
| 
 | |
| // AddCannedPolicy - PUT /minio/admin/v3/add-canned-policy?name=<policy_name>
 | |
| func (a adminAPIHandlers) AddCannedPolicy(w http.ResponseWriter, r *http.Request) {
 | |
| 	ctx := newContext(r, w, "AddCannedPolicy")
 | |
| 
 | |
| 	defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r))
 | |
| 
 | |
| 	objectAPI, _ := validateAdminReq(ctx, w, r, iampolicy.CreatePolicyAdminAction)
 | |
| 	if objectAPI == nil {
 | |
| 		return
 | |
| 	}
 | |
| 
 | |
| 	vars := mux.Vars(r)
 | |
| 	policyName := vars["name"]
 | |
| 
 | |
| 	// 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 := ioutil.ReadAll(io.LimitReader(r.Body, r.ContentLength))
 | |
| 	if err != nil {
 | |
| 		writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
 | |
| 		return
 | |
| 	}
 | |
| 
 | |
| 	iamPolicy, err := iampolicy.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(ErrMalformedPolicy), r.URL)
 | |
| 		return
 | |
| 	}
 | |
| 
 | |
| 	if err = globalIAMSys.SetPolicy(ctx, policyName, *iamPolicy); err != nil {
 | |
| 		writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
 | |
| 		return
 | |
| 	}
 | |
| 
 | |
| 	// Call cluster-replication policy creation hook to replicate policy to
 | |
| 	// other minio clusters.
 | |
| 	if err := globalSiteReplicationSys.IAMChangeHook(ctx, madmin.SRIAMItem{
 | |
| 		Type:   madmin.SRIAMItemPolicy,
 | |
| 		Name:   policyName,
 | |
| 		Policy: iamPolicyBytes,
 | |
| 	}); err != nil {
 | |
| 		writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
 | |
| 		return
 | |
| 	}
 | |
| }
 | |
| 
 | |
| // SetPolicyForUserOrGroup - PUT /minio/admin/v3/set-policy?policy=xxx&user-or-group=?[&is-group]
 | |
| func (a adminAPIHandlers) SetPolicyForUserOrGroup(w http.ResponseWriter, r *http.Request) {
 | |
| 	ctx := newContext(r, w, "SetPolicyForUserOrGroup")
 | |
| 
 | |
| 	defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r))
 | |
| 
 | |
| 	objectAPI, _ := validateAdminReq(ctx, w, r, iampolicy.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
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	if err := globalIAMSys.PolicyDBSet(ctx, entityName, policyName, isGroup); err != nil {
 | |
| 		writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
 | |
| 		return
 | |
| 	}
 | |
| 
 | |
| 	if err := globalSiteReplicationSys.IAMChangeHook(ctx, madmin.SRIAMItem{
 | |
| 		Type: madmin.SRIAMItemPolicyMapping,
 | |
| 		PolicyMapping: &madmin.SRPolicyMapping{
 | |
| 			UserOrGroup: entityName,
 | |
| 			IsGroup:     isGroup,
 | |
| 			Policy:      policyName,
 | |
| 		},
 | |
| 	}); err != nil {
 | |
| 		writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
 | |
| 		return
 | |
| 	}
 | |
| }
 |