Repository: getmoto/moto
Pull request #8084 (2024-09-04 19:34:26)

Core: Support auth calls for AWSLambda
Supercedes #8063

When determining whether the current request is allowed, we need the action that is invoked - e.g. S3:ListBucket, Lambda:Invoke.
Before it would only look at the `X-Amz-Target`-header and at the `Action=`-parameter (typically found in request bodies).

However, for a lot of other services, we need to query `botocore` to determine the action based on the request-url and method. Luckily, the `_get_action`-method combines all methods (`X-Amz-Target`, `Action=`, `botocore`), so we can just use that.

codecov[bot]: ## [Codecov](https://app.codecov.io/gh/getmoto/moto/pull/8084?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto) Report
All modified and coverable lines are covered by tests :white_check_mark:
> Project coverage is 94.44%. Comparing base [(`b873d89`)](https://app.codecov.io/gh/getmoto/moto/commit/b873d8992f8a386cf54c045a4a3f6175442853ee?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto) to head [(`3d4d4c4`)](https://app.codecov.io/gh/getmoto/moto/commit/3d4d4c45c04b27a12d4f585fbc6f8aa39e460b01?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto).
> Report is 7 commits behind head on master.

<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##           master    #8084      +/-   ##
==========================================
+ Coverage   94.43%   94.44%   +0.01%     
==========================================
  Files        1139     1141       +2     
  Lines       97489    98172     +683     
==========================================
+ Hits        92064    92722     +658     
- Misses       5425     5450      +25     
```

| [Flag](https://app.codecov.io/gh/getmoto/moto/pull/8084/flags?src=pr&el=flags&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto) | Coverage Δ | |
|---|---|---|
| [servertests](https://app.codecov.io/gh/getmoto/moto/pull/8084/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto) | `28.96% <100.00%> (-0.03%)` | :arrow_down: |
| [unittests](https://app.codecov.io/gh/getmoto/moto/pull/8084/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto) | `94.42% <100.00%> (+0.01%)` | :arrow_up: |

Flags with carried forward coverage won't be shown. [Click here](https://docs.codecov.io/docs/carryforward-flags?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto#carryforward-flags-in-the-pull-request-comment) to find out more.

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/getmoto/moto/pull/8084?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto).


Patch:
diff --git a/moto/core/responses.py b/moto/core/responses.py
index b849fee21..ac80578f5 100644
--- a/moto/core/responses.py
+++ b/moto/core/responses.py
@@ -196,6 +196,7 @@ class ActionAuthenticatorMixin(object):
                 data=self.data,  # type: ignore[attr-defined]
                 body=self.body,  # type: ignore[attr-defined]
                 headers=self.headers,  # type: ignore[attr-defined]
+                action=self._get_action(),  # type: ignore[attr-defined]
             )
             iam_request.check_signature()
             iam_request.check_action_permitted(resource)
@@ -552,7 +553,9 @@ class BaseResponse(_TemplateEnvironmentMixin, ActionAuthenticatorMixin):
         return None  # type: ignore[return-value]
 
     def _get_action(self) -> str:
-        action = self.querystring.get("Action", [""])[0]
+        action = self.querystring.get("Action")
+        if action and isinstance(action, list):
+            action = action[0]
         if action:
             return action
         # Some services use a header for the action
diff --git a/moto/iam/access_control.py b/moto/iam/access_control.py
index c74c2bd49..b13bfe7b3 100644
--- a/moto/iam/access_control.py
+++ b/moto/iam/access_control.py
@@ -204,6 +204,7 @@ class IAMRequestBase(object, metaclass=ABCMeta):
         data: Dict[str, str],
         body: bytes,
         headers: Dict[str, str],
+        action: str,
     ):
         log.debug(
             f"Creating {self.__class__.__name__} with method={method}, path={path}, data={data}, headers={headers}"
@@ -220,15 +221,8 @@ class IAMRequestBase(object, metaclass=ABCMeta):
         credential_data = credential_scope.split("/")
         self._region = credential_data[2]
         self._service = credential_data[3]
-        action_from_request = self._action_from_request()
         self._action = (
-            self._service
-            + ":"
-            + (
-                action_from_request[0]
-                if isinstance(action_from_request, list)
-                else action_from_request
-            )
+            f"{self._service}:{action[0] if isinstance(action, list) else action}"
         )
         try:
             self._access_key = create_access_key(
@@ -240,11 +234,6 @@ class IAMRequestBase(object, metaclass=ABCMeta):
         except CreateAccessKeyFailure as e:
             self._raise_invalid_access_key(e.reason)
 
-    def _action_from_request(self) -> str:
-        if "X-Amz-Target" in self._headers:
-            return self._headers["X-Amz-Target"].split(".")[-1]
-        return self._data["Action"]
-
     def check_signature(self) -> None:
         original_signature = self._get_string_between(
             "Signature=", ",", self._headers["Authorization"]


Test patch:
diff --git a/tests/test_awslambda/test_lambda_invoke.py b/tests/test_awslambda/test_lambda_invoke.py
index 262817101..a2047d19a 100644
--- a/tests/test_awslambda/test_lambda_invoke.py
+++ b/tests/test_awslambda/test_lambda_invoke.py
@@ -8,6 +8,7 @@ import pytest
 from botocore.exceptions import ClientError
 
 from moto import mock_aws, settings
+from moto.core import set_initial_no_auth_action_count
 
 from ..markers import requires_docker
 from .test_lambda import LooseVersion, boto3_version
@@ -418,3 +419,36 @@ def test_invoke_lambda_with_entrypoint():
     payload = result["Payload"].read().decode("utf-8")
 
     assert json.loads(payload) == in_data
+
+
+@set_initial_no_auth_action_count(4)
+@mock_aws
+def test_lambda_request_unauthorized_user():
+    if not settings.TEST_DECORATOR_MODE:
+        raise SkipTest("Auth decorator does not work in server mode")
+    iam = boto3.client("iam", region_name="us-west-2")
+    user_name = "test-user"
+    iam.create_user(UserName=user_name)
+    policy_document = {
+        "Version": "2012-10-17",
+        "Statement": {
+            "Effect": "Deny",
+            "Action": ["s3:*", "secretsmanager:*", "lambda:*"],
+            "Resource": "*",
+        },
+    }
+    policy_arn = iam.create_policy(
+        PolicyName="policy2", PolicyDocument=json.dumps(policy_document)
+    )["Policy"]["Arn"]
+    iam.attach_user_policy(UserName=user_name, PolicyArn=policy_arn)
+    access_key = iam.create_access_key(UserName=user_name)["AccessKey"]
+
+    _lambda = boto3.session.Session(
+        aws_access_key_id=access_key["AccessKeyId"],
+        aws_secret_access_key=access_key["SecretAccessKey"],
+        region_name="us-west-2",
+    ).client(service_name="lambda")
+
+    with pytest.raises(ClientError) as exc:
+        _lambda.invoke(FunctionName="n/a", Payload="{}")
+    assert "not authorized to perform: lambda:Invoke" in str(exc.value)