Repository: getmoto/moto

Issue:
S3 IAM: Head object for objects without permissions raises wrong error.
Hi there! Thanks for writing this library, it's been exceptionally useful for testing error handling of boto code under simulated real life conditions! I've been writing some tests which mock IAM perms for S3 and one of my fake accounts with zero permissions receives a 404 every time it tries to HEAD an object in S3. Against the live AWS endpoint these are 403 errors, regardless of whether or not the file exists.

Here's a minimum working example:

```python
import boto3
from botocore.exceptions import ClientError
from moto import mock_s3, mock_iam
from moto.core import set_initial_no_auth_action_count


def head_fake_object():
    """Head an S3 object we should have no access to."""
    resource = boto3.resource(
        "s3",
        aws_access_key_id="fake_key",
        aws_secret_access_key="fake_pass"
    )

    obj = resource.Object("myfakebucket", "myfakekey")
    try:
        obj.load()
    except ClientError as err:
        response = err.response
        status_code = response["ResponseMetadata"]["HTTPStatusCode"]
        assert status_code == 403


if __name__ == "__main__":
    # Using AWS.
    print("Trying to head object with no perms (against live AWS).")
    head_fake_object()
    print("Successfully returns 403.")

    # Using moto.
    print("Trying to head object with no perms (against moto endpoint).")
    mock_s3().start()
    mock_iam().start()
    decorator = set_initial_no_auth_action_count(0)
    decorator(head_fake_object)()
    # Assertion error here.
    print("Successfully returns 403.")
```

This code outputs the following:

```
Trying to head object with no perms (against live AWS).
Successfully returns 403.
Trying to head object with no perms (against moto endpoint).
Traceback (most recent call last):
  File "example.py", line 17, in head_fake_object
    obj.load()
  File "/home/thesketh/.anaconda/envs/blobject/lib/python3.8/site-packages/boto3/resources/factory.py", line 505, in do_action
    response = action(self, *args, **kwargs)
  File "/home/thesketh/.anaconda/envs/blobject/lib/python3.8/site-packages/boto3/resources/action.py", line 83, in __call__
    response = getattr(parent.meta.client, operation_name)(*args, **params)
  File "/home/thesketh/.anaconda/envs/blobject/lib/python3.8/site-packages/botocore/client.py", line 386, in _api_call
    return self._make_api_call(operation_name, kwargs)
  File "/home/thesketh/.anaconda/envs/blobject/lib/python3.8/site-packages/botocore/client.py", line 705, in _make_api_call
    raise error_class(parsed_response, operation_name)
botocore.errorfactory.NoSuchBucket: An error occurred (NoSuchBucket) when calling the HeadObject operation: The specified bucket does not exist

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "example.py", line 35, in <module>
    decorator(head_fake_object)()
  File "/home/thesketh/.anaconda/envs/blobject/lib/python3.8/site-packages/moto/core/responses.py", line 165, in wrapper
    result = function(*args, **kwargs)
  File "example.py", line 21, in head_fake_object
    assert status_code == 403
AssertionError
```


Discussion:
Thanks for raising this @thesketh. Looks like we're authenticating all other HTTP methods (GET/PUT/POST/DELETE), but not for HEAD requests. Marking it as a bug.

Resolving patch:
diff --git a/moto/s3/responses.py b/moto/s3/responses.py
--- a/moto/s3/responses.py
+++ b/moto/s3/responses.py
@@ -122,6 +122,7 @@
         },
     },
     "KEY": {
+        "HEAD": {"DEFAULT": "HeadObject",},
         "GET": {
             "uploadId": "ListMultipartUploadParts",
             "acl": "GetObjectAcl",
@@ -1517,6 +1518,9 @@ def _key_response_put(self, request, body, bucket_name, query, key_name, headers
         return 200, response_headers, ""
 
     def _key_response_head(self, bucket_name, query, key_name, headers):
+        self._set_action("KEY", "HEAD", query)
+        self._authenticate_and_authorize_s3_action()
+
         response_headers = {}
         version_id = query.get("versionId", [None])[0]
         part_number = query.get("partNumber", [None])[0]


Test patch:
diff --git a/tests/test_s3/test_s3_auth.py b/tests/test_s3/test_s3_auth.py
new file mode 100644
--- /dev/null
+++ b/tests/test_s3/test_s3_auth.py
@@ -0,0 +1,26 @@
+import boto3
+import pytest
+
+from botocore.exceptions import ClientError
+from moto import mock_s3, settings
+from moto.core import set_initial_no_auth_action_count
+from unittest import SkipTest
+
+
+@mock_s3
+@set_initial_no_auth_action_count(0)
+def test_load_unexisting_object_without_auth_should_return_403():
+    if settings.TEST_SERVER_MODE:
+        raise SkipTest("Auth decorator does not work in server mode")
+
+    """Head an S3 object we should have no access to."""
+    resource = boto3.resource("s3", region_name="us-east-1")
+
+    obj = resource.Object("myfakebucket", "myfakekey")
+    with pytest.raises(ClientError) as ex:
+        obj.load()
+    err = ex.value.response["Error"]
+    err["Code"].should.equal("InvalidAccessKeyId")
+    err["Message"].should.equal(
+        "The AWS Access Key Id you provided does not exist in our records."
+    )