使用 django-storages 和 boto3 在 Django 中调用 CreateMultipartUpload 操作时出现 AccessDenied

AccessDenied when calling the CreateMultipartUpload operation in Django using django-storages and boto3

我想使用 django-storages 将我的模型文件存储在 Amazon S3 中,但出现 Access Denied 错误。我已授予用户几乎所有资源的 S3 权限 PutObject、ListBucketMultipartUploads、ListMultipartUploadParts、AbortMultipartUpload 权限等,但这并没有解决问题。

settings.py

...
DEFAULT_FILE_STORAGE = 'storages.backends.s3boto3.S3Boto3Storage'
AWS_S3_REGION_NAME = 'eu-west-1'
AWS_S3_CUSTOM_DOMAIN = 'www.xyz.com'
AWS_DEFAULT_ACL = None
AWS_STORAGE_BUCKET_NAME = 'www.xyz.com'
...

使用Django shell,我尝试使用如下所示的存储系统。

Python 3.6.6 (default, Sep 12 2018, 18:26:19)
[GCC 8.0.1 20180414 (experimental) [trunk revision 259383]] on linux
Type "help", "copyright", "credits" or "license" for more information.
(InteractiveConsole)
>>> import os
>>> AWS_ACCESS_KEY_ID = os.environ.get( 'AWS_ACCESS_KEY_ID', 'anything' )
>>> AWS_SECRET_ACCESS_KEY = os.environ.get( 'AWS_SECRET_ACCESS_KEY', 'anything' )
>>> AWS_DEFAULT_ACL = 'public-read'
>>> from django.core.files.storage import default_storage
>>> file = default_storage.open('test', 'w')
...
>>> file.write('storage contents')
2018-09-27 16:41:42,596 botocore.hooks [DEBUG] Event before-parameter-build.s3.CreateMultipartUpload: calling handler <function validate_ascii_metadata at 0x7fdb5e848d08>
2018-09-27 16:41:42,596 botocore.hooks [DEBUG] Event before-parameter-build.s3.CreateMultipartUpload: calling handler <function sse_md5 at 0x7fdb5e848158>
2018-09-27 16:41:42,597 botocore.hooks [DEBUG] Event before-parameter-build.s3.CreateMultipartUpload: calling handler <function validate_bucket_name at 0x7fdb5e8480d0>
2018-09-27 16:41:42,597 botocore.hooks [DEBUG] Event before-parameter-build.s3.CreateMultipartUpload: calling handler <bound method S3RegionRedirector.redirect_from_cache of <botocore.utils.S3RegionRedirector object at 0x7fdb5c5d1128>>
2018-09-27 16:41:42,597 botocore.hooks [DEBUG] Event before-parameter-build.s3.CreateMultipartUpload: calling handler <function generate_idempotent_uuid at 0x7fdb5e846c80>
2018-09-27 16:41:42,598 botocore.hooks [DEBUG] Event before-call.s3.CreateMultipartUpload: calling handler <function add_expect_header at 0x7fdb5e848598>
2018-09-27 16:41:42,598 botocore.hooks [DEBUG] Event before-call.s3.CreateMultipartUpload: calling handler <bound method S3RegionRedirector.set_request_url of <botocore.utils.S3RegionRedirector object at 0x7fdb5c5d1128>>
2018-09-27 16:41:42,598 botocore.endpoint [DEBUG] Making request for OperationModel(name=CreateMultipartUpload) with params: {'url_path': '/www.xyz.com/test?uploads', 'query_string': {}, 'method': 'POST', 'headers': {'Content-Type': 'application/octet-stream', 'User-Agent': 'Boto3/1.7.80 Python/3.6.6 Linux/4.14.67-66.56.amzn1.x86_64 Botocore/1.11.1 Resource'}, 'body': b'', 'url': 'https://s3.eu-west-1.amazonaws.com/www.xyz.com/test?uploads', 'context': {'client_region': 'eu-west-1', 'client_config': <botocore.config.Config object at 0x7fdb5c8e80b8>, 'has_streaming_input': False, 'auth_type': None, 'signing': {'bucket': 'www.xyz.com'}}}
2018-09-27 16:41:42,599 botocore.hooks [DEBUG] Event request-created.s3.CreateMultipartUpload: calling handler <bound method RequestSigner.handler of <botocore.signers.RequestSigner object at 0x7fdb5c8db780>>
2018-09-27 16:41:42,599 botocore.hooks [DEBUG] Event choose-signer.s3.CreateMultipartUpload: calling handler <bound method ClientCreator._default_s3_presign_to_sigv2 of <botocore.client.ClientCreator object at 0x7fdb5cabff98>>
2018-09-27 16:41:42,599 botocore.hooks [DEBUG] Event choose-signer.s3.CreateMultipartUpload: calling handler <function set_operation_specific_signer at 0x7fdb5e846b70>
2018-09-27 16:41:42,599 botocore.hooks [DEBUG] Event before-sign.s3.CreateMultipartUpload: calling handler <function fix_s3_host at 0x7fdb5e983048>
2018-09-27 16:41:42,600 botocore.utils [DEBUG] Checking for DNS compatible bucket for: https://s3.eu-west-1.amazonaws.com/www.xyz.com/test?uploads
2018-09-27 16:41:42,600 botocore.utils [DEBUG] Not changing URI, bucket is not DNS compatible: www.xyz.com
2018-09-27 16:41:42,601 botocore.auth [DEBUG] Calculating signature using v4 auth.
2018-09-27 16:41:42,601 botocore.auth [DEBUG] CanonicalRequest:
POST
/www.xyz.com/test
uploads=
content-type:application/octet-stream
host:s3.eu-west-1.amazonaws.com
x-amz-content-sha256:e3b0c44298fc1c149afbf343ddd27ae41e4649b934ca495991b7852b855
x-amz-date:20180927T164142Z

content-type;host;x-amz-content-sha256;x-amz-date
e3b0c44298fc1c149afb65gdfg33441e4649b934ca495991b7852b855
2018-09-27 16:41:42,601 botocore.auth [DEBUG] StringToSign:
AWS4-HMAC-SHA256
20180927T164142Z
20180927/eu-west-1/s3/aws4_request
8649ef591fb64412e923359a4sfvvffdd6d00915b9756d1611b38e346ae
2018-09-27 16:41:42,602 botocore.auth [DEBUG] Signature:
61db9afe5f87730a75692af5a95ggffdssd6f4e8e712d85c414edb14f
2018-09-27 16:41:42,602 botocore.endpoint [DEBUG] Sending http request: <AWSPreparedRequest stream_output=False, method=POST, url=https://s3.eu-west-1.amazonaws.com/www.xyz.com/test?uploads, headers={'Content-Type': b'application/octet-stream', 'User-Agent': b'Boto3/1.7.80 Python/3.6.6 Linux/4.14.67-66.56.amzn1.x86_64 Botocore/1.11.1 Resource', 'X-Amz-Date': b'20180927T164142Z', 'X-Amz-Content-SHA256': b'e3b0c44298fc1c149afbf4c8996fbdsdsffdss649b934ca495991b7852b855', 'Authorization': b'AWS4-HMAC-SHA256 Credential=X1234567890/20180927/eu-west-1/s3/aws4_request, SignedHeaders=content-type;host;x-amz-content-sha256;x-amz-date, Signature=61db9afe5f87730a7sdfsdfs20b7137cf5d6f4e8e712d85c414edb14f', 'Content-Length': '0'}>
2018-09-27 16:41:42,638 botocore.parsers [DEBUG] Response headers: {'x-amz-request-id': '9E879E78E4883471', 'x-amz-id-2': 'ZkCfOMwLoD08Yy4Nzfxsdfdsdfds3y9wLxzqFw+o3175I+QEdtdtAi8vIEH1vi9iq9VGUC98GqlE=', 'Content-Type': 'application/xml', 'Transfer-Encoding': 'chunked', 'Date': 'Thu, 27 Sep 2018 16:41:42 GMT', 'Server': 'AmazonS3'}
2018-09-27 16:41:42,639 botocore.parsers [DEBUG] Response body:
b'<?xml version="1.0" encoding="UTF-8"?>\n<Error><Code>AccessDenied</Code><Message>Access Denied</Message><RequestId>9E879E78E4883471</RequestId><HostId>ZkCfOMwLoD08Yy4Nzfxo8RpzsdfsdfsxzqFw+o3175I+QEdtdtAi8vIEH1vi9iq9VGUC98GqlE=</HostId></Error>'
2018-09-27 16:41:42,639 botocore.hooks [DEBUG] Event needs-retry.s3.CreateMultipartUpload: calling handler <botocore.retryhandler.RetryHandler object at 0x7fdb5c618ac8>
2018-09-27 16:41:42,640 botocore.retryhandler [DEBUG] No retry needed.
2018-09-27 16:41:42,640 botocore.hooks [DEBUG] Event needs-retry.s3.CreateMultipartUpload: calling handler <bound method S3RegionRedirector.redirect_from_error of <botocore.utils.S3RegionRedirector object at 0x7fdb5c5d1128>>
Traceback (most recent call last):
  File "<console>", line 1, in <module>
  File "/usr/local/lib/python3.6/dist-packages/storages/backends/s3boto3.py", line 127, in write
    self._multipart = self.obj.initiate_multipart_upload(**parameters)
  File "/usr/local/lib/python3.6/dist-packages/boto3/resources/factory.py", line 520, in do_action
    response = action(self, *args, **kwargs)
  File "/usr/local/lib/python3.6/dist-packages/boto3/resources/action.py", line 83, in __call__
    response = getattr(parent.meta.client, operation_name)(**params)
  File "/usr/local/lib/python3.6/dist-packages/botocore/client.py", line 314, in _api_call
    return self._make_api_call(operation_name, kwargs)
  File "/usr/local/lib/python3.6/dist-packages/botocore/client.py", line 612, in _make_api_call
    raise error_class(parsed_response, operation_name)
botocore.exceptions.ClientError: An error occurred (AccessDenied) when calling the CreateMultipartUpload operation: Access Denied

这些是我正在使用的版本。

boto3==1.7.80
botocore==1.11.1
Django==2.1
s3transfer==0.1.13
django-storages==1.7.1

为什么会引发异常?

事实证明,我必须指定一个策略来添加权限以使用存储桶下的任何对象 /*

之前

...
"Resource": [
            "arn:aws:s3:::www.xyz.com"
            ]
...

之后

...
"Resource": [
            "arn:aws:s3:::www.xyz.com/*"
            ]
...

我也遇到了这个错误,但我犯了一个不同的错误。 django-storages 函数正在创建 ACL 为 "public-read" 的对象。这是默认设置,这对 Web 框架有意义,而且确实是我想要的,但我没有在我的 IAM 策略中包含与 ACL 相关的权限。

  • PutObjectAcl
  • PutObjectVersionAcl

这个政策对我有用(它基于this one):

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "VisualEditor0",
            "Effect": "Allow",
            "Action": [
                "s3:PutObject",
                "s3:GetObject",
                "s3:ListBucketMultipartUploads",
                "s3:AbortMultipartUpload",
                "s3:PutObjectVersionAcl",
                "s3:DeleteObject",
                "s3:PutObjectAcl",
                "s3:ListMultipartUploadParts"
            ],
            "Resource": [
                "arn:aws:s3:::bucketname/*",
                "arn:aws:s3:::bucketname"
            ]
        },
        {
            "Sid": "VisualEditor1",
            "Effect": "Allow",
            "Action": [
                "s3:ListBucket",
                "s3:GetBucketLocation"
            ],
            "Resource": "arn:aws:s3:::bucketname"
        },
        {
            "Sid": "VisualEditor2",
            "Effect": "Allow",
            "Action": "s3:ListAllMyBuckets",
            "Resource": "*"
        }
    ]
}

另一个可能的原因是您的存储桶启用了加密。您需要添加 kms:GenerateDataKeykms:Decrypt 的第二个语句。这是我的声明:

        {
            "Sid": "VisualEditor1",
            "Effect": "Allow",
            "Action": [
               "kms:Decrypt",
               "kms:GenerateDataKey"
            ],
            "Resource": "*"
        }

请注意,我使用的是内置密钥,而不是 CMK。有关更多信息,请参阅 AWS docs here

仅供参考,另一个原因是您的目标存储桶没有正确的策略定义。

对于我的用例,我试图将 S3 文件从 AWS 账户 A 中的一个存储桶复制到 AWS 账户 B 中的另一个存储桶。我创建了一个启用此功能的角色和策略,但我没有添加存储桶策略允许外部 AWS 角色写入它。我能够通过关注此 AWS 文档站点来解决此问题:https://aws.amazon.com/premiumsupport/knowledge-center/copy-s3-objects-account/

(如果以上link有效则忽略)

如果上述 link 中断,该站点提到:

重要提示:Amazon S3 中的对象不再自动归上传它的 AWS 账户所有。默认情况下,任何新创建的存储桶现在都启用了存储桶所有者强制设置。在更改对象所有权时使用存储桶所有者强制设置也是最佳做法。但是,请注意,此选项会禁用所有存储桶 ACL 以及存储桶中任何对象的 ACL。

通过 S3 对象所有权中的存储桶所有者强制设置,Amazon S3 存储桶中的所有对象都自动归存储桶所有者所有。 Bucket 所有者强制执行的功能还禁用所有访问控制列表 (ACL),这简化了对存储在 S3 中的数据的访问管理。但是,对于现有存储桶,除非您明确禁用 ACL,否则 Amazon S3 对象仍归上传它的 AWS 账户所有。要更改现有存储桶中对象的对象所有权,请参阅如何更改我的 S3 存储桶中公有对象的所有权?

如果您现有的对象共享方法依赖于使用 ACL,请确定使用 ACL 访问对象的主体。有关如何在禁用任何 ACL 之前查看权限的详细信息,请参阅禁用 ACL 的先决条件。

如果您无法禁用 ACL,请按照以下步骤获取对象的所有权,直到您可以调整存储桶策略:

  1. 在源账户中,创建一个 AWS Identity and Access Management (IAM) 客户托管策略,授予 IAM 身份(用户或角色)适当的权限。 IAM 用户必须有权从源存储桶中检索对象并将对象放回目标存储桶。您可以使用类似于以下内容的 IAM 策略:

{ "版本": "2012-10-17", “陈述”: [ { "效果": "允许", “行动”: [ “s3:列表桶”, “s3:获取对象” ], “资源”:[ “arn:aws:s3:::source-DOC-EXAMPLE-BUCKET”, “arn:aws:s3:::source-DOC-EXAMPLE-BUCKET/” ] }, { "效果": "允许", “行动”: [ “s3:列表桶”, “s3:放置对象”, “s3:PutObjectAcl” ], “资源”:[ “arn:aws:s3:::destination-DOC-EXAMPLE-BUCKET”, “arn:aws:s3:::destination-DOC-EXAMPLE-BUCKET/” ] } ] } 注意:此示例 IAM 策略仅包含列出对象和跨不同账户中的存储桶复制对象所需的最低权限。您必须根据您的用例自定义允许的 S3 操作。例如,如果用户必须复制具有对象标签的对象,那么您还必须授予 s3:GetObjectTagging 的权限。如果遇到错误,请尝试以管理员用户身份执行这些步骤。

  1. 在源账户中,将客户托管策略附加到您要用于将对象复制到目标存储桶的 IAM 身份。

  2. 在目标账户中,将目标存储桶上的 S3 对象所有权设置为首选存储桶所有者。设置 S3 对象所有权后,上传的访问控制列表 (ACL) 设置为 bucket-owner-full-control 的新对象自动归存储桶帐户所有。

  3. 在目标账户中,修改目标bucket的bucket policy,授予源账户上传对象的权限。此外,在存储桶策略中包含一个条件,要求上传对象以将 ACL 设置为 bucket-owner-full-control。您可以使用类似于以下的语句:

注意:将 destination-DOC-EXAMPLE-BUCKET 替换为目标存储桶的名称。然后,将 arn:aws:iam::222222222222:user/Jane 替换为来自源账户的 IAM 身份的 Amazon 资源名称 (ARN)。

{ "版本": "2012-10-17", "Id": "Policy1611277539797", “陈述”: [ { "Sid": "Stmt1611277535086", "效果": "允许", “主要的”: { “AWS”:“arn:aws:iam::222222222222:user/Jane” }, “动作”:“s3:PutObject”, “资源”:“arn:aws:s3:::destination-DOC-EXAMPLE-BUCKET/*”, “健康)状况”: { “字符串等于”:{ "s3:x-amz-acl": "bucket-owner-full-control" } } }, { "Sid": "Stmt1611277877767", "效果": "允许", “主要的”: { “AWS”:“arn:aws:iam::222222222222:user/Jane” }, “动作”:“s3:ListBucket”, “资源”:“arn:aws:s3:::destination-DOC-EXAMPLE-BUCKET” } ] } 注意:这个例子存储桶策略仅包含上传具有所需 ACL 的对象所需的最低权限。您必须根据您的用例自定义允许的 S3 操作。例如,如果用户必须复制具有对象标签的对象,您还必须授予 s3:GetObjectTagging

权限
  1. 配置 IAM 策略和存储桶策略后,来自源帐户的 IAM 身份必须将对象上传到目标存储桶。确保 ACL 设置为 bucket-owner-full-control。例如,源 IAM 身份必须 运行 带有 --acl 选项的 cp AWS CLI 命令:

aws s3 cp s3://source-DOC-EXAMPLE-BUCKET/object.txt s3://destination-DOC-EXAMPLE-BUCKET/object.txt --acl bucket-owner-full-control