Repository: minio/minio (language: go)

Issue:
Can't login to SFTP using Active Directory account
## Expected Behavior
Login to SFTP using AD account

## Current Behavior
I can successfully login to UI using AD account, but can't login to SFTP.

## Steps to Reproduce
1. Run minio with configured LDAP/AD identity
2. Attach policy, for example
```
mc idp ldap policy entities local/
Query time: 2023-06-27T11:19:09Z
Policy -> Entity Mappings:
  Policy: consoleAdmin
    User Mappings:
      CN=Some User,OU=Accounts,DC=example,DC=com
    Group Mappings:
      CN=infra-admins,OU=Groups,OU=Accounts,DC=example,DC=com
```
3. Try to connect to SFTP:
sftp -P 8022 some.user@1.2.3.4
some.user@1.2.3.4's password:
Permission denied, please try again.

4. Minio logs:
```
API: SYSTEM()
Time: 11:20:31 UTC 06/27/2023
DeploymentID: ab7522bc-b824-4d60-a8c1-a053076aa445
Error: [ssh: no auth passed yet, Specified user does not exist, Specified user does not exist, Specified user does not exist] (*ssh.ServerAuthError)
       2: internal/logger/logger.go:258:logger.LogIf()
       1: cmd/ftp-server.go:168:cmd.startSFTPServer()
```
But account really exists (and it's consoleAdmin), i use it to login to UI.

## Your Environment
docker-compose installation
minio/minio:RELEASE.2023-06-19T19-52-50Z

Environment variables:
```
      - MINIO_ROOT_USER=minio
      - MINIO_ROOT_PASSWORD=**********
      - MINIO_IDENTITY_LDAP_SERVER_ADDR=1.2.3.4:389
      - MINIO_IDENTITY_LDAP_SERVER_INSECURE=on
      - MINIO_IDENTITY_LDAP_LOOKUP_BIND_DN=CN=svc-account,OU=ServiceAccounts,OU=Accounts,DC=example,DC=com
      - MINIO_IDENTITY_LDAP_USER_DN_SEARCH_BASE_DN=OU=Accounts,DC=example,DC=com
      - MINIO_IDENTITY_LDAP_USER_DN_SEARCH_FILTER=(|(sAMAccountName=%s)(userPrincipalName=%s))
      - MINIO_IDENTITY_LDAP_LOOKUP_BIND_PASSWORD==**********
      - MINIO_IDENTITY_LDAP_GROUP_SEARCH_FILTER=(&(objectClass=group)(member=%d))
      - MINIO_IDENTITY_LDAP_GROUP_SEARCH_BASE_DN=OU=Accounts,DC=example,DC=com
```
`command: minio server --console-address ":9001" --sftp="address=:8022" --sftp="ssh-private-key=/opt/id_rsa" /data`


Discussion / hints:
@andy812 is your `some.user` is getting translated to your LDAP CN properly? can you try logging into Console UI with the same user and password - does that work?
> @andy812 is your `some.user` is getting translated to your LDAP CN properly? can you try logging into Console UI with the same user and password - does that work?

yes, it works, i use this AD account to login into Console UI.
Okay I will check looks like something simple is missing. 

Fix patch:
diff --git a/cmd/ftp-server-driver.go b/cmd/ftp-server-driver.go
index 1733cad60da7d..01f20fce4530a 100644
--- a/cmd/ftp-server-driver.go
+++ b/cmd/ftp-server-driver.go
@@ -253,11 +253,7 @@ func (driver *ftpDriver) CheckPasswd(c *ftp.Context, username, password string)
 			return false, err
 		}
 		ldapPolicies, _ := globalIAMSys.PolicyDBGet(ldapUserDN, false, groupDistNames...)
-		if len(ldapPolicies) == 0 {
-			// no policy associated reject it.
-			return false, nil
-		}
-		return true, nil
+		return len(ldapPolicies) > 0, nil
 	}
 
 	ui, ok := globalIAMSys.GetUser(context.Background(), username)
@@ -362,12 +358,20 @@ func (driver *ftpDriver) DeleteDir(ctx *ftp.Context, path string) (err error) {
 	cctx, cancel := context.WithCancel(context.Background())
 	defer cancel()
 
+	if prefix == "" {
+		// if all objects are not deleted yet this call may fail.
+		return clnt.RemoveBucket(cctx, bucket)
+	}
+
 	objectsCh := make(chan minio.ObjectInfo)
 
 	// Send object names that are needed to be removed to objectsCh
 	go func() {
 		defer close(objectsCh)
-		opts := minio.ListObjectsOptions{Prefix: prefix, Recursive: true}
+		opts := minio.ListObjectsOptions{
+			Prefix:    prefix,
+			Recursive: true,
+		}
 		for object := range clnt.ListObjects(cctx, bucket, opts) {
 			if object.Err != nil {
 				return
@@ -427,6 +431,10 @@ func (driver *ftpDriver) MakeDir(ctx *ftp.Context, path string) (err error) {
 		return err
 	}
 
+	if prefix == "" {
+		return clnt.MakeBucket(context.Background(), bucket, minio.MakeBucketOptions{Region: globalSite.Region})
+	}
+
 	dirPath := buildMinioDir(prefix)
 
 	_, err = clnt.PutObject(context.Background(), bucket, dirPath, bytes.NewReader([]byte("")), 0,
diff --git a/cmd/ftp-server.go b/cmd/ftp-server.go
index 371a62e213ad4..9605184e98e6e 100644
--- a/cmd/ftp-server.go
+++ b/cmd/ftp-server.go
@@ -129,10 +129,29 @@ func startSFTPServer(c *cli.Context) {
 	// certificate details and handles authentication of ServerConns.
 	config := &ssh.ServerConfig{
 		PasswordCallback: func(c ssh.ConnMetadata, pass []byte) (*ssh.Permissions, error) {
+			if globalIAMSys.LDAPConfig.Enabled() {
+				targetUser, targetGroups, err := globalIAMSys.LDAPConfig.Bind(c.User(), string(pass))
+				if err != nil {
+					return nil, err
+				}
+				ldapPolicies, _ := globalIAMSys.PolicyDBGet(targetUser, false, targetGroups...)
+				if len(ldapPolicies) == 0 {
+					return nil, errAuthentication
+				}
+				return &ssh.Permissions{
+					CriticalOptions: map[string]string{
+						ldapUser:  targetUser,
+						ldapUserN: c.User(),
+					},
+					Extensions: make(map[string]string),
+				}, nil
+			}
+
 			ui, ok := globalIAMSys.GetUser(context.Background(), c.User())
 			if !ok {
 				return nil, errNoSuchUser
 			}
+
 			if subtle.ConstantTimeCompare([]byte(ui.Credentials.SecretKey), pass) == 1 {
 				return &ssh.Permissions{
 					CriticalOptions: map[string]string{
diff --git a/cmd/sftp-server-driver.go b/cmd/sftp-server-driver.go
index 2d15763b8218e..6d28e9fe854c5 100644
--- a/cmd/sftp-server-driver.go
+++ b/cmd/sftp-server-driver.go
@@ -97,18 +97,15 @@ func (f *sftpDriver) getMinIOClient() (*minio.Client, error) {
 		if err != nil {
 			return nil, err
 		}
-		ldapPolicies, _ := globalIAMSys.PolicyDBGet(targetUser, false, targetGroups...)
-		if len(ldapPolicies) == 0 {
-			return nil, errAuthentication
-		}
 		expiryDur, err := globalIAMSys.LDAPConfig.GetExpiryDuration("")
 		if err != nil {
 			return nil, err
 		}
 		claims := make(map[string]interface{})
 		claims[expClaim] = UTCNow().Add(expiryDur).Unix()
-		claims[ldapUser] = targetUser
-		claims[ldapUserN] = f.AccessKey()
+		for k, v := range f.permissions.CriticalOptions {
+			claims[k] = v
+		}
 
 		cred, err := auth.GetNewCredentialsWithMetadata(claims, globalActiveCred.SecretKey)
 		if err != nil {
@@ -165,6 +162,9 @@ func (f *sftpDriver) getMinIOClient() (*minio.Client, error) {
 }
 
 func (f *sftpDriver) AccessKey() string {
+	if _, ok := f.permissions.CriticalOptions["accessKey"]; !ok {
+		return f.permissions.CriticalOptions[ldapUserN]
+	}
 	return f.permissions.CriticalOptions["accessKey"]
 }
 
@@ -270,12 +270,20 @@ func (f *sftpDriver) Filecmd(r *sftp.Request) (err error) {
 		cctx, cancel := context.WithCancel(context.Background())
 		defer cancel()
 
+		if prefix == "" {
+			// if all objects are not deleted yet this call may fail.
+			return clnt.RemoveBucket(cctx, bucket)
+		}
+
 		objectsCh := make(chan minio.ObjectInfo)
 
 		// Send object names that are needed to be removed to objectsCh
 		go func() {
 			defer close(objectsCh)
-			opts := minio.ListObjectsOptions{Prefix: prefix, Recursive: true}
+			opts := minio.ListObjectsOptions{
+				Prefix:    prefix,
+				Recursive: true,
+			}
 			for object := range clnt.ListObjects(cctx, bucket, opts) {
 				if object.Err != nil {
 					return
@@ -305,6 +313,10 @@ func (f *sftpDriver) Filecmd(r *sftp.Request) (err error) {
 			return errors.New("bucket name cannot be empty")
 		}
 
+		if prefix == "" {
+			return clnt.MakeBucket(context.Background(), bucket, minio.MakeBucketOptions{Region: globalSite.Region})
+		}
+
 		dirPath := buildMinioDir(prefix)
 
 		_, err = clnt.PutObject(context.Background(), bucket, dirPath, bytes.NewReader([]byte("")), 0,


Test patch: