diff --git a/cmd/admin-handlers-users.go b/cmd/admin-handlers-users.go index 5718dd758..64ba71291 100644 --- a/cmd/admin-handlers-users.go +++ b/cmd/admin-handlers-users.go @@ -129,13 +129,40 @@ func (a adminAPIHandlers) GetUserInfo(w http.ResponseWriter, r *http.Request) { defer logger.AuditLog(w, r, "GetUserInfo", mustGetClaimsFromToken(r)) - objectAPI, _ := validateAdminUsersReq(ctx, w, r, iampolicy.GetUserAdminAction) - if objectAPI == nil { + 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 } - vars := mux.Vars(r) - name := vars["accessKey"] + cred, claims, owner, s3Err := validateAdminSignature(ctx, r, "") + if s3Err != ErrNone { + writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(s3Err), r.URL) + return + } + + accessKey := cred.AccessKey + if cred.ParentUser != "" { + accessKey = cred.ParentUser + } + + implicitPerm := name == accessKey + if !implicitPerm { + if !globalIAMSys.IsAllowed(iampolicy.Args{ + AccountName: accessKey, + Action: iampolicy.GetUserAdminAction, + ConditionValues: getConditionValues(r, "", accessKey, claims), + IsOwner: owner, + Claims: claims, + }) { + writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAccessDenied), r.URL) + return + } + } userInfo, err := globalIAMSys.GetUserInfo(name) if err != nil { @@ -304,7 +331,7 @@ func (a adminAPIHandlers) SetUserStatus(w http.ResponseWriter, r *http.Request) accessKey := vars["accessKey"] status := vars["status"] - // Custom IAM policies not allowed for admin user. + // This API is not allowed to lookup accessKey user status if accessKey == globalActiveCred.AccessKey { writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrInvalidRequest), r.URL) return @@ -330,20 +357,47 @@ func (a adminAPIHandlers) AddUser(w http.ResponseWriter, r *http.Request) { defer logger.AuditLog(w, r, "AddUser", mustGetClaimsFromToken(r)) - objectAPI, cred := validateAdminUsersReq(ctx, w, r, iampolicy.CreateUserAdminAction) - if objectAPI == nil { + 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 } - vars := mux.Vars(r) - accessKey := vars["accessKey"] + cred, claims, owner, s3Err := validateAdminSignature(ctx, r, "") + if s3Err != ErrNone { + writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(s3Err), r.URL) + return + } - // Custom IAM policies not allowed for admin user. - if accessKey == globalActiveCred.AccessKey { + if cred.IsTemp() || cred.IsServiceAccount() { + writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAccountNotEligible), r.URL) + return + } + + // Not allowed to add a user with same access key as root credential + if owner { writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAddUserInvalidArgument), r.URL) return } + implicitPerm := accessKey == cred.AccessKey + if !implicitPerm { + if !globalIAMSys.IsAllowed(iampolicy.Args{ + AccountName: accessKey, + Action: iampolicy.CreateUserAdminAction, + ConditionValues: getConditionValues(r, "", accessKey, claims), + IsOwner: owner, + Claims: claims, + }) { + 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) @@ -398,6 +452,12 @@ func (a adminAPIHandlers) AddServiceAccount(w http.ResponseWriter, r *http.Reque return } + // Disallow creating service accounts by root user. + if owner { + writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAdminAccountNotEligible), r.URL) + return + } + password := cred.SecretKey reqBytes, err := madmin.DecryptData(password, io.LimitReader(r.Body, r.ContentLength)) if err != nil { @@ -411,12 +471,6 @@ func (a adminAPIHandlers) AddServiceAccount(w http.ResponseWriter, r *http.Reque return } - // Disallow creating service accounts by root user. - if owner { - writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAdminAccountNotEligible), r.URL) - return - } - parentUser := cred.AccessKey if cred.ParentUser != "" { parentUser = cred.ParentUser @@ -572,11 +626,11 @@ func (a adminAPIHandlers) DeleteServiceAccount(w http.ResponseWriter, r *http.Re writeSuccessNoContent(w) } -// AccountUsageInfoHandler returns usage -func (a adminAPIHandlers) AccountUsageInfoHandler(w http.ResponseWriter, r *http.Request) { - ctx := newContext(r, w, "AccountUsageInfo") +// AccountInfoHandler returns usage +func (a adminAPIHandlers) AccountInfoHandler(w http.ResponseWriter, r *http.Request) { + ctx := newContext(r, w, "AccountInfo") - defer logger.AuditLog(w, r, "AccountUsageInfo", mustGetClaimsFromToken(r)) + defer logger.AuditLog(w, r, "AccountInfo", mustGetClaimsFromToken(r)) // Get current object layer instance. objectAPI := newObjectLayerFn() @@ -645,8 +699,16 @@ func (a adminAPIHandlers) AccountUsageInfoHandler(w http.ResponseWriter, r *http accountName = cred.ParentUser } - acctInfo := madmin.AccountUsageInfo{ + policies, err := globalIAMSys.PolicyDBGet(accountName, false) + if err != nil { + logger.LogIf(ctx, err) + writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL) + return + } + + acctInfo := madmin.AccountInfo{ AccountName: accountName, + Policy: globalIAMSys.GetCombinedPolicy(policies...), } for _, bucket := range buckets { diff --git a/cmd/admin-router.go b/cmd/admin-router.go index 2051512c4..c82d3752e 100644 --- a/cmd/admin-router.go +++ b/cmd/admin-router.go @@ -116,7 +116,7 @@ func registerAdminRouter(router *mux.Router, enableConfigOps, enableIAMOps bool) // Add user IAM - adminRouter.Methods(http.MethodGet).Path(adminVersion + "/accountusageinfo").HandlerFunc(httpTraceAll(adminAPI.AccountUsageInfoHandler)) + adminRouter.Methods(http.MethodGet).Path(adminVersion + "/accountinfo").HandlerFunc(httpTraceAll(adminAPI.AccountInfoHandler)) adminRouter.Methods(http.MethodPut).Path(adminVersion+"/add-user").HandlerFunc(httpTraceHdrs(adminAPI.AddUser)).Queries("accessKey", "{accessKey:.*}") diff --git a/cmd/api-errors.go b/cmd/api-errors.go index 4ce7ab530..0441abbea 100644 --- a/cmd/api-errors.go +++ b/cmd/api-errors.go @@ -363,6 +363,7 @@ const ( ErrInvalidDecompressedSize ErrAddUserInvalidArgument ErrAdminAccountNotEligible + ErrAccountNotEligible ErrServiceAccountNotFound ErrPostPolicyConditionInvalidFormat ) @@ -1726,12 +1727,17 @@ var errorCodes = errorCodeMap{ ErrAddUserInvalidArgument: { Code: "XMinioInvalidIAMCredentials", Description: "User is not allowed to be same as admin access key", - HTTPStatusCode: http.StatusConflict, + HTTPStatusCode: http.StatusForbidden, }, ErrAdminAccountNotEligible: { Code: "XMinioInvalidIAMCredentials", Description: "The administrator key is not eligible for this operation", - HTTPStatusCode: http.StatusConflict, + HTTPStatusCode: http.StatusForbidden, + }, + ErrAccountNotEligible: { + Code: "XMinioInvalidIAMCredentials", + Description: "The account key is not eligible for this operation", + HTTPStatusCode: http.StatusForbidden, }, ErrServiceAccountNotFound: { Code: "XMinioInvalidIAMCredentials", diff --git a/cmd/iam.go b/cmd/iam.go index 99d05b869..e46315716 100644 --- a/cmd/iam.go +++ b/cmd/iam.go @@ -1827,6 +1827,33 @@ func (sys *IAMSys) IsAllowedSTS(args iampolicy.Args) bool { return combinedPolicy.IsAllowed(args) } +// GetCombinedPolicy returns a combined policy combining all policies +func (sys *IAMSys) GetCombinedPolicy(policies ...string) iampolicy.Policy { + // Policies were found, evaluate all of them. + sys.store.rlock() + defer sys.store.runlock() + + var availablePolicies []iampolicy.Policy + for _, pname := range policies { + p, found := sys.iamPolicyDocsMap[pname] + if found { + availablePolicies = append(availablePolicies, p) + } + } + + if len(availablePolicies) == 0 { + return iampolicy.Policy{} + } + + combinedPolicy := availablePolicies[0] + for i := 1; i < len(availablePolicies); i++ { + combinedPolicy.Statements = append(combinedPolicy.Statements, + availablePolicies[i].Statements...) + } + + return combinedPolicy +} + // IsAllowed - checks given policy args is allowed to continue the Rest API. func (sys *IAMSys) IsAllowed(args iampolicy.Args) bool { // If opa is configured, use OPA always. @@ -1873,25 +1900,7 @@ func (sys *IAMSys) IsAllowed(args iampolicy.Args) bool { } // Policies were found, evaluate all of them. - sys.store.rlock() - defer sys.store.runlock() - - var availablePolicies []iampolicy.Policy - for _, pname := range policies { - p, found := sys.iamPolicyDocsMap[pname] - if found { - availablePolicies = append(availablePolicies, p) - } - } - if len(availablePolicies) == 0 { - return false - } - combinedPolicy := availablePolicies[0] - for i := 1; i < len(availablePolicies); i++ { - combinedPolicy.Statements = append(combinedPolicy.Statements, - availablePolicies[i].Statements...) - } - return combinedPolicy.IsAllowed(args) + return sys.GetCombinedPolicy(policies...).IsAllowed(args) } // Set default canned policies only if not already overridden by users. diff --git a/pkg/madmin/README.md b/pkg/madmin/README.md index b45cf07c8..00cbfff0c 100644 --- a/pkg/madmin/README.md +++ b/pkg/madmin/README.md @@ -47,7 +47,7 @@ func main() { |:------------------------------------|:-----------------------------------------|:-------------------|:--------------------------| | [`ServiceTrace`](#ServiceTrace) | [`ServerInfo`](#ServerInfo) | [`Heal`](#Heal) | [`GetConfig`](#GetConfig) | | [`ServiceStop`](#ServiceStop) | [`StorageInfo`](#StorageInfo) | | [`SetConfig`](#SetConfig) | -| [`ServiceRestart`](#ServiceRestart) | [`AccountUsageInfo`](#AccountUsageInfo) | | | +| [`ServiceRestart`](#ServiceRestart) | [`AccountInfo`](#AccountInfo) | | | @@ -251,16 +251,16 @@ __Example__ ``` - + -### AccountUsageInfo(ctx context.Context) (AccountUsageInfo, error) +### AccountInfo(ctx context.Context) (AccountInfo, error) Fetches accounting usage information for the current authenticated user | Param | Type | Description | |--------------------------------|----------------------|-------------------------| -| `AccountUsageInfo.AccountName` | _string_ | Account name. | -| `AccountUsageInfo.Buckets` | _[]BucketUsageInfo_ | Bucket usage info. | +| `AccountInfo.AccountName` | _string_ | Account name. | +| `AccountInfo.Buckets` | _[]BucketUsageInfo_ | Bucket usage info. | | Param | Type | Description | @@ -281,12 +281,12 @@ __Example__ ```go - accountUsageInfo, err := madmClnt.AccountUsageInfo(context.Background()) + accountInfo, err := madmClnt.AccountInfo(context.Background()) if err != nil { log.Fatalln(err) } - log.Println(accountUsageInfo) + log.Println(accountInfo) ``` diff --git a/pkg/madmin/examples/accounting-usage-info.go b/pkg/madmin/examples/accounting-info.go similarity index 91% rename from pkg/madmin/examples/accounting-usage-info.go rename to pkg/madmin/examples/accounting-info.go index 969f6effa..d3e5b8235 100644 --- a/pkg/madmin/examples/accounting-usage-info.go +++ b/pkg/madmin/examples/accounting-info.go @@ -37,10 +37,10 @@ func main() { log.Fatalln(err) } - accountUsageInfo, err := madmClnt.AccountUsageInfo(context.Background()) + accountInfo, err := madmClnt.AccountInfo(context.Background()) if err != nil { log.Fatalln(err) } - log.Println(accountUsageInfo) + log.Println(accountInfo) } diff --git a/pkg/madmin/user-commands.go b/pkg/madmin/user-commands.go index a5b9f0fb0..adc5796a8 100644 --- a/pkg/madmin/user-commands.go +++ b/pkg/madmin/user-commands.go @@ -44,37 +44,38 @@ type BucketUsageInfo struct { Access AccountAccess `json:"access"` } -// AccountUsageInfo represents the account usage info of an +// AccountInfo represents the account usage info of an // account across buckets. -type AccountUsageInfo struct { +type AccountInfo struct { AccountName string + Policy iampolicy.Policy Buckets []BucketUsageInfo } -// AccountUsageInfo returns the usage info for the authenticating account. -func (adm *AdminClient) AccountUsageInfo(ctx context.Context) (AccountUsageInfo, error) { - resp, err := adm.executeMethod(ctx, http.MethodGet, requestData{relPath: adminAPIPrefix + "/accountusageinfo"}) +// AccountInfo returns the usage info for the authenticating account. +func (adm *AdminClient) AccountInfo(ctx context.Context) (AccountInfo, error) { + resp, err := adm.executeMethod(ctx, http.MethodGet, requestData{relPath: adminAPIPrefix + "/accountinfo"}) defer closeResponse(resp) if err != nil { - return AccountUsageInfo{}, err + return AccountInfo{}, err } // Check response http status code if resp.StatusCode != http.StatusOK { - return AccountUsageInfo{}, httpRespToErrorResponse(resp) + return AccountInfo{}, httpRespToErrorResponse(resp) } // Unmarshal the server's json response - var accountInfo AccountUsageInfo + var accountInfo AccountInfo respBytes, err := ioutil.ReadAll(resp.Body) if err != nil { - return AccountUsageInfo{}, err + return AccountInfo{}, err } err = json.Unmarshal(respBytes, &accountInfo) if err != nil { - return AccountUsageInfo{}, err + return AccountInfo{}, err } return accountInfo, nil