Repository: getmoto/moto

Issue:
S3 `put_object` allows for both Canned ACL and Header Grant
`moto==2.0.9` (Python mocks)
`boto3==1.15.18`
`botocore==1.18.18`

From Amazon's AWS documentation [here](https://awscli.amazonaws.com/v2/documentation/api/latest/reference/s3api/put-object-acl.html), it seems that you cannot specify a Canned ACL **AND** a Header Grant when you are setting an ACL for an object within an S3 bucket.

```
@mock_s3
def test_canned_acl_and_header_grant(self):
   client = boto3.client('s3')
   s3 = boto3.resource('s3')
   client.create_bucket(Bucket='example-bucket')
   bucket = s3.Bucket('example-bucket')
   acl = client.get_bucket_acl(Bucket='example-bucket')
   acl_grantee_id = acl['Owner']['ID']
   
   # This should raise an exception or provide some error message, but runs without exception instead.
   bucket.put_object(ACL='bucket-owner-full-control', Body='example-file-path', Key='example-key', ContentType='text/plain', GrantFullControl=f'id="{acl_grantee_id}"')
```

The above code passes currently, but should fail since an `ACL` and `GrantFullControl` header were both passed into `put_object`.

This is the exception that `botocore` raises with this code: 

`botocore.exceptions.ClientError: An error occurred (InvalidRequest) when calling the PutObject operation: Specifying both Canned ACLs and Header Grants is not allowed`

It seems that the code [here](https://github.com/spulec/moto/blob/master/moto/s3/responses.py#L1511) returns immediately if a `canned_acl` is found when it should check if both a `canned_acl` and `grantees` exist? 


Discussion:
Thanks for raising this @niraj-rao - marking it as an enhancement to implement this validation.

Resolving patch:
diff --git a/moto/s3/exceptions.py b/moto/s3/exceptions.py
--- a/moto/s3/exceptions.py
+++ b/moto/s3/exceptions.py
@@ -347,6 +347,18 @@ def __init__(self, *args, **kwargs):
         )
 
 
+class S3AclAndGrantError(S3ClientError):
+    code = 400
+
+    def __init__(self, *args, **kwargs):
+        super(S3AclAndGrantError, self).__init__(
+            "InvalidRequest",
+            "Specifying both Canned ACLs and Header Grants is not allowed",
+            *args,
+            **kwargs,
+        )
+
+
 class BucketInvalidTokenError(BucketError):
     code = 400
 
diff --git a/moto/s3/responses.py b/moto/s3/responses.py
--- a/moto/s3/responses.py
+++ b/moto/s3/responses.py
@@ -46,6 +46,7 @@
     IllegalLocationConstraintException,
     InvalidNotificationARN,
     InvalidNotificationEvent,
+    S3AclAndGrantError,
     InvalidObjectState,
     ObjectNotInActiveTierError,
     NoSystemTags,
@@ -1730,8 +1731,6 @@ def _get_grants_from_xml(self, grant_list, exception_type, permissions):
 
     def _acl_from_headers(self, headers):
         canned_acl = headers.get("x-amz-acl", "")
-        if canned_acl:
-            return get_canned_acl(canned_acl)
 
         grants = []
         for header, value in headers.items():
@@ -1758,6 +1757,10 @@ def _acl_from_headers(self, headers):
                     grantees.append(FakeGrantee(uri=value))
             grants.append(FakeGrant(grantees, [permission]))
 
+        if canned_acl and grants:
+            raise S3AclAndGrantError()
+        if canned_acl:
+            return get_canned_acl(canned_acl)
         if grants:
             return FakeAcl(grants)
         else:


Test patch:
diff --git a/tests/test_s3/test_s3_acl.py b/tests/test_s3/test_s3_acl.py
--- a/tests/test_s3/test_s3_acl.py
+++ b/tests/test_s3/test_s3_acl.py
@@ -326,3 +326,29 @@ def test_acl_setting_via_headers_boto3():
             "Permission": "READ",
         }
     )
+
+
+@mock_s3
+def test_raise_exception_for_grant_and_acl():
+    client = boto3.client("s3")
+    s3 = boto3.resource("s3")
+    bucket_name = "bucketname"
+    client.create_bucket(Bucket=bucket_name)
+    bucket = s3.Bucket(bucket_name)
+    acl = client.get_bucket_acl(Bucket=bucket_name)
+    acl_grantee_id = acl["Owner"]["ID"]
+
+    # This should raise an exception or provide some error message, but runs without exception instead.
+    with pytest.raises(ClientError) as exc:
+        bucket.put_object(
+            ACL="bucket-owner-full-control",
+            Body="example-file-path",
+            Key="example-key",
+            ContentType="text/plain",
+            GrantFullControl=f'id="{acl_grantee_id}"',
+        )
+    err = exc.value.response["Error"]
+    err["Code"].should.equal("InvalidRequest")
+    err["Message"].should.equal(
+        "Specifying both Canned ACLs and Header Grants is not allowed"
+    )