moto/moto/s3/models.py

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

2476 lines
89 KiB
Python
Raw Normal View History

import json
2013-03-26 14:52:33 +00:00
import os
import base64
2013-03-29 17:45:33 -04:00
import datetime
import copy
2014-06-27 16:21:32 -06:00
import itertools
2014-08-26 13:25:50 -04:00
import codecs
import string
import tempfile
import threading
import sys
import urllib.parse
from bisect import insort
2023-04-20 16:47:39 +00:00
from typing import Any, Dict, List, Optional, Set, Tuple, Iterator, Union
from importlib import reload
from moto.core import BaseBackend, BaseModel, BackendDict, CloudFormationModel
2022-08-13 09:49:43 +00:00
from moto.core import CloudWatchMetricProvider
from moto.core.utils import (
iso_8601_datetime_without_milliseconds_s3,
rfc_1123_datetime,
unix_time,
unix_time_millis,
)
2020-04-27 12:48:23 -07:00
from moto.cloudwatch.models import MetricDatum
from moto.moto_api import state_manager
from moto.moto_api._internal import mock_random as random
from moto.moto_api._internal.managed_state_model import ManagedState
2020-03-31 11:10:38 +01:00
from moto.utilities.tagging_service import TaggingService
from moto.utilities.utils import LowercaseDict, md5_hash
from moto.s3.exceptions import (
2021-08-17 00:16:59 -05:00
AccessDeniedByLock,
BucketAlreadyExists,
2021-08-17 00:16:59 -05:00
BucketNeedsToBeNew,
CopyObjectMustChangeSomething,
MissingBucket,
InvalidBucketName,
InvalidPart,
InvalidRequest,
EntityTooSmall,
MissingKey,
InvalidNotificationDestination,
MalformedXML,
InvalidStorageClass,
InvalidTargetBucketForLogging,
CrossLocationLoggingProhibitted,
2019-12-09 17:38:26 -08:00
NoSuchPublicAccessBlockConfiguration,
InvalidPublicAccessBlockConfiguration,
NoSuchUpload,
ObjectLockConfigurationNotFoundError,
InvalidTagError,
)
from .cloud_formation import cfn_to_api_encryption, is_replacement_update
from . import notifications
from .select_object_content import parse_query
2023-04-20 16:47:39 +00:00
from .utils import (
clean_key_name,
_VersionedKeyStore,
undo_clean_key_name,
CaseInsensitiveDict,
)
2023-02-27 14:44:30 -01:00
from .utils import ARCHIVE_STORAGE_CLASSES, STORAGE_CLASS
from ..events.notifications import send_notification as events_send_notification
from ..settings import get_s3_default_key_buffer_size, S3_UPLOAD_PART_MIN_SIZE
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 = datetime.datetime.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: Optional[str] = None,
storage: Optional[str] = "STANDARD",
etag: Optional[str] = None,
is_versioned: bool = False,
version_id: str = "null",
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 = datetime.datetime.utcnow()
2023-04-20 16:47:39 +00:00
self.acl: Optional[FakeAcl] = get_canned_acl("private")
self.website_redirect_location = 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
@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 = datetime.datetime.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.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 = datetime.datetime.utcnow()
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
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.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
2023-04-20 16:47:39 +00:00
def complete(self, body: Iterator[Tuple[int, str]]) -> Tuple[bytes, str]:
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()
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)
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))
return total, f"{full_etag.hexdigest()}-{count}"
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(
2023-04-20 16:47:39 +00:00
part_id, value, 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
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,
transition_days: Optional[str] = None,
transition_date: Optional[str] = None,
storage_class: Optional[str] = None,
expired_object_delete_marker: Optional[str] = None,
nve_noncurrent_days: Optional[str] = None,
nvt_noncurrent_days: Optional[str] = None,
nvt_storage_class: Optional[str] = None,
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.transition_days = transition_days
self.transition_date = transition_date
self.storage_class = storage_class
self.expired_object_delete_marker = expired_object_delete_marker
self.nve_noncurrent_days = nve_noncurrent_days
self.nvt_noncurrent_days = nvt_noncurrent_days
self.nvt_storage_class = nvt_storage_class
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.
Note: The following are missing that should be added in the future:
- transitions (returns None for now)
- noncurrentVersionTransitions (returns None for now)
: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,
"transitions": None, # Replace me with logic to fill in
"noncurrentVersionTransitions": None, # Replace me with logic to fill in
}
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
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
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")
transition = rule.get("Transition")
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"
]
nvt_noncurrent_days = None
nvt_storage_class = None
if rule.get("NoncurrentVersionTransition") is not None:
2018-10-03 01:26:09 -05:00
if rule["NoncurrentVersionTransition"].get("NoncurrentDays") is None:
raise MalformedXML()
2018-10-03 01:26:09 -05:00
if rule["NoncurrentVersionTransition"].get("StorageClass") is None:
raise MalformedXML()
nvt_noncurrent_days = rule["NoncurrentVersionTransition"][
"NoncurrentDays"
]
nvt_storage_class = rule["NoncurrentVersionTransition"]["StorageClass"]
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,
transition_days=transition.get("Days") if transition else None,
transition_date=transition.get("Date") if transition else None,
storage_class=transition.get("StorageClass")
if transition
else None,
expired_object_delete_marker=eodm,
nve_noncurrent_days=nve_noncurrent_days,
nvt_noncurrent_days=nvt_noncurrent_days,
nvt_storage_class=nvt_storage_class,
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 = []
2023-04-20 16:47:39 +00:00
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):
if not bucket_backend.buckets.get(logging_config["TargetBucket"]):
raise InvalidTargetBucketForLogging(
"The target bucket for logging does not exist."
)
# Does the target bucket have the log-delivery WRITE and READ_ACP permissions?
write = read_acp = False
2023-04-20 16:47:39 +00:00
for grant in bucket_backend.buckets[logging_config["TargetBucket"]].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
if not write or not read_acp:
raise InvalidTargetBucketForLogging(
"You must give the log-delivery group WRITE and READ_ACP"
" permissions to the target bucket"
)
# Buckets must also exist within the same region:
if (
bucket_backend.buckets[logging_config["TargetBucket"]].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:
2021-08-17 00:16:59 -05:00
now = datetime.datetime.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
@mock_s3
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
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
"""
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()
2013-02-18 16:09:40 -05:00
state_manager.register_default_transition(
"s3::keyrestore", transition={"progression": "immediate"}
)
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()
#
# 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 self.buckets:
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:
try:
return self.buckets[bucket_name]
except KeyError:
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:
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,
prefix: str = "",
) -> Tuple[List[FakeKey], List[str], List[FakeDeleteMarker]]:
bucket = self.get_bucket(bucket_name)
2014-06-27 16:21:32 -06:00
2023-04-20 16:47:39 +00:00
common_prefixes: List[str] = []
requested_versions: List[FakeKey] = []
delete_markers: List[FakeDeleteMarker] = []
all_versions = list(
itertools.chain(*(copy.deepcopy(l) for key, l 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
for version in all_versions:
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
# skip all keys that alphabetically come before keymarker
if key_marker and name < key_marker:
continue
# 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
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]
common_prefixes.append(prefix_including_delimiter)
continue
# Differentiate between FakeKey and FakeDeleteMarkers
if not isinstance(version, FakeKey):
delete_markers.append(version)
continue
requested_versions.append(version)
common_prefixes = sorted(set(common_prefixes))
return requested_versions, common_prefixes, delete_markers
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:
2013-04-13 19:00:37 -04:00
key_name = clean_key_name(key_name)
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
2023-04-20 16:47:39 +00:00
version_id=str(random.uuid4()) if bucket.is_versioned else "null", # type: ignore
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.S3_OBJECT_CREATE_PUT, bucket, new_key
)
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]
) -> 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,
key_is_clean: bool = False,
) -> Optional[FakeKey]:
2022-01-25 18:25:39 -01:00
if not key_is_clean:
key_name = clean_key_name(key_name)
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:
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]:
return self.get_object(bucket_name, key_name, version_id, part_number)
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)
2023-04-20 16:47:39 +00:00
def set_key_tags(
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)
boto_tags_dict = self.tagger.convert_dict_to_tags_input(tags)
errmsg = self.tagger.validate_tags(boto_tags_dict)
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, boto_tags_dict)
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
- 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)
2023-04-20 16:47:39 +00:00
def put_bucket_public_access_block(
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,
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]:
bucket = self.get_bucket(bucket_name)
multipart = bucket.multiparts[multipart_id]
value, etag = multipart.complete(body)
if value is not None:
del bucket.multiparts[multipart_id]
return multipart, value, etag
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 copy_part(
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,
src_version_id: str,
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]
) -> Tuple[Set[FakeKey], Set[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)
]
return key_results, folder_results
2023-04-20 16:47:39 +00:00
def list_objects_v2(
self, bucket: FakeBucket, prefix: Optional[str], delimiter: Optional[str]
) -> Set[Union[FakeKey, str]]:
2021-08-28 17:13:52 +01:00
result_keys, result_folders = self.list_objects(bucket, prefix, delimiter)
# 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)
return all_keys
@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]]]:
2013-04-13 19:00:37 -04:00
key_name = clean_key_name(key_name)
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):
2021-08-17 00:16:59 -05:00
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, undo_clean_key_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,
acl: Optional[FakeAcl] = None,
encryption: Optional[str] = None,
kms_key_id: Optional[str] = None,
bucket_key_enabled: bool = False,
mdirective: Optional[str] = None,
) -> None:
if (
src_key.name == dest_key_name
and src_key.bucket_name == dest_bucket_name
and storage == src_key.storage_class
and acl == src_key.acl
and encryption == src_key.encryption
and kms_key_id == src_key.kms_key_id
and bucket_key_enabled == (src_key.bucket_key_enabled or False)
and mdirective != "REPLACE"
):
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 or src_key.storage_class,
multipart=src_key.multipart,
encryption=encryption or src_key.encryption,
kms_key_id=kms_key_id or src_key.kms_key_id,
bucket_key_enabled=bucket_key_enabled or src_key.bucket_key_enabled,
lock_mode=src_key.lock_mode,
lock_legal_status=src_key.lock_legal_status,
lock_until=src_key.lock_until,
)
2022-01-25 18:25:39 -01:00
self.tagger.copy_tags(src_key.arn, new_key.arn)
new_key.set_metadata(src_key.metadata)
2015-10-07 00:04:22 -07:00
if acl is not None:
new_key.set_acl(acl)
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)
# Send notifications that an object was copied
bucket = self.get_bucket(dest_bucket_name)
2022-08-13 09:49:43 +00:00
notifications.send_event(
self.account_id, notifications.S3_OBJECT_CREATE_COPY, bucket, new_key
)
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],
output_details: Dict[str, Any], # pylint: disable=unused-argument
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 and RecordDelimiters 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)
return [
json.dumps(x, indent=None, separators=(",", ":")).encode("utf-8")
for x in parse_query(query_input, select_query)
]
2015-11-11 20:26:29 -05:00
s3_backends = BackendDict(
S3Backend, service_name="s3", use_boto3_regions=False, additional_regions=["global"]
)