merging from master
This commit is contained in:
commit
445f474534
125 changed files with 7406 additions and 3847 deletions
|
|
@ -39,7 +39,7 @@ class InvalidResourcePathException(BadRequestException):
|
|||
def __init__(self):
|
||||
super(InvalidResourcePathException, self).__init__(
|
||||
"BadRequestException",
|
||||
"Resource's path part only allow a-zA-Z0-9._- and curly braces at the beginning and the end.",
|
||||
"Resource's path part only allow a-zA-Z0-9._- and curly braces at the beginning and the end and an optional plus sign before the closing brace.",
|
||||
)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -83,14 +83,14 @@ class MethodResponse(BaseModel, dict):
|
|||
|
||||
|
||||
class Method(BaseModel, dict):
|
||||
def __init__(self, method_type, authorization_type):
|
||||
def __init__(self, method_type, authorization_type, **kwargs):
|
||||
super(Method, self).__init__()
|
||||
self.update(
|
||||
dict(
|
||||
httpMethod=method_type,
|
||||
authorizationType=authorization_type,
|
||||
authorizerId=None,
|
||||
apiKeyRequired=None,
|
||||
apiKeyRequired=kwargs.get("api_key_required") or False,
|
||||
requestParameters=None,
|
||||
requestModels=None,
|
||||
methodIntegration=None,
|
||||
|
|
@ -117,14 +117,15 @@ class Resource(BaseModel):
|
|||
self.api_id = api_id
|
||||
self.path_part = path_part
|
||||
self.parent_id = parent_id
|
||||
self.resource_methods = {"GET": {}}
|
||||
self.resource_methods = {}
|
||||
|
||||
def to_dict(self):
|
||||
response = {
|
||||
"path": self.get_path(),
|
||||
"id": self.id,
|
||||
"resourceMethods": self.resource_methods,
|
||||
}
|
||||
if self.resource_methods:
|
||||
response["resourceMethods"] = self.resource_methods
|
||||
if self.parent_id:
|
||||
response["parentId"] = self.parent_id
|
||||
response["pathPart"] = self.path_part
|
||||
|
|
@ -158,8 +159,12 @@ class Resource(BaseModel):
|
|||
)
|
||||
return response.status_code, response.text
|
||||
|
||||
def add_method(self, method_type, authorization_type):
|
||||
method = Method(method_type=method_type, authorization_type=authorization_type)
|
||||
def add_method(self, method_type, authorization_type, api_key_required):
|
||||
method = Method(
|
||||
method_type=method_type,
|
||||
authorization_type=authorization_type,
|
||||
api_key_required=api_key_required,
|
||||
)
|
||||
self.resource_methods[method_type] = method
|
||||
return method
|
||||
|
||||
|
|
@ -394,12 +399,17 @@ class UsagePlanKey(BaseModel, dict):
|
|||
|
||||
|
||||
class RestAPI(BaseModel):
|
||||
def __init__(self, id, region_name, name, description):
|
||||
def __init__(self, id, region_name, name, description, **kwargs):
|
||||
self.id = id
|
||||
self.region_name = region_name
|
||||
self.name = name
|
||||
self.description = description
|
||||
self.create_date = int(time.time())
|
||||
self.api_key_source = kwargs.get("api_key_source") or "HEADER"
|
||||
self.endpoint_configuration = kwargs.get("endpoint_configuration") or {
|
||||
"types": ["EDGE"]
|
||||
}
|
||||
self.tags = kwargs.get("tags") or {}
|
||||
|
||||
self.deployments = {}
|
||||
self.stages = {}
|
||||
|
|
@ -416,6 +426,9 @@ class RestAPI(BaseModel):
|
|||
"name": self.name,
|
||||
"description": self.description,
|
||||
"createdDate": int(time.time()),
|
||||
"apiKeySource": self.api_key_source,
|
||||
"endpointConfiguration": self.endpoint_configuration,
|
||||
"tags": self.tags,
|
||||
}
|
||||
|
||||
def add_child(self, path, parent_id=None):
|
||||
|
|
@ -529,9 +542,24 @@ class APIGatewayBackend(BaseBackend):
|
|||
self.__dict__ = {}
|
||||
self.__init__(region_name)
|
||||
|
||||
def create_rest_api(self, name, description):
|
||||
def create_rest_api(
|
||||
self,
|
||||
name,
|
||||
description,
|
||||
api_key_source=None,
|
||||
endpoint_configuration=None,
|
||||
tags=None,
|
||||
):
|
||||
api_id = create_id()
|
||||
rest_api = RestAPI(api_id, self.region_name, name, description)
|
||||
rest_api = RestAPI(
|
||||
api_id,
|
||||
self.region_name,
|
||||
name,
|
||||
description,
|
||||
api_key_source=api_key_source,
|
||||
endpoint_configuration=endpoint_configuration,
|
||||
tags=tags,
|
||||
)
|
||||
self.apis[api_id] = rest_api
|
||||
return rest_api
|
||||
|
||||
|
|
@ -556,7 +584,7 @@ class APIGatewayBackend(BaseBackend):
|
|||
return resource
|
||||
|
||||
def create_resource(self, function_id, parent_resource_id, path_part):
|
||||
if not re.match("^\\{?[a-zA-Z0-9._-]+\\}?$", path_part):
|
||||
if not re.match("^\\{?[a-zA-Z0-9._-]+\\+?\\}?$", path_part):
|
||||
raise InvalidResourcePathException()
|
||||
api = self.get_rest_api(function_id)
|
||||
child = api.add_child(path=path_part, parent_id=parent_resource_id)
|
||||
|
|
@ -571,9 +599,18 @@ class APIGatewayBackend(BaseBackend):
|
|||
resource = self.get_resource(function_id, resource_id)
|
||||
return resource.get_method(method_type)
|
||||
|
||||
def create_method(self, function_id, resource_id, method_type, authorization_type):
|
||||
def create_method(
|
||||
self,
|
||||
function_id,
|
||||
resource_id,
|
||||
method_type,
|
||||
authorization_type,
|
||||
api_key_required=None,
|
||||
):
|
||||
resource = self.get_resource(function_id, resource_id)
|
||||
method = resource.add_method(method_type, authorization_type)
|
||||
method = resource.add_method(
|
||||
method_type, authorization_type, api_key_required=api_key_required
|
||||
)
|
||||
return method
|
||||
|
||||
def get_stage(self, function_id, stage_name):
|
||||
|
|
|
|||
|
|
@ -12,6 +12,9 @@ from .exceptions import (
|
|||
ApiKeyAlreadyExists,
|
||||
)
|
||||
|
||||
API_KEY_SOURCES = ["AUTHORIZER", "HEADER"]
|
||||
ENDPOINT_CONFIGURATION_TYPES = ["PRIVATE", "EDGE", "REGIONAL"]
|
||||
|
||||
|
||||
class APIGatewayResponse(BaseResponse):
|
||||
def error(self, type_, message, status=400):
|
||||
|
|
@ -45,7 +48,45 @@ class APIGatewayResponse(BaseResponse):
|
|||
elif self.method == "POST":
|
||||
name = self._get_param("name")
|
||||
description = self._get_param("description")
|
||||
rest_api = self.backend.create_rest_api(name, description)
|
||||
api_key_source = self._get_param("apiKeySource")
|
||||
endpoint_configuration = self._get_param("endpointConfiguration")
|
||||
tags = self._get_param("tags")
|
||||
|
||||
# Param validation
|
||||
if api_key_source and api_key_source not in API_KEY_SOURCES:
|
||||
return self.error(
|
||||
"ValidationException",
|
||||
(
|
||||
"1 validation error detected: "
|
||||
"Value '{api_key_source}' at 'createRestApiInput.apiKeySource' failed "
|
||||
"to satisfy constraint: Member must satisfy enum value set: "
|
||||
"[AUTHORIZER, HEADER]"
|
||||
).format(api_key_source=api_key_source),
|
||||
)
|
||||
|
||||
if endpoint_configuration and "types" in endpoint_configuration:
|
||||
invalid_types = list(
|
||||
set(endpoint_configuration["types"])
|
||||
- set(ENDPOINT_CONFIGURATION_TYPES)
|
||||
)
|
||||
if invalid_types:
|
||||
return self.error(
|
||||
"ValidationException",
|
||||
(
|
||||
"1 validation error detected: Value '{endpoint_type}' "
|
||||
"at 'createRestApiInput.endpointConfiguration.types' failed "
|
||||
"to satisfy constraint: Member must satisfy enum value set: "
|
||||
"[PRIVATE, EDGE, REGIONAL]"
|
||||
).format(endpoint_type=invalid_types[0]),
|
||||
)
|
||||
|
||||
rest_api = self.backend.create_rest_api(
|
||||
name,
|
||||
description,
|
||||
api_key_source=api_key_source,
|
||||
endpoint_configuration=endpoint_configuration,
|
||||
tags=tags,
|
||||
)
|
||||
return 200, {}, json.dumps(rest_api.to_dict())
|
||||
|
||||
def restapis_individual(self, request, full_url, headers):
|
||||
|
|
@ -104,8 +145,13 @@ class APIGatewayResponse(BaseResponse):
|
|||
return 200, {}, json.dumps(method)
|
||||
elif self.method == "PUT":
|
||||
authorization_type = self._get_param("authorizationType")
|
||||
api_key_required = self._get_param("apiKeyRequired")
|
||||
method = self.backend.create_method(
|
||||
function_id, resource_id, method_type, authorization_type
|
||||
function_id,
|
||||
resource_id,
|
||||
method_type,
|
||||
authorization_type,
|
||||
api_key_required,
|
||||
)
|
||||
return 200, {}, json.dumps(method)
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
from botocore.client import ClientError
|
||||
from moto.core.exceptions import JsonRESTError
|
||||
|
||||
|
||||
class LambdaClientError(ClientError):
|
||||
|
|
@ -29,3 +30,12 @@ class InvalidRoleFormat(LambdaClientError):
|
|||
role, InvalidRoleFormat.pattern
|
||||
)
|
||||
super(InvalidRoleFormat, self).__init__("ValidationException", message)
|
||||
|
||||
|
||||
class PreconditionFailedException(JsonRESTError):
|
||||
code = 412
|
||||
|
||||
def __init__(self, message):
|
||||
super(PreconditionFailedException, self).__init__(
|
||||
"PreconditionFailedException", message
|
||||
)
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ import requests.adapters
|
|||
|
||||
from boto3 import Session
|
||||
|
||||
from moto.awslambda.policy import Policy
|
||||
from moto.core import BaseBackend, BaseModel
|
||||
from moto.core.exceptions import RESTError
|
||||
from moto.iam.models import iam_backend
|
||||
|
|
@ -47,15 +48,11 @@ from moto.core import ACCOUNT_ID
|
|||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
try:
|
||||
from tempfile import TemporaryDirectory
|
||||
except ImportError:
|
||||
from backports.tempfile import TemporaryDirectory
|
||||
|
||||
# The lambci container is returning a special escape character for the "RequestID" fields. Unicode 033:
|
||||
# _stderr_regex = re.compile(r"START|END|REPORT RequestId: .*")
|
||||
_stderr_regex = re.compile(r"\033\[\d+.*")
|
||||
_orig_adapter_send = requests.adapters.HTTPAdapter.send
|
||||
docker_3 = docker.__version__[0] >= "3"
|
||||
|
||||
|
|
@ -164,7 +161,8 @@ class LambdaFunction(BaseModel):
|
|||
self.logs_backend = logs_backends[self.region]
|
||||
self.environment_vars = spec.get("Environment", {}).get("Variables", {})
|
||||
self.docker_client = docker.from_env()
|
||||
self.policy = ""
|
||||
self.policy = None
|
||||
self.state = "Active"
|
||||
|
||||
# Unfortunately mocking replaces this method w/o fallback enabled, so we
|
||||
# need to replace it if we detect it's been mocked
|
||||
|
|
@ -274,11 +272,11 @@ class LambdaFunction(BaseModel):
|
|||
"MemorySize": self.memory_size,
|
||||
"Role": self.role,
|
||||
"Runtime": self.run_time,
|
||||
"State": self.state,
|
||||
"Timeout": self.timeout,
|
||||
"Version": str(self.version),
|
||||
"VpcConfig": self.vpc_config,
|
||||
}
|
||||
|
||||
if self.environment_vars:
|
||||
config["Environment"] = {"Variables": self.environment_vars}
|
||||
|
||||
|
|
@ -385,7 +383,7 @@ class LambdaFunction(BaseModel):
|
|||
try:
|
||||
# TODO: I believe we can keep the container running and feed events as needed
|
||||
# also need to hook it up to the other services so it can make kws/s3 etc calls
|
||||
# Should get invoke_id /RequestId from invovation
|
||||
# Should get invoke_id /RequestId from invocation
|
||||
env_vars = {
|
||||
"AWS_LAMBDA_FUNCTION_TIMEOUT": self.timeout,
|
||||
"AWS_LAMBDA_FUNCTION_NAME": self.function_name,
|
||||
|
|
@ -397,6 +395,7 @@ class LambdaFunction(BaseModel):
|
|||
env_vars.update(self.environment_vars)
|
||||
|
||||
container = output = exit_code = None
|
||||
log_config = docker.types.LogConfig(type=docker.types.LogConfig.types.JSON)
|
||||
with _DockerDataVolumeContext(self) as data_vol:
|
||||
try:
|
||||
run_kwargs = (
|
||||
|
|
@ -412,6 +411,7 @@ class LambdaFunction(BaseModel):
|
|||
volumes=["{}:/var/task".format(data_vol.name)],
|
||||
environment=env_vars,
|
||||
detach=True,
|
||||
log_config=log_config,
|
||||
**run_kwargs
|
||||
)
|
||||
finally:
|
||||
|
|
@ -453,14 +453,9 @@ class LambdaFunction(BaseModel):
|
|||
if exit_code != 0:
|
||||
raise Exception("lambda invoke failed output: {}".format(output))
|
||||
|
||||
# strip out RequestId lines (TODO: This will return an additional '\n' in the response)
|
||||
output = os.linesep.join(
|
||||
[
|
||||
line
|
||||
for line in self.convert(output).splitlines()
|
||||
if not _stderr_regex.match(line)
|
||||
]
|
||||
)
|
||||
# We only care about the response from the lambda
|
||||
# Which is the last line of the output, according to https://github.com/lambci/docker-lambda/issues/25
|
||||
output = output.splitlines()[-1]
|
||||
return output, False
|
||||
except BaseException as e:
|
||||
traceback.print_exc()
|
||||
|
|
@ -480,7 +475,7 @@ class LambdaFunction(BaseModel):
|
|||
payload["result"] = response_headers["x-amz-log-result"]
|
||||
result = res.encode("utf-8")
|
||||
else:
|
||||
result = json.dumps(payload)
|
||||
result = res
|
||||
if errored:
|
||||
response_headers["x-amz-function-error"] = "Handled"
|
||||
|
||||
|
|
@ -709,7 +704,8 @@ class LambdaStorage(object):
|
|||
"versions": [],
|
||||
"alias": weakref.WeakValueDictionary(),
|
||||
}
|
||||
|
||||
# instantiate a new policy for this version of the lambda
|
||||
fn.policy = Policy(fn)
|
||||
self._arns[fn.function_arn] = fn
|
||||
|
||||
def publish_function(self, name):
|
||||
|
|
@ -1010,8 +1006,21 @@ class LambdaBackend(BaseBackend):
|
|||
return True
|
||||
return False
|
||||
|
||||
def add_policy(self, function_name, policy):
|
||||
self.get_function(function_name).policy = policy
|
||||
def add_policy_statement(self, function_name, raw):
|
||||
fn = self.get_function(function_name)
|
||||
fn.policy.add_statement(raw)
|
||||
|
||||
def del_policy_statement(self, function_name, sid, revision=""):
|
||||
fn = self.get_function(function_name)
|
||||
fn.policy.del_statement(sid, revision)
|
||||
|
||||
def get_policy(self, function_name):
|
||||
fn = self.get_function(function_name)
|
||||
return fn.policy.get_policy()
|
||||
|
||||
def get_policy_wire_format(self, function_name):
|
||||
fn = self.get_function(function_name)
|
||||
return fn.policy.wire_format()
|
||||
|
||||
def update_function_code(self, function_name, qualifier, body):
|
||||
fn = self.get_function(function_name, qualifier)
|
||||
|
|
|
|||
134
moto/awslambda/policy.py
Normal file
134
moto/awslambda/policy.py
Normal file
|
|
@ -0,0 +1,134 @@
|
|||
from __future__ import unicode_literals
|
||||
|
||||
import json
|
||||
import uuid
|
||||
|
||||
from six import string_types
|
||||
|
||||
from moto.awslambda.exceptions import PreconditionFailedException
|
||||
|
||||
|
||||
class Policy:
|
||||
def __init__(self, parent):
|
||||
self.revision = str(uuid.uuid4())
|
||||
self.statements = []
|
||||
self.parent = parent
|
||||
|
||||
def wire_format(self):
|
||||
p = self.get_policy()
|
||||
p["Policy"] = json.dumps(p["Policy"])
|
||||
return json.dumps(p)
|
||||
|
||||
def get_policy(self):
|
||||
return {
|
||||
"Policy": {
|
||||
"Version": "2012-10-17",
|
||||
"Id": "default",
|
||||
"Statement": self.statements,
|
||||
},
|
||||
"RevisionId": self.revision,
|
||||
}
|
||||
|
||||
# adds the raw JSON statement to the policy
|
||||
def add_statement(self, raw):
|
||||
policy = json.loads(raw, object_hook=self.decode_policy)
|
||||
if len(policy.revision) > 0 and self.revision != policy.revision:
|
||||
raise PreconditionFailedException(
|
||||
"The RevisionId provided does not match the latest RevisionId"
|
||||
" for the Lambda function or alias. Call the GetFunction or the GetAlias API to retrieve"
|
||||
" the latest RevisionId for your resource."
|
||||
)
|
||||
self.statements.append(policy.statements[0])
|
||||
self.revision = str(uuid.uuid4())
|
||||
|
||||
# removes the statement that matches 'sid' from the policy
|
||||
def del_statement(self, sid, revision=""):
|
||||
if len(revision) > 0 and self.revision != revision:
|
||||
raise PreconditionFailedException(
|
||||
"The RevisionId provided does not match the latest RevisionId"
|
||||
" for the Lambda function or alias. Call the GetFunction or the GetAlias API to retrieve"
|
||||
" the latest RevisionId for your resource."
|
||||
)
|
||||
for statement in self.statements:
|
||||
if "Sid" in statement and statement["Sid"] == sid:
|
||||
self.statements.remove(statement)
|
||||
|
||||
# converts AddPermission request to PolicyStatement
|
||||
# https://docs.aws.amazon.com/lambda/latest/dg/API_AddPermission.html
|
||||
def decode_policy(self, obj):
|
||||
# import pydevd
|
||||
# pydevd.settrace("localhost", port=5678)
|
||||
policy = Policy(self.parent)
|
||||
policy.revision = obj.get("RevisionId", "")
|
||||
|
||||
# set some default values if these keys are not set
|
||||
self.ensure_set(obj, "Effect", "Allow")
|
||||
self.ensure_set(obj, "Resource", self.parent.function_arn + ":$LATEST")
|
||||
self.ensure_set(obj, "StatementId", str(uuid.uuid4()))
|
||||
|
||||
# transform field names and values
|
||||
self.transform_property(obj, "StatementId", "Sid", self.nop_formatter)
|
||||
self.transform_property(obj, "Principal", "Principal", self.principal_formatter)
|
||||
|
||||
self.transform_property(
|
||||
obj, "SourceArn", "SourceArn", self.source_arn_formatter
|
||||
)
|
||||
self.transform_property(
|
||||
obj, "SourceAccount", "SourceAccount", self.source_account_formatter
|
||||
)
|
||||
|
||||
# remove RevisionId and EventSourceToken if they are set
|
||||
self.remove_if_set(obj, ["RevisionId", "EventSourceToken"])
|
||||
|
||||
# merge conditional statements into a single map under the Condition key
|
||||
self.condition_merge(obj)
|
||||
|
||||
# append resulting statement to policy.statements
|
||||
policy.statements.append(obj)
|
||||
|
||||
return policy
|
||||
|
||||
def nop_formatter(self, obj):
|
||||
return obj
|
||||
|
||||
def ensure_set(self, obj, key, value):
|
||||
if key not in obj:
|
||||
obj[key] = value
|
||||
|
||||
def principal_formatter(self, obj):
|
||||
if isinstance(obj, string_types):
|
||||
if obj.endswith(".amazonaws.com"):
|
||||
return {"Service": obj}
|
||||
if obj.endswith(":root"):
|
||||
return {"AWS": obj}
|
||||
return obj
|
||||
|
||||
def source_account_formatter(self, obj):
|
||||
return {"StringEquals": {"AWS:SourceAccount": obj}}
|
||||
|
||||
def source_arn_formatter(self, obj):
|
||||
return {"ArnLike": {"AWS:SourceArn": obj}}
|
||||
|
||||
def transform_property(self, obj, old_name, new_name, formatter):
|
||||
if old_name in obj:
|
||||
obj[new_name] = formatter(obj[old_name])
|
||||
if new_name != old_name:
|
||||
del obj[old_name]
|
||||
|
||||
def remove_if_set(self, obj, keys):
|
||||
for key in keys:
|
||||
if key in obj:
|
||||
del obj[key]
|
||||
|
||||
def condition_merge(self, obj):
|
||||
if "SourceArn" in obj:
|
||||
if "Condition" not in obj:
|
||||
obj["Condition"] = {}
|
||||
obj["Condition"].update(obj["SourceArn"])
|
||||
del obj["SourceArn"]
|
||||
|
||||
if "SourceAccount" in obj:
|
||||
if "Condition" not in obj:
|
||||
obj["Condition"] = {}
|
||||
obj["Condition"].update(obj["SourceAccount"])
|
||||
del obj["SourceAccount"]
|
||||
|
|
@ -120,8 +120,12 @@ class LambdaResponse(BaseResponse):
|
|||
self.setup_class(request, full_url, headers)
|
||||
if request.method == "GET":
|
||||
return self._get_policy(request, full_url, headers)
|
||||
if request.method == "POST":
|
||||
elif request.method == "POST":
|
||||
return self._add_policy(request, full_url, headers)
|
||||
elif request.method == "DELETE":
|
||||
return self._del_policy(request, full_url, headers, self.querystring)
|
||||
else:
|
||||
raise ValueError("Cannot handle {0} request".format(request.method))
|
||||
|
||||
def configuration(self, request, full_url, headers):
|
||||
self.setup_class(request, full_url, headers)
|
||||
|
|
@ -141,9 +145,9 @@ class LambdaResponse(BaseResponse):
|
|||
path = request.path if hasattr(request, "path") else path_url(request.url)
|
||||
function_name = path.split("/")[-2]
|
||||
if self.lambda_backend.get_function(function_name):
|
||||
policy = self.body
|
||||
self.lambda_backend.add_policy(function_name, policy)
|
||||
return 200, {}, json.dumps(dict(Statement=policy))
|
||||
statement = self.body
|
||||
self.lambda_backend.add_policy_statement(function_name, statement)
|
||||
return 200, {}, json.dumps({"Statement": statement})
|
||||
else:
|
||||
return 404, {}, "{}"
|
||||
|
||||
|
|
@ -151,28 +155,42 @@ class LambdaResponse(BaseResponse):
|
|||
path = request.path if hasattr(request, "path") else path_url(request.url)
|
||||
function_name = path.split("/")[-2]
|
||||
if self.lambda_backend.get_function(function_name):
|
||||
lambda_function = self.lambda_backend.get_function(function_name)
|
||||
return (
|
||||
200,
|
||||
{},
|
||||
json.dumps(
|
||||
dict(Policy='{"Statement":[' + lambda_function.policy + "]}")
|
||||
),
|
||||
out = self.lambda_backend.get_policy_wire_format(function_name)
|
||||
return 200, {}, out
|
||||
else:
|
||||
return 404, {}, "{}"
|
||||
|
||||
def _del_policy(self, request, full_url, headers, querystring):
|
||||
path = request.path if hasattr(request, "path") else path_url(request.url)
|
||||
function_name = path.split("/")[-3]
|
||||
statement_id = path.split("/")[-1].split("?")[0]
|
||||
revision = querystring.get("RevisionId", "")
|
||||
if self.lambda_backend.get_function(function_name):
|
||||
self.lambda_backend.del_policy_statement(
|
||||
function_name, statement_id, revision
|
||||
)
|
||||
return 204, {}, "{}"
|
||||
else:
|
||||
return 404, {}, "{}"
|
||||
|
||||
def _invoke(self, request, full_url):
|
||||
response_headers = {}
|
||||
|
||||
function_name = self.path.rsplit("/", 2)[-2]
|
||||
# URL Decode in case it's a ARN:
|
||||
function_name = unquote(self.path.rsplit("/", 2)[-2])
|
||||
qualifier = self._get_param("qualifier")
|
||||
|
||||
response_header, payload = self.lambda_backend.invoke(
|
||||
function_name, qualifier, self.body, self.headers, response_headers
|
||||
)
|
||||
if payload:
|
||||
return 202, response_headers, payload
|
||||
if request.headers["X-Amz-Invocation-Type"] == "Event":
|
||||
status_code = 202
|
||||
elif request.headers["X-Amz-Invocation-Type"] == "DryRun":
|
||||
status_code = 204
|
||||
else:
|
||||
status_code = 200
|
||||
return status_code, response_headers, payload
|
||||
else:
|
||||
return 404, response_headers, "{}"
|
||||
|
||||
|
|
@ -283,7 +301,7 @@ class LambdaResponse(BaseResponse):
|
|||
code["Configuration"]["FunctionArn"] += ":$LATEST"
|
||||
return 200, {}, json.dumps(code)
|
||||
else:
|
||||
return 404, {}, "{}"
|
||||
return 404, {"x-amzn-ErrorType": "ResourceNotFoundException"}, "{}"
|
||||
|
||||
def _get_aws_region(self, full_url):
|
||||
region = self.region_regex.search(full_url)
|
||||
|
|
|
|||
|
|
@ -6,14 +6,16 @@ url_bases = ["https?://lambda.(.+).amazonaws.com"]
|
|||
response = LambdaResponse()
|
||||
|
||||
url_paths = {
|
||||
"{0}/(?P<api_version>[^/]+)/functions/?$": response.root,
|
||||
r"{0}/(?P<api_version>[^/]+)/functions/?$": response.root,
|
||||
r"{0}/(?P<api_version>[^/]+)/functions/(?P<function_name>[\w_:%-]+)/?$": response.function,
|
||||
r"{0}/(?P<api_version>[^/]+)/functions/(?P<function_name>[\w_-]+)/versions/?$": response.versions,
|
||||
r"{0}/(?P<api_version>[^/]+)/event-source-mappings/?$": response.event_source_mappings,
|
||||
r"{0}/(?P<api_version>[^/]+)/event-source-mappings/(?P<UUID>[\w_-]+)/?$": response.event_source_mapping,
|
||||
r"{0}/(?P<api_version>[^/]+)/functions/(?P<function_name>[\w_-]+)/invocations/?$": response.invoke,
|
||||
r"{0}/(?P<api_version>[^/]+)/functions/(?P<resource_arn>.+)/invocations/?$": response.invoke,
|
||||
r"{0}/(?P<api_version>[^/]+)/functions/(?P<function_name>[\w_-]+)/invoke-async/?$": response.invoke_async,
|
||||
r"{0}/(?P<api_version>[^/]+)/tags/(?P<resource_arn>.+)": response.tag,
|
||||
r"{0}/(?P<api_version>[^/]+)/functions/(?P<function_name>[\w_-]+)/policy/(?P<statement_id>[\w_-]+)$": response.policy,
|
||||
r"{0}/(?P<api_version>[^/]+)/functions/(?P<function_name>[\w_-]+)/policy/?$": response.policy,
|
||||
r"{0}/(?P<api_version>[^/]+)/functions/(?P<function_name>[\w_-]+)/configuration/?$": response.configuration,
|
||||
r"{0}/(?P<api_version>[^/]+)/functions/(?P<function_name>[\w_-]+)/code/?$": response.code,
|
||||
|
|
|
|||
|
|
@ -677,6 +677,8 @@ class CloudFormationBackend(BaseBackend):
|
|||
|
||||
def list_stack_resources(self, stack_name_or_id):
|
||||
stack = self.get_stack(stack_name_or_id)
|
||||
if stack is None:
|
||||
return None
|
||||
return stack.stack_resources
|
||||
|
||||
def delete_stack(self, name_or_stack_id):
|
||||
|
|
|
|||
|
|
@ -229,6 +229,9 @@ class CloudFormationResponse(BaseResponse):
|
|||
stack_name_or_id = self._get_param("StackName")
|
||||
resources = self.cloudformation_backend.list_stack_resources(stack_name_or_id)
|
||||
|
||||
if resources is None:
|
||||
raise ValidationError(stack_name_or_id)
|
||||
|
||||
template = self.response_template(LIST_STACKS_RESOURCES_RESPONSE)
|
||||
return template.render(resources=resources)
|
||||
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ from jose import jws
|
|||
|
||||
from moto.compat import OrderedDict
|
||||
from moto.core import BaseBackend, BaseModel
|
||||
from moto.core import ACCOUNT_ID as DEFAULT_ACCOUNT_ID
|
||||
from .exceptions import (
|
||||
GroupExistsException,
|
||||
NotAuthorizedError,
|
||||
|
|
@ -69,6 +70,9 @@ class CognitoIdpUserPool(BaseModel):
|
|||
def __init__(self, region, name, extended_config):
|
||||
self.region = region
|
||||
self.id = "{}_{}".format(self.region, str(uuid.uuid4().hex))
|
||||
self.arn = "arn:aws:cognito-idp:{}:{}:userpool/{}".format(
|
||||
self.region, DEFAULT_ACCOUNT_ID, self.id
|
||||
)
|
||||
self.name = name
|
||||
self.status = None
|
||||
self.extended_config = extended_config or {}
|
||||
|
|
@ -91,6 +95,7 @@ class CognitoIdpUserPool(BaseModel):
|
|||
def _base_json(self):
|
||||
return {
|
||||
"Id": self.id,
|
||||
"Arn": self.arn,
|
||||
"Name": self.name,
|
||||
"Status": self.status,
|
||||
"CreationDate": time.mktime(self.creation_date.timetuple()),
|
||||
|
|
@ -108,7 +113,9 @@ class CognitoIdpUserPool(BaseModel):
|
|||
|
||||
return user_pool_json
|
||||
|
||||
def create_jwt(self, client_id, username, expires_in=60 * 60, extra_data={}):
|
||||
def create_jwt(
|
||||
self, client_id, username, token_use, expires_in=60 * 60, extra_data={}
|
||||
):
|
||||
now = int(time.time())
|
||||
payload = {
|
||||
"iss": "https://cognito-idp.{}.amazonaws.com/{}".format(
|
||||
|
|
@ -116,7 +123,7 @@ class CognitoIdpUserPool(BaseModel):
|
|||
),
|
||||
"sub": self.users[username].id,
|
||||
"aud": client_id,
|
||||
"token_use": "id",
|
||||
"token_use": token_use,
|
||||
"auth_time": now,
|
||||
"exp": now + expires_in,
|
||||
}
|
||||
|
|
@ -125,7 +132,10 @@ class CognitoIdpUserPool(BaseModel):
|
|||
return jws.sign(payload, self.json_web_key, algorithm="RS256"), expires_in
|
||||
|
||||
def create_id_token(self, client_id, username):
|
||||
id_token, expires_in = self.create_jwt(client_id, username)
|
||||
extra_data = self.get_user_extra_data_by_client_id(client_id, username)
|
||||
id_token, expires_in = self.create_jwt(
|
||||
client_id, username, "id", extra_data=extra_data
|
||||
)
|
||||
self.id_tokens[id_token] = (client_id, username)
|
||||
return id_token, expires_in
|
||||
|
||||
|
|
@ -135,10 +145,7 @@ class CognitoIdpUserPool(BaseModel):
|
|||
return refresh_token
|
||||
|
||||
def create_access_token(self, client_id, username):
|
||||
extra_data = self.get_user_extra_data_by_client_id(client_id, username)
|
||||
access_token, expires_in = self.create_jwt(
|
||||
client_id, username, extra_data=extra_data
|
||||
)
|
||||
access_token, expires_in = self.create_jwt(client_id, username, "access")
|
||||
self.access_tokens[access_token] = (client_id, username)
|
||||
return access_token, expires_in
|
||||
|
||||
|
|
@ -562,12 +569,17 @@ class CognitoIdpBackend(BaseBackend):
|
|||
user.groups.discard(group)
|
||||
|
||||
# User
|
||||
def admin_create_user(self, user_pool_id, username, temporary_password, attributes):
|
||||
def admin_create_user(
|
||||
self, user_pool_id, username, message_action, temporary_password, attributes
|
||||
):
|
||||
user_pool = self.user_pools.get(user_pool_id)
|
||||
if not user_pool:
|
||||
raise ResourceNotFoundError(user_pool_id)
|
||||
|
||||
if username in user_pool.users:
|
||||
if message_action and message_action == "RESEND":
|
||||
if username not in user_pool.users:
|
||||
raise UserNotFoundError(username)
|
||||
elif username in user_pool.users:
|
||||
raise UsernameExistsException(username)
|
||||
|
||||
user = CognitoIdpUser(
|
||||
|
|
|
|||
|
|
@ -259,10 +259,12 @@ class CognitoIdpResponse(BaseResponse):
|
|||
def admin_create_user(self):
|
||||
user_pool_id = self._get_param("UserPoolId")
|
||||
username = self._get_param("Username")
|
||||
message_action = self._get_param("MessageAction")
|
||||
temporary_password = self._get_param("TemporaryPassword")
|
||||
user = cognitoidp_backends[self.region].admin_create_user(
|
||||
user_pool_id,
|
||||
username,
|
||||
message_action,
|
||||
temporary_password,
|
||||
self._get_param("UserAttributes", []),
|
||||
)
|
||||
|
|
@ -279,9 +281,18 @@ class CognitoIdpResponse(BaseResponse):
|
|||
user_pool_id = self._get_param("UserPoolId")
|
||||
limit = self._get_param("Limit")
|
||||
token = self._get_param("PaginationToken")
|
||||
filt = self._get_param("Filter")
|
||||
users, token = cognitoidp_backends[self.region].list_users(
|
||||
user_pool_id, limit=limit, pagination_token=token
|
||||
)
|
||||
if filt:
|
||||
name, value = filt.replace('"', "").split("=")
|
||||
users = [
|
||||
user
|
||||
for user in users
|
||||
for attribute in user.attributes
|
||||
if attribute["Name"] == name and attribute["Value"] == value
|
||||
]
|
||||
response = {"Users": [user.to_json(extended=True) for user in users]}
|
||||
if token:
|
||||
response["PaginationToken"] = str(token)
|
||||
|
|
|
|||
|
|
@ -43,7 +43,7 @@ from moto.config.exceptions import (
|
|||
)
|
||||
|
||||
from moto.core import BaseBackend, BaseModel
|
||||
from moto.s3.config import s3_config_query
|
||||
from moto.s3.config import s3_account_public_access_block_query, s3_config_query
|
||||
|
||||
from moto.core import ACCOUNT_ID as DEFAULT_ACCOUNT_ID
|
||||
|
||||
|
|
@ -58,7 +58,10 @@ POP_STRINGS = [
|
|||
DEFAULT_PAGE_SIZE = 100
|
||||
|
||||
# Map the Config resource type to a backend:
|
||||
RESOURCE_MAP = {"AWS::S3::Bucket": s3_config_query}
|
||||
RESOURCE_MAP = {
|
||||
"AWS::S3::Bucket": s3_config_query,
|
||||
"AWS::S3::AccountPublicAccessBlock": s3_account_public_access_block_query,
|
||||
}
|
||||
|
||||
|
||||
def datetime2int(date):
|
||||
|
|
@ -867,16 +870,17 @@ class ConfigBackend(BaseBackend):
|
|||
backend_region=backend_query_region,
|
||||
)
|
||||
|
||||
result = {
|
||||
"resourceIdentifiers": [
|
||||
{
|
||||
"resourceType": identifier["type"],
|
||||
"resourceId": identifier["id"],
|
||||
"resourceName": identifier["name"],
|
||||
}
|
||||
for identifier in identifiers
|
||||
]
|
||||
}
|
||||
resource_identifiers = []
|
||||
for identifier in identifiers:
|
||||
item = {"resourceType": identifier["type"], "resourceId": identifier["id"]}
|
||||
|
||||
# Some resource types lack names:
|
||||
if identifier.get("name"):
|
||||
item["resourceName"] = identifier["name"]
|
||||
|
||||
resource_identifiers.append(item)
|
||||
|
||||
result = {"resourceIdentifiers": resource_identifiers}
|
||||
|
||||
if new_token:
|
||||
result["nextToken"] = new_token
|
||||
|
|
@ -927,18 +931,21 @@ class ConfigBackend(BaseBackend):
|
|||
resource_region=resource_region,
|
||||
)
|
||||
|
||||
result = {
|
||||
"ResourceIdentifiers": [
|
||||
{
|
||||
"SourceAccountId": DEFAULT_ACCOUNT_ID,
|
||||
"SourceRegion": identifier["region"],
|
||||
"ResourceType": identifier["type"],
|
||||
"ResourceId": identifier["id"],
|
||||
"ResourceName": identifier["name"],
|
||||
}
|
||||
for identifier in identifiers
|
||||
]
|
||||
}
|
||||
resource_identifiers = []
|
||||
for identifier in identifiers:
|
||||
item = {
|
||||
"SourceAccountId": DEFAULT_ACCOUNT_ID,
|
||||
"SourceRegion": identifier["region"],
|
||||
"ResourceType": identifier["type"],
|
||||
"ResourceId": identifier["id"],
|
||||
}
|
||||
|
||||
if identifier.get("name"):
|
||||
item["ResourceName"] = identifier["name"]
|
||||
|
||||
resource_identifiers.append(item)
|
||||
|
||||
result = {"ResourceIdentifiers": resource_identifiers}
|
||||
|
||||
if new_token:
|
||||
result["NextToken"] = new_token
|
||||
|
|
|
|||
|
|
@ -606,12 +606,13 @@ class ConfigQueryModel(object):
|
|||
As such, the proper way to implement is to first obtain a full list of results from all the region backends, and then filter
|
||||
from there. It may be valuable to make this a concatenation of the region and resource name.
|
||||
|
||||
:param resource_region:
|
||||
:param resource_ids:
|
||||
:param resource_name:
|
||||
:param limit:
|
||||
:param next_token:
|
||||
:param resource_ids: A list of resource IDs
|
||||
:param resource_name: The individual name of a resource
|
||||
:param limit: How many per page
|
||||
:param next_token: The item that will page on
|
||||
:param backend_region: The region for the backend to pull results from. Set to `None` if this is an aggregated query.
|
||||
:param resource_region: The region for where the resources reside to pull results from. Set to `None` if this is a
|
||||
non-aggregated query.
|
||||
:return: This should return a list of Dicts that have the following fields:
|
||||
[
|
||||
{
|
||||
|
|
|
|||
|
|
@ -977,10 +977,8 @@ class OpLessThan(Op):
|
|||
lhs = self.lhs.expr(item)
|
||||
rhs = self.rhs.expr(item)
|
||||
# In python3 None is not a valid comparator when using < or > so must be handled specially
|
||||
if lhs and rhs:
|
||||
if lhs is not None and rhs is not None:
|
||||
return lhs < rhs
|
||||
elif lhs is None and rhs:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
|
|
@ -992,10 +990,8 @@ class OpGreaterThan(Op):
|
|||
lhs = self.lhs.expr(item)
|
||||
rhs = self.rhs.expr(item)
|
||||
# In python3 None is not a valid comparator when using < or > so must be handled specially
|
||||
if lhs and rhs:
|
||||
if lhs is not None and rhs is not None:
|
||||
return lhs > rhs
|
||||
elif lhs and rhs is None:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
|
|
@ -1025,10 +1021,8 @@ class OpLessThanOrEqual(Op):
|
|||
lhs = self.lhs.expr(item)
|
||||
rhs = self.rhs.expr(item)
|
||||
# In python3 None is not a valid comparator when using < or > so must be handled specially
|
||||
if lhs and rhs:
|
||||
if lhs is not None and rhs is not None:
|
||||
return lhs <= rhs
|
||||
elif lhs is None and rhs or lhs is None and rhs is None:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
|
|
@ -1040,10 +1034,8 @@ class OpGreaterThanOrEqual(Op):
|
|||
lhs = self.lhs.expr(item)
|
||||
rhs = self.rhs.expr(item)
|
||||
# In python3 None is not a valid comparator when using < or > so must be handled specially
|
||||
if lhs and rhs:
|
||||
if lhs is not None and rhs is not None:
|
||||
return lhs >= rhs
|
||||
elif lhs and rhs is None or lhs is None and rhs is None:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
|
|
|
|||
|
|
@ -448,16 +448,21 @@ class Item(BaseModel):
|
|||
if list_append_re:
|
||||
new_value = expression_attribute_values[list_append_re.group(2).strip()]
|
||||
old_list_key = list_append_re.group(1)
|
||||
# Get the existing value
|
||||
old_list = self.attrs[old_list_key.split(".")[0]]
|
||||
if "." in old_list_key:
|
||||
# Value is nested inside a map - find the appropriate child attr
|
||||
old_list = old_list.child_attr(
|
||||
".".join(old_list_key.split(".")[1:])
|
||||
# old_key could be a function itself (if_not_exists)
|
||||
if old_list_key.startswith("if_not_exists"):
|
||||
old_list = DynamoType(
|
||||
expression_attribute_values[self._get_default(old_list_key)]
|
||||
)
|
||||
else:
|
||||
old_list = self.attrs[old_list_key.split(".")[0]]
|
||||
if "." in old_list_key:
|
||||
# Value is nested inside a map - find the appropriate child attr
|
||||
old_list = old_list.child_attr(
|
||||
".".join(old_list_key.split(".")[1:])
|
||||
)
|
||||
if not old_list.is_list():
|
||||
raise ParamValidationError
|
||||
old_list.value.extend(new_value["L"])
|
||||
old_list.value.extend([DynamoType(v) for v in new_value["L"]])
|
||||
value = old_list
|
||||
return value
|
||||
|
||||
|
|
|
|||
|
|
@ -508,6 +508,13 @@ class DynamoHandler(BaseResponse):
|
|||
# 'KeyConditions': {u'forum_name': {u'ComparisonOperator': u'EQ', u'AttributeValueList': [{u'S': u'the-key'}]}}
|
||||
key_conditions = self.body.get("KeyConditions")
|
||||
query_filters = self.body.get("QueryFilter")
|
||||
|
||||
if not (key_conditions or query_filters):
|
||||
return self.error(
|
||||
"com.amazonaws.dynamodb.v20111205#ValidationException",
|
||||
"Either KeyConditions or QueryFilter should be present",
|
||||
)
|
||||
|
||||
if key_conditions:
|
||||
(
|
||||
hash_key_name,
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ from moto.core.utils import (
|
|||
iso_8601_datetime_with_milliseconds,
|
||||
camelcase_to_underscores,
|
||||
)
|
||||
from moto.iam.models import ACCOUNT_ID
|
||||
from .exceptions import (
|
||||
CidrLimitExceeded,
|
||||
DependencyViolationError,
|
||||
|
|
@ -139,18 +140,23 @@ from .utils import (
|
|||
rsa_public_key_fingerprint,
|
||||
)
|
||||
|
||||
INSTANCE_TYPES = json.load(
|
||||
open(resource_filename(__name__, "resources/instance_types.json"), "r")
|
||||
)
|
||||
AMIS = json.load(
|
||||
open(
|
||||
os.environ.get("MOTO_AMIS_PATH")
|
||||
or resource_filename(__name__, "resources/amis.json"),
|
||||
"r",
|
||||
)
|
||||
|
||||
def _load_resource(filename):
|
||||
with open(filename, "r") as f:
|
||||
return json.load(f)
|
||||
|
||||
|
||||
INSTANCE_TYPES = _load_resource(
|
||||
resource_filename(__name__, "resources/instance_types.json")
|
||||
)
|
||||
|
||||
OWNER_ID = "111122223333"
|
||||
AMIS = _load_resource(
|
||||
os.environ.get("MOTO_AMIS_PATH")
|
||||
or resource_filename(__name__, "resources/amis.json"),
|
||||
)
|
||||
|
||||
|
||||
OWNER_ID = ACCOUNT_ID
|
||||
|
||||
|
||||
def utc_date_and_time():
|
||||
|
|
@ -1336,7 +1342,7 @@ class AmiBackend(object):
|
|||
source_ami=None,
|
||||
name=name,
|
||||
description=description,
|
||||
owner_id=context.get_current_user() if context else OWNER_ID,
|
||||
owner_id=OWNER_ID,
|
||||
)
|
||||
self.amis[ami_id] = ami
|
||||
return ami
|
||||
|
|
@ -1387,14 +1393,7 @@ class AmiBackend(object):
|
|||
# Limit by owner ids
|
||||
if owners:
|
||||
# support filtering by Owners=['self']
|
||||
owners = list(
|
||||
map(
|
||||
lambda o: context.get_current_user()
|
||||
if context and o == "self"
|
||||
else o,
|
||||
owners,
|
||||
)
|
||||
)
|
||||
owners = list(map(lambda o: OWNER_ID if o == "self" else o, owners,))
|
||||
images = [ami for ami in images if ami.owner_id in owners]
|
||||
|
||||
# Generic filters
|
||||
|
|
|
|||
|
|
@ -104,7 +104,7 @@ class SecurityGroups(BaseResponse):
|
|||
if self.is_not_dryrun("GrantSecurityGroupIngress"):
|
||||
for args in self._process_rules_from_querystring():
|
||||
self.ec2_backend.authorize_security_group_ingress(*args)
|
||||
return AUTHORIZE_SECURITY_GROUP_INGRESS_REPONSE
|
||||
return AUTHORIZE_SECURITY_GROUP_INGRESS_RESPONSE
|
||||
|
||||
def create_security_group(self):
|
||||
name = self._get_param("GroupName")
|
||||
|
|
@ -158,7 +158,7 @@ class SecurityGroups(BaseResponse):
|
|||
if self.is_not_dryrun("RevokeSecurityGroupIngress"):
|
||||
for args in self._process_rules_from_querystring():
|
||||
self.ec2_backend.revoke_security_group_ingress(*args)
|
||||
return REVOKE_SECURITY_GROUP_INGRESS_REPONSE
|
||||
return REVOKE_SECURITY_GROUP_INGRESS_RESPONSE
|
||||
|
||||
|
||||
CREATE_SECURITY_GROUP_RESPONSE = """<CreateSecurityGroupResponse xmlns="http://ec2.amazonaws.com/doc/2013-10-15/">
|
||||
|
|
@ -265,12 +265,12 @@ DESCRIBE_SECURITY_GROUPS_RESPONSE = (
|
|||
</DescribeSecurityGroupsResponse>"""
|
||||
)
|
||||
|
||||
AUTHORIZE_SECURITY_GROUP_INGRESS_REPONSE = """<AuthorizeSecurityGroupIngressResponse xmlns="http://ec2.amazonaws.com/doc/2013-10-15/">
|
||||
AUTHORIZE_SECURITY_GROUP_INGRESS_RESPONSE = """<AuthorizeSecurityGroupIngressResponse xmlns="http://ec2.amazonaws.com/doc/2013-10-15/">
|
||||
<requestId>59dbff89-35bd-4eac-99ed-be587EXAMPLE</requestId>
|
||||
<return>true</return>
|
||||
</AuthorizeSecurityGroupIngressResponse>"""
|
||||
|
||||
REVOKE_SECURITY_GROUP_INGRESS_REPONSE = """<RevokeSecurityGroupIngressResponse xmlns="http://ec2.amazonaws.com/doc/2013-10-15/">
|
||||
REVOKE_SECURITY_GROUP_INGRESS_RESPONSE = """<RevokeSecurityGroupIngressResponse xmlns="http://ec2.amazonaws.com/doc/2013-10-15/">
|
||||
<requestId>59dbff89-35bd-4eac-99ed-be587EXAMPLE</requestId>
|
||||
<return>true</return>
|
||||
</RevokeSecurityGroupIngressResponse>"""
|
||||
|
|
|
|||
|
|
@ -118,6 +118,7 @@ class TaskDefinition(BaseObject):
|
|||
revision,
|
||||
container_definitions,
|
||||
region_name,
|
||||
network_mode=None,
|
||||
volumes=None,
|
||||
tags=None,
|
||||
):
|
||||
|
|
@ -132,6 +133,10 @@ class TaskDefinition(BaseObject):
|
|||
self.volumes = []
|
||||
else:
|
||||
self.volumes = volumes
|
||||
if network_mode is None:
|
||||
self.network_mode = "bridge"
|
||||
else:
|
||||
self.network_mode = network_mode
|
||||
|
||||
@property
|
||||
def response_object(self):
|
||||
|
|
@ -553,7 +558,7 @@ class EC2ContainerServiceBackend(BaseBackend):
|
|||
raise Exception("{0} is not a cluster".format(cluster_name))
|
||||
|
||||
def register_task_definition(
|
||||
self, family, container_definitions, volumes, tags=None
|
||||
self, family, container_definitions, volumes=None, network_mode=None, tags=None
|
||||
):
|
||||
if family in self.task_definitions:
|
||||
last_id = self._get_last_task_definition_revision_id(family)
|
||||
|
|
@ -562,7 +567,13 @@ class EC2ContainerServiceBackend(BaseBackend):
|
|||
self.task_definitions[family] = {}
|
||||
revision = 1
|
||||
task_definition = TaskDefinition(
|
||||
family, revision, container_definitions, self.region_name, volumes, tags
|
||||
family,
|
||||
revision,
|
||||
container_definitions,
|
||||
self.region_name,
|
||||
volumes=volumes,
|
||||
network_mode=network_mode,
|
||||
tags=tags,
|
||||
)
|
||||
self.task_definitions[family][revision] = task_definition
|
||||
|
||||
|
|
|
|||
|
|
@ -62,8 +62,13 @@ class EC2ContainerServiceResponse(BaseResponse):
|
|||
container_definitions = self._get_param("containerDefinitions")
|
||||
volumes = self._get_param("volumes")
|
||||
tags = self._get_param("tags")
|
||||
network_mode = self._get_param("networkMode")
|
||||
task_definition = self.ecs_backend.register_task_definition(
|
||||
family, container_definitions, volumes, tags
|
||||
family,
|
||||
container_definitions,
|
||||
volumes=volumes,
|
||||
network_mode=network_mode,
|
||||
tags=tags,
|
||||
)
|
||||
return json.dumps({"taskDefinition": task_definition.response_object})
|
||||
|
||||
|
|
|
|||
|
|
@ -143,6 +143,9 @@ class EventsBackend(BaseBackend):
|
|||
|
||||
def delete_rule(self, name):
|
||||
self.rules_order.pop(self.rules_order.index(name))
|
||||
arn = self.rules.get(name).arn
|
||||
if self.tagger.has_tags(arn):
|
||||
self.tagger.delete_all_tags_for_resource(arn)
|
||||
return self.rules.pop(name) is not None
|
||||
|
||||
def describe_rule(self, name):
|
||||
|
|
@ -362,32 +365,41 @@ class EventsBackend(BaseBackend):
|
|||
)
|
||||
|
||||
self.event_buses.pop(name, None)
|
||||
|
||||
|
||||
def list_tags_for_resource(self, arn):
|
||||
name = arn.split('/')[-1]
|
||||
if name in self.rules:
|
||||
return self.tagger.list_tags_for_resource(self.rules[name].arn)
|
||||
raise JsonRESTError(
|
||||
"ResourceNotFoundException", "An entity that you specified does not exist."
|
||||
)
|
||||
"ResourceNotFoundException", "An entity that you specified does not exist."
|
||||
)
|
||||
|
||||
def list_tags_for_resource(self, arn):
|
||||
name = arn.split("/")[-1]
|
||||
if name in self.rules:
|
||||
return self.tagger.list_tags_for_resource(self.rules[name].arn)
|
||||
raise JsonRESTError(
|
||||
"ResourceNotFoundException", "An entity that you specified does not exist."
|
||||
)
|
||||
|
||||
def tag_resource(self, arn, tags):
|
||||
name = arn.split('/')[-1]
|
||||
name = arn.split("/")[-1]
|
||||
if name in self.rules:
|
||||
self.tagger.tag_resource(self.rules[name].arn, tags)
|
||||
return {}
|
||||
raise JsonRESTError(
|
||||
"ResourceNotFoundException", "An entity that you specified does not exist."
|
||||
)
|
||||
"ResourceNotFoundException", "An entity that you specified does not exist."
|
||||
)
|
||||
|
||||
def untag_resource(self, arn, tag_names):
|
||||
name = arn.split('/')[-1]
|
||||
name = arn.split("/")[-1]
|
||||
if name in self.rules:
|
||||
self.tagger.untag_resource_using_names(self.rules[name].arn, tag_names)
|
||||
return {}
|
||||
raise JsonRESTError(
|
||||
"ResourceNotFoundException", "An entity that you specified does not exist."
|
||||
)
|
||||
"ResourceNotFoundException", "An entity that you specified does not exist."
|
||||
)
|
||||
|
||||
|
||||
events_backends = {}
|
||||
for region in Session().get_available_regions("events"):
|
||||
|
|
|
|||
|
|
@ -563,6 +563,10 @@ class IamResponse(BaseResponse):
|
|||
|
||||
def create_access_key(self):
|
||||
user_name = self._get_param("UserName")
|
||||
if not user_name:
|
||||
access_key_id = self.get_current_user()
|
||||
access_key = iam_backend.get_access_key_last_used(access_key_id)
|
||||
user_name = access_key["user_name"]
|
||||
|
||||
key = iam_backend.create_access_key(user_name)
|
||||
template = self.response_template(CREATE_ACCESS_KEY_TEMPLATE)
|
||||
|
|
@ -572,6 +576,10 @@ class IamResponse(BaseResponse):
|
|||
user_name = self._get_param("UserName")
|
||||
access_key_id = self._get_param("AccessKeyId")
|
||||
status = self._get_param("Status")
|
||||
if not user_name:
|
||||
access_key = iam_backend.get_access_key_last_used(access_key_id)
|
||||
user_name = access_key["user_name"]
|
||||
|
||||
iam_backend.update_access_key(user_name, access_key_id, status)
|
||||
template = self.response_template(GENERIC_EMPTY_TEMPLATE)
|
||||
return template.render(name="UpdateAccessKey")
|
||||
|
|
@ -587,6 +595,11 @@ class IamResponse(BaseResponse):
|
|||
|
||||
def list_access_keys(self):
|
||||
user_name = self._get_param("UserName")
|
||||
if not user_name:
|
||||
access_key_id = self.get_current_user()
|
||||
access_key = iam_backend.get_access_key_last_used(access_key_id)
|
||||
user_name = access_key["user_name"]
|
||||
|
||||
keys = iam_backend.get_all_access_keys(user_name)
|
||||
template = self.response_template(LIST_ACCESS_KEYS_TEMPLATE)
|
||||
return template.render(user_name=user_name, keys=keys)
|
||||
|
|
@ -594,6 +607,9 @@ class IamResponse(BaseResponse):
|
|||
def delete_access_key(self):
|
||||
user_name = self._get_param("UserName")
|
||||
access_key_id = self._get_param("AccessKeyId")
|
||||
if not user_name:
|
||||
access_key = iam_backend.get_access_key_last_used(access_key_id)
|
||||
user_name = access_key["user_name"]
|
||||
|
||||
iam_backend.delete_access_key(access_key_id, user_name)
|
||||
template = self.response_template(GENERIC_EMPTY_TEMPLATE)
|
||||
|
|
|
|||
|
|
@ -22,6 +22,15 @@ class InvalidRequestException(IoTClientError):
|
|||
)
|
||||
|
||||
|
||||
class InvalidStateTransitionException(IoTClientError):
|
||||
def __init__(self, msg=None):
|
||||
self.code = 409
|
||||
super(InvalidStateTransitionException, self).__init__(
|
||||
"InvalidStateTransitionException",
|
||||
msg or "An attempt was made to change to an invalid state.",
|
||||
)
|
||||
|
||||
|
||||
class VersionConflictException(IoTClientError):
|
||||
def __init__(self, name):
|
||||
self.code = 409
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ from .exceptions import (
|
|||
DeleteConflictException,
|
||||
ResourceNotFoundException,
|
||||
InvalidRequestException,
|
||||
InvalidStateTransitionException,
|
||||
VersionConflictException,
|
||||
)
|
||||
|
||||
|
|
@ -29,7 +30,7 @@ class FakeThing(BaseModel):
|
|||
self.attributes = attributes
|
||||
self.arn = "arn:aws:iot:%s:1:thing/%s" % (self.region_name, thing_name)
|
||||
self.version = 1
|
||||
# TODO: we need to handle 'version'?
|
||||
# TODO: we need to handle "version"?
|
||||
|
||||
# for iot-data
|
||||
self.thing_shadow = None
|
||||
|
|
@ -174,18 +175,19 @@ class FakeCertificate(BaseModel):
|
|||
|
||||
|
||||
class FakePolicy(BaseModel):
|
||||
def __init__(self, name, document, region_name):
|
||||
def __init__(self, name, document, region_name, default_version_id="1"):
|
||||
self.name = name
|
||||
self.document = document
|
||||
self.arn = "arn:aws:iot:%s:1:policy/%s" % (region_name, name)
|
||||
self.version = "1" # TODO: handle version
|
||||
self.default_version_id = default_version_id
|
||||
self.versions = [FakePolicyVersion(self.name, document, True, region_name)]
|
||||
|
||||
def to_get_dict(self):
|
||||
return {
|
||||
"policyName": self.name,
|
||||
"policyArn": self.arn,
|
||||
"policyDocument": self.document,
|
||||
"defaultVersionId": self.version,
|
||||
"defaultVersionId": self.default_version_id,
|
||||
}
|
||||
|
||||
def to_dict_at_creation(self):
|
||||
|
|
@ -193,13 +195,52 @@ class FakePolicy(BaseModel):
|
|||
"policyName": self.name,
|
||||
"policyArn": self.arn,
|
||||
"policyDocument": self.document,
|
||||
"policyVersionId": self.version,
|
||||
"policyVersionId": self.default_version_id,
|
||||
}
|
||||
|
||||
def to_dict(self):
|
||||
return {"policyName": self.name, "policyArn": self.arn}
|
||||
|
||||
|
||||
class FakePolicyVersion(object):
|
||||
def __init__(self, policy_name, document, is_default, region_name):
|
||||
self.name = policy_name
|
||||
self.arn = "arn:aws:iot:%s:1:policy/%s" % (region_name, policy_name)
|
||||
self.document = document or {}
|
||||
self.is_default = is_default
|
||||
self.version_id = "1"
|
||||
|
||||
self.create_datetime = time.mktime(datetime(2015, 1, 1).timetuple())
|
||||
self.last_modified_datetime = time.mktime(datetime(2015, 1, 2).timetuple())
|
||||
|
||||
def to_get_dict(self):
|
||||
return {
|
||||
"policyName": self.name,
|
||||
"policyArn": self.arn,
|
||||
"policyDocument": self.document,
|
||||
"policyVersionId": self.version_id,
|
||||
"isDefaultVersion": self.is_default,
|
||||
"creationDate": self.create_datetime,
|
||||
"lastModifiedDate": self.last_modified_datetime,
|
||||
"generationId": self.version_id,
|
||||
}
|
||||
|
||||
def to_dict_at_creation(self):
|
||||
return {
|
||||
"policyArn": self.arn,
|
||||
"policyDocument": self.document,
|
||||
"policyVersionId": self.version_id,
|
||||
"isDefaultVersion": self.is_default,
|
||||
}
|
||||
|
||||
def to_dict(self):
|
||||
return {
|
||||
"versionId": self.version_id,
|
||||
"isDefaultVersion": self.is_default,
|
||||
"createDate": self.create_datetime,
|
||||
}
|
||||
|
||||
|
||||
class FakeJob(BaseModel):
|
||||
JOB_ID_REGEX_PATTERN = "[a-zA-Z0-9_-]"
|
||||
JOB_ID_REGEX = re.compile(JOB_ID_REGEX_PATTERN)
|
||||
|
|
@ -226,12 +267,14 @@ class FakeJob(BaseModel):
|
|||
self.targets = targets
|
||||
self.document_source = document_source
|
||||
self.document = document
|
||||
self.force = False
|
||||
self.description = description
|
||||
self.presigned_url_config = presigned_url_config
|
||||
self.target_selection = target_selection
|
||||
self.job_executions_rollout_config = job_executions_rollout_config
|
||||
self.status = None # IN_PROGRESS | CANCELED | COMPLETED
|
||||
self.status = "QUEUED" # IN_PROGRESS | CANCELED | COMPLETED
|
||||
self.comment = None
|
||||
self.reason_code = None
|
||||
self.created_at = time.mktime(datetime(2015, 1, 1).timetuple())
|
||||
self.last_updated_at = time.mktime(datetime(2015, 1, 1).timetuple())
|
||||
self.completed_at = None
|
||||
|
|
@ -258,9 +301,11 @@ class FakeJob(BaseModel):
|
|||
"jobExecutionsRolloutConfig": self.job_executions_rollout_config,
|
||||
"status": self.status,
|
||||
"comment": self.comment,
|
||||
"forceCanceled": self.force,
|
||||
"reasonCode": self.reason_code,
|
||||
"createdAt": self.created_at,
|
||||
"lastUpdatedAt": self.last_updated_at,
|
||||
"completedAt": self.completedAt,
|
||||
"completedAt": self.completed_at,
|
||||
"jobProcessDetails": self.job_process_details,
|
||||
"documentParameters": self.document_parameters,
|
||||
"document": self.document,
|
||||
|
|
@ -275,12 +320,67 @@ class FakeJob(BaseModel):
|
|||
return regex_match and length_match
|
||||
|
||||
|
||||
class FakeJobExecution(BaseModel):
|
||||
def __init__(
|
||||
self,
|
||||
job_id,
|
||||
thing_arn,
|
||||
status="QUEUED",
|
||||
force_canceled=False,
|
||||
status_details_map={},
|
||||
):
|
||||
self.job_id = job_id
|
||||
self.status = status # IN_PROGRESS | CANCELED | COMPLETED
|
||||
self.force_canceled = force_canceled
|
||||
self.status_details_map = status_details_map
|
||||
self.thing_arn = thing_arn
|
||||
self.queued_at = time.mktime(datetime(2015, 1, 1).timetuple())
|
||||
self.started_at = time.mktime(datetime(2015, 1, 1).timetuple())
|
||||
self.last_updated_at = time.mktime(datetime(2015, 1, 1).timetuple())
|
||||
self.execution_number = 123
|
||||
self.version_number = 123
|
||||
self.approximate_seconds_before_time_out = 123
|
||||
|
||||
def to_get_dict(self):
|
||||
obj = {
|
||||
"jobId": self.job_id,
|
||||
"status": self.status,
|
||||
"forceCanceled": self.force_canceled,
|
||||
"statusDetails": {"detailsMap": self.status_details_map},
|
||||
"thingArn": self.thing_arn,
|
||||
"queuedAt": self.queued_at,
|
||||
"startedAt": self.started_at,
|
||||
"lastUpdatedAt": self.last_updated_at,
|
||||
"executionNumber": self.execution_number,
|
||||
"versionNumber": self.version_number,
|
||||
"approximateSecondsBeforeTimedOut": self.approximate_seconds_before_time_out,
|
||||
}
|
||||
|
||||
return obj
|
||||
|
||||
def to_dict(self):
|
||||
obj = {
|
||||
"jobId": self.job_id,
|
||||
"thingArn": self.thing_arn,
|
||||
"jobExecutionSummary": {
|
||||
"status": self.status,
|
||||
"queuedAt": self.queued_at,
|
||||
"startedAt": self.started_at,
|
||||
"lastUpdatedAt": self.last_updated_at,
|
||||
"executionNumber": self.execution_number,
|
||||
},
|
||||
}
|
||||
|
||||
return obj
|
||||
|
||||
|
||||
class IoTBackend(BaseBackend):
|
||||
def __init__(self, region_name=None):
|
||||
super(IoTBackend, self).__init__()
|
||||
self.region_name = region_name
|
||||
self.things = OrderedDict()
|
||||
self.jobs = OrderedDict()
|
||||
self.job_executions = OrderedDict()
|
||||
self.thing_types = OrderedDict()
|
||||
self.thing_groups = OrderedDict()
|
||||
self.certificates = OrderedDict()
|
||||
|
|
@ -535,6 +635,28 @@ class IoTBackend(BaseBackend):
|
|||
self.policies[policy.name] = policy
|
||||
return policy
|
||||
|
||||
def attach_policy(self, policy_name, target):
|
||||
principal = self._get_principal(target)
|
||||
policy = self.get_policy(policy_name)
|
||||
k = (target, policy_name)
|
||||
if k in self.principal_policies:
|
||||
return
|
||||
self.principal_policies[k] = (principal, policy)
|
||||
|
||||
def detach_policy(self, policy_name, target):
|
||||
# this may raises ResourceNotFoundException
|
||||
self._get_principal(target)
|
||||
self.get_policy(policy_name)
|
||||
|
||||
k = (target, policy_name)
|
||||
if k not in self.principal_policies:
|
||||
raise ResourceNotFoundException()
|
||||
del self.principal_policies[k]
|
||||
|
||||
def list_attached_policies(self, target):
|
||||
policies = [v[1] for k, v in self.principal_policies.items() if k[0] == target]
|
||||
return policies
|
||||
|
||||
def list_policies(self):
|
||||
policies = self.policies.values()
|
||||
return policies
|
||||
|
|
@ -559,6 +681,60 @@ class IoTBackend(BaseBackend):
|
|||
policy = self.get_policy(policy_name)
|
||||
del self.policies[policy.name]
|
||||
|
||||
def create_policy_version(self, policy_name, policy_document, set_as_default):
|
||||
policy = self.get_policy(policy_name)
|
||||
if not policy:
|
||||
raise ResourceNotFoundException()
|
||||
version = FakePolicyVersion(
|
||||
policy_name, policy_document, set_as_default, self.region_name
|
||||
)
|
||||
policy.versions.append(version)
|
||||
version.version_id = "{0}".format(len(policy.versions))
|
||||
if set_as_default:
|
||||
self.set_default_policy_version(policy_name, version.version_id)
|
||||
return version
|
||||
|
||||
def set_default_policy_version(self, policy_name, version_id):
|
||||
policy = self.get_policy(policy_name)
|
||||
if not policy:
|
||||
raise ResourceNotFoundException()
|
||||
for version in policy.versions:
|
||||
if version.version_id == version_id:
|
||||
version.is_default = True
|
||||
policy.default_version_id = version.version_id
|
||||
policy.document = version.document
|
||||
else:
|
||||
version.is_default = False
|
||||
|
||||
def get_policy_version(self, policy_name, version_id):
|
||||
policy = self.get_policy(policy_name)
|
||||
if not policy:
|
||||
raise ResourceNotFoundException()
|
||||
for version in policy.versions:
|
||||
if version.version_id == version_id:
|
||||
return version
|
||||
raise ResourceNotFoundException()
|
||||
|
||||
def list_policy_versions(self, policy_name):
|
||||
policy = self.get_policy(policy_name)
|
||||
if not policy:
|
||||
raise ResourceNotFoundException()
|
||||
return policy.versions
|
||||
|
||||
def delete_policy_version(self, policy_name, version_id):
|
||||
policy = self.get_policy(policy_name)
|
||||
if not policy:
|
||||
raise ResourceNotFoundException()
|
||||
if version_id == policy.default_version_id:
|
||||
raise InvalidRequestException(
|
||||
"Cannot delete the default version of a policy"
|
||||
)
|
||||
for i, v in enumerate(policy.versions):
|
||||
if v.version_id == version_id:
|
||||
del policy.versions[i]
|
||||
return
|
||||
raise ResourceNotFoundException()
|
||||
|
||||
def _get_principal(self, principal_arn):
|
||||
"""
|
||||
raise ResourceNotFoundException
|
||||
|
|
@ -574,14 +750,6 @@ class IoTBackend(BaseBackend):
|
|||
pass
|
||||
raise ResourceNotFoundException()
|
||||
|
||||
def attach_policy(self, policy_name, target):
|
||||
principal = self._get_principal(target)
|
||||
policy = self.get_policy(policy_name)
|
||||
k = (target, policy_name)
|
||||
if k in self.principal_policies:
|
||||
return
|
||||
self.principal_policies[k] = (principal, policy)
|
||||
|
||||
def attach_principal_policy(self, policy_name, principal_arn):
|
||||
principal = self._get_principal(principal_arn)
|
||||
policy = self.get_policy(policy_name)
|
||||
|
|
@ -590,15 +758,6 @@ class IoTBackend(BaseBackend):
|
|||
return
|
||||
self.principal_policies[k] = (principal, policy)
|
||||
|
||||
def detach_policy(self, policy_name, target):
|
||||
# this may raises ResourceNotFoundException
|
||||
self._get_principal(target)
|
||||
self.get_policy(policy_name)
|
||||
k = (target, policy_name)
|
||||
if k not in self.principal_policies:
|
||||
raise ResourceNotFoundException()
|
||||
del self.principal_policies[k]
|
||||
|
||||
def detach_principal_policy(self, policy_name, principal_arn):
|
||||
# this may raises ResourceNotFoundException
|
||||
self._get_principal(principal_arn)
|
||||
|
|
@ -819,11 +978,187 @@ class IoTBackend(BaseBackend):
|
|||
self.region_name,
|
||||
)
|
||||
self.jobs[job_id] = job
|
||||
|
||||
for thing_arn in targets:
|
||||
thing_name = thing_arn.split(":")[-1].split("/")[-1]
|
||||
job_execution = FakeJobExecution(job_id, thing_arn)
|
||||
self.job_executions[(job_id, thing_name)] = job_execution
|
||||
return job.job_arn, job_id, description
|
||||
|
||||
def describe_job(self, job_id):
|
||||
jobs = [_ for _ in self.jobs.values() if _.job_id == job_id]
|
||||
if len(jobs) == 0:
|
||||
raise ResourceNotFoundException()
|
||||
return jobs[0]
|
||||
|
||||
def delete_job(self, job_id, force):
|
||||
job = self.jobs[job_id]
|
||||
|
||||
if job.status == "IN_PROGRESS" and force:
|
||||
del self.jobs[job_id]
|
||||
elif job.status != "IN_PROGRESS":
|
||||
del self.jobs[job_id]
|
||||
else:
|
||||
raise InvalidStateTransitionException()
|
||||
|
||||
def cancel_job(self, job_id, reason_code, comment, force):
|
||||
job = self.jobs[job_id]
|
||||
|
||||
job.reason_code = reason_code if reason_code is not None else job.reason_code
|
||||
job.comment = comment if comment is not None else job.comment
|
||||
job.force = force if force is not None and force != job.force else job.force
|
||||
job.status = "CANCELED"
|
||||
|
||||
if job.status == "IN_PROGRESS" and force:
|
||||
self.jobs[job_id] = job
|
||||
elif job.status != "IN_PROGRESS":
|
||||
self.jobs[job_id] = job
|
||||
else:
|
||||
raise InvalidStateTransitionException()
|
||||
|
||||
return job
|
||||
|
||||
def get_job_document(self, job_id):
|
||||
return self.jobs[job_id]
|
||||
|
||||
def list_jobs(
|
||||
self,
|
||||
status,
|
||||
target_selection,
|
||||
max_results,
|
||||
token,
|
||||
thing_group_name,
|
||||
thing_group_id,
|
||||
):
|
||||
# TODO: implement filters
|
||||
all_jobs = [_.to_dict() for _ in self.jobs.values()]
|
||||
filtered_jobs = all_jobs
|
||||
|
||||
if token is None:
|
||||
jobs = filtered_jobs[0:max_results]
|
||||
next_token = str(max_results) if len(filtered_jobs) > max_results else None
|
||||
else:
|
||||
token = int(token)
|
||||
jobs = filtered_jobs[token : token + max_results]
|
||||
next_token = (
|
||||
str(token + max_results)
|
||||
if len(filtered_jobs) > token + max_results
|
||||
else None
|
||||
)
|
||||
|
||||
return jobs, next_token
|
||||
|
||||
def describe_job_execution(self, job_id, thing_name, execution_number):
|
||||
try:
|
||||
job_execution = self.job_executions[(job_id, thing_name)]
|
||||
except KeyError:
|
||||
raise ResourceNotFoundException()
|
||||
|
||||
if job_execution is None or (
|
||||
execution_number is not None
|
||||
and job_execution.execution_number != execution_number
|
||||
):
|
||||
raise ResourceNotFoundException()
|
||||
|
||||
return job_execution
|
||||
|
||||
def cancel_job_execution(
|
||||
self, job_id, thing_name, force, expected_version, status_details
|
||||
):
|
||||
job_execution = self.job_executions[(job_id, thing_name)]
|
||||
|
||||
if job_execution is None:
|
||||
raise ResourceNotFoundException()
|
||||
|
||||
job_execution.force_canceled = (
|
||||
force if force is not None else job_execution.force_canceled
|
||||
)
|
||||
# TODO: implement expected_version and status_details (at most 10 can be specified)
|
||||
|
||||
if job_execution.status == "IN_PROGRESS" and force:
|
||||
job_execution.status = "CANCELED"
|
||||
self.job_executions[(job_id, thing_name)] = job_execution
|
||||
elif job_execution.status != "IN_PROGRESS":
|
||||
job_execution.status = "CANCELED"
|
||||
self.job_executions[(job_id, thing_name)] = job_execution
|
||||
else:
|
||||
raise InvalidStateTransitionException()
|
||||
|
||||
def delete_job_execution(self, job_id, thing_name, execution_number, force):
|
||||
job_execution = self.job_executions[(job_id, thing_name)]
|
||||
|
||||
if job_execution.execution_number != execution_number:
|
||||
raise ResourceNotFoundException()
|
||||
|
||||
if job_execution.status == "IN_PROGRESS" and force:
|
||||
del self.job_executions[(job_id, thing_name)]
|
||||
elif job_execution.status != "IN_PROGRESS":
|
||||
del self.job_executions[(job_id, thing_name)]
|
||||
else:
|
||||
raise InvalidStateTransitionException()
|
||||
|
||||
def list_job_executions_for_job(self, job_id, status, max_results, next_token):
|
||||
job_executions = [
|
||||
self.job_executions[je].to_dict()
|
||||
for je in self.job_executions
|
||||
if je[0] == job_id
|
||||
]
|
||||
|
||||
if status is not None:
|
||||
job_executions = list(
|
||||
filter(
|
||||
lambda elem: status in elem["status"] and elem["status"] == status,
|
||||
job_executions,
|
||||
)
|
||||
)
|
||||
|
||||
token = next_token
|
||||
if token is None:
|
||||
job_executions = job_executions[0:max_results]
|
||||
next_token = str(max_results) if len(job_executions) > max_results else None
|
||||
else:
|
||||
token = int(token)
|
||||
job_executions = job_executions[token : token + max_results]
|
||||
next_token = (
|
||||
str(token + max_results)
|
||||
if len(job_executions) > token + max_results
|
||||
else None
|
||||
)
|
||||
|
||||
return job_executions, next_token
|
||||
|
||||
def list_job_executions_for_thing(
|
||||
self, thing_name, status, max_results, next_token
|
||||
):
|
||||
job_executions = [
|
||||
self.job_executions[je].to_dict()
|
||||
for je in self.job_executions
|
||||
if je[1] == thing_name
|
||||
]
|
||||
|
||||
if status is not None:
|
||||
job_executions = list(
|
||||
filter(
|
||||
lambda elem: status in elem["status"] and elem["status"] == status,
|
||||
job_executions,
|
||||
)
|
||||
)
|
||||
|
||||
token = next_token
|
||||
if token is None:
|
||||
job_executions = job_executions[0:max_results]
|
||||
next_token = str(max_results) if len(job_executions) > max_results else None
|
||||
else:
|
||||
token = int(token)
|
||||
job_executions = job_executions[token : token + max_results]
|
||||
next_token = (
|
||||
str(token + max_results)
|
||||
if len(job_executions) > token + max_results
|
||||
else None
|
||||
)
|
||||
|
||||
return job_executions, next_token
|
||||
|
||||
|
||||
iot_backends = {}
|
||||
for region in Session().get_available_regions("iot"):
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
from __future__ import unicode_literals
|
||||
|
||||
import json
|
||||
from six.moves.urllib.parse import unquote
|
||||
|
||||
from moto.core.responses import BaseResponse
|
||||
from .models import iot_backends
|
||||
|
|
@ -141,6 +142,8 @@ class IoTResponse(BaseResponse):
|
|||
createdAt=job.created_at,
|
||||
description=job.description,
|
||||
documentParameters=job.document_parameters,
|
||||
forceCanceled=job.force,
|
||||
reasonCode=job.reason_code,
|
||||
jobArn=job.job_arn,
|
||||
jobExecutionsRolloutConfig=job.job_executions_rollout_config,
|
||||
jobId=job.job_id,
|
||||
|
|
@ -154,6 +157,127 @@ class IoTResponse(BaseResponse):
|
|||
)
|
||||
)
|
||||
|
||||
def delete_job(self):
|
||||
job_id = self._get_param("jobId")
|
||||
force = self._get_bool_param("force")
|
||||
|
||||
self.iot_backend.delete_job(job_id=job_id, force=force)
|
||||
|
||||
return json.dumps(dict())
|
||||
|
||||
def cancel_job(self):
|
||||
job_id = self._get_param("jobId")
|
||||
reason_code = self._get_param("reasonCode")
|
||||
comment = self._get_param("comment")
|
||||
force = self._get_bool_param("force")
|
||||
|
||||
job = self.iot_backend.cancel_job(
|
||||
job_id=job_id, reason_code=reason_code, comment=comment, force=force
|
||||
)
|
||||
|
||||
return json.dumps(job.to_dict())
|
||||
|
||||
def get_job_document(self):
|
||||
job = self.iot_backend.get_job_document(job_id=self._get_param("jobId"))
|
||||
|
||||
if job.document is not None:
|
||||
return json.dumps({"document": job.document})
|
||||
else:
|
||||
# job.document_source is not None:
|
||||
# TODO: needs to be implemented to get document_source's content from S3
|
||||
return json.dumps({"document": ""})
|
||||
|
||||
def list_jobs(self):
|
||||
status = (self._get_param("status"),)
|
||||
target_selection = (self._get_param("targetSelection"),)
|
||||
max_results = self._get_int_param(
|
||||
"maxResults", 50
|
||||
) # not the default, but makes testing easier
|
||||
previous_next_token = self._get_param("nextToken")
|
||||
thing_group_name = (self._get_param("thingGroupName"),)
|
||||
thing_group_id = self._get_param("thingGroupId")
|
||||
jobs, next_token = self.iot_backend.list_jobs(
|
||||
status=status,
|
||||
target_selection=target_selection,
|
||||
max_results=max_results,
|
||||
token=previous_next_token,
|
||||
thing_group_name=thing_group_name,
|
||||
thing_group_id=thing_group_id,
|
||||
)
|
||||
|
||||
return json.dumps(dict(jobs=jobs, nextToken=next_token))
|
||||
|
||||
def describe_job_execution(self):
|
||||
job_id = self._get_param("jobId")
|
||||
thing_name = self._get_param("thingName")
|
||||
execution_number = self._get_int_param("executionNumber")
|
||||
job_execution = self.iot_backend.describe_job_execution(
|
||||
job_id=job_id, thing_name=thing_name, execution_number=execution_number
|
||||
)
|
||||
|
||||
return json.dumps(dict(execution=job_execution.to_get_dict()))
|
||||
|
||||
def cancel_job_execution(self):
|
||||
job_id = self._get_param("jobId")
|
||||
thing_name = self._get_param("thingName")
|
||||
force = self._get_bool_param("force")
|
||||
expected_version = self._get_int_param("expectedVersion")
|
||||
status_details = self._get_param("statusDetails")
|
||||
|
||||
self.iot_backend.cancel_job_execution(
|
||||
job_id=job_id,
|
||||
thing_name=thing_name,
|
||||
force=force,
|
||||
expected_version=expected_version,
|
||||
status_details=status_details,
|
||||
)
|
||||
|
||||
return json.dumps(dict())
|
||||
|
||||
def delete_job_execution(self):
|
||||
job_id = self._get_param("jobId")
|
||||
thing_name = self._get_param("thingName")
|
||||
execution_number = self._get_int_param("executionNumber")
|
||||
force = self._get_bool_param("force")
|
||||
|
||||
self.iot_backend.delete_job_execution(
|
||||
job_id=job_id,
|
||||
thing_name=thing_name,
|
||||
execution_number=execution_number,
|
||||
force=force,
|
||||
)
|
||||
|
||||
return json.dumps(dict())
|
||||
|
||||
def list_job_executions_for_job(self):
|
||||
job_id = self._get_param("jobId")
|
||||
status = self._get_param("status")
|
||||
max_results = self._get_int_param(
|
||||
"maxResults", 50
|
||||
) # not the default, but makes testing easier
|
||||
next_token = self._get_param("nextToken")
|
||||
job_executions, next_token = self.iot_backend.list_job_executions_for_job(
|
||||
job_id=job_id, status=status, max_results=max_results, next_token=next_token
|
||||
)
|
||||
|
||||
return json.dumps(dict(executionSummaries=job_executions, nextToken=next_token))
|
||||
|
||||
def list_job_executions_for_thing(self):
|
||||
thing_name = self._get_param("thingName")
|
||||
status = self._get_param("status")
|
||||
max_results = self._get_int_param(
|
||||
"maxResults", 50
|
||||
) # not the default, but makes testing easier
|
||||
next_token = self._get_param("nextToken")
|
||||
job_executions, next_token = self.iot_backend.list_job_executions_for_thing(
|
||||
thing_name=thing_name,
|
||||
status=status,
|
||||
max_results=max_results,
|
||||
next_token=next_token,
|
||||
)
|
||||
|
||||
return json.dumps(dict(executionSummaries=job_executions, nextToken=next_token))
|
||||
|
||||
def create_keys_and_certificate(self):
|
||||
set_as_active = self._get_bool_param("setAsActive")
|
||||
cert, key_pair = self.iot_backend.create_keys_and_certificate(
|
||||
|
|
@ -241,12 +365,61 @@ class IoTResponse(BaseResponse):
|
|||
self.iot_backend.delete_policy(policy_name=policy_name)
|
||||
return json.dumps(dict())
|
||||
|
||||
def create_policy_version(self):
|
||||
policy_name = self._get_param("policyName")
|
||||
policy_document = self._get_param("policyDocument")
|
||||
set_as_default = self._get_bool_param("setAsDefault")
|
||||
policy_version = self.iot_backend.create_policy_version(
|
||||
policy_name, policy_document, set_as_default
|
||||
)
|
||||
|
||||
return json.dumps(dict(policy_version.to_dict_at_creation()))
|
||||
|
||||
def set_default_policy_version(self):
|
||||
policy_name = self._get_param("policyName")
|
||||
version_id = self._get_param("policyVersionId")
|
||||
self.iot_backend.set_default_policy_version(policy_name, version_id)
|
||||
|
||||
return json.dumps(dict())
|
||||
|
||||
def get_policy_version(self):
|
||||
policy_name = self._get_param("policyName")
|
||||
version_id = self._get_param("policyVersionId")
|
||||
policy_version = self.iot_backend.get_policy_version(policy_name, version_id)
|
||||
return json.dumps(dict(policy_version.to_get_dict()))
|
||||
|
||||
def list_policy_versions(self):
|
||||
policy_name = self._get_param("policyName")
|
||||
policiy_versions = self.iot_backend.list_policy_versions(
|
||||
policy_name=policy_name
|
||||
)
|
||||
|
||||
return json.dumps(dict(policyVersions=[_.to_dict() for _ in policiy_versions]))
|
||||
|
||||
def delete_policy_version(self):
|
||||
policy_name = self._get_param("policyName")
|
||||
version_id = self._get_param("policyVersionId")
|
||||
self.iot_backend.delete_policy_version(policy_name, version_id)
|
||||
|
||||
return json.dumps(dict())
|
||||
|
||||
def attach_policy(self):
|
||||
policy_name = self._get_param("policyName")
|
||||
target = self._get_param("target")
|
||||
self.iot_backend.attach_policy(policy_name=policy_name, target=target)
|
||||
return json.dumps(dict())
|
||||
|
||||
def list_attached_policies(self):
|
||||
principal = unquote(self._get_param("target"))
|
||||
# marker = self._get_param("marker")
|
||||
# page_size = self._get_int_param("pageSize")
|
||||
policies = self.iot_backend.list_attached_policies(target=principal)
|
||||
# TODO: implement pagination in the future
|
||||
next_marker = None
|
||||
return json.dumps(
|
||||
dict(policies=[_.to_dict() for _ in policies], nextMarker=next_marker)
|
||||
)
|
||||
|
||||
def attach_principal_policy(self):
|
||||
policy_name = self._get_param("policyName")
|
||||
principal = self.headers.get("x-amzn-iot-principal")
|
||||
|
|
|
|||
|
|
@ -7,25 +7,42 @@ from datetime import datetime, timedelta
|
|||
from boto3 import Session
|
||||
|
||||
from moto.core import BaseBackend, BaseModel
|
||||
<<<<<<< HEAD
|
||||
from moto.core.exceptions import JsonRESTError
|
||||
from moto.core.utils import iso_8601_datetime_without_milliseconds
|
||||
from moto.utilities.tagging_service import TaggingService
|
||||
=======
|
||||
from moto.core.utils import unix_time
|
||||
|
||||
from moto.iam.models import ACCOUNT_ID
|
||||
|
||||
>>>>>>> 100dbd529f174f18d579a1dcc066d55409f2e38f
|
||||
from .utils import decrypt, encrypt, generate_key_id, generate_master_key
|
||||
|
||||
|
||||
class Key(BaseModel):
|
||||
<<<<<<< HEAD
|
||||
def __init__(self, policy, key_usage, description, region):
|
||||
=======
|
||||
def __init__(
|
||||
self, policy, key_usage, customer_master_key_spec, description, tags, region
|
||||
):
|
||||
>>>>>>> 100dbd529f174f18d579a1dcc066d55409f2e38f
|
||||
self.id = generate_key_id()
|
||||
self.creation_date = unix_time()
|
||||
self.policy = policy
|
||||
self.key_usage = key_usage
|
||||
self.key_state = "Enabled"
|
||||
self.description = description
|
||||
self.enabled = True
|
||||
self.region = region
|
||||
self.account_id = "012345678912"
|
||||
self.account_id = ACCOUNT_ID
|
||||
self.key_rotation_status = False
|
||||
self.deletion_date = None
|
||||
self.key_material = generate_master_key()
|
||||
self.origin = "AWS_KMS"
|
||||
self.key_manager = "CUSTOMER"
|
||||
self.customer_master_key_spec = customer_master_key_spec or "SYMMETRIC_DEFAULT"
|
||||
|
||||
@property
|
||||
def physical_resource_id(self):
|
||||
|
|
@ -37,23 +54,55 @@ class Key(BaseModel):
|
|||
self.region, self.account_id, self.id
|
||||
)
|
||||
|
||||
@property
|
||||
def encryption_algorithms(self):
|
||||
if self.key_usage == "SIGN_VERIFY":
|
||||
return None
|
||||
elif self.customer_master_key_spec == "SYMMETRIC_DEFAULT":
|
||||
return ["SYMMETRIC_DEFAULT"]
|
||||
else:
|
||||
return ["RSAES_OAEP_SHA_1", "RSAES_OAEP_SHA_256"]
|
||||
|
||||
@property
|
||||
def signing_algorithms(self):
|
||||
if self.key_usage == "ENCRYPT_DECRYPT":
|
||||
return None
|
||||
elif self.customer_master_key_spec in ["ECC_NIST_P256", "ECC_SECG_P256K1"]:
|
||||
return ["ECDSA_SHA_256"]
|
||||
elif self.customer_master_key_spec == "ECC_NIST_P384":
|
||||
return ["ECDSA_SHA_384"]
|
||||
elif self.customer_master_key_spec == "ECC_NIST_P521":
|
||||
return ["ECDSA_SHA_512"]
|
||||
else:
|
||||
return [
|
||||
"RSASSA_PKCS1_V1_5_SHA_256",
|
||||
"RSASSA_PKCS1_V1_5_SHA_384",
|
||||
"RSASSA_PKCS1_V1_5_SHA_512",
|
||||
"RSASSA_PSS_SHA_256",
|
||||
"RSASSA_PSS_SHA_384",
|
||||
"RSASSA_PSS_SHA_512",
|
||||
]
|
||||
|
||||
def to_dict(self):
|
||||
key_dict = {
|
||||
"KeyMetadata": {
|
||||
"AWSAccountId": self.account_id,
|
||||
"Arn": self.arn,
|
||||
"CreationDate": iso_8601_datetime_without_milliseconds(datetime.now()),
|
||||
"CreationDate": self.creation_date,
|
||||
"CustomerMasterKeySpec": self.customer_master_key_spec,
|
||||
"Description": self.description,
|
||||
"Enabled": self.enabled,
|
||||
"EncryptionAlgorithms": self.encryption_algorithms,
|
||||
"KeyId": self.id,
|
||||
"KeyManager": self.key_manager,
|
||||
"KeyUsage": self.key_usage,
|
||||
"KeyState": self.key_state,
|
||||
"Origin": self.origin,
|
||||
"SigningAlgorithms": self.signing_algorithms,
|
||||
}
|
||||
}
|
||||
if self.key_state == "PendingDeletion":
|
||||
key_dict["KeyMetadata"][
|
||||
"DeletionDate"
|
||||
] = iso_8601_datetime_without_milliseconds(self.deletion_date)
|
||||
key_dict["KeyMetadata"]["DeletionDate"] = unix_time(self.deletion_date)
|
||||
return key_dict
|
||||
|
||||
def delete(self, region_name):
|
||||
|
|
@ -69,6 +118,7 @@ class Key(BaseModel):
|
|||
key = kms_backend.create_key(
|
||||
policy=properties["KeyPolicy"],
|
||||
key_usage="ENCRYPT_DECRYPT",
|
||||
customer_master_key_spec="SYMMETRIC_DEFAULT",
|
||||
description=properties["Description"],
|
||||
region=region_name,
|
||||
)
|
||||
|
|
@ -92,8 +142,17 @@ class KmsBackend(BaseBackend):
|
|||
self.key_to_aliases = defaultdict(set)
|
||||
self.tagger = TaggingService(keyName='TagKey', valueName='TagValue')
|
||||
|
||||
<<<<<<< HEAD
|
||||
def create_key(self, policy, key_usage, description, tags, region):
|
||||
key = Key(policy, key_usage, description, region)
|
||||
=======
|
||||
def create_key(
|
||||
self, policy, key_usage, customer_master_key_spec, description, tags, region
|
||||
):
|
||||
key = Key(
|
||||
policy, key_usage, customer_master_key_spec, description, tags, region
|
||||
)
|
||||
>>>>>>> 100dbd529f174f18d579a1dcc066d55409f2e38f
|
||||
self.keys[key.id] = key
|
||||
if tags != None and len(tags) > 0:
|
||||
self.tag_resource(key.id, tags)
|
||||
|
|
@ -211,9 +270,7 @@ class KmsBackend(BaseBackend):
|
|||
self.keys[key_id].deletion_date = datetime.now() + timedelta(
|
||||
days=pending_window_in_days
|
||||
)
|
||||
return iso_8601_datetime_without_milliseconds(
|
||||
self.keys[key_id].deletion_date
|
||||
)
|
||||
return unix_time(self.keys[key_id].deletion_date)
|
||||
|
||||
def encrypt(self, key_id, plaintext, encryption_context):
|
||||
key_id = self.any_id_to_key_id(key_id)
|
||||
|
|
|
|||
|
|
@ -118,11 +118,12 @@ class KmsResponse(BaseResponse):
|
|||
"""https://docs.aws.amazon.com/kms/latest/APIReference/API_CreateKey.html"""
|
||||
policy = self.parameters.get("Policy")
|
||||
key_usage = self.parameters.get("KeyUsage")
|
||||
customer_master_key_spec = self.parameters.get("CustomerMasterKeySpec")
|
||||
description = self.parameters.get("Description")
|
||||
tags = self.parameters.get("Tags")
|
||||
|
||||
key = self.kms_backend.create_key(
|
||||
policy, key_usage, description, tags, self.region
|
||||
policy, key_usage, customer_master_key_spec, description, tags, self.region
|
||||
)
|
||||
return json.dumps(key.to_dict())
|
||||
|
||||
|
|
|
|||
|
|
@ -103,7 +103,7 @@ class LogsResponse(BaseResponse):
|
|||
(
|
||||
events,
|
||||
next_backward_token,
|
||||
next_foward_token,
|
||||
next_forward_token,
|
||||
) = self.logs_backend.get_log_events(
|
||||
log_group_name,
|
||||
log_stream_name,
|
||||
|
|
@ -117,7 +117,7 @@ class LogsResponse(BaseResponse):
|
|||
{
|
||||
"events": events,
|
||||
"nextBackwardToken": next_backward_token,
|
||||
"nextForwardToken": next_foward_token,
|
||||
"nextForwardToken": next_forward_token,
|
||||
}
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -10,3 +10,13 @@ class InvalidInputException(JsonRESTError):
|
|||
"InvalidInputException",
|
||||
"You provided a value that does not match the required pattern.",
|
||||
)
|
||||
|
||||
|
||||
class DuplicateOrganizationalUnitException(JsonRESTError):
|
||||
code = 400
|
||||
|
||||
def __init__(self):
|
||||
super(DuplicateOrganizationalUnitException, self).__init__(
|
||||
"DuplicateOrganizationalUnitException",
|
||||
"An OU with the same name already exists.",
|
||||
)
|
||||
|
|
|
|||
|
|
@ -8,7 +8,10 @@ from moto.core import BaseBackend, BaseModel
|
|||
from moto.core.exceptions import RESTError
|
||||
from moto.core.utils import unix_time
|
||||
from moto.organizations import utils
|
||||
from moto.organizations.exceptions import InvalidInputException
|
||||
from moto.organizations.exceptions import (
|
||||
InvalidInputException,
|
||||
DuplicateOrganizationalUnitException,
|
||||
)
|
||||
|
||||
|
||||
class FakeOrganization(BaseModel):
|
||||
|
|
@ -222,6 +225,14 @@ class OrganizationsBackend(BaseBackend):
|
|||
self.attach_policy(PolicyId=utils.DEFAULT_POLICY_ID, TargetId=new_ou.id)
|
||||
return new_ou.describe()
|
||||
|
||||
def update_organizational_unit(self, **kwargs):
|
||||
for ou in self.ou:
|
||||
if ou.name == kwargs["Name"]:
|
||||
raise DuplicateOrganizationalUnitException
|
||||
ou = self.get_organizational_unit_by_id(kwargs["OrganizationalUnitId"])
|
||||
ou.name = kwargs["Name"]
|
||||
return ou.describe()
|
||||
|
||||
def get_organizational_unit_by_id(self, ou_id):
|
||||
ou = next((ou for ou in self.ou if ou.id == ou_id), None)
|
||||
if ou is None:
|
||||
|
|
|
|||
|
|
@ -36,6 +36,11 @@ class OrganizationsResponse(BaseResponse):
|
|||
self.organizations_backend.create_organizational_unit(**self.request_params)
|
||||
)
|
||||
|
||||
def update_organizational_unit(self):
|
||||
return json.dumps(
|
||||
self.organizations_backend.update_organizational_unit(**self.request_params)
|
||||
)
|
||||
|
||||
def describe_organizational_unit(self):
|
||||
return json.dumps(
|
||||
self.organizations_backend.describe_organizational_unit(
|
||||
|
|
|
|||
|
|
@ -130,7 +130,9 @@ class Database(BaseModel):
|
|||
if not self.option_group_name and self.engine in self.default_option_groups:
|
||||
self.option_group_name = self.default_option_groups[self.engine]
|
||||
self.character_set_name = kwargs.get("character_set_name", None)
|
||||
self.iam_database_authentication_enabled = False
|
||||
self.enable_iam_database_authentication = kwargs.get(
|
||||
"enable_iam_database_authentication", False
|
||||
)
|
||||
self.dbi_resource_id = "db-M5ENSHXFPU6XHZ4G4ZEI5QIO2U"
|
||||
self.tags = kwargs.get("tags", [])
|
||||
|
||||
|
|
@ -214,7 +216,7 @@ class Database(BaseModel):
|
|||
<ReadReplicaSourceDBInstanceIdentifier>{{ database.source_db_identifier }}</ReadReplicaSourceDBInstanceIdentifier>
|
||||
{% endif %}
|
||||
<Engine>{{ database.engine }}</Engine>
|
||||
<IAMDatabaseAuthenticationEnabled>{{database.iam_database_authentication_enabled }}</IAMDatabaseAuthenticationEnabled>
|
||||
<IAMDatabaseAuthenticationEnabled>{{database.enable_iam_database_authentication|lower }}</IAMDatabaseAuthenticationEnabled>
|
||||
<LicenseModel>{{ database.license_model }}</LicenseModel>
|
||||
<EngineVersion>{{ database.engine_version }}</EngineVersion>
|
||||
<OptionGroupMemberships>
|
||||
|
|
@ -542,7 +544,7 @@ class Snapshot(BaseModel):
|
|||
<KmsKeyId>{{ database.kms_key_id }}</KmsKeyId>
|
||||
<DBSnapshotArn>{{ snapshot.snapshot_arn }}</DBSnapshotArn>
|
||||
<Timezone></Timezone>
|
||||
<IAMDatabaseAuthenticationEnabled>false</IAMDatabaseAuthenticationEnabled>
|
||||
<IAMDatabaseAuthenticationEnabled>{{ database.enable_iam_database_authentication|lower }}</IAMDatabaseAuthenticationEnabled>
|
||||
</DBSnapshot>"""
|
||||
)
|
||||
return template.render(snapshot=self, database=self.database)
|
||||
|
|
@ -986,7 +988,7 @@ class RDS2Backend(BaseBackend):
|
|||
)
|
||||
if option_group_kwargs["engine_name"] not in valid_option_group_engines.keys():
|
||||
raise RDSClientError(
|
||||
"InvalidParameterValue", "Invalid DB engine: non-existant"
|
||||
"InvalidParameterValue", "Invalid DB engine: non-existent"
|
||||
)
|
||||
if (
|
||||
option_group_kwargs["major_engine_version"]
|
||||
|
|
|
|||
|
|
@ -27,6 +27,9 @@ class RDS2Response(BaseResponse):
|
|||
"db_subnet_group_name": self._get_param("DBSubnetGroupName"),
|
||||
"engine": self._get_param("Engine"),
|
||||
"engine_version": self._get_param("EngineVersion"),
|
||||
"enable_iam_database_authentication": self._get_bool_param(
|
||||
"EnableIAMDatabaseAuthentication"
|
||||
),
|
||||
"license_model": self._get_param("LicenseModel"),
|
||||
"iops": self._get_int_param("Iops"),
|
||||
"kms_key_id": self._get_param("KmsKeyId"),
|
||||
|
|
@ -367,14 +370,14 @@ class RDS2Response(BaseResponse):
|
|||
|
||||
def modify_db_parameter_group(self):
|
||||
db_parameter_group_name = self._get_param("DBParameterGroupName")
|
||||
db_parameter_group_parameters = self._get_db_parameter_group_paramters()
|
||||
db_parameter_group_parameters = self._get_db_parameter_group_parameters()
|
||||
db_parameter_group = self.backend.modify_db_parameter_group(
|
||||
db_parameter_group_name, db_parameter_group_parameters
|
||||
)
|
||||
template = self.response_template(MODIFY_DB_PARAMETER_GROUP_TEMPLATE)
|
||||
return template.render(db_parameter_group=db_parameter_group)
|
||||
|
||||
def _get_db_parameter_group_paramters(self):
|
||||
def _get_db_parameter_group_parameters(self):
|
||||
parameter_group_parameters = defaultdict(dict)
|
||||
for param_name, value in self.querystring.items():
|
||||
if not param_name.startswith("Parameters.Parameter"):
|
||||
|
|
|
|||
|
|
@ -271,6 +271,7 @@ LIST_RRSET_RESPONSE = """<ListResourceRecordSetsResponse xmlns="https://route53.
|
|||
{{ record_set.to_xml() }}
|
||||
{% endfor %}
|
||||
</ResourceRecordSets>
|
||||
<IsTruncated>false</IsTruncated>
|
||||
</ListResourceRecordSetsResponse>"""
|
||||
|
||||
CHANGE_RRSET_RESPONSE = """<ChangeResourceRecordSetsResponse xmlns="https://route53.amazonaws.com/doc/2012-12-12/">
|
||||
|
|
|
|||
|
|
@ -1,8 +1,13 @@
|
|||
import datetime
|
||||
import json
|
||||
import time
|
||||
|
||||
from boto3 import Session
|
||||
|
||||
from moto.core.exceptions import InvalidNextTokenException
|
||||
from moto.core.models import ConfigQueryModel
|
||||
from moto.s3 import s3_backends
|
||||
from moto.s3.models import get_moto_s3_account_id
|
||||
|
||||
|
||||
class S3ConfigQuery(ConfigQueryModel):
|
||||
|
|
@ -118,4 +123,146 @@ class S3ConfigQuery(ConfigQueryModel):
|
|||
return config_data
|
||||
|
||||
|
||||
class S3AccountPublicAccessBlockConfigQuery(ConfigQueryModel):
|
||||
def list_config_service_resources(
|
||||
self,
|
||||
resource_ids,
|
||||
resource_name,
|
||||
limit,
|
||||
next_token,
|
||||
backend_region=None,
|
||||
resource_region=None,
|
||||
):
|
||||
# For the Account Public Access Block, they are the same for all regions. The resource ID is the AWS account ID
|
||||
# There is no resource name -- it should be a blank string "" if provided.
|
||||
|
||||
# The resource name can only ever be None or an empty string:
|
||||
if resource_name is not None and resource_name != "":
|
||||
return [], None
|
||||
|
||||
pab = None
|
||||
account_id = get_moto_s3_account_id()
|
||||
regions = [region for region in Session().get_available_regions("config")]
|
||||
|
||||
# If a resource ID was passed in, then filter accordingly:
|
||||
if resource_ids:
|
||||
for id in resource_ids:
|
||||
if account_id == id:
|
||||
pab = self.backends["global"].account_public_access_block
|
||||
break
|
||||
|
||||
# Otherwise, just grab the one from the backend:
|
||||
if not resource_ids:
|
||||
pab = self.backends["global"].account_public_access_block
|
||||
|
||||
# If it's not present, then return nothing
|
||||
if not pab:
|
||||
return [], None
|
||||
|
||||
# Filter on regions (and paginate on them as well):
|
||||
if backend_region:
|
||||
pab_list = [backend_region]
|
||||
elif resource_region:
|
||||
# Invalid region?
|
||||
if resource_region not in regions:
|
||||
return [], None
|
||||
|
||||
pab_list = [resource_region]
|
||||
|
||||
# Aggregated query where no regions were supplied so return them all:
|
||||
else:
|
||||
pab_list = regions
|
||||
|
||||
# Pagination logic:
|
||||
sorted_regions = sorted(pab_list)
|
||||
new_token = None
|
||||
|
||||
# Get the start:
|
||||
if not next_token:
|
||||
start = 0
|
||||
else:
|
||||
# Tokens for this moto feature is just the region-name:
|
||||
# For OTHER non-global resource types, it's the region concatenated with the resource ID.
|
||||
if next_token not in sorted_regions:
|
||||
raise InvalidNextTokenException()
|
||||
|
||||
start = sorted_regions.index(next_token)
|
||||
|
||||
# Get the list of items to collect:
|
||||
pab_list = sorted_regions[start : (start + limit)]
|
||||
|
||||
if len(sorted_regions) > (start + limit):
|
||||
new_token = sorted_regions[start + limit]
|
||||
|
||||
return (
|
||||
[
|
||||
{
|
||||
"type": "AWS::S3::AccountPublicAccessBlock",
|
||||
"id": account_id,
|
||||
"region": region,
|
||||
}
|
||||
for region in pab_list
|
||||
],
|
||||
new_token,
|
||||
)
|
||||
|
||||
def get_config_resource(
|
||||
self, resource_id, resource_name=None, backend_region=None, resource_region=None
|
||||
):
|
||||
# Do we even have this defined?
|
||||
if not self.backends["global"].account_public_access_block:
|
||||
return None
|
||||
|
||||
# Resource name can only ever be "" if it's supplied:
|
||||
if resource_name is not None and resource_name != "":
|
||||
return None
|
||||
|
||||
# Are we filtering based on region?
|
||||
account_id = get_moto_s3_account_id()
|
||||
regions = [region for region in Session().get_available_regions("config")]
|
||||
|
||||
# Is the resource ID correct?:
|
||||
if account_id == resource_id:
|
||||
if backend_region:
|
||||
pab_region = backend_region
|
||||
|
||||
# Invalid region?
|
||||
elif resource_region not in regions:
|
||||
return None
|
||||
|
||||
else:
|
||||
pab_region = resource_region
|
||||
|
||||
else:
|
||||
return None
|
||||
|
||||
# Format the PAB to the AWS Config format:
|
||||
creation_time = datetime.datetime.utcnow()
|
||||
config_data = {
|
||||
"version": "1.3",
|
||||
"accountId": account_id,
|
||||
"configurationItemCaptureTime": str(creation_time),
|
||||
"configurationItemStatus": "OK",
|
||||
"configurationStateId": str(
|
||||
int(time.mktime(creation_time.timetuple()))
|
||||
), # PY2 and 3 compatible
|
||||
"resourceType": "AWS::S3::AccountPublicAccessBlock",
|
||||
"resourceId": account_id,
|
||||
"awsRegion": pab_region,
|
||||
"availabilityZone": "Not Applicable",
|
||||
"configuration": self.backends[
|
||||
"global"
|
||||
].account_public_access_block.to_config_dict(),
|
||||
"supplementaryConfiguration": {},
|
||||
}
|
||||
|
||||
# The 'configuration' field is also a JSON string:
|
||||
config_data["configuration"] = json.dumps(config_data["configuration"])
|
||||
|
||||
return config_data
|
||||
|
||||
|
||||
s3_config_query = S3ConfigQuery(s3_backends)
|
||||
s3_account_public_access_block_query = S3AccountPublicAccessBlockConfigQuery(
|
||||
s3_backends
|
||||
)
|
||||
|
|
|
|||
|
|
@ -127,6 +127,18 @@ class InvalidRequest(S3ClientError):
|
|||
)
|
||||
|
||||
|
||||
class IllegalLocationConstraintException(S3ClientError):
|
||||
code = 400
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(IllegalLocationConstraintException, self).__init__(
|
||||
"IllegalLocationConstraintException",
|
||||
"The unspecified location constraint is incompatible for the region specific endpoint this request was sent to.",
|
||||
*args,
|
||||
**kwargs
|
||||
)
|
||||
|
||||
|
||||
class MalformedXML(S3ClientError):
|
||||
code = 400
|
||||
|
||||
|
|
@ -347,3 +359,12 @@ class InvalidPublicAccessBlockConfiguration(S3ClientError):
|
|||
*args,
|
||||
**kwargs
|
||||
)
|
||||
|
||||
|
||||
class WrongPublicAccessBlockAccountIdError(S3ClientError):
|
||||
code = 403
|
||||
|
||||
def __init__(self):
|
||||
super(WrongPublicAccessBlockAccountIdError, self).__init__(
|
||||
"AccessDenied", "Access Denied"
|
||||
)
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ import uuid
|
|||
import six
|
||||
|
||||
from bisect import insort
|
||||
from moto.core import BaseBackend, BaseModel
|
||||
from moto.core import ACCOUNT_ID, BaseBackend, BaseModel
|
||||
from moto.core.utils import iso_8601_datetime_with_milliseconds, rfc_1123_datetime
|
||||
from .exceptions import (
|
||||
BucketAlreadyExists,
|
||||
|
|
@ -37,6 +37,7 @@ from .exceptions import (
|
|||
CrossLocationLoggingProhibitted,
|
||||
NoSuchPublicAccessBlockConfiguration,
|
||||
InvalidPublicAccessBlockConfiguration,
|
||||
WrongPublicAccessBlockAccountIdError,
|
||||
)
|
||||
from .utils import clean_key_name, _VersionedKeyStore
|
||||
|
||||
|
|
@ -58,6 +59,13 @@ DEFAULT_TEXT_ENCODING = sys.getdefaultencoding()
|
|||
OWNER = "75aa57f09aa0c8caeab4f8c24e99d10f8e7faeebf76c078efc7c6caea54ba06a"
|
||||
|
||||
|
||||
def get_moto_s3_account_id():
|
||||
"""This makes it easy for mocking AWS Account IDs when using AWS Config
|
||||
-- Simply mock.patch the ACCOUNT_ID here, and Config gets it for free.
|
||||
"""
|
||||
return ACCOUNT_ID
|
||||
|
||||
|
||||
class FakeDeleteMarker(BaseModel):
|
||||
def __init__(self, key):
|
||||
self.key = key
|
||||
|
|
@ -1163,6 +1171,7 @@ class FakeBucket(BaseModel):
|
|||
class S3Backend(BaseBackend):
|
||||
def __init__(self):
|
||||
self.buckets = {}
|
||||
self.account_public_access_block = None
|
||||
|
||||
def create_bucket(self, bucket_name, region_name):
|
||||
if bucket_name in self.buckets:
|
||||
|
|
@ -1264,6 +1273,16 @@ class S3Backend(BaseBackend):
|
|||
|
||||
return bucket.public_access_block
|
||||
|
||||
def get_account_public_access_block(self, account_id):
|
||||
# The account ID should equal the account id that is set for Moto:
|
||||
if account_id != ACCOUNT_ID:
|
||||
raise WrongPublicAccessBlockAccountIdError()
|
||||
|
||||
if not self.account_public_access_block:
|
||||
raise NoSuchPublicAccessBlockConfiguration()
|
||||
|
||||
return self.account_public_access_block
|
||||
|
||||
def set_key(
|
||||
self, bucket_name, key_name, value, storage=None, etag=None, multipart=None
|
||||
):
|
||||
|
|
@ -1356,6 +1375,13 @@ class S3Backend(BaseBackend):
|
|||
bucket = self.get_bucket(bucket_name)
|
||||
bucket.public_access_block = None
|
||||
|
||||
def delete_account_public_access_block(self, account_id):
|
||||
# The account ID should equal the account id that is set for Moto:
|
||||
if account_id != ACCOUNT_ID:
|
||||
raise WrongPublicAccessBlockAccountIdError()
|
||||
|
||||
self.account_public_access_block = None
|
||||
|
||||
def put_bucket_notification_configuration(self, bucket_name, notification_config):
|
||||
bucket = self.get_bucket(bucket_name)
|
||||
bucket.set_notification_configuration(notification_config)
|
||||
|
|
@ -1384,6 +1410,21 @@ class S3Backend(BaseBackend):
|
|||
pub_block_config.get("RestrictPublicBuckets"),
|
||||
)
|
||||
|
||||
def put_account_public_access_block(self, account_id, pub_block_config):
|
||||
# The account ID should equal the account id that is set for Moto:
|
||||
if account_id != ACCOUNT_ID:
|
||||
raise WrongPublicAccessBlockAccountIdError()
|
||||
|
||||
if not pub_block_config:
|
||||
raise InvalidPublicAccessBlockConfiguration()
|
||||
|
||||
self.account_public_access_block = PublicAccessBlock(
|
||||
pub_block_config.get("BlockPublicAcls"),
|
||||
pub_block_config.get("IgnorePublicAcls"),
|
||||
pub_block_config.get("BlockPublicPolicy"),
|
||||
pub_block_config.get("RestrictPublicBuckets"),
|
||||
)
|
||||
|
||||
def initiate_multipart(self, bucket_name, key_name, metadata):
|
||||
bucket = self.get_bucket(bucket_name)
|
||||
new_multipart = FakeMultipart(key_name, metadata)
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import re
|
|||
import sys
|
||||
|
||||
import six
|
||||
from botocore.awsrequest import AWSPreparedRequest
|
||||
|
||||
from moto.core.utils import str_to_rfc_1123_datetime, py2_strip_unicode_keys
|
||||
from six.moves.urllib.parse import parse_qs, urlparse, unquote
|
||||
|
|
@ -29,6 +30,7 @@ from .exceptions import (
|
|||
InvalidPartOrder,
|
||||
MalformedXML,
|
||||
MalformedACLError,
|
||||
IllegalLocationConstraintException,
|
||||
InvalidNotificationARN,
|
||||
InvalidNotificationEvent,
|
||||
ObjectNotInActiveTierError,
|
||||
|
|
@ -122,6 +124,11 @@ ACTION_MAP = {
|
|||
"uploadId": "PutObject",
|
||||
},
|
||||
},
|
||||
"CONTROL": {
|
||||
"GET": {"publicAccessBlock": "GetPublicAccessBlock"},
|
||||
"PUT": {"publicAccessBlock": "PutPublicAccessBlock"},
|
||||
"DELETE": {"publicAccessBlock": "DeletePublicAccessBlock"},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -167,7 +174,7 @@ class ResponseObject(_TemplateEnvironmentMixin, ActionAuthenticatorMixin):
|
|||
or host.startswith("localhost")
|
||||
or host.startswith("localstack")
|
||||
or re.match(r"^[^.]+$", host)
|
||||
or re.match(r"^.*\.svc\.cluster\.local$", host)
|
||||
or re.match(r"^.*\.svc\.cluster\.local:?\d*$", host)
|
||||
):
|
||||
# Default to path-based buckets for (1) localhost, (2) localstack hosts (e.g. localstack.dev),
|
||||
# (3) local host names that do not contain a "." (e.g., Docker container host names), or
|
||||
|
|
@ -219,7 +226,7 @@ class ResponseObject(_TemplateEnvironmentMixin, ActionAuthenticatorMixin):
|
|||
# Depending on which calling format the client is using, we don't know
|
||||
# if this is a bucket or key request so we have to check
|
||||
if self.subdomain_based_buckets(request):
|
||||
return self.key_response(request, full_url, headers)
|
||||
return self.key_or_control_response(request, full_url, headers)
|
||||
else:
|
||||
# Using path-based buckets
|
||||
return self.bucket_response(request, full_url, headers)
|
||||
|
|
@ -286,7 +293,7 @@ class ResponseObject(_TemplateEnvironmentMixin, ActionAuthenticatorMixin):
|
|||
return self._bucket_response_post(request, body, bucket_name)
|
||||
else:
|
||||
raise NotImplementedError(
|
||||
"Method {0} has not been impelemented in the S3 backend yet".format(
|
||||
"Method {0} has not been implemented in the S3 backend yet".format(
|
||||
method
|
||||
)
|
||||
)
|
||||
|
|
@ -585,6 +592,29 @@ class ResponseObject(_TemplateEnvironmentMixin, ActionAuthenticatorMixin):
|
|||
next_continuation_token = None
|
||||
return result_keys, is_truncated, next_continuation_token
|
||||
|
||||
def _body_contains_location_constraint(self, body):
|
||||
if body:
|
||||
try:
|
||||
xmltodict.parse(body)["CreateBucketConfiguration"]["LocationConstraint"]
|
||||
return True
|
||||
except KeyError:
|
||||
pass
|
||||
return False
|
||||
|
||||
def _parse_pab_config(self, body):
|
||||
parsed_xml = xmltodict.parse(body)
|
||||
parsed_xml["PublicAccessBlockConfiguration"].pop("@xmlns", None)
|
||||
|
||||
# If Python 2, fix the unicode strings:
|
||||
if sys.version_info[0] < 3:
|
||||
parsed_xml = {
|
||||
"PublicAccessBlockConfiguration": py2_strip_unicode_keys(
|
||||
dict(parsed_xml["PublicAccessBlockConfiguration"])
|
||||
)
|
||||
}
|
||||
|
||||
return parsed_xml
|
||||
|
||||
def _bucket_response_put(
|
||||
self, request, body, region_name, bucket_name, querystring
|
||||
):
|
||||
|
|
@ -663,27 +693,23 @@ class ResponseObject(_TemplateEnvironmentMixin, ActionAuthenticatorMixin):
|
|||
raise e
|
||||
|
||||
elif "publicAccessBlock" in querystring:
|
||||
parsed_xml = xmltodict.parse(body)
|
||||
parsed_xml["PublicAccessBlockConfiguration"].pop("@xmlns", None)
|
||||
|
||||
# If Python 2, fix the unicode strings:
|
||||
if sys.version_info[0] < 3:
|
||||
parsed_xml = {
|
||||
"PublicAccessBlockConfiguration": py2_strip_unicode_keys(
|
||||
dict(parsed_xml["PublicAccessBlockConfiguration"])
|
||||
)
|
||||
}
|
||||
|
||||
pab_config = self._parse_pab_config(body)
|
||||
self.backend.put_bucket_public_access_block(
|
||||
bucket_name, parsed_xml["PublicAccessBlockConfiguration"]
|
||||
bucket_name, pab_config["PublicAccessBlockConfiguration"]
|
||||
)
|
||||
return ""
|
||||
|
||||
else:
|
||||
# us-east-1, the default AWS region behaves a bit differently
|
||||
# - you should not use it as a location constraint --> it fails
|
||||
# - querying the location constraint returns None
|
||||
# - LocationConstraint has to be specified if outside us-east-1
|
||||
if (
|
||||
region_name != DEFAULT_REGION_NAME
|
||||
and not self._body_contains_location_constraint(body)
|
||||
):
|
||||
raise IllegalLocationConstraintException()
|
||||
if body:
|
||||
# us-east-1, the default AWS region behaves a bit differently
|
||||
# - you should not use it as a location constraint --> it fails
|
||||
# - querying the location constraint returns None
|
||||
try:
|
||||
forced_region = xmltodict.parse(body)["CreateBucketConfiguration"][
|
||||
"LocationConstraint"
|
||||
|
|
@ -854,15 +880,21 @@ class ResponseObject(_TemplateEnvironmentMixin, ActionAuthenticatorMixin):
|
|||
)
|
||||
return 206, response_headers, response_content[begin : end + 1]
|
||||
|
||||
def key_response(self, request, full_url, headers):
|
||||
def key_or_control_response(self, request, full_url, headers):
|
||||
# Key and Control are lumped in because splitting out the regex is too much of a pain :/
|
||||
self.method = request.method
|
||||
self.path = self._get_path(request)
|
||||
self.headers = request.headers
|
||||
if "host" not in self.headers:
|
||||
self.headers["host"] = urlparse(full_url).netloc
|
||||
response_headers = {}
|
||||
|
||||
try:
|
||||
response = self._key_response(request, full_url, headers)
|
||||
# Is this an S3 control response?
|
||||
if isinstance(request, AWSPreparedRequest) and "s3-control" in request.url:
|
||||
response = self._control_response(request, full_url, headers)
|
||||
else:
|
||||
response = self._key_response(request, full_url, headers)
|
||||
except S3ClientError as s3error:
|
||||
response = s3error.code, {}, s3error.description
|
||||
|
||||
|
|
@ -878,6 +910,94 @@ class ResponseObject(_TemplateEnvironmentMixin, ActionAuthenticatorMixin):
|
|||
)
|
||||
return status_code, response_headers, response_content
|
||||
|
||||
def _control_response(self, request, full_url, headers):
|
||||
parsed_url = urlparse(full_url)
|
||||
query = parse_qs(parsed_url.query, keep_blank_values=True)
|
||||
method = request.method
|
||||
|
||||
if hasattr(request, "body"):
|
||||
# Boto
|
||||
body = request.body
|
||||
if hasattr(body, "read"):
|
||||
body = body.read()
|
||||
else:
|
||||
# Flask server
|
||||
body = request.data
|
||||
if body is None:
|
||||
body = b""
|
||||
|
||||
if method == "GET":
|
||||
return self._control_response_get(request, query, headers)
|
||||
elif method == "PUT":
|
||||
return self._control_response_put(request, body, query, headers)
|
||||
elif method == "DELETE":
|
||||
return self._control_response_delete(request, query, headers)
|
||||
else:
|
||||
raise NotImplementedError(
|
||||
"Method {0} has not been implemented in the S3 backend yet".format(
|
||||
method
|
||||
)
|
||||
)
|
||||
|
||||
def _control_response_get(self, request, query, headers):
|
||||
action = self.path.split("?")[0].split("/")[
|
||||
-1
|
||||
] # Gets the action out of the URL sans query params.
|
||||
self._set_action("CONTROL", "GET", action)
|
||||
self._authenticate_and_authorize_s3_action()
|
||||
|
||||
response_headers = {}
|
||||
if "publicAccessBlock" in action:
|
||||
public_block_config = self.backend.get_account_public_access_block(
|
||||
headers["x-amz-account-id"]
|
||||
)
|
||||
template = self.response_template(S3_PUBLIC_ACCESS_BLOCK_CONFIGURATION)
|
||||
return (
|
||||
200,
|
||||
response_headers,
|
||||
template.render(public_block_config=public_block_config),
|
||||
)
|
||||
|
||||
raise NotImplementedError(
|
||||
"Method {0} has not been implemented in the S3 backend yet".format(action)
|
||||
)
|
||||
|
||||
def _control_response_put(self, request, body, query, headers):
|
||||
action = self.path.split("?")[0].split("/")[
|
||||
-1
|
||||
] # Gets the action out of the URL sans query params.
|
||||
self._set_action("CONTROL", "PUT", action)
|
||||
self._authenticate_and_authorize_s3_action()
|
||||
|
||||
response_headers = {}
|
||||
if "publicAccessBlock" in action:
|
||||
pab_config = self._parse_pab_config(body)
|
||||
self.backend.put_account_public_access_block(
|
||||
headers["x-amz-account-id"],
|
||||
pab_config["PublicAccessBlockConfiguration"],
|
||||
)
|
||||
return 200, response_headers, ""
|
||||
|
||||
raise NotImplementedError(
|
||||
"Method {0} has not been implemented in the S3 backend yet".format(action)
|
||||
)
|
||||
|
||||
def _control_response_delete(self, request, query, headers):
|
||||
action = self.path.split("?")[0].split("/")[
|
||||
-1
|
||||
] # Gets the action out of the URL sans query params.
|
||||
self._set_action("CONTROL", "DELETE", action)
|
||||
self._authenticate_and_authorize_s3_action()
|
||||
|
||||
response_headers = {}
|
||||
if "publicAccessBlock" in action:
|
||||
self.backend.delete_account_public_access_block(headers["x-amz-account-id"])
|
||||
return 200, response_headers, ""
|
||||
|
||||
raise NotImplementedError(
|
||||
"Method {0} has not been implemented in the S3 backend yet".format(action)
|
||||
)
|
||||
|
||||
def _key_response(self, request, full_url, headers):
|
||||
parsed_url = urlparse(full_url)
|
||||
query = parse_qs(parsed_url.query, keep_blank_values=True)
|
||||
|
|
@ -1082,6 +1202,10 @@ class ResponseObject(_TemplateEnvironmentMixin, ActionAuthenticatorMixin):
|
|||
if mdirective is not None and mdirective == "REPLACE":
|
||||
metadata = metadata_from_headers(request.headers)
|
||||
new_key.set_metadata(metadata, replace=True)
|
||||
tdirective = request.headers.get("x-amz-tagging-directive")
|
||||
if tdirective == "REPLACE":
|
||||
tagging = self._tagging_from_headers(request.headers)
|
||||
new_key.set_tagging(tagging)
|
||||
template = self.response_template(S3_OBJECT_COPY_RESPONSE)
|
||||
response_headers.update(new_key.response_dict)
|
||||
return 200, response_headers, template.render(key=new_key)
|
||||
|
|
@ -1482,7 +1606,7 @@ S3_ALL_BUCKETS = """<ListAllMyBucketsResult xmlns="http://s3.amazonaws.com/doc/2
|
|||
{% for bucket in buckets %}
|
||||
<Bucket>
|
||||
<Name>{{ bucket.name }}</Name>
|
||||
<CreationDate>{{ bucket.creation_date }}</CreationDate>
|
||||
<CreationDate>{{ bucket.creation_date.isoformat() }}</CreationDate>
|
||||
</Bucket>
|
||||
{% endfor %}
|
||||
</Buckets>
|
||||
|
|
@ -1869,7 +1993,6 @@ S3_MULTIPART_LIST_RESPONSE = """<?xml version="1.0" encoding="UTF-8"?>
|
|||
<ID>75aa57f09aa0c8caeab4f8c24e99d10f8e7faeebf76c078efc7c6caea54ba06a</ID>
|
||||
<DisplayName>webfile</DisplayName>
|
||||
</Owner>
|
||||
<StorageClass>STANDARD</StorageClass>
|
||||
<PartNumberMarker>1</PartNumberMarker>
|
||||
<NextPartNumberMarker>{{ count }}</NextPartNumberMarker>
|
||||
<MaxParts>{{ count }}</MaxParts>
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ url_paths = {
|
|||
# subdomain key of path-based bucket
|
||||
"{0}/(?P<key_or_bucket_name>[^/]+)/?$": S3ResponseInstance.ambiguous_response,
|
||||
# path-based bucket + key
|
||||
"{0}/(?P<bucket_name_path>[^/]+)/(?P<key_name>.+)": S3ResponseInstance.key_response,
|
||||
"{0}/(?P<bucket_name_path>[^/]+)/(?P<key_name>.+)": S3ResponseInstance.key_or_control_response,
|
||||
# subdomain bucket + key with empty first part of path
|
||||
"{0}//(?P<key_name>.*)$": S3ResponseInstance.key_response,
|
||||
"{0}//(?P<key_name>.*)$": S3ResponseInstance.key_or_control_response,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@ def bucket_name_from_url(url):
|
|||
|
||||
REGION_URL_REGEX = re.compile(
|
||||
r"^https?://(s3[-\.](?P<region1>.+)\.amazonaws\.com/(.+)|"
|
||||
r"(.+)\.s3-(?P<region2>.+)\.amazonaws\.com)/?"
|
||||
r"(.+)\.s3[-\.](?P<region2>.+)\.amazonaws\.com)/?"
|
||||
)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -148,11 +148,15 @@ class SESBackend(BaseBackend):
|
|||
def __type_of_message__(self, destinations):
|
||||
"""Checks the destination for any special address that could indicate delivery,
|
||||
complaint or bounce like in SES simulator"""
|
||||
alladdress = (
|
||||
destinations.get("ToAddresses", [])
|
||||
+ destinations.get("CcAddresses", [])
|
||||
+ destinations.get("BccAddresses", [])
|
||||
)
|
||||
if isinstance(destinations, list):
|
||||
alladdress = destinations
|
||||
else:
|
||||
alladdress = (
|
||||
destinations.get("ToAddresses", [])
|
||||
+ destinations.get("CcAddresses", [])
|
||||
+ destinations.get("BccAddresses", [])
|
||||
)
|
||||
|
||||
for addr in alladdress:
|
||||
if SESFeedback.SUCCESS_ADDR in addr:
|
||||
return SESFeedback.DELIVERY
|
||||
|
|
|
|||
|
|
@ -99,3 +99,28 @@ class InvalidAttributeName(RESTError):
|
|||
super(InvalidAttributeName, self).__init__(
|
||||
"InvalidAttributeName", "Unknown Attribute {}.".format(attribute_name)
|
||||
)
|
||||
|
||||
|
||||
class InvalidParameterValue(RESTError):
|
||||
code = 400
|
||||
|
||||
def __init__(self, message):
|
||||
super(InvalidParameterValue, self).__init__("InvalidParameterValue", message)
|
||||
|
||||
|
||||
class MissingParameter(RESTError):
|
||||
code = 400
|
||||
|
||||
def __init__(self):
|
||||
super(MissingParameter, self).__init__(
|
||||
"MissingParameter", "The request must contain the parameter Actions."
|
||||
)
|
||||
|
||||
|
||||
class OverLimit(RESTError):
|
||||
code = 403
|
||||
|
||||
def __init__(self, count):
|
||||
super(OverLimit, self).__init__(
|
||||
"OverLimit", "{} Actions were found, maximum allowed is 7.".format(count)
|
||||
)
|
||||
|
|
|
|||
|
|
@ -30,6 +30,9 @@ from .exceptions import (
|
|||
BatchEntryIdsNotDistinct,
|
||||
TooManyEntriesInBatchRequest,
|
||||
InvalidAttributeName,
|
||||
InvalidParameterValue,
|
||||
MissingParameter,
|
||||
OverLimit,
|
||||
)
|
||||
|
||||
from moto.core import ACCOUNT_ID as DEFAULT_ACCOUNT_ID
|
||||
|
|
@ -183,6 +186,8 @@ class Queue(BaseModel):
|
|||
"MaximumMessageSize",
|
||||
"MessageRetentionPeriod",
|
||||
"QueueArn",
|
||||
"Policy",
|
||||
"RedrivePolicy",
|
||||
"ReceiveMessageWaitTimeSeconds",
|
||||
"VisibilityTimeout",
|
||||
]
|
||||
|
|
@ -194,6 +199,8 @@ class Queue(BaseModel):
|
|||
"DeleteMessage",
|
||||
"GetQueueAttributes",
|
||||
"GetQueueUrl",
|
||||
"ListDeadLetterSourceQueues",
|
||||
"PurgeQueue",
|
||||
"ReceiveMessage",
|
||||
"SendMessage",
|
||||
)
|
||||
|
|
@ -272,7 +279,7 @@ class Queue(BaseModel):
|
|||
if key in bool_fields:
|
||||
value = value == "true"
|
||||
|
||||
if key == "RedrivePolicy" and value is not None:
|
||||
if key in ["Policy", "RedrivePolicy"] and value is not None:
|
||||
continue
|
||||
|
||||
setattr(self, camelcase_to_underscores(key), value)
|
||||
|
|
@ -280,6 +287,9 @@ class Queue(BaseModel):
|
|||
if attributes.get("RedrivePolicy", None):
|
||||
self._setup_dlq(attributes["RedrivePolicy"])
|
||||
|
||||
if attributes.get("Policy"):
|
||||
self.policy = attributes["Policy"]
|
||||
|
||||
self.last_modified_timestamp = now
|
||||
|
||||
def _setup_dlq(self, policy):
|
||||
|
|
@ -471,6 +481,24 @@ class Queue(BaseModel):
|
|||
return self.name
|
||||
raise UnformattedGetAttTemplateException()
|
||||
|
||||
@property
|
||||
def policy(self):
|
||||
if self._policy_json.get("Statement"):
|
||||
return json.dumps(self._policy_json)
|
||||
else:
|
||||
return None
|
||||
|
||||
@policy.setter
|
||||
def policy(self, policy):
|
||||
if policy:
|
||||
self._policy_json = json.loads(policy)
|
||||
else:
|
||||
self._policy_json = {
|
||||
"Version": "2012-10-17",
|
||||
"Id": "{}/SQSDefaultPolicy".format(self.queue_arn),
|
||||
"Statement": [],
|
||||
}
|
||||
|
||||
|
||||
class SQSBackend(BaseBackend):
|
||||
def __init__(self, region_name):
|
||||
|
|
@ -539,7 +567,7 @@ class SQSBackend(BaseBackend):
|
|||
for name, q in self.queues.items():
|
||||
if prefix_re.search(name):
|
||||
qs.append(q)
|
||||
return qs
|
||||
return qs[:1000]
|
||||
|
||||
def get_queue(self, queue_name):
|
||||
queue = self.queues.get(queue_name)
|
||||
|
|
@ -801,25 +829,75 @@ class SQSBackend(BaseBackend):
|
|||
def add_permission(self, queue_name, actions, account_ids, label):
|
||||
queue = self.get_queue(queue_name)
|
||||
|
||||
if actions is None or len(actions) == 0:
|
||||
raise RESTError("InvalidParameterValue", "Need at least one Action")
|
||||
if account_ids is None or len(account_ids) == 0:
|
||||
raise RESTError("InvalidParameterValue", "Need at least one Account ID")
|
||||
if not actions:
|
||||
raise MissingParameter()
|
||||
|
||||
if not all([item in Queue.ALLOWED_PERMISSIONS for item in actions]):
|
||||
raise RESTError("InvalidParameterValue", "Invalid permissions")
|
||||
if not account_ids:
|
||||
raise InvalidParameterValue(
|
||||
"Value [] for parameter PrincipalId is invalid. Reason: Unable to verify."
|
||||
)
|
||||
|
||||
queue.permissions[label] = (account_ids, actions)
|
||||
count = len(actions)
|
||||
if count > 7:
|
||||
raise OverLimit(count)
|
||||
|
||||
invalid_action = next(
|
||||
(action for action in actions if action not in Queue.ALLOWED_PERMISSIONS),
|
||||
None,
|
||||
)
|
||||
if invalid_action:
|
||||
raise InvalidParameterValue(
|
||||
"Value SQS:{} for parameter ActionName is invalid. "
|
||||
"Reason: Only the queue owner is allowed to invoke this action.".format(
|
||||
invalid_action
|
||||
)
|
||||
)
|
||||
|
||||
policy = queue._policy_json
|
||||
statement = next(
|
||||
(
|
||||
statement
|
||||
for statement in policy["Statement"]
|
||||
if statement["Sid"] == label
|
||||
),
|
||||
None,
|
||||
)
|
||||
if statement:
|
||||
raise InvalidParameterValue(
|
||||
"Value {} for parameter Label is invalid. "
|
||||
"Reason: Already exists.".format(label)
|
||||
)
|
||||
|
||||
principals = [
|
||||
"arn:aws:iam::{}:root".format(account_id) for account_id in account_ids
|
||||
]
|
||||
actions = ["SQS:{}".format(action) for action in actions]
|
||||
|
||||
statement = {
|
||||
"Sid": label,
|
||||
"Effect": "Allow",
|
||||
"Principal": {"AWS": principals[0] if len(principals) == 1 else principals},
|
||||
"Action": actions[0] if len(actions) == 1 else actions,
|
||||
"Resource": queue.queue_arn,
|
||||
}
|
||||
|
||||
queue._policy_json["Statement"].append(statement)
|
||||
|
||||
def remove_permission(self, queue_name, label):
|
||||
queue = self.get_queue(queue_name)
|
||||
|
||||
if label not in queue.permissions:
|
||||
raise RESTError(
|
||||
"InvalidParameterValue", "Permission doesnt exist for the given label"
|
||||
statements = queue._policy_json["Statement"]
|
||||
statements_new = [
|
||||
statement for statement in statements if statement["Sid"] != label
|
||||
]
|
||||
|
||||
if len(statements) == len(statements_new):
|
||||
raise InvalidParameterValue(
|
||||
"Value {} for parameter Label is invalid. "
|
||||
"Reason: can't find label on existing policy.".format(label)
|
||||
)
|
||||
|
||||
del queue.permissions[label]
|
||||
queue._policy_json["Statement"] = statements_new
|
||||
|
||||
def tag_queue(self, queue_name, tags):
|
||||
queue = self.get_queue(queue_name)
|
||||
|
|
|
|||
|
|
@ -127,6 +127,10 @@ class WorkflowExecution(BaseModel):
|
|||
"executionInfo": self.to_medium_dict(),
|
||||
"executionConfiguration": {"taskList": {"name": self.task_list}},
|
||||
}
|
||||
# info
|
||||
if self.execution_status == "CLOSED":
|
||||
hsh["executionInfo"]["closeStatus"] = self.close_status
|
||||
hsh["executionInfo"]["closeTimestamp"] = self.close_timestamp
|
||||
# configuration
|
||||
for key in self._configuration_keys:
|
||||
attr = camelcase_to_underscores(key)
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
class TaggingService:
|
||||
def __init__(self, tagName='Tags', keyName='Key', valueName='Value'):
|
||||
def __init__(self, tagName="Tags", keyName="Key", valueName="Value"):
|
||||
self.tagName = tagName
|
||||
self.keyName = keyName
|
||||
self.valueName = valueName
|
||||
|
|
@ -12,6 +12,12 @@ class TaggingService:
|
|||
result.append({self.keyName: k, self.valueName: v})
|
||||
return {self.tagName: result}
|
||||
|
||||
def delete_all_tags_for_resource(self, arn):
|
||||
del self.tags[arn]
|
||||
|
||||
def has_tags(self, arn):
|
||||
return arn in self.tags
|
||||
|
||||
def tag_resource(self, arn, tags):
|
||||
if arn not in self.tags:
|
||||
self.tags[arn] = {}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue