mirror of
https://github.com/minio/minio.git
synced 2024-12-27 15:45:55 -05:00
c3d70e0795
AccountInfo is quite frequently called by the Console UI login attempts, when many users are logging in it is important that we provide them with better responsiveness. - ListBuckets information is cached every second - Bucket usage info is cached for up to 10 seconds - Prefix usage (optional) info is cached for up to 10 secs Failure to update after cache expiration, would still allow login which would end up providing information previously cached. This allows for seamless responsiveness for the Console UI logins, and overall responsiveness on a heavily loaded system.
2497 lines
76 KiB
Go
2497 lines
76 KiB
Go
// Copyright (c) 2015-2023 MinIO, Inc.
|
|
//
|
|
// This file is part of MinIO Object Storage stack
|
|
//
|
|
// This program is free software: you can redistribute it and/or modify
|
|
// it under the terms of the GNU Affero General Public License as published by
|
|
// the Free Software Foundation, either version 3 of the License, or
|
|
// (at your option) any later version.
|
|
//
|
|
// This program is distributed in the hope that it will be useful
|
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
// GNU Affero General Public License for more details.
|
|
//
|
|
// You should have received a copy of the GNU Affero General Public License
|
|
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
package cmd
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"os"
|
|
"sort"
|
|
"time"
|
|
|
|
"github.com/klauspost/compress/zip"
|
|
"github.com/minio/madmin-go/v3"
|
|
"github.com/minio/minio/internal/auth"
|
|
"github.com/minio/minio/internal/config/dns"
|
|
"github.com/minio/minio/internal/logger"
|
|
"github.com/minio/mux"
|
|
"github.com/minio/pkg/v2/policy"
|
|
)
|
|
|
|
// RemoveUser - DELETE /minio/admin/v3/remove-user?accessKey=<access_key>
|
|
func (a adminAPIHandlers) RemoveUser(w http.ResponseWriter, r *http.Request) {
|
|
ctx := r.Context()
|
|
|
|
objectAPI, cred := validateAdminReq(ctx, w, r, policy.DeleteUserAdminAction)
|
|
if objectAPI == nil {
|
|
return
|
|
}
|
|
|
|
vars := mux.Vars(r)
|
|
accessKey := vars["accessKey"]
|
|
|
|
ok, _, err := globalIAMSys.IsTempUser(accessKey)
|
|
if err != nil {
|
|
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
|
return
|
|
}
|
|
if ok {
|
|
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, errIAMActionNotAllowed), r.URL)
|
|
return
|
|
}
|
|
|
|
// When the user is root credential you are not allowed to
|
|
// remove the root user. Also you cannot delete yourself.
|
|
if accessKey == globalActiveCred.AccessKey || accessKey == cred.AccessKey {
|
|
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, errIAMActionNotAllowed), r.URL)
|
|
return
|
|
}
|
|
|
|
if err := globalIAMSys.DeleteUser(ctx, accessKey, true); err != nil {
|
|
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
|
return
|
|
}
|
|
|
|
logger.LogIf(ctx, globalSiteReplicationSys.IAMChangeHook(ctx, madmin.SRIAMItem{
|
|
Type: madmin.SRIAMItemIAMUser,
|
|
IAMUser: &madmin.SRIAMUser{
|
|
AccessKey: accessKey,
|
|
IsDeleteReq: true,
|
|
},
|
|
UpdatedAt: UTCNow(),
|
|
}))
|
|
}
|
|
|
|
// ListBucketUsers - GET /minio/admin/v3/list-users?bucket={bucket}
|
|
func (a adminAPIHandlers) ListBucketUsers(w http.ResponseWriter, r *http.Request) {
|
|
ctx := r.Context()
|
|
|
|
objectAPI, cred := validateAdminReq(ctx, w, r, policy.ListUsersAdminAction)
|
|
if objectAPI == nil {
|
|
return
|
|
}
|
|
|
|
bucket := mux.Vars(r)["bucket"]
|
|
|
|
password := cred.SecretKey
|
|
|
|
allCredentials, err := globalIAMSys.ListBucketUsers(ctx, bucket)
|
|
if err != nil {
|
|
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
|
return
|
|
}
|
|
|
|
data, err := json.Marshal(allCredentials)
|
|
if err != nil {
|
|
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
|
return
|
|
}
|
|
|
|
econfigData, err := madmin.EncryptData(password, data)
|
|
if err != nil {
|
|
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
|
return
|
|
}
|
|
|
|
writeSuccessResponseJSON(w, econfigData)
|
|
}
|
|
|
|
// ListUsers - GET /minio/admin/v3/list-users
|
|
func (a adminAPIHandlers) ListUsers(w http.ResponseWriter, r *http.Request) {
|
|
ctx := r.Context()
|
|
|
|
objectAPI, cred := validateAdminReq(ctx, w, r, policy.ListUsersAdminAction)
|
|
if objectAPI == nil {
|
|
return
|
|
}
|
|
|
|
password := cred.SecretKey
|
|
|
|
allCredentials, err := globalIAMSys.ListUsers(ctx)
|
|
if err != nil {
|
|
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
|
return
|
|
}
|
|
|
|
// Add ldap users which have mapped policies if in LDAP mode
|
|
// FIXME(vadmeste): move this to policy info in the future
|
|
ldapUsers, err := globalIAMSys.ListLDAPUsers(ctx)
|
|
if err != nil && err != errIAMActionNotAllowed {
|
|
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
|
return
|
|
}
|
|
for k, v := range ldapUsers {
|
|
allCredentials[k] = v
|
|
}
|
|
|
|
// Marshal the response
|
|
data, err := json.Marshal(allCredentials)
|
|
if err != nil {
|
|
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
|
return
|
|
}
|
|
|
|
econfigData, err := madmin.EncryptData(password, data)
|
|
if err != nil {
|
|
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
|
return
|
|
}
|
|
|
|
writeSuccessResponseJSON(w, econfigData)
|
|
}
|
|
|
|
// GetUserInfo - GET /minio/admin/v3/user-info
|
|
func (a adminAPIHandlers) GetUserInfo(w http.ResponseWriter, r *http.Request) {
|
|
ctx := r.Context()
|
|
|
|
vars := mux.Vars(r)
|
|
name := vars["accessKey"]
|
|
|
|
// Get current object layer instance.
|
|
objectAPI := newObjectLayerFn()
|
|
if objectAPI == nil || globalNotificationSys == nil {
|
|
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrServerNotInitialized), r.URL)
|
|
return
|
|
}
|
|
|
|
cred, owner, s3Err := validateAdminSignature(ctx, r, "")
|
|
if s3Err != ErrNone {
|
|
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(s3Err), r.URL)
|
|
return
|
|
}
|
|
|
|
checkDenyOnly := false
|
|
if name == cred.AccessKey {
|
|
// Check that there is no explicit deny - otherwise it's allowed
|
|
// to view one's own info.
|
|
checkDenyOnly = true
|
|
}
|
|
|
|
if !globalIAMSys.IsAllowed(policy.Args{
|
|
AccountName: cred.AccessKey,
|
|
Groups: cred.Groups,
|
|
Action: policy.GetUserAdminAction,
|
|
ConditionValues: getConditionValues(r, "", cred),
|
|
IsOwner: owner,
|
|
Claims: cred.Claims,
|
|
DenyOnly: checkDenyOnly,
|
|
}) {
|
|
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAccessDenied), r.URL)
|
|
return
|
|
}
|
|
|
|
userInfo, err := globalIAMSys.GetUserInfo(ctx, name)
|
|
if err != nil {
|
|
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
|
return
|
|
}
|
|
|
|
data, err := json.Marshal(userInfo)
|
|
if err != nil {
|
|
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
|
return
|
|
}
|
|
|
|
writeSuccessResponseJSON(w, data)
|
|
}
|
|
|
|
// UpdateGroupMembers - PUT /minio/admin/v3/update-group-members
|
|
func (a adminAPIHandlers) UpdateGroupMembers(w http.ResponseWriter, r *http.Request) {
|
|
ctx := r.Context()
|
|
|
|
objectAPI, _ := validateAdminReq(ctx, w, r, policy.AddUserToGroupAdminAction)
|
|
if objectAPI == nil {
|
|
return
|
|
}
|
|
|
|
data, err := io.ReadAll(r.Body)
|
|
if err != nil {
|
|
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrInvalidRequest), r.URL)
|
|
return
|
|
}
|
|
|
|
var updReq madmin.GroupAddRemove
|
|
err = json.Unmarshal(data, &updReq)
|
|
if err != nil {
|
|
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrInvalidRequest), r.URL)
|
|
return
|
|
}
|
|
|
|
// Reject if the group add and remove are temporary credentials, or root credential.
|
|
for _, member := range updReq.Members {
|
|
ok, _, err := globalIAMSys.IsTempUser(member)
|
|
if err != nil && err != errNoSuchUser {
|
|
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
|
return
|
|
}
|
|
if ok {
|
|
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, errIAMActionNotAllowed), r.URL)
|
|
return
|
|
}
|
|
// When the user is root credential you are not allowed to
|
|
// add policies for root user.
|
|
if member == globalActiveCred.AccessKey {
|
|
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, errIAMActionNotAllowed), r.URL)
|
|
return
|
|
}
|
|
}
|
|
|
|
var updatedAt time.Time
|
|
if updReq.IsRemove {
|
|
updatedAt, err = globalIAMSys.RemoveUsersFromGroup(ctx, updReq.Group, updReq.Members)
|
|
} else {
|
|
// Check if group already exists
|
|
if _, gerr := globalIAMSys.GetGroupDescription(updReq.Group); gerr != nil {
|
|
// If group does not exist, then check if the group has beginning and end space characters
|
|
// we will reject such group names.
|
|
if errors.Is(gerr, errNoSuchGroup) && hasSpaceBE(updReq.Group) {
|
|
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAdminResourceInvalidArgument), r.URL)
|
|
return
|
|
}
|
|
}
|
|
updatedAt, err = globalIAMSys.AddUsersToGroup(ctx, updReq.Group, updReq.Members)
|
|
}
|
|
if err != nil {
|
|
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
|
return
|
|
}
|
|
|
|
logger.LogIf(ctx, globalSiteReplicationSys.IAMChangeHook(ctx, madmin.SRIAMItem{
|
|
Type: madmin.SRIAMItemGroupInfo,
|
|
GroupInfo: &madmin.SRGroupInfo{
|
|
UpdateReq: updReq,
|
|
},
|
|
UpdatedAt: updatedAt,
|
|
}))
|
|
}
|
|
|
|
// GetGroup - /minio/admin/v3/group?group=mygroup1
|
|
func (a adminAPIHandlers) GetGroup(w http.ResponseWriter, r *http.Request) {
|
|
ctx := r.Context()
|
|
|
|
objectAPI, _ := validateAdminReq(ctx, w, r, policy.GetGroupAdminAction)
|
|
if objectAPI == nil {
|
|
return
|
|
}
|
|
|
|
vars := mux.Vars(r)
|
|
group := vars["group"]
|
|
|
|
gdesc, err := globalIAMSys.GetGroupDescription(group)
|
|
if err != nil {
|
|
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
|
return
|
|
}
|
|
|
|
body, err := json.Marshal(gdesc)
|
|
if err != nil {
|
|
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
|
return
|
|
}
|
|
|
|
writeSuccessResponseJSON(w, body)
|
|
}
|
|
|
|
// ListGroups - GET /minio/admin/v3/groups
|
|
func (a adminAPIHandlers) ListGroups(w http.ResponseWriter, r *http.Request) {
|
|
ctx := r.Context()
|
|
|
|
objectAPI, _ := validateAdminReq(ctx, w, r, policy.ListGroupsAdminAction)
|
|
if objectAPI == nil {
|
|
return
|
|
}
|
|
|
|
groups, err := globalIAMSys.ListGroups(ctx)
|
|
if err != nil {
|
|
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
|
return
|
|
}
|
|
|
|
body, err := json.Marshal(groups)
|
|
if err != nil {
|
|
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
|
return
|
|
}
|
|
|
|
writeSuccessResponseJSON(w, body)
|
|
}
|
|
|
|
// SetGroupStatus - PUT /minio/admin/v3/set-group-status?group=mygroup1&status=enabled
|
|
func (a adminAPIHandlers) SetGroupStatus(w http.ResponseWriter, r *http.Request) {
|
|
ctx := r.Context()
|
|
|
|
objectAPI, _ := validateAdminReq(ctx, w, r, policy.EnableGroupAdminAction)
|
|
if objectAPI == nil {
|
|
return
|
|
}
|
|
|
|
vars := mux.Vars(r)
|
|
group := vars["group"]
|
|
status := vars["status"]
|
|
|
|
var (
|
|
err error
|
|
updatedAt time.Time
|
|
)
|
|
switch status {
|
|
case statusEnabled:
|
|
updatedAt, err = globalIAMSys.SetGroupStatus(ctx, group, true)
|
|
case statusDisabled:
|
|
updatedAt, err = globalIAMSys.SetGroupStatus(ctx, group, false)
|
|
default:
|
|
err = errInvalidArgument
|
|
}
|
|
if err != nil {
|
|
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
|
return
|
|
}
|
|
|
|
logger.LogIf(ctx, globalSiteReplicationSys.IAMChangeHook(ctx, madmin.SRIAMItem{
|
|
Type: madmin.SRIAMItemGroupInfo,
|
|
GroupInfo: &madmin.SRGroupInfo{
|
|
UpdateReq: madmin.GroupAddRemove{
|
|
Group: group,
|
|
Status: madmin.GroupStatus(status),
|
|
IsRemove: false,
|
|
},
|
|
},
|
|
UpdatedAt: updatedAt,
|
|
}))
|
|
}
|
|
|
|
// SetUserStatus - PUT /minio/admin/v3/set-user-status?accessKey=<access_key>&status=[enabled|disabled]
|
|
func (a adminAPIHandlers) SetUserStatus(w http.ResponseWriter, r *http.Request) {
|
|
ctx := r.Context()
|
|
|
|
objectAPI, creds := validateAdminReq(ctx, w, r, policy.EnableUserAdminAction)
|
|
if objectAPI == nil {
|
|
return
|
|
}
|
|
|
|
vars := mux.Vars(r)
|
|
accessKey := vars["accessKey"]
|
|
status := vars["status"]
|
|
|
|
// you cannot enable or disable yourself.
|
|
if accessKey == creds.AccessKey {
|
|
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, errInvalidArgument), r.URL)
|
|
return
|
|
}
|
|
|
|
updatedAt, err := globalIAMSys.SetUserStatus(ctx, accessKey, madmin.AccountStatus(status))
|
|
if err != nil {
|
|
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
|
return
|
|
}
|
|
|
|
logger.LogIf(ctx, globalSiteReplicationSys.IAMChangeHook(ctx, madmin.SRIAMItem{
|
|
Type: madmin.SRIAMItemIAMUser,
|
|
IAMUser: &madmin.SRIAMUser{
|
|
AccessKey: accessKey,
|
|
IsDeleteReq: false,
|
|
UserReq: &madmin.AddOrUpdateUserReq{
|
|
Status: madmin.AccountStatus(status),
|
|
},
|
|
},
|
|
UpdatedAt: updatedAt,
|
|
}))
|
|
}
|
|
|
|
// AddUser - PUT /minio/admin/v3/add-user?accessKey=<access_key>
|
|
func (a adminAPIHandlers) AddUser(w http.ResponseWriter, r *http.Request) {
|
|
ctx := r.Context()
|
|
|
|
vars := mux.Vars(r)
|
|
accessKey := vars["accessKey"]
|
|
|
|
// Get current object layer instance.
|
|
objectAPI := newObjectLayerFn()
|
|
if objectAPI == nil || globalNotificationSys == nil {
|
|
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrServerNotInitialized), r.URL)
|
|
return
|
|
}
|
|
|
|
cred, owner, s3Err := validateAdminSignature(ctx, r, "")
|
|
if s3Err != ErrNone {
|
|
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(s3Err), r.URL)
|
|
return
|
|
}
|
|
|
|
// Not allowed to add a user with same access key as root credential
|
|
if accessKey == globalActiveCred.AccessKey {
|
|
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAddUserInvalidArgument), r.URL)
|
|
return
|
|
}
|
|
|
|
user, exists := globalIAMSys.GetUser(ctx, accessKey)
|
|
if exists && (user.Credentials.IsTemp() || user.Credentials.IsServiceAccount()) {
|
|
// Updating STS credential is not allowed, and this API does not
|
|
// support updating service accounts.
|
|
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAddUserInvalidArgument), r.URL)
|
|
return
|
|
}
|
|
|
|
if (cred.IsTemp() || cred.IsServiceAccount()) && cred.ParentUser == accessKey {
|
|
// Incoming access key matches parent user then we should
|
|
// reject password change requests.
|
|
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAddUserInvalidArgument), r.URL)
|
|
return
|
|
}
|
|
|
|
// Check if accessKey has beginning and end space characters, this only applies to new users.
|
|
if !exists && hasSpaceBE(accessKey) {
|
|
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAdminResourceInvalidArgument), r.URL)
|
|
return
|
|
}
|
|
|
|
checkDenyOnly := false
|
|
if accessKey == cred.AccessKey {
|
|
// Check that there is no explicit deny - otherwise it's allowed
|
|
// to change one's own password.
|
|
checkDenyOnly = true
|
|
}
|
|
|
|
if !globalIAMSys.IsAllowed(policy.Args{
|
|
AccountName: cred.AccessKey,
|
|
Groups: cred.Groups,
|
|
Action: policy.CreateUserAdminAction,
|
|
ConditionValues: getConditionValues(r, "", cred),
|
|
IsOwner: owner,
|
|
Claims: cred.Claims,
|
|
DenyOnly: checkDenyOnly,
|
|
}) {
|
|
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAccessDenied), r.URL)
|
|
return
|
|
}
|
|
|
|
if r.ContentLength > maxEConfigJSONSize || r.ContentLength == -1 {
|
|
// More than maxConfigSize bytes were available
|
|
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAdminConfigTooLarge), r.URL)
|
|
return
|
|
}
|
|
|
|
password := cred.SecretKey
|
|
configBytes, err := madmin.DecryptData(password, io.LimitReader(r.Body, r.ContentLength))
|
|
if err != nil {
|
|
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
|
|
}
|
|
|
|
updatedAt, err := globalIAMSys.CreateUser(ctx, accessKey, ureq)
|
|
if err != nil {
|
|
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
|
return
|
|
}
|
|
|
|
logger.LogIf(ctx, globalSiteReplicationSys.IAMChangeHook(ctx, madmin.SRIAMItem{
|
|
Type: madmin.SRIAMItemIAMUser,
|
|
IAMUser: &madmin.SRIAMUser{
|
|
AccessKey: accessKey,
|
|
IsDeleteReq: false,
|
|
UserReq: &ureq,
|
|
},
|
|
UpdatedAt: updatedAt,
|
|
}))
|
|
}
|
|
|
|
// TemporaryAccountInfo - GET /minio/admin/v3/temporary-account-info
|
|
func (a adminAPIHandlers) TemporaryAccountInfo(w http.ResponseWriter, r *http.Request) {
|
|
ctx := r.Context()
|
|
|
|
// Get current object layer instance.
|
|
objectAPI := newObjectLayerFn()
|
|
if objectAPI == nil || globalNotificationSys == nil {
|
|
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrServerNotInitialized), r.URL)
|
|
return
|
|
}
|
|
|
|
cred, owner, s3Err := validateAdminSignature(ctx, r, "")
|
|
if s3Err != ErrNone {
|
|
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(s3Err), r.URL)
|
|
return
|
|
}
|
|
|
|
accessKey := mux.Vars(r)["accessKey"]
|
|
if accessKey == "" {
|
|
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrInvalidRequest), r.URL)
|
|
return
|
|
}
|
|
|
|
if !globalIAMSys.IsAllowed(policy.Args{
|
|
AccountName: cred.AccessKey,
|
|
Groups: cred.Groups,
|
|
Action: policy.ListTemporaryAccountsAdminAction,
|
|
ConditionValues: getConditionValues(r, "", cred),
|
|
IsOwner: owner,
|
|
Claims: cred.Claims,
|
|
}) {
|
|
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAccessDenied), r.URL)
|
|
return
|
|
}
|
|
|
|
stsAccount, sessionPolicy, err := globalIAMSys.GetTemporaryAccount(ctx, accessKey)
|
|
if err != nil {
|
|
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
|
return
|
|
}
|
|
|
|
var stsAccountPolicy policy.Policy
|
|
|
|
if sessionPolicy != nil {
|
|
stsAccountPolicy = *sessionPolicy
|
|
} else {
|
|
policiesNames, err := globalIAMSys.PolicyDBGet(stsAccount.ParentUser, false)
|
|
if err != nil {
|
|
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
|
return
|
|
}
|
|
stsAccountPolicy = globalIAMSys.GetCombinedPolicy(policiesNames...)
|
|
}
|
|
|
|
policyJSON, err := json.MarshalIndent(stsAccountPolicy, "", " ")
|
|
if err != nil {
|
|
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
|
return
|
|
}
|
|
|
|
infoResp := madmin.TemporaryAccountInfoResp{
|
|
ParentUser: stsAccount.ParentUser,
|
|
AccountStatus: stsAccount.Status,
|
|
ImpliedPolicy: sessionPolicy == nil,
|
|
Policy: string(policyJSON),
|
|
Expiration: &stsAccount.Expiration,
|
|
}
|
|
|
|
data, err := json.Marshal(infoResp)
|
|
if err != nil {
|
|
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
|
return
|
|
}
|
|
|
|
encryptedData, err := madmin.EncryptData(cred.SecretKey, data)
|
|
if err != nil {
|
|
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
|
return
|
|
}
|
|
|
|
writeSuccessResponseJSON(w, encryptedData)
|
|
}
|
|
|
|
// AddServiceAccount - PUT /minio/admin/v3/add-service-account
|
|
func (a adminAPIHandlers) AddServiceAccount(w http.ResponseWriter, r *http.Request) {
|
|
ctx := r.Context()
|
|
|
|
// Get current object layer instance.
|
|
objectAPI := newObjectLayerFn()
|
|
if objectAPI == nil || globalNotificationSys == nil {
|
|
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrServerNotInitialized), r.URL)
|
|
return
|
|
}
|
|
|
|
cred, owner, s3Err := validateAdminSignature(ctx, r, "")
|
|
if s3Err != ErrNone {
|
|
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(s3Err), r.URL)
|
|
return
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
if err := createReq.Validate(); err != nil {
|
|
// Since this validation would happen client side as well, we only send
|
|
// a generic error message here.
|
|
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAdminResourceInvalidArgument), r.URL)
|
|
return
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
description := createReq.Description
|
|
if description == "" {
|
|
description = createReq.Comment
|
|
}
|
|
opts := newServiceAccountOpts{
|
|
accessKey: createReq.AccessKey,
|
|
secretKey: createReq.SecretKey,
|
|
name: createReq.Name,
|
|
description: description,
|
|
expiration: createReq.Expiration,
|
|
claims: make(map[string]interface{}),
|
|
}
|
|
|
|
// Find the user for the request sender (as it may be sent via a service
|
|
// account or STS account):
|
|
requestorUser := cred.AccessKey
|
|
requestorParentUser := cred.AccessKey
|
|
requestorGroups := cred.Groups
|
|
requestorIsDerivedCredential := false
|
|
if cred.IsServiceAccount() || cred.IsTemp() {
|
|
requestorParentUser = cred.ParentUser
|
|
requestorIsDerivedCredential = true
|
|
}
|
|
|
|
if globalIAMSys.GetUsersSysType() == MinIOUsersSysType && targetUser != cred.AccessKey {
|
|
// For internal IDP, ensure that the targetUser's parent account exists.
|
|
// It could be a regular user account or the root account.
|
|
_, isRegularUser := globalIAMSys.GetUser(ctx, targetUser)
|
|
if !isRegularUser && targetUser != globalActiveCred.AccessKey {
|
|
apiErr := toAdminAPIErr(ctx, errNoSuchUser)
|
|
apiErr.Description = fmt.Sprintf("Specified target user %s does not exist", targetUser)
|
|
writeErrorResponseJSON(ctx, w, apiErr, r.URL)
|
|
return
|
|
}
|
|
}
|
|
|
|
// Check if we are creating svc account for request sender.
|
|
isSvcAccForRequestor := false
|
|
if targetUser == requestorUser || targetUser == requestorParentUser {
|
|
isSvcAccForRequestor = true
|
|
}
|
|
|
|
// If we are creating svc account for request sender, ensure
|
|
// that targetUser is a real user (i.e. not derived
|
|
// credentials).
|
|
if isSvcAccForRequestor {
|
|
// 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(policy.Args{
|
|
AccountName: requestorUser,
|
|
Groups: requestorGroups,
|
|
Action: policy.CreateServiceAccountAdminAction,
|
|
ConditionValues: getConditionValues(r, "", cred),
|
|
IsOwner: owner,
|
|
Claims: cred.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 account for a
|
|
// user <> to the request sender
|
|
if !globalIAMSys.IsAllowed(policy.Args{
|
|
AccountName: requestorUser,
|
|
Groups: requestorGroups,
|
|
Action: policy.CreateServiceAccountAdminAction,
|
|
ConditionValues: getConditionValues(r, "", cred),
|
|
IsOwner: owner,
|
|
Claims: cred.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 globalIAMSys.LDAPConfig.Enabled() {
|
|
opts.claims[ldapUserN] = targetUser // simple username
|
|
targetUser, targetGroups, err = globalIAMSys.LDAPConfig.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 *policy.Policy
|
|
if len(createReq.Policy) > 0 {
|
|
sp, err = policy.ParseConfig(bytes.NewReader(createReq.Policy))
|
|
if err != nil {
|
|
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
|
return
|
|
}
|
|
}
|
|
|
|
opts.sessionPolicy = sp
|
|
newCred, updatedAt, err := globalIAMSys.NewServiceAccount(ctx, targetUser, targetGroups, opts)
|
|
if err != nil {
|
|
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
|
return
|
|
}
|
|
|
|
createResp := madmin.AddServiceAccountResp{
|
|
Credentials: madmin.Credentials{
|
|
AccessKey: newCred.AccessKey,
|
|
SecretKey: newCred.SecretKey,
|
|
Expiration: newCred.Expiration,
|
|
},
|
|
}
|
|
|
|
data, err := json.Marshal(createResp)
|
|
if err != nil {
|
|
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
|
return
|
|
}
|
|
|
|
encryptedData, err := madmin.EncryptData(password, data)
|
|
if err != nil {
|
|
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
|
return
|
|
}
|
|
|
|
writeSuccessResponseJSON(w, encryptedData)
|
|
|
|
// Call hook for cluster-replication if the service account is not for a
|
|
// root user.
|
|
if newCred.ParentUser != globalActiveCred.AccessKey {
|
|
logger.LogIf(ctx, globalSiteReplicationSys.IAMChangeHook(ctx, madmin.SRIAMItem{
|
|
Type: madmin.SRIAMItemSvcAcc,
|
|
SvcAccChange: &madmin.SRSvcAccChange{
|
|
Create: &madmin.SRSvcAccCreate{
|
|
Parent: newCred.ParentUser,
|
|
AccessKey: newCred.AccessKey,
|
|
SecretKey: newCred.SecretKey,
|
|
Groups: newCred.Groups,
|
|
Name: newCred.Name,
|
|
Description: newCred.Description,
|
|
Claims: opts.claims,
|
|
SessionPolicy: createReq.Policy,
|
|
Status: auth.AccountOn,
|
|
Expiration: createReq.Expiration,
|
|
},
|
|
},
|
|
UpdatedAt: updatedAt,
|
|
}))
|
|
}
|
|
}
|
|
|
|
// UpdateServiceAccount - POST /minio/admin/v3/update-service-account
|
|
func (a adminAPIHandlers) UpdateServiceAccount(w http.ResponseWriter, r *http.Request) {
|
|
ctx := r.Context()
|
|
|
|
// Get current object layer instance.
|
|
objectAPI := newObjectLayerFn()
|
|
if objectAPI == nil || globalNotificationSys == nil {
|
|
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrServerNotInitialized), r.URL)
|
|
return
|
|
}
|
|
|
|
cred, owner, s3Err := validateAdminSignature(ctx, r, "")
|
|
if s3Err != ErrNone {
|
|
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(s3Err), r.URL)
|
|
return
|
|
}
|
|
|
|
accessKey := mux.Vars(r)["accessKey"]
|
|
if accessKey == "" {
|
|
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrInvalidRequest), r.URL)
|
|
return
|
|
}
|
|
|
|
svcAccount, _, err := globalIAMSys.GetServiceAccount(ctx, accessKey)
|
|
if err != nil {
|
|
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
|
return
|
|
}
|
|
|
|
if !globalIAMSys.IsAllowed(policy.Args{
|
|
AccountName: cred.AccessKey,
|
|
Groups: cred.Groups,
|
|
Action: policy.UpdateServiceAccountAdminAction,
|
|
ConditionValues: getConditionValues(r, "", cred),
|
|
IsOwner: owner,
|
|
Claims: cred.Claims,
|
|
}) {
|
|
requestUser := cred.AccessKey
|
|
if cred.ParentUser != "" {
|
|
requestUser = cred.ParentUser
|
|
}
|
|
|
|
if requestUser != svcAccount.ParentUser {
|
|
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAccessDenied), r.URL)
|
|
return
|
|
}
|
|
}
|
|
|
|
password := cred.SecretKey
|
|
reqBytes, err := madmin.DecryptData(password, io.LimitReader(r.Body, r.ContentLength))
|
|
if err != nil {
|
|
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErrWithErr(ErrAdminConfigBadJSON, err), r.URL)
|
|
return
|
|
}
|
|
|
|
var updateReq madmin.UpdateServiceAccountReq
|
|
if err = json.Unmarshal(reqBytes, &updateReq); err != nil {
|
|
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErrWithErr(ErrAdminConfigBadJSON, err), r.URL)
|
|
return
|
|
}
|
|
|
|
if err := updateReq.Validate(); err != nil {
|
|
// Since this validation would happen client side as well, we only send
|
|
// a generic error message here.
|
|
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAdminResourceInvalidArgument), r.URL)
|
|
return
|
|
}
|
|
|
|
var sp *policy.Policy
|
|
if len(updateReq.NewPolicy) > 0 {
|
|
sp, err = policy.ParseConfig(bytes.NewReader(updateReq.NewPolicy))
|
|
if err != nil {
|
|
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
|
return
|
|
}
|
|
}
|
|
opts := updateServiceAccountOpts{
|
|
secretKey: updateReq.NewSecretKey,
|
|
status: updateReq.NewStatus,
|
|
name: updateReq.NewName,
|
|
description: updateReq.NewDescription,
|
|
expiration: updateReq.NewExpiration,
|
|
sessionPolicy: sp,
|
|
}
|
|
updatedAt, err := globalIAMSys.UpdateServiceAccount(ctx, accessKey, opts)
|
|
if err != nil {
|
|
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
|
return
|
|
}
|
|
|
|
// Call site replication hook - non-root user accounts are replicated.
|
|
if svcAccount.ParentUser != globalActiveCred.AccessKey {
|
|
logger.LogIf(ctx, globalSiteReplicationSys.IAMChangeHook(ctx, madmin.SRIAMItem{
|
|
Type: madmin.SRIAMItemSvcAcc,
|
|
SvcAccChange: &madmin.SRSvcAccChange{
|
|
Update: &madmin.SRSvcAccUpdate{
|
|
AccessKey: accessKey,
|
|
SecretKey: opts.secretKey,
|
|
Status: opts.status,
|
|
Name: opts.name,
|
|
Description: opts.description,
|
|
SessionPolicy: updateReq.NewPolicy,
|
|
Expiration: updateReq.NewExpiration,
|
|
},
|
|
},
|
|
UpdatedAt: updatedAt,
|
|
}))
|
|
}
|
|
|
|
writeSuccessNoContent(w)
|
|
}
|
|
|
|
// InfoServiceAccount - GET /minio/admin/v3/info-service-account
|
|
func (a adminAPIHandlers) InfoServiceAccount(w http.ResponseWriter, r *http.Request) {
|
|
ctx := r.Context()
|
|
|
|
// Get current object layer instance.
|
|
objectAPI := newObjectLayerFn()
|
|
if objectAPI == nil || globalNotificationSys == nil {
|
|
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrServerNotInitialized), r.URL)
|
|
return
|
|
}
|
|
|
|
cred, owner, s3Err := validateAdminSignature(ctx, r, "")
|
|
if s3Err != ErrNone {
|
|
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(s3Err), r.URL)
|
|
return
|
|
}
|
|
|
|
accessKey := mux.Vars(r)["accessKey"]
|
|
if accessKey == "" {
|
|
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrInvalidRequest), r.URL)
|
|
return
|
|
}
|
|
|
|
svcAccount, sessionPolicy, err := globalIAMSys.GetServiceAccount(ctx, accessKey)
|
|
if err != nil {
|
|
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
|
return
|
|
}
|
|
|
|
if !globalIAMSys.IsAllowed(policy.Args{
|
|
AccountName: cred.AccessKey,
|
|
Groups: cred.Groups,
|
|
Action: policy.ListServiceAccountsAdminAction,
|
|
ConditionValues: getConditionValues(r, "", cred),
|
|
IsOwner: owner,
|
|
Claims: cred.Claims,
|
|
}) {
|
|
requestUser := cred.AccessKey
|
|
if cred.ParentUser != "" {
|
|
requestUser = cred.ParentUser
|
|
}
|
|
|
|
if requestUser != svcAccount.ParentUser {
|
|
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAccessDenied), r.URL)
|
|
return
|
|
}
|
|
}
|
|
|
|
var svcAccountPolicy policy.Policy
|
|
|
|
if sessionPolicy != nil {
|
|
svcAccountPolicy = *sessionPolicy
|
|
} 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
|
|
}
|
|
|
|
var expiration *time.Time
|
|
if !svcAccount.Expiration.IsZero() && !svcAccount.Expiration.Equal(timeSentinel) {
|
|
expiration = &svcAccount.Expiration
|
|
}
|
|
|
|
infoResp := madmin.InfoServiceAccountResp{
|
|
ParentUser: svcAccount.ParentUser,
|
|
Name: svcAccount.Name,
|
|
Description: svcAccount.Description,
|
|
AccountStatus: svcAccount.Status,
|
|
ImpliedPolicy: sessionPolicy == nil,
|
|
Policy: string(policyJSON),
|
|
Expiration: expiration,
|
|
}
|
|
|
|
data, err := json.Marshal(infoResp)
|
|
if err != nil {
|
|
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
|
return
|
|
}
|
|
|
|
encryptedData, err := madmin.EncryptData(cred.SecretKey, data)
|
|
if err != nil {
|
|
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
|
return
|
|
}
|
|
|
|
writeSuccessResponseJSON(w, encryptedData)
|
|
}
|
|
|
|
// ListServiceAccounts - GET /minio/admin/v3/list-service-accounts
|
|
func (a adminAPIHandlers) ListServiceAccounts(w http.ResponseWriter, r *http.Request) {
|
|
ctx := r.Context()
|
|
|
|
// Get current object layer instance.
|
|
objectAPI := newObjectLayerFn()
|
|
if objectAPI == nil || globalNotificationSys == nil {
|
|
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrServerNotInitialized), r.URL)
|
|
return
|
|
}
|
|
|
|
cred, owner, s3Err := validateAdminSignature(ctx, r, "")
|
|
if s3Err != ErrNone {
|
|
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(s3Err), r.URL)
|
|
return
|
|
}
|
|
|
|
var targetAccount string
|
|
|
|
// If listing is requested for a specific user (who is not the request
|
|
// sender), check that the user has permissions.
|
|
user := r.Form.Get("user")
|
|
if user != "" && user != cred.AccessKey {
|
|
if !globalIAMSys.IsAllowed(policy.Args{
|
|
AccountName: cred.AccessKey,
|
|
Groups: cred.Groups,
|
|
Action: policy.ListServiceAccountsAdminAction,
|
|
ConditionValues: getConditionValues(r, "", cred),
|
|
IsOwner: owner,
|
|
Claims: cred.Claims,
|
|
}) {
|
|
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAccessDenied), r.URL)
|
|
return
|
|
}
|
|
targetAccount = user
|
|
} else {
|
|
targetAccount = cred.AccessKey
|
|
if cred.ParentUser != "" {
|
|
targetAccount = cred.ParentUser
|
|
}
|
|
}
|
|
|
|
serviceAccounts, err := globalIAMSys.ListServiceAccounts(ctx, targetAccount)
|
|
if err != nil {
|
|
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
|
return
|
|
}
|
|
|
|
var serviceAccountList []madmin.ServiceAccountInfo
|
|
|
|
for _, svc := range serviceAccounts {
|
|
expiryTime := svc.Expiration
|
|
serviceAccountList = append(serviceAccountList, madmin.ServiceAccountInfo{
|
|
AccessKey: svc.AccessKey,
|
|
Expiration: &expiryTime,
|
|
})
|
|
}
|
|
|
|
listResp := madmin.ListServiceAccountsResp{
|
|
Accounts: serviceAccountList,
|
|
}
|
|
|
|
data, err := json.Marshal(listResp)
|
|
if err != nil {
|
|
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
|
return
|
|
}
|
|
|
|
encryptedData, err := madmin.EncryptData(cred.SecretKey, data)
|
|
if err != nil {
|
|
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
|
return
|
|
}
|
|
|
|
writeSuccessResponseJSON(w, encryptedData)
|
|
}
|
|
|
|
// DeleteServiceAccount - DELETE /minio/admin/v3/delete-service-account
|
|
func (a adminAPIHandlers) DeleteServiceAccount(w http.ResponseWriter, r *http.Request) {
|
|
ctx := r.Context()
|
|
|
|
// Get current object layer instance.
|
|
objectAPI := newObjectLayerFn()
|
|
if objectAPI == nil || globalNotificationSys == nil {
|
|
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrServerNotInitialized), r.URL)
|
|
return
|
|
}
|
|
|
|
cred, owner, s3Err := validateAdminSignature(ctx, r, "")
|
|
if s3Err != ErrNone {
|
|
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(s3Err), r.URL)
|
|
return
|
|
}
|
|
|
|
serviceAccount := mux.Vars(r)["accessKey"]
|
|
if serviceAccount == "" {
|
|
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAdminInvalidArgument), r.URL)
|
|
return
|
|
}
|
|
|
|
// 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(policy.Args{
|
|
AccountName: cred.AccessKey,
|
|
Groups: cred.Groups,
|
|
Action: policy.RemoveServiceAccountAdminAction,
|
|
ConditionValues: getConditionValues(r, "", cred),
|
|
IsOwner: owner,
|
|
Claims: cred.Claims,
|
|
})
|
|
|
|
if !adminPrivilege {
|
|
parentUser := cred.AccessKey
|
|
if cred.ParentUser != "" {
|
|
parentUser = cred.ParentUser
|
|
}
|
|
if svcAccount.ParentUser != "" && parentUser != svcAccount.ParentUser {
|
|
// The service account belongs to another user but return not
|
|
// found error to mitigate brute force attacks. or the
|
|
// serviceAccount doesn't exist.
|
|
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAdminServiceAccountNotFound), r.URL)
|
|
return
|
|
}
|
|
}
|
|
|
|
if err := globalIAMSys.DeleteServiceAccount(ctx, serviceAccount, true); err != nil {
|
|
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
|
return
|
|
}
|
|
|
|
// Call site replication hook - non-root user accounts are replicated.
|
|
if svcAccount.ParentUser != "" && svcAccount.ParentUser != globalActiveCred.AccessKey {
|
|
logger.LogIf(ctx, globalSiteReplicationSys.IAMChangeHook(ctx, madmin.SRIAMItem{
|
|
Type: madmin.SRIAMItemSvcAcc,
|
|
SvcAccChange: &madmin.SRSvcAccChange{
|
|
Delete: &madmin.SRSvcAccDelete{
|
|
AccessKey: serviceAccount,
|
|
},
|
|
},
|
|
UpdatedAt: UTCNow(),
|
|
}))
|
|
}
|
|
|
|
writeSuccessNoContent(w)
|
|
}
|
|
|
|
// AccountInfoHandler returns usage, permissions and other bucket metadata for incoming us
|
|
func (a adminAPIHandlers) AccountInfoHandler(w http.ResponseWriter, r *http.Request) {
|
|
ctx := r.Context()
|
|
|
|
// Get current object layer instance.
|
|
objectAPI := newObjectLayerFn()
|
|
if objectAPI == nil || globalNotificationSys == nil {
|
|
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrServerNotInitialized), r.URL)
|
|
return
|
|
}
|
|
|
|
cred, owner, s3Err := validateAdminSignature(ctx, r, "")
|
|
if s3Err != ErrNone {
|
|
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(s3Err), r.URL)
|
|
return
|
|
}
|
|
|
|
// Set prefix value for "s3:prefix" policy conditionals.
|
|
r.Header.Set("prefix", "")
|
|
|
|
// Set delimiter value for "s3:delimiter" policy conditionals.
|
|
r.Header.Set("delimiter", SlashSeparator)
|
|
|
|
// Check if we are asked to return prefix usage
|
|
enablePrefixUsage := r.Form.Get("prefix-usage") == "true"
|
|
|
|
isAllowedAccess := func(bucketName string) (rd, wr bool) {
|
|
if globalIAMSys.IsAllowed(policy.Args{
|
|
AccountName: cred.AccessKey,
|
|
Groups: cred.Groups,
|
|
Action: policy.ListBucketAction,
|
|
BucketName: bucketName,
|
|
ConditionValues: getConditionValues(r, "", cred),
|
|
IsOwner: owner,
|
|
ObjectName: "",
|
|
Claims: cred.Claims,
|
|
}) {
|
|
rd = true
|
|
}
|
|
|
|
if globalIAMSys.IsAllowed(policy.Args{
|
|
AccountName: cred.AccessKey,
|
|
Groups: cred.Groups,
|
|
Action: policy.GetBucketLocationAction,
|
|
BucketName: bucketName,
|
|
ConditionValues: getConditionValues(r, "", cred),
|
|
IsOwner: owner,
|
|
ObjectName: "",
|
|
Claims: cred.Claims,
|
|
}) {
|
|
rd = true
|
|
}
|
|
|
|
if globalIAMSys.IsAllowed(policy.Args{
|
|
AccountName: cred.AccessKey,
|
|
Groups: cred.Groups,
|
|
Action: policy.PutObjectAction,
|
|
BucketName: bucketName,
|
|
ConditionValues: getConditionValues(r, "", cred),
|
|
IsOwner: owner,
|
|
ObjectName: "",
|
|
Claims: cred.Claims,
|
|
}) {
|
|
wr = true
|
|
}
|
|
|
|
return rd, wr
|
|
}
|
|
|
|
bucketStorageCache.Once.Do(func() {
|
|
// Set this to 10 secs since its enough, as scanner
|
|
// does not update the bucket usage values frequently.
|
|
bucketStorageCache.TTL = 10 * time.Second
|
|
|
|
// Rely on older value if usage loading fails from disk.
|
|
bucketStorageCache.Relax = true
|
|
bucketStorageCache.Update = func() (interface{}, error) {
|
|
ctx, done := context.WithTimeout(context.Background(), 2*time.Second)
|
|
defer done()
|
|
|
|
return loadDataUsageFromBackend(ctx, objectAPI)
|
|
}
|
|
})
|
|
|
|
var dataUsageInfo DataUsageInfo
|
|
v, _ := bucketStorageCache.Get()
|
|
if v != nil {
|
|
dataUsageInfo, _ = v.(DataUsageInfo)
|
|
}
|
|
|
|
// If etcd, dns federation configured list buckets from etcd.
|
|
var err error
|
|
var buckets []BucketInfo
|
|
if globalDNSConfig != nil && globalBucketFederation {
|
|
dnsBuckets, err := globalDNSConfig.List()
|
|
if err != nil && !IsErrIgnored(err,
|
|
dns.ErrNoEntriesFound,
|
|
dns.ErrDomainMissing) {
|
|
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
|
return
|
|
}
|
|
for _, dnsRecords := range dnsBuckets {
|
|
buckets = append(buckets, BucketInfo{
|
|
Name: dnsRecords[0].Key,
|
|
Created: dnsRecords[0].CreationDate,
|
|
})
|
|
}
|
|
sort.Slice(buckets, func(i, j int) bool {
|
|
return buckets[i].Name < buckets[j].Name
|
|
})
|
|
} else {
|
|
buckets, err = objectAPI.ListBuckets(ctx, BucketOptions{Cached: true})
|
|
if err != nil {
|
|
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
|
return
|
|
}
|
|
}
|
|
|
|
accountName := cred.AccessKey
|
|
if cred.IsTemp() || cred.IsServiceAccount() {
|
|
// For derived credentials, check the parent user's permissions.
|
|
accountName = cred.ParentUser
|
|
}
|
|
|
|
roleArn := policy.Args{Claims: cred.Claims}.GetRoleArn()
|
|
policySetFromClaims, hasPolicyClaim := policy.GetPoliciesFromClaims(cred.Claims, iamPolicyClaimNameOpenID())
|
|
var effectivePolicy policy.Policy
|
|
|
|
var buf []byte
|
|
switch {
|
|
case accountName == globalActiveCred.AccessKey:
|
|
for _, policy := range policy.DefaultPolicies {
|
|
if policy.Name == "consoleAdmin" {
|
|
effectivePolicy = policy.Definition
|
|
break
|
|
}
|
|
}
|
|
|
|
case roleArn != "":
|
|
_, policy, err := globalIAMSys.GetRolePolicy(roleArn)
|
|
if err != nil {
|
|
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
|
return
|
|
}
|
|
policySlice := newMappedPolicy(policy).toSlice()
|
|
effectivePolicy = globalIAMSys.GetCombinedPolicy(policySlice...)
|
|
|
|
case hasPolicyClaim:
|
|
effectivePolicy = globalIAMSys.GetCombinedPolicy(policySetFromClaims.ToSlice()...)
|
|
|
|
default:
|
|
policies, err := globalIAMSys.PolicyDBGet(accountName, false, cred.Groups...)
|
|
if err != nil {
|
|
logger.LogIf(ctx, err)
|
|
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
|
return
|
|
}
|
|
effectivePolicy = globalIAMSys.GetCombinedPolicy(policies...)
|
|
|
|
}
|
|
buf, err = json.MarshalIndent(effectivePolicy, "", " ")
|
|
if err != nil {
|
|
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
|
return
|
|
}
|
|
|
|
acctInfo := madmin.AccountInfo{
|
|
AccountName: accountName,
|
|
Server: objectAPI.BackendInfo(),
|
|
Policy: buf,
|
|
}
|
|
|
|
for _, bucket := range buckets {
|
|
rd, wr := isAllowedAccess(bucket.Name)
|
|
if rd || wr {
|
|
// Fetch the data usage of the current bucket
|
|
var size uint64
|
|
var objectsCount uint64
|
|
var objectsHist, versionsHist 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
|
|
versionsHist = dataUsageInfo.BucketsUsage[bucket.Name].ObjectVersionsHistogram
|
|
}
|
|
// Fetch the prefix usage of the current bucket
|
|
var prefixUsage map[string]uint64
|
|
if enablePrefixUsage {
|
|
prefixUsage, _ = loadPrefixUsageFromBackend(ctx, objectAPI, bucket.Name)
|
|
}
|
|
|
|
lcfg, _ := globalBucketObjectLockSys.Get(bucket.Name)
|
|
quota, _ := globalBucketQuotaSys.Get(ctx, bucket.Name)
|
|
rcfg, _, _ := globalBucketMetadataSys.GetReplicationConfig(ctx, bucket.Name)
|
|
tcfg, _, _ := globalBucketMetadataSys.GetTaggingConfig(bucket.Name)
|
|
|
|
acctInfo.Buckets = append(acctInfo.Buckets, madmin.BucketAccessInfo{
|
|
Name: bucket.Name,
|
|
Created: bucket.Created,
|
|
Size: size,
|
|
Objects: objectsCount,
|
|
ObjectSizesHistogram: objectsHist,
|
|
ObjectVersionsHistogram: versionsHist,
|
|
PrefixUsage: prefixUsage,
|
|
Details: &madmin.BucketDetails{
|
|
Versioning: globalBucketVersioningSys.Enabled(bucket.Name),
|
|
VersioningSuspended: globalBucketVersioningSys.Suspended(bucket.Name),
|
|
Replication: rcfg != nil,
|
|
Locking: lcfg.LockEnabled,
|
|
Quota: quota,
|
|
Tagging: tcfg,
|
|
},
|
|
Access: madmin.AccountAccess{
|
|
Read: rd,
|
|
Write: wr,
|
|
},
|
|
})
|
|
}
|
|
}
|
|
|
|
usageInfoJSON, err := json.Marshal(acctInfo)
|
|
if err != nil {
|
|
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
|
return
|
|
}
|
|
|
|
writeSuccessResponseJSON(w, usageInfoJSON)
|
|
}
|
|
|
|
// InfoCannedPolicy - GET /minio/admin/v3/info-canned-policy?name={policyName}
|
|
//
|
|
// Newer API response with policy timestamps is returned with query parameter
|
|
// `v=2` like:
|
|
//
|
|
// GET /minio/admin/v3/info-canned-policy?name={policyName}&v=2
|
|
//
|
|
// The newer API will eventually become the default (and only) one. The older
|
|
// response is to return only the policy JSON. The newer response returns
|
|
// timestamps along with the policy JSON. Both versions are supported for now,
|
|
// for smooth transition to new API.
|
|
func (a adminAPIHandlers) InfoCannedPolicy(w http.ResponseWriter, r *http.Request) {
|
|
ctx := r.Context()
|
|
|
|
objectAPI, _ := validateAdminReq(ctx, w, r, policy.GetPolicyAdminAction)
|
|
if objectAPI == nil {
|
|
return
|
|
}
|
|
|
|
name := mux.Vars(r)["name"]
|
|
policies := newMappedPolicy(name).toSlice()
|
|
if len(policies) != 1 {
|
|
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, errTooManyPolicies), r.URL)
|
|
return
|
|
}
|
|
|
|
policyDoc, err := globalIAMSys.InfoPolicy(name)
|
|
if err != nil {
|
|
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
|
return
|
|
}
|
|
|
|
// Is the new API version being requested?
|
|
infoPolicyAPIVersion := r.Form.Get("v")
|
|
if infoPolicyAPIVersion == "2" {
|
|
buf, err := json.MarshalIndent(policyDoc, "", " ")
|
|
if err != nil {
|
|
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
|
return
|
|
}
|
|
w.Write(buf)
|
|
return
|
|
} else if infoPolicyAPIVersion != "" {
|
|
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, errors.New("invalid version parameter 'v' supplied")), r.URL)
|
|
return
|
|
}
|
|
|
|
// Return the older API response value of just the policy json.
|
|
buf, err := json.MarshalIndent(policyDoc.Policy, "", " ")
|
|
if err != nil {
|
|
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
|
return
|
|
}
|
|
w.Write(buf)
|
|
}
|
|
|
|
// ListBucketPolicies - GET /minio/admin/v3/list-canned-policies?bucket={bucket}
|
|
func (a adminAPIHandlers) ListBucketPolicies(w http.ResponseWriter, r *http.Request) {
|
|
ctx := r.Context()
|
|
|
|
objectAPI, _ := validateAdminReq(ctx, w, r, policy.ListUserPoliciesAdminAction)
|
|
if objectAPI == nil {
|
|
return
|
|
}
|
|
|
|
bucket := mux.Vars(r)["bucket"]
|
|
policies, err := globalIAMSys.ListPolicies(ctx, bucket)
|
|
if err != nil {
|
|
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
|
return
|
|
}
|
|
|
|
newPolicies := make(map[string]policy.Policy)
|
|
for name, p := range policies {
|
|
_, err = json.Marshal(p)
|
|
if err != nil {
|
|
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 := r.Context()
|
|
|
|
objectAPI, _ := validateAdminReq(ctx, w, r, policy.ListUserPoliciesAdminAction)
|
|
if objectAPI == nil {
|
|
return
|
|
}
|
|
|
|
policies, err := globalIAMSys.ListPolicies(ctx, "")
|
|
if err != nil {
|
|
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
|
return
|
|
}
|
|
|
|
newPolicies := make(map[string]policy.Policy)
|
|
for name, p := range policies {
|
|
_, err = json.Marshal(p)
|
|
if err != nil {
|
|
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 := r.Context()
|
|
|
|
objectAPI, _ := validateAdminReq(ctx, w, r, policy.DeletePolicyAdminAction)
|
|
if objectAPI == nil {
|
|
return
|
|
}
|
|
|
|
vars := mux.Vars(r)
|
|
policyName := vars["name"]
|
|
|
|
if err := globalIAMSys.DeletePolicy(ctx, policyName, true); err != nil {
|
|
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
|
return
|
|
}
|
|
|
|
// Call cluster-replication policy creation hook to replicate policy deletion to
|
|
// other minio clusters.
|
|
logger.LogIf(ctx, globalSiteReplicationSys.IAMChangeHook(ctx, madmin.SRIAMItem{
|
|
Type: madmin.SRIAMItemPolicy,
|
|
Name: policyName,
|
|
UpdatedAt: UTCNow(),
|
|
}))
|
|
}
|
|
|
|
// AddCannedPolicy - PUT /minio/admin/v3/add-canned-policy?name=<policy_name>
|
|
func (a adminAPIHandlers) AddCannedPolicy(w http.ResponseWriter, r *http.Request) {
|
|
ctx := r.Context()
|
|
|
|
objectAPI, _ := validateAdminReq(ctx, w, r, policy.CreatePolicyAdminAction)
|
|
if objectAPI == nil {
|
|
return
|
|
}
|
|
|
|
vars := mux.Vars(r)
|
|
policyName := vars["name"]
|
|
|
|
// Policy has space characters in begin and end reject such inputs.
|
|
if hasSpaceBE(policyName) {
|
|
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAdminResourceInvalidArgument), r.URL)
|
|
return
|
|
}
|
|
|
|
// Error out if Content-Length is missing.
|
|
if r.ContentLength <= 0 {
|
|
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrMissingContentLength), r.URL)
|
|
return
|
|
}
|
|
|
|
// Error out if Content-Length is beyond allowed size.
|
|
if r.ContentLength > maxBucketPolicySize {
|
|
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrEntityTooLarge), r.URL)
|
|
return
|
|
}
|
|
|
|
iamPolicyBytes, err := io.ReadAll(io.LimitReader(r.Body, r.ContentLength))
|
|
if err != nil {
|
|
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
|
return
|
|
}
|
|
|
|
iamPolicy, err := policy.ParseConfig(bytes.NewReader(iamPolicyBytes))
|
|
if err != nil {
|
|
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
|
return
|
|
}
|
|
|
|
// Version in policy must not be empty
|
|
if iamPolicy.Version == "" {
|
|
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrPolicyInvalidVersion), r.URL)
|
|
return
|
|
}
|
|
|
|
updatedAt, err := globalIAMSys.SetPolicy(ctx, policyName, *iamPolicy)
|
|
if err != nil {
|
|
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
|
return
|
|
}
|
|
|
|
// Call cluster-replication policy creation hook to replicate policy to
|
|
// other minio clusters.
|
|
logger.LogIf(ctx, globalSiteReplicationSys.IAMChangeHook(ctx, madmin.SRIAMItem{
|
|
Type: madmin.SRIAMItemPolicy,
|
|
Name: policyName,
|
|
Policy: iamPolicyBytes,
|
|
UpdatedAt: updatedAt,
|
|
}))
|
|
}
|
|
|
|
// 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 := r.Context()
|
|
|
|
objectAPI, _ := validateAdminReq(ctx, w, r, policy.AttachPolicyAdminAction)
|
|
if objectAPI == nil {
|
|
return
|
|
}
|
|
|
|
vars := mux.Vars(r)
|
|
policyName := vars["policyName"]
|
|
entityName := vars["userOrGroup"]
|
|
isGroup := vars["isGroup"] == "true"
|
|
|
|
if !isGroup {
|
|
ok, _, err := globalIAMSys.IsTempUser(entityName)
|
|
if err != nil && err != errNoSuchUser {
|
|
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
|
return
|
|
}
|
|
if ok {
|
|
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, errIAMActionNotAllowed), r.URL)
|
|
return
|
|
}
|
|
// When the user is root credential you are not allowed to
|
|
// add policies for root user.
|
|
if entityName == globalActiveCred.AccessKey {
|
|
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, errIAMActionNotAllowed), r.URL)
|
|
return
|
|
}
|
|
}
|
|
|
|
// Validate that user or group exists.
|
|
if !isGroup {
|
|
if globalIAMSys.GetUsersSysType() == MinIOUsersSysType {
|
|
_, ok := globalIAMSys.GetUser(ctx, entityName)
|
|
if !ok {
|
|
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, errNoSuchUser), r.URL)
|
|
return
|
|
}
|
|
}
|
|
} else {
|
|
_, err := globalIAMSys.GetGroupDescription(entityName)
|
|
if err != nil {
|
|
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
|
return
|
|
}
|
|
}
|
|
|
|
userType := regUser
|
|
if globalIAMSys.GetUsersSysType() == LDAPUsersSysType {
|
|
userType = stsUser
|
|
}
|
|
|
|
updatedAt, err := globalIAMSys.PolicyDBSet(ctx, entityName, policyName, userType, isGroup)
|
|
if err != nil {
|
|
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
|
return
|
|
}
|
|
|
|
logger.LogIf(ctx, globalSiteReplicationSys.IAMChangeHook(ctx, madmin.SRIAMItem{
|
|
Type: madmin.SRIAMItemPolicyMapping,
|
|
PolicyMapping: &madmin.SRPolicyMapping{
|
|
UserOrGroup: entityName,
|
|
UserType: int(userType),
|
|
IsGroup: isGroup,
|
|
Policy: policyName,
|
|
},
|
|
UpdatedAt: updatedAt,
|
|
}))
|
|
}
|
|
|
|
// ListPolicyMappingEntities - GET /minio/admin/v3/idp/builtin/polciy-entities?policy=xxx&user=xxx&group=xxx
|
|
func (a adminAPIHandlers) ListPolicyMappingEntities(w http.ResponseWriter, r *http.Request) {
|
|
ctx := r.Context()
|
|
|
|
// Check authorization.
|
|
objectAPI, cred := validateAdminReq(ctx, w, r,
|
|
policy.ListGroupsAdminAction, policy.ListUsersAdminAction, policy.ListUserPoliciesAdminAction)
|
|
if objectAPI == nil {
|
|
return
|
|
}
|
|
|
|
// Validate API arguments.
|
|
q := madmin.PolicyEntitiesQuery{
|
|
Users: r.Form["user"],
|
|
Groups: r.Form["group"],
|
|
Policy: r.Form["policy"],
|
|
}
|
|
|
|
// Query IAM
|
|
res, err := globalIAMSys.QueryPolicyEntities(r.Context(), q)
|
|
if err != nil {
|
|
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
|
return
|
|
}
|
|
|
|
// Encode result and send response.
|
|
data, err := json.Marshal(res)
|
|
if err != nil {
|
|
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
|
return
|
|
}
|
|
password := cred.SecretKey
|
|
econfigData, err := madmin.EncryptData(password, data)
|
|
if err != nil {
|
|
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
|
return
|
|
}
|
|
writeSuccessResponseJSON(w, econfigData)
|
|
}
|
|
|
|
// AttachDetachPolicyBuiltin - POST /minio/admin/v3/idp/builtin/policy/{operation}
|
|
func (a adminAPIHandlers) AttachDetachPolicyBuiltin(w http.ResponseWriter, r *http.Request) {
|
|
ctx := r.Context()
|
|
|
|
objectAPI, cred := validateAdminReq(ctx, w, r, policy.UpdatePolicyAssociationAction,
|
|
policy.AttachPolicyAdminAction)
|
|
if objectAPI == nil {
|
|
return
|
|
}
|
|
|
|
if r.ContentLength > maxEConfigJSONSize || r.ContentLength == -1 {
|
|
// More than maxConfigSize bytes were available
|
|
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAdminConfigTooLarge), r.URL)
|
|
return
|
|
}
|
|
|
|
// Ensure body content type is opaque to ensure that request body has not
|
|
// been interpreted as form data.
|
|
contentType := r.Header.Get("Content-Type")
|
|
if contentType != "application/octet-stream" {
|
|
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrBadRequest), r.URL)
|
|
return
|
|
}
|
|
|
|
operation := mux.Vars(r)["operation"]
|
|
if operation != "attach" && operation != "detach" {
|
|
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAdminInvalidArgument), r.URL)
|
|
return
|
|
}
|
|
isAttach := operation == "attach"
|
|
|
|
password := cred.SecretKey
|
|
reqBytes, err := madmin.DecryptData(password, io.LimitReader(r.Body, r.ContentLength))
|
|
if err != nil {
|
|
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
|
return
|
|
}
|
|
|
|
var par madmin.PolicyAssociationReq
|
|
if err = json.Unmarshal(reqBytes, &par); err != nil {
|
|
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
|
return
|
|
}
|
|
|
|
if err = par.IsValid(); err != nil {
|
|
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
|
return
|
|
}
|
|
|
|
updatedAt, addedOrRemoved, _, err := globalIAMSys.PolicyDBUpdateBuiltin(ctx, isAttach, par)
|
|
if err != nil {
|
|
if err == errNoSuchUser || err == errNoSuchGroup {
|
|
if globalIAMSys.LDAPConfig.Enabled() {
|
|
// When LDAP is enabled, warn user that they are using the wrong
|
|
// API. FIXME: error can be no such group as well - fix errNoSuchUserLDAPWarn
|
|
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, errNoSuchUserLDAPWarn), r.URL)
|
|
return
|
|
}
|
|
}
|
|
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
|
return
|
|
}
|
|
|
|
respBody := madmin.PolicyAssociationResp{
|
|
UpdatedAt: updatedAt,
|
|
}
|
|
if isAttach {
|
|
respBody.PoliciesAttached = addedOrRemoved
|
|
} else {
|
|
respBody.PoliciesDetached = addedOrRemoved
|
|
}
|
|
|
|
data, err := json.Marshal(respBody)
|
|
if err != nil {
|
|
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
|
return
|
|
}
|
|
|
|
encryptedData, err := madmin.EncryptData(password, data)
|
|
if err != nil {
|
|
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
|
return
|
|
}
|
|
|
|
writeSuccessResponseJSON(w, encryptedData)
|
|
}
|
|
|
|
const (
|
|
allPoliciesFile = "policies.json"
|
|
allUsersFile = "users.json"
|
|
allGroupsFile = "groups.json"
|
|
allSvcAcctsFile = "svcaccts.json"
|
|
userPolicyMappingsFile = "user_mappings.json"
|
|
groupPolicyMappingsFile = "group_mappings.json"
|
|
stsUserPolicyMappingsFile = "stsuser_mappings.json"
|
|
stsGroupPolicyMappingsFile = "stsgroup_mappings.json"
|
|
iamAssetsDir = "iam-assets"
|
|
)
|
|
|
|
// ExportIAMHandler - exports all iam info as a zipped file
|
|
func (a adminAPIHandlers) ExportIAM(w http.ResponseWriter, r *http.Request) {
|
|
ctx := r.Context()
|
|
|
|
// Get current object layer instance.
|
|
objectAPI, _ := validateAdminReq(ctx, w, r, policy.ExportIAMAction)
|
|
if objectAPI == nil {
|
|
return
|
|
}
|
|
// Initialize a zip writer which will provide a zipped content
|
|
// of bucket metadata
|
|
zipWriter := zip.NewWriter(w)
|
|
defer zipWriter.Close()
|
|
rawDataFn := func(r io.Reader, filename string, sz int) error {
|
|
header, zerr := zip.FileInfoHeader(dummyFileInfo{
|
|
name: filename,
|
|
size: int64(sz),
|
|
mode: 0o600,
|
|
modTime: time.Now(),
|
|
isDir: false,
|
|
sys: nil,
|
|
})
|
|
if zerr != nil {
|
|
logger.LogIf(ctx, zerr)
|
|
return nil
|
|
}
|
|
header.Method = zip.Deflate
|
|
zwriter, zerr := zipWriter.CreateHeader(header)
|
|
if zerr != nil {
|
|
logger.LogIf(ctx, zerr)
|
|
return nil
|
|
}
|
|
if _, err := io.Copy(zwriter, r); err != nil {
|
|
logger.LogIf(ctx, err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
iamFiles := []string{
|
|
allPoliciesFile,
|
|
allUsersFile,
|
|
allGroupsFile,
|
|
allSvcAcctsFile,
|
|
userPolicyMappingsFile,
|
|
groupPolicyMappingsFile,
|
|
stsUserPolicyMappingsFile,
|
|
stsGroupPolicyMappingsFile,
|
|
}
|
|
for _, f := range iamFiles {
|
|
iamFile := pathJoin(iamAssetsDir, f)
|
|
switch f {
|
|
case allPoliciesFile:
|
|
allPolicies, err := globalIAMSys.ListPolicies(ctx, "")
|
|
if err != nil {
|
|
logger.LogIf(ctx, err)
|
|
writeErrorResponse(ctx, w, exportError(ctx, err, iamFile, ""), r.URL)
|
|
return
|
|
}
|
|
|
|
policiesData, err := json.Marshal(allPolicies)
|
|
if err != nil {
|
|
writeErrorResponse(ctx, w, exportError(ctx, err, iamFile, ""), r.URL)
|
|
return
|
|
}
|
|
if err = rawDataFn(bytes.NewReader(policiesData), iamFile, len(policiesData)); err != nil {
|
|
writeErrorResponse(ctx, w, exportError(ctx, err, iamFile, ""), r.URL)
|
|
return
|
|
}
|
|
case allUsersFile:
|
|
userIdentities := make(map[string]UserIdentity)
|
|
err := globalIAMSys.store.loadUsers(ctx, regUser, userIdentities)
|
|
if err != nil {
|
|
writeErrorResponse(ctx, w, exportError(ctx, err, iamFile, ""), r.URL)
|
|
return
|
|
}
|
|
userAccounts := make(map[string]madmin.AddOrUpdateUserReq)
|
|
for u, uid := range userIdentities {
|
|
userAccounts[u] = madmin.AddOrUpdateUserReq{
|
|
SecretKey: uid.Credentials.SecretKey,
|
|
Status: func() madmin.AccountStatus {
|
|
// Export current credential status
|
|
if uid.Credentials.Status == auth.AccountOff {
|
|
return madmin.AccountDisabled
|
|
}
|
|
return madmin.AccountEnabled
|
|
}(),
|
|
}
|
|
}
|
|
userData, err := json.Marshal(userAccounts)
|
|
if err != nil {
|
|
writeErrorResponse(ctx, w, exportError(ctx, err, iamFile, ""), r.URL)
|
|
return
|
|
}
|
|
|
|
if err = rawDataFn(bytes.NewReader(userData), iamFile, len(userData)); err != nil {
|
|
writeErrorResponse(ctx, w, exportError(ctx, err, iamFile, ""), r.URL)
|
|
return
|
|
}
|
|
case allGroupsFile:
|
|
groups := make(map[string]GroupInfo)
|
|
err := globalIAMSys.store.loadGroups(ctx, groups)
|
|
if err != nil {
|
|
writeErrorResponse(ctx, w, exportError(ctx, err, iamFile, ""), r.URL)
|
|
return
|
|
}
|
|
groupData, err := json.Marshal(groups)
|
|
if err != nil {
|
|
writeErrorResponse(ctx, w, exportError(ctx, err, iamFile, ""), r.URL)
|
|
return
|
|
}
|
|
|
|
if err = rawDataFn(bytes.NewReader(groupData), iamFile, len(groupData)); err != nil {
|
|
writeErrorResponse(ctx, w, exportError(ctx, err, iamFile, ""), r.URL)
|
|
return
|
|
}
|
|
case allSvcAcctsFile:
|
|
serviceAccounts := make(map[string]UserIdentity)
|
|
err := globalIAMSys.store.loadUsers(ctx, svcUser, serviceAccounts)
|
|
if err != nil {
|
|
writeErrorResponse(ctx, w, exportError(ctx, err, iamFile, ""), r.URL)
|
|
return
|
|
}
|
|
svcAccts := make(map[string]madmin.SRSvcAccCreate)
|
|
for user, acc := range serviceAccounts {
|
|
if user == siteReplicatorSvcAcc {
|
|
// skip site-replication service account.
|
|
continue
|
|
}
|
|
claims, err := globalIAMSys.GetClaimsForSvcAcc(ctx, acc.Credentials.AccessKey)
|
|
if err != nil {
|
|
writeErrorResponse(ctx, w, exportError(ctx, err, iamFile, ""), r.URL)
|
|
return
|
|
}
|
|
_, policy, err := globalIAMSys.GetServiceAccount(ctx, acc.Credentials.AccessKey)
|
|
if err != nil {
|
|
writeErrorResponse(ctx, w, exportError(ctx, err, iamFile, ""), r.URL)
|
|
return
|
|
}
|
|
|
|
var policyJSON []byte
|
|
if policy != nil {
|
|
policyJSON, err = json.Marshal(policy)
|
|
if err != nil {
|
|
writeErrorResponse(ctx, w, exportError(ctx, err, iamFile, ""), r.URL)
|
|
return
|
|
}
|
|
}
|
|
svcAccts[user] = madmin.SRSvcAccCreate{
|
|
Parent: acc.Credentials.ParentUser,
|
|
AccessKey: user,
|
|
SecretKey: acc.Credentials.SecretKey,
|
|
Groups: acc.Credentials.Groups,
|
|
Claims: claims,
|
|
SessionPolicy: json.RawMessage(policyJSON),
|
|
Status: acc.Credentials.Status,
|
|
}
|
|
}
|
|
|
|
svcAccData, err := json.Marshal(svcAccts)
|
|
if err != nil {
|
|
writeErrorResponse(ctx, w, exportError(ctx, err, iamFile, ""), r.URL)
|
|
return
|
|
}
|
|
|
|
if err = rawDataFn(bytes.NewReader(svcAccData), iamFile, len(svcAccData)); err != nil {
|
|
writeErrorResponse(ctx, w, exportError(ctx, err, iamFile, ""), r.URL)
|
|
return
|
|
}
|
|
case userPolicyMappingsFile:
|
|
userPolicyMap := make(map[string]MappedPolicy)
|
|
err := globalIAMSys.store.loadMappedPolicies(ctx, regUser, false, userPolicyMap)
|
|
if err != nil {
|
|
writeErrorResponse(ctx, w, exportError(ctx, err, iamFile, ""), r.URL)
|
|
return
|
|
}
|
|
userPolData, err := json.Marshal(userPolicyMap)
|
|
if err != nil {
|
|
writeErrorResponse(ctx, w, exportError(ctx, err, iamFile, ""), r.URL)
|
|
return
|
|
}
|
|
|
|
if err = rawDataFn(bytes.NewReader(userPolData), iamFile, len(userPolData)); err != nil {
|
|
writeErrorResponse(ctx, w, exportError(ctx, err, iamFile, ""), r.URL)
|
|
return
|
|
}
|
|
case groupPolicyMappingsFile:
|
|
groupPolicyMap := make(map[string]MappedPolicy)
|
|
err := globalIAMSys.store.loadMappedPolicies(ctx, regUser, true, groupPolicyMap)
|
|
if err != nil {
|
|
writeErrorResponse(ctx, w, exportError(ctx, err, iamFile, ""), r.URL)
|
|
return
|
|
}
|
|
grpPolData, err := json.Marshal(groupPolicyMap)
|
|
if err != nil {
|
|
writeErrorResponse(ctx, w, exportError(ctx, err, iamFile, ""), r.URL)
|
|
return
|
|
}
|
|
|
|
if err = rawDataFn(bytes.NewReader(grpPolData), iamFile, len(grpPolData)); err != nil {
|
|
writeErrorResponse(ctx, w, exportError(ctx, err, iamFile, ""), r.URL)
|
|
return
|
|
}
|
|
case stsUserPolicyMappingsFile:
|
|
userPolicyMap := make(map[string]MappedPolicy)
|
|
err := globalIAMSys.store.loadMappedPolicies(ctx, stsUser, false, userPolicyMap)
|
|
if err != nil {
|
|
writeErrorResponse(ctx, w, exportError(ctx, err, iamFile, ""), r.URL)
|
|
return
|
|
}
|
|
userPolData, err := json.Marshal(userPolicyMap)
|
|
if err != nil {
|
|
writeErrorResponse(ctx, w, exportError(ctx, err, iamFile, ""), r.URL)
|
|
return
|
|
}
|
|
if err = rawDataFn(bytes.NewReader(userPolData), iamFile, len(userPolData)); err != nil {
|
|
writeErrorResponse(ctx, w, exportError(ctx, err, iamFile, ""), r.URL)
|
|
return
|
|
}
|
|
case stsGroupPolicyMappingsFile:
|
|
groupPolicyMap := make(map[string]MappedPolicy)
|
|
err := globalIAMSys.store.loadMappedPolicies(ctx, stsUser, true, groupPolicyMap)
|
|
if err != nil {
|
|
writeErrorResponse(ctx, w, exportError(ctx, err, iamFile, ""), r.URL)
|
|
return
|
|
}
|
|
grpPolData, err := json.Marshal(groupPolicyMap)
|
|
if err != nil {
|
|
writeErrorResponse(ctx, w, exportError(ctx, err, iamFile, ""), r.URL)
|
|
return
|
|
}
|
|
if err = rawDataFn(bytes.NewReader(grpPolData), iamFile, len(grpPolData)); err != nil {
|
|
writeErrorResponse(ctx, w, exportError(ctx, err, iamFile, ""), r.URL)
|
|
return
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// ImportIAM - imports all IAM info into MinIO
|
|
func (a adminAPIHandlers) ImportIAM(w http.ResponseWriter, r *http.Request) {
|
|
ctx := r.Context()
|
|
|
|
// Get current object layer instance.
|
|
objectAPI := newObjectLayerFn()
|
|
if objectAPI == nil || globalNotificationSys == nil {
|
|
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrServerNotInitialized), r.URL)
|
|
return
|
|
}
|
|
cred, owner, s3Err := validateAdminSignature(ctx, r, "")
|
|
if s3Err != ErrNone {
|
|
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(s3Err), r.URL)
|
|
return
|
|
}
|
|
data, err := io.ReadAll(r.Body)
|
|
if err != nil {
|
|
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrInvalidRequest), r.URL)
|
|
return
|
|
}
|
|
reader := bytes.NewReader(data)
|
|
zr, err := zip.NewReader(reader, int64(len(data)))
|
|
if err != nil {
|
|
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrInvalidRequest), r.URL)
|
|
return
|
|
}
|
|
// import policies first
|
|
{
|
|
|
|
f, err := zr.Open(pathJoin(iamAssetsDir, allPoliciesFile))
|
|
switch {
|
|
case errors.Is(err, os.ErrNotExist):
|
|
case err != nil:
|
|
writeErrorResponseJSON(ctx, w, importErrorWithAPIErr(ctx, ErrInvalidRequest, err, allPoliciesFile, ""), r.URL)
|
|
return
|
|
default:
|
|
defer f.Close()
|
|
var allPolicies map[string]policy.Policy
|
|
data, err = io.ReadAll(f)
|
|
if err != nil {
|
|
writeErrorResponseJSON(ctx, w, importErrorWithAPIErr(ctx, ErrInvalidRequest, err, allPoliciesFile, ""), r.URL)
|
|
return
|
|
}
|
|
err = json.Unmarshal(data, &allPolicies)
|
|
if err != nil {
|
|
writeErrorResponseJSON(ctx, w, importErrorWithAPIErr(ctx, ErrAdminConfigBadJSON, err, allPoliciesFile, ""), r.URL)
|
|
return
|
|
}
|
|
for policyName, policy := range allPolicies {
|
|
if policy.IsEmpty() {
|
|
err = globalIAMSys.DeletePolicy(ctx, policyName, true)
|
|
} else {
|
|
_, err = globalIAMSys.SetPolicy(ctx, policyName, policy)
|
|
}
|
|
if err != nil {
|
|
writeErrorResponseJSON(ctx, w, importError(ctx, err, allPoliciesFile, policyName), r.URL)
|
|
return
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// import users
|
|
{
|
|
f, err := zr.Open(pathJoin(iamAssetsDir, allUsersFile))
|
|
switch {
|
|
case errors.Is(err, os.ErrNotExist):
|
|
case err != nil:
|
|
writeErrorResponseJSON(ctx, w, importErrorWithAPIErr(ctx, ErrInvalidRequest, err, allUsersFile, ""), r.URL)
|
|
return
|
|
default:
|
|
defer f.Close()
|
|
var userAccts map[string]madmin.AddOrUpdateUserReq
|
|
data, err := io.ReadAll(f)
|
|
if err != nil {
|
|
writeErrorResponseJSON(ctx, w, importErrorWithAPIErr(ctx, ErrInvalidRequest, err, allUsersFile, ""), r.URL)
|
|
return
|
|
}
|
|
err = json.Unmarshal(data, &userAccts)
|
|
if err != nil {
|
|
writeErrorResponseJSON(ctx, w, importErrorWithAPIErr(ctx, ErrAdminConfigBadJSON, err, allUsersFile, ""), r.URL)
|
|
return
|
|
}
|
|
for accessKey, ureq := range userAccts {
|
|
// Not allowed to add a user with same access key as root credential
|
|
if accessKey == globalActiveCred.AccessKey {
|
|
writeErrorResponseJSON(ctx, w, importErrorWithAPIErr(ctx, ErrAddUserInvalidArgument, err, allUsersFile, accessKey), r.URL)
|
|
return
|
|
}
|
|
|
|
user, exists := globalIAMSys.GetUser(ctx, accessKey)
|
|
if exists && (user.Credentials.IsTemp() || user.Credentials.IsServiceAccount()) {
|
|
// Updating STS credential is not allowed, and this API does not
|
|
// support updating service accounts.
|
|
writeErrorResponseJSON(ctx, w, importErrorWithAPIErr(ctx, ErrAddUserInvalidArgument, err, allUsersFile, accessKey), r.URL)
|
|
return
|
|
}
|
|
|
|
if (cred.IsTemp() || cred.IsServiceAccount()) && cred.ParentUser == accessKey {
|
|
// Incoming access key matches parent user then we should
|
|
// reject password change requests.
|
|
writeErrorResponseJSON(ctx, w, importErrorWithAPIErr(ctx, ErrAddUserInvalidArgument, err, allUsersFile, accessKey), r.URL)
|
|
return
|
|
}
|
|
|
|
// Check if accessKey has beginning and end space characters, this only applies to new users.
|
|
if !exists && hasSpaceBE(accessKey) {
|
|
writeErrorResponseJSON(ctx, w, importErrorWithAPIErr(ctx, ErrAdminResourceInvalidArgument, err, allUsersFile, accessKey), r.URL)
|
|
return
|
|
}
|
|
|
|
checkDenyOnly := false
|
|
if accessKey == cred.AccessKey {
|
|
// Check that there is no explicit deny - otherwise it's allowed
|
|
// to change one's own password.
|
|
checkDenyOnly = true
|
|
}
|
|
|
|
if !globalIAMSys.IsAllowed(policy.Args{
|
|
AccountName: cred.AccessKey,
|
|
Groups: cred.Groups,
|
|
Action: policy.CreateUserAdminAction,
|
|
ConditionValues: getConditionValues(r, "", cred),
|
|
IsOwner: owner,
|
|
Claims: cred.Claims,
|
|
DenyOnly: checkDenyOnly,
|
|
}) {
|
|
writeErrorResponseJSON(ctx, w, importErrorWithAPIErr(ctx, ErrAccessDenied, err, allUsersFile, accessKey), r.URL)
|
|
return
|
|
}
|
|
if _, err = globalIAMSys.CreateUser(ctx, accessKey, ureq); err != nil {
|
|
writeErrorResponseJSON(ctx, w, importErrorWithAPIErr(ctx, toAdminAPIErrCode(ctx, err), err, allUsersFile, accessKey), r.URL)
|
|
return
|
|
}
|
|
|
|
}
|
|
}
|
|
}
|
|
|
|
// import groups
|
|
{
|
|
f, err := zr.Open(pathJoin(iamAssetsDir, allGroupsFile))
|
|
switch {
|
|
case errors.Is(err, os.ErrNotExist):
|
|
case err != nil:
|
|
writeErrorResponseJSON(ctx, w, importErrorWithAPIErr(ctx, ErrInvalidRequest, err, allGroupsFile, ""), r.URL)
|
|
return
|
|
default:
|
|
defer f.Close()
|
|
var grpInfos map[string]GroupInfo
|
|
data, err := io.ReadAll(f)
|
|
if err != nil {
|
|
writeErrorResponseJSON(ctx, w, importErrorWithAPIErr(ctx, ErrInvalidRequest, err, allGroupsFile, ""), r.URL)
|
|
return
|
|
}
|
|
if err = json.Unmarshal(data, &grpInfos); err != nil {
|
|
writeErrorResponseJSON(ctx, w, importErrorWithAPIErr(ctx, ErrAdminConfigBadJSON, err, allGroupsFile, ""), r.URL)
|
|
return
|
|
}
|
|
for group, grpInfo := range grpInfos {
|
|
// Check if group already exists
|
|
if _, gerr := globalIAMSys.GetGroupDescription(group); gerr != nil {
|
|
// If group does not exist, then check if the group has beginning and end space characters
|
|
// we will reject such group names.
|
|
if errors.Is(gerr, errNoSuchGroup) && hasSpaceBE(group) {
|
|
writeErrorResponseJSON(ctx, w, importErrorWithAPIErr(ctx, ErrAdminResourceInvalidArgument, err, allGroupsFile, group), r.URL)
|
|
return
|
|
}
|
|
}
|
|
if _, gerr := globalIAMSys.AddUsersToGroup(ctx, group, grpInfo.Members); gerr != nil {
|
|
writeErrorResponseJSON(ctx, w, importError(ctx, err, allGroupsFile, group), r.URL)
|
|
return
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// import service accounts
|
|
{
|
|
f, err := zr.Open(pathJoin(iamAssetsDir, allSvcAcctsFile))
|
|
switch {
|
|
case errors.Is(err, os.ErrNotExist):
|
|
case err != nil:
|
|
writeErrorResponseJSON(ctx, w, importErrorWithAPIErr(ctx, ErrInvalidRequest, err, allSvcAcctsFile, ""), r.URL)
|
|
return
|
|
default:
|
|
defer f.Close()
|
|
var serviceAcctReqs map[string]madmin.SRSvcAccCreate
|
|
data, err := io.ReadAll(f)
|
|
if err != nil {
|
|
writeErrorResponseJSON(ctx, w, importErrorWithAPIErr(ctx, ErrInvalidRequest, err, allSvcAcctsFile, ""), r.URL)
|
|
return
|
|
}
|
|
if err = json.Unmarshal(data, &serviceAcctReqs); err != nil {
|
|
writeErrorResponseJSON(ctx, w, importErrorWithAPIErr(ctx, ErrAdminConfigBadJSON, err, allSvcAcctsFile, ""), r.URL)
|
|
return
|
|
}
|
|
for user, svcAcctReq := range serviceAcctReqs {
|
|
var sp *policy.Policy
|
|
var err error
|
|
if len(svcAcctReq.SessionPolicy) > 0 {
|
|
sp, err = policy.ParseConfig(bytes.NewReader(svcAcctReq.SessionPolicy))
|
|
if err != nil {
|
|
writeErrorResponseJSON(ctx, w, importError(ctx, err, allSvcAcctsFile, user), r.URL)
|
|
return
|
|
}
|
|
}
|
|
// service account access key cannot have space characters beginning and end of the string.
|
|
if hasSpaceBE(svcAcctReq.AccessKey) {
|
|
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAdminResourceInvalidArgument), r.URL)
|
|
return
|
|
}
|
|
if !globalIAMSys.IsAllowed(policy.Args{
|
|
AccountName: cred.AccessKey,
|
|
Groups: cred.Groups,
|
|
Action: policy.CreateServiceAccountAdminAction,
|
|
ConditionValues: getConditionValues(r, "", cred),
|
|
IsOwner: owner,
|
|
Claims: cred.Claims,
|
|
}) {
|
|
writeErrorResponseJSON(ctx, w, importErrorWithAPIErr(ctx, ErrAccessDenied, err, allSvcAcctsFile, user), r.URL)
|
|
return
|
|
}
|
|
updateReq := true
|
|
_, _, err = globalIAMSys.GetServiceAccount(ctx, svcAcctReq.AccessKey)
|
|
if err != nil {
|
|
if !errors.Is(err, errNoSuchServiceAccount) {
|
|
writeErrorResponseJSON(ctx, w, importError(ctx, err, allSvcAcctsFile, user), r.URL)
|
|
return
|
|
}
|
|
updateReq = false
|
|
}
|
|
if updateReq {
|
|
opts := updateServiceAccountOpts{
|
|
secretKey: svcAcctReq.SecretKey,
|
|
status: svcAcctReq.Status,
|
|
name: svcAcctReq.Name,
|
|
description: svcAcctReq.Description,
|
|
expiration: svcAcctReq.Expiration,
|
|
sessionPolicy: sp,
|
|
}
|
|
_, err = globalIAMSys.UpdateServiceAccount(ctx, svcAcctReq.AccessKey, opts)
|
|
if err != nil {
|
|
writeErrorResponseJSON(ctx, w, importError(ctx, err, allSvcAcctsFile, user), r.URL)
|
|
return
|
|
}
|
|
continue
|
|
}
|
|
opts := newServiceAccountOpts{
|
|
accessKey: user,
|
|
secretKey: svcAcctReq.SecretKey,
|
|
sessionPolicy: sp,
|
|
claims: svcAcctReq.Claims,
|
|
name: svcAcctReq.Name,
|
|
description: svcAcctReq.Description,
|
|
expiration: svcAcctReq.Expiration,
|
|
allowSiteReplicatorAccount: false,
|
|
}
|
|
|
|
// In case of LDAP we need to resolve the targetUser to a DN and
|
|
// query their groups:
|
|
if globalIAMSys.LDAPConfig.Enabled() {
|
|
opts.claims[ldapUserN] = svcAcctReq.AccessKey // simple username
|
|
targetUser, _, err := globalIAMSys.LDAPConfig.LookupUserDN(svcAcctReq.AccessKey)
|
|
if err != nil {
|
|
writeErrorResponseJSON(ctx, w, importError(ctx, err, allSvcAcctsFile, user), r.URL)
|
|
return
|
|
}
|
|
opts.claims[ldapUser] = targetUser // username DN
|
|
}
|
|
|
|
if _, _, err = globalIAMSys.NewServiceAccount(ctx, svcAcctReq.Parent, svcAcctReq.Groups, opts); err != nil {
|
|
writeErrorResponseJSON(ctx, w, importError(ctx, err, allSvcAcctsFile, user), r.URL)
|
|
return
|
|
}
|
|
|
|
}
|
|
}
|
|
}
|
|
|
|
// import user policy mappings
|
|
{
|
|
f, err := zr.Open(pathJoin(iamAssetsDir, userPolicyMappingsFile))
|
|
switch {
|
|
case errors.Is(err, os.ErrNotExist):
|
|
case err != nil:
|
|
writeErrorResponseJSON(ctx, w, importErrorWithAPIErr(ctx, ErrInvalidRequest, err, userPolicyMappingsFile, ""), r.URL)
|
|
return
|
|
default:
|
|
defer f.Close()
|
|
var userPolicyMap map[string]MappedPolicy
|
|
data, err := io.ReadAll(f)
|
|
if err != nil {
|
|
writeErrorResponseJSON(ctx, w, importErrorWithAPIErr(ctx, ErrInvalidRequest, err, userPolicyMappingsFile, ""), r.URL)
|
|
return
|
|
}
|
|
if err = json.Unmarshal(data, &userPolicyMap); err != nil {
|
|
writeErrorResponseJSON(ctx, w, importErrorWithAPIErr(ctx, ErrAdminConfigBadJSON, err, userPolicyMappingsFile, ""), r.URL)
|
|
return
|
|
}
|
|
for u, pm := range userPolicyMap {
|
|
// disallow setting policy mapping if user is a temporary user
|
|
ok, _, err := globalIAMSys.IsTempUser(u)
|
|
if err != nil && err != errNoSuchUser {
|
|
writeErrorResponseJSON(ctx, w, importError(ctx, err, userPolicyMappingsFile, u), r.URL)
|
|
return
|
|
}
|
|
if ok {
|
|
writeErrorResponseJSON(ctx, w, importError(ctx, errIAMActionNotAllowed, userPolicyMappingsFile, u), r.URL)
|
|
return
|
|
}
|
|
if _, err := globalIAMSys.PolicyDBSet(ctx, u, pm.Policies, regUser, false); err != nil {
|
|
writeErrorResponseJSON(ctx, w, importError(ctx, err, userPolicyMappingsFile, u), r.URL)
|
|
return
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// import group policy mappings
|
|
{
|
|
f, err := zr.Open(pathJoin(iamAssetsDir, groupPolicyMappingsFile))
|
|
switch {
|
|
case errors.Is(err, os.ErrNotExist):
|
|
case err != nil:
|
|
writeErrorResponseJSON(ctx, w, importErrorWithAPIErr(ctx, ErrInvalidRequest, err, groupPolicyMappingsFile, ""), r.URL)
|
|
return
|
|
default:
|
|
defer f.Close()
|
|
var grpPolicyMap map[string]MappedPolicy
|
|
data, err := io.ReadAll(f)
|
|
if err != nil {
|
|
writeErrorResponseJSON(ctx, w, importErrorWithAPIErr(ctx, ErrInvalidRequest, err, groupPolicyMappingsFile, ""), r.URL)
|
|
return
|
|
}
|
|
if err = json.Unmarshal(data, &grpPolicyMap); err != nil {
|
|
writeErrorResponseJSON(ctx, w, importErrorWithAPIErr(ctx, ErrAdminConfigBadJSON, err, groupPolicyMappingsFile, ""), r.URL)
|
|
return
|
|
}
|
|
for g, pm := range grpPolicyMap {
|
|
if _, err := globalIAMSys.PolicyDBSet(ctx, g, pm.Policies, unknownIAMUserType, true); err != nil {
|
|
writeErrorResponseJSON(ctx, w, importError(ctx, err, groupPolicyMappingsFile, g), r.URL)
|
|
return
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// import sts user policy mappings
|
|
{
|
|
f, err := zr.Open(pathJoin(iamAssetsDir, stsUserPolicyMappingsFile))
|
|
switch {
|
|
case errors.Is(err, os.ErrNotExist):
|
|
case err != nil:
|
|
writeErrorResponseJSON(ctx, w, importErrorWithAPIErr(ctx, ErrInvalidRequest, err, stsUserPolicyMappingsFile, ""), r.URL)
|
|
return
|
|
default:
|
|
defer f.Close()
|
|
var userPolicyMap map[string]MappedPolicy
|
|
data, err := io.ReadAll(f)
|
|
if err != nil {
|
|
writeErrorResponseJSON(ctx, w, importErrorWithAPIErr(ctx, ErrInvalidRequest, err, stsUserPolicyMappingsFile, ""), r.URL)
|
|
return
|
|
}
|
|
if err = json.Unmarshal(data, &userPolicyMap); err != nil {
|
|
writeErrorResponseJSON(ctx, w, importErrorWithAPIErr(ctx, ErrAdminConfigBadJSON, err, stsUserPolicyMappingsFile, ""), r.URL)
|
|
return
|
|
}
|
|
for u, pm := range userPolicyMap {
|
|
// disallow setting policy mapping if user is a temporary user
|
|
ok, _, err := globalIAMSys.IsTempUser(u)
|
|
if err != nil && err != errNoSuchUser {
|
|
writeErrorResponseJSON(ctx, w, importError(ctx, err, stsUserPolicyMappingsFile, u), r.URL)
|
|
return
|
|
}
|
|
if ok {
|
|
writeErrorResponseJSON(ctx, w, importError(ctx, errIAMActionNotAllowed, stsUserPolicyMappingsFile, u), r.URL)
|
|
return
|
|
}
|
|
if _, err := globalIAMSys.PolicyDBSet(ctx, u, pm.Policies, stsUser, false); err != nil {
|
|
writeErrorResponseJSON(ctx, w, importError(ctx, err, stsUserPolicyMappingsFile, u), r.URL)
|
|
return
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// import sts group policy mappings
|
|
{
|
|
f, err := zr.Open(pathJoin(iamAssetsDir, stsGroupPolicyMappingsFile))
|
|
switch {
|
|
case errors.Is(err, os.ErrNotExist):
|
|
case err != nil:
|
|
writeErrorResponseJSON(ctx, w, importErrorWithAPIErr(ctx, ErrInvalidRequest, err, stsGroupPolicyMappingsFile, ""), r.URL)
|
|
return
|
|
default:
|
|
defer f.Close()
|
|
var grpPolicyMap map[string]MappedPolicy
|
|
data, err := io.ReadAll(f)
|
|
if err != nil {
|
|
writeErrorResponseJSON(ctx, w, importErrorWithAPIErr(ctx, ErrInvalidRequest, err, stsGroupPolicyMappingsFile, ""), r.URL)
|
|
return
|
|
}
|
|
if err = json.Unmarshal(data, &grpPolicyMap); err != nil {
|
|
writeErrorResponseJSON(ctx, w, importErrorWithAPIErr(ctx, ErrAdminConfigBadJSON, err, stsGroupPolicyMappingsFile, ""), r.URL)
|
|
return
|
|
}
|
|
for g, pm := range grpPolicyMap {
|
|
if _, err := globalIAMSys.PolicyDBSet(ctx, g, pm.Policies, unknownIAMUserType, true); err != nil {
|
|
writeErrorResponseJSON(ctx, w, importError(ctx, err, stsGroupPolicyMappingsFile, g), r.URL)
|
|
return
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|