From 3c2141513f5639cfed67623c5f865c3c9cd690c2 Mon Sep 17 00:00:00 2001 From: Taran Pelkey Date: Tue, 25 Jun 2024 16:21:28 -0500 Subject: [PATCH] add `ListAccessKeysLDAPBulk` API to list accessKeys for multiple/all LDAP users (#19835) --- cmd/admin-handlers-idp-ldap.go | 176 +++++++++++++++++++++++++++++++++ cmd/admin-router.go | 5 +- cmd/iam-store.go | 37 +++++++ cmd/iam.go | 10 +- go.mod | 2 +- go.sum | 4 +- 6 files changed, 226 insertions(+), 8 deletions(-) diff --git a/cmd/admin-handlers-idp-ldap.go b/cmd/admin-handlers-idp-ldap.go index 2da48e23d..97211bb5d 100644 --- a/cmd/admin-handlers-idp-ldap.go +++ b/cmd/admin-handlers-idp-ldap.go @@ -479,3 +479,179 @@ func (a adminAPIHandlers) ListAccessKeysLDAP(w http.ResponseWriter, r *http.Requ writeSuccessResponseJSON(w, encryptedData) } + +// ListAccessKeysLDAPBulk - GET /minio/admin/v3/idp/ldap/list-access-keys-bulk +func (a adminAPIHandlers) ListAccessKeysLDAPBulk(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + + // Get current object layer instance. + objectAPI := newObjectLayerFn() + if objectAPI == nil || globalNotificationSys == nil { + writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrServerNotInitialized), r.URL) + return + } + + cred, owner, s3Err := validateAdminSignature(ctx, r, "") + if s3Err != ErrNone { + writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(s3Err), r.URL) + return + } + + dnList := r.Form["userDNs"] + isAll := r.Form.Get("all") == "true" + onlySelf := !isAll && len(dnList) == 0 + + if isAll && len(dnList) > 0 { + // This should be checked on client side, so return generic error + writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrInvalidRequest), r.URL) + return + } + + // Empty DN list and not self, list access keys for all users + if isAll { + if !globalIAMSys.IsAllowed(policy.Args{ + AccountName: cred.AccessKey, + Groups: cred.Groups, + Action: policy.ListUsersAdminAction, + ConditionValues: getConditionValues(r, "", cred), + IsOwner: owner, + Claims: cred.Claims, + }) { + writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAccessDenied), r.URL) + return + } + } else if len(dnList) == 1 { + var dn string + foundResult, err := globalIAMSys.LDAPConfig.GetValidatedDNForUsername(dnList[0]) + if err == nil { + dn = foundResult.NormDN + } + if dn == cred.ParentUser || dnList[0] == cred.ParentUser { + onlySelf = true + } + } + + if !globalIAMSys.IsAllowed(policy.Args{ + AccountName: cred.AccessKey, + Groups: cred.Groups, + Action: policy.ListServiceAccountsAdminAction, + ConditionValues: getConditionValues(r, "", cred), + IsOwner: owner, + Claims: cred.Claims, + DenyOnly: onlySelf, + }) { + writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAccessDenied), r.URL) + return + } + + if onlySelf && len(dnList) == 0 { + selfDN := cred.AccessKey + if cred.ParentUser != "" { + selfDN = cred.ParentUser + } + dnList = append(dnList, selfDN) + } + + accessKeyMap := make(map[string]madmin.ListAccessKeysLDAPResp) + if isAll { + ldapUsers, err := globalIAMSys.ListLDAPUsers(ctx) + if err != nil { + writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL) + return + } + for user := range ldapUsers { + accessKeyMap[user] = madmin.ListAccessKeysLDAPResp{} + } + } else { + for _, userDN := range dnList { + // Validate the userDN + foundResult, err := globalIAMSys.LDAPConfig.GetValidatedDNForUsername(userDN) + if err != nil { + writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL) + return + } + if foundResult == nil { + continue + } + accessKeyMap[foundResult.NormDN] = madmin.ListAccessKeysLDAPResp{} + } + } + + listType := r.Form.Get("listType") + var listSTSKeys, listServiceAccounts bool + switch listType { + case madmin.AccessKeyListUsersOnly: + listSTSKeys = false + listServiceAccounts = false + case madmin.AccessKeyListSTSOnly: + listSTSKeys = true + listServiceAccounts = false + case madmin.AccessKeyListSvcaccOnly: + listSTSKeys = false + listServiceAccounts = true + case madmin.AccessKeyListAll: + listSTSKeys = true + listServiceAccounts = true + default: + err := errors.New("invalid list type") + writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErrWithErr(ErrInvalidRequest, err), r.URL) + return + } + + for dn, accessKeys := range accessKeyMap { + if listSTSKeys { + stsKeys, err := globalIAMSys.ListSTSAccounts(ctx, dn) + if err != nil { + writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL) + return + } + for _, sts := range stsKeys { + expiryTime := sts.Expiration + accessKeys.STSKeys = append(accessKeys.STSKeys, madmin.ServiceAccountInfo{ + AccessKey: sts.AccessKey, + Expiration: &expiryTime, + }) + } + // if only STS keys, skip if user has no STS keys + if !listServiceAccounts && len(stsKeys) == 0 { + delete(accessKeyMap, dn) + continue + } + } + + if listServiceAccounts { + serviceAccounts, err := globalIAMSys.ListServiceAccounts(ctx, dn) + if err != nil { + writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL) + return + } + for _, svc := range serviceAccounts { + expiryTime := svc.Expiration + accessKeys.ServiceAccounts = append(accessKeys.ServiceAccounts, madmin.ServiceAccountInfo{ + AccessKey: svc.AccessKey, + Expiration: &expiryTime, + }) + } + // if only service accounts, skip if user has no service accounts + if !listSTSKeys && len(serviceAccounts) == 0 { + delete(accessKeyMap, dn) + continue + } + } + accessKeyMap[dn] = accessKeys + } + + data, err := json.Marshal(accessKeyMap) + if err != nil { + writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL) + return + } + + encryptedData, err := madmin.EncryptData(cred.SecretKey, data) + if err != nil { + writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL) + return + } + + writeSuccessResponseJSON(w, encryptedData) +} diff --git a/cmd/admin-router.go b/cmd/admin-router.go index d441e1196..f6d9acf69 100644 --- a/cmd/admin-router.go +++ b/cmd/admin-router.go @@ -301,8 +301,9 @@ func registerAdminRouter(router *mux.Router, enableConfigOps bool) { // LDAP specific service accounts ops adminRouter.Methods(http.MethodPut).Path(adminVersion + "/idp/ldap/add-service-account").HandlerFunc(adminMiddleware(adminAPI.AddServiceAccountLDAP)) adminRouter.Methods(http.MethodGet).Path(adminVersion+"/idp/ldap/list-access-keys"). - HandlerFunc(adminMiddleware(adminAPI.ListAccessKeysLDAP)). - Queries("userDN", "{userDN:.*}", "listType", "{listType:.*}") + HandlerFunc(adminMiddleware(adminAPI.ListAccessKeysLDAP)).Queries("userDN", "{userDN:.*}", "listType", "{listType:.*}") + adminRouter.Methods(http.MethodGet).Path(adminVersion+"/idp/ldap/list-access-keys-bulk"). + HandlerFunc(adminMiddleware(adminAPI.ListAccessKeysLDAPBulk)).Queries("listType", "{listType:.*}") // LDAP IAM operations adminRouter.Methods(http.MethodGet).Path(adminVersion + "/idp/ldap/policy-entities").HandlerFunc(adminMiddleware(adminAPI.ListLDAPPolicyMappingEntities)) diff --git a/cmd/iam-store.go b/cmd/iam-store.go index 0a0a53c1f..658f42563 100644 --- a/cmd/iam-store.go +++ b/cmd/iam-store.go @@ -1907,6 +1907,11 @@ func (store *IAMStoreSys) GetAllParentUsers() map[string]ParentUserInfo { cache := store.rlock() defer store.runlock() + return store.getParentUsers(cache) +} + +// assumes store is locked by caller. +func (store *IAMStoreSys) getParentUsers(cache *iamCache) map[string]ParentUserInfo { res := map[string]ParentUserInfo{} for _, ui := range cache.iamUsersMap { cred := ui.Credentials @@ -1977,6 +1982,38 @@ func (store *IAMStoreSys) GetAllParentUsers() map[string]ParentUserInfo { return res } +// GetAllSTSUserMappings - Loads all STS user policy mappings from storage and +// returns them. Also gets any STS users that do not have policy mappings but have +// Service Accounts or STS keys (This is useful if the user is part of a group) +func (store *IAMStoreSys) GetAllSTSUserMappings(userPredicate func(string) bool) (map[string]string, error) { + cache := store.rlock() + defer store.runlock() + + stsMap := make(map[string]string) + m := xsync.NewMapOf[string, MappedPolicy]() + if err := store.loadMappedPolicies(context.Background(), stsUser, false, m); err != nil { + return nil, err + } + + m.Range(func(user string, mappedPolicy MappedPolicy) bool { + if userPredicate != nil && !userPredicate(user) { + return true + } + stsMap[user] = mappedPolicy.Policies + return true + }) + + for user := range store.getParentUsers(cache) { + if _, ok := stsMap[user]; !ok { + if userPredicate != nil && !userPredicate(user) { + continue + } + stsMap[user] = "" + } + } + return stsMap, nil +} + // Assumes store is locked by caller. If users is empty, returns all user mappings. func (store *IAMStoreSys) listUserPolicyMappings(cache *iamCache, users []string, userPredicate func(string) bool, diff --git a/cmd/iam.go b/cmd/iam.go index f2e5a0c41..50e436a10 100644 --- a/cmd/iam.go +++ b/cmd/iam.go @@ -786,11 +786,15 @@ func (sys *IAMSys) ListLDAPUsers(ctx context.Context) (map[string]madmin.UserInf select { case <-sys.configLoaded: - ldapUsers := make(map[string]madmin.UserInfo) - for user, policy := range sys.store.GetUsersWithMappedPolicies() { + stsMap, err := sys.store.GetAllSTSUserMappings(sys.LDAPConfig.IsLDAPUserDN) + if err != nil { + return nil, err + } + ldapUsers := make(map[string]madmin.UserInfo, len(stsMap)) + for user, policy := range stsMap { ldapUsers[user] = madmin.UserInfo{ PolicyName: policy, - Status: madmin.AccountEnabled, + Status: statusEnabled, } } return ldapUsers, nil diff --git a/go.mod b/go.mod index 2e1d9854b..8e67916fa 100644 --- a/go.mod +++ b/go.mod @@ -52,7 +52,7 @@ require ( github.com/minio/highwayhash v1.0.2 github.com/minio/kms-go/kes v0.3.0 github.com/minio/kms-go/kms v0.4.0 - github.com/minio/madmin-go/v3 v3.0.55 + github.com/minio/madmin-go/v3 v3.0.57 github.com/minio/minio-go/v7 v7.0.72-0.20240610154810-fa174cbf14b0 github.com/minio/mux v1.9.0 github.com/minio/pkg/v3 v3.0.2 diff --git a/go.sum b/go.sum index e56cc4bd4..71d1b47d5 100644 --- a/go.sum +++ b/go.sum @@ -456,8 +456,8 @@ github.com/minio/kms-go/kes v0.3.0 h1:SU8VGVM/Hk9w1OiSby3OatkcojooUqIdDHl6dtM6Nk github.com/minio/kms-go/kes v0.3.0/go.mod h1:w6DeVT878qEOU3nUrYVy1WOT5H1Ig9hbDIh698NYJKY= github.com/minio/kms-go/kms v0.4.0 h1:cLPZceEp+05xHotVBaeFJrgL7JcXM4lBy6PU0idkE7I= github.com/minio/kms-go/kms v0.4.0/go.mod h1:q12CehiIy2qgBnDKq6Q7wmPi2PHSyRVug5DKp0HAVeE= -github.com/minio/madmin-go/v3 v3.0.55 h1:Vm5AWS0kFoWwoJX4epskjVwmmS64xMNORMZaGR3cbK8= -github.com/minio/madmin-go/v3 v3.0.55/go.mod h1:IFAwr0XMrdsLovxAdCcuq/eoL4nRuMVQQv0iubJANQw= +github.com/minio/madmin-go/v3 v3.0.57 h1:fXoOnYP8/k9x0MWWowXkAQWYu59hongieCcT3urUaAQ= +github.com/minio/madmin-go/v3 v3.0.57/go.mod h1:IFAwr0XMrdsLovxAdCcuq/eoL4nRuMVQQv0iubJANQw= github.com/minio/mc v0.0.0-20240612143403-e7c9a733c680 h1:Ns5mhSm86qJx6a9GJ1kzHkZMjRMZrQGsptakVRmq4QA= github.com/minio/mc v0.0.0-20240612143403-e7c9a733c680/go.mod h1:21/cb+wUd+lLRsdX7ACqyO8DzPNSpXftp1bOkQlIbh8= github.com/minio/md5-simd v1.1.2 h1:Gdi1DZK69+ZVMoNHRXJyNcxrMA4dSxoYHZSQbirFg34=