From ed37b7a9d53f267bca01a1eeae2ea3b18aff8ba2 Mon Sep 17 00:00:00 2001 From: Taran Pelkey <84724408+taran-p@users.noreply.github.com> Date: Mon, 19 Dec 2022 13:37:03 -0500 Subject: [PATCH] Add API to fetch policy user/group associations (#16239) --- cmd/admin-handlers-users.go | 42 +++++++++++ cmd/admin-router.go | 3 + cmd/iam-store.go | 139 ++++++++++++++++++++++++++++++++++++ cmd/iam.go | 16 +++++ 4 files changed, 200 insertions(+) diff --git a/cmd/admin-handlers-users.go b/cmd/admin-handlers-users.go index 330900ef4..e8759009a 100644 --- a/cmd/admin-handlers-users.go +++ b/cmd/admin-handlers-users.go @@ -1667,6 +1667,48 @@ func (a adminAPIHandlers) SetPolicyForUserOrGroup(w http.ResponseWriter, r *http } } +// 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 := newContext(r, w, "ListPolicyMappingEntities") + + defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r)) + + // Check authorization. + objectAPI, cred := validateAdminReq(ctx, w, r, + iampolicy.ListGroupsAdminAction, iampolicy.ListUsersAdminAction, iampolicy.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) +} + // AttachPolicyBuiltin - POST /minio/admin/v3/idp/builtin/attach func (a adminAPIHandlers) AttachPolicyBuiltin(w http.ResponseWriter, r *http.Request) { ctx := newContext(r, w, "AttachPolicyBuiltin") diff --git a/cmd/admin-router.go b/cmd/admin-router.go index 3ba96fcdd..08a29f3d7 100644 --- a/cmd/admin-router.go +++ b/cmd/admin-router.go @@ -151,6 +151,9 @@ func registerAdminRouter(router *mux.Router, enableConfigOps bool) { adminRouter.Methods(http.MethodGet).Path(adminVersion+"/list-canned-policies").HandlerFunc(gz(httpTraceHdrs(adminAPI.ListBucketPolicies))).Queries("bucket", "{bucket:.*}") adminRouter.Methods(http.MethodGet).Path(adminVersion + "/list-canned-policies").HandlerFunc(gz(httpTraceHdrs(adminAPI.ListCannedPolicies))) + // Builtin IAM policy associations + adminRouter.Methods(http.MethodGet).Path(adminVersion + "/idp/builtin/policy-entities").HandlerFunc(gz(httpTraceHdrs(adminAPI.ListPolicyMappingEntities))) + // Remove policy IAM adminRouter.Methods(http.MethodDelete).Path(adminVersion+"/remove-canned-policy").HandlerFunc(gz(httpTraceHdrs(adminAPI.RemoveCannedPolicy))).Queries("name", "{name:.*}") diff --git a/cmd/iam-store.go b/cmd/iam-store.go index 9a2084ab3..0c12dace7 100644 --- a/cmd/iam-store.go +++ b/cmd/iam-store.go @@ -1887,6 +1887,145 @@ func (store *IAMStoreSys) GetAllParentUsers() map[string]ParentUserInfo { return res } +// Assumes store is locked by caller. If users is empty, returns all user mappings. +func (store *IAMStoreSys) listUserPolicyMappings(cache *iamCache, users []string) []madmin.UserPolicyEntities { + var r []madmin.UserPolicyEntities + usersSet := set.CreateStringSet(users...) + for user, mappedPolicy := range cache.iamUserPolicyMap { + if !usersSet.IsEmpty() && !usersSet.Contains(user) { + continue + } + + ps := mappedPolicy.toSlice() + sort.Strings(ps) + r = append(r, madmin.UserPolicyEntities{ + User: user, + Policies: ps, + }) + } + + sort.Slice(r, func(i, j int) bool { + return r[i].User < r[j].User + }) + + return r +} + +// Assumes store is locked by caller. If groups is empty, returns all group mappings. +func (store *IAMStoreSys) listGroupPolicyMappings(cache *iamCache, groups []string) []madmin.GroupPolicyEntities { + var r []madmin.GroupPolicyEntities + groupsSet := set.CreateStringSet(groups...) + for group, mappedPolicy := range cache.iamGroupPolicyMap { + if !groupsSet.IsEmpty() && !groupsSet.Contains(group) { + continue + } + + ps := mappedPolicy.toSlice() + sort.Strings(ps) + r = append(r, madmin.GroupPolicyEntities{ + Group: group, + Policies: ps, + }) + } + + sort.Slice(r, func(i, j int) bool { + return r[i].Group < r[j].Group + }) + + return r +} + +// Assumes store is locked by caller. If policies is empty, returns all policy mappings. +func (store *IAMStoreSys) listPolicyMappings(cache *iamCache, policies []string) []madmin.PolicyEntities { + queryPolSet := set.CreateStringSet(policies...) + + policyToUsersMap := make(map[string]set.StringSet) + for user, mappedPolicy := range cache.iamUserPolicyMap { + commonPolicySet := mappedPolicy.policySet() + if !queryPolSet.IsEmpty() { + commonPolicySet = commonPolicySet.Intersection(queryPolSet) + } + for _, policy := range commonPolicySet.ToSlice() { + s, ok := policyToUsersMap[policy] + if !ok { + policyToUsersMap[policy] = set.CreateStringSet(user) + } else { + s.Add(user) + policyToUsersMap[policy] = s + } + } + } + + policyToGroupsMap := make(map[string]set.StringSet) + for group, mappedPolicy := range cache.iamGroupPolicyMap { + commonPolicySet := mappedPolicy.policySet() + if !queryPolSet.IsEmpty() { + commonPolicySet = commonPolicySet.Intersection(queryPolSet) + } + for _, policy := range commonPolicySet.ToSlice() { + s, ok := policyToGroupsMap[policy] + if !ok { + policyToGroupsMap[policy] = set.CreateStringSet(group) + } else { + s.Add(group) + policyToGroupsMap[policy] = s + } + } + } + + m := make(map[string]madmin.PolicyEntities, len(policyToGroupsMap)) + for policy, groups := range policyToGroupsMap { + s := groups.ToSlice() + sort.Strings(s) + m[policy] = madmin.PolicyEntities{ + Policy: policy, + Groups: s, + } + } + for policy, users := range policyToUsersMap { + s := users.ToSlice() + sort.Strings(s) + + // Update existing value in map + pe := m[policy] + pe.Policy = policy + pe.Users = s + m[policy] = pe + } + + policyEntities := make([]madmin.PolicyEntities, 0, len(m)) + for _, v := range m { + policyEntities = append(policyEntities, v) + } + + sort.Slice(policyEntities, func(i, j int) bool { + return policyEntities[i].Policy < policyEntities[j].Policy + }) + + return policyEntities +} + +// ListPolicyMappings - return builtin users/groups mapped to policies. +func (store *IAMStoreSys) ListPolicyMappings(q madmin.PolicyEntitiesQuery) madmin.PolicyEntitiesResult { + cache := store.rlock() + defer store.runlock() + + var result madmin.PolicyEntitiesResult + + isAllPoliciesQuery := len(q.Users) == 0 && len(q.Groups) == 0 && len(q.Policy) == 0 + + if len(q.Users) > 0 { + result.UserMappings = store.listUserPolicyMappings(cache, q.Users) + } + if len(q.Groups) > 0 { + result.GroupMappings = store.listGroupPolicyMappings(cache, q.Groups) + } + if len(q.Policy) > 0 || isAllPoliciesQuery { + result.PolicyMappings = store.listPolicyMappings(cache, q.Policy) + } + return result +} + // SetUserStatus - sets current user status. func (store *IAMStoreSys) SetUserStatus(ctx context.Context, accessKey string, status madmin.AccountStatus) (updatedAt time.Time, err error) { if accessKey != "" && status != madmin.AccountEnabled && status != madmin.AccountDisabled { diff --git a/cmd/iam.go b/cmd/iam.go index 3581f37a2..8e7142a97 100644 --- a/cmd/iam.go +++ b/cmd/iam.go @@ -856,6 +856,22 @@ func (sys *IAMSys) GetUserInfo(ctx context.Context, name string) (u madmin.UserI return sys.store.GetUserInfo(name) } +// QueryPolicyEntities - queries policy associations for builtin users/groups/policies. +func (sys *IAMSys) QueryPolicyEntities(ctx context.Context, q madmin.PolicyEntitiesQuery) (*madmin.PolicyEntitiesResult, error) { + if !sys.Initialized() { + return nil, errServerNotInitialized + } + + select { + case <-sys.configLoaded: + pe := sys.store.ListPolicyMappings(q) + pe.Timestamp = UTCNow() + return &pe, nil + case <-ctx.Done(): + return nil, ctx.Err() + } +} + // GetUserPolicies - get policies attached to a user. func (sys *IAMSys) GetUserPolicies(name string) (p []string, err error) { if !sys.Initialized() {