From 76d822bf1e0c7e47cc1952edb6d3c8b64593863f Mon Sep 17 00:00:00 2001 From: Aditya Manthramurthy Date: Mon, 7 Nov 2022 14:35:09 -0800 Subject: [PATCH] Add LDAP policy entities API (#15908) --- cmd/admin-handlers-idp-ldap.go | 88 ++++++++++++++ cmd/admin-router.go | 3 + cmd/iam-store.go | 164 ++++++++++++++++++++++++++ cmd/iam.go | 20 ++++ go.mod | 3 +- go.sum | 6 +- internal/config/identity/ldap/ldap.go | 10 ++ 7 files changed, 288 insertions(+), 6 deletions(-) create mode 100644 cmd/admin-handlers-idp-ldap.go diff --git a/cmd/admin-handlers-idp-ldap.go b/cmd/admin-handlers-idp-ldap.go new file mode 100644 index 000000000..c39b9c50a --- /dev/null +++ b/cmd/admin-handlers-idp-ldap.go @@ -0,0 +1,88 @@ +// Copyright (c) 2015-2022 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 . + +package cmd + +import ( + "encoding/json" + "net/http" + + "github.com/minio/madmin-go" + "github.com/minio/minio/internal/logger" + iampolicy "github.com/minio/pkg/iam/policy" +) + +// ListLDAPPolicyMappingEntities lists users/groups mapped to given/all policies. +// +// GET /idp/ldap/policy-entities?[query-params] +// +// Query params: +// +// user=... -> repeatable query parameter, specifying users to query for +// policy mapping +// +// group=... -> repeatable query parameter, specifying groups to query for +// policy mapping +// +// policy=... -> repeatable query parameter, specifying policy to query for +// user/group mapping +// +// When all query parameters are omitted, returns mappings for all policies. +func (a adminAPIHandlers) ListLDAPPolicyMappingEntities(w http.ResponseWriter, r *http.Request) { + ctx := newContext(r, w, "ListLDAPPolicyMappingEntities") + + 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.QueryLDAPPolicyEntities(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) +} diff --git a/cmd/admin-router.go b/cmd/admin-router.go index d09fed1d8..74fc4ad02 100644 --- a/cmd/admin-router.go +++ b/cmd/admin-router.go @@ -190,6 +190,9 @@ func registerAdminRouter(router *mux.Router, enableConfigOps bool) { adminRouter.Methods(http.MethodGet).Path(adminVersion + "/idp-config/{type}/{name}").HandlerFunc(gz(httpTraceHdrs(adminAPI.GetIdentityProviderCfg))) adminRouter.Methods(http.MethodDelete).Path(adminVersion + "/idp-config/{type}/{name}").HandlerFunc(gz(httpTraceHdrs(adminAPI.DeleteIdentityProviderCfg))) + // LDAP IAM operations + adminRouter.Methods(http.MethodGet).Path(adminVersion + "/idp/ldap/policy-entities").HandlerFunc(gz(httpTraceHdrs(adminAPI.ListLDAPPolicyMappingEntities))) + // -- END IAM APIs -- // GetBucketQuotaConfig diff --git a/cmd/iam-store.go b/cmd/iam-store.go index e9b55ad84..4b0b77591 100644 --- a/cmd/iam-store.go +++ b/cmd/iam-store.go @@ -23,6 +23,7 @@ import ( "encoding/json" "errors" "fmt" + "sort" "strings" "time" @@ -1252,6 +1253,169 @@ func (store *IAMStoreSys) GetUsers() map[string]madmin.UserInfo { return result } +// Assumes store is locked by caller. If users is empty, returns all user mappings. +func (store *IAMStoreSys) listLDAPUserPolicyMappings(cache *iamCache, users []string, + isLDAPUserDN func(string) bool, +) []madmin.UserPolicyEntities { + var r []madmin.UserPolicyEntities + usersSet := set.CreateStringSet(users...) + for user, mappedPolicy := range cache.iamUserPolicyMap { + if !isLDAPUserDN(user) { + continue + } + + 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) listLDAPGroupPolicyMappings(cache *iamCache, groups []string, + isLDAPGroupDN func(string) bool, +) []madmin.GroupPolicyEntities { + var r []madmin.GroupPolicyEntities + groupsSet := set.CreateStringSet(groups...) + for group, mappedPolicy := range cache.iamGroupPolicyMap { + if !isLDAPGroupDN(group) { + continue + } + + 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) listLDAPPolicyMappings(cache *iamCache, policy []string, + isLDAPUserDN, isLDAPGroupDN func(string) bool, +) []madmin.PolicyEntities { + queryPolSet := set.CreateStringSet(policy...) + + policyToUsersMap := make(map[string]set.StringSet) + for user, mappedPolicy := range cache.iamUserPolicyMap { + if !isLDAPUserDN(user) { + continue + } + + 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 { + if !isLDAPGroupDN(group) { + continue + } + + commonPolicySet := mappedPolicy.policySet() + if !queryPolSet.IsEmpty() { + commonPolicySet = commonPolicySet.Intersection(queryPolSet) + } + for _, policy := range commonPolicySet.ToSlice() { + s, ok := policyToUsersMap[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 +} + +// ListLDAPPolicyMappings - return LDAP users/groups mapped to policies. +func (store *IAMStoreSys) ListLDAPPolicyMappings(q madmin.PolicyEntitiesQuery, + isLDAPUserDN, isLDAPGroupDN func(string) bool, +) 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.listLDAPUserPolicyMappings(cache, q.Users, isLDAPUserDN) + } + if len(q.Groups) > 0 { + result.GroupMappings = store.listLDAPGroupPolicyMappings(cache, q.Groups, isLDAPGroupDN) + } + if len(q.Policy) > 0 || isAllPoliciesQuery { + result.PolicyMappings = store.listLDAPPolicyMappings(cache, q.Policy, isLDAPUserDN, isLDAPGroupDN) + } + return result +} + // GetUsersWithMappedPolicies - safely returns the name of access keys with associated policies func (store *IAMStoreSys) GetUsersWithMappedPolicies() map[string]string { cache := store.rlock() diff --git a/cmd/iam.go b/cmd/iam.go index ddba60d69..71bec5f3b 100644 --- a/cmd/iam.go +++ b/cmd/iam.go @@ -786,6 +786,26 @@ func (sys *IAMSys) ListLDAPUsers(ctx context.Context) (map[string]madmin.UserInf } } +// QueryLDAPPolicyEntities - queries policy associations for LDAP users/groups/policies. +func (sys *IAMSys) QueryLDAPPolicyEntities(ctx context.Context, q madmin.PolicyEntitiesQuery) (*madmin.PolicyEntitiesResult, error) { + if !sys.Initialized() { + return nil, errServerNotInitialized + } + + if sys.usersSysType != LDAPUsersSysType { + return nil, errIAMActionNotAllowed + } + + select { + case <-sys.configLoaded: + pe := sys.store.ListLDAPPolicyMappings(q, sys.ldapConfig.IsLDAPUserDN, sys.ldapConfig.IsLDAPGroupDN) + pe.Timestamp = UTCNow() + return &pe, nil + case <-ctx.Done(): + return nil, ctx.Err() + } +} + // IsTempUser - returns if given key is a temporary user. func (sys *IAMSys) IsTempUser(name string) (bool, string, error) { if !sys.Initialized() { diff --git a/go.mod b/go.mod index b64dad7f4..ca200befb 100644 --- a/go.mod +++ b/go.mod @@ -48,7 +48,7 @@ require ( github.com/minio/dperf v0.4.2 github.com/minio/highwayhash v1.0.2 github.com/minio/kes v0.21.1 - github.com/minio/madmin-go v1.7.3 + github.com/minio/madmin-go v1.7.4 github.com/minio/minio-go/v7 v7.0.43-0.20221021202758-c6319beb6b27 github.com/minio/pkg v1.5.4 github.com/minio/selfupdate v0.5.0 @@ -73,7 +73,6 @@ require ( github.com/rs/cors v1.8.2 github.com/rs/dnscache v0.0.0-20211102005908-e0241e321417 github.com/secure-io/sio-go v0.3.1 - github.com/shirou/gopsutil v3.21.11+incompatible github.com/shirou/gopsutil/v3 v3.22.9 github.com/streadway/amqp v1.0.0 github.com/tinylib/msgp v1.1.7-0.20220719154719-f3635b96e483 diff --git a/go.sum b/go.sum index 116a01def..4acacbf8a 100644 --- a/go.sum +++ b/go.sum @@ -757,8 +757,8 @@ github.com/minio/highwayhash v1.0.2/go.mod h1:BQskDq+xkJ12lmlUUi7U0M5Swg3EWR+dLT github.com/minio/kes v0.21.1 h1:Af+CsnuvnOA9mGBAf05VY8ebf4vDfLDDu3uCO0VrKJU= github.com/minio/kes v0.21.1/go.mod h1:3FW1BQkMGQW78yhy+69tUq5bdcf5rnXJizyeKB9a/tc= github.com/minio/madmin-go v1.6.6/go.mod h1:ATvkBOLiP3av4D++2v1UEHC/QzsGtgXD5kYvvRYzdKs= -github.com/minio/madmin-go v1.7.3 h1:ayR0rHXBQXsdkcd74t0mIqt9Tp0Rzy1cZ5K9gYBoFm8= -github.com/minio/madmin-go v1.7.3/go.mod h1:3SO8SROxHN++tF6QxdTii2SSUaYSrr8lnE9EJWjvz0k= +github.com/minio/madmin-go v1.7.4 h1:xEx9P4lFGfwyg5aiEYEyfGxPLzlPIoXakMU6TULs5rE= +github.com/minio/madmin-go v1.7.4/go.mod h1:3SO8SROxHN++tF6QxdTii2SSUaYSrr8lnE9EJWjvz0k= github.com/minio/mc v0.0.0-20221103000258-583d449e38cd h1:9FqmFhidgzw4YI5x30Jbff9psgUQrMr61Wuq1ndTwug= github.com/minio/mc v0.0.0-20221103000258-583d449e38cd/go.mod h1:cP4HBhF2WqgxEcyZHskWrIV6q4GpInRUjmglrPUutW0= github.com/minio/md5-simd v1.1.2 h1:Gdi1DZK69+ZVMoNHRXJyNcxrMA4dSxoYHZSQbirFg34= @@ -947,8 +947,6 @@ github.com/scylladb/termtables v0.0.0-20191203121021-c4c0b6d42ff4/go.mod h1:C1a7 github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= github.com/secure-io/sio-go v0.3.1 h1:dNvY9awjabXTYGsTF1PiCySl9Ltofk9GA3VdWlo7rRc= github.com/secure-io/sio-go v0.3.1/go.mod h1:+xbkjDzPjwh4Axd07pRKSNriS9SCiYksWnZqdnfpQxs= -github.com/shirou/gopsutil v3.21.11+incompatible h1:+1+c1VGhc88SSonWP6foOcLhvnKlUeu/erjjvaPEYiI= -github.com/shirou/gopsutil v3.21.11+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA= github.com/shirou/gopsutil/v3 v3.22.9 h1:yibtJhIVEMcdw+tCTbOPiF1VcsuDeTE4utJ8Dm4c5eA= github.com/shirou/gopsutil/v3 v3.22.9/go.mod h1:bBYl1kjgEJpWpxeHmLI+dVHWtyAwfcmSBLDsp2TNT8A= github.com/shurcooL/go v0.0.0-20200502201357-93f07166e636/go.mod h1:TDJrrUr11Vxrven61rcy3hJMUqaf/CLWYhHNPmT14Lk= diff --git a/internal/config/identity/ldap/ldap.go b/internal/config/identity/ldap/ldap.go index 18c8890a5..36451afd3 100644 --- a/internal/config/identity/ldap/ldap.go +++ b/internal/config/identity/ldap/ldap.go @@ -128,6 +128,16 @@ func (l Config) IsLDAPUserDN(user string) bool { return false } +// IsLDAPGroupDN determines if the given string could be a group DN from LDAP. +func (l Config) IsLDAPGroupDN(user string) bool { + for _, baseDN := range l.LDAP.GroupSearchBaseDistNames { + if strings.HasSuffix(user, ","+baseDN) { + return true + } + } + return false +} + // GetNonEligibleUserDistNames - find user accounts (DNs) that are no longer // present in the LDAP server or do not meet filter criteria anymore func (l *Config) GetNonEligibleUserDistNames(userDistNames []string) ([]string, error) {