Repository: getmoto/moto

Issue:
Unable to request for a presigned URL
I am unable to perform a GET request on a presigned URL for a file that has been uploaded.
I've attached a reproduction here: https://gist.github.com/amarjandu/77a7d8e33623bae1e4e5ba40dc043cb9
The py file that was executed in the reproduction is attached within the gist as well.


My expectation is that the file should be accessible from the presigned URL, this functionality was working on 1.3.13
https://gist.github.com/amarjandu/fb4454e90924d3814772eca68d10dace

This seems like it has to do with the `self._matches` attribute that is used around the `_find_match` functionality in `moto/core/models.py` def https://github.com/spulec/moto/blob/74272ae51cd26397c5f8453ea85a5aeb09f40cdb/moto/core/models.py#L279

The ordering of the object when its enumerated looks off, the CallbackResponse that should be returned is not the first match that is found, its one of the `The method is not implemented` responses, seems like these should only be used as a last resort.

<img width="1786" alt="Screen Shot 2021-03-19 at 1 51 16 PM" src="https://user-images.githubusercontent.com/1947534/111840775-2f12fc00-88ba-11eb-8d85-11887eaa12f6.png">

Moto Version: 
```
(.venv) CBSEspecter:azul amar$ pip list | grep oto
boto3              1.17.33
botocore           1.20.33
moto               2.0.2
```
I started a clean virtualenv and ran `pip install moto[all]==2.0.2`





Discussion:
Here is a more simple reproduction that also fails on `moto 2.0.2`.

https://gist.github.com/amarjandu/631d741ffa5c554d8a4398843ba54fc3
Hey @amarjandu, thanks for raising this! Looks like you're right on point with your analysis.

This is the list of callbacks/matches for your example:
`[<function not_implemented_callback at ...>, <moto.core.utils.convert_flask_to_responses_response object at ...>, <function not_implemented_callback at ...>]`

The `not_implemented_callback` is added twice, once by the `mock_sts` and once by the `mock_s3` decorator.
Because of the ordering, the `mock_sts`-decorator is processed first, so the `not_implemented_callback` is added to the list first. 

As a workaround, you can switch the decorators:
```
@moto.mock_s3
@moto.mock_sts
def bar(self):
```
Which would result in this list of callbacks:
`[<moto.core.utils.convert_flask_to_responses_response object at ...>, <function not_implemented_callback at ...>, <function not_implemented_callback at ...>]`

Will submit a PR with a fix shortly.


Resolving patch:
diff --git a/moto/core/models.py b/moto/core/models.py
--- a/moto/core/models.py
+++ b/moto/core/models.py
@@ -286,22 +286,47 @@ def _url_matches(self, url, other, match_querystring=False):
 
 
 def _find_first_match_legacy(self, request):
+    matches = []
     for i, match in enumerate(self._matches):
         if match.matches(request):
-            return match
-
+            matches.append(match)
+
+    # Look for implemented callbacks first
+    implemented_matches = [
+        m
+        for m in matches
+        if type(m) is not CallbackResponse or m.callback != not_implemented_callback
+    ]
+    if implemented_matches:
+        return implemented_matches[0]
+    elif matches:
+        # We had matches, but all were of type not_implemented_callback
+        return matches[0]
     return None
 
 
 def _find_first_match(self, request):
+    matches = []
     match_failed_reasons = []
     for i, match in enumerate(self._matches):
         match_result, reason = match.matches(request)
         if match_result:
-            return match, match_failed_reasons
+            matches.append(match)
         else:
             match_failed_reasons.append(reason)
 
+    # Look for implemented callbacks first
+    implemented_matches = [
+        m
+        for m in matches
+        if type(m) is not CallbackResponse or m.callback != not_implemented_callback
+    ]
+    if implemented_matches:
+        return implemented_matches[0], []
+    elif matches:
+        # We had matches, but all were of type not_implemented_callback
+        return matches[0], match_failed_reasons
+
     return None, match_failed_reasons
 
 


Test patch:
diff --git a/tests/test_core/test_request_mocking.py b/tests/test_core/test_request_mocking.py
--- a/tests/test_core/test_request_mocking.py
+++ b/tests/test_core/test_request_mocking.py
@@ -3,7 +3,7 @@
 import sure  # noqa
 
 import boto3
-from moto import mock_sqs, settings
+from moto import mock_s3, mock_sts, mock_sqs, settings
 
 
 @mock_sqs
@@ -23,3 +23,30 @@ def test_requests_to_amazon_subdomains_dont_work():
         res = requests.get("https://fakeservice.amazonaws.com/foo/bar")
         assert res.content == b"The method is not implemented"
         assert res.status_code == 400
+
+
+@mock_sts
+@mock_s3
+def test_decorator_ordering():
+    """
+    https://github.com/spulec/moto/issues/3790#issuecomment-803979809
+    """
+    bucket_name = "banana-slugs"
+    key = "trash-file"
+    region = "us-east-1"
+    client = boto3.client("s3", region_name=region)
+    s3 = boto3.resource("s3", region_name=region)
+    bucket = s3.Bucket(bucket_name)
+    bucket.create()
+    bucket.put_object(Body=b"ABCD", Key=key)
+    presigned_url = client.generate_presigned_url(
+        ClientMethod=client.get_object.__name__,
+        Params={
+            "Bucket": bucket_name,
+            "Key": key,
+            "ResponseContentDisposition": "attachment;filename=bar",
+        },
+    )
+
+    resp = requests.get(presigned_url)
+    resp.status_code.should.equal(200)