Repository: minio/minio (language: go)

Issue:
PostPolicyBucketHandler, Access Denied with LDAP service account, PutObject works
When creating a Service Account under an LDAP user, we are seeing continuous Access Denied responses when using s3.PostPolicyBucket. Using PUT works fine. Using a normal non-LDAP configuration and service accounts also works fine.

Is there something in the behavior of PostPolicyBucketHandler when using LDAP that would cause an Access Denied?

I have tried everything I can think of, bucket policies, service account policies, IAM policies, all to no effect.

## Expected Behavior

Allow use of PostPolicyBucket with LDAP. I am trying to integrate Uppy (uppy.io) as an uploader using its S3 integration. It works well with play.min.io but does not work in our setup.

## Current Behavior

See attached trace files from minio console, and HTTP POST body.

[ldap-post-trace.txt](https://github.com/minio/minio/files/7937673/ldap-post-trace.txt)

## Possible Solution

Difference in handling globalIAMSys.IsAllowed when using Service Accounts created by an LDAP user? That is the only thing I can see returning a blank 'Access Denied'.

https://github.com/minio/minio/blob/5a9f133491153b546982e4e140e87c7397799eb0/cmd/bucket-handlers.go#L952

## Steps to Reproduce (for bugs)
1. Setup LDAP
2. Create Service Account under user with full consoleAdmin and readwrite privileges, do not restrict policy.
3. Attempt to use a POST with policy to upload a file.
4. Use Uppy Companion with the following settings:

```
...
export COMPANION_AWS_ENDPOINT="https://minio.x.y"
export COMPANION_AWS_BUCKET="uploads"
export COMPANION_AWS_REGION="us-vr-gip"
export COMPANION_AWS_USE_ACCELERATE_ENDPOINT="false"
export COMPANION_AWS_EXPIRES="300"
export COMPANION_AWS_ACL="public-read"
...
```
5. Uppy frontend:
```
...
  .use(AwsS3, {
    limit: 4,
    timeout: 300000,
    companionUrl: 'http://upload.x.y/companion'
  });
...
```

## Your Environment
<!--- Include as many relevant details about the environment you experienced the bug in -->
* Version used (`minio --version`): minio version RELEASE.2022-01-08T03-11-54Z
* Server setup and configuration: VMware VM, single server configuration with 4 disks
  * API Requests forwarded through Nginx.
* Operating System and version (`uname -a`): Debian 11 bullseye, installed through .deb



Discussion / hints:
can you share your policy for the service account here @kevinbaker ?
Thank you for the help. I think that is part of the problem... Parent user shows up for the service account:

```
$ mc admin user svcacct info minio 7RXMDSTBJ2X7YF3LK6PQ
AccessKey: 7RXMDSTBJ2X7YF3LK6PQ
ParentUser: CN=Kevin Baker,CN=Users,DC=x,DC=y
Status: on
Policy: embedded
$ mc admin user svcacct info minio 7RXMDSTBJ2X7YF3LK6PQ --json
{
 "status": "success",
 "accessKey": "7RXMDSTBJ2X7YF3LK6PQ",
 "parentUser": "CN=Kevin Baker,CN=Users,DC=x,DC=y",
 "policy": "{\n \"Version\": \"\",\n \"Statement\": null\n}",
 "accountStatus": "on"
}
```

There are no users defined - since this is LDAP-integrated, only the group was added in a policy; I have never created specific user accounts:

```
$ mc admin user list minio
$
$ mc admin user info minio "CN=Kevin Baker,CN=Users,DC=x,DC=y"
mc: <ERROR> Unable to get user info. The specified user does not exist. (Specified user does not exist).
$ mc admin group list minio
CN=Developers,CN=Users,DC=x,DC=y
CN=Domain Users,CN=Users,DC=x,DC=y
$ mc admin group info minio "CN=Developers,CN=Users,DC=x,DC=y"
Group: CN=Developers,CN=Users,DC=x,DC=y
Status:
Policy: consoleAdmin
Members:
$ mc admin group info minio "CN=Domain Users,CN=Users,DC=x,DC=y"
Group: CN=Domain Users,CN=Users,DC=crm,DC=x,DC=y
Status:
Policy: readwrite
Members:
```

My user "CN=Kevin Baker,CN=Users,DC=x,DC=y" is a member of both groups in LDAP. Maybe that is another part of the issue?

Maybe do I need to create a specific policy to attach to the service account? Or should the LDAP group relation work with inheritance through the Service Accounts?
> 

If there is direct policy then it's inherited by parent user. So it should work fine as long as the policy is present on the LDAP account.

Let me check why it won't work.

Fix patch:
diff --git a/cmd/bucket-handlers.go b/cmd/bucket-handlers.go
index 24f72292adf00..7754741cb95e2 100644
--- a/cmd/bucket-handlers.go
+++ b/cmd/bucket-handlers.go
@@ -951,6 +951,7 @@ func (api objectAPIHandlers) PostPolicyBucketHandler(w http.ResponseWriter, r *h
 	// explicit permissions for the user.
 	if !globalIAMSys.IsAllowed(iampolicy.Args{
 		AccountName:     cred.AccessKey,
+		Groups:          cred.Groups,
 		Action:          iampolicy.PutObjectAction,
 		ConditionValues: getConditionValues(r, "", cred.AccessKey, cred.Claims),
 		BucketName:      bucket,
diff --git a/cmd/jwt.go b/cmd/jwt.go
index 288a3cb3745da..f2b18d19fb384 100644
--- a/cmd/jwt.go
+++ b/cmd/jwt.go
@@ -132,13 +132,13 @@ func authenticateURL(accessKey, secretKey string) (string, error) {
 // Check if the request is authenticated.
 // Returns nil if the request is authenticated. errNoAuthToken if token missing.
 // Returns errAuthentication for all other errors.
-func webRequestAuthenticate(req *http.Request) (*xjwt.MapClaims, bool, error) {
+func webRequestAuthenticate(req *http.Request) (*xjwt.MapClaims, []string, bool, error) {
 	token, err := jwtreq.AuthorizationHeaderExtractor.ExtractToken(req)
 	if err != nil {
 		if err == jwtreq.ErrNoTokenInRequest {
-			return nil, false, errNoAuthToken
+			return nil, nil, false, errNoAuthToken
 		}
-		return nil, false, err
+		return nil, nil, false, err
 	}
 	claims := xjwt.NewMapClaims()
 	if err := xjwt.ParseWithClaims(token, claims, func(claims *xjwt.MapClaims) ([]byte, error) {
@@ -151,20 +151,21 @@ func webRequestAuthenticate(req *http.Request) (*xjwt.MapClaims, bool, error) {
 		}
 		return []byte(cred.SecretKey), nil
 	}); err != nil {
-		return claims, false, errAuthentication
+		return claims, nil, false, errAuthentication
 	}
 	owner := true
+	var groups []string
 	if globalActiveCred.AccessKey != claims.AccessKey {
 		// Check if the access key is part of users credentials.
 		ucred, ok := globalIAMSys.GetUser(req.Context(), claims.AccessKey)
 		if !ok {
-			return nil, false, errInvalidAccessKeyID
+			return nil, nil, false, errInvalidAccessKeyID
 		}
 
 		// get embedded claims
 		eclaims, s3Err := checkClaimsFromToken(req, ucred)
 		if s3Err != ErrNone {
-			return nil, false, errAuthentication
+			return nil, nil, false, errAuthentication
 		}
 
 		for k, v := range eclaims {
@@ -177,9 +178,11 @@ func webRequestAuthenticate(req *http.Request) (*xjwt.MapClaims, bool, error) {
 		} else {
 			owner = globalActiveCred.AccessKey == ucred.ParentUser
 		}
+
+		groups = ucred.Groups
 	}
 
-	return claims, owner, nil
+	return claims, groups, owner, nil
 }
 
 // newCachedAuthToken returns a token that is cached up to 15 seconds.
diff --git a/cmd/metrics.go b/cmd/metrics.go
index dc7956dbf205b..e626eddaa6a9e 100644
--- a/cmd/metrics.go
+++ b/cmd/metrics.go
@@ -674,7 +674,7 @@ func metricsHandler() http.Handler {
 // AuthMiddleware checks if the bearer token is valid and authorized.
 func AuthMiddleware(h http.Handler) http.Handler {
 	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
-		claims, owner, authErr := webRequestAuthenticate(r)
+		claims, groups, owner, authErr := webRequestAuthenticate(r)
 		if authErr != nil || !claims.VerifyIssuer("prometheus", true) {
 			w.WriteHeader(http.StatusForbidden)
 			return
@@ -682,6 +682,7 @@ func AuthMiddleware(h http.Handler) http.Handler {
 		// For authenticated users apply IAM policy.
 		if !globalIAMSys.IsAllowed(iampolicy.Args{
 			AccountName:     claims.AccessKey,
+			Groups:          groups,
 			Action:          iampolicy.PrometheusAdminAction,
 			ConditionValues: getConditionValues(r, "", claims.AccessKey, claims.Map()),
 			IsOwner:         owner,


Test patch:
diff --git a/cmd/jwt_test.go b/cmd/jwt_test.go
index b7f10476e379d..8a6b09be21b56 100644
--- a/cmd/jwt_test.go
+++ b/cmd/jwt_test.go
@@ -149,7 +149,7 @@ func TestWebRequestAuthenticate(t *testing.T) {
 	}
 
 	for i, testCase := range testCases {
-		_, _, gotErr := webRequestAuthenticate(testCase.req)
+		_, _, _, gotErr := webRequestAuthenticate(testCase.req)
 		if testCase.expectedErr != gotErr {
 			t.Errorf("Test %d, expected err %s, got %s", i+1, testCase.expectedErr, gotErr)
 		}