Fix: IoT does not work in server mode (#3644)

Closes #1631
This commit is contained in:
Brian Pandola 2021-02-01 05:15:57 -08:00 committed by GitHub
parent 0211e9d78d
commit c9dd9cc7f9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 120 additions and 13 deletions

View File

@ -1,13 +1,10 @@
SHELL := /bin/bash
ifeq ($(TEST_SERVER_MODE), true)
# exclude test_iot and test_iotdata for now
# because authentication of iot is very complicated
# exclude test_kinesisvideoarchivedmedia
# because testing with moto_server is difficult with data-endpoint
TEST_EXCLUDE := -k 'not (test_iot or test_kinesisvideoarchivedmedia)'
TEST_EXCLUDE := -k 'not test_kinesisvideoarchivedmedia'
else
TEST_EXCLUDE :=
endif

View File

@ -425,8 +425,19 @@ class IoTResponse(BaseResponse):
self.iot_backend.attach_policy(policy_name=policy_name, target=target)
return json.dumps(dict())
def dispatch_attached_policies(self, request, full_url, headers):
# This endpoint requires specialized handling because it has
# a uri parameter containing forward slashes that is not
# correctly url encoded when we're running in server mode.
# https://github.com/pallets/flask/issues/900
self.setup_class(request, full_url, headers)
self.querystring["Action"] = ["ListAttachedPolicies"]
target = self.path.partition("/attached-policies/")[-1]
self.querystring["target"] = [unquote(target)] if "%" in target else [target]
return self.call_action()
def list_attached_policies(self):
principal = unquote(self._get_param("target"))
principal = 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)

View File

@ -7,4 +7,19 @@ url_bases = ["https?://iot.(.+).amazonaws.com"]
response = IoTResponse()
url_paths = {"{0}/.*$": response.dispatch}
url_paths = {
#
# Paths for :class:`moto.core.models.MockAWS`
#
# This route requires special handling.
"{0}/attached-policies/(?P<target>.*)$": response.dispatch_attached_policies,
# The remaining routes can be handled by the default dispatcher.
"{0}/.*$": response.dispatch,
#
# (Flask) Paths for :class:`moto.core.models.ServerModeMockAWS`
#
# This route requires special handling.
"{0}/attached-policies/<path:target>$": response.dispatch_attached_policies,
# The remaining routes can be handled by the default dispatcher.
"{0}/<path:route>$": response.dispatch,
}

View File

@ -2,6 +2,7 @@ from __future__ import unicode_literals
from moto.core.responses import BaseResponse
from .models import iotdata_backends
import json
from six.moves.urllib.parse import unquote
class IoTDataPlaneResponse(BaseResponse):
@ -29,6 +30,17 @@ class IoTDataPlaneResponse(BaseResponse):
payload = self.iotdata_backend.delete_thing_shadow(thing_name=thing_name)
return json.dumps(payload.to_dict())
def dispatch_publish(self, request, full_url, headers):
# This endpoint requires specialized handling because it has
# a uri parameter containing forward slashes that is not
# correctly url encoded when we're running in server mode.
# https://github.com/pallets/flask/issues/900
self.setup_class(request, full_url, headers)
self.querystring["Action"] = ["Publish"]
topic = self.path.partition("/topics/")[-1]
self.querystring["target"] = [unquote(topic)] if "%" in topic else [topic]
return self.call_action()
def publish(self):
topic = self._get_param("topic")
qos = self._get_int_param("qos")

View File

@ -7,4 +7,19 @@ url_bases = ["https?://data.iot.(.+).amazonaws.com"]
response = IoTDataPlaneResponse()
url_paths = {"{0}/.*$": response.dispatch}
url_paths = {
#
# Paths for :class:`moto.core.models.MockAWS`
#
# This route requires special handling.
"{0}/topics/(?P<topic>.*)$": response.dispatch_publish,
# The remaining routes can be handled by the default dispatcher.
"{0}/.*$": response.dispatch,
#
# (Flask) Paths for :class:`moto.core.models.ServerModeMockAWS`
#
# This route requires special handling.
"{0}/topics/<path:topic>$": response.dispatch_publish,
# The remaining routes can be handled by the default dispatcher.
"{0}/<path:route>$": response.dispatch,
}

View File

@ -34,6 +34,13 @@ UNSIGNED_REQUESTS = {
}
UNSIGNED_ACTIONS = {"AssumeRoleWithSAML": ("sts", "us-east-1")}
# Some services have v4 signing names that differ from the backend service name/id.
SIGNING_ALIASES = {
"eventbridge": "events",
"execute-api": "iot",
"iotdata": "data.iot",
}
class DomainDispatcherApplication(object):
"""
@ -74,6 +81,7 @@ class DomainDispatcherApplication(object):
try:
credential_scope = auth.split(",")[0].split()[1]
_, _, region, service, _ = credential_scope.split("/")
service = SIGNING_ALIASES.get(service.lower(), service)
except ValueError:
# Signature format does not match, this is exceptional and we can't
# infer a service-region. A reduced set of services still use
@ -94,11 +102,6 @@ class DomainDispatcherApplication(object):
# S3 is the last resort when the target is also unknown
service, region = DEFAULT_SERVICE_REGION
if service == "EventBridge":
# Go SDK uses 'EventBridge' in the SigV4 request instead of 'events'
# see https://github.com/spulec/moto/issues/3494
service = "events"
if service == "dynamodb":
if environ["HTTP_X_AMZ_TARGET"].startswith("DynamoDBStreams"):
host = "dynamodbstreams"

View File

@ -1,5 +1,9 @@
from __future__ import unicode_literals
import json
from six.moves.urllib.parse import quote
import pytest
import sure # noqa
import moto.server as server
@ -17,4 +21,32 @@ def test_iot_list():
# just making sure that server is up
res = test_client.get("/things")
res.status_code.should.equal(404)
res.status_code.should.equal(200)
@pytest.mark.parametrize(
"url_encode_arn",
[
pytest.param(True, id="Target Arn in Path is URL encoded"),
pytest.param(False, id="Target Arn in Path is *not* URL encoded"),
],
)
@mock_iot
def test_list_attached_policies(url_encode_arn):
backend = server.create_backend_app("iot")
test_client = backend.test_client()
result = test_client.post("/keys-and-certificate?setAsActive=true")
result_dict = json.loads(result.data.decode("utf-8"))
certificate_arn = result_dict["certificateArn"]
test_client.post("/policies/my-policy", json={"policyDocument": {}})
test_client.put("/target-policies/my-policy", json={"target": certificate_arn})
if url_encode_arn:
certificate_arn = quote(certificate_arn, safe="")
result = test_client.post("/attached-policies/{}".format(certificate_arn))
result.status_code.should.equal(200)
result_dict = json.loads(result.data.decode("utf-8"))
result_dict["policies"][0]["policyName"].should.equal("my-policy")

View File

@ -1,5 +1,8 @@
from __future__ import unicode_literals
from six.moves.urllib.parse import quote
import pytest
import sure # noqa
import moto.server as server
@ -19,3 +22,22 @@ def test_iotdata_list():
thing_name = "nothing"
res = test_client.get("/things/{}/shadow".format(thing_name))
res.status_code.should.equal(404)
@pytest.mark.parametrize(
"url_encode_topic",
[
pytest.param(True, id="Topic in Path is URL encoded"),
pytest.param(False, id="Topic in Path is *not* URL encoded"),
],
)
@mock_iotdata
def test_publish(url_encode_topic):
backend = server.create_backend_app("iot-data")
test_client = backend.test_client()
topic = "test/topic"
topic_for_path = quote(topic, safe="") if url_encode_topic else topic
result = test_client.post("/topics/{}".format(topic_for_path))
result.status_code.should.equal(200)