mirror of
https://github.com/minio/minio.git
synced 2024-12-25 22:55:54 -05:00
dc99f4a7a3
currently, we allowed buckets to be listed from the API call if and when the user has ListObject() permission at the global level, this is okay to be extended to GetBucketLocation() as well since GetBucketLocation() is a "read" call and allowing "reads" on a bucket has an implicit assumption that ListBuckets() should be allowed. This makes discoverability of access for read-only users becomes easier or users with specific restrictions on their policies.
1515 lines
44 KiB
Go
1515 lines
44 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(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 := 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(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()
|
|
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 := 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 {
|
|
// 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
|
|
}
|
|
}
|
|
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
|
|
}
|
|
|
|
// 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
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
// service account access key cannot have space characters beginning and end of the string.
|
|
if hasSpaceBE(createReq.AccessKey) {
|
|
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAdminResourceInvalidArgument), 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,
|
|
}) {
|
|
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
|
|
}
|
|
|
|
// 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, _, _ := globalIAMSys.GetServiceAccount(ctx, serviceAccount)
|
|
|
|
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 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 {
|
|
if err := globalSiteReplicationSys.IAMChangeHook(ctx, madmin.SRIAMItem{
|
|
Type: madmin.SRIAMItemSvcAcc,
|
|
SvcAccChange: &madmin.SRSvcAccChange{
|
|
Delete: &madmin.SRSvcAccDelete{
|
|
AccessKey: serviceAccount,
|
|
},
|
|
},
|
|
}); 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.GetBucketLocationAction,
|
|
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"]
|
|
|
|
// 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 := 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
|
|
}
|
|
}
|