mirror of
https://github.com/minio/minio.git
synced 2024-12-26 23:25:54 -05:00
7ff4164d65
Fix races in IAM cache
Fixes #19344
On the top level we only grab a read lock, but we write to the cache if we manage to fetch it.
a03dac41eb/cmd/iam-store.go (L446)
is also flipped to what it should be AFAICT.
Change the internal cache structure to a concurrency safe implementation.
Bonus: Also switch grid implementation.
2517 lines
77 KiB
Go
2517 lines
77 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"
|
|
"strconv"
|
|
"time"
|
|
|
|
"github.com/klauspost/compress/zip"
|
|
"github.com/minio/madmin-go/v3"
|
|
"github.com/minio/minio/internal/auth"
|
|
"github.com/minio/minio/internal/cachevalue"
|
|
"github.com/minio/minio/internal/config/dns"
|
|
"github.com/minio/minio/internal/logger"
|
|
"github.com/minio/mux"
|
|
"github.com/minio/pkg/v2/policy"
|
|
"github.com/puzpuzpuz/xsync/v3"
|
|
)
|
|
|
|
// 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
|
|
}
|
|
|
|
args := policy.Args{
|
|
AccountName: cred.AccessKey,
|
|
Groups: cred.Groups,
|
|
Action: policy.ListTemporaryAccountsAdminAction,
|
|
ConditionValues: getConditionValues(r, "", cred),
|
|
IsOwner: owner,
|
|
Claims: cred.Claims,
|
|
}
|
|
|
|
if !globalIAMSys.IsAllowed(args) {
|
|
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAccessDenied), r.URL)
|
|
return
|
|
}
|
|
|
|
stsAccount, sessionPolicy, err := globalIAMSys.GetTemporaryAccount(ctx, accessKey)
|
|
if err != nil {
|
|
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
|
return
|
|
}
|
|
|
|
var stsAccountPolicy policy.Policy
|
|
|
|
if sessionPolicy != nil {
|
|
stsAccountPolicy = *sessionPolicy
|
|
} else {
|
|
policiesNames, err := globalIAMSys.PolicyDBGet(stsAccount.ParentUser, stsAccount.Groups...)
|
|
if err != nil {
|
|
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
|
return
|
|
}
|
|
if len(policiesNames) == 0 {
|
|
policySet, _ := args.GetPolicies(iamPolicyClaimNameOpenID())
|
|
policiesNames = policySet.ToSlice()
|
|
}
|
|
|
|
stsAccountPolicy = globalIAMSys.GetCombinedPolicy(policiesNames...)
|
|
}
|
|
|
|
policyJSON, err := json.MarshalIndent(stsAccountPolicy, "", " ")
|
|
if err != nil {
|
|
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
|
return
|
|
}
|
|
|
|
infoResp := madmin.TemporaryAccountInfoResp{
|
|
ParentUser: stsAccount.ParentUser,
|
|
AccountStatus: stsAccount.Status,
|
|
ImpliedPolicy: sessionPolicy == nil,
|
|
Policy: string(policyJSON),
|
|
Expiration: &stsAccount.Expiration,
|
|
}
|
|
|
|
data, err := json.Marshal(infoResp)
|
|
if err != nil {
|
|
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
|
return
|
|
}
|
|
|
|
encryptedData, err := madmin.EncryptData(cred.SecretKey, data)
|
|
if err != nil {
|
|
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
|
return
|
|
}
|
|
|
|
writeSuccessResponseJSON(w, encryptedData)
|
|
}
|
|
|
|
// AddServiceAccount - PUT /minio/admin/v3/add-service-account
|
|
func (a adminAPIHandlers) AddServiceAccount(w http.ResponseWriter, r *http.Request) {
|
|
ctx, cred, opts, createReq, targetUser, APIError := commonAddServiceAccount(r)
|
|
if APIError.Code != "" {
|
|
writeErrorResponseJSON(ctx, w, APIError, r.URL)
|
|
return
|
|
}
|
|
|
|
if createReq.AccessKey == globalActiveCred.AccessKey {
|
|
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAddUserInvalidArgument), r.URL)
|
|
return
|
|
}
|
|
|
|
var (
|
|
targetGroups []string
|
|
err error
|
|
)
|
|
|
|
// Find the user for the request sender (as it may be sent via a service
|
|
// account or STS account):
|
|
requestorUser := cred.AccessKey
|
|
requestorParentUser := cred.AccessKey
|
|
requestorGroups := cred.Groups
|
|
requestorIsDerivedCredential := false
|
|
if cred.IsServiceAccount() || cred.IsTemp() {
|
|
requestorParentUser = cred.ParentUser
|
|
requestorIsDerivedCredential = true
|
|
}
|
|
|
|
if globalIAMSys.GetUsersSysType() == MinIOUsersSysType && targetUser != cred.AccessKey {
|
|
// For internal IDP, ensure that the targetUser's parent account exists.
|
|
// It could be a regular user account or the root account.
|
|
_, isRegularUser := globalIAMSys.GetUser(ctx, targetUser)
|
|
if !isRegularUser && targetUser != globalActiveCred.AccessKey {
|
|
apiErr := toAdminAPIErr(ctx, errNoSuchUser)
|
|
apiErr.Description = fmt.Sprintf("Specified target user %s does not exist", targetUser)
|
|
writeErrorResponseJSON(ctx, w, apiErr, r.URL)
|
|
return
|
|
}
|
|
}
|
|
|
|
// Check if we are creating svc account for request sender.
|
|
isSvcAccForRequestor := false
|
|
if targetUser == requestorUser || targetUser == requestorParentUser {
|
|
isSvcAccForRequestor = true
|
|
}
|
|
|
|
// If we are creating svc account for request sender, ensure
|
|
// that targetUser is a real user (i.e. not derived
|
|
// credentials).
|
|
if isSvcAccForRequestor {
|
|
if requestorIsDerivedCredential {
|
|
if requestorParentUser == "" {
|
|
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx,
|
|
errors.New("service accounts cannot be generated for temporary credentials without parent")), r.URL)
|
|
return
|
|
}
|
|
targetUser = requestorParentUser
|
|
}
|
|
targetGroups = requestorGroups
|
|
|
|
// In case of LDAP/OIDC we need to set `opts.claims` to ensure
|
|
// it is associated with the LDAP/OIDC user properly.
|
|
for k, v := range cred.Claims {
|
|
if k == expClaim {
|
|
continue
|
|
}
|
|
opts.claims[k] = v
|
|
}
|
|
} else if globalIAMSys.LDAPConfig.Enabled() {
|
|
// In case of LDAP we need to resolve the targetUser to a DN and
|
|
// query their groups:
|
|
opts.claims[ldapUserN] = targetUser // simple username
|
|
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.
|
|
}
|
|
|
|
newCred, updatedAt, err := globalIAMSys.NewServiceAccount(ctx, targetUser, targetGroups, opts)
|
|
if err != nil {
|
|
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
|
return
|
|
}
|
|
|
|
createResp := madmin.AddServiceAccountResp{
|
|
Credentials: madmin.Credentials{
|
|
AccessKey: newCred.AccessKey,
|
|
SecretKey: newCred.SecretKey,
|
|
Expiration: newCred.Expiration,
|
|
},
|
|
}
|
|
|
|
data, err := json.Marshal(createResp)
|
|
if err != nil {
|
|
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
|
return
|
|
}
|
|
|
|
encryptedData, err := madmin.EncryptData(cred.SecretKey, data)
|
|
if err != nil {
|
|
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
|
return
|
|
}
|
|
|
|
writeSuccessResponseJSON(w, encryptedData)
|
|
|
|
// Call hook for cluster-replication if the service account is not for a
|
|
// root user.
|
|
if newCred.ParentUser != globalActiveCred.AccessKey {
|
|
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
|
|
}
|
|
|
|
password := cred.SecretKey
|
|
reqBytes, err := madmin.DecryptData(password, io.LimitReader(r.Body, r.ContentLength))
|
|
if err != nil {
|
|
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErrWithErr(ErrAdminConfigBadJSON, err), r.URL)
|
|
return
|
|
}
|
|
|
|
var updateReq madmin.UpdateServiceAccountReq
|
|
if err = json.Unmarshal(reqBytes, &updateReq); err != nil {
|
|
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErrWithErr(ErrAdminConfigBadJSON, err), r.URL)
|
|
return
|
|
}
|
|
|
|
if err := updateReq.Validate(); err != nil {
|
|
// Since this validation would happen client side as well, we only send
|
|
// a generic error message here.
|
|
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAdminResourceInvalidArgument), r.URL)
|
|
return
|
|
}
|
|
|
|
condValues := getConditionValues(r, "", cred)
|
|
addExpirationToCondValues(updateReq.NewExpiration, condValues)
|
|
|
|
// Permission checks:
|
|
//
|
|
// 1. Any type of account (i.e. access keys (previously/still called service
|
|
// accounts), STS accounts, internal IDP accounts, etc) with the
|
|
// policy.UpdateServiceAccountAdminAction permission can update any service
|
|
// account.
|
|
//
|
|
// 2. We would like to let a user update their own access keys, however it
|
|
// is currently blocked pending a re-design. Users are still able to delete
|
|
// and re-create them.
|
|
if !globalIAMSys.IsAllowed(policy.Args{
|
|
AccountName: cred.AccessKey,
|
|
Groups: cred.Groups,
|
|
Action: policy.UpdateServiceAccountAdminAction,
|
|
ConditionValues: condValues,
|
|
IsOwner: owner,
|
|
Claims: cred.Claims,
|
|
}) {
|
|
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAccessDenied), r.URL)
|
|
return
|
|
}
|
|
|
|
var sp *policy.Policy
|
|
if len(updateReq.NewPolicy) > 0 {
|
|
sp, err = policy.ParseConfig(bytes.NewReader(updateReq.NewPolicy))
|
|
if err != nil {
|
|
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
|
return
|
|
}
|
|
if sp.Version == "" && len(sp.Statements) == 0 {
|
|
sp = nil
|
|
}
|
|
}
|
|
opts := updateServiceAccountOpts{
|
|
secretKey: updateReq.NewSecretKey,
|
|
status: updateReq.NewStatus,
|
|
name: updateReq.NewName,
|
|
description: updateReq.NewDescription,
|
|
expiration: updateReq.NewExpiration,
|
|
sessionPolicy: sp,
|
|
}
|
|
updatedAt, err := globalIAMSys.UpdateServiceAccount(ctx, accessKey, opts)
|
|
if err != nil {
|
|
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
|
return
|
|
}
|
|
|
|
// Call site replication hook - non-root user accounts are replicated.
|
|
if svcAccount.ParentUser != globalActiveCred.AccessKey {
|
|
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
|
|
}
|
|
}
|
|
|
|
// if session policy is nil or empty, then it is implied policy
|
|
impliedPolicy := sessionPolicy == nil || (sessionPolicy.Version == "" && len(sessionPolicy.Statements) == 0)
|
|
|
|
var svcAccountPolicy policy.Policy
|
|
|
|
if !impliedPolicy {
|
|
svcAccountPolicy = *sessionPolicy
|
|
} else {
|
|
policiesNames, err := globalIAMSys.PolicyDBGet(svcAccount.ParentUser, svcAccount.Groups...)
|
|
if err != nil {
|
|
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
|
return
|
|
}
|
|
svcAccountPolicy = globalIAMSys.GetCombinedPolicy(policiesNames...)
|
|
}
|
|
|
|
policyJSON, err := json.MarshalIndent(svcAccountPolicy, "", " ")
|
|
if err != nil {
|
|
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
|
return
|
|
}
|
|
|
|
var expiration *time.Time
|
|
if !svcAccount.Expiration.IsZero() && !svcAccount.Expiration.Equal(timeSentinel) {
|
|
expiration = &svcAccount.Expiration
|
|
}
|
|
|
|
infoResp := madmin.InfoServiceAccountResp{
|
|
ParentUser: svcAccount.ParentUser,
|
|
Name: svcAccount.Name,
|
|
Description: svcAccount.Description,
|
|
AccountStatus: svcAccount.Status,
|
|
ImpliedPolicy: impliedPolicy,
|
|
Policy: string(policyJSON),
|
|
Expiration: expiration,
|
|
}
|
|
|
|
data, err := json.Marshal(infoResp)
|
|
if err != nil {
|
|
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
|
return
|
|
}
|
|
|
|
encryptedData, err := madmin.EncryptData(cred.SecretKey, data)
|
|
if err != nil {
|
|
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
|
return
|
|
}
|
|
|
|
writeSuccessResponseJSON(w, encryptedData)
|
|
}
|
|
|
|
// ListServiceAccounts - GET /minio/admin/v3/list-service-accounts
|
|
func (a adminAPIHandlers) ListServiceAccounts(w http.ResponseWriter, r *http.Request) {
|
|
ctx := r.Context()
|
|
|
|
// Get current object layer instance.
|
|
objectAPI := newObjectLayerFn()
|
|
if objectAPI == nil || globalNotificationSys == nil {
|
|
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrServerNotInitialized), r.URL)
|
|
return
|
|
}
|
|
|
|
cred, owner, s3Err := validateAdminSignature(ctx, r, "")
|
|
if s3Err != ErrNone {
|
|
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(s3Err), r.URL)
|
|
return
|
|
}
|
|
|
|
var targetAccount string
|
|
|
|
// If listing is requested for a specific user (who is not the request
|
|
// sender), check that the user has permissions.
|
|
user := r.Form.Get("user")
|
|
if user != "" && user != cred.AccessKey {
|
|
if !globalIAMSys.IsAllowed(policy.Args{
|
|
AccountName: cred.AccessKey,
|
|
Groups: cred.Groups,
|
|
Action: policy.ListServiceAccountsAdminAction,
|
|
ConditionValues: getConditionValues(r, "", cred),
|
|
IsOwner: owner,
|
|
Claims: cred.Claims,
|
|
}) {
|
|
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAccessDenied), r.URL)
|
|
return
|
|
}
|
|
targetAccount = user
|
|
} else {
|
|
targetAccount = cred.AccessKey
|
|
if cred.ParentUser != "" {
|
|
targetAccount = cred.ParentUser
|
|
}
|
|
}
|
|
|
|
serviceAccounts, err := globalIAMSys.ListServiceAccounts(ctx, targetAccount)
|
|
if err != nil {
|
|
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
|
return
|
|
}
|
|
|
|
var serviceAccountList []madmin.ServiceAccountInfo
|
|
|
|
for _, svc := range serviceAccounts {
|
|
expiryTime := svc.Expiration
|
|
serviceAccountList = append(serviceAccountList, madmin.ServiceAccountInfo{
|
|
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
|
|
}
|
|
|
|
if serviceAccount == siteReplicatorSvcAcc && globalSiteReplicationSys.isEnabled() {
|
|
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrInvalidArgument), r.URL)
|
|
return
|
|
}
|
|
// We do not care if service account is readable or not at this point,
|
|
// since this is a delete call we shall allow it to be deleted if possible.
|
|
svcAccount, _, err := globalIAMSys.GetServiceAccount(ctx, serviceAccount)
|
|
if errors.Is(err, errNoSuchServiceAccount) {
|
|
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAdminServiceAccountNotFound), r.URL)
|
|
return
|
|
}
|
|
|
|
adminPrivilege := globalIAMSys.IsAllowed(policy.Args{
|
|
AccountName: cred.AccessKey,
|
|
Groups: cred.Groups,
|
|
Action: policy.RemoveServiceAccountAdminAction,
|
|
ConditionValues: getConditionValues(r, "", cred),
|
|
IsOwner: owner,
|
|
Claims: cred.Claims,
|
|
})
|
|
|
|
if !adminPrivilege {
|
|
parentUser := cred.AccessKey
|
|
if cred.ParentUser != "" {
|
|
parentUser = cred.ParentUser
|
|
}
|
|
if svcAccount.ParentUser != "" && parentUser != svcAccount.ParentUser {
|
|
// The service account belongs to another user but return not
|
|
// found error to mitigate brute force attacks. or the
|
|
// serviceAccount doesn't exist.
|
|
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAdminServiceAccountNotFound), r.URL)
|
|
return
|
|
}
|
|
}
|
|
|
|
if err := globalIAMSys.DeleteServiceAccount(ctx, serviceAccount, true); err != nil {
|
|
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
|
return
|
|
}
|
|
|
|
// Call site replication hook - non-root user accounts are replicated.
|
|
if svcAccount.ParentUser != "" && svcAccount.ParentUser != globalActiveCred.AccessKey {
|
|
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.InitOnce(10*time.Second,
|
|
cachevalue.Opts{ReturnLastGood: true, NoWait: true},
|
|
func() (DataUsageInfo, error) {
|
|
ctx, done := context.WithTimeout(context.Background(), 2*time.Second)
|
|
defer done()
|
|
|
|
return loadDataUsageFromBackend(ctx, objectAPI)
|
|
},
|
|
)
|
|
|
|
dataUsageInfo, _ := bucketStorageCache.Get()
|
|
|
|
// 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, 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 := xsync.NewMapOf[string, MappedPolicy]()
|
|
err := globalIAMSys.store.loadMappedPolicies(ctx, regUser, false, userPolicyMap)
|
|
if err != nil {
|
|
writeErrorResponse(ctx, w, exportError(ctx, err, iamFile, ""), r.URL)
|
|
return
|
|
}
|
|
userPolData, err := json.Marshal(mappedPoliciesToMap(userPolicyMap))
|
|
if err != nil {
|
|
writeErrorResponse(ctx, w, exportError(ctx, err, iamFile, ""), r.URL)
|
|
return
|
|
}
|
|
|
|
if err = rawDataFn(bytes.NewReader(userPolData), iamFile, len(userPolData)); err != nil {
|
|
writeErrorResponse(ctx, w, exportError(ctx, err, iamFile, ""), r.URL)
|
|
return
|
|
}
|
|
case groupPolicyMappingsFile:
|
|
groupPolicyMap := xsync.NewMapOf[string, MappedPolicy]()
|
|
err := globalIAMSys.store.loadMappedPolicies(ctx, regUser, true, groupPolicyMap)
|
|
if err != nil {
|
|
writeErrorResponse(ctx, w, exportError(ctx, err, iamFile, ""), r.URL)
|
|
return
|
|
}
|
|
grpPolData, err := json.Marshal(mappedPoliciesToMap(groupPolicyMap))
|
|
if err != nil {
|
|
writeErrorResponse(ctx, w, exportError(ctx, err, iamFile, ""), r.URL)
|
|
return
|
|
}
|
|
|
|
if err = rawDataFn(bytes.NewReader(grpPolData), iamFile, len(grpPolData)); err != nil {
|
|
writeErrorResponse(ctx, w, exportError(ctx, err, iamFile, ""), r.URL)
|
|
return
|
|
}
|
|
case stsUserPolicyMappingsFile:
|
|
userPolicyMap := xsync.NewMapOf[string, MappedPolicy]()
|
|
err := globalIAMSys.store.loadMappedPolicies(ctx, stsUser, false, userPolicyMap)
|
|
if err != nil {
|
|
writeErrorResponse(ctx, w, exportError(ctx, err, iamFile, ""), r.URL)
|
|
return
|
|
}
|
|
userPolData, err := json.Marshal(mappedPoliciesToMap(userPolicyMap))
|
|
if err != nil {
|
|
writeErrorResponse(ctx, w, exportError(ctx, err, iamFile, ""), r.URL)
|
|
return
|
|
}
|
|
if err = rawDataFn(bytes.NewReader(userPolData), iamFile, len(userPolData)); err != nil {
|
|
writeErrorResponse(ctx, w, exportError(ctx, err, iamFile, ""), r.URL)
|
|
return
|
|
}
|
|
case stsGroupPolicyMappingsFile:
|
|
groupPolicyMap := xsync.NewMapOf[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(mappedPoliciesToMap(groupPolicyMap))
|
|
if err != nil {
|
|
writeErrorResponse(ctx, w, exportError(ctx, err, iamFile, ""), r.URL)
|
|
return
|
|
}
|
|
if err = rawDataFn(bytes.NewReader(grpPolData), iamFile, len(grpPolData)); err != nil {
|
|
writeErrorResponse(ctx, w, exportError(ctx, err, iamFile, ""), r.URL)
|
|
return
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// 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
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func addExpirationToCondValues(exp *time.Time, condValues map[string][]string) {
|
|
if exp == nil {
|
|
return
|
|
}
|
|
condValues["DurationSeconds"] = []string{strconv.FormatInt(int64(exp.Sub(time.Now()).Seconds()), 10)}
|
|
}
|
|
|
|
func commonAddServiceAccount(r *http.Request) (context.Context, auth.Credentials, newServiceAccountOpts, madmin.AddServiceAccountReq, string, APIError) {
|
|
ctx := r.Context()
|
|
|
|
// Get current object layer instance.
|
|
objectAPI := newObjectLayerFn()
|
|
if objectAPI == nil || globalNotificationSys == nil {
|
|
return ctx, auth.Credentials{}, newServiceAccountOpts{}, madmin.AddServiceAccountReq{}, "", errorCodes.ToAPIErr(ErrServerNotInitialized)
|
|
}
|
|
|
|
cred, owner, s3Err := validateAdminSignature(ctx, r, "")
|
|
if s3Err != ErrNone {
|
|
return ctx, auth.Credentials{}, newServiceAccountOpts{}, madmin.AddServiceAccountReq{}, "", errorCodes.ToAPIErr(s3Err)
|
|
}
|
|
|
|
password := cred.SecretKey
|
|
reqBytes, err := madmin.DecryptData(password, io.LimitReader(r.Body, r.ContentLength))
|
|
if err != nil {
|
|
return ctx, auth.Credentials{}, newServiceAccountOpts{}, madmin.AddServiceAccountReq{}, "", errorCodes.ToAPIErrWithErr(ErrAdminConfigBadJSON, err)
|
|
}
|
|
|
|
var createReq madmin.AddServiceAccountReq
|
|
if err = json.Unmarshal(reqBytes, &createReq); err != nil {
|
|
return ctx, auth.Credentials{}, newServiceAccountOpts{}, madmin.AddServiceAccountReq{}, "", errorCodes.ToAPIErrWithErr(ErrAdminConfigBadJSON, err)
|
|
}
|
|
|
|
// service account access key cannot have space characters beginning and end of the string.
|
|
if hasSpaceBE(createReq.AccessKey) {
|
|
return ctx, auth.Credentials{}, newServiceAccountOpts{}, madmin.AddServiceAccountReq{}, "", errorCodes.ToAPIErr(ErrAdminResourceInvalidArgument)
|
|
}
|
|
|
|
if err := createReq.Validate(); err != nil {
|
|
// Since this validation would happen client side as well, we only send
|
|
// a generic error message here.
|
|
return ctx, auth.Credentials{}, newServiceAccountOpts{}, madmin.AddServiceAccountReq{}, "", errorCodes.ToAPIErr(ErrAdminResourceInvalidArgument)
|
|
}
|
|
// If the request did not set a TargetUser, the service account is
|
|
// created for the request sender.
|
|
targetUser := createReq.TargetUser
|
|
if targetUser == "" {
|
|
targetUser = cred.AccessKey
|
|
}
|
|
|
|
description := createReq.Description
|
|
if description == "" {
|
|
description = createReq.Comment
|
|
}
|
|
opts := newServiceAccountOpts{
|
|
accessKey: createReq.AccessKey,
|
|
secretKey: createReq.SecretKey,
|
|
name: createReq.Name,
|
|
description: description,
|
|
expiration: createReq.Expiration,
|
|
claims: make(map[string]interface{}),
|
|
}
|
|
|
|
condValues := getConditionValues(r, "", cred)
|
|
addExpirationToCondValues(createReq.Expiration, condValues)
|
|
|
|
// Check if action is allowed if creating access key for another user
|
|
// Check if action is explicitly denied if for self
|
|
if !globalIAMSys.IsAllowed(policy.Args{
|
|
AccountName: cred.AccessKey,
|
|
Groups: cred.Groups,
|
|
Action: policy.CreateServiceAccountAdminAction,
|
|
ConditionValues: condValues,
|
|
IsOwner: owner,
|
|
Claims: cred.Claims,
|
|
DenyOnly: (targetUser == cred.AccessKey || targetUser == cred.ParentUser),
|
|
}) {
|
|
return ctx, auth.Credentials{}, newServiceAccountOpts{}, madmin.AddServiceAccountReq{}, "", errorCodes.ToAPIErr(ErrAccessDenied)
|
|
}
|
|
|
|
var sp *policy.Policy
|
|
if len(createReq.Policy) > 0 {
|
|
sp, err = policy.ParseConfig(bytes.NewReader(createReq.Policy))
|
|
if err != nil {
|
|
return ctx, auth.Credentials{}, newServiceAccountOpts{}, madmin.AddServiceAccountReq{}, "", toAdminAPIErr(ctx, err)
|
|
}
|
|
}
|
|
|
|
opts.sessionPolicy = sp
|
|
|
|
return ctx, cred, opts, createReq, targetUser, APIError{}
|
|
}
|