diff --git a/cmd/admin-handlers-users.go b/cmd/admin-handlers-users.go index a9e651eda..6d3893d7a 100644 --- a/cmd/admin-handlers-users.go +++ b/cmd/admin-handlers-users.go @@ -94,6 +94,42 @@ func (a adminAPIHandlers) RemoveUser(w http.ResponseWriter, r *http.Request) { } } +// ListUsers - GET /minio/admin/v3/list-users?bucket={bucket} +func (a adminAPIHandlers) ListBucketUsers(w http.ResponseWriter, r *http.Request) { + ctx := newContext(r, w, "ListBucketUsers") + + defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r)) + + objectAPI, cred := validateAdminUsersReq(ctx, w, r, iampolicy.ListUsersAdminAction) + if objectAPI == nil { + return + } + + bucket := mux.Vars(r)["bucket"] + + password := cred.SecretKey + + allCredentials, err := globalIAMSys.ListBucketUsers(bucket) + if err != nil { + writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL) + return + } + + data, err := json.Marshal(allCredentials) + if err != nil { + writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL) + return + } + + econfigData, err := madmin.EncryptData(password, data) + if err != nil { + writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL) + return + } + + writeSuccessResponseJSON(w, econfigData) +} + // ListUsers - GET /minio/admin/v3/list-users func (a adminAPIHandlers) ListUsers(w http.ResponseWriter, r *http.Request) { ctx := newContext(r, w, "ListUsers") @@ -1062,33 +1098,6 @@ func (a adminAPIHandlers) AccountInfoHandler(w http.ResponseWriter, r *http.Requ writeSuccessResponseJSON(w, usageInfoJSON) } -// InfoCannedPolicyV2 - GET /minio/admin/v2/info-canned-policy?name={policyName} -func (a adminAPIHandlers) InfoCannedPolicyV2(w http.ResponseWriter, r *http.Request) { - ctx := newContext(r, w, "InfoCannedPolicyV2") - - defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r)) - - objectAPI, _ := validateAdminUsersReq(ctx, w, r, iampolicy.GetPolicyAdminAction) - if objectAPI == nil { - return - } - - policy, err := globalIAMSys.InfoPolicy(mux.Vars(r)["name"]) - if err != nil { - writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL) - return - } - - data, err := json.Marshal(policy) - if err != nil { - writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL) - return - } - - w.Write(data) - w.(http.Flusher).Flush() -} - // InfoCannedPolicy - GET /minio/admin/v3/info-canned-policy?name={policyName} func (a adminAPIHandlers) InfoCannedPolicy(w http.ResponseWriter, r *http.Request) { ctx := newContext(r, w, "InfoCannedPolicy") @@ -1113,9 +1122,9 @@ func (a adminAPIHandlers) InfoCannedPolicy(w http.ResponseWriter, r *http.Reques w.(http.Flusher).Flush() } -// ListCannedPoliciesV2 - GET /minio/admin/v2/list-canned-policies -func (a adminAPIHandlers) ListCannedPoliciesV2(w http.ResponseWriter, r *http.Request) { - ctx := newContext(r, w, "ListCannedPoliciesV2") +// ListBucketPolicies - GET /minio/admin/v3/list-canned-policies?bucket={bucket} +func (a adminAPIHandlers) ListBucketPolicies(w http.ResponseWriter, r *http.Request) { + ctx := newContext(r, w, "ListBucketPolicies") defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r)) @@ -1124,27 +1133,29 @@ func (a adminAPIHandlers) ListCannedPoliciesV2(w http.ResponseWriter, r *http.Re return } - policies, err := globalIAMSys.ListPolicies() + bucket := mux.Vars(r)["bucket"] + policies, err := globalIAMSys.ListPolicies(bucket) if err != nil { writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL) return } - policyMap := make(map[string][]byte, len(policies)) - for k, p := range policies { - var err error - policyMap[k], err = json.Marshal(p) + var newPolicies = make(map[string]iampolicy.Policy) + for name, p := range policies { + _, err = json.Marshal(p) if err != nil { logger.LogIf(ctx, err) continue } + newPolicies[name] = p } - if err = json.NewEncoder(w).Encode(policyMap); err != nil { + if err = json.NewEncoder(w).Encode(newPolicies); err != nil { writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL) return } w.(http.Flusher).Flush() + } // ListCannedPolicies - GET /minio/admin/v3/list-canned-policies @@ -1158,7 +1169,7 @@ func (a adminAPIHandlers) ListCannedPolicies(w http.ResponseWriter, r *http.Requ return } - policies, err := globalIAMSys.ListPolicies() + policies, err := globalIAMSys.ListPolicies("") if err != nil { writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL) return diff --git a/cmd/admin-router.go b/cmd/admin-router.go index 87ab62de0..7ba37444c 100644 --- a/cmd/admin-router.go +++ b/cmd/admin-router.go @@ -25,11 +25,9 @@ import ( ) const ( - adminPathPrefix = minioReservedBucketPath + "/admin" - adminAPIVersionV2 = madmin.AdminAPIVersionV2 - adminAPIVersion = madmin.AdminAPIVersion - adminAPIVersionPrefix = SlashSeparator + adminAPIVersion - adminAPIVersionV2Prefix = SlashSeparator + adminAPIVersionV2 + adminPathPrefix = minioReservedBucketPath + "/admin" + adminAPIVersion = madmin.AdminAPIVersion + adminAPIVersionPrefix = SlashSeparator + adminAPIVersion ) // adminAPIHandlers provides HTTP handlers for MinIO admin API. @@ -46,7 +44,6 @@ func registerAdminRouter(router *mux.Router, enableConfigOps, enableIAMOps bool) adminVersions := []string{ adminAPIVersionPrefix, - adminAPIVersionV2Prefix, } for _, adminVersion := range adminVersions { @@ -127,19 +124,11 @@ func registerAdminRouter(router *mux.Router, enableConfigOps, enableIAMOps bool) adminRouter.Methods(http.MethodGet).Path(adminVersion + "/list-service-accounts").HandlerFunc(httpTraceHdrs(adminAPI.ListServiceAccounts)) adminRouter.Methods(http.MethodDelete).Path(adminVersion+"/delete-service-account").HandlerFunc(httpTraceHdrs(adminAPI.DeleteServiceAccount)).Queries("accessKey", "{accessKey:.*}") - if adminVersion == adminAPIVersionV2Prefix { - // Info policy IAM v2 - adminRouter.Methods(http.MethodGet).Path(adminVersion+"/info-canned-policy").HandlerFunc(httpTraceHdrs(adminAPI.InfoCannedPolicyV2)).Queries("name", "{name:.*}") - - // List policies v2 - adminRouter.Methods(http.MethodGet).Path(adminVersion + "/list-canned-policies").HandlerFunc(httpTraceHdrs(adminAPI.ListCannedPoliciesV2)) - } else { - // Info policy IAM latest - adminRouter.Methods(http.MethodGet).Path(adminVersion+"/info-canned-policy").HandlerFunc(httpTraceHdrs(adminAPI.InfoCannedPolicy)).Queries("name", "{name:.*}") - - // List policies latest - adminRouter.Methods(http.MethodGet).Path(adminVersion + "/list-canned-policies").HandlerFunc(httpTraceHdrs(adminAPI.ListCannedPolicies)) - } + // Info policy IAM latest + adminRouter.Methods(http.MethodGet).Path(adminVersion+"/info-canned-policy").HandlerFunc(httpTraceHdrs(adminAPI.InfoCannedPolicy)).Queries("name", "{name:.*}") + // List policies latest + adminRouter.Methods(http.MethodGet).Path(adminVersion+"/list-canned-policies").HandlerFunc(httpTraceHdrs(adminAPI.ListBucketPolicies)).Queries("bucket", "{bucket:.*}") + adminRouter.Methods(http.MethodGet).Path(adminVersion + "/list-canned-policies").HandlerFunc(httpTraceHdrs(adminAPI.ListCannedPolicies)) // Remove policy IAM adminRouter.Methods(http.MethodDelete).Path(adminVersion+"/remove-canned-policy").HandlerFunc(httpTraceHdrs(adminAPI.RemoveCannedPolicy)).Queries("name", "{name:.*}") @@ -153,11 +142,11 @@ func registerAdminRouter(router *mux.Router, enableConfigOps, enableIAMOps bool) adminRouter.Methods(http.MethodDelete).Path(adminVersion+"/remove-user").HandlerFunc(httpTraceHdrs(adminAPI.RemoveUser)).Queries("accessKey", "{accessKey:.*}") // List users + adminRouter.Methods(http.MethodGet).Path(adminVersion+"/list-users").HandlerFunc(httpTraceHdrs(adminAPI.ListBucketUsers)).Queries("bucket", "{bucket:.*}") adminRouter.Methods(http.MethodGet).Path(adminVersion + "/list-users").HandlerFunc(httpTraceHdrs(adminAPI.ListUsers)) // User info adminRouter.Methods(http.MethodGet).Path(adminVersion+"/user-info").HandlerFunc(httpTraceHdrs(adminAPI.GetUserInfo)).Queries("accessKey", "{accessKey:.*}") - // Add/Remove members from group adminRouter.Methods(http.MethodPut).Path(adminVersion + "/update-group-members").HandlerFunc(httpTraceHdrs(adminAPI.UpdateGroupMembers)) diff --git a/cmd/iam.go b/cmd/iam.go index 6b451489e..816defb0b 100644 --- a/cmd/iam.go +++ b/cmd/iam.go @@ -747,7 +747,7 @@ func (sys *IAMSys) InfoPolicy(policyName string) (iampolicy.Policy, error) { } // ListPolicies - lists all canned policies. -func (sys *IAMSys) ListPolicies() (map[string]iampolicy.Policy, error) { +func (sys *IAMSys) ListPolicies(bucketName string) (map[string]iampolicy.Policy, error) { if !sys.Initialized() { return nil, errServerNotInitialized } @@ -759,7 +759,11 @@ func (sys *IAMSys) ListPolicies() (map[string]iampolicy.Policy, error) { policyDocsMap := make(map[string]iampolicy.Policy, len(sys.iamPolicyDocsMap)) for k, v := range sys.iamPolicyDocsMap { - policyDocsMap[k] = v + if bucketName != "" && v.MatchResource(bucketName) { + policyDocsMap[k] = v + } else { + policyDocsMap[k] = v + } } return policyDocsMap, nil @@ -921,16 +925,60 @@ func (sys *IAMSys) SetTempUser(accessKey string, cred auth.Credentials, policyNa return nil } +// ListBucketUsers - list all users who can access this 'bucket' +func (sys *IAMSys) ListBucketUsers(bucket string) (map[string]madmin.UserInfo, error) { + if bucket == "" { + return nil, errInvalidArgument + } + + sys.store.rlock() + defer sys.store.runlock() + + var users = make(map[string]madmin.UserInfo) + + for k, v := range sys.iamUsersMap { + if v.IsTemp() || v.IsServiceAccount() { + continue + } + var policies []string + mp, ok := sys.iamUserPolicyMap[k] + if ok { + policies = append(policies, mp.toSlice()...) + for _, group := range sys.iamUserGroupMemberships[k].ToSlice() { + if nmp, ok := sys.iamGroupPolicyMap[group]; ok { + policies = append(policies, nmp.toSlice()...) + } + } + } + var matchesPolices []string + for _, p := range policies { + if sys.iamPolicyDocsMap[p].MatchResource(bucket) { + matchesPolices = append(matchesPolices, p) + } + } + if len(matchesPolices) > 0 { + users[k] = madmin.UserInfo{ + PolicyName: strings.Join(matchesPolices, ","), + Status: func() madmin.AccountStatus { + if v.IsValid() { + return madmin.AccountEnabled + } + return madmin.AccountDisabled + }(), + MemberOf: sys.iamUserGroupMemberships[k].ToSlice(), + } + } + } + + return users, nil +} + // ListUsers - list all users. func (sys *IAMSys) ListUsers() (map[string]madmin.UserInfo, error) { if !sys.Initialized() { return nil, errServerNotInitialized } - if sys.usersSysType != MinIOUsersSysType { - return nil, errIAMActionNotAllowed - } - <-sys.configLoaded sys.store.rlock() @@ -948,6 +996,16 @@ func (sys *IAMSys) ListUsers() (map[string]madmin.UserInfo, error) { } return madmin.AccountDisabled }(), + MemberOf: sys.iamUserGroupMemberships[k].ToSlice(), + } + } + } + + if sys.usersSysType == LDAPUsersSysType { + for k, v := range sys.iamUserPolicyMap { + users[k] = madmin.UserInfo{ + PolicyName: v.Policies, + Status: madmin.AccountEnabled, } } } @@ -1013,15 +1071,21 @@ func (sys *IAMSys) GetUserInfo(name string) (u madmin.UserInfo, err error) { sys.store.rlock() // If the user has a mapped policy or is a member of a group, we // return that info. Otherwise we return error. - mappedPolicy, ok1 := sys.iamUserPolicyMap[name] - memberships, ok2 := sys.iamUserGroupMemberships[name] + var groups []string + for _, v := range sys.iamUsersMap { + if v.ParentUser == name { + groups = v.Groups + break + } + } + mappedPolicy, ok := sys.iamUserPolicyMap[name] sys.store.runlock() - if !ok1 && !ok2 { + if !ok { return u, errNoSuchUser } return madmin.UserInfo{ PolicyName: mappedPolicy.Policies, - MemberOf: memberships.ToSlice(), + MemberOf: groups, }, nil } @@ -1741,10 +1805,6 @@ func (sys *IAMSys) ListGroups() (r []string, err error) { return r, errServerNotInitialized } - if sys.usersSysType != MinIOUsersSysType { - return nil, errIAMActionNotAllowed - } - <-sys.configLoaded sys.store.rlock() @@ -1755,6 +1815,12 @@ func (sys *IAMSys) ListGroups() (r []string, err error) { r = append(r, k) } + if sys.usersSysType == LDAPUsersSysType { + for k := range sys.iamGroupPolicyMap { + r = append(r, k) + } + } + return r, nil } diff --git a/pkg/iam/policy/policy.go b/pkg/iam/policy/policy.go index 182e6f5e3..de8b86acb 100644 --- a/pkg/iam/policy/policy.go +++ b/pkg/iam/policy/policy.go @@ -97,6 +97,16 @@ type Policy struct { Statements []Statement `json:"Statement"` } +// MatchResource matches resource with match resource patterns +func (iamp Policy) MatchResource(resource string) bool { + for _, statement := range iamp.Statements { + if statement.Resources.MatchResource(resource) { + return true + } + } + return false +} + // IsAllowed - checks given policy args is allowed to continue the Rest API. func (iamp Policy) IsAllowed(args Args) bool { // Check all deny statements. If any one statement denies, return false. diff --git a/pkg/iam/policy/resource.go b/pkg/iam/policy/resource.go index 44d3d2520..712770164 100644 --- a/pkg/iam/policy/resource.go +++ b/pkg/iam/policy/resource.go @@ -48,7 +48,12 @@ func (r Resource) IsValid() bool { return r.Pattern != "" } -// Match - matches object name with resource pattern. +// MatchResource matches object name with resource pattern only. +func (r Resource) MatchResource(resource string) bool { + return r.Match(resource, nil) +} + +// Match - matches object name with resource pattern, including specific conditionals. func (r Resource) Match(resource string, conditionValues map[string][]string) bool { pattern := r.Pattern for _, key := range condition.CommonKeys { diff --git a/pkg/iam/policy/resourceset.go b/pkg/iam/policy/resourceset.go index cef5649dd..3b16568ca 100644 --- a/pkg/iam/policy/resourceset.go +++ b/pkg/iam/policy/resourceset.go @@ -99,6 +99,16 @@ func (resourceSet ResourceSet) MarshalJSON() ([]byte, error) { return json.Marshal(resources) } +// MatchResource matches object name with resource patterns only. +func (resourceSet ResourceSet) MatchResource(resource string) bool { + for r := range resourceSet { + if r.MatchResource(resource) { + return true + } + } + return false +} + // Match - matches object name with anyone of resource pattern in resource set. func (resourceSet ResourceSet) Match(resource string, conditionValues map[string][]string) bool { for r := range resourceSet {