moto/moto/s3/models.py

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

2814 lines
101 KiB
Python
Raw Normal View History

2013-03-26 14:52:33 +00:00
import base64
import codecs
import copy
import datetime
2014-06-27 16:21:32 -06:00
import itertools
import json
import os
import string
import sys
import tempfile
import threading
import urllib.parse
from bisect import insort
from importlib import reload
from typing import Any, Dict, Iterator, List, Optional, Set, Tuple, Union
from moto.cloudwatch.models import MetricDatum
from moto.core.base_backend import BackendDict, BaseBackend
from moto.core.common_models import (
BaseModel,
CloudFormationModel,
CloudWatchMetricProvider,
)
from moto.core.utils import (
iso_8601_datetime_without_milliseconds_s3,
rfc_1123_datetime,
unix_time,
unix_time_millis,
utcnow,
)
from moto.moto_api._internal import mock_random as random
from moto.moto_api._internal.managed_state_model import ManagedState
from moto.s3.exceptions import (
2021-08-17 00:16:59 -05:00
AccessDeniedByLock,
BadRequest,
BucketAlreadyExists,
2021-08-17 00:16:59 -05:00
BucketNeedsToBeNew,
CopyObjectMustChangeSomething,
CrossLocationLoggingProhibitted,
EntityTooSmall,
HeadOnDeleteMarker,
InvalidBucketName,
InvalidNotificationDestination,
InvalidNotificationEvent,
InvalidPart,
InvalidPublicAccessBlockConfiguration,
InvalidRequest,
InvalidStorageClass,
InvalidTagError,
InvalidTargetBucketForLogging,
MalformedXML,
MissingBucket,
MissingKey,
2019-12-09 17:38:26 -08:00
NoSuchPublicAccessBlockConfiguration,
NoSuchUpload,
ObjectLockConfigurationNotFoundError,
)
from moto.utilities.tagging_service import TaggingService
from moto.utilities.utils import LowercaseDict, md5_hash
from ..events.notifications import send_notification as events_send_notification
from ..settings import (
S3_UPLOAD_PART_MIN_SIZE,
get_s3_default_key_buffer_size,
s3_allow_crossdomain_access,
)
from . import notifications
from .cloud_formation import cfn_to_api_encryption, is_replacement_update
from .select_object_content import parse_query
from .utils import (
ARCHIVE_STORAGE_CLASSES,
LOGGING_SERVICE_PRINCIPAL,
STORAGE_CLASS,
CaseInsensitiveDict,
_VersionedKeyStore,
compute_checksum,
)
2013-02-18 16:09:40 -05:00
MAX_BUCKET_NAME_LENGTH = 63
MIN_BUCKET_NAME_LENGTH = 3
UPLOAD_ID_BYTES = 43
DEFAULT_TEXT_ENCODING = sys.getdefaultencoding()
OWNER = "75aa57f09aa0c8caeab4f8c24e99d10f8e7faeebf76c078efc7c6caea54ba06a"
2013-11-15 11:53:39 +02:00
2013-02-18 16:09:40 -05:00
class FakeDeleteMarker(BaseModel):
2023-04-20 16:47:39 +00:00
def __init__(self, key: "FakeKey"):
self.key = key
self.name = key.name
self.last_modified = utcnow()
self._version_id = str(random.uuid4())
@property
2023-04-20 16:47:39 +00:00
def last_modified_ISO8601(self) -> str:
return iso_8601_datetime_without_milliseconds_s3(self.last_modified) # type: ignore
@property
2023-04-20 16:47:39 +00:00
def version_id(self) -> str:
return self._version_id
class FakeKey(BaseModel, ManagedState):
def __init__(
self,
2023-04-20 16:47:39 +00:00
name: str,
value: bytes,
account_id: str,
2023-04-20 16:47:39 +00:00
storage: Optional[str] = "STANDARD",
etag: Optional[str] = None,
is_versioned: bool = False,
version_id: Optional[str] = None,
2023-04-20 16:47:39 +00:00
max_buffer_size: Optional[int] = None,
multipart: Optional["FakeMultipart"] = None,
bucket_name: Optional[str] = None,
encryption: Optional[str] = None,
kms_key_id: Optional[str] = None,
bucket_key_enabled: Any = None,
lock_mode: Optional[str] = None,
lock_legal_status: Optional[str] = None,
lock_until: Optional[str] = None,
checksum_value: Optional[str] = None,
):
ManagedState.__init__(
self,
"s3::keyrestore",
transitions=[
(None, "IN_PROGRESS"),
("IN_PROGRESS", "RESTORED"),
],
)
2013-02-18 16:09:40 -05:00
self.name = name
2022-08-13 09:49:43 +00:00
self.account_id = account_id
self.last_modified = utcnow()
2023-04-20 16:47:39 +00:00
self.acl: Optional[FakeAcl] = get_canned_acl("private")
self.website_redirect_location: Optional[str] = None
self.checksum_algorithm = None
2023-04-20 16:47:39 +00:00
self._storage_class: Optional[str] = storage if storage else "STANDARD"
self._metadata = LowercaseDict()
2023-04-20 16:47:39 +00:00
self._expiry: Optional[datetime.datetime] = None
self._etag = etag
self._version_id = version_id
self._is_versioned = is_versioned
self.multipart = multipart
2020-03-31 12:04:04 +01:00
self.bucket_name = bucket_name
2013-05-17 19:41:39 -04:00
self._max_buffer_size = (
max_buffer_size if max_buffer_size else get_s3_default_key_buffer_size()
)
self._value_buffer = tempfile.SpooledTemporaryFile(self._max_buffer_size)
2022-10-12 21:08:01 +00:00
self.disposed = False
2023-04-20 16:47:39 +00:00
self.value = value # type: ignore
self.lock = threading.Lock()
self.encryption = encryption
self.kms_key_id = kms_key_id
self.bucket_key_enabled = bucket_key_enabled
2021-08-17 00:16:59 -05:00
self.lock_mode = lock_mode
self.lock_legal_status = lock_legal_status
self.lock_until = lock_until
2023-03-16 10:56:20 -01:00
self.checksum_value = checksum_value
2021-08-17 00:16:59 -05:00
# Default metadata values
self._metadata["Content-Type"] = "binary/octet-stream"
2023-04-20 16:47:39 +00:00
def safe_name(self, encoding_type: Optional[str] = None) -> str:
if encoding_type == "url":
2022-06-24 17:55:20 -05:00
return urllib.parse.quote(self.name)
return self.name
@property
def version_id(self) -> str:
return self._version_id or "null"
@property
2023-04-20 16:47:39 +00:00
def value(self) -> bytes:
with self.lock:
self._value_buffer.seek(0)
r = self._value_buffer.read()
r = copy.copy(r)
return r
2020-03-31 12:04:04 +01:00
@property
2023-04-20 16:47:39 +00:00
def arn(self) -> str:
2020-03-31 12:04:04 +01:00
# S3 Objects don't have an ARN, but we do need something unique when creating tags against this resource
return f"arn:aws:s3:::{self.bucket_name}/{self.name}/{self.version_id}"
2020-03-31 12:04:04 +01:00
2023-04-20 16:47:39 +00:00
@value.setter # type: ignore
def value(self, new_value: bytes) -> None:
2018-12-20 11:15:15 -08:00
self._value_buffer.seek(0)
self._value_buffer.truncate()
# Hack for working around moto's own unit tests; this probably won't
# actually get hit in normal use.
2021-07-26 07:40:39 +01:00
if isinstance(new_value, str):
new_value = new_value.encode(DEFAULT_TEXT_ENCODING)
2018-12-20 11:15:15 -08:00
self._value_buffer.write(new_value)
self.contentsize = len(new_value)
2023-04-20 16:47:39 +00:00
def set_metadata(self, metadata: Any, replace: bool = False) -> None:
if replace:
2023-04-20 16:47:39 +00:00
self._metadata = {} # type: ignore
self._metadata.update(metadata)
2023-04-20 16:47:39 +00:00
def set_storage_class(self, storage: Optional[str]) -> None:
if storage is not None and storage not in STORAGE_CLASS:
raise InvalidStorageClass(storage=storage)
self._storage_class = storage
2014-03-26 17:52:31 +02:00
2023-04-20 16:47:39 +00:00
def set_expiry(self, expiry: Optional[datetime.datetime]) -> None:
self._expiry = expiry
2023-04-20 16:47:39 +00:00
def set_acl(self, acl: Optional["FakeAcl"]) -> None:
2015-10-07 00:04:22 -07:00
self.acl = acl
2023-04-20 16:47:39 +00:00
def restore(self, days: int) -> None:
self._expiry = utcnow() + datetime.timedelta(days)
2014-03-26 19:15:08 +02:00
2013-02-18 16:09:40 -05:00
@property
2023-04-20 16:47:39 +00:00
def etag(self) -> str:
if self._etag is None:
value_md5 = md5_hash()
2018-12-20 11:15:15 -08:00
self._value_buffer.seek(0)
while True:
block = self._value_buffer.read(16 * 1024 * 1024) # read in 16MB chunks
if not block:
break
value_md5.update(block)
self._etag = value_md5.hexdigest()
return f'"{self._etag}"'
2013-02-18 16:09:40 -05:00
2013-03-29 17:45:33 -04:00
@property
2023-04-20 16:47:39 +00:00
def last_modified_ISO8601(self) -> str:
return iso_8601_datetime_without_milliseconds_s3(self.last_modified) # type: ignore
2013-03-29 17:45:33 -04:00
@property
2023-04-20 16:47:39 +00:00
def last_modified_RFC1123(self) -> str:
2013-03-29 17:45:33 -04:00
# Different datetime formats depending on how the key is obtained
# https://github.com/boto/boto/issues/466
2013-05-24 17:22:34 -04:00
return rfc_1123_datetime(self.last_modified)
2013-05-17 19:41:39 -04:00
@property
2023-04-20 16:47:39 +00:00
def metadata(self) -> LowercaseDict:
return self._metadata
2013-03-29 17:45:33 -04:00
@property
2023-04-20 16:47:39 +00:00
def response_dict(self) -> Dict[str, Any]: # type: ignore[misc]
res: Dict[str, Any] = {
"ETag": self.etag,
2013-03-29 17:45:33 -04:00
"last-modified": self.last_modified_RFC1123,
"content-length": str(self.size),
2013-03-29 17:45:33 -04:00
}
if self.encryption is not None:
res["x-amz-server-side-encryption"] = self.encryption
if self.encryption == "aws:kms" and self.kms_key_id is not None:
res["x-amz-server-side-encryption-aws-kms-key-id"] = self.kms_key_id
if self.encryption == "aws:kms" and self.bucket_key_enabled is not None:
res[
"x-amz-server-side-encryption-bucket-key-enabled"
] = self.bucket_key_enabled
2014-03-30 11:50:36 -04:00
if self._storage_class != "STANDARD":
res["x-amz-storage-class"] = self._storage_class
2014-03-26 19:15:08 +02:00
if self._expiry is not None:
if self.status == "IN_PROGRESS":
header = 'ongoing-request="true"'
else:
header = f'ongoing-request="false", expiry-date="{self.expiry_date}"'
res["x-amz-restore"] = header
if self._is_versioned:
res["x-amz-version-id"] = str(self.version_id)
if self.checksum_algorithm is not None:
res["x-amz-sdk-checksum-algorithm"] = self.checksum_algorithm
if self.website_redirect_location:
res["x-amz-website-redirect-location"] = self.website_redirect_location
if self.lock_legal_status:
res["x-amz-object-lock-legal-hold"] = self.lock_legal_status
if self.lock_until:
res["x-amz-object-lock-retain-until-date"] = self.lock_until
if self.lock_mode:
res["x-amz-object-lock-mode"] = self.lock_mode
if self.lock_legal_status:
res["x-amz-object-lock-legal-hold"] = self.lock_legal_status
if self.lock_until:
res["x-amz-object-lock-retain-until-date"] = self.lock_until
if self.lock_mode:
res["x-amz-object-lock-mode"] = self.lock_mode
2022-08-13 09:49:43 +00:00
tags = s3_backends[self.account_id]["global"].tagger.get_tag_dict_for_resource(
self.arn
)
2022-01-25 18:25:39 -01:00
if tags:
res["x-amz-tagging-count"] = str(len(tags.keys()))
return res
2013-03-29 17:45:33 -04:00
@property
2023-04-20 16:47:39 +00:00
def size(self) -> int:
return self.contentsize
2014-03-26 17:52:31 +02:00
@property
2023-04-20 16:47:39 +00:00
def storage_class(self) -> Optional[str]:
2014-03-30 11:50:36 -04:00
return self._storage_class
2014-03-26 17:52:31 +02:00
2014-03-26 19:15:08 +02:00
@property
2023-04-20 16:47:39 +00:00
def expiry_date(self) -> Optional[str]:
2014-03-26 19:15:08 +02:00
if self._expiry is not None:
return self._expiry.strftime("%a, %d %b %Y %H:%M:%S GMT")
2023-04-20 16:47:39 +00:00
return None
2014-03-26 19:15:08 +02:00
2018-12-20 11:15:15 -08:00
# Keys need to be pickleable due to some implementation details of boto3.
# Since file objects aren't pickleable, we need to override the default
# behavior. The following is adapted from the Python docs:
# https://docs.python.org/3/library/pickle.html#handling-stateful-objects
2023-04-20 16:47:39 +00:00
def __getstate__(self) -> Dict[str, Any]:
2018-12-20 11:15:15 -08:00
state = self.__dict__.copy()
try:
state["value"] = self.value
except ValueError:
# Buffer is already closed, so we can't reach the data
# Only happens if the key was deleted
state["value"] = ""
2018-12-20 11:15:15 -08:00
del state["_value_buffer"]
del state["lock"]
2018-12-20 11:15:15 -08:00
return state
2023-04-20 16:47:39 +00:00
def __setstate__(self, state: Dict[str, Any]) -> None:
2021-07-26 07:40:39 +01:00
self.__dict__.update({k: v for k, v in state.items() if k != "value"})
2018-12-20 11:15:15 -08:00
self._value_buffer = tempfile.SpooledTemporaryFile(
max_size=self._max_buffer_size
)
2023-04-20 16:47:39 +00:00
self.value = state["value"] # type: ignore
self.lock = threading.Lock()
2018-12-20 11:15:15 -08:00
2021-08-17 00:16:59 -05:00
@property
2023-04-20 16:47:39 +00:00
def is_locked(self) -> bool:
2021-08-17 00:16:59 -05:00
if self.lock_legal_status == "ON":
return True
if self.lock_mode == "COMPLIANCE":
now = utcnow()
2021-08-17 00:16:59 -05:00
try:
until = datetime.datetime.strptime(
2023-04-20 16:47:39 +00:00
self.lock_until, "%Y-%m-%dT%H:%M:%SZ" # type: ignore
2021-08-17 00:16:59 -05:00
)
except ValueError:
until = datetime.datetime.strptime(
2023-04-20 16:47:39 +00:00
self.lock_until, "%Y-%m-%dT%H:%M:%S.%fZ" # type: ignore
2021-08-17 00:16:59 -05:00
)
if until > now:
return True
return False
2023-04-20 16:47:39 +00:00
def dispose(self, garbage: bool = False) -> None:
2022-10-12 21:08:01 +00:00
if garbage and not self.disposed:
import warnings
warnings.warn("S3 key was not disposed of in time", ResourceWarning)
try:
self._value_buffer.close()
if self.multipart:
self.multipart.dispose()
except: # noqa: E722 Do not use bare except
pass
self.disposed = True
2023-04-20 16:47:39 +00:00
def __del__(self) -> None:
2022-10-12 21:08:01 +00:00
self.dispose(garbage=True)
2013-02-26 00:31:01 -05:00
2017-03-11 23:41:12 -05:00
class FakeMultipart(BaseModel):
def __init__(
self,
2023-04-20 16:47:39 +00:00
key_name: str,
metadata: CaseInsensitiveDict, # type: ignore
account_id: str,
2023-04-20 16:47:39 +00:00
storage: Optional[str] = None,
tags: Optional[Dict[str, str]] = None,
acl: Optional["FakeAcl"] = None,
sse_encryption: Optional[str] = None,
kms_key_id: Optional[str] = None,
):
2013-03-26 14:52:33 +00:00
self.key_name = key_name
self.metadata = metadata
self.account_id = account_id
self.storage = storage
self.tags = tags
2022-06-23 09:56:21 +00:00
self.acl = acl
2023-04-20 16:47:39 +00:00
self.parts: Dict[int, FakeKey] = {}
self.partlist: List[int] = [] # ordered list of part ID's
2014-08-26 13:25:50 -04:00
rand_b64 = base64.b64encode(os.urandom(UPLOAD_ID_BYTES))
self.id = (
rand_b64.decode("utf-8").replace("=", "").replace("+", "").replace("/", "")
)
self.sse_encryption = sse_encryption
self.kms_key_id = kms_key_id
def complete(
self, body: Iterator[Tuple[int, str]]
) -> Tuple[bytes, str, Optional[str]]:
checksum_algo = self.metadata.get("x-amz-checksum-algorithm")
2014-08-26 13:25:50 -04:00
decode_hex = codecs.getdecoder("hex_codec")
2013-03-26 14:52:33 +00:00
total = bytearray()
md5s = bytearray()
checksum = bytearray()
2013-03-26 14:52:33 +00:00
last = None
count = 0
for pn, etag in body:
part = self.parts.get(pn)
part_etag = None
if part is not None:
part_etag = part.etag.replace('"', "")
etag = etag.replace('"', "")
if part is None or part_etag != etag:
raise InvalidPart()
if last is not None and last.contentsize < S3_UPLOAD_PART_MIN_SIZE:
raise EntityTooSmall()
2023-04-20 16:47:39 +00:00
md5s.extend(decode_hex(part_etag)[0]) # type: ignore
2013-09-30 12:09:35 +03:00
total.extend(part.value)
if checksum_algo:
checksum.extend(
compute_checksum(part.value, checksum_algo, encode_base64=False)
)
last = part
count += 1
2013-03-26 14:52:33 +00:00
if count == 0:
raise MalformedXML
2023-04-20 16:47:39 +00:00
full_etag = md5_hash()
full_etag.update(bytes(md5s))
if checksum_algo:
encoded_checksum = compute_checksum(checksum, checksum_algo).decode("utf-8")
else:
encoded_checksum = None
return total, f"{full_etag.hexdigest()}-{count}", encoded_checksum
2013-03-26 14:52:33 +00:00
2023-04-20 16:47:39 +00:00
def set_part(self, part_id: int, value: bytes) -> FakeKey:
2013-03-26 14:52:33 +00:00
if part_id < 1:
raise NoSuchUpload(upload_id=part_id)
2013-03-26 14:52:33 +00:00
key = FakeKey(
part_id, value, account_id=self.account_id, encryption=self.sse_encryption, kms_key_id=self.kms_key_id # type: ignore
)
2022-10-12 21:08:01 +00:00
if part_id in self.parts:
# We're overwriting the current part - dispose of it first
self.parts[part_id].dispose()
self.parts[part_id] = key
if part_id not in self.partlist:
insort(self.partlist, part_id)
return key
2013-03-26 14:52:33 +00:00
2023-04-20 16:47:39 +00:00
def list_parts(self, part_number_marker: int, max_parts: int) -> Iterator[FakeKey]:
max_marker = part_number_marker + max_parts
for part_id in self.partlist[part_number_marker:max_marker]:
yield self.parts[part_id]
2013-09-30 12:09:35 +03:00
2023-04-20 16:47:39 +00:00
def dispose(self) -> None:
2022-10-12 21:08:01 +00:00
for part in self.parts.values():
part.dispose()
2013-03-26 14:52:33 +00:00
2017-03-11 23:41:12 -05:00
class FakeGrantee(BaseModel):
2023-04-20 16:47:39 +00:00
def __init__(self, grantee_id: str = "", uri: str = "", display_name: str = ""):
2022-01-14 18:51:49 -01:00
self.id = grantee_id
2015-10-07 00:04:22 -07:00
self.uri = uri
self.display_name = display_name
2023-04-20 16:47:39 +00:00
def __eq__(self, other: Any) -> bool:
2017-09-16 06:08:27 -07:00
if not isinstance(other, FakeGrantee):
return False
return (
self.id == other.id
and self.uri == other.uri
and self.display_name == other.display_name
2019-10-31 08:44:26 -07:00
)
2017-09-16 06:08:27 -07:00
2015-10-07 00:04:22 -07:00
@property
2023-04-20 16:47:39 +00:00
def type(self) -> str:
2015-10-07 00:04:22 -07:00
return "Group" if self.uri else "CanonicalUser"
2023-04-20 16:47:39 +00:00
def __repr__(self) -> str:
return f"FakeGrantee(display_name: '{self.display_name}', id: '{self.id}', uri: '{self.uri}')"
2017-09-16 06:08:27 -07:00
2015-10-07 00:04:22 -07:00
2017-02-23 21:37:43 -05:00
ALL_USERS_GRANTEE = FakeGrantee(uri="http://acs.amazonaws.com/groups/global/AllUsers")
AUTHENTICATED_USERS_GRANTEE = FakeGrantee(
uri="http://acs.amazonaws.com/groups/global/AuthenticatedUsers"
)
LOG_DELIVERY_GRANTEE = FakeGrantee(uri="http://acs.amazonaws.com/groups/s3/LogDelivery")
2015-10-07 00:04:22 -07:00
PERMISSION_FULL_CONTROL = "FULL_CONTROL"
PERMISSION_WRITE = "WRITE"
PERMISSION_READ = "READ"
PERMISSION_WRITE_ACP = "WRITE_ACP"
PERMISSION_READ_ACP = "READ_ACP"
CAMEL_CASED_PERMISSIONS = {
"FULL_CONTROL": "FullControl",
"WRITE": "Write",
"READ": "Read",
"WRITE_ACP": "WriteAcp",
"READ_ACP": "ReadAcp",
}
2015-10-07 00:04:22 -07:00
2017-03-11 23:41:12 -05:00
class FakeGrant(BaseModel):
2023-04-20 16:47:39 +00:00
def __init__(self, grantees: List[FakeGrantee], permissions: List[str]):
2015-10-07 00:04:22 -07:00
self.grantees = grantees
self.permissions = permissions
2023-04-20 16:47:39 +00:00
def __repr__(self) -> str:
return f"FakeGrant(grantees: {self.grantees}, permissions: {self.permissions})"
2017-09-16 06:08:27 -07:00
2015-10-07 00:04:22 -07:00
2017-03-11 23:41:12 -05:00
class FakeAcl(BaseModel):
2023-04-20 16:47:39 +00:00
def __init__(self, grants: Optional[List[FakeGrant]] = None):
self.grants = grants or []
2015-10-07 00:04:22 -07:00
2017-09-16 06:08:27 -07:00
@property
2023-04-20 16:47:39 +00:00
def public_read(self) -> bool:
2017-09-16 06:08:27 -07:00
for grant in self.grants:
if ALL_USERS_GRANTEE in grant.grantees:
if PERMISSION_READ in grant.permissions:
return True
if PERMISSION_FULL_CONTROL in grant.permissions:
return True
return False
2023-04-20 16:47:39 +00:00
def __repr__(self) -> str:
return f"FakeAcl(grants: {self.grants})"
2017-09-16 06:08:27 -07:00
2023-04-20 16:47:39 +00:00
def to_config_dict(self) -> Dict[str, Any]:
"""Returns the object into the format expected by AWS Config"""
2023-04-20 16:47:39 +00:00
data: Dict[str, Any] = {
"grantSet": None, # Always setting this to None. Feel free to change.
"owner": {"displayName": None, "id": OWNER},
}
# Add details for each Grant:
grant_list = []
for grant in self.grants:
permissions = (
grant.permissions
if isinstance(grant.permissions, list)
2023-04-20 16:47:39 +00:00
else [grant.permissions] # type: ignore
2019-10-31 08:44:26 -07:00
)
for permission in permissions:
for grantee in grant.grantees:
if grantee.uri:
grant_list.append(
2019-10-31 08:44:26 -07:00
{
"grantee": grantee.uri.split(
"http://acs.amazonaws.com/groups/s3/"
)[1],
"permission": CAMEL_CASED_PERMISSIONS[permission],
2019-10-31 08:44:26 -07:00
}
)
else:
grant_list.append(
{
2023-04-20 16:47:39 +00:00
"grantee": { # type: ignore
"id": grantee.id,
"displayName": None
if not grantee.display_name
else grantee.display_name,
},
"permission": CAMEL_CASED_PERMISSIONS[permission],
}
)
if grant_list:
data["grantList"] = grant_list
return data
2015-10-07 00:04:22 -07:00
2023-04-20 16:47:39 +00:00
def get_canned_acl(acl: str) -> FakeAcl:
2022-01-14 18:51:49 -01:00
owner_grantee = FakeGrantee(grantee_id=OWNER)
2015-10-07 00:04:22 -07:00
grants = [FakeGrant([owner_grantee], [PERMISSION_FULL_CONTROL])]
if acl == "private":
pass # no other permissions
elif acl == "public-read":
grants.append(FakeGrant([ALL_USERS_GRANTEE], [PERMISSION_READ]))
elif acl == "public-read-write":
2017-02-23 21:37:43 -05:00
grants.append(
FakeGrant([ALL_USERS_GRANTEE], [PERMISSION_READ, PERMISSION_WRITE])
2019-10-31 08:44:26 -07:00
)
2015-10-07 00:04:22 -07:00
elif acl == "authenticated-read":
2017-02-23 21:37:43 -05:00
grants.append(FakeGrant([AUTHENTICATED_USERS_GRANTEE], [PERMISSION_READ]))
2015-10-07 00:04:22 -07:00
elif acl == "bucket-owner-read":
pass # TODO: bucket owner ACL
elif acl == "bucket-owner-full-control":
pass # TODO: bucket owner ACL
elif acl == "aws-exec-read":
pass # TODO: bucket owner, EC2 Read
2015-10-07 00:04:22 -07:00
elif acl == "log-delivery-write":
2017-02-23 21:37:43 -05:00
grants.append(
FakeGrant([LOG_DELIVERY_GRANTEE], [PERMISSION_READ_ACP, PERMISSION_WRITE])
2019-10-31 08:44:26 -07:00
)
2015-10-07 00:04:22 -07:00
else:
assert False, f"Unknown canned acl: {acl}"
2015-10-07 00:04:22 -07:00
return FakeAcl(grants=grants)
class LifecycleFilter(BaseModel):
2023-04-20 16:47:39 +00:00
def __init__(
self,
prefix: Optional[str] = None,
tag: Optional[Tuple[str, str]] = None,
and_filter: Optional["LifecycleAndFilter"] = None,
):
self.prefix = prefix
2020-04-01 15:35:25 +01:00
(self.tag_key, self.tag_value) = tag if tag else (None, None)
self.and_filter = and_filter
2023-04-20 16:47:39 +00:00
def to_config_dict(self) -> Dict[str, Any]:
if self.prefix is not None:
return {
"predicate": {"type": "LifecyclePrefixPredicate", "prefix": self.prefix}
}
2020-04-01 15:35:25 +01:00
elif self.tag_key:
return {
"predicate": {
"type": "LifecycleTagPredicate",
2020-04-01 15:35:25 +01:00
"tag": {"key": self.tag_key, "value": self.tag_value},
}
}
else:
return {
"predicate": {
"type": "LifecycleAndOperator",
2023-04-20 16:47:39 +00:00
"operands": self.and_filter.to_config_dict(), # type: ignore
}
}
class LifecycleAndFilter(BaseModel):
2023-04-20 16:47:39 +00:00
def __init__(
self, prefix: Optional[str] = None, tags: Optional[Dict[str, str]] = None
):
self.prefix = prefix
2023-04-20 16:47:39 +00:00
self.tags = tags or {}
2023-04-20 16:47:39 +00:00
def to_config_dict(self) -> List[Dict[str, Any]]:
data: List[Dict[str, Any]] = []
if self.prefix is not None:
data.append({"type": "LifecyclePrefixPredicate", "prefix": self.prefix})
2020-04-01 15:35:25 +01:00
for key, value in self.tags.items():
data.append(
2021-08-17 00:16:59 -05:00
{"type": "LifecycleTagPredicate", "tag": {"key": key, "value": value}}
)
return data
class LifecycleTransition(BaseModel):
def __init__(
self,
date: Optional[str] = None,
days: Optional[int] = None,
storage_class: Optional[str] = None,
):
self.date = date
self.days = days
self.storage_class = storage_class
def to_config_dict(self) -> Dict[str, Any]:
config: Dict[str, Any] = {}
if self.date is not None:
config["date"] = self.date
if self.days is not None:
config["days"] = self.days
if self.storage_class is not None:
config["storageClass"] = self.storage_class
return config
class LifeCycleNoncurrentVersionTransition(BaseModel):
def __init__(
self, days: int, storage_class: str, newer_versions: Optional[int] = None
):
self.newer_versions = newer_versions
self.days = days
self.storage_class = storage_class
def to_config_dict(self) -> Dict[str, Any]:
config: Dict[str, Any] = {}
if self.newer_versions is not None:
config["newerNoncurrentVersions"] = self.newer_versions
if self.days is not None:
config["noncurrentDays"] = self.days
if self.storage_class is not None:
config["storageClass"] = self.storage_class
return config
2017-03-11 23:41:12 -05:00
class LifecycleRule(BaseModel):
def __init__(
self,
2023-04-20 16:47:39 +00:00
rule_id: Optional[str] = None,
prefix: Optional[str] = None,
lc_filter: Optional[LifecycleFilter] = None,
status: Optional[str] = None,
expiration_days: Optional[str] = None,
expiration_date: Optional[str] = None,
transitions: Optional[List[LifecycleTransition]] = None,
2023-04-20 16:47:39 +00:00
expired_object_delete_marker: Optional[str] = None,
nve_noncurrent_days: Optional[str] = None,
noncurrent_version_transitions: Optional[
List[LifeCycleNoncurrentVersionTransition]
] = None,
2023-04-20 16:47:39 +00:00
aimu_days: Optional[str] = None,
):
2022-01-14 18:51:49 -01:00
self.id = rule_id
2015-06-02 23:11:23 -04:00
self.prefix = prefix
self.filter = lc_filter
2015-06-02 23:11:23 -04:00
self.status = status
self.expiration_days = expiration_days
self.expiration_date = expiration_date
self.transitions = transitions
self.expired_object_delete_marker = expired_object_delete_marker
self.nve_noncurrent_days = nve_noncurrent_days
self.noncurrent_version_transitions = noncurrent_version_transitions
self.aimu_days = aimu_days
2015-06-02 23:11:23 -04:00
2023-04-20 16:47:39 +00:00
def to_config_dict(self) -> Dict[str, Any]:
"""Converts the object to the AWS Config data dict.
:param kwargs:
:return:
"""
2023-04-20 16:47:39 +00:00
lifecycle_dict: Dict[str, Any] = {
"id": self.id,
"prefix": self.prefix,
"status": self.status,
"expirationInDays": int(self.expiration_days)
if self.expiration_days
else None,
"expiredObjectDeleteMarker": self.expired_object_delete_marker,
2023-04-20 16:47:39 +00:00
"noncurrentVersionExpirationInDays": -1 or int(self.nve_noncurrent_days), # type: ignore
"expirationDate": self.expiration_date,
}
if self.transitions:
lifecycle_dict["transitions"] = [
t.to_config_dict() for t in self.transitions
]
else:
lifecycle_dict["transitions"] = None
if self.noncurrent_version_transitions:
lifecycle_dict["noncurrentVersionTransitions"] = [
t.to_config_dict() for t in self.noncurrent_version_transitions
]
else:
lifecycle_dict["noncurrentVersionTransitions"] = None
if self.aimu_days:
lifecycle_dict["abortIncompleteMultipartUpload"] = {
"daysAfterInitiation": self.aimu_days
}
else:
lifecycle_dict["abortIncompleteMultipartUpload"] = None
# Format the filter:
if self.prefix is None and self.filter is None:
lifecycle_dict["filter"] = {"predicate": None}
elif self.prefix:
lifecycle_dict["filter"] = None
else:
2023-04-20 16:47:39 +00:00
lifecycle_dict["filter"] = self.filter.to_config_dict() # type: ignore
return lifecycle_dict
2015-06-02 23:11:23 -04:00
class CorsRule(BaseModel):
def __init__(
self,
2023-04-20 16:47:39 +00:00
allowed_methods: Any,
allowed_origins: Any,
allowed_headers: Any = None,
expose_headers: Any = None,
max_age_seconds: Any = None,
):
self.allowed_methods = (
2021-07-26 07:40:39 +01:00
[allowed_methods] if isinstance(allowed_methods, str) else allowed_methods
2019-10-31 08:44:26 -07:00
)
self.allowed_origins = (
2021-07-26 07:40:39 +01:00
[allowed_origins] if isinstance(allowed_origins, str) else allowed_origins
2019-10-31 08:44:26 -07:00
)
self.allowed_headers = (
2021-07-26 07:40:39 +01:00
[allowed_headers] if isinstance(allowed_headers, str) else allowed_headers
2019-10-31 08:44:26 -07:00
)
self.exposed_headers = (
2021-07-26 07:40:39 +01:00
[expose_headers] if isinstance(expose_headers, str) else expose_headers
2019-10-31 08:44:26 -07:00
)
self.max_age_seconds = max_age_seconds
class Notification(BaseModel):
2023-04-20 16:47:39 +00:00
def __init__(
self,
arn: str,
events: List[str],
filters: Optional[Dict[str, Any]] = None,
notification_id: Optional[str] = None,
):
2022-01-14 18:51:49 -01:00
self.id = notification_id or "".join(
random.choice(string.ascii_letters + string.digits) for _ in range(50)
2019-10-31 08:44:26 -07:00
)
self.arn = arn
for event_name in events:
if not notifications.S3NotificationEvent.is_event_valid(event_name):
raise InvalidNotificationEvent(event_name)
self.events = events
self.filters = filters if filters else {}
2023-04-20 16:47:39 +00:00
def _event_matches(self, event_name: str) -> bool:
if event_name in self.events:
return True
# s3:ObjectCreated:Put --> s3:ObjectCreated:*
wildcard = ":".join(event_name.rsplit(":")[0:2]) + ":*"
if wildcard in self.events:
return True
return False
2023-04-20 16:47:39 +00:00
def _key_matches(self, key_name: str) -> bool:
if "S3Key" not in self.filters:
return True
_filters = {f["Name"]: f["Value"] for f in self.filters["S3Key"]["FilterRule"]}
prefix_matches = "prefix" not in _filters or key_name.startswith(
_filters["prefix"]
)
suffix_matches = "suffix" not in _filters or key_name.endswith(
_filters["suffix"]
)
return prefix_matches and suffix_matches
2023-04-20 16:47:39 +00:00
def matches(self, event_name: str, key_name: str) -> bool:
if self._event_matches(event_name):
if self._key_matches(key_name):
return True
return False
2023-04-20 16:47:39 +00:00
def to_config_dict(self) -> Dict[str, Any]:
# Type and ARN will be filled in by NotificationConfiguration's to_config_dict:
2023-04-20 16:47:39 +00:00
data: Dict[str, Any] = {"events": [event for event in self.events]}
if self.filters:
data["filter"] = {
"s3KeyFilter": {
"filterRules": [
{"name": fr["Name"], "value": fr["Value"]}
for fr in self.filters["S3Key"]["FilterRule"]
]
}
2019-10-31 08:44:26 -07:00
}
else:
data["filter"] = None
2019-12-09 17:38:26 -08:00
# Not sure why this is a thing since AWS just seems to return this as filters ¯\_(ツ)_/¯
data["objectPrefixes"] = []
return data
class NotificationConfiguration(BaseModel):
2023-04-20 16:47:39 +00:00
def __init__(
self,
topic: Optional[List[Dict[str, Any]]] = None,
queue: Optional[List[Dict[str, Any]]] = None,
cloud_function: Optional[List[Dict[str, Any]]] = None,
):
self.topic = (
[
Notification(
2022-01-14 18:51:49 -01:00
t["Topic"],
t["Event"],
filters=t.get("Filter"),
notification_id=t.get("Id"),
2019-10-31 08:44:26 -07:00
)
for t in topic
]
if topic
else []
2019-10-31 08:44:26 -07:00
)
self.queue = (
[
Notification(
2022-01-14 18:51:49 -01:00
q["Queue"],
q["Event"],
filters=q.get("Filter"),
notification_id=q.get("Id"),
2019-10-31 08:44:26 -07:00
)
for q in queue
]
if queue
else []
)
self.cloud_function = (
[
Notification(
c["CloudFunction"],
c["Event"],
filters=c.get("Filter"),
2022-01-14 18:51:49 -01:00
notification_id=c.get("Id"),
2019-10-31 08:44:26 -07:00
)
for c in cloud_function
]
if cloud_function
else []
2019-10-31 08:44:26 -07:00
)
2023-04-20 16:47:39 +00:00
def to_config_dict(self) -> Dict[str, Any]:
data: Dict[str, Any] = {"configurations": {}}
for topic in self.topic:
topic_config = topic.to_config_dict()
topic_config["topicARN"] = topic.arn
topic_config["type"] = "TopicConfiguration"
data["configurations"][topic.id] = topic_config
for queue in self.queue:
queue_config = queue.to_config_dict()
queue_config["queueARN"] = queue.arn
queue_config["type"] = "QueueConfiguration"
data["configurations"][queue.id] = queue_config
for cloud_function in self.cloud_function:
cf_config = cloud_function.to_config_dict()
cf_config["queueARN"] = cloud_function.arn
cf_config["type"] = "LambdaConfiguration"
data["configurations"][cloud_function.id] = cf_config
return data
2023-04-20 16:47:39 +00:00
def convert_str_to_bool(item: Any) -> bool:
2019-12-09 17:38:26 -08:00
"""Converts a boolean string to a boolean value"""
if isinstance(item, str):
return item.lower() == "true"
return False
class PublicAccessBlock(BaseModel):
def __init__(
self,
2023-04-20 16:47:39 +00:00
block_public_acls: Optional[str],
ignore_public_acls: Optional[str],
block_public_policy: Optional[str],
restrict_public_buckets: Optional[str],
2019-12-09 17:38:26 -08:00
):
# The boto XML appears to expect these values to exist as lowercase strings...
self.block_public_acls = block_public_acls or "false"
self.ignore_public_acls = ignore_public_acls or "false"
self.block_public_policy = block_public_policy or "false"
self.restrict_public_buckets = restrict_public_buckets or "false"
2023-04-20 16:47:39 +00:00
def to_config_dict(self) -> Dict[str, bool]:
2019-12-09 17:38:26 -08:00
# Need to make the string values booleans for Config:
return {
"blockPublicAcls": convert_str_to_bool(self.block_public_acls),
"ignorePublicAcls": convert_str_to_bool(self.ignore_public_acls),
"blockPublicPolicy": convert_str_to_bool(self.block_public_policy),
"restrictPublicBuckets": convert_str_to_bool(self.restrict_public_buckets),
}
2023-04-20 16:47:39 +00:00
class MultipartDict(Dict[str, FakeMultipart]):
def __delitem__(self, key: str) -> None:
2022-10-12 21:08:01 +00:00
if key in self:
self[key].dispose()
super().__delitem__(key)
class FakeBucket(CloudFormationModel):
2023-04-20 16:47:39 +00:00
def __init__(self, name: str, account_id: str, region_name: str):
2013-02-18 16:09:40 -05:00
self.name = name
2022-08-13 09:49:43 +00:00
self.account_id = account_id
2014-12-10 20:44:00 -05:00
self.region_name = region_name
self.keys = _VersionedKeyStore()
2022-10-12 21:08:01 +00:00
self.multiparts = MultipartDict()
2023-04-20 16:47:39 +00:00
self.versioning_status: Optional[str] = None
self.rules: List[LifecycleRule] = []
self.policy: Optional[bytes] = None
self.website_configuration: Optional[Dict[str, Any]] = None
self.acl: Optional[FakeAcl] = get_canned_acl("private")
self.cors: List[CorsRule] = []
self.logging: Dict[str, Any] = {}
self.notification_configuration: Optional[NotificationConfiguration] = None
self.accelerate_configuration: Optional[str] = None
self.payer = "BucketOwner"
2022-12-10 00:56:08 +01:00
self.creation_date = datetime.datetime.now(tz=datetime.timezone.utc)
2023-04-20 16:47:39 +00:00
self.public_access_block: Optional[PublicAccessBlock] = None
self.encryption: Optional[Dict[str, Any]] = None
2021-08-17 00:16:59 -05:00
self.object_lock_enabled = False
2023-04-20 16:47:39 +00:00
self.default_lock_mode: Optional[str] = ""
self.default_lock_days: Optional[int] = 0
self.default_lock_years: Optional[int] = 0
self.ownership_rule: Optional[Dict[str, Any]] = None
s3_backends.bucket_accounts[name] = account_id
2014-12-10 20:44:00 -05:00
@property
2023-04-20 16:47:39 +00:00
def location(self) -> str:
2014-12-10 20:44:00 -05:00
return self.region_name
@property
2023-04-20 16:47:39 +00:00
def creation_date_ISO8601(self) -> str:
return iso_8601_datetime_without_milliseconds_s3(self.creation_date) # type: ignore
@property
2023-04-20 16:47:39 +00:00
def is_versioned(self) -> bool:
return self.versioning_status == "Enabled"
2013-02-18 16:09:40 -05:00
2023-04-20 16:47:39 +00:00
def get_permission(self, action: str, resource: str) -> Any:
2022-09-17 11:46:42 +00:00
from moto.iam.access_control import IAMPolicy, PermissionResult
if self.policy is None:
return PermissionResult.NEUTRAL
iam_policy = IAMPolicy(self.policy.decode())
return iam_policy.is_action_permitted(action, resource)
2023-04-20 16:47:39 +00:00
def set_lifecycle(self, rules: List[Dict[str, Any]]) -> None:
2015-06-02 23:11:23 -04:00
self.rules = []
for rule in rules:
# Extract and validate actions from Lifecycle rule
2015-06-02 23:11:23 -04:00
expiration = rule.get("Expiration")
transitions_input = rule.get("Transition", [])
if transitions_input and not isinstance(transitions_input, list):
transitions_input = [rule.get("Transition")]
transitions = [
LifecycleTransition(
date=transition.get("Date"),
days=transition.get("Days"),
storage_class=transition.get("StorageClass"),
)
for transition in transitions_input
]
try:
top_level_prefix = (
rule["Prefix"] or ""
) # If it's `None` the set to the empty string
except KeyError:
top_level_prefix = None
nve_noncurrent_days = None
if rule.get("NoncurrentVersionExpiration") is not None:
2018-10-03 01:26:09 -05:00
if rule["NoncurrentVersionExpiration"].get("NoncurrentDays") is None:
raise MalformedXML()
nve_noncurrent_days = rule["NoncurrentVersionExpiration"][
"NoncurrentDays"
]
nv_transitions_input = rule.get("NoncurrentVersionTransition", [])
if nv_transitions_input and not isinstance(nv_transitions_input, list):
nv_transitions_input = [rule.get("NoncurrentVersionTransition")]
noncurrent_version_transitions = []
for nvt in nv_transitions_input:
if nvt.get("NoncurrentDays") is None or nvt.get("StorageClass") is None:
raise MalformedXML()
transition = LifeCycleNoncurrentVersionTransition(
newer_versions=nvt.get("NewerNoncurrentVersions"),
days=nvt.get("NoncurrentDays"),
storage_class=nvt.get("StorageClass"),
)
noncurrent_version_transitions.append(transition)
aimu_days = None
if rule.get("AbortIncompleteMultipartUpload") is not None:
2018-10-03 01:26:09 -05:00
if (
rule["AbortIncompleteMultipartUpload"].get("DaysAfterInitiation")
is None
):
raise MalformedXML()
aimu_days = rule["AbortIncompleteMultipartUpload"][
"DaysAfterInitiation"
]
eodm = None
if expiration and expiration.get("ExpiredObjectDeleteMarker") is not None:
# This cannot be set if Date or Days is set:
if expiration.get("Days") or expiration.get("Date"):
raise MalformedXML()
eodm = expiration["ExpiredObjectDeleteMarker"]
# Pull out the filter:
lc_filter = None
if rule.get("Filter"):
# Can't have both `Filter` and `Prefix` (need to check for the presence of the key):
try:
# 'Prefix' cannot be outside of a Filter:
if rule["Prefix"] or not rule["Prefix"]:
raise MalformedXML()
except KeyError:
pass
filters = 0
try:
prefix_filter = (
rule["Filter"]["Prefix"] or ""
) # If it's `None` the set to the empty string
filters += 1
except KeyError:
prefix_filter = None
and_filter = None
if rule["Filter"].get("And"):
filters += 1
2020-04-01 15:35:25 +01:00
and_tags = {}
if rule["Filter"]["And"].get("Tag"):
if not isinstance(rule["Filter"]["And"]["Tag"], list):
rule["Filter"]["And"]["Tag"] = [
rule["Filter"]["And"]["Tag"]
2019-10-31 08:44:26 -07:00
]
for t in rule["Filter"]["And"]["Tag"]:
2020-04-01 15:35:25 +01:00
and_tags[t["Key"]] = t.get("Value", "")
try:
and_prefix = (
rule["Filter"]["And"]["Prefix"] or ""
) # If it's `None` then set to the empty string
except KeyError:
and_prefix = None
and_filter = LifecycleAndFilter(prefix=and_prefix, tags=and_tags)
filter_tag = None
if rule["Filter"].get("Tag"):
filters += 1
2020-04-01 15:35:25 +01:00
filter_tag = (
rule["Filter"]["Tag"]["Key"],
rule["Filter"]["Tag"].get("Value", ""),
)
# Can't have more than 1 filter:
if filters > 1:
raise MalformedXML()
lc_filter = LifecycleFilter(
prefix=prefix_filter, tag=filter_tag, and_filter=and_filter
)
# If no top level prefix and no filter is present, then this is invalid:
if top_level_prefix is None:
try:
rule["Filter"]
except KeyError:
raise MalformedXML()
2015-06-02 23:11:23 -04:00
self.rules.append(
LifecycleRule(
2022-01-14 18:51:49 -01:00
rule_id=rule.get("ID"),
prefix=top_level_prefix,
lc_filter=lc_filter,
2015-06-02 23:11:23 -04:00
status=rule["Status"],
expiration_days=expiration.get("Days") if expiration else None,
expiration_date=expiration.get("Date") if expiration else None,
transitions=transitions,
expired_object_delete_marker=eodm,
nve_noncurrent_days=nve_noncurrent_days,
noncurrent_version_transitions=noncurrent_version_transitions,
aimu_days=aimu_days,
2019-10-31 08:44:26 -07:00
)
2015-06-02 23:11:23 -04:00
)
2023-04-20 16:47:39 +00:00
def delete_lifecycle(self) -> None:
2015-06-02 23:11:23 -04:00
self.rules = []
2023-04-20 16:47:39 +00:00
def set_cors(self, rules: List[Dict[str, Any]]) -> None:
self.cors = []
if len(rules) > 100:
raise MalformedXML()
for rule in rules:
assert isinstance(rule["AllowedMethod"], list) or isinstance(
2021-07-26 07:40:39 +01:00
rule["AllowedMethod"], str
)
assert isinstance(rule["AllowedOrigin"], list) or isinstance(
2021-07-26 07:40:39 +01:00
rule["AllowedOrigin"], str
2019-10-31 08:44:26 -07:00
)
assert isinstance(rule.get("AllowedHeader", []), list) or isinstance(
2021-07-26 07:40:39 +01:00
rule.get("AllowedHeader", ""), str
)
assert isinstance(rule.get("ExposeHeader", []), list) or isinstance(
rule.get("ExposeHeader", ""), str
)
2021-07-26 07:40:39 +01:00
assert isinstance(rule.get("MaxAgeSeconds", "0"), str)
2021-07-26 07:40:39 +01:00
if isinstance(rule["AllowedMethod"], str):
methods = [rule["AllowedMethod"]]
else:
methods = rule["AllowedMethod"]
for method in methods:
if method not in ["GET", "PUT", "HEAD", "POST", "DELETE"]:
raise InvalidRequest(method)
self.cors.append(
CorsRule(
rule["AllowedMethod"],
rule["AllowedOrigin"],
rule.get("AllowedHeader"),
rule.get("ExposeHeader"),
rule.get("MaxAgeSeconds"),
)
)
2023-04-20 16:47:39 +00:00
def delete_cors(self) -> None:
self.cors = []
@staticmethod
def _log_permissions_enabled_policy(
target_bucket: "FakeBucket", target_prefix: Optional[str]
) -> bool:
target_bucket_policy = target_bucket.policy
if target_bucket_policy:
target_bucket_policy_json = json.loads(target_bucket_policy.decode())
for stmt in target_bucket_policy_json["Statement"]:
if (
stmt.get("Principal", {}).get("Service")
!= LOGGING_SERVICE_PRINCIPAL
):
continue
if stmt.get("Effect", "") != "Allow":
continue
if "s3:PutObject" not in stmt.get("Action", []):
continue
if (
stmt.get("Resource")
!= f"arn:aws:s3:::{target_bucket.name}/{target_prefix if target_prefix else ''}*"
and stmt.get("Resource") != f"arn:aws:s3:::{target_bucket.name}/*"
and stmt.get("Resource") != f"arn:aws:s3:::{target_bucket.name}"
):
continue
return True
return False
@staticmethod
def _log_permissions_enabled_acl(target_bucket: "FakeBucket") -> bool:
write = read_acp = False
for grant in target_bucket.acl.grants: # type: ignore
# Must be granted to: http://acs.amazonaws.com/groups/s3/LogDelivery
for grantee in grant.grantees:
if grantee.uri == "http://acs.amazonaws.com/groups/s3/LogDelivery":
if (
"WRITE" in grant.permissions
or "FULL_CONTROL" in grant.permissions
):
write = True
if (
"READ_ACP" in grant.permissions
or "FULL_CONTROL" in grant.permissions
):
read_acp = True
break
return write and read_acp
def set_logging(
self, logging_config: Optional[Dict[str, Any]], bucket_backend: "S3Backend"
) -> None:
if not logging_config:
self.logging = {}
return
# Target bucket must exist in the same account (assuming all moto buckets are in the same account):
target_bucket = bucket_backend.buckets.get(logging_config["TargetBucket"])
if not target_bucket:
raise InvalidTargetBucketForLogging(
"The target bucket for logging does not exist."
)
target_prefix = self.logging.get("TargetPrefix", None)
has_policy_permissions = self._log_permissions_enabled_policy(
target_bucket=target_bucket, target_prefix=target_prefix
)
has_acl_permissions = self._log_permissions_enabled_acl(
target_bucket=target_bucket
)
if not (has_policy_permissions or has_acl_permissions):
raise InvalidTargetBucketForLogging(
"You must either provide the necessary permissions to the logging service using a bucket "
"policy or give the log-delivery group WRITE and READ_ACP permissions to the target bucket"
)
# Buckets must also exist within the same region:
if target_bucket.region_name != self.region_name:
raise CrossLocationLoggingProhibitted()
# Checks pass -- set the logging config:
self.logging = logging_config
2023-04-20 16:47:39 +00:00
def set_notification_configuration(
self, notification_config: Optional[Dict[str, Any]]
) -> None:
if not notification_config:
self.notification_configuration = None
return
self.notification_configuration = NotificationConfiguration(
topic=notification_config.get("TopicConfiguration"),
queue=notification_config.get("QueueConfiguration"),
cloud_function=notification_config.get("CloudFunctionConfiguration"),
)
# Validate that the region is correct:
for thing in ["topic", "queue", "cloud_function"]:
for t in getattr(self.notification_configuration, thing):
region = t.arn.split(":")[3]
if region != self.region_name:
raise InvalidNotificationDestination()
# Send test events so the user can verify these notifications were set correctly
2022-08-13 09:49:43 +00:00
notifications.send_test_event(account_id=self.account_id, bucket=self)
2023-04-20 16:47:39 +00:00
def set_accelerate_configuration(self, accelerate_config: str) -> None:
if self.accelerate_configuration is None and accelerate_config == "Suspended":
# Cannot "suspend" a not active acceleration. Leaves it undefined
return
self.accelerate_configuration = accelerate_config
@classmethod
2023-04-20 16:47:39 +00:00
def has_cfn_attr(cls, attr: str) -> bool:
return attr in [
"Arn",
"DomainName",
"DualStackDomainName",
"RegionalDomainName",
"WebsiteURL",
]
2023-04-20 16:47:39 +00:00
def get_cfn_attribute(self, attribute_name: str) -> Any:
from moto.cloudformation.exceptions import UnformattedGetAttTemplateException
2019-10-31 08:44:26 -07:00
if attribute_name == "Arn":
return self.arn
elif attribute_name == "DomainName":
return self.domain_name
elif attribute_name == "DualStackDomainName":
return self.dual_stack_domain_name
elif attribute_name == "RegionalDomainName":
return self.regional_domain_name
elif attribute_name == "WebsiteURL":
return self.website_url
raise UnformattedGetAttTemplateException()
2023-04-20 16:47:39 +00:00
def set_acl(self, acl: Optional[FakeAcl]) -> None:
2015-11-11 20:26:29 -05:00
self.acl = acl
2020-03-31 11:10:38 +01:00
@property
2023-04-20 16:47:39 +00:00
def arn(self) -> str:
return f"arn:aws:s3:::{self.name}"
2020-03-31 11:10:38 +01:00
@property
2023-04-20 16:47:39 +00:00
def domain_name(self) -> str:
return f"{self.name}.s3.amazonaws.com"
@property
2023-04-20 16:47:39 +00:00
def dual_stack_domain_name(self) -> str:
return f"{self.name}.s3.dualstack.{self.region_name}.amazonaws.com"
@property
2023-04-20 16:47:39 +00:00
def regional_domain_name(self) -> str:
return f"{self.name}.s3.{self.region_name}.amazonaws.com"
@property
2023-04-20 16:47:39 +00:00
def website_url(self) -> str:
return f"http://{self.name}.s3-website.{self.region_name}.amazonaws.com"
2016-08-15 10:57:40 -07:00
@property
2023-04-20 16:47:39 +00:00
def physical_resource_id(self) -> str:
2016-08-15 10:57:40 -07:00
return self.name
@staticmethod
2023-04-20 16:47:39 +00:00
def cloudformation_name_type() -> str:
return "BucketName"
@staticmethod
2023-04-20 16:47:39 +00:00
def cloudformation_type() -> str:
# https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-s3-bucket.html
return "AWS::S3::Bucket"
2016-08-15 10:57:40 -07:00
@classmethod
2023-04-20 16:47:39 +00:00
def create_from_cloudformation_json( # type: ignore[misc]
cls,
resource_name: str,
cloudformation_json: Any,
account_id: str,
region_name: str,
**kwargs: Any,
) -> "FakeBucket":
2022-08-13 09:49:43 +00:00
bucket = s3_backends[account_id]["global"].create_bucket(
resource_name, region_name
)
properties = cloudformation_json.get("Properties", {})
if "BucketEncryption" in properties:
bucket_encryption = cfn_to_api_encryption(properties["BucketEncryption"])
2022-08-13 09:49:43 +00:00
s3_backends[account_id]["global"].put_bucket_encryption(
bucket_name=resource_name, encryption=bucket_encryption
)
2016-08-15 10:57:40 -07:00
return bucket
@classmethod
2023-04-20 16:47:39 +00:00
def update_from_cloudformation_json( # type: ignore[misc]
2022-08-13 09:49:43 +00:00
cls,
2023-04-20 16:47:39 +00:00
original_resource: Any,
new_resource_name: str,
cloudformation_json: Any,
account_id: str,
region_name: str,
) -> "FakeBucket":
properties = cloudformation_json["Properties"]
if is_replacement_update(properties):
resource_name_property = cls.cloudformation_name_type()
if resource_name_property not in properties:
properties[resource_name_property] = new_resource_name
new_resource = cls.create_from_cloudformation_json(
2022-08-13 09:49:43 +00:00
properties[resource_name_property],
cloudformation_json,
account_id,
region_name,
)
properties[resource_name_property] = original_resource.name
cls.delete_from_cloudformation_json(
2022-08-13 09:49:43 +00:00
original_resource.name, cloudformation_json, account_id, region_name
)
return new_resource
else: # No Interruption
if "BucketEncryption" in properties:
bucket_encryption = cfn_to_api_encryption(
properties["BucketEncryption"]
)
2022-08-13 09:49:43 +00:00
s3_backends[account_id]["global"].put_bucket_encryption(
bucket_name=original_resource.name, encryption=bucket_encryption
)
return original_resource
@classmethod
2023-04-20 16:47:39 +00:00
def delete_from_cloudformation_json( # type: ignore[misc]
cls,
resource_name: str,
cloudformation_json: Any,
account_id: str,
region_name: str,
) -> None:
2022-08-13 09:49:43 +00:00
s3_backends[account_id]["global"].delete_bucket(resource_name)
2023-04-20 16:47:39 +00:00
def to_config_dict(self) -> Dict[str, Any]:
"""Return the AWS Config JSON format of this S3 bucket.
Note: The following features are not implemented and will need to be if you care about them:
- Bucket Accelerate Configuration
"""
2023-04-20 16:47:39 +00:00
config_dict: Dict[str, Any] = {
"version": "1.3",
"configurationItemCaptureTime": str(self.creation_date),
"configurationItemStatus": "ResourceDiscovered",
"configurationStateId": str(int(unix_time())),
"configurationItemMD5Hash": "",
2020-03-31 11:10:38 +01:00
"arn": self.arn,
"resourceType": "AWS::S3::Bucket",
"resourceId": self.name,
"resourceName": self.name,
"awsRegion": self.region_name,
"availabilityZone": "Regional",
"resourceCreationTime": str(self.creation_date),
"relatedEvents": [],
"relationships": [],
2022-08-13 09:49:43 +00:00
"tags": s3_backends[self.account_id][
"global"
].tagger.get_tag_dict_for_resource(self.arn),
"configuration": {
"name": self.name,
"owner": {"id": OWNER},
"creationDate": self.creation_date.isoformat(),
},
}
# Make the supplementary configuration:
# This is a dobule-wrapped JSON for some reason...
2023-04-20 16:47:39 +00:00
s_config: Dict[str, Any] = {
"AccessControlList": json.dumps(json.dumps(self.acl.to_config_dict())) # type: ignore
}
2019-12-09 17:38:26 -08:00
if self.public_access_block:
s_config["PublicAccessBlockConfiguration"] = json.dumps(
self.public_access_block.to_config_dict()
)
# Tagging is special:
if config_dict["tags"]:
s_config["BucketTaggingConfiguration"] = json.dumps(
{"tagSets": [{"tags": config_dict["tags"]}]}
)
# TODO implement Accelerate Configuration:
s_config["BucketAccelerateConfiguration"] = {"status": None}
if self.rules:
s_config["BucketLifecycleConfiguration"] = {
"rules": [rule.to_config_dict() for rule in self.rules]
}
s_config["BucketLoggingConfiguration"] = {
"destinationBucketName": self.logging.get("TargetBucket", None),
"logFilePrefix": self.logging.get("TargetPrefix", None),
}
s_config["BucketPolicy"] = {
"policyText": self.policy.decode("utf-8") if self.policy else None
}
s_config["IsRequesterPaysEnabled"] = (
"false" if self.payer == "BucketOwner" else "true"
2019-10-31 08:44:26 -07:00
)
if self.notification_configuration:
s_config[
"BucketNotificationConfiguration"
] = self.notification_configuration.to_config_dict()
else:
s_config["BucketNotificationConfiguration"] = {"configurations": {}}
config_dict["supplementaryConfiguration"] = s_config
return config_dict
2021-08-17 00:16:59 -05:00
@property
2023-04-20 16:47:39 +00:00
def has_default_lock(self) -> bool:
2021-08-17 00:16:59 -05:00
if not self.object_lock_enabled:
return False
if self.default_lock_mode:
return True
return False
2023-04-20 16:47:39 +00:00
def default_retention(self) -> str:
now = utcnow()
2023-04-20 16:47:39 +00:00
now += datetime.timedelta(self.default_lock_days) # type: ignore
now += datetime.timedelta(self.default_lock_years * 365) # type: ignore
2021-08-17 00:16:59 -05:00
return now.strftime("%Y-%m-%dT%H:%M:%SZ")
2013-02-18 16:09:40 -05:00
class S3Backend(BaseBackend, CloudWatchMetricProvider):
"""
Custom S3 endpoints are supported, if you are using a S3-compatible storage solution like Ceph.
Example usage:
.. sourcecode:: python
os.environ["MOTO_S3_CUSTOM_ENDPOINTS"] = "http://custom.internal.endpoint,http://custom.other.endpoint"
2023-04-23 15:59:22 +00:00
2024-01-07 12:03:33 +00:00
@mock_aws
def test_my_custom_endpoint():
boto3.client("s3", endpoint_url="http://custom.internal.endpoint")
...
Note that this only works if the environment variable is set **before** the mock is initialized.
2023-04-23 15:59:22 +00:00
_-_-_-_
2023-04-23 15:59:22 +00:00
When using the MultiPart-API manually, the minimum part size is 5MB, just as with AWS. Use the following environment variable to lower this:
.. sourcecode:: bash
S3_UPLOAD_PART_MIN_SIZE=256
_-_-_-_
CrossAccount access is allowed by default. If you want Moto to throw an AccessDenied-error when accessing a bucket in another account, use this environment variable:
.. sourcecode:: bash
MOTO_S3_ALLOW_CROSSACCOUNT_ACCESS=false
_-_-_-_
Install `moto[s3crc32c]` if you use the CRC32C algorithm, and absolutely need the correct value. Alternatively, you can install the `crc32c` dependency manually.
If this dependency is not installed, Moto will fall-back to the CRC32-computation when computing checksums.
"""
2023-04-20 16:47:39 +00:00
def __init__(self, region_name: str, account_id: str):
super().__init__(region_name, account_id)
2023-04-20 16:47:39 +00:00
self.buckets: Dict[str, FakeBucket] = {}
2020-03-31 11:10:38 +01:00
self.tagger = TaggingService()
self._pagination_tokens: Dict[str, str] = {}
2013-02-18 16:09:40 -05:00
2023-04-20 16:47:39 +00:00
def reset(self) -> None:
2022-10-09 12:22:46 +00:00
# For every key and multipart, Moto opens a TemporaryFile to write the value of those keys
# Ensure that these TemporaryFile-objects are closed, and leave no filehandles open
#
# First, check all known buckets/keys
for bucket in self.buckets.values():
2023-04-20 16:47:39 +00:00
for key in bucket.keys.values(): # type: ignore
if isinstance(key, FakeKey):
key.dispose()
for part in bucket.multiparts.values():
part.dispose()
s3_backends.bucket_accounts.pop(bucket.name, None)
#
# Second, go through the list of instances
# It may contain FakeKeys created earlier, which are no longer tracked
2023-04-20 16:47:39 +00:00
for mp in FakeMultipart.instances: # type: ignore
mp.dispose()
2023-04-20 16:47:39 +00:00
for key in FakeKey.instances: # type: ignore
key.dispose()
2022-10-09 12:22:46 +00:00
super().reset()
2023-04-20 16:47:39 +00:00
def log_incoming_request(self, request: Any, bucket_name: str) -> None:
2023-01-16 22:36:08 -01:00
"""
Process incoming requests
If the request is made to a bucket with logging enabled, logs will be persisted in the appropriate bucket
"""
try:
bucket = self.get_bucket(bucket_name)
target_bucket = bucket.logging["TargetBucket"]
prefix = bucket.logging.get("TargetPrefix", "")
now = datetime.datetime.now()
file_name = now.strftime(
f"%Y-%m-%d-%H-%M-%S-{random.get_random_hex(16).upper()}"
)
date = now.strftime("%d/%b/%Y:%H:%M:%S +0000")
source_ip = "0.0.0.0"
source_iam = "-" # Can be the user ARN, or empty
unknown_hex = random.get_random_hex(16)
source = f"REST.{request.method}.BUCKET" # REST/CLI/CONSOLE
key_name = "-"
path = urllib.parse.urlparse(request.url).path or "-"
http_line = f"{request.method} {path} HTTP/1.1"
response = '200 - - 1 2 "-"'
user_agent = f"{request.headers.get('User-Agent')} prompt/off command/s3api.put-object"
content = f"{random.get_random_hex(64)} originbucket [{date}] {source_ip} {source_iam} {unknown_hex} {source} {key_name} {http_line} {response} {user_agent} - c29tZSB1bmtub3duIGRhdGE= SigV4 ECDHE-RSA-AES128-GCM-SHA256 AuthHeader {request.url.split('amazonaws.com')[0]}amazonaws.com TLSv1.2 - -"
2023-04-20 16:47:39 +00:00
self.put_object(target_bucket, prefix + file_name, value=content) # type: ignore
2023-01-16 22:36:08 -01:00
except: # noqa: E722 Do not use bare except
# log delivery is not guaranteed in AWS, so if anything goes wrong, it's 'safe' to just ignore it
# Realistically, we should only get here when the bucket does not exist, or logging is not enabled
pass
@property
2023-04-20 16:47:39 +00:00
def _url_module(self) -> Any: # type: ignore
# The urls-property can be different depending on env variables
# Force a reload, to retrieve the correct set of URLs
import moto.s3.urls as backend_urls_module
reload(backend_urls_module)
return backend_urls_module
@staticmethod
2023-04-20 16:47:39 +00:00
def default_vpc_endpoint_service(
service_region: str, zones: List[str]
) -> List[Dict[str, str]]:
"""List of dicts representing default VPC endpoints for this service."""
accesspoint = {
"AcceptanceRequired": False,
"AvailabilityZones": zones,
"BaseEndpointDnsNames": [
f"accesspoint.s3-global.{service_region}.vpce.amazonaws.com",
],
"ManagesVpcEndpoints": False,
"Owner": "amazon",
"PrivateDnsName": "*.accesspoint.s3-global.amazonaws.com",
"PrivateDnsNameVerificationState": "verified",
"PrivateDnsNames": [
{"PrivateDnsName": "*.accesspoint.s3-global.amazonaws.com"}
],
"ServiceId": f"vpce-svc-{BaseBackend.vpce_random_number()}",
"ServiceName": "com.amazonaws.s3-global.accesspoint",
"ServiceType": [{"ServiceType": "Interface"}],
"Tags": [],
"VpcEndpointPolicySupported": True,
}
return (
BaseBackend.default_vpc_endpoint_service_factory(
service_region, zones, "s3", "Interface"
)
+ BaseBackend.default_vpc_endpoint_service_factory(
service_region, zones, "s3", "Gateway"
)
+ [accesspoint]
)
@classmethod
2023-04-20 16:47:39 +00:00
def get_cloudwatch_metrics(cls, account_id: str) -> List[MetricDatum]:
metrics = []
2022-08-13 09:49:43 +00:00
for name, bucket in s3_backends[account_id]["global"].buckets.items():
metrics.append(
MetricDatum(
namespace="AWS/S3",
name="BucketSizeBytes",
value=bucket.keys.item_size(),
dimensions=[
{"Name": "StorageType", "Value": "StandardStorage"},
{"Name": "BucketName", "Value": name},
],
2022-12-10 00:56:08 +01:00
timestamp=datetime.datetime.now(tz=datetime.timezone.utc).replace(
hour=0, minute=0, second=0, microsecond=0
),
unit="Bytes",
)
)
metrics.append(
MetricDatum(
namespace="AWS/S3",
name="NumberOfObjects",
value=len(bucket.keys),
dimensions=[
{"Name": "StorageType", "Value": "AllStorageTypes"},
{"Name": "BucketName", "Value": name},
],
2022-12-10 00:56:08 +01:00
timestamp=datetime.datetime.now(tz=datetime.timezone.utc).replace(
hour=0, minute=0, second=0, microsecond=0
),
unit="Count",
)
)
return metrics
2013-02-18 16:09:40 -05:00
2023-04-20 16:47:39 +00:00
def create_bucket(self, bucket_name: str, region_name: str) -> FakeBucket:
if bucket_name in s3_backends.bucket_accounts.keys():
raise BucketAlreadyExists(bucket=bucket_name)
if not MIN_BUCKET_NAME_LENGTH <= len(bucket_name) <= MAX_BUCKET_NAME_LENGTH:
raise InvalidBucketName()
2022-08-13 09:49:43 +00:00
new_bucket = FakeBucket(
name=bucket_name, account_id=self.account_id, region_name=region_name
)
2021-08-17 00:16:59 -05:00
2013-02-18 16:09:40 -05:00
self.buckets[bucket_name] = new_bucket
notification_detail = {
"version": "0",
"bucket": {"name": bucket_name},
"request-id": "N4N7GDK58NMKJ12R",
2022-08-13 09:49:43 +00:00
"requester": self.account_id,
"source-ip-address": "1.2.3.4",
"reason": "PutObject",
}
events_send_notification(
source="aws.s3",
event_name="CreateBucket",
region=region_name,
resources=[f"arn:aws:s3:::{bucket_name}"],
detail=notification_detail,
)
2013-02-18 16:09:40 -05:00
return new_bucket
2023-04-20 16:47:39 +00:00
def list_buckets(self) -> List[FakeBucket]:
return list(self.buckets.values())
2013-02-18 17:31:15 -05:00
2023-04-20 16:47:39 +00:00
def get_bucket(self, bucket_name: str) -> FakeBucket:
if bucket_name in self.buckets:
return self.buckets[bucket_name]
if bucket_name in s3_backends.bucket_accounts:
if not s3_allow_crossdomain_access():
raise AccessDeniedByLock
account_id = s3_backends.bucket_accounts[bucket_name]
return s3_backends[account_id]["global"].get_bucket(bucket_name)
raise MissingBucket(bucket=bucket_name)
2013-02-18 16:09:40 -05:00
def head_bucket(self, bucket_name: str) -> FakeBucket:
return self.get_bucket(bucket_name)
2023-04-20 16:47:39 +00:00
def delete_bucket(self, bucket_name: str) -> Optional[FakeBucket]:
bucket = self.get_bucket(bucket_name)
if bucket.keys:
# Can't delete a bucket with keys
2023-04-20 16:47:39 +00:00
return None
else:
s3_backends.bucket_accounts.pop(bucket_name, None)
return self.buckets.pop(bucket_name)
2013-02-18 17:17:19 -05:00
2023-04-20 16:47:39 +00:00
def put_bucket_versioning(self, bucket_name: str, status: str) -> None:
self.get_bucket(bucket_name).versioning_status = status
2023-04-20 16:47:39 +00:00
def get_bucket_versioning(self, bucket_name: str) -> Optional[str]:
return self.get_bucket(bucket_name).versioning_status
2023-04-20 16:47:39 +00:00
def get_bucket_encryption(self, bucket_name: str) -> Optional[Dict[str, Any]]:
return self.get_bucket(bucket_name).encryption
def list_object_versions(
2023-04-20 16:47:39 +00:00
self,
bucket_name: str,
delimiter: Optional[str] = None,
key_marker: Optional[str] = None,
max_keys: Optional[int] = 1000,
2023-04-20 16:47:39 +00:00
prefix: str = "",
version_id_marker: Optional[str] = None,
) -> Tuple[
List[FakeKey], List[str], List[FakeDeleteMarker], Optional[str], Optional[str]
]:
bucket = self.get_bucket(bucket_name)
2014-06-27 16:21:32 -06:00
common_prefixes: Set[str] = set()
2023-04-20 16:47:39 +00:00
requested_versions: List[FakeKey] = []
delete_markers: List[FakeDeleteMarker] = []
next_key_marker: Optional[str] = None
next_version_id_marker: Optional[str] = None
2023-04-20 16:47:39 +00:00
all_versions = list(
2023-09-06 19:02:58 +09:00
itertools.chain(
*(
copy.deepcopy(version_key)
for key, version_key in bucket.keys.iterlists()
)
)
2019-10-31 08:44:26 -07:00
)
# sort by name, revert last-modified-date
all_versions.sort(key=lambda r: (r.name, -unix_time_millis(r.last_modified)))
last_name = None
skip_versions = True
last_item_added: Union[None, FakeKey, FakeDeleteMarker] = None
for version in all_versions:
# Pagination
if skip_versions:
if key_marker is None:
# If KeyMarker is not supplied, we do not skip anything
skip_versions = False
elif not version_id_marker:
# Only KeyMarker is supplied, and it will be set to the last item of the previous page
# We skip all versions with ``name < key_marker``
# Because our list is ordered, we keep everything where ``name >= key_marker`` (i.e.: the next page)
skip_versions = version.name < key_marker
continue
elif (
version.name == key_marker
and version.version_id == version_id_marker
):
# KeyMarker and VersionIdMarker are set to the last item of the previous page
# Which means we should still skip the current version
# But continue processing all subsequent versions
skip_versions = False
continue
else:
continue
name = version.name
# guaranteed to be sorted - so the first key with this name will be the latest
version.is_latest = name != last_name
if version.is_latest:
last_name = name
# Filter for keys that start with prefix
if not name.startswith(prefix):
continue
# separate keys that contain the same string between the prefix and the first occurrence of the delimiter
is_common_prefix = False
if delimiter and delimiter in name[len(prefix) :]:
end_of_delimiter = (
len(prefix) + name[len(prefix) :].index(delimiter) + len(delimiter)
)
prefix_including_delimiter = name[0:end_of_delimiter]
# Skip already-processed common prefix.
if prefix_including_delimiter == key_marker:
continue
common_prefixes.add(prefix_including_delimiter)
name = prefix_including_delimiter
is_common_prefix = True
elif last_item_added:
name = last_item_added.name
# Only return max_keys items.
if (
max_keys is not None
and len(requested_versions) + len(delete_markers) + len(common_prefixes)
>= max_keys
):
next_key_marker = name
if is_common_prefix:
# No NextToken when returning common prefixes
next_version_id_marker = None
elif last_item_added is not None:
# NextToken is set to the (version of the) latest item
next_version_id_marker = last_item_added.version_id
else:
# Should only happen when max_keys == 0, so when we do not have a last item
next_version_id_marker = None
break
if not is_common_prefix:
last_item_added = version
# Differentiate between FakeKey and FakeDeleteMarkers
if not isinstance(version, FakeKey):
delete_markers.append(version)
else:
requested_versions.append(version)
return (
requested_versions,
sorted(common_prefixes),
delete_markers,
next_key_marker,
next_version_id_marker,
)
2023-04-20 16:47:39 +00:00
def get_bucket_policy(self, bucket_name: str) -> Optional[bytes]:
2015-07-23 17:33:52 -04:00
return self.get_bucket(bucket_name).policy
2023-04-20 16:47:39 +00:00
def put_bucket_policy(self, bucket_name: str, policy: bytes) -> None:
"""
Basic policy enforcement is in place.
Restrictions:
- Only statements with principal=* are taken into account
- Conditions are not taken into account
"""
2015-07-23 17:33:52 -04:00
self.get_bucket(bucket_name).policy = policy
2023-04-20 16:47:39 +00:00
def delete_bucket_policy(self, bucket_name: str) -> None:
bucket = self.get_bucket(bucket_name)
bucket.policy = None
2023-04-20 16:47:39 +00:00
def put_bucket_encryption(
self, bucket_name: str, encryption: Dict[str, Any]
) -> None:
self.get_bucket(bucket_name).encryption = encryption
2023-04-20 16:47:39 +00:00
def delete_bucket_encryption(self, bucket_name: str) -> None:
self.get_bucket(bucket_name).encryption = None
2023-04-20 16:47:39 +00:00
def get_bucket_ownership_controls(
self, bucket_name: str
) -> Optional[Dict[str, Any]]:
return self.get_bucket(bucket_name).ownership_rule
2023-04-20 16:47:39 +00:00
def put_bucket_ownership_controls(
self, bucket_name: str, ownership: Dict[str, Any]
) -> None:
self.get_bucket(bucket_name).ownership_rule = ownership
2023-04-20 16:47:39 +00:00
def delete_bucket_ownership_controls(self, bucket_name: str) -> None:
self.get_bucket(bucket_name).ownership_rule = None
2023-04-20 16:47:39 +00:00
def get_bucket_replication(self, bucket_name: str) -> Optional[Dict[str, Any]]:
bucket = self.get_bucket(bucket_name)
return getattr(bucket, "replication", None)
2023-04-20 16:47:39 +00:00
def put_bucket_replication(
self, bucket_name: str, replication: Dict[str, Any]
) -> None:
if isinstance(replication["Rule"], dict):
replication["Rule"] = [replication["Rule"]]
for rule in replication["Rule"]:
if "Priority" not in rule:
rule["Priority"] = 1
if "ID" not in rule:
rule["ID"] = "".join(
random.choice(string.ascii_letters + string.digits)
for _ in range(30)
)
bucket = self.get_bucket(bucket_name)
2023-04-20 16:47:39 +00:00
bucket.replication = replication # type: ignore
2023-04-20 16:47:39 +00:00
def delete_bucket_replication(self, bucket_name: str) -> None:
bucket = self.get_bucket(bucket_name)
2023-04-20 16:47:39 +00:00
bucket.replication = None # type: ignore
2023-04-20 16:47:39 +00:00
def put_bucket_lifecycle(
self, bucket_name: str, rules: List[Dict[str, Any]]
) -> None:
2015-06-02 23:11:23 -04:00
bucket = self.get_bucket(bucket_name)
bucket.set_lifecycle(rules)
2023-04-20 16:47:39 +00:00
def delete_bucket_lifecycle(self, bucket_name: str) -> None:
bucket = self.get_bucket(bucket_name)
bucket.delete_lifecycle()
2023-04-20 16:47:39 +00:00
def set_bucket_website_configuration(
self, bucket_name: str, website_configuration: Dict[str, Any]
) -> None:
bucket = self.get_bucket(bucket_name)
Merge LocalStack changes into upstream moto (#4082) * fix OPTIONS requests on non-existing API GW integrations * add cloudformation models for API Gateway deployments * bump version * add backdoor to return CloudWatch metrics * Updating implementation coverage * Updating implementation coverage * add cloudformation models for API Gateway deployments * Updating implementation coverage * Updating implementation coverage * Implemented get-caller-identity returning real data depending on the access key used. * bump version * minor fixes * fix Number data_type for SQS message attribute * fix handling of encoding errors * bump version * make CF stack queryable before starting to initialize its resources * bump version * fix integration_method for API GW method integrations * fix undefined status in CF FakeStack * Fix apigateway issues with terraform v0.12.21 * resource_methods -> add handle for "DELETE" method * integrations -> fix issue that "httpMethod" wasn't included in body request (this value was set as the value from refer method resource) * bump version * Fix setting http method for API gateway integrations (#6) * bump version * remove duplicate methods * add storage class to S3 Key when completing multipart upload (#7) * fix SQS performance issues; bump version * add pagination to SecretsManager list-secrets (#9) * fix default parameter groups in RDS * fix adding S3 metadata headers with names containing dots (#13) * Updating implementation coverage * Updating implementation coverage * add cloudformation models for API Gateway deployments * Updating implementation coverage * Updating implementation coverage * Implemented get-caller-identity returning real data depending on the access key used. * make CF stack queryable before starting to initialize its resources * bump version * remove duplicate methods * fix adding S3 metadata headers with names containing dots (#13) * Update amis.json to support EKS AMI mocks (#15) * fix PascalCase for boolean value in ListMultipartUploads response (#17); fix _get_multi_param to parse nested list/dict query params * determine non-zero container exit code in Batch API * support filtering by dimensions in CW get_metric_statistics * fix storing attributes for ELBv2 Route entities; API GW refactorings for TF tests * add missing fields for API GW resources * fix error messages for Route53 (TF-compat) * various fixes for IAM resources (tf-compat) * minor fixes for API GW models (tf-compat) * minor fixes for API GW responses (tf-compat) * add s3 exception for bucket notification filter rule validation * change the way RESTErrors generate the response body and content-type header * fix lint errors and disable "black" syntax enforcement * remove return type hint in RESTError.get_body * add RESTError XML template for IAM exceptions * add support for API GW minimumCompressionSize * fix casing getting PrivateDnsEnabled API GW attribute * minor fixes for error responses * fix escaping special chars for IAM role descriptions (tf-compat) * minor fixes and tagging support for API GW and ELB v2 (tf-compat) * Merge branch 'master' into localstack * add "AlarmRule" attribute to enable support for composite CloudWatch metrics * fix recursive parsing of complex/nested query params * bump version * add API to delete S3 website configurations (#18) * use dict copy to allow parallelism and avoid concurrent modification exceptions in S3 * fix precondition check for etags in S3 (#19) * minor fix for user filtering in Cognito * fix API Gateway error response; avoid returning empty response templates (tf-compat) * support tags and tracingEnabled attribute for API GW stages * fix boolean value in S3 encryption response (#20) * fix connection arn structure * fix api destination arn structure * black format * release 2.0.3.37 * fix s3 exception tests see botocore/parsers.py:1002 where RequestId is removed from parsed * remove python 2 from build action * add test failure annotations in build action * fix events test arn comparisons * fix s3 encryption response test * return default value "0" if EC2 availableIpAddressCount is empty * fix extracting SecurityGroupIds for EC2 VPC endpoints * support deleting/updating API Gateway DomainNames * fix(events): Return empty string instead of null when no pattern is specified in EventPattern (tf-compat) (#22) * fix logic and revert CF changes to get tests running again (#21) * add support for EC2 customer gateway API (#25) * add support for EC2 Transit Gateway APIs (#24) * feat(logs): add `kmsKeyId` into `LogGroup` entity (#23) * minor change in ELBv2 logic to fix tests * feat(events): add APIs to describe and delete CloudWatch Events connections (#26) * add support for EC2 transit gateway route tables (#27) * pass transit gateway route table ID in Describe API, minor refactoring (#29) * add support for EC2 Transit Gateway Routes (#28) * fix region on ACM certificate import (#31) * add support for EC2 transit gateway attachments (#30) * add support for EC2 Transit Gateway VPN attachments (#32) * fix account ID for logs API * add support for DeleteOrganization API * feat(events): store raw filter representation for CloudWatch events patterns (tf-compat) (#36) * feat(events): add support to describe/update/delete CloudWatch API destinations (#35) * add Cognito UpdateIdentityPool, CW Logs PutResourcePolicy * feat(events): add support for tags in EventBus API (#38) * fix parameter validation for Batch compute environments (tf-compat) * revert merge conflicts in IMPLEMENTATION_COVERAGE.md * format code using black * restore original README; re-enable and fix CloudFormation tests * restore tests and old logic for CF stack parameters from SSM * parameterize RequestId/RequestID in response messages and revert related test changes * undo LocalStack-specific adaptations * minor fix * Update CodeCov config to reflect removal of Py2 * undo change related to CW metric filtering; add additional test for CW metric statistics with dimensions * Terraform - Extend whitelist of running tests Co-authored-by: acsbendi <acsbendi28@gmail.com> Co-authored-by: Phan Duong <duongpv@outlook.com> Co-authored-by: Thomas Rausch <thomas@thrau.at> Co-authored-by: Macwan Nevil <macnev2013@gmail.com> Co-authored-by: Dominik Schubert <dominik.schubert91@gmail.com> Co-authored-by: Gonzalo Saad <saad.gonzalo.ale@gmail.com> Co-authored-by: Mohit Alonja <monty16597@users.noreply.github.com> Co-authored-by: Miguel Gagliardo <migag9@gmail.com> Co-authored-by: Bert Blommers <info@bertblommers.nl>
2021-07-26 16:21:17 +02:00
bucket.website_configuration = website_configuration
2023-04-20 16:47:39 +00:00
def get_bucket_website_configuration(
self, bucket_name: str
) -> Optional[Dict[str, Any]]:
bucket = self.get_bucket(bucket_name)
return bucket.website_configuration
2023-04-20 16:47:39 +00:00
def delete_bucket_website(self, bucket_name: str) -> None:
Merge LocalStack changes into upstream moto (#4082) * fix OPTIONS requests on non-existing API GW integrations * add cloudformation models for API Gateway deployments * bump version * add backdoor to return CloudWatch metrics * Updating implementation coverage * Updating implementation coverage * add cloudformation models for API Gateway deployments * Updating implementation coverage * Updating implementation coverage * Implemented get-caller-identity returning real data depending on the access key used. * bump version * minor fixes * fix Number data_type for SQS message attribute * fix handling of encoding errors * bump version * make CF stack queryable before starting to initialize its resources * bump version * fix integration_method for API GW method integrations * fix undefined status in CF FakeStack * Fix apigateway issues with terraform v0.12.21 * resource_methods -> add handle for "DELETE" method * integrations -> fix issue that "httpMethod" wasn't included in body request (this value was set as the value from refer method resource) * bump version * Fix setting http method for API gateway integrations (#6) * bump version * remove duplicate methods * add storage class to S3 Key when completing multipart upload (#7) * fix SQS performance issues; bump version * add pagination to SecretsManager list-secrets (#9) * fix default parameter groups in RDS * fix adding S3 metadata headers with names containing dots (#13) * Updating implementation coverage * Updating implementation coverage * add cloudformation models for API Gateway deployments * Updating implementation coverage * Updating implementation coverage * Implemented get-caller-identity returning real data depending on the access key used. * make CF stack queryable before starting to initialize its resources * bump version * remove duplicate methods * fix adding S3 metadata headers with names containing dots (#13) * Update amis.json to support EKS AMI mocks (#15) * fix PascalCase for boolean value in ListMultipartUploads response (#17); fix _get_multi_param to parse nested list/dict query params * determine non-zero container exit code in Batch API * support filtering by dimensions in CW get_metric_statistics * fix storing attributes for ELBv2 Route entities; API GW refactorings for TF tests * add missing fields for API GW resources * fix error messages for Route53 (TF-compat) * various fixes for IAM resources (tf-compat) * minor fixes for API GW models (tf-compat) * minor fixes for API GW responses (tf-compat) * add s3 exception for bucket notification filter rule validation * change the way RESTErrors generate the response body and content-type header * fix lint errors and disable "black" syntax enforcement * remove return type hint in RESTError.get_body * add RESTError XML template for IAM exceptions * add support for API GW minimumCompressionSize * fix casing getting PrivateDnsEnabled API GW attribute * minor fixes for error responses * fix escaping special chars for IAM role descriptions (tf-compat) * minor fixes and tagging support for API GW and ELB v2 (tf-compat) * Merge branch 'master' into localstack * add "AlarmRule" attribute to enable support for composite CloudWatch metrics * fix recursive parsing of complex/nested query params * bump version * add API to delete S3 website configurations (#18) * use dict copy to allow parallelism and avoid concurrent modification exceptions in S3 * fix precondition check for etags in S3 (#19) * minor fix for user filtering in Cognito * fix API Gateway error response; avoid returning empty response templates (tf-compat) * support tags and tracingEnabled attribute for API GW stages * fix boolean value in S3 encryption response (#20) * fix connection arn structure * fix api destination arn structure * black format * release 2.0.3.37 * fix s3 exception tests see botocore/parsers.py:1002 where RequestId is removed from parsed * remove python 2 from build action * add test failure annotations in build action * fix events test arn comparisons * fix s3 encryption response test * return default value "0" if EC2 availableIpAddressCount is empty * fix extracting SecurityGroupIds for EC2 VPC endpoints * support deleting/updating API Gateway DomainNames * fix(events): Return empty string instead of null when no pattern is specified in EventPattern (tf-compat) (#22) * fix logic and revert CF changes to get tests running again (#21) * add support for EC2 customer gateway API (#25) * add support for EC2 Transit Gateway APIs (#24) * feat(logs): add `kmsKeyId` into `LogGroup` entity (#23) * minor change in ELBv2 logic to fix tests * feat(events): add APIs to describe and delete CloudWatch Events connections (#26) * add support for EC2 transit gateway route tables (#27) * pass transit gateway route table ID in Describe API, minor refactoring (#29) * add support for EC2 Transit Gateway Routes (#28) * fix region on ACM certificate import (#31) * add support for EC2 transit gateway attachments (#30) * add support for EC2 Transit Gateway VPN attachments (#32) * fix account ID for logs API * add support for DeleteOrganization API * feat(events): store raw filter representation for CloudWatch events patterns (tf-compat) (#36) * feat(events): add support to describe/update/delete CloudWatch API destinations (#35) * add Cognito UpdateIdentityPool, CW Logs PutResourcePolicy * feat(events): add support for tags in EventBus API (#38) * fix parameter validation for Batch compute environments (tf-compat) * revert merge conflicts in IMPLEMENTATION_COVERAGE.md * format code using black * restore original README; re-enable and fix CloudFormation tests * restore tests and old logic for CF stack parameters from SSM * parameterize RequestId/RequestID in response messages and revert related test changes * undo LocalStack-specific adaptations * minor fix * Update CodeCov config to reflect removal of Py2 * undo change related to CW metric filtering; add additional test for CW metric statistics with dimensions * Terraform - Extend whitelist of running tests Co-authored-by: acsbendi <acsbendi28@gmail.com> Co-authored-by: Phan Duong <duongpv@outlook.com> Co-authored-by: Thomas Rausch <thomas@thrau.at> Co-authored-by: Macwan Nevil <macnev2013@gmail.com> Co-authored-by: Dominik Schubert <dominik.schubert91@gmail.com> Co-authored-by: Gonzalo Saad <saad.gonzalo.ale@gmail.com> Co-authored-by: Mohit Alonja <monty16597@users.noreply.github.com> Co-authored-by: Miguel Gagliardo <migag9@gmail.com> Co-authored-by: Bert Blommers <info@bertblommers.nl>
2021-07-26 16:21:17 +02:00
bucket = self.get_bucket(bucket_name)
bucket.website_configuration = None
2023-04-20 16:47:39 +00:00
def get_public_access_block(self, bucket_name: str) -> PublicAccessBlock:
2019-12-09 17:38:26 -08:00
bucket = self.get_bucket(bucket_name)
if not bucket.public_access_block:
raise NoSuchPublicAccessBlockConfiguration()
return bucket.public_access_block
def put_object(
self,
2023-04-20 16:47:39 +00:00
bucket_name: str,
key_name: str,
value: bytes,
storage: Optional[str] = None,
etag: Optional[str] = None,
multipart: Optional[FakeMultipart] = None,
encryption: Optional[str] = None,
kms_key_id: Optional[str] = None,
bucket_key_enabled: Any = None,
lock_mode: Optional[str] = None,
lock_legal_status: Optional[str] = None,
lock_until: Optional[str] = None,
checksum_value: Optional[str] = None,
) -> FakeKey:
if storage is not None and storage not in STORAGE_CLASS:
raise InvalidStorageClass(storage=storage)
2013-04-13 19:00:37 -04:00
bucket = self.get_bucket(bucket_name)
# getting default config from bucket if not included in put request
if bucket.encryption:
bucket_key_enabled = bucket_key_enabled or bucket.encryption["Rule"].get(
"BucketKeyEnabled", False
)
kms_key_id = kms_key_id or bucket.encryption["Rule"][
"ApplyServerSideEncryptionByDefault"
].get("KMSMasterKeyID")
encryption = (
encryption
or bucket.encryption["Rule"]["ApplyServerSideEncryptionByDefault"][
"SSEAlgorithm"
]
)
new_key = FakeKey(
name=key_name,
bucket_name=bucket_name,
value=value,
2022-08-13 09:49:43 +00:00
account_id=self.account_id,
storage=storage,
etag=etag,
is_versioned=bucket.is_versioned,
# AWS uses VersionId=null in both requests and responses
version_id=str(random.uuid4()) if bucket.is_versioned else "null",
multipart=multipart,
encryption=encryption,
kms_key_id=kms_key_id,
bucket_key_enabled=bucket_key_enabled,
2021-08-17 00:16:59 -05:00
lock_mode=lock_mode,
lock_legal_status=lock_legal_status,
lock_until=lock_until,
2023-03-16 10:56:20 -01:00
checksum_value=checksum_value,
)
2022-10-12 21:08:01 +00:00
existing_keys = bucket.keys.getlist(key_name, [])
if bucket.is_versioned:
keys = existing_keys + [new_key]
else:
keys = [new_key]
bucket.keys.setlist(key_name, keys)
2013-02-18 16:09:40 -05:00
2022-08-13 09:49:43 +00:00
notifications.send_event(
self.account_id,
notifications.S3NotificationEvent.OBJECT_CREATED_PUT_EVENT,
bucket,
new_key,
2022-08-13 09:49:43 +00:00
)
2013-02-18 16:09:40 -05:00
return new_key
2023-04-20 16:47:39 +00:00
def put_object_acl(
self,
bucket_name: str,
key_name: str,
acl: Optional[FakeAcl],
2023-04-20 16:47:39 +00:00
) -> None:
key = self.get_object(bucket_name, key_name)
# TODO: Support the XML-based ACL format
if key is not None:
key.set_acl(acl)
else:
2021-11-14 16:16:58 -01:00
raise MissingKey(key=key_name)
def put_object_legal_hold(
2023-04-20 16:47:39 +00:00
self,
bucket_name: str,
key_name: str,
version_id: Optional[str],
legal_hold_status: Dict[str, Any],
) -> None:
key = self.get_object(bucket_name, key_name, version_id=version_id)
2023-04-20 16:47:39 +00:00
key.lock_legal_status = legal_hold_status # type: ignore
2023-04-20 16:47:39 +00:00
def put_object_retention(
self,
bucket_name: str,
key_name: str,
version_id: Optional[str],
retention: Tuple[Optional[str], Optional[str]],
) -> None:
key = self.get_object(bucket_name, key_name, version_id=version_id)
2023-04-20 16:47:39 +00:00
key.lock_mode = retention[0] # type: ignore
key.lock_until = retention[1] # type: ignore
2023-03-16 10:56:20 -01:00
def get_object_attributes(
self,
key: FakeKey,
attributes_to_get: List[str],
) -> Dict[str, Any]:
"""
The following attributes are not yet returned: DeleteMarker, RequestCharged, ObjectParts
"""
2023-04-20 16:47:39 +00:00
response_keys: Dict[str, Any] = {
2023-03-16 10:56:20 -01:00
"etag": None,
"checksum": None,
"size": None,
"storage_class": None,
}
if "ETag" in attributes_to_get:
response_keys["etag"] = key.etag.replace('"', "")
if "Checksum" in attributes_to_get and key.checksum_value is not None:
response_keys["checksum"] = {key.checksum_algorithm: key.checksum_value}
if "ObjectSize" in attributes_to_get:
response_keys["size"] = key.size
if "StorageClass" in attributes_to_get:
response_keys["storage_class"] = key.storage_class
return response_keys
2022-01-25 18:25:39 -01:00
def get_object(
self,
2023-04-20 16:47:39 +00:00
bucket_name: str,
key_name: str,
version_id: Optional[str] = None,
part_number: Optional[str] = None,
return_delete_marker: bool = False,
) -> Optional[FakeKey]:
2013-03-05 08:14:43 -05:00
bucket = self.get_bucket(bucket_name)
key = None
2013-03-05 08:14:43 -05:00
if bucket:
2014-06-27 16:21:32 -06:00
if version_id is None:
if key_name in bucket.keys:
key = bucket.keys[key_name]
2014-06-27 16:21:32 -06:00
else:
for key_version in bucket.keys.getlist(key_name, default=[]):
if str(key_version.version_id) == str(version_id):
key = key_version
break
if part_number and key and key.multipart:
key = key.multipart.parts[part_number]
if isinstance(key, FakeKey):
key.advance()
return key
else:
if return_delete_marker and isinstance(key, FakeDeleteMarker):
return key # type: ignore
return None
2013-02-18 16:09:40 -05:00
2023-04-20 16:47:39 +00:00
def head_object(
self,
bucket_name: str,
key_name: str,
version_id: Optional[str] = None,
part_number: Optional[str] = None,
) -> Optional[FakeKey]:
obj = self.get_object(
bucket_name, key_name, version_id, part_number, return_delete_marker=True
)
if isinstance(obj, FakeDeleteMarker):
raise HeadOnDeleteMarker(obj)
return obj
2023-04-20 16:47:39 +00:00
def get_object_acl(self, key: FakeKey) -> Optional[FakeAcl]:
return key.acl
2023-04-20 16:47:39 +00:00
def get_object_legal_hold(self, key: FakeKey) -> Optional[str]:
return key.lock_legal_status
2023-04-20 16:47:39 +00:00
def get_object_lock_configuration(
self, bucket_name: str
) -> Tuple[bool, Optional[str], Optional[int], Optional[int]]:
bucket = self.get_bucket(bucket_name)
if not bucket.object_lock_enabled:
raise ObjectLockConfigurationNotFoundError
return (
bucket.object_lock_enabled,
bucket.default_lock_mode,
bucket.default_lock_days,
bucket.default_lock_years,
)
2023-04-20 16:47:39 +00:00
def get_object_tagging(self, key: FakeKey) -> Dict[str, List[Dict[str, str]]]:
2020-03-31 12:04:04 +01:00
return self.tagger.list_tags_for_resource(key.arn)
def put_object_tagging(
2023-04-20 16:47:39 +00:00
self,
key: Optional[FakeKey],
tags: Optional[Dict[str, str]],
key_name: Optional[str] = None,
) -> FakeKey:
2017-07-15 22:36:12 -04:00
if key is None:
2021-11-14 16:16:58 -01:00
raise MissingKey(key=key_name)
tags_input = self.tagger.convert_dict_to_tags_input(tags)
# Validation custom to S3
if tags:
if len(tags_input) > 10:
raise BadRequest("Object tags cannot be greater than 10")
if any([tagkey.startswith("aws") for tagkey in tags.keys()]):
raise InvalidTagError("Your TagKey cannot be prefixed with aws:")
# Validation shared across all services
errmsg = self.tagger.validate_tags(tags_input)
if errmsg:
raise InvalidTagError(errmsg)
2020-03-31 12:04:04 +01:00
self.tagger.delete_all_tags_for_resource(key.arn)
self.tagger.tag_resource(key.arn, tags_input)
2017-07-15 22:36:12 -04:00
return key
2023-04-20 16:47:39 +00:00
def get_bucket_tagging(self, bucket_name: str) -> Dict[str, List[Dict[str, str]]]:
2020-03-31 11:10:38 +01:00
bucket = self.get_bucket(bucket_name)
return self.tagger.list_tags_for_resource(bucket.arn)
2023-04-20 16:47:39 +00:00
def put_bucket_tagging(self, bucket_name: str, tags: Dict[str, str]) -> None:
bucket = self.get_bucket(bucket_name)
2020-03-31 11:10:38 +01:00
self.tagger.delete_all_tags_for_resource(bucket.arn)
self.tagger.tag_resource(
2020-04-01 15:35:25 +01:00
bucket.arn, [{"Key": key, "Value": value} for key, value in tags.items()]
2020-03-31 11:10:38 +01:00
)
def put_object_lock_configuration(
2023-04-20 16:47:39 +00:00
self,
bucket_name: str,
lock_enabled: bool,
mode: Optional[str] = None,
days: Optional[int] = None,
years: Optional[int] = None,
) -> None:
2021-08-17 00:16:59 -05:00
bucket = self.get_bucket(bucket_name)
if bucket.keys.item_size() > 0:
raise BucketNeedsToBeNew
if lock_enabled:
bucket.object_lock_enabled = True
bucket.versioning_status = "Enabled"
bucket.default_lock_mode = mode
bucket.default_lock_days = days
bucket.default_lock_years = years
2023-04-20 16:47:39 +00:00
def delete_bucket_tagging(self, bucket_name: str) -> None:
bucket = self.get_bucket(bucket_name)
2020-03-31 11:10:38 +01:00
self.tagger.delete_all_tags_for_resource(bucket.arn)
2023-04-20 16:47:39 +00:00
def put_bucket_cors(
self, bucket_name: str, cors_rules: List[Dict[str, Any]]
) -> None:
bucket = self.get_bucket(bucket_name)
bucket.set_cors(cors_rules)
2023-04-20 16:47:39 +00:00
def put_bucket_logging(
self, bucket_name: str, logging_config: Dict[str, Any]
) -> None:
bucket = self.get_bucket(bucket_name)
bucket.set_logging(logging_config, self)
2023-04-20 16:47:39 +00:00
def delete_bucket_cors(self, bucket_name: str) -> None:
bucket = self.get_bucket(bucket_name)
bucket.delete_cors()
2023-04-20 16:47:39 +00:00
def delete_public_access_block(self, bucket_name: str) -> None:
2019-12-09 17:38:26 -08:00
bucket = self.get_bucket(bucket_name)
bucket.public_access_block = None
2023-04-20 16:47:39 +00:00
def put_bucket_notification_configuration(
self, bucket_name: str, notification_config: Dict[str, Any]
) -> None:
"""
The configuration can be persisted, but at the moment we only send notifications to the following targets:
- AWSLambda
2023-09-22 15:17:01 +02:00
- SNS
- SQS
For the following events:
- 's3:ObjectCreated:Copy'
- 's3:ObjectCreated:Put'
"""
bucket = self.get_bucket(bucket_name)
bucket.set_notification_configuration(notification_config)
def put_bucket_accelerate_configuration(
2023-04-20 16:47:39 +00:00
self, bucket_name: str, accelerate_configuration: str
) -> None:
if accelerate_configuration not in ["Enabled", "Suspended"]:
raise MalformedXML()
bucket = self.get_bucket(bucket_name)
if bucket.name.find(".") != -1:
raise InvalidRequest("PutBucketAccelerateConfiguration")
bucket.set_accelerate_configuration(accelerate_configuration)
def put_public_access_block(
2023-04-20 16:47:39 +00:00
self, bucket_name: str, pub_block_config: Optional[Dict[str, Any]]
) -> None:
2019-12-09 17:38:26 -08:00
bucket = self.get_bucket(bucket_name)
if not pub_block_config:
raise InvalidPublicAccessBlockConfiguration()
bucket.public_access_block = PublicAccessBlock(
pub_block_config.get("BlockPublicAcls"),
pub_block_config.get("IgnorePublicAcls"),
pub_block_config.get("BlockPublicPolicy"),
pub_block_config.get("RestrictPublicBuckets"),
)
2023-04-20 16:47:39 +00:00
def abort_multipart_upload(self, bucket_name: str, multipart_id: str) -> None:
bucket = self.get_bucket(bucket_name)
multipart_data = bucket.multiparts.get(multipart_id, None)
if not multipart_data:
raise NoSuchUpload(upload_id=multipart_id)
2013-09-30 18:36:25 +03:00
del bucket.multiparts[multipart_id]
2021-08-28 17:13:52 +01:00
def list_parts(
2023-04-20 16:47:39 +00:00
self,
bucket_name: str,
multipart_id: str,
part_number_marker: int = 0,
max_parts: int = 1000,
) -> List[FakeKey]:
bucket = self.get_bucket(bucket_name)
if multipart_id not in bucket.multiparts:
raise NoSuchUpload(upload_id=multipart_id)
return list(
bucket.multiparts[multipart_id].list_parts(part_number_marker, max_parts)
)
2023-04-20 16:47:39 +00:00
def is_truncated(
self, bucket_name: str, multipart_id: str, next_part_number_marker: int
) -> bool:
bucket = self.get_bucket(bucket_name)
return len(bucket.multiparts[multipart_id].parts) > next_part_number_marker
def create_multipart_upload(
self,
2023-04-20 16:47:39 +00:00
bucket_name: str,
key_name: str,
metadata: CaseInsensitiveDict, # type: ignore
storage_type: str,
tags: Dict[str, str],
acl: Optional[FakeAcl],
sse_encryption: str,
kms_key_id: str,
) -> str:
2022-06-23 09:56:21 +00:00
multipart = FakeMultipart(
key_name,
metadata,
account_id=self.account_id,
storage=storage_type,
tags=tags,
acl=acl,
sse_encryption=sse_encryption,
kms_key_id=kms_key_id,
2022-06-23 09:56:21 +00:00
)
bucket = self.get_bucket(bucket_name)
bucket.multiparts[multipart.id] = multipart
return multipart.id
2023-04-20 16:47:39 +00:00
def complete_multipart_upload(
self, bucket_name: str, multipart_id: str, body: Iterator[Tuple[int, str]]
) -> Tuple[FakeMultipart, bytes, str, Optional[str]]:
bucket = self.get_bucket(bucket_name)
multipart = bucket.multiparts[multipart_id]
value, etag, checksum = multipart.complete(body)
if value is not None:
del bucket.multiparts[multipart_id]
return multipart, value, etag, checksum
2023-04-20 16:47:39 +00:00
def get_all_multiparts(self, bucket_name: str) -> Dict[str, FakeMultipart]:
bucket = self.get_bucket(bucket_name)
2014-04-02 19:03:40 +03:00
return bucket.multiparts
2023-04-20 16:47:39 +00:00
def upload_part(
self, bucket_name: str, multipart_id: str, part_id: int, value: bytes
) -> FakeKey:
bucket = self.get_bucket(bucket_name)
2013-03-26 14:52:33 +00:00
multipart = bucket.multiparts[multipart_id]
return multipart.set_part(part_id, value)
def upload_part_copy(
self,
2023-04-20 16:47:39 +00:00
dest_bucket_name: str,
multipart_id: str,
part_id: int,
src_bucket_name: str,
src_key_name: str,
2023-10-10 23:09:03 +00:00
src_version_id: Optional[str],
2023-04-20 16:47:39 +00:00
start_byte: int,
end_byte: int,
) -> FakeKey:
dest_bucket = self.get_bucket(dest_bucket_name)
multipart = dest_bucket.multiparts[multipart_id]
2023-04-20 16:47:39 +00:00
src_value = self.get_object( # type: ignore
src_bucket_name, src_key_name, version_id=src_version_id
).value
if start_byte is not None:
src_value = src_value[start_byte : end_byte + 1]
return multipart.set_part(part_id, src_value)
2023-04-20 16:47:39 +00:00
def list_objects(
self,
bucket: FakeBucket,
prefix: Optional[str],
delimiter: Optional[str],
marker: Optional[str],
max_keys: Optional[int],
) -> Tuple[Set[FakeKey], Set[str], bool, Optional[str]]:
key_results = set()
folder_results = set()
if prefix:
2023-04-20 16:47:39 +00:00
for key_name, key in bucket.keys.items(): # type: ignore
if key_name.startswith(prefix):
key_without_prefix = key_name.replace(prefix, "", 1)
if delimiter and delimiter in key_without_prefix:
# If delimiter, we need to split out folder_results
2017-02-23 21:37:43 -05:00
key_without_delimiter = key_without_prefix.split(delimiter)[0]
folder_results.add(
f"{prefix}{key_without_delimiter}{delimiter}"
2019-10-31 08:44:26 -07:00
)
else:
key_results.add(key)
else:
2023-04-20 16:47:39 +00:00
for key_name, key in bucket.keys.items(): # type: ignore
if delimiter and delimiter in key_name:
# If delimiter, we need to split out folder_results
2017-02-23 21:37:43 -05:00
folder_results.add(key_name.split(delimiter)[0] + delimiter)
else:
key_results.add(key)
2023-04-20 16:47:39 +00:00
key_results = filter( # type: ignore
lambda key: not isinstance(key, FakeDeleteMarker), key_results
)
2023-04-20 16:47:39 +00:00
key_results = sorted(key_results, key=lambda key: key.name) # type: ignore
folder_results = [ # type: ignore
2017-02-23 21:37:43 -05:00
folder_name for folder_name in sorted(folder_results, key=lambda key: key)
]
if marker:
limit = self._pagination_tokens.get(marker)
key_results = self._get_results_from_token(key_results, limit)
if max_keys is not None:
key_results, is_truncated, next_marker = self._truncate_result(
key_results, max_keys
)
else:
is_truncated = False
next_marker = None
return key_results, folder_results, is_truncated, next_marker
2023-04-20 16:47:39 +00:00
def list_objects_v2(
self,
bucket: FakeBucket,
prefix: Optional[str],
delimiter: Optional[str],
continuation_token: Optional[str],
start_after: Optional[str],
max_keys: int,
) -> Tuple[Set[Union[FakeKey, str]], bool, Optional[str]]:
result_keys, result_folders, _, _ = self.list_objects(
bucket, prefix, delimiter, marker=None, max_keys=None
)
2021-08-28 17:13:52 +01:00
# sort the combination of folders and keys into lexicographical order
2023-04-20 16:47:39 +00:00
all_keys = result_keys + result_folders # type: ignore
2021-08-28 17:13:52 +01:00
all_keys.sort(key=self._get_name)
if continuation_token or start_after:
limit = (
self._pagination_tokens.get(continuation_token)
if continuation_token
else start_after
)
all_keys = self._get_results_from_token(all_keys, limit)
truncated_keys, is_truncated, next_continuation_token = self._truncate_result(
all_keys, max_keys
)
return truncated_keys, is_truncated, next_continuation_token
def _get_results_from_token(self, result_keys: Any, token: Any) -> Any:
continuation_index = 0
for key in result_keys:
if (key.name if isinstance(key, FakeKey) else key) > token:
break
continuation_index += 1
return result_keys[continuation_index:]
def _truncate_result(self, result_keys: Any, max_keys: int) -> Any:
if max_keys == 0:
result_keys = []
is_truncated = True
next_continuation_token = None
elif len(result_keys) > max_keys:
is_truncated = "true" # type: ignore
result_keys = result_keys[:max_keys]
item = result_keys[-1]
key_id = item.name if isinstance(item, FakeKey) else item
next_continuation_token = md5_hash(key_id.encode("utf-8")).hexdigest()
self._pagination_tokens[next_continuation_token] = key_id
else:
is_truncated = "false" # type: ignore
next_continuation_token = None
return result_keys, is_truncated, next_continuation_token
2021-08-28 17:13:52 +01:00
@staticmethod
2023-04-20 16:47:39 +00:00
def _get_name(key: Union[str, FakeKey]) -> str:
2021-08-28 17:13:52 +01:00
if isinstance(key, FakeKey):
return key.name
else:
return key
2023-04-20 16:47:39 +00:00
def _set_delete_marker(self, bucket_name: str, key_name: str) -> FakeDeleteMarker:
bucket = self.get_bucket(bucket_name)
delete_marker = FakeDeleteMarker(key=bucket.keys[key_name])
bucket.keys[key_name] = delete_marker
return delete_marker
2023-04-20 16:47:39 +00:00
def delete_object_tagging(
self, bucket_name: str, key_name: str, version_id: Optional[str] = None
) -> None:
2020-06-20 12:15:29 +01:00
key = self.get_object(bucket_name, key_name, version_id=version_id)
2023-04-20 16:47:39 +00:00
self.tagger.delete_all_tags_for_resource(key.arn) # type: ignore
2020-06-20 12:15:29 +01:00
2023-04-20 16:47:39 +00:00
def delete_object(
self,
bucket_name: str,
key_name: str,
version_id: Optional[str] = None,
bypass: bool = False,
) -> Tuple[bool, Optional[Dict[str, Any]]]:
bucket = self.get_bucket(bucket_name)
response_meta = {}
try:
if not bucket.is_versioned:
bucket.keys.pop(key_name)
else:
if version_id is None:
delete_marker = self._set_delete_marker(bucket_name, key_name)
response_meta["version-id"] = delete_marker.version_id
response_meta["delete-marker"] = "true"
else:
if key_name not in bucket.keys:
raise KeyError
response_meta["version-id"] = version_id
for key in bucket.keys.getlist(key_name):
if str(key.version_id) == str(version_id):
if (
hasattr(key, "is_locked")
and key.is_locked
and not bypass
):
2021-08-17 00:16:59 -05:00
raise AccessDeniedByLock
if type(key) is FakeDeleteMarker:
2023-04-20 16:47:39 +00:00
if type(key.key) is FakeDeleteMarker: # type: ignore
# Our key is a DeleteMarker, that usually contains a link to the actual FakeKey
# But: If we have deleted the FakeKey multiple times,
# We have a DeleteMarker linking to a DeleteMarker (etc..) linking to a FakeKey
response_meta["delete-marker"] = "true"
# The alternative is that we're deleting the DeleteMarker that points directly to a FakeKey
# In this scenario, AWS does not return the `delete-marker` header
break
bucket.keys.setlist(
key_name,
[
key
for key in bucket.keys.getlist(key_name)
if str(key.version_id) != str(version_id)
],
)
2018-05-03 01:40:49 -07:00
if not bucket.keys.getlist(key_name):
bucket.keys.pop(key_name)
return True, response_meta
except KeyError:
return False, None
2013-02-18 16:09:40 -05:00
2023-04-20 16:47:39 +00:00
def delete_objects(
self, bucket_name: str, objects: List[Dict[str, Any]]
) -> List[Tuple[str, Optional[str]]]:
deleted_objects = []
for object_ in objects:
key_name = object_["Key"]
version_id = object_.get("VersionId", None)
self.delete_object(bucket_name, key_name, version_id=version_id)
deleted_objects.append((key_name, version_id))
return deleted_objects
def copy_object(
self,
2023-04-20 16:47:39 +00:00
src_key: FakeKey,
dest_bucket_name: str,
dest_key_name: str,
storage: Optional[str] = None,
encryption: Optional[str] = None,
kms_key_id: Optional[str] = None,
bucket_key_enabled: Any = None,
2023-04-20 16:47:39 +00:00
mdirective: Optional[str] = None,
metadata: Optional[Any] = None,
website_redirect_location: Optional[str] = None,
lock_mode: Optional[str] = None,
lock_legal_status: Optional[str] = None,
lock_until: Optional[str] = None,
2023-04-20 16:47:39 +00:00
) -> None:
bucket = self.get_bucket(dest_bucket_name)
if src_key.name == dest_key_name and src_key.bucket_name == dest_bucket_name:
if src_key.encryption and src_key.encryption != "AES256" and not encryption:
# this a special case, as now S3 default to AES256 when not provided
# if the source key had encryption, and we did not specify it for the destination, S3 will accept a
# copy in place even without any required attributes
encryption = "AES256"
if not any(
(
storage,
encryption,
mdirective == "REPLACE",
website_redirect_location,
bucket.encryption, # S3 will allow copy in place if the bucket has encryption configured
src_key._version_id and bucket.is_versioned,
)
):
raise CopyObjectMustChangeSomething
new_key = self.put_object(
bucket_name=dest_bucket_name,
key_name=dest_key_name,
2022-01-25 18:25:39 -01:00
value=src_key.value,
storage=storage,
2022-01-25 18:25:39 -01:00
multipart=src_key.multipart,
encryption=encryption,
kms_key_id=kms_key_id, # TODO: use aws managed key if not provided
bucket_key_enabled=bucket_key_enabled,
lock_mode=lock_mode,
lock_legal_status=lock_legal_status,
lock_until=lock_until,
)
2022-01-25 18:25:39 -01:00
self.tagger.copy_tags(src_key.arn, new_key.arn)
if mdirective != "REPLACE":
new_key.set_metadata(src_key.metadata)
else:
new_key.set_metadata(metadata)
if website_redirect_location:
new_key.website_redirect_location = website_redirect_location
2023-02-27 14:44:30 -01:00
if src_key.storage_class in ARCHIVE_STORAGE_CLASSES:
# Object copied from Glacier object should not have expiry
new_key.set_expiry(None)
if src_key.checksum_value:
new_key.checksum_value = src_key.checksum_value
new_key.checksum_algorithm = src_key.checksum_algorithm
# Send notifications that an object was copied
2022-08-13 09:49:43 +00:00
notifications.send_event(
self.account_id,
notifications.S3NotificationEvent.OBJECT_CREATED_COPY_EVENT,
bucket,
new_key,
2022-08-13 09:49:43 +00:00
)
2023-04-20 16:47:39 +00:00
def put_bucket_acl(self, bucket_name: str, acl: Optional[FakeAcl]) -> None:
2015-11-11 20:26:29 -05:00
bucket = self.get_bucket(bucket_name)
bucket.set_acl(acl)
2023-04-20 16:47:39 +00:00
def get_bucket_acl(self, bucket_name: str) -> Optional[FakeAcl]:
2015-11-11 20:26:29 -05:00
bucket = self.get_bucket(bucket_name)
return bucket.acl
2023-04-20 16:47:39 +00:00
def get_bucket_cors(self, bucket_name: str) -> List[CorsRule]:
2020-06-06 13:15:50 +01:00
bucket = self.get_bucket(bucket_name)
return bucket.cors
2023-04-20 16:47:39 +00:00
def get_bucket_lifecycle(self, bucket_name: str) -> List[LifecycleRule]:
bucket = self.get_bucket(bucket_name)
return bucket.rules
2023-04-20 16:47:39 +00:00
def get_bucket_location(self, bucket_name: str) -> str:
bucket = self.get_bucket(bucket_name)
return bucket.location
2023-04-20 16:47:39 +00:00
def get_bucket_logging(self, bucket_name: str) -> Dict[str, Any]:
2020-06-06 13:15:50 +01:00
bucket = self.get_bucket(bucket_name)
return bucket.logging
2023-04-20 16:47:39 +00:00
def get_bucket_notification_configuration(
self, bucket_name: str
) -> Optional[NotificationConfiguration]:
2020-06-06 13:15:50 +01:00
bucket = self.get_bucket(bucket_name)
return bucket.notification_configuration
def select_object_content(
self,
bucket_name: str,
key_name: str,
select_query: str,
input_details: Dict[str, Any],
2023-04-20 16:47:39 +00:00
) -> List[bytes]:
"""
Highly experimental. Please raise an issue if you find any inconsistencies/bugs.
Known missing features:
- Function aliases (count(*) as cnt)
- Most functions (only count() is supported)
- Result is always in JSON
- FieldDelimiters are ignored
"""
self.get_bucket(bucket_name)
key = self.get_object(bucket_name, key_name)
2023-04-20 16:47:39 +00:00
query_input = key.value.decode("utf-8") # type: ignore
if "CSV" in input_details:
# input is in CSV - we need to convert it to JSON before parsing
from py_partiql_parser._internal.csv_converter import ( # noqa # pylint: disable=unused-import
csv_to_json,
)
use_headers = input_details["CSV"].get("FileHeaderInfo", "") == "USE"
query_input = csv_to_json(query_input, use_headers)
2023-05-25 16:37:45 +00:00
query_result = parse_query(query_input, select_query)
from py_partiql_parser import SelectEncoder
return [
2023-05-25 16:37:45 +00:00
json.dumps(x, indent=None, separators=(",", ":"), cls=SelectEncoder).encode(
"utf-8"
)
for x in query_result
]
2015-11-11 20:26:29 -05:00
2023-12-01 03:51:28 -08:00
class S3BackendDict(BackendDict[S3Backend]):
"""
Encapsulation class to hold S3 backends.
This is specialised to include additional attributes to help multi-account support in S3
but is otherwise identical to the superclass.
"""
def __init__(
self,
backend: Any,
service_name: str,
use_boto3_regions: bool = True,
additional_regions: Optional[List[str]] = None,
):
super().__init__(backend, service_name, use_boto3_regions, additional_regions)
# Maps bucket names to account IDs. This is used to locate the exact S3Backend
# holding the bucket and to maintain the common bucket namespace.
self.bucket_accounts: Dict[str, str] = {}
s3_backends = S3BackendDict(
S3Backend, service_name="s3", use_boto3_regions=False, additional_regions=["global"]
)