fix: allow accountInfo, addUser and getUserInfo implicit (#10978)

- accountInfo API that returns information about
  user, access to buckets and the size per bucket
- addUser - user is allowed to change their secretKey
- getUserInfo - returns user info if the incoming
  is the same user requesting their information
master
Harshavardhana 4 years ago committed by GitHub
parent 350c5ff8f8
commit e6fa410778
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 106
      cmd/admin-handlers-users.go
  2. 2
      cmd/admin-router.go
  3. 10
      cmd/api-errors.go
  4. 47
      cmd/iam.go
  5. 14
      pkg/madmin/README.md
  6. 4
      pkg/madmin/examples/accounting-info.go
  7. 21
      pkg/madmin/user-commands.go

@ -129,13 +129,40 @@ func (a adminAPIHandlers) GetUserInfo(w http.ResponseWriter, r *http.Request) {
defer logger.AuditLog(w, r, "GetUserInfo", mustGetClaimsFromToken(r)) defer logger.AuditLog(w, r, "GetUserInfo", mustGetClaimsFromToken(r))
objectAPI, _ := validateAdminUsersReq(ctx, w, r, iampolicy.GetUserAdminAction) vars := mux.Vars(r)
if objectAPI == nil { name := vars["accessKey"]
// Get current object layer instance.
objectAPI := newObjectLayerFn()
if objectAPI == nil || globalNotificationSys == nil {
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrServerNotInitialized), r.URL)
return return
} }
vars := mux.Vars(r) cred, claims, owner, s3Err := validateAdminSignature(ctx, r, "")
name := vars["accessKey"] 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) userInfo, err := globalIAMSys.GetUserInfo(name)
if err != nil { if err != nil {
@ -304,7 +331,7 @@ func (a adminAPIHandlers) SetUserStatus(w http.ResponseWriter, r *http.Request)
accessKey := vars["accessKey"] accessKey := vars["accessKey"]
status := vars["status"] 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 { if accessKey == globalActiveCred.AccessKey {
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrInvalidRequest), r.URL) writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrInvalidRequest), r.URL)
return return
@ -330,20 +357,47 @@ func (a adminAPIHandlers) AddUser(w http.ResponseWriter, r *http.Request) {
defer logger.AuditLog(w, r, "AddUser", mustGetClaimsFromToken(r)) defer logger.AuditLog(w, r, "AddUser", mustGetClaimsFromToken(r))
objectAPI, cred := validateAdminUsersReq(ctx, w, r, iampolicy.CreateUserAdminAction) vars := mux.Vars(r)
if objectAPI == nil { accessKey := vars["accessKey"]
// Get current object layer instance.
objectAPI := newObjectLayerFn()
if objectAPI == nil || globalNotificationSys == nil {
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrServerNotInitialized), r.URL)
return return
} }
vars := mux.Vars(r) cred, claims, owner, s3Err := validateAdminSignature(ctx, r, "")
accessKey := vars["accessKey"] if s3Err != ErrNone {
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(s3Err), r.URL)
return
}
// Custom IAM policies not allowed for admin user. if cred.IsTemp() || cred.IsServiceAccount() {
if accessKey == globalActiveCred.AccessKey { 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) writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAddUserInvalidArgument), r.URL)
return 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 { if r.ContentLength > maxEConfigJSONSize || r.ContentLength == -1 {
// More than maxConfigSize bytes were available // More than maxConfigSize bytes were available
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAdminConfigTooLarge), r.URL) writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAdminConfigTooLarge), r.URL)
@ -398,6 +452,12 @@ func (a adminAPIHandlers) AddServiceAccount(w http.ResponseWriter, r *http.Reque
return return
} }
// Disallow creating service accounts by root user.
if owner {
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAdminAccountNotEligible), r.URL)
return
}
password := cred.SecretKey password := cred.SecretKey
reqBytes, err := madmin.DecryptData(password, io.LimitReader(r.Body, r.ContentLength)) reqBytes, err := madmin.DecryptData(password, io.LimitReader(r.Body, r.ContentLength))
if err != nil { if err != nil {
@ -411,12 +471,6 @@ func (a adminAPIHandlers) AddServiceAccount(w http.ResponseWriter, r *http.Reque
return return
} }
// Disallow creating service accounts by root user.
if owner {
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAdminAccountNotEligible), r.URL)
return
}
parentUser := cred.AccessKey parentUser := cred.AccessKey
if cred.ParentUser != "" { if cred.ParentUser != "" {
parentUser = cred.ParentUser parentUser = cred.ParentUser
@ -572,11 +626,11 @@ func (a adminAPIHandlers) DeleteServiceAccount(w http.ResponseWriter, r *http.Re
writeSuccessNoContent(w) writeSuccessNoContent(w)
} }
// AccountUsageInfoHandler returns usage // AccountInfoHandler returns usage
func (a adminAPIHandlers) AccountUsageInfoHandler(w http.ResponseWriter, r *http.Request) { func (a adminAPIHandlers) AccountInfoHandler(w http.ResponseWriter, r *http.Request) {
ctx := newContext(r, w, "AccountUsageInfo") 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. // Get current object layer instance.
objectAPI := newObjectLayerFn() objectAPI := newObjectLayerFn()
@ -645,8 +699,16 @@ func (a adminAPIHandlers) AccountUsageInfoHandler(w http.ResponseWriter, r *http
accountName = cred.ParentUser 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, AccountName: accountName,
Policy: globalIAMSys.GetCombinedPolicy(policies...),
} }
for _, bucket := range buckets { for _, bucket := range buckets {

@ -116,7 +116,7 @@ func registerAdminRouter(router *mux.Router, enableConfigOps, enableIAMOps bool)
// Add user IAM // 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:.*}") adminRouter.Methods(http.MethodPut).Path(adminVersion+"/add-user").HandlerFunc(httpTraceHdrs(adminAPI.AddUser)).Queries("accessKey", "{accessKey:.*}")

@ -363,6 +363,7 @@ const (
ErrInvalidDecompressedSize ErrInvalidDecompressedSize
ErrAddUserInvalidArgument ErrAddUserInvalidArgument
ErrAdminAccountNotEligible ErrAdminAccountNotEligible
ErrAccountNotEligible
ErrServiceAccountNotFound ErrServiceAccountNotFound
ErrPostPolicyConditionInvalidFormat ErrPostPolicyConditionInvalidFormat
) )
@ -1726,12 +1727,17 @@ var errorCodes = errorCodeMap{
ErrAddUserInvalidArgument: { ErrAddUserInvalidArgument: {
Code: "XMinioInvalidIAMCredentials", Code: "XMinioInvalidIAMCredentials",
Description: "User is not allowed to be same as admin access key", Description: "User is not allowed to be same as admin access key",
HTTPStatusCode: http.StatusConflict, HTTPStatusCode: http.StatusForbidden,
}, },
ErrAdminAccountNotEligible: { ErrAdminAccountNotEligible: {
Code: "XMinioInvalidIAMCredentials", Code: "XMinioInvalidIAMCredentials",
Description: "The administrator key is not eligible for this operation", 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: { ErrServiceAccountNotFound: {
Code: "XMinioInvalidIAMCredentials", Code: "XMinioInvalidIAMCredentials",

@ -1827,6 +1827,33 @@ func (sys *IAMSys) IsAllowedSTS(args iampolicy.Args) bool {
return combinedPolicy.IsAllowed(args) 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. // IsAllowed - checks given policy args is allowed to continue the Rest API.
func (sys *IAMSys) IsAllowed(args iampolicy.Args) bool { func (sys *IAMSys) IsAllowed(args iampolicy.Args) bool {
// If opa is configured, use OPA always. // 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. // Policies were found, evaluate all of them.
sys.store.rlock() return sys.GetCombinedPolicy(policies...).IsAllowed(args)
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)
} }
// Set default canned policies only if not already overridden by users. // Set default canned policies only if not already overridden by users.

@ -47,7 +47,7 @@ func main() {
|:------------------------------------|:-----------------------------------------|:-------------------|:--------------------------| |:------------------------------------|:-----------------------------------------|:-------------------|:--------------------------|
| [`ServiceTrace`](#ServiceTrace) | [`ServerInfo`](#ServerInfo) | [`Heal`](#Heal) | [`GetConfig`](#GetConfig) | | [`ServiceTrace`](#ServiceTrace) | [`ServerInfo`](#ServerInfo) | [`Heal`](#Heal) | [`GetConfig`](#GetConfig) |
| [`ServiceStop`](#ServiceStop) | [`StorageInfo`](#StorageInfo) | | [`SetConfig`](#SetConfig) | | [`ServiceStop`](#ServiceStop) | [`StorageInfo`](#StorageInfo) | | [`SetConfig`](#SetConfig) |
| [`ServiceRestart`](#ServiceRestart) | [`AccountUsageInfo`](#AccountUsageInfo) | | | | [`ServiceRestart`](#ServiceRestart) | [`AccountInfo`](#AccountInfo) | | |
@ -251,16 +251,16 @@ __Example__
``` ```
<a name="AccountUsageInfo"></a> <a name="AccountInfo"></a>
### AccountUsageInfo(ctx context.Context) (AccountUsageInfo, error) ### AccountInfo(ctx context.Context) (AccountInfo, error)
Fetches accounting usage information for the current authenticated user Fetches accounting usage information for the current authenticated user
| Param | Type | Description | | Param | Type | Description |
|--------------------------------|----------------------|-------------------------| |--------------------------------|----------------------|-------------------------|
| `AccountUsageInfo.AccountName` | _string_ | Account name. | | `AccountInfo.AccountName` | _string_ | Account name. |
| `AccountUsageInfo.Buckets` | _[]BucketUsageInfo_ | Bucket usage info. | | `AccountInfo.Buckets` | _[]BucketUsageInfo_ | Bucket usage info. |
| Param | Type | Description | | Param | Type | Description |
@ -281,12 +281,12 @@ __Example__
```go ```go
accountUsageInfo, err := madmClnt.AccountUsageInfo(context.Background()) accountInfo, err := madmClnt.AccountInfo(context.Background())
if err != nil { if err != nil {
log.Fatalln(err) log.Fatalln(err)
} }
log.Println(accountUsageInfo) log.Println(accountInfo)
``` ```

@ -37,10 +37,10 @@ func main() {
log.Fatalln(err) log.Fatalln(err)
} }
accountUsageInfo, err := madmClnt.AccountUsageInfo(context.Background()) accountInfo, err := madmClnt.AccountInfo(context.Background())
if err != nil { if err != nil {
log.Fatalln(err) log.Fatalln(err)
} }
log.Println(accountUsageInfo) log.Println(accountInfo)
} }

@ -44,37 +44,38 @@ type BucketUsageInfo struct {
Access AccountAccess `json:"access"` Access AccountAccess `json:"access"`
} }
// AccountUsageInfo represents the account usage info of an // AccountInfo represents the account usage info of an
// account across buckets. // account across buckets.
type AccountUsageInfo struct { type AccountInfo struct {
AccountName string AccountName string
Policy iampolicy.Policy
Buckets []BucketUsageInfo Buckets []BucketUsageInfo
} }
// AccountUsageInfo returns the usage info for the authenticating account. // AccountInfo returns the usage info for the authenticating account.
func (adm *AdminClient) AccountUsageInfo(ctx context.Context) (AccountUsageInfo, error) { func (adm *AdminClient) AccountInfo(ctx context.Context) (AccountInfo, error) {
resp, err := adm.executeMethod(ctx, http.MethodGet, requestData{relPath: adminAPIPrefix + "/accountusageinfo"}) resp, err := adm.executeMethod(ctx, http.MethodGet, requestData{relPath: adminAPIPrefix + "/accountinfo"})
defer closeResponse(resp) defer closeResponse(resp)
if err != nil { if err != nil {
return AccountUsageInfo{}, err return AccountInfo{}, err
} }
// Check response http status code // Check response http status code
if resp.StatusCode != http.StatusOK { if resp.StatusCode != http.StatusOK {
return AccountUsageInfo{}, httpRespToErrorResponse(resp) return AccountInfo{}, httpRespToErrorResponse(resp)
} }
// Unmarshal the server's json response // Unmarshal the server's json response
var accountInfo AccountUsageInfo var accountInfo AccountInfo
respBytes, err := ioutil.ReadAll(resp.Body) respBytes, err := ioutil.ReadAll(resp.Body)
if err != nil { if err != nil {
return AccountUsageInfo{}, err return AccountInfo{}, err
} }
err = json.Unmarshal(respBytes, &accountInfo) err = json.Unmarshal(respBytes, &accountInfo)
if err != nil { if err != nil {
return AccountUsageInfo{}, err return AccountInfo{}, err
} }
return accountInfo, nil return accountInfo, nil

Loading…
Cancel
Save