Repository: minio/minio (language: go)

Issue:
Unable to access files when using ldap:username policy substitution
I'm attempting to setup home directory access for LDAP-authenticated users as described in the [MinIO Multi-user Quickstart Guide](https://github.com/minio/minio/blob/master/docs/multi-user/README.md). In my environment, I'd like to have a bucket named `user-data` with subdirectories containing LDAP usernames with access scoped to the corresponding LDAP user, e.g.,

```
└── user-data
        └──  example.username
```

I've defined the following policy file to enable this scenario:

```
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Action": [
        "s3:ListBucket"
      ],
      "Effect": "Allow",
      "Resource": [
        "arn:aws:s3:::user-data"
      ]
    },
    {
      "Action": [
        "s3:ListBucket"
      ],
      "Effect": "Allow",
      "Resource": [
        "arn:aws:s3:::user-data"
      ],
      "Condition": {
        "StringLike": {
          "s3:prefix": [
            "${ldap:username}/*"
          ]
        }
      }
    },
    {
      "Action": [
        "s3:GetObject",
        "s3:PutObject",
        "s3:DeleteObject"
      ],
      "Effect": "Allow",
      "Resource": [
        "arn:aws:s3:::user-data/${ldap:username}/*"
      ]
    }
  ]
}
```

## Expected Behavior

LDAP-authenticated users should be able view the contents of their home directory as well as create and delete files.

<!--- If you're describing a bug, tell us what should happen -->
<!--- If you're suggesting a change/improvement, tell us how it should work -->

## Current Behavior
<!--- If describing a bug, tell us what happens instead of the expected behavior -->
<!--- If suggesting a change/improvement, explain the difference from current behavior -->

Contents of the home directory can be viewed but access to upload or delete files is denied.

## Possible Solution
<!--- Not obligatory, but suggest a fix/reason for the bug, -->
<!--- or ideas how to implement the addition or change -->

A static policy file defined on a per-user basis could be used to workaround the issue.

## Steps to Reproduce (for bugs)
<!--- Provide a link to a live example, or an unambiguous set of steps to -->
<!--- reproduce this bug. Include code to reproduce, if relevant -->
<!--- and make sure you have followed https://github.com/minio/minio/tree/release/docs/debugging to capture relevant logs -->

The following steps can be used to reproduce the issue:

1. Start a local MinIO instance using `docker-compose`:

```
cat docker-compose.yaml << EOF
version: '3'
services:
  minio:
    image: minio/minio:RELEASE.2023-01-31T02-24-19Z
    ports:
      - "9000:9000"
      - "9001:9001"
    environment:
      MINIO_ROOT_USER: admin
      MINIO_ROOT_PASSWORD: secretkey
    volumes:
      - minio-data:/data
    command: server /data --console-address ":9001"
volumes:
  minio-data:
EOF
```

```
docker-compose up -d
```

2. Define an alias for the MinIO instance:

```
mc alias set localdev-admin http://localhost:9000 admin secretkey
```

3. Enable the LDAP identity provider:

```
mc admin config set localdev-admin identity_ldap \
    server_addr="*****" \
    lookup_bind_dn="*****" \
    lookup_bind_password="*****" \
    user_dn_search_base_dn="*****" \
    user_dn_search_filter="*****" \
    group_search_filter="*****" \
    group_search_base_dn="*****"
```

```
mc admin service restart localdev-admin
```

4. Create the `user-data` bucket and copy a file into it.

```
mc mb localdev-admin/user-data
```

```
touch test.txt
mc cp test.txt localdev-admin/user-data/example.username/test.txt
```

5. Add the home directory policy using LDAP username policy substitution:

```
cat user-data-readwrite.json << EOF
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Action": [
        "s3:ListBucket"
      ],
      "Effect": "Allow",
      "Resource": [
        "arn:aws:s3:::user-data"
      ]
    },
    {
      "Action": [
        "s3:ListBucket"
      ],
      "Effect": "Allow",
      "Resource": [
        "arn:aws:s3:::user-data"
      ],
      "Condition": {
        "StringLike": {
          "s3:prefix": [
            "${ldap:username}/*"
          ]
        }
      }
    },
    {
      "Action": [
        "s3:GetObject",
        "s3:PutObject",
        "s3:DeleteObject"
      ],
      "Effect": "Allow",
      "Resource": [
        "arn:aws:s3:::user-data/${ldap:username}/*"
      ]
    }
  ]
}
EOF
```

```
mc admin policy add localdev-admin user-data-readwrite user-data-readwrite.json
```

6. Assign the home directory policy to an LDAP user:

```
mc admin policy set localdev-admin user-data-readwrite user='CN=Example Username,OU=Users,DC=company,DC=com'
```

7. Log in to the web console as the LDAP user and create a service account with credentials `user:secretkey`.

8. Define an alias for the MinIO instance using the LDAP user's service account:

```
mc alias set localdev-user http://localhost:9000 user secretkey
```

9. Observe that the home directory contents can be listed:

```
mc ls localdev-user/user-data
```

10. Attempt to copy the file created in step 4:

```
mc cp --debug localdev-user/user-data/example.username/test.txt /tmp/test.txt
```

The following output is produced:

```
mc: <DEBUG> GET /user-data/?location= HTTP/1.1
Host: localhost:9000
User-Agent: MinIO (darwin; amd64) minio-go/v7.0.46 mc/RELEASE.2023-01-28T20-29-38Z
Authorization: AWS4-HMAC-SHA256 Credential=user/20230206/us-east-1/s3/aws4_request, SignedHeaders=host;x-amz-content-sha256;x-amz-date, Signature=**REDACTED**
X-Amz-Content-Sha256: e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
X-Amz-Date: 20230206T192300Z
Accept-Encoding: gzip

mc: <DEBUG> HTTP/1.1 403 Forbidden
Content-Length: 270
Accept-Ranges: bytes
Content-Security-Policy: block-all-mixed-content
Content-Type: application/xml
Date: Mon, 06 Feb 2023 19:23:00 GMT
Server: MinIO
Strict-Transport-Security: max-age=31536000; includeSubDomains
Vary: Origin
Vary: Accept-Encoding
X-Amz-Id-2: e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
X-Amz-Request-Id: 1741529F5FE77EF4
X-Content-Type-Options: nosniff
X-Xss-Protection: 1; mode=block

<?xml version="1.0" encoding="UTF-8"?>
<Error><Code>AccessDenied</Code><Message>Access Denied.</Message><BucketName>user-data</BucketName><Resource>/user-data/</Resource><RequestId>1741529F5FE77EF4</RequestId><HostId>ace2bdb5-ed3b-4da0-a485-a02021c57f8c</HostId></Error>mc: <DEBUG> Response Time:  4.496965ms

mc: <DEBUG> HEAD /user-data/example.username/test.txt HTTP/1.1
Host: localhost:9000
User-Agent: MinIO (darwin; amd64) minio-go/v7.0.46 mc/RELEASE.2023-01-28T20-29-38Z
Authorization: AWS4-HMAC-SHA256 Credential=user/20230206/us-east-1/s3/aws4_request, SignedHeaders=host;x-amz-content-sha256;x-amz-date, Signature=**REDACTED**
X-Amz-Content-Sha256: e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
X-Amz-Date: 20230206T192300Z

mc: <DEBUG> HTTP/1.1 403 Forbidden
Content-Length: 319
Accept-Ranges: bytes
Content-Security-Policy: block-all-mixed-content
Content-Type: application/xml
Date: Mon, 06 Feb 2023 19:23:00 GMT
Server: MinIO
Strict-Transport-Security: max-age=31536000; includeSubDomains
Vary: Origin
Vary: Accept-Encoding
X-Amz-Id-2: e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
X-Amz-Request-Id: 1741529F601A9FA0
X-Content-Type-Options: nosniff
X-Xss-Protection: 1; mode=block

mc: <DEBUG> Response Time:  2.844763ms

mc: <ERROR> Unable to validate source `localdev-user/user-data/example.username/test.txt`.
```

*Note: Access is also denied when the LDAP user is logged into the web console.

## Your Environment
<!--- Include as many relevant details about the environment you experienced the bug in -->
* MinIO server version:

RELEASE.2023-01-31T02-24-19Z

* MinIO client version:

RELEASE.2023-01-28T20-29-38Z

* Operating System and version (`uname -a`):

21.6.0 Darwin Kernel Version 21.6.0; root:xnu-8020.140.49~2/RELEASE_X86_64 x86_64


Discussion / hints:

Fix patch:
diff --git a/cmd/bucket-policy.go b/cmd/bucket-policy.go
index 0028dca257cb7..e01d09c43ddc5 100644
--- a/cmd/bucket-policy.go
+++ b/cmd/bucket-policy.go
@@ -199,15 +199,8 @@ func getConditionValues(r *http.Request, lc string, cred auth.Credentials) map[s
 	for k, v := range claims {
 		vStr, ok := v.(string)
 		if ok {
-			// Special case for AD/LDAP STS users
-			switch k {
-			case ldapUser:
-				args["user"] = []string{vStr}
-			case ldapUserN:
-				args["username"] = []string{vStr}
-			default:
-				args[k] = []string{vStr}
-			}
+			// Trim any LDAP specific prefix
+			args[strings.ToLower(strings.TrimPrefix(k, "ldap"))] = []string{vStr}
 		}
 	}
 
diff --git a/cmd/iam.go b/cmd/iam.go
index 0f7ce56996202..2f6f845c1e337 100644
--- a/cmd/iam.go
+++ b/cmd/iam.go
@@ -1693,9 +1693,6 @@ func (sys *IAMSys) IsAllowedServiceAccount(args iampolicy.Args, parentUser strin
 
 	parentArgs := args
 	parentArgs.AccountName = parentUser
-	// These are dynamic values set them appropriately.
-	parentArgs.ConditionValues["username"] = []string{parentUser}
-	parentArgs.ConditionValues["userid"] = []string{parentUser}
 
 	saPolicyClaim, ok := args.Claims[iamPolicyClaimNameSA()]
 	if !ok {
@@ -1822,10 +1819,6 @@ func (sys *IAMSys) IsAllowedSTS(args iampolicy.Args, parentUser string) bool {
 
 	// 3. If an inline session-policy is present, evaluate it.
 
-	// These are dynamic values set them appropriately.
-	args.ConditionValues["username"] = []string{parentUser}
-	args.ConditionValues["userid"] = []string{parentUser}
-
 	// Now check if we have a sessionPolicy.
 	hasSessionPolicy, isAllowedSP := isAllowedBySessionPolicy(args)
 	if hasSessionPolicy {


Test patch:
diff --git a/cmd/admin-handlers-users_test.go b/cmd/admin-handlers-users_test.go
index cecdc974c5d79..49eaa1d459e2a 100644
--- a/cmd/admin-handlers-users_test.go
+++ b/cmd/admin-handlers-users_test.go
@@ -1261,6 +1261,25 @@ func (c *check) mustListBuckets(ctx context.Context, client *minio.Client) {
 	}
 }
 
+func (c *check) mustDownload(ctx context.Context, client *minio.Client, bucket string) {
+	c.Helper()
+	rd, err := client.GetObject(ctx, bucket, "some-object", minio.GetObjectOptions{})
+	if err != nil {
+		c.Fatalf("download did not succeed got %#v", err)
+	}
+	if _, err = io.Copy(io.Discard, rd); err != nil {
+		c.Fatalf("download did not succeed got %#v", err)
+	}
+}
+
+func (c *check) mustUpload(ctx context.Context, client *minio.Client, bucket string) {
+	c.Helper()
+	_, err := client.PutObject(ctx, bucket, "some-object", bytes.NewBuffer([]byte("stuff")), 5, minio.PutObjectOptions{})
+	if err != nil {
+		c.Fatalf("upload did not succeed got %#v", err)
+	}
+}
+
 func (c *check) mustNotUpload(ctx context.Context, client *minio.Client, bucket string) {
 	c.Helper()
 	_, err := client.PutObject(ctx, bucket, "some-object", bytes.NewBuffer([]byte("stuff")), 5, minio.PutObjectOptions{})
diff --git a/cmd/sts-handlers_test.go b/cmd/sts-handlers_test.go
index 2a7a9a16c4e16..0bfce0907dcca 100644
--- a/cmd/sts-handlers_test.go
+++ b/cmd/sts-handlers_test.go
@@ -485,6 +485,7 @@ func TestIAMWithLDAPServerSuite(t *testing.T) {
 				suite.SetUpLDAP(c, ldapServer)
 				suite.TestLDAPSTS(c)
 				suite.TestLDAPSTSServiceAccounts(c)
+				suite.TestLDAPSTSServiceAccountsWithUsername(c)
 				suite.TestLDAPSTSServiceAccountsWithGroups(c)
 				suite.TearDownSuite(c)
 			},
@@ -726,6 +727,95 @@ func (s *TestSuiteIAM) TestLDAPSTSServiceAccounts(c *check) {
 	c.mustNotCreateSvcAccount(ctx, globalActiveCred.AccessKey, userAdmClient)
 }
 
+func (s *TestSuiteIAM) TestLDAPSTSServiceAccountsWithUsername(c *check) {
+	ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
+	defer cancel()
+
+	bucket := "dillon"
+	err := s.client.MakeBucket(ctx, bucket, minio.MakeBucketOptions{})
+	if err != nil {
+		c.Fatalf("bucket create error: %v", err)
+	}
+
+	// Create policy
+	policy := "mypolicy-username"
+	policyBytes := []byte(`{
+ "Version": "2012-10-17",
+ "Statement": [
+  {
+   "Effect": "Allow",
+   "Action": [
+    "s3:PutObject",
+    "s3:GetObject",
+    "s3:ListBucket"
+   ],
+   "Resource": [
+    "arn:aws:s3:::${ldap:username}/*"
+   ]
+  }
+ ]
+}`)
+	err = s.adm.AddCannedPolicy(ctx, policy, policyBytes)
+	if err != nil {
+		c.Fatalf("policy add error: %v", err)
+	}
+
+	userDN := "uid=dillon,ou=people,ou=swengg,dc=min,dc=io"
+	err = s.adm.SetPolicy(ctx, policy, userDN, false)
+	if err != nil {
+		c.Fatalf("Unable to set policy: %v", err)
+	}
+
+	ldapID := cr.LDAPIdentity{
+		Client:       s.TestSuiteCommon.client,
+		STSEndpoint:  s.endPoint,
+		LDAPUsername: "dillon",
+		LDAPPassword: "dillon",
+	}
+
+	value, err := ldapID.Retrieve()
+	if err != nil {
+		c.Fatalf("Expected to generate STS creds, got err: %#v", err)
+	}
+
+	// Check that the LDAP sts cred is actually working.
+	minioClient, err := minio.New(s.endpoint, &minio.Options{
+		Creds:     cr.NewStaticV4(value.AccessKeyID, value.SecretAccessKey, value.SessionToken),
+		Secure:    s.secure,
+		Transport: s.TestSuiteCommon.client.Transport,
+	})
+	if err != nil {
+		c.Fatalf("Error initializing client: %v", err)
+	}
+
+	// Validate that the client from sts creds can access the bucket.
+	c.mustListObjects(ctx, minioClient, bucket)
+
+	// Create an madmin client with user creds
+	userAdmClient, err := madmin.NewWithOptions(s.endpoint, &madmin.Options{
+		Creds:  cr.NewStaticV4(value.AccessKeyID, value.SecretAccessKey, value.SessionToken),
+		Secure: s.secure,
+	})
+	if err != nil {
+		c.Fatalf("Err creating user admin client: %v", err)
+	}
+	userAdmClient.SetCustomTransport(s.TestSuiteCommon.client.Transport)
+
+	// Create svc acc
+	cr := c.mustCreateSvcAccount(ctx, value.AccessKeyID, userAdmClient)
+
+	svcClient := s.getUserClient(c, cr.AccessKey, cr.SecretKey, "")
+
+	// 1. Check S3 access for service account ListObjects()
+	c.mustListObjects(ctx, svcClient, bucket)
+
+	// 2. Check S3 access for upload
+	c.mustUpload(ctx, svcClient, bucket)
+
+	// 3. Check S3 access for download
+	c.mustDownload(ctx, svcClient, bucket)
+}
+
 // In this test, the parent users gets their permissions from a group, rather
 // than having a policy set directly on them.
 func (s *TestSuiteIAM) TestLDAPSTSServiceAccountsWithGroups(c *check) {