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) {