Repository: minio/minio (language: go)

Issue:
[iam/ldap] User AccessKeys stop works after short period of time for special character UserDNs
## Environment
Minio version - 2024-07-15T19:02:30Z
Clean minio install
MinIO for Authentication using Active Directory / LDAP (Win2012) configured

## Expected Behavior
User accesskey grant access to S3 bucket until it expires or user has been disabled

## Current Behavior
After accesskey creation in minio console UI, accesskey works as expected a short period of time - approximately in 30 minutes, accesskey stop working - when user try to connect with it, got 'Access disabled'

Problem persists only with CN, in which present national (non-ASCII) characters.

## Steps to Reproduce (for bugs)
I have two LDAP users:
* test.m
* tešt.r

Both create accesskeys. User test.m has no problems, user tešt.r exactly what I have described.

Output of `mc idp ldap accesskey ls ALIAS`:
<pre>
mc: <ERROR> Unable to retrieve access keys for user 'cn=te\c5\a1t.r,cn=Users,dc=sdsys,dc=local'. The specified user does not exist. (Specified user does not exist)
DN cn=test.m,cn=Users,dc=sdsys,dc=local
  STS Access Keys:
    PXTU6U3DK5LJS2L4PGIZ
  Service Account Access Keys:
    LGXtt3qDErbyLBUxQPsZ
</pre>

## Regression
Is this issue a regression? - Yes. Older versions of minio - I have `2024-02-04T22:36:13Z` - not suffer from such user names.


Discussion / hints:
when I try to make `ldapsearch -b 'cn=te\c5\a1t.r,cn=Users,dc=sdsys,dc=local'`  have same result:

<pre>
# extended LDIF
#
# LDAPv3
# base <cn=te\c5\a1t.r,cn=Users,dc=sdsys,dc=local> with scope subtree
# filter: (objectclass=*)
# requesting: ALL
#

# search result
search: 2
result: 32 No such object
matchedDN: CN=Users,DC=sdsys,DC=local
text: 0000208D: NameErr: DSID-03100217, problem 2001 (NO_OBJECT), data 0, bes
 t match of:
	'CN=Users,DC=sdsys,DC=local'
</pre>


but with `ldapsearch -b "cn=Users,dc=sdsys,dc=local" "(&(objectClass=user)(cn=te\c5\a1t.r))"` it's ok:
<pre>
# extended LDIF
#
# LDAPv3
# base <CN=Users,DC=sdsys,DC=local> with scope subtree
# filter: (&(objectClass=user)(cn=te\c5\a1t.r))
# requesting: ALL
#

# te\C5\A1t.r, Users, sdsys.local
dn:: Q049dGXFoXQucixDTj1Vc2VycyxEQz1zZHN5cyxEQz1sb2NhbA==
objectClass: top
objectClass: person
objectClass: organizationalPerson
objectClass: user
cn:: dGXFoXQucg==
givenName:: dGXFoXQucg==
distinguishedName:: Q049dGXFoXQucixDTj1Vc2VycyxEQz1zZHN5cyxEQz1sb2NhbA==
....
....
</pre>
Looks like we brought back regression with decodedn changes @taran-p @donatello 
@redtex can you test with `RELEASE.2024-07-10T18-41-49Z` ? 
and is MinIO running on Windows or Linux? 
> @redtex can you test with `RELEASE.2024-07-10T18-41-49Z` ?

Yes. Done:
- deleted all accesskeys
- deployed RELEASE.2024-07-10T18-41-49Z
- created new accesskeys for test.m and tešt.r
- same errors, absolutely

It's Linux. Official k8s helm - operator+tenant 5.0.15 with mentioned images.
@redtex A few more things:

- Could you check the output for `mc idp ldap accesskey ls ALIAS 'cn=tešt.r,cn=Users,dc=sdsys,dc=local'`
- What is `user_dn_search_filter` in your LDAP config
- What is the output when trying `mc alias` with the non-functional access key?
- How many nodes/drives in your setup?
Updated to RELEASE.2024-07-16T23-46-41Z

> * Could you check the output for `mc idp ldap accesskey ls ALIAS 'cn=tešt.r,cn=Users,dc=sdsys,dc=local'`. 

No error:
<pre>
DN cn=tešt.r,cn=Users,dc=sdsys,dc=local
  Service Account Access Keys:
    h7WTFZe1eSp20qTaxQi6
</pre>

> * What is `user_dn_search_filter` in your LDAP config

`MINIO_IDENTITY_LDAP_USER_DN_SEARCH_FILTER='(&(objectCategory=user)(sAMAccountName=%s))'`

> * What is the output when trying `mc alias` with the non-functional access key?

<pre>
mc alias set test-r https://s3.... h7WTFZe1eSp20qTaxQi6 'd.....5'
mc ls test-r
mc: <ERROR> Unable to list folder. Access Denied.
</pre>

> * How many nodes/drives in your setup?

3 nodes, 2 drives on each node

can you tar up your backend drives on each node both drives at `/backend_mount/.minio.sys/config/iam` and attach it here? 

Does re-attaching policy work? or does it never work? 
sorry for long answer. I've re-create my minio tenant with identical settings, as before. Only access keys changed.
@harshavardhana 
[exp0-0.tar.gz](https://github.com/user-attachments/files/16349429/exp0-0.tar.gz)
[exp0-1.tar.gz](https://github.com/user-attachments/files/16349430/exp0-1.tar.gz)
[exp1-0.tar.gz](https://github.com/user-attachments/files/16349431/exp1-0.tar.gz)
[exp1-1.tar.gz](https://github.com/user-attachments/files/16349432/exp1-1.tar.gz)
[exp2-0.tar.gz](https://github.com/user-attachments/files/16349433/exp2-0.tar.gz)
[exp2-1.tar.gz](https://github.com/user-attachments/files/16349434/exp2-1.tar.gz)
attached required files
Policies work as expected, even for both users - with and without special characters
So only access keys created for these users stop working? @redtex 
> So only access keys created for these users stop working? @redtex

Correct. Only access key for user ```tešt.r``` stops working. Anything else - ok.
@donatello @taran-p can we look at this again - it looks like the policy lookup issue again that was considered fixed and we have CI tests on these as well.

@redtex to confirm you enable policy directly on the user not it's group right? And these generated credentials have no other inline policy?

And how long does it take for it to move from working to non working? Or does it never work?
I've reproduced the issue. We're searching using the internal normalized DN when periodically updating LDAP group memberships. Working on a fix.
@harshavardhana 

Fix patch:
diff --git a/cmd/iam.go b/cmd/iam.go
index 56a2bf080b537..51120e543e3e6 100644
--- a/cmd/iam.go
+++ b/cmd/iam.go
@@ -1442,11 +1442,13 @@ func (sys *IAMSys) updateGroupMembershipsForLDAP(ctx context.Context) {
 	// 1. Collect all LDAP users with active creds.
 	allCreds := sys.store.GetSTSAndServiceAccounts()
 	// List of unique LDAP (parent) user DNs that have active creds
-	var parentUsers []string
-	// Map of LDAP user to list of active credential objects
+	var parentUserActualDNList []string
+	// Map of LDAP user (internal representation) to list of active credential objects
 	parentUserToCredsMap := make(map[string][]auth.Credentials)
 	// DN to ldap username mapping for each LDAP user
-	parentUserToLDAPUsernameMap := make(map[string]string)
+	actualDNToLDAPUsernameMap := make(map[string]string)
+	// External (actual) LDAP DN to internal normalized representation
+	actualDNToParentUserMap := make(map[string]string)
 	for _, cred := range allCreds {
 		// Expired credentials don't need parent user updates.
 		if cred.IsExpired() {
@@ -1489,25 +1491,28 @@ func (sys *IAMSys) updateGroupMembershipsForLDAP(ctx context.Context) {
 				continue
 			}
 
-			ldapUsername, ok := jwtClaims.Lookup(ldapUserN)
-			if !ok {
+			ldapUsername, okUserN := jwtClaims.Lookup(ldapUserN)
+			ldapActualDN, okDN := jwtClaims.Lookup(ldapActualUser)
+			if !okUserN || !okDN {
 				// skip this cred - we dont have the
 				// username info needed
 				continue
 			}
 
 			// Collect each new cred.ParentUser into parentUsers
-			parentUsers = append(parentUsers, cred.ParentUser)
+			parentUserActualDNList = append(parentUserActualDNList, ldapActualDN)
 
 			// Update the ldapUsernameMap
-			parentUserToLDAPUsernameMap[cred.ParentUser] = ldapUsername
+			actualDNToLDAPUsernameMap[ldapActualDN] = ldapUsername
+
+			// Update the actualDNToParentUserMap
+			actualDNToParentUserMap[ldapActualDN] = cred.ParentUser
 		}
 		parentUserToCredsMap[cred.ParentUser] = append(parentUserToCredsMap[cred.ParentUser], cred)
-
 	}
 
 	// 2. Query LDAP server for groups of the LDAP users collected.
-	updatedGroups, err := sys.LDAPConfig.LookupGroupMemberships(parentUsers, parentUserToLDAPUsernameMap)
+	updatedGroups, err := sys.LDAPConfig.LookupGroupMemberships(parentUserActualDNList, actualDNToLDAPUsernameMap)
 	if err != nil {
 		// Log and return on error - perhaps it'll work the next time.
 		iamLogIf(GlobalContext, err)
@@ -1515,8 +1520,9 @@ func (sys *IAMSys) updateGroupMembershipsForLDAP(ctx context.Context) {
 	}
 
 	// 3. Update creds for those users whose groups are changed
-	for _, parentUser := range parentUsers {
-		currGroupsSet := updatedGroups[parentUser]
+	for _, parentActualDN := range parentUserActualDNList {
+		currGroupsSet := updatedGroups[parentActualDN]
+		parentUser := actualDNToParentUserMap[parentActualDN]
 		currGroups := currGroupsSet.ToSlice()
 		for _, cred := range parentUserToCredsMap[parentUser] {
 			gSet := set.CreateStringSet(cred.Groups...)


Test patch: