Repository: getmoto/moto

Issue:
Error is not displaying when we add "deny policy" to S3 buckets.
@bblommers 

Steps:

1. Create a new S3 bucket.
2. Add the policy with the effect set to deny.
3. Upload a file and observe.

Actual: No error is displayed after uploading the file.

Expected : When our policy effect is "Deny," an access denied error message should be displayed. 

Code : 
     bucket_policy = {
        "Version": "2012-10-17",
        "Statement": [
            {
                "Effect": "Deny",
                "Principal": "*",
                "Action": "s3:*",
                "Resource": ["arn:aws:s3:::testbucket","arn:aws:s3:::testbucket/*"],
                "Condition": {
                    "ArnNotLikeIfExists": {
                        "aws:PrincipalArn": [
                            "arn:aws:iam::(userid):role/lambda-execution-role"
                        ]
                    }
                }
            }
        ]
    }


    s3_client = boto3.client('s3')
    s3_client.create_bucket(Bucket='testbucket')


    file_path = 'C://Users//PycharmProjects//datbase//inputs//'
    object_name = 'expectedfile.csv'
    s3_client.put_bucket_policy(Bucket='testbucket', Policy=json.dumps(bucket_policy))
    result = s3_client.get_bucket_policy_status(Bucket='testbucket')
    path_of_file = os.path.join(file_path, object_name)
    res = s3_client.upload_file(path_of_file, 'testbucket', 'hello.csv')
    reading_content = s3_client.get_object(Bucket='testbucket', Key='hello.csv')
    print(reading_content["Body"].read())

Please do let me know where i am doing wrong.

Help me

https://www.geeksforgeeks.org/create-bucket-policy-in-aws-s3-bucket-with-python/

i need to restrict bucket policy in s3 bucket.


Discussion:
Hi @GovindSkand, thanks for raising this. Bucket policies are not yet fully supported in Moto; at the moment we only validate the policy when calling the `get_object` operation. So we would have to extend this to also cover the `upload_file` op.

I'm not sure how we would handle the `ArnNotLikeIfExists`-condition, though. Moto does not know about IAM roles when a request comes in, as we completely ignore the provided authentication credentials.

My initial instinct would be to always evaluate the condition to True - to block all incoming `upload_object`-requests.
Would that work for you?
@bblommers Thanks for your reply. Yeah it will work for me. I just want to know in which line I need to evaluate my condition to True. So that it can block all my upload object. Please do suggest me.
Hello Spulec/Moto,

I have replied to the ticket. Please let me know where I need to evaluate
the condition to become true. So that it can block the upload object.

Regards,
Govind.K.R

On Fri, Jan 6, 2023 at 1:36 AM Bert Blommers ***@***.***>
wrote:

> Hi @GovindSkand <https://github.com/GovindSkand>, thanks for raising
> this. Bucket policies are not yet fully supported in Moto; at the moment we
> only validate the policy when calling the get_object operation. So we
> would have to extend this to also cover the upload_file op.
>
> I'm not sure how we would handle the ArnNotLikeIfExists-condition,
> though. Moto does not know about IAM roles when a request comes in, as we
> completely ignore the provided authentication credentials.
>
> My initial instinct would be to always evaluate the condition to True - to
> block all incoming upload_object-requests.
> Would that work for you?
>
> —
> Reply to this email directly, view it on GitHub
> <https://github.com/spulec/moto/issues/5820#issuecomment-1372677496>, or
> unsubscribe
> <https://github.com/notifications/unsubscribe-auth/A5C5TDU4MYEHK7EYZHDEJKTWQ4ST5ANCNFSM6AAAAAATSF233U>
> .
> You are receiving this because you were mentioned.Message ID:
> ***@***.***>
>

Bucket policies are not yet fully supported in Moto @GovindSkand , so we will need to make some enhancements before this works. We'll let you know when this feature is added.

Resolving patch:
diff --git a/moto/iam/access_control.py b/moto/iam/access_control.py
--- a/moto/iam/access_control.py
+++ b/moto/iam/access_control.py
@@ -376,6 +376,8 @@ def is_action_permitted(self, action, resource="*"):
                 is_action_concerned = True
 
         if is_action_concerned:
+            if self.is_unknown_principal(self._statement.get("Principal")):
+                return PermissionResult.NEUTRAL
             same_resource = self._match(self._statement["Resource"], resource)
             if self._statement["Effect"] == "Allow" and same_resource:
                 return PermissionResult.PERMITTED
@@ -384,6 +386,21 @@ def is_action_permitted(self, action, resource="*"):
         else:
             return PermissionResult.NEUTRAL
 
+    def is_unknown_principal(self, principal) -> bool:
+        # https://docs.aws.amazon.com/AmazonS3/latest/userguide/s3-bucket-user-policy-specifying-principal-intro.html
+        # For now, Moto only verifies principal == *
+        # 'Unknown' principals are not verified
+        #
+        # This should be extended to check:
+        # - Can the principal be empty? How behaves AWS?
+        # - allow one/multiple account ARN's
+        # - allow one/multiple rules
+        if principal is None:
+            return False
+        if isinstance(principal, str) and principal != "*":
+            return True
+        return False
+
     def _check_element_matches(self, statement_element, value):
         if isinstance(self._statement[statement_element], list):
             for statement_element_value in self._statement[statement_element]:
diff --git a/moto/s3/models.py b/moto/s3/models.py
--- a/moto/s3/models.py
+++ b/moto/s3/models.py
@@ -12,6 +12,7 @@
 import urllib.parse
 
 from bisect import insort
+from typing import Optional
 from importlib import reload
 from moto.core import BaseBackend, BaseModel, BackendDict, CloudFormationModel
 from moto.core import CloudWatchMetricProvider
@@ -933,14 +934,14 @@ def creation_date_ISO8601(self):
     def is_versioned(self):
         return self.versioning_status == "Enabled"
 
-    def allow_action(self, action, resource):
-        if self.policy is None:
-            return False
+    def get_permission(self, action, resource):
         from moto.iam.access_control import IAMPolicy, PermissionResult
 
+        if self.policy is None:
+            return PermissionResult.NEUTRAL
+
         iam_policy = IAMPolicy(self.policy.decode())
-        result = iam_policy.is_action_permitted(action, resource)
-        return result == PermissionResult.PERMITTED
+        return iam_policy.is_action_permitted(action, resource)
 
     def set_lifecycle(self, rules):
         self.rules = []
@@ -1679,6 +1680,13 @@ def get_bucket_policy(self, bucket_name):
         return self.get_bucket(bucket_name).policy
 
     def put_bucket_policy(self, bucket_name, policy):
+        """
+        Basic policy enforcement is in place.
+
+        Restrictions:
+         - Only statements with principal=* are taken into account
+         - Conditions are not taken into account
+        """
         self.get_bucket(bucket_name).policy = policy
 
     def delete_bucket_policy(self, bucket_name):
@@ -1845,7 +1853,7 @@ def get_object(
         version_id=None,
         part_number=None,
         key_is_clean=False,
-    ):
+    ) -> Optional[FakeKey]:
         if not key_is_clean:
             key_name = clean_key_name(key_name)
         bucket = self.get_bucket(bucket_name)
diff --git a/moto/s3/responses.py b/moto/s3/responses.py
--- a/moto/s3/responses.py
+++ b/moto/s3/responses.py
@@ -1150,29 +1150,48 @@ def _key_response(self, request, full_url, headers):
         key_name = self.parse_key_name(request, parsed_url.path)
         bucket_name = self.parse_bucket_name_from_url(request, full_url)
 
-        # Because we patch the requests library the boto/boto3 API
-        # requests go through this method but so do
-        # `requests.get("https://bucket-name.s3.amazonaws.com/file-name")`
-        # Here we deny public access to private files by checking the
-        # ACL and checking for the mere presence of an Authorization
-        # header.
-        if "Authorization" not in request.headers:
-            if hasattr(request, "url"):
-                signed_url = "Signature=" in request.url
-            elif hasattr(request, "requestline"):
-                signed_url = "Signature=" in request.path
+        # SDK requests tend to have Authorization set automatically
+        # If users make an HTTP-request, such as `requests.get("https://bucket-name.s3.amazonaws.com/file-name")`,
+        # The authorization-header may not be set
+        authorized_request = "Authorization" in request.headers
+        if hasattr(request, "url"):
+            signed_url = "Signature=" in request.url
+        elif hasattr(request, "requestline"):
+            signed_url = "Signature=" in request.path
+        try:
             key = self.backend.get_object(bucket_name, key_name)
-
-            if key and not signed_url:
-                bucket = self.backend.get_bucket(bucket_name)
-                resource = f"arn:aws:s3:::{bucket_name}/{key_name}"
-                bucket_policy_allows = bucket.allow_action("s3:GetObject", resource)
-                if not bucket_policy_allows and (key.acl and not key.acl.public_read):
+            bucket = self.backend.get_bucket(bucket_name)
+        except S3ClientError:
+            key = bucket = None
+        if key:
+            resource = f"arn:aws:s3:::{bucket_name}/{key_name}"
+
+            # Authorization Workflow
+            # https://docs.aws.amazon.com/AmazonS3/latest/userguide/access-control-auth-workflow-object-operation.html
+
+            # A bucket can deny all actions, regardless of who makes the request
+            from moto.iam.access_control import PermissionResult
+
+            action = f"s3:{method.upper()[0]}{method.lower()[1:]}Object"
+            bucket_permissions = bucket.get_permission(action, resource)
+            if bucket_permissions == PermissionResult.DENIED:
+                return 403, {}, ""
+
+            # If the request is not authorized, and not signed,
+            # that means that the action should be allowed for anonymous users
+            if not authorized_request and not signed_url:
+                # We already know that the bucket permissions do not explicitly deny this
+                # So bucket permissions are either not set, or do not explicitly allow
+                # Next check is to see if the ACL of the individual key allows this action
+                if bucket_permissions != PermissionResult.PERMITTED and (
+                    key.acl and not key.acl.public_read
+                ):
                     return 403, {}, ""
-            elif signed_url and not key:
-                # coming in from requests.get(s3.generate_presigned_url())
-                if self._invalid_headers(request.url, dict(request.headers)):
-                    return 403, {}, S3_INVALID_PRESIGNED_PARAMETERS
+
+        elif signed_url and not authorized_request:
+            # coming in from requests.get(s3.generate_presigned_url())
+            if self._invalid_headers(request.url, dict(request.headers)):
+                return 403, {}, S3_INVALID_PRESIGNED_PARAMETERS
 
         if hasattr(request, "body"):
             # Boto


Test patch:
diff --git a/tests/test_s3/test_s3_bucket_policy.py b/tests/test_s3/test_s3_bucket_policy.py
--- a/tests/test_s3/test_s3_bucket_policy.py
+++ b/tests/test_s3/test_s3_bucket_policy.py
@@ -4,6 +4,7 @@
 import pytest
 import sure  # noqa # pylint: disable=unused-import
 
+from botocore.exceptions import ClientError
 from moto.moto_server.threaded_moto_server import ThreadedMotoServer
 
 
@@ -36,24 +37,78 @@ def teardown_class(cls):
             ({"resource": "arn:aws:s3:::mybucket/test_txt"}, 200),
             ({"resource": "arn:aws:s3:::notmybucket/*"}, 403),
             ({"resource": "arn:aws:s3:::mybucket/other*"}, 403),
-            ({"actions": ["s3:PutObject"]}, 403),
             ({"effect": "Deny"}, 403),
         ],
     )
-    def test_policy_allow_all(self, kwargs, status):
+    def test_block_or_allow_get_object(self, kwargs, status):
         self._put_policy(**kwargs)
 
+        if status == 200:
+            self.client.get_object(Bucket="mybucket", Key="test_txt")
+        else:
+            with pytest.raises(ClientError):
+                self.client.get_object(Bucket="mybucket", Key="test_txt")
+
         requests.get(self.key_name).status_code.should.equal(status)
 
+    def test_block_put_object(self):
+        # Block Put-access
+        self._put_policy(**{"effect": "Deny", "actions": ["s3:PutObject"]})
+
+        # GET still works
+        self.client.get_object(Bucket="mybucket", Key="test_txt")
+
+        # But Put (via boto3 or requests) is not allowed
+        with pytest.raises(ClientError) as exc:
+            self.client.put_object(Bucket="mybucket", Key="test_txt", Body="new data")
+        err = exc.value.response["Error"]
+        err["Message"].should.equal("Forbidden")
+
+        requests.put(self.key_name).status_code.should.equal(403)
+
+    def test_block_all_actions(self):
+        # Block all access
+        self._put_policy(**{"effect": "Deny", "actions": ["s3:*"]})
+
+        # Nothing works
+        with pytest.raises(ClientError) as exc:
+            self.client.get_object(Bucket="mybucket", Key="test_txt")
+        err = exc.value.response["Error"]
+        err["Message"].should.equal("Forbidden")
+
+        # But Put (via boto3 or requests) is not allowed
+        with pytest.raises(ClientError) as exc:
+            self.client.put_object(Bucket="mybucket", Key="test_txt", Body="new data")
+        err = exc.value.response["Error"]
+        err["Message"].should.equal("Forbidden")
+
+        requests.get(self.key_name).status_code.should.equal(403)
+        requests.put(self.key_name).status_code.should.equal(403)
+
+        # Allow access again, because we want to delete the object during teardown
+        self._put_policy(**{"effect": "Allow", "actions": ["s3:*"]})
+
+    def test_block_all_with_different_principal(self):
+        # Block all access for principal y
+        self._put_policy(**{"effect": "Deny", "actions": ["s3:*"], "principal": "y"})
+
+        # Everything works - Moto only blocks access for principal *
+        self.client.get_object(Bucket="mybucket", Key="test_txt")
+        self.client.put_object(Bucket="mybucket", Key="test_txt", Body="new data")
+
     def _put_policy(
-        self, resource="arn:aws:s3:::mybucket/*", effect="Allow", actions=None
+        self,
+        resource="arn:aws:s3:::mybucket/*",
+        effect="Allow",
+        actions=None,
+        principal=None,
     ):
         policy = {
             "Version": "2012-10-17",
             "Statement": [
                 {
                     "Effect": effect,
-                    "Principal": "*",
+                    "Principal": principal or "*",
                     "Action": actions or ["s3:GetObject"],
                     "Resource": resource,
                 }