Repository: getmoto/moto
Pull request #8134 (2024-09-20 22:08:24)

Workspaces Web User Settings Support

jamarcelin: @bblommers Hello! Any idea how to fix this linting issue? The line with the `# type: ignore[misc]` keeps getting moved around by the formatter.

Patch:
diff --git a/moto/networkmanager/models.py b/moto/networkmanager/models.py
index 238b29c8a..a60a3886d 100644
--- a/moto/networkmanager/models.py
+++ b/moto/networkmanager/models.py
@@ -270,6 +270,11 @@ class NetworkManagerBackend(BaseBackend):
         except (KeyError, ValueError, AttributeError):
             raise ResourceNotFound(arn)
 
+    def update_resource_state(self, resource_arn: str, state: str) -> None:
+        # Acceptable states: PENDING, AVAILABLE, DELETING, UPDATING
+        resource = self._get_resource_from_arn(resource_arn)
+        resource.state = state
+
     def create_global_network(
         self,
         description: Optional[str],
diff --git a/moto/networkmanager/responses.py b/moto/networkmanager/responses.py
index d64cf8e1a..443db2e72 100644
--- a/moto/networkmanager/responses.py
+++ b/moto/networkmanager/responses.py
@@ -27,7 +27,11 @@ class NetworkManagerResponse(BaseResponse):
             description=description,
             tags=tags,
         )
-        return json.dumps(dict(GlobalNetwork=global_network.to_dict()))
+        resp_dict = dict(GlobalNetwork=global_network.to_dict())
+        self.networkmanager_backend.update_resource_state(
+            global_network.global_network_arn, "AVAILABLE"
+        )
+        return json.dumps(resp_dict)
 
     def create_core_network(self) -> str:
         params = json.loads(self.body)
@@ -43,7 +47,11 @@ class NetworkManagerResponse(BaseResponse):
             policy_document=policy_document,
             client_token=client_token,
         )
-        return json.dumps(dict(CoreNetwork=core_network.to_dict()))
+        resp_dict = dict(CoreNetwork=core_network.to_dict())
+        self.networkmanager_backend.update_resource_state(
+            core_network.core_network_arn, "AVAILABLE"
+        )
+        return json.dumps(resp_dict)
 
     def delete_core_network(self) -> str:
         core_network_id = unquote(self.path.split("/")[-1])
@@ -122,7 +130,9 @@ class NetworkManagerResponse(BaseResponse):
             location=location,
             tags=tags,
         )
-        return json.dumps(dict(Site=site.to_dict()))
+        resp_dict = dict(Site=site.to_dict())
+        self.networkmanager_backend.update_resource_state(site.site_arn, "AVAILABLE")
+        return json.dumps(resp_dict)
 
     def delete_site(self) -> str:
         global_network_id = unquote(self.path.split("/")[-3])
@@ -166,7 +176,9 @@ class NetworkManagerResponse(BaseResponse):
             site_id=site_id,
             tags=tags,
         )
-        return json.dumps(dict(Link=link.to_dict()))
+        resp_dict = dict(Link=link.to_dict())
+        self.networkmanager_backend.update_resource_state(link.link_arn, "AVAILABLE")
+        return json.dumps(resp_dict)
 
     def get_links(self) -> str:
         params = self._get_params()
@@ -222,7 +234,11 @@ class NetworkManagerResponse(BaseResponse):
             site_id=site_id,
             tags=tags,
         )
-        return json.dumps(dict(Device=device.to_dict()))
+        resp_dict = dict(Device=device.to_dict())
+        self.networkmanager_backend.update_resource_state(
+            device.device_arn, "AVAILABLE"
+        )
+        return json.dumps(resp_dict)
 
     def get_devices(self) -> str:
         params = self._get_params()
diff --git a/moto/route53/models.py b/moto/route53/models.py
index 4fe146320..dc8e4e21e 100644
--- a/moto/route53/models.py
+++ b/moto/route53/models.py
@@ -647,10 +647,12 @@ class Route53Backend(BaseBackend):
             return self.resource_tags[resource_id]
         return {}
 
-    def list_tags_for_resources(self, resource_ids: List[str]) -> Dict[str, str]:
-        resources = {}
+    def list_tags_for_resources(self, resource_ids: List[str]) -> List[Dict[str, Any]]:
+        resources = []
         for id in resource_ids:
-            resources.update(self.list_tags_for_resource(id))
+            resource_set = {"ResourceId": id, "Tags": {}}
+            resource_set["Tags"] = self.list_tags_for_resource(id)
+            resources.append(resource_set)
         return resources
 
     def list_resource_record_sets(
diff --git a/moto/route53/responses.py b/moto/route53/responses.py
index 9dbc16423..6150ae02d 100644
--- a/moto/route53/responses.py
+++ b/moto/route53/responses.py
@@ -2,7 +2,7 @@
 
 import re
 from typing import Any
-from urllib.parse import parse_qs
+from urllib.parse import parse_qs, unquote
 
 import xmltodict
 from jinja2 import Template
@@ -346,8 +346,10 @@ class Route53(BaseResponse):
     ) -> TYPE_RESPONSE:
         self.setup_class(request, full_url, headers)
 
-        id_ = self.parsed_url.path.split("/")[-1]
-        type_ = self.parsed_url.path.split("/")[-2]
+        id_matcher = re.search(r"/tags/([-a-z]+)/(.+)", self.parsed_url.path)
+        assert id_matcher
+        type_ = id_matcher.group(1)
+        id_ = unquote(id_matcher.group(2))
 
         if request.method == "GET":
             tags = self.backend.list_tags_for_resource(id_)
@@ -371,12 +373,16 @@ class Route53(BaseResponse):
             return 200, headers, template.render()
 
     def list_tags_for_resources(self) -> str:
+        if id_matcher := re.search(r"/tags/[-a-z]+/(.+)", self.parsed_url.path):
+            resource_type = unquote(id_matcher.group(1))
+        else:
+            resource_type = ""
         resource_ids = xmltodict.parse(self.body)["ListTagsForResourcesRequest"][
             "ResourceIds"
-        ]
+        ]["ResourceId"]
         tag_sets = self.backend.list_tags_for_resources(resource_ids=resource_ids)
         template = Template(LIST_TAGS_FOR_RESOURCES_RESPONSE)
-        return template.render(tag_sets)
+        return template.render(tag_sets=tag_sets, resource_type=resource_type)
 
     def get_change(self) -> str:
         change_id = self.parsed_url.path.rstrip("/").rsplit("/", 1)[1]
@@ -494,10 +500,10 @@ LIST_TAGS_FOR_RESOURCES_RESPONSE = """
     <ResourceTagSets>
         {% for set in tag_sets %}
         <ResourceTagSet>
-            <ResourceType>{{set.resource_type}}</ResourceType>
-            <ResourceId>{{set.resource_id}}</ResourceId>
+            <ResourceType>{{resource_type}}</ResourceType>
+            <ResourceId>{{set["ResourceId"]}}</ResourceId>
             <Tags>
-                {% for key, value in set.tags.items() %}
+                {% for key, value in set["Tags"].items() %}
                 <Tag>
                     <Key>{{key}}</Key>
                     <Value>{{value}}</Value>
diff --git a/moto/workspacesweb/models.py b/moto/workspacesweb/models.py
index 2cd4612d4..23c95af99 100644
--- a/moto/workspacesweb/models.py
+++ b/moto/workspacesweb/models.py
@@ -9,6 +9,95 @@ from moto.core.common_models import BaseModel
 from moto.utilities.utils import get_partition
 
 
+class FakeUserSettings(BaseModel):
+    def __init__(
+        self,
+        additional_encryption_context: Any,
+        client_token: str,
+        cookie_synchronization_configuration: str,
+        copy_allowed: bool,
+        customer_managed_key: str,
+        deep_link_allowed: bool,
+        disconnect_timeout_in_minutes: int,
+        download_allowed: bool,
+        idle_disconnect_timeout_in_minutes: int,
+        paste_allowed: bool,
+        print_allowed: bool,
+        tags: Dict[str, str],
+        upload_allowed: bool,
+        region_name: str,
+        account_id: str,
+    ):
+        self.user_settings_id = str(uuid.uuid4())
+        self.arn = self.arn_formatter(self.user_settings_id, account_id, region_name)
+        self.additional_encryption_context = additional_encryption_context
+        self.client_token = client_token
+        self.cookie_synchronization_configuration = cookie_synchronization_configuration
+        self.copy_allowed = "Enabled" if copy_allowed else "Disabled"
+        self.customer_managed_key = customer_managed_key
+        self.deep_link_allowed = "Enabled" if deep_link_allowed else "Disabled"
+        self.disconnect_timeout_in_minutes = disconnect_timeout_in_minutes
+        self.download_allowed = "Enabled" if download_allowed else "Disabled"
+        self.idle_disconnect_timeout_in_minutes = idle_disconnect_timeout_in_minutes
+        self.paste_allowed = "Enabled" if paste_allowed else "Disabled"
+        self.print_allowed = "Enabled" if print_allowed else "Disabled"
+        self.tags = tags
+        self.upload_allowed = "Enabled" if upload_allowed else "Disabled"
+        self.associated_portal_arns: List[str] = []
+
+    def arn_formatter(self, _id: str, account_id: str, region_name: str) -> str:
+        return f"arn:{get_partition(region_name)}:workspaces-web:{region_name}:{account_id}:user-settings/{_id}"
+
+    def to_dict(self) -> Dict[str, Any]:
+        return {
+            "associatedPortalArns": self.associated_portal_arns,
+            "additionalEncryptionContext": self.additional_encryption_context,
+            "clientToken": self.client_token,
+            "cookieSynchronizationConfiguration": self.cookie_synchronization_configuration,
+            "copyAllowed": self.copy_allowed,
+            "customerManagedKey": self.customer_managed_key,
+            "deepLinkAllowed": self.deep_link_allowed,
+            "disconnectTimeoutInMinutes": self.disconnect_timeout_in_minutes,
+            "downloadAllowed": self.download_allowed,
+            "idleDisconnectTimeoutInMinutes": self.idle_disconnect_timeout_in_minutes,
+            "pasteAllowed": self.paste_allowed,
+            "printAllowed": self.print_allowed,
+            "tags": self.tags,
+            "uploadAllowed": self.upload_allowed,
+            "userSettingsArn": self.arn,
+        }
+
+
+class FakeUserAccessLoggingSettings(BaseModel):
+    def __init__(
+        self,
+        client_token: str,
+        kinesis_stream_arn: str,
+        tags: Dict[str, str],
+        region_name: str,
+        account_id: str,
+    ):
+        self.user_access_logging_settings_id = str(uuid.uuid4())
+        self.arn = self.arn_formatter(
+            self.user_access_logging_settings_id, account_id, region_name
+        )
+        self.client_token = client_token
+        self.kinesis_stream_arn = kinesis_stream_arn
+        self.tags = tags
+        self.associated_portal_arns: List[str] = []
+
+    def arn_formatter(self, _id: str, account_id: str, region_name: str) -> str:
+        return f"arn:{get_partition(region_name)}:workspaces-web:{region_name}:{account_id}:user-access-logging-settings/{_id}"
+
+    def to_dict(self) -> Dict[str, Any]:
+        return {
+            "associatedPortalArns": self.associated_portal_arns,
+            "kinesisStreamArn": self.kinesis_stream_arn,
+            "tags": self.tags,
+            "userAccessLoggingSettingsArn": self.arn,
+        }
+
+
 class FakeNetworkSettings(BaseModel):
     def __init__(
         self,
@@ -147,6 +236,8 @@ class WorkSpacesWebBackend(BaseBackend):
         super().__init__(region_name, account_id)
         self.network_settings: Dict[str, FakeNetworkSettings] = {}
         self.browser_settings: Dict[str, FakeBrowserSettings] = {}
+        self.user_settings: Dict[str, FakeUserSettings] = {}
+        self.user_access_logging_settings: Dict[str, FakeUserAccessLoggingSettings] = {}
         self.portals: Dict[str, FakePortal] = {}
 
     def create_network_settings(
@@ -286,5 +377,104 @@ class WorkSpacesWebBackend(BaseBackend):
         portal_object.network_settings_arn = network_settings_arn
         return network_settings_arn, portal_arn
 
+    def create_user_settings(
+        self,
+        additional_encryption_context: Any,
+        client_token: Any,
+        cookie_synchronization_configuration: Any,
+        copy_allowed: Any,
+        customer_managed_key: Any,
+        deep_link_allowed: Any,
+        disconnect_timeout_in_minutes: Any,
+        download_allowed: Any,
+        idle_disconnect_timeout_in_minutes: Any,
+        paste_allowed: Any,
+        print_allowed: Any,
+        tags: Any,
+        upload_allowed: Any,
+    ) -> str:
+        user_settings_object = FakeUserSettings(
+            additional_encryption_context,
+            client_token,
+            cookie_synchronization_configuration,
+            copy_allowed,
+            customer_managed_key,
+            deep_link_allowed,
+            disconnect_timeout_in_minutes,
+            download_allowed,
+            idle_disconnect_timeout_in_minutes,
+            paste_allowed,
+            print_allowed,
+            tags,
+            upload_allowed,
+            self.region_name,
+            self.account_id,
+        )
+        self.user_settings[user_settings_object.arn] = user_settings_object
+        return user_settings_object.arn
+
+    def get_user_settings(self, user_settings_arn: str) -> Dict[str, Any]:
+        return self.user_settings[user_settings_arn].to_dict()
+
+    def delete_user_settings(self, user_settings_arn: str) -> None:
+        self.user_settings.pop(user_settings_arn)
+
+    def create_user_access_logging_settings(
+        self, client_token: Any, kinesis_stream_arn: Any, tags: Any
+    ) -> str:
+        user_access_logging_settings_object = FakeUserAccessLoggingSettings(
+            client_token, kinesis_stream_arn, tags, self.region_name, self.account_id
+        )
+        self.user_access_logging_settings[user_access_logging_settings_object.arn] = (
+            user_access_logging_settings_object
+        )
+        return user_access_logging_settings_object.arn
+
+    def get_user_access_logging_settings(
+        self, user_access_logging_settings_arn: str
+    ) -> Dict[str, Any]:
+        return self.user_access_logging_settings[
+            user_access_logging_settings_arn
+        ].to_dict()
+
+    def delete_user_access_logging_settings(
+        self, user_access_logging_settings_arn: str
+    ) -> None:
+        self.user_access_logging_settings.pop(user_access_logging_settings_arn)
+
+    def associate_user_settings(
+        self, portal_arn: str, user_settings_arn: str
+    ) -> Tuple[str, str]:
+        user_settings_object = self.user_settings[user_settings_arn]
+        portal_object = self.portals[portal_arn]
+        user_settings_object.associated_portal_arns.append(portal_arn)
+        portal_object.user_settings_arn = user_settings_arn
+        return portal_arn, user_settings_arn
+
+    def associate_user_access_logging_settings(
+        self, portal_arn: str, user_access_logging_settings_arn: str
+    ) -> Tuple[str, str]:
+        user_access_logging_settings_object = self.user_access_logging_settings[
+            user_access_logging_settings_arn
+        ]
+        portal_object = self.portals[portal_arn]
+        user_access_logging_settings_object.associated_portal_arns.append(portal_arn)
+        portal_object.user_access_logging_settings_arn = (
+            user_access_logging_settings_arn
+        )
+        return portal_arn, user_access_logging_settings_arn
+
+    def list_user_settings(self) -> List[Dict[str, str]]:
+        return [
+            {"userSettingsArn": user_settings.arn}
+            for user_settings in self.user_settings.values()
+        ]
+
+    def list_user_access_logging_settings(self) -> List[Dict[str, str]]:
+        return [
+            {"userAccessLoggingSettingsArn": user_access_logging_settings.arn}
+            for user_access_logging_settings in self.user_access_logging_settings.values()
+        ]
+
 
 workspacesweb_backends = BackendDict(WorkSpacesWebBackend, "workspaces-web")
diff --git a/moto/workspacesweb/responses.py b/moto/workspacesweb/responses.py
index dcda234f3..e4f43f478 100644
--- a/moto/workspacesweb/responses.py
+++ b/moto/workspacesweb/responses.py
@@ -38,6 +38,26 @@ class WorkSpacesWebResponse(BaseResponse):
         else:
             return handler.delete_browser_settings()
 
+    @staticmethod
+    def user_settings(request: Any, full_url: str, headers: Any) -> TYPE_RESPONSE:  # type: ignore[misc]
+        handler = WorkSpacesWebResponse()
+        handler.setup_class(request, full_url, headers)
+        if request.method == "GET":
+            return handler.get_user_settings()
+        else:
+            return handler.delete_user_settings()
+
+    @staticmethod
+    def user_access_logging_settings(  # type: ignore[misc]
+        request: Any, full_url: str, headers: Any
+    ) -> TYPE_RESPONSE:
+        handler = WorkSpacesWebResponse()
+        handler.setup_class(request, full_url, headers)
+        if request.method == "GET":
+            return handler.get_user_access_logging_settings()
+        else:
+            return handler.delete_user_access_logging_settings()
+
     @staticmethod
     def portal(request: Any, full_url: str, headers: Any) -> TYPE_RESPONSE:  # type: ignore[misc]
         handler = WorkSpacesWebResponse()
@@ -183,3 +203,139 @@ class WorkSpacesWebResponse(BaseResponse):
         return json.dumps(
             dict(networkSettingsArn=network_settings_arn, portalArn=portal_arn)
         )
+
+    def create_user_settings(self) -> str:
+        params = self._get_params()
+        additional_encryption_context = params.get("additionalEncryptionContext")
+        client_token = params.get("clientToken")
+        cookie_synchronization_configuration = params.get(
+            "cookieSynchronizationConfiguration"
+        )
+        copy_allowed = params.get("copyAllowed")
+        customer_managed_key = params.get("customerManagedKey")
+        deep_link_allowed = params.get("deepLinkAllowed")
+        disconnect_timeout_in_minutes = params.get("disconnectTimeoutInMinutes")
+        download_allowed = params.get("downloadAllowed")
+        idle_disconnect_timeout_in_minutes = params.get(
+            "idleDisconnectTimeoutInMinutes"
+        )
+        paste_allowed = params.get("pasteAllowed")
+        print_allowed = params.get("printAllowed")
+        tags = params.get("tags")
+        upload_allowed = params.get("uploadAllowed")
+        user_settings_arn = self.workspacesweb_backend.create_user_settings(
+            additional_encryption_context=additional_encryption_context,
+            client_token=client_token,
+            cookie_synchronization_configuration=cookie_synchronization_configuration,
+            copy_allowed=copy_allowed,
+            customer_managed_key=customer_managed_key,
+            deep_link_allowed=deep_link_allowed,
+            disconnect_timeout_in_minutes=disconnect_timeout_in_minutes,
+            download_allowed=download_allowed,
+            idle_disconnect_timeout_in_minutes=idle_disconnect_timeout_in_minutes,
+            paste_allowed=paste_allowed,
+            print_allowed=print_allowed,
+            tags=tags,
+            upload_allowed=upload_allowed,
+        )
+        return json.dumps(dict(userSettingsArn=user_settings_arn))
+
+    def get_user_settings(self) -> TYPE_RESPONSE:
+        user_settings_arn = unquote(self.parsed_url.path.split("/userSettings/")[-1])
+        user_settings = self.workspacesweb_backend.get_user_settings(
+            user_settings_arn=user_settings_arn,
+        )
+        return 200, {}, json.dumps(dict(userSettings=user_settings))
+
+    def delete_user_settings(self) -> TYPE_RESPONSE:
+        user_settings_arn = unquote(self.parsed_url.path.split("/userSettings/")[-1])
+        self.workspacesweb_backend.delete_user_settings(
+            user_settings_arn=user_settings_arn,
+        )
+        return 200, {}, "{}"
+
+    def create_user_access_logging_settings(self) -> str:
+        params = self._get_params()
+        params = json.loads(list(params.keys())[0])
+        client_token = params.get("clientToken")
+        kinesis_stream_arn = params.get("kinesisStreamArn")
+        tags = params.get("tags")
+        user_access_logging_settings_arn = (
+            self.workspacesweb_backend.create_user_access_logging_settings(
+                client_token=client_token,
+                kinesis_stream_arn=kinesis_stream_arn,
+                tags=tags,
+            )
+        )
+        return json.dumps(
+            dict(userAccessLoggingSettingsArn=user_access_logging_settings_arn)
+        )
+
+    def get_user_access_logging_settings(self) -> TYPE_RESPONSE:
+        user_access_logging_settings_arn = unquote(
+            self.parsed_url.path.split("/userAccessLoggingSettings/")[-1]
+        )
+        user_access_logging_settings = (
+            self.workspacesweb_backend.get_user_access_logging_settings(
+                user_access_logging_settings_arn=user_access_logging_settings_arn,
+            )
+        )
+        return (
+            200,
+            {},
+            json.dumps(dict(userAccessLoggingSettings=user_access_logging_settings)),
+        )
+
+    def delete_user_access_logging_settings(self) -> TYPE_RESPONSE:
+        user_access_logging_settings_arn = unquote(
+            self.parsed_url.path.split("/userAccessLoggingSettings/")[-1]
+        )
+        self.workspacesweb_backend.delete_user_access_logging_settings(
+            user_access_logging_settings_arn=user_access_logging_settings_arn,
+        )
+        return 200, {}, "{}"
+
+    def associate_user_settings(self) -> str:
+        user_settings_arn = unquote(self._get_param("userSettingsArn"))
+        portal_arn = unquote(
+            self.parsed_url.path.split("/portals/")[-1].split("/userSettings")[0]
+        )
+        user_settings_arn, portal_arn = (
+            self.workspacesweb_backend.associate_user_settings(
+                user_settings_arn=user_settings_arn,
+                portal_arn=portal_arn,
+            )
+        )
+        return json.dumps(dict(userSettingsArn=user_settings_arn, portalArn=portal_arn))
+
+    def associate_user_access_logging_settings(self) -> str:
+        user_access_logging_settings_arn = unquote(
+            self._get_param("userAccessLoggingSettingsArn")
+        )
+        portal_arn = unquote(
+            self.parsed_url.path.split("/portals/")[-1].split(
+                "/userAccessLoggingSettings"
+            )[0]
+        )
+        user_access_logging_settings_arn, portal_arn = (
+            self.workspacesweb_backend.associate_user_access_logging_settings(
+                user_access_logging_settings_arn=user_access_logging_settings_arn,
+                portal_arn=portal_arn,
+            )
+        )
+        return json.dumps(
+            dict(
+                userAccessLoggingSettingsArn=user_access_logging_settings_arn,
+                portalArn=portal_arn,
+            )
+        )
+
+    def list_user_settings(self) -> str:
+        user_settings = self.workspacesweb_backend.list_user_settings()
+        return json.dumps(dict(userSettings=user_settings))
+
+    def list_user_access_logging_settings(self) -> str:
+        user_access_logging_settings = (
+            self.workspacesweb_backend.list_user_access_logging_settings()
+        )
+        return json.dumps(dict(userAccessLoggingSettings=user_access_logging_settings))
diff --git a/moto/workspacesweb/urls.py b/moto/workspacesweb/urls.py
index 6fe82a99e..8c2fb156a 100644
--- a/moto/workspacesweb/urls.py
+++ b/moto/workspacesweb/urls.py
@@ -9,10 +9,16 @@ url_bases = [
 url_paths = {
     "{0}/browserSettings$": WorkSpacesWebResponse.dispatch,
     "{0}/networkSettings$": WorkSpacesWebResponse.dispatch,
+    "{0}/userSettings$": WorkSpacesWebResponse.dispatch,
+    "{0}/userAccessLoggingSettings$": WorkSpacesWebResponse.dispatch,
     "{0}/portals$": WorkSpacesWebResponse.dispatch,
+    "{0}/portals/(?P<portalArn>[^/]+)portal/(?P<uuid>[^/]+)$": WorkSpacesWebResponse.portal,
     "{0}/browserSettings/(?P<browserSettingsArn>.+)$": WorkSpacesWebResponse.browser_settings,
     "{0}/networkSettings/(?P<networkSettingsArn>.+)$": WorkSpacesWebResponse.network_settings,
-    "{0}/portals/(?P<portalArn>[^/]+)portal/(?P<uuid>[^/]+)$": WorkSpacesWebResponse.portal,
+    "{0}/userSettings/(?P<userSettingsArn>.+)$": WorkSpacesWebResponse.user_settings,
+    "{0}/userAccessLoggingSettings/(?P<userAccessLoggingSettingsArn>.+)$": WorkSpacesWebResponse.user_access_logging_settings,
     "{0}/portals/(?P<portalArn>.*)/browserSettings$": WorkSpacesWebResponse.dispatch,
     "{0}/portals/(?P<portalArn>.*)/networkSettings$": WorkSpacesWebResponse.dispatch,
+    "{0}/portals/(?P<portalArn>.*)/userSettings$": WorkSpacesWebResponse.dispatch,
+    "{0}/portals/(?P<portalArn>.*)/userAccessLoggingSettings$": WorkSpacesWebResponse.dispatch,
 }


Test patch:
diff --git a/tests/test_events/test_events_integration.py b/tests/test_events/test_events_integration.py
index 074c5b76b..7e03c74e3 100644
--- a/tests/test_events/test_events_integration.py
+++ b/tests/test_events/test_events_integration.py
@@ -1,9 +1,11 @@
+import contextlib
 import json
 import os
 from datetime import datetime
 from unittest import SkipTest, mock
 
 import boto3
+import pytest
 
 from moto import mock_aws, settings
 from moto.core import DEFAULT_ACCOUNT_ID as ACCOUNT_ID
@@ -110,16 +112,23 @@ def test_send_to_sqs_fifo_queue():
 
     # when
     event_time = datetime(2021, 1, 1, 12, 23, 34)
-    client_events.put_events(
-        Entries=[
-            {
-                "Time": event_time,
-                "Source": "source",
-                "DetailType": "type",
-                "Detail": json.dumps({"key": "value"}),
-            }
-        ]
+    # Catch and validate a UserWarning when running locally.
+    context = (
+        pytest.warns(UserWarning, match="you must enable content-based deduplication")
+        if not settings.TEST_SERVER_MODE
+        else contextlib.nullcontext()
     )
+    with context:
+        client_events.put_events(
+            Entries=[
+                {
+                    "Time": event_time,
+                    "Source": "source",
+                    "DetailType": "type",
+                    "Detail": json.dumps({"key": "value"}),
+                }
+            ]
+        )
 
     # then
     response = client_sqs.receive_message(
diff --git a/tests/test_networkmanager/test_networkmanager.py b/tests/test_networkmanager/test_networkmanager.py
index 0a35d08c5..09bfc2663 100644
--- a/tests/test_networkmanager/test_networkmanager.py
+++ b/tests/test_networkmanager/test_networkmanager.py
@@ -36,6 +36,10 @@ def test_create_global_network():
     assert global_network["Tags"] == [{"Key": "Name", "Value": "TestNetwork"}]
     assert global_network["State"] == "PENDING"
 
+    global_network = client.describe_global_networks()["GlobalNetworks"][0]
+    assert global_network["GlobalNetworkId"] == resp["GlobalNetwork"]["GlobalNetworkId"]
+    assert global_network["State"] == "AVAILABLE"
+
 
 @mock_aws
 def test_create_core_network():
diff --git a/tests/test_route53/test_route53.py b/tests/test_route53/test_route53.py
index cc34720a6..309333f3f 100644
--- a/tests/test_route53/test_route53.py
+++ b/tests/test_route53/test_route53.py
@@ -553,6 +553,7 @@ def test_list_or_change_tags_for_resource_request():
 @mock_aws
 def test_list_tags_for_resources():
     conn = boto3.client("route53", region_name="us-east-1")
+    # Create two hosted zones
     zone1 = conn.create_hosted_zone(
         Name="testdns1.aws.com", CallerReference=str(hash("foo"))
     )
@@ -562,11 +563,48 @@ def test_list_tags_for_resources():
     )
     zone2_id = zone2["HostedZone"]["Id"]
 
-    # confirm this works for resources with zero tags
+    # Create two healthchecks
+    health_check1 = conn.create_health_check(
+        CallerReference="foo",
+        HealthCheckConfig={
+            "IPAddress": "192.0.2.44",
+            "Port": 123,
+            "Type": "HTTP",
+            "ResourcePath": "/",
+            "RequestInterval": 30,
+            "FailureThreshold": 123,
+            "HealthThreshold": 123,
+        },
+    )
+    healthcheck1_id = health_check1["HealthCheck"]["Id"]
+
+    health_check2 = conn.create_health_check(
+        CallerReference="bar",
+        HealthCheckConfig={
+            "IPAddress": "192.0.2.44",
+            "Port": 123,
+            "Type": "HTTP",
+            "ResourcePath": "/",
+            "RequestInterval": 30,
+            "FailureThreshold": 123,
+            "HealthThreshold": 123,
+        },
+    )
+    healthcheck2_id = health_check2["HealthCheck"]["Id"]
+
+    # confirm this works for resources with zero tags for hostedzone
     response = conn.list_tags_for_resources(
         ResourceIds=[zone1_id, zone2_id], ResourceType="hostedzone"
     )
 
+    for set in response["ResourceTagSets"]:
+        assert set["Tags"] == []
+
+    # confirm this works for resources with zero tags for healthchecks
+    response = conn.list_tags_for_resources(
+        ResourceIds=[healthcheck1_id, healthcheck2_id], ResourceType="healthcheck"
+    )
+
     for set in response["ResourceTagSets"]:
         assert set["Tags"] == []
 
@@ -582,14 +620,46 @@ def test_list_tags_for_resources():
         ResourceType="hostedzone", ResourceId=zone2_id, AddTags=[tag3, tag4]
     )
 
+    conn.change_tags_for_resource(
+        ResourceType="healthcheck", ResourceId=healthcheck1_id, AddTags=[tag1, tag2]
+    )
+    conn.change_tags_for_resource(
+        ResourceType="healthcheck", ResourceId=healthcheck2_id, AddTags=[tag3, tag4]
+    )
+
+    # Test hostedzone
     response = conn.list_tags_for_resources(
         ResourceIds=[zone1_id, zone2_id], ResourceType="hostedzone"
     )
+    assert len(response["ResourceTagSets"]) == 2
     for set in response["ResourceTagSets"]:
+        assert set["ResourceId"] in (zone1_id, zone2_id)
         if set["ResourceId"] == zone1_id:
             assert tag1 in set["Tags"]
             assert tag2 in set["Tags"]
+            assert tag3 not in set["Tags"]
+            assert tag4 not in set["Tags"]
         elif set["ResourceId"] == zone2_id:
+            assert tag1 not in set["Tags"]
+            assert tag2 not in set["Tags"]
+            assert tag3 in set["Tags"]
+            assert tag4 in set["Tags"]
+
+    # Test healthcheck
+    response = conn.list_tags_for_resources(
+        ResourceIds=[healthcheck1_id, healthcheck2_id], ResourceType="healthcheck"
+    )
+    assert len(response["ResourceTagSets"]) == 2
+    for set in response["ResourceTagSets"]:
+        assert set["ResourceId"] in (healthcheck1_id, healthcheck2_id)
+        if set["ResourceId"] == healthcheck1_id:
+            assert tag1 in set["Tags"]
+            assert tag2 in set["Tags"]
+            assert tag3 not in set["Tags"]
+            assert tag4 not in set["Tags"]
+        elif set["ResourceId"] == healthcheck2_id:
+            assert tag1 not in set["Tags"]
+            assert tag2 not in set["Tags"]
             assert tag3 in set["Tags"]
             assert tag4 in set["Tags"]
 
diff --git a/tests/test_workspacesweb/test_workspacesweb.py b/tests/test_workspacesweb/test_workspacesweb.py
index 854a7599b..e9f89deb1 100644
--- a/tests/test_workspacesweb/test_workspacesweb.py
+++ b/tests/test_workspacesweb/test_workspacesweb.py
@@ -269,3 +269,170 @@ def test_associate_network_settings():
         "networkSettings"
     ]
     assert resp["associatedPortalArns"] == [portal_arn]
+
+
+@mock_aws
+def test_create_user_settings():
+    client = boto3.client("workspaces-web", region_name="eu-west-1")
+    resp = client.create_user_settings(
+        copyAllowed="Disabled",
+        pasteAllowed="Disabled",
+        printAllowed="Disabled",
+        uploadAllowed="Disabled",
+        downloadAllowed="Disabled",
+    )
+    user_settings_arn = resp["userSettingsArn"]
+    arn_regex = r"^arn:aws:workspaces-web:eu-west-1:\d{12}:user-settings/[a-f0-9-]+$"
+    assert re.match(arn_regex, user_settings_arn) is not None
+
+
+@mock_aws
+def test_get_user_settings():
+    client = boto3.client("workspaces-web", region_name="eu-west-1")
+    resp = client.create_user_settings(
+        copyAllowed="Disabled",
+        pasteAllowed="Disabled",
+        printAllowed="Disabled",
+        uploadAllowed="Disabled",
+        downloadAllowed="Disabled",
+    )
+    user_settings_arn = resp["userSettingsArn"]
+    resp = client.get_user_settings(userSettingsArn=user_settings_arn)["userSettings"]
+    assert resp["userSettingsArn"] == user_settings_arn
+    assert resp["copyAllowed"] == "Disabled"
+    assert resp["pasteAllowed"] == "Disabled"
+    assert resp["printAllowed"] == "Disabled"
+    assert resp["uploadAllowed"] == "Disabled"
+    assert resp["downloadAllowed"] == "Disabled"
+
+
+@mock_aws
+def test_list_user_settings():
+    client = boto3.client("workspaces-web", region_name="ap-southeast-1")
+    arn = client.create_user_settings(
+        copyAllowed="Disabled",
+        pasteAllowed="Disabled",
+        printAllowed="Disabled",
+        uploadAllowed="Disabled",
+        downloadAllowed="Disabled",
+    )["userSettingsArn"]
+    resp = client.list_user_settings()
+    assert resp["userSettings"][0]["userSettingsArn"] == arn
+
+
+@mock_aws
+def test_delete_user_settings():
+    client = boto3.client("workspaces-web", region_name="eu-west-1")
+    arn = client.create_user_settings(
+        copyAllowed="Disabled",
+        pasteAllowed="Disabled",
+        printAllowed="Disabled",
+        uploadAllowed="Disabled",
+        downloadAllowed="Disabled",
+    )["userSettingsArn"]
+    client.delete_user_settings(userSettingsArn=arn)
+    assert len(client.list_user_settings()["userSettings"]) == 0
+
+
+@mock_aws
+def test_associate_user_settings():
+    client = boto3.client("workspaces-web", region_name="eu-west-1")
+    user_settings_arn = client.create_user_settings(
+        copyAllowed="Disabled",
+        pasteAllowed="Disabled",
+        printAllowed="Disabled",
+        uploadAllowed="Disabled",
+        downloadAllowed="Disabled",
+    )["userSettingsArn"]
+    portal_arn = client.create_portal(
+        additionalEncryptionContext={"Key1": "Value1", "Key2": "Value2"},
+        authenticationType="Standard",
+        clientToken="TestClient",
+        customerManagedKey=FAKE_KMS_KEY_ID,
+        displayName="TestDisplayName",
+        instanceType="TestInstanceType",
+        maxConcurrentSessions=5,
+        tags=FAKE_TAGS,
+    )["portalArn"]
+    client.associate_user_settings(
+        userSettingsArn=user_settings_arn, portalArn=portal_arn
+    )
+    resp = client.get_portal(portalArn=portal_arn)["portal"]
+    assert resp["userSettingsArn"] == user_settings_arn
+    resp = client.get_user_settings(userSettingsArn=user_settings_arn)["userSettings"]
+    assert resp["associatedPortalArns"] == [portal_arn]
+
+
+@mock_aws
+def test_create_user_access_logging_settings():
+    client = boto3.client("workspaces-web", region_name="ap-southeast-1")
+    user_access_logging_settings_arn = client.create_user_access_logging_settings(
+        kinesisStreamArn="arn:aws:kinesis:ap-southeast-1:123456789012:stream/TestStream",
+    )["userAccessLoggingSettingsArn"]
+    arn_regex = r"^arn:aws:workspaces-web:ap-southeast-1:\d{12}:user-access-logging-settings/[a-f0-9-]+$"
+    assert re.match(arn_regex, user_access_logging_settings_arn) is not None
+
+
+@mock_aws
+def test_get_user_access_logging_settings():
+    client = boto3.client("workspaces-web", region_name="eu-west-1")
+    user_access_logging_settings_arn = client.create_user_access_logging_settings(
+        kinesisStreamArn="arn:aws:kinesis:ap-southeast-1:123456789012:stream/TestStream",
+    )["userAccessLoggingSettingsArn"]
+    resp = client.get_user_access_logging_settings(
+        userAccessLoggingSettingsArn=user_access_logging_settings_arn
+    )["userAccessLoggingSettings"]
+    assert resp["userAccessLoggingSettingsArn"] == user_access_logging_settings_arn
+    assert (
+        resp["kinesisStreamArn"]
+        == "arn:aws:kinesis:ap-southeast-1:123456789012:stream/TestStream"
+    )
+
+
+@mock_aws
+def test_list_user_access_logging_settings():
+    client = boto3.client("workspaces-web", region_name="eu-west-1")
+    arn = client.create_user_access_logging_settings(
+        kinesisStreamArn="arn:aws:kinesis:ap-southeast-1:123456789012:stream/TestStream",
+    )["userAccessLoggingSettingsArn"]
+    resp = client.list_user_access_logging_settings()
+    assert resp["userAccessLoggingSettings"][0]["userAccessLoggingSettingsArn"] == arn
+
+
+@mock_aws
+def test_delete_user_access_logging_settings():
+    client = boto3.client("workspaces-web", region_name="ap-southeast-1")
+    arn = client.create_user_access_logging_settings(
+        kinesisStreamArn="arn:aws:kinesis:ap-southeast-1:123456789012:stream/TestStream",
+    )["userAccessLoggingSettingsArn"]
+    client.delete_user_access_logging_settings(userAccessLoggingSettingsArn=arn)
+    resp = client.list_user_access_logging_settings()
+    assert resp["userAccessLoggingSettings"] == []
+
+
+@mock_aws
+def test_associate_user_access_logging_settings():
+    client = boto3.client("workspaces-web", region_name="eu-west-1")
+    user_access_logging_settings_arn = client.create_user_access_logging_settings(
+        kinesisStreamArn="arn:aws:kinesis:ap-southeast-1:123456789012:stream/TestStream",
+    )["userAccessLoggingSettingsArn"]
+    portal_arn = client.create_portal(
+        additionalEncryptionContext={"Key1": "Value1", "Key2": "Value2"},
+        authenticationType="Standard",
+        clientToken="TestClient",
+        customerManagedKey=FAKE_KMS_KEY_ID,
+        displayName="TestDisplayName",
+        instanceType="TestInstanceType",
+        maxConcurrentSessions=5,
+        tags=FAKE_TAGS,
+    )["portalArn"]
+    client.associate_user_access_logging_settings(
+        userAccessLoggingSettingsArn=user_access_logging_settings_arn,
+        portalArn=portal_arn,
+    )
+    resp = client.get_portal(portalArn=portal_arn)["portal"]
+    assert resp["userAccessLoggingSettingsArn"] == user_access_logging_settings_arn
+    resp = client.get_user_access_logging_settings(
+        userAccessLoggingSettingsArn=user_access_logging_settings_arn
+    )["userAccessLoggingSettings"]
+    assert resp["associatedPortalArns"] == [portal_arn]