Repository: cloud-custodian/cloud-custodian

Issue:
aws - Adding Transfer Family base resources



Discussion:

Resolving patch:
diff --git a/c7n/resources/resource_map.py b/c7n/resources/resource_map.py
--- a/c7n/resources/resource_map.py
+++ b/c7n/resources/resource_map.py
@@ -211,6 +211,8 @@
     "aws.subnet": "c7n.resources.vpc.Subnet",
     "aws.support-case": "c7n.resources.support.SupportCase",
     "aws.swf-domain": "c7n.resources.swf.SimpleWorkflowDomain",
+    "aws.transfer-server": "c7n.resources.transfer.TransferServer",
+    "aws.transfer-user": "c7n.resources.transfer.TransferUser",
     "aws.transit-attachment": "c7n.resources.vpc.TransitGatewayAttachment",
     "aws.transit-gateway": "c7n.resources.vpc.TransitGateway",
     "aws.user-pool": "c7n.resources.cognito.CognitoUserPool",
diff --git a/c7n/resources/transfer.py b/c7n/resources/transfer.py
new file mode 100644
--- /dev/null
+++ b/c7n/resources/transfer.py
@@ -0,0 +1,227 @@
+# Copyright The Cloud Custodian Authors.
+# SPDX-License-Identifier: Apache-2.0
+from c7n.actions import BaseAction
+from c7n.manager import resources
+from concurrent.futures import as_completed
+from c7n.query import QueryResourceManager, ChildResourceManager, TypeInfo, ChildDescribeSource
+from c7n.utils import local_session, type_schema
+
+
+@resources.register('transfer-server')
+class TransferServer(QueryResourceManager):
+
+    class resource_type(TypeInfo):
+        service = 'transfer'
+        enum_spec = ('list_servers', 'Servers', {'MaxResults': 60})
+        detail_spec = (
+            'describe_server', 'ServerId', 'ServerId', None)
+        id = name = 'ServerId'
+        arn_type = "server"
+        cfn_type = 'AWS::Transfer::Server'
+
+
+@TransferServer.action_registry.register('stop')
+class StopServer(BaseAction):
+    """Action to stop a Transfer Server
+
+    :example:
+
+    .. code-block:: yaml
+
+            policies:
+              - name: transfer-server-stop
+                resource: transfer-server
+                actions:
+                  - stop
+    """
+    valid_status = ('ONLINE', 'STARTING', 'STOP_FAILED',)
+    schema = type_schema('stop')
+    permissions = ("transfer:StopServer",)
+
+    def process(self, resources):
+        resources = self.filter_resources(
+            resources, 'State', self.valid_status)
+        if not len(resources):
+            return
+
+        client = local_session(
+            self.manager.session_factory).client('transfer')
+        with self.executor_factory(
+                max_workers=min(3, len(resources) or 1)) as w:
+            futures = {}
+            for r in resources:
+                futures[w.submit(self.process_server, client, r)] = r
+            for f in as_completed(futures):
+                r = futures[f]
+                if f.exception():
+                    self.log.warning(
+                        "Exception stoping transfer server:%s error:\n%s",
+                        r['ServerId'], f.exception())
+                    continue
+
+    def process_server(self, client, server):
+        client.stop_server(ServerId=server['ServerId'])
+
+
+@TransferServer.action_registry.register('start')
+class StartServer(BaseAction):
+    """Action to start a Transfer Server
+
+    :example:
+
+    .. code-block:: yaml
+
+            policies:
+              - name: transfer-server-start
+                resource: transfer-server
+                actions:
+                  - start
+    """
+    valid_status = ('OFFLINE', 'STOPPING', 'START_FAILED', 'STOP_FAILED',)
+    schema = type_schema('start')
+    permissions = ("transfer:StartServer",)
+
+    def process(self, resources):
+        resources = self.filter_resources(
+            resources, 'State', self.valid_status)
+        if not len(resources):
+            return
+
+        client = local_session(
+            self.manager.session_factory).client('transfer')
+        with self.executor_factory(
+                max_workers=min(3, len(resources) or 1)) as w:
+            futures = {}
+            for r in resources:
+                futures[w.submit(self.process_server, client, r)] = r
+            for f in as_completed(futures):
+                r = futures[f]
+                if f.exception():
+                    self.log.warning(
+                        "Exception starting transfer server:%s error:\n%s",
+                        r['ServerId'], f.exception())
+                    continue
+
+    def process_server(self, client, server):
+        client.start_server(ServerId=server['ServerId'])
+
+
+@TransferServer.action_registry.register('delete')
+class DeleteServer(BaseAction):
+    """Action to delete a Transfer Server
+
+    :example:
+
+    .. code-block:: yaml
+
+            policies:
+              - name: transfer-server-delete
+                resource: transfer-server
+                actions:
+                  - delete
+    """
+    schema = type_schema('delete')
+    permissions = ("transfer:DeleteServer",)
+
+    def process(self, resources):
+        client = local_session(
+            self.manager.session_factory).client('transfer')
+        with self.executor_factory(
+                max_workers=min(3, len(resources) or 1)) as w:
+            futures = {}
+            for r in resources:
+                futures[w.submit(self.process_server, client, r)] = r
+            for f in as_completed(futures):
+                r = futures[f]
+                if f.exception():
+                    self.log.warning(
+                        "Exception deleting transfer server:%s error:\n%s",
+                        r['ServerId'], f.exception())
+                    continue
+
+    def process_server(self, client, server):
+        try:
+            client.delete_server(ServerId=server['ServerId'])
+        except client.exceptions.NotFoundException:
+            pass
+
+
+class DescribeTransferUser(ChildDescribeSource):
+
+    def get_query(self):
+        query = super().get_query()
+        query.capture_parent_id = True
+        return query
+
+    def augment(self, resources):
+        client = local_session(self.manager.session_factory).client('transfer')
+        results = []
+        for parent_id, user in resources:
+            tu = self.manager.retry(
+                client.describe_user, ServerId=parent_id,
+                UserName=user['UserName']).get('User')
+            results.append(tu)
+        return results
+
+
+@resources.register('transfer-user')
+class TransferUser(ChildResourceManager):
+
+    class resource_type(TypeInfo):
+        service = 'transfer'
+        arn = 'Arn'
+        arn_type = 'user'
+        enum_spec = ('list_users', 'Users', None)
+        detail_spec = ('describe_user', 'UserName', None)
+        parent_spec = ('transfer-server', 'ServerId', True)
+        name = id = 'UserName'
+        cfn_type = 'AWS::Transfer::User'
+
+    source_mapping = {
+        'describe-child': DescribeTransferUser
+    }
+
+    def get_resources(self, ids, cache=True, augment=True):
+        return super(TransferUser, self).get_resources(ids, cache, augment=False)
+
+
+@TransferUser.action_registry.register('delete')
+class DeleteUser(BaseAction):
+    """Action to delete a Transfer User
+
+    :example:
+
+    .. code-block:: yaml
+
+            policies:
+              - name: transfer-user-delete
+                resource: transfer-user
+                actions:
+                  - delete
+    """
+    schema = type_schema('delete')
+    permissions = ("transfer:DeleteUser",)
+
+    def process(self, resources):
+        client = local_session(
+            self.manager.session_factory).client('transfer')
+        with self.executor_factory(
+                max_workers=min(3, len(resources) or 1)) as w:
+            futures = {}
+            for r in resources:
+                futures[w.submit(self.process_user, client, r)] = r
+            for f in as_completed(futures):
+                r = futures[f]
+                if f.exception():
+                    self.log.warning(
+                        "Exception deleting transfer user:%s error:\n%s",
+                        r['UserName'], f.exception())
+                    continue
+
+    def process_user(self, client, user):
+        try:
+            client.delete_user(
+                ServerId=user['Arn'].split('/')[1],
+                UserName=user['UserName'])
+        except client.exceptions.NotFoundException:
+            pass


Test patch:
diff --git a/tests/data/placebo/test_transfer_server/transfer.DescribeServer_1.json b/tests/data/placebo/test_transfer_server/transfer.DescribeServer_1.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_transfer_server/transfer.DescribeServer_1.json
@@ -0,0 +1,25 @@
+{
+    "status_code": 200,
+    "data": {
+        "Server": {
+            "Arn": "arn:aws:transfer:us-east-1:644160558196:server/s-4a6d521483294bd79",
+            "ProtocolDetails": {
+                "PassiveIp": "AUTO"
+            },
+            "Domain": "S3",
+            "EndpointType": "PUBLIC",
+            "HostKeyFingerprint": "SHA256:Nc+Gi84M3QEPVTGvI28TVsJ2g6MEHuQIh4+H9aPTk1w=",
+            "IdentityProviderType": "SERVICE_MANAGED",
+            "LoggingRole": "arn:aws:iam::644160558196:role/service-role/AWSTransferLoggingAccess",
+            "Protocols": [
+                "SFTP"
+            ],
+            "SecurityPolicyName": "TransferSecurityPolicy-2020-06",
+            "ServerId": "s-4a6d521483294bd79",
+            "State": "ONLINE",
+            "Tags": [],
+            "UserCount": 0
+        },
+        "ResponseMetadata": {}
+    }
+}
\ No newline at end of file
diff --git a/tests/data/placebo/test_transfer_server/transfer.ListServers_1.json b/tests/data/placebo/test_transfer_server/transfer.ListServers_1.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_transfer_server/transfer.ListServers_1.json
@@ -0,0 +1,18 @@
+{
+    "status_code": 200,
+    "data": {
+        "Servers": [
+            {
+                "Arn": "arn:aws:transfer:us-east-1:644160558196:server/s-4a6d521483294bd79",
+                "Domain": "S3",
+                "IdentityProviderType": "SERVICE_MANAGED",
+                "EndpointType": "PUBLIC",
+                "LoggingRole": "arn:aws:iam::644160558196:role/service-role/AWSTransferLoggingAccess",
+                "ServerId": "s-4a6d521483294bd79",
+                "State": "ONLINE",
+                "UserCount": 0
+            }
+        ],
+        "ResponseMetadata": {}
+    }
+}
\ No newline at end of file
diff --git a/tests/data/placebo/test_transfer_server_delete/transfer.DeleteServer_1.json b/tests/data/placebo/test_transfer_server_delete/transfer.DeleteServer_1.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_transfer_server_delete/transfer.DeleteServer_1.json
@@ -0,0 +1,6 @@
+{
+    "status_code": 200,
+    "data": {
+        "ResponseMetadata": {}
+    }
+}
\ No newline at end of file
diff --git a/tests/data/placebo/test_transfer_server_delete/transfer.DescribeServer_1.json b/tests/data/placebo/test_transfer_server_delete/transfer.DescribeServer_1.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_transfer_server_delete/transfer.DescribeServer_1.json
@@ -0,0 +1,25 @@
+{
+    "status_code": 200,
+    "data": {
+        "Server": {
+            "Arn": "arn:aws:transfer:us-east-1:644160558196:server/s-4a6d521483294bd79",
+            "ProtocolDetails": {
+                "PassiveIp": "AUTO"
+            },
+            "Domain": "S3",
+            "EndpointType": "PUBLIC",
+            "HostKeyFingerprint": "SHA256:Nc+Gi84M3QEPVTGvI28TVsJ2g6MEHuQIh4+H9aPTk1w=",
+            "IdentityProviderType": "SERVICE_MANAGED",
+            "LoggingRole": "arn:aws:iam::644160558196:role/service-role/AWSTransferLoggingAccess",
+            "Protocols": [
+                "SFTP"
+            ],
+            "SecurityPolicyName": "TransferSecurityPolicy-2020-06",
+            "ServerId": "s-4a6d521483294bd79",
+            "State": "STARTING",
+            "Tags": [],
+            "UserCount": 0
+        },
+        "ResponseMetadata": {}
+    }
+}
\ No newline at end of file
diff --git a/tests/data/placebo/test_transfer_server_delete/transfer.ListServers_1.json b/tests/data/placebo/test_transfer_server_delete/transfer.ListServers_1.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_transfer_server_delete/transfer.ListServers_1.json
@@ -0,0 +1,18 @@
+{
+    "status_code": 200,
+    "data": {
+        "Servers": [
+            {
+                "Arn": "arn:aws:transfer:us-east-1:644160558196:server/s-4a6d521483294bd79",
+                "Domain": "S3",
+                "IdentityProviderType": "SERVICE_MANAGED",
+                "EndpointType": "PUBLIC",
+                "LoggingRole": "arn:aws:iam::644160558196:role/service-role/AWSTransferLoggingAccess",
+                "ServerId": "s-4a6d521483294bd79",
+                "State": "STARTING",
+                "UserCount": 0
+            }
+        ],
+        "ResponseMetadata": {}
+    }
+}
\ No newline at end of file
diff --git a/tests/data/placebo/test_transfer_server_start/transfer.DescribeServer_1.json b/tests/data/placebo/test_transfer_server_start/transfer.DescribeServer_1.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_transfer_server_start/transfer.DescribeServer_1.json
@@ -0,0 +1,25 @@
+{
+    "status_code": 200,
+    "data": {
+        "Server": {
+            "Arn": "arn:aws:transfer:us-east-1:644160558196:server/s-4a6d521483294bd79",
+            "ProtocolDetails": {
+                "PassiveIp": "AUTO"
+            },
+            "Domain": "S3",
+            "EndpointType": "PUBLIC",
+            "HostKeyFingerprint": "SHA256:Nc+Gi84M3QEPVTGvI28TVsJ2g6MEHuQIh4+H9aPTk1w=",
+            "IdentityProviderType": "SERVICE_MANAGED",
+            "LoggingRole": "arn:aws:iam::644160558196:role/service-role/AWSTransferLoggingAccess",
+            "Protocols": [
+                "SFTP"
+            ],
+            "SecurityPolicyName": "TransferSecurityPolicy-2020-06",
+            "ServerId": "s-4a6d521483294bd79",
+            "State": "OFFLINE",
+            "Tags": [],
+            "UserCount": 0
+        },
+        "ResponseMetadata": {}
+    }
+}
\ No newline at end of file
diff --git a/tests/data/placebo/test_transfer_server_start/transfer.ListServers_1.json b/tests/data/placebo/test_transfer_server_start/transfer.ListServers_1.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_transfer_server_start/transfer.ListServers_1.json
@@ -0,0 +1,18 @@
+{
+    "status_code": 200,
+    "data": {
+        "Servers": [
+            {
+                "Arn": "arn:aws:transfer:us-east-1:644160558196:server/s-4a6d521483294bd79",
+                "Domain": "S3",
+                "IdentityProviderType": "SERVICE_MANAGED",
+                "EndpointType": "PUBLIC",
+                "LoggingRole": "arn:aws:iam::644160558196:role/service-role/AWSTransferLoggingAccess",
+                "ServerId": "s-4a6d521483294bd79",
+                "State": "OFFLINE",
+                "UserCount": 0
+            }
+        ],
+        "ResponseMetadata": {}
+    }
+}
\ No newline at end of file
diff --git a/tests/data/placebo/test_transfer_server_start/transfer.StartServer_1.json b/tests/data/placebo/test_transfer_server_start/transfer.StartServer_1.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_transfer_server_start/transfer.StartServer_1.json
@@ -0,0 +1,6 @@
+{
+    "status_code": 200,
+    "data": {
+        "ResponseMetadata": {}
+    }
+}
\ No newline at end of file
diff --git a/tests/data/placebo/test_transfer_server_stop/transfer.DescribeServer_1.json b/tests/data/placebo/test_transfer_server_stop/transfer.DescribeServer_1.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_transfer_server_stop/transfer.DescribeServer_1.json
@@ -0,0 +1,25 @@
+{
+    "status_code": 200,
+    "data": {
+        "Server": {
+            "Arn": "arn:aws:transfer:us-east-1:644160558196:server/s-4a6d521483294bd79",
+            "ProtocolDetails": {
+                "PassiveIp": "AUTO"
+            },
+            "Domain": "S3",
+            "EndpointType": "PUBLIC",
+            "HostKeyFingerprint": "SHA256:Nc+Gi84M3QEPVTGvI28TVsJ2g6MEHuQIh4+H9aPTk1w=",
+            "IdentityProviderType": "SERVICE_MANAGED",
+            "LoggingRole": "arn:aws:iam::644160558196:role/service-role/AWSTransferLoggingAccess",
+            "Protocols": [
+                "SFTP"
+            ],
+            "SecurityPolicyName": "TransferSecurityPolicy-2020-06",
+            "ServerId": "s-4a6d521483294bd79",
+            "State": "ONLINE",
+            "Tags": [],
+            "UserCount": 0
+        },
+        "ResponseMetadata": {}
+    }
+}
\ No newline at end of file
diff --git a/tests/data/placebo/test_transfer_server_stop/transfer.ListServers_1.json b/tests/data/placebo/test_transfer_server_stop/transfer.ListServers_1.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_transfer_server_stop/transfer.ListServers_1.json
@@ -0,0 +1,18 @@
+{
+    "status_code": 200,
+    "data": {
+        "Servers": [
+            {
+                "Arn": "arn:aws:transfer:us-east-1:644160558196:server/s-4a6d521483294bd79",
+                "Domain": "S3",
+                "IdentityProviderType": "SERVICE_MANAGED",
+                "EndpointType": "PUBLIC",
+                "LoggingRole": "arn:aws:iam::644160558196:role/service-role/AWSTransferLoggingAccess",
+                "ServerId": "s-4a6d521483294bd79",
+                "State": "ONLINE",
+                "UserCount": 0
+            }
+        ],
+        "ResponseMetadata": {}
+    }
+}
\ No newline at end of file
diff --git a/tests/data/placebo/test_transfer_server_stop/transfer.StopServer_1.json b/tests/data/placebo/test_transfer_server_stop/transfer.StopServer_1.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_transfer_server_stop/transfer.StopServer_1.json
@@ -0,0 +1,6 @@
+{
+    "status_code": 200,
+    "data": {
+        "ResponseMetadata": {}
+    }
+}
\ No newline at end of file
diff --git a/tests/data/placebo/test_transfer_user/transfer.DescribeUser_1.json b/tests/data/placebo/test_transfer_user/transfer.DescribeUser_1.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_transfer_user/transfer.DescribeUser_1.json
@@ -0,0 +1,15 @@
+{
+    "status_code": 200,
+    "data": {
+        "ServerId": "s-04070bd555b846c98",
+        "User": {
+            "Arn": "arn:aws:transfer:us-east-1:644160558196:user/s-04070bd555b846c98/test",
+            "HomeDirectoryType": "PATH",
+            "Role": "arn:aws:iam::644160558196:role/role",
+            "SshPublicKeys": [],
+            "Tags": [],
+            "UserName": "test"
+        },
+        "ResponseMetadata": {}
+    }
+}
\ No newline at end of file
diff --git a/tests/data/placebo/test_transfer_user/transfer.ListServers_1.json b/tests/data/placebo/test_transfer_user/transfer.ListServers_1.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_transfer_user/transfer.ListServers_1.json
@@ -0,0 +1,18 @@
+{
+    "status_code": 200,
+    "data": {
+        "Servers": [
+            {
+                "Arn": "arn:aws:transfer:us-east-1:644160558196:server/s-04070bd555b846c98",
+                "Domain": "S3",
+                "IdentityProviderType": "SERVICE_MANAGED",
+                "EndpointType": "PUBLIC",
+                "LoggingRole": "arn:aws:iam::644160558196:role/role",
+                "ServerId": "s-04070bd555b846c98",
+                "State": "ONLINE",
+                "UserCount": 1
+            }
+        ],
+        "ResponseMetadata": {}
+    }
+}
\ No newline at end of file
diff --git a/tests/data/placebo/test_transfer_user/transfer.ListUsers_1.json b/tests/data/placebo/test_transfer_user/transfer.ListUsers_1.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_transfer_user/transfer.ListUsers_1.json
@@ -0,0 +1,16 @@
+{
+    "status_code": 200,
+    "data": {
+        "ServerId": "s-04070bd555b846c98",
+        "Users": [
+            {
+                "Arn": "arn:aws:transfer:us-east-1:644160558196:user/s-04070bd555b846c98/test",
+                "HomeDirectoryType": "PATH",
+                "Role": "arn:aws:iam::644160558196:role/role",
+                "SshPublicKeyCount": 0,
+                "UserName": "test"
+            }
+        ],
+        "ResponseMetadata": {}
+    }
+}
\ No newline at end of file
diff --git a/tests/data/placebo/test_transfer_user_delete/transfer.DeleteUser_1.json b/tests/data/placebo/test_transfer_user_delete/transfer.DeleteUser_1.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_transfer_user_delete/transfer.DeleteUser_1.json
@@ -0,0 +1,6 @@
+{
+    "status_code": 200,
+    "data": {
+        "ResponseMetadata": {}
+    }
+}
\ No newline at end of file
diff --git a/tests/data/placebo/test_transfer_user_delete/transfer.DescribeUser_1.json b/tests/data/placebo/test_transfer_user_delete/transfer.DescribeUser_1.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_transfer_user_delete/transfer.DescribeUser_1.json
@@ -0,0 +1,15 @@
+{
+    "status_code": 200,
+    "data": {
+        "ServerId": "s-04070bd555b846c98",
+        "User": {
+            "Arn": "arn:aws:transfer:us-east-1:644160558196:user/s-04070bd555b846c98/test",
+            "HomeDirectoryType": "PATH",
+            "Role": "arn:aws:iam::644160558196:role/role",
+            "SshPublicKeys": [],
+            "Tags": [],
+            "UserName": "test"
+        },
+        "ResponseMetadata": {}
+    }
+}
\ No newline at end of file
diff --git a/tests/data/placebo/test_transfer_user_delete/transfer.ListServers_1.json b/tests/data/placebo/test_transfer_user_delete/transfer.ListServers_1.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_transfer_user_delete/transfer.ListServers_1.json
@@ -0,0 +1,18 @@
+{
+    "status_code": 200,
+    "data": {
+        "Servers": [
+            {
+                "Arn": "arn:aws:transfer:us-east-1:644160558196:server/s-04070bd555b846c98",
+                "Domain": "S3",
+                "IdentityProviderType": "SERVICE_MANAGED",
+                "EndpointType": "PUBLIC",
+                "LoggingRole": "arn:aws:iam::644160558196:role/role",
+                "ServerId": "s-04070bd555b846c98",
+                "State": "ONLINE",
+                "UserCount": 1
+            }
+        ],
+        "ResponseMetadata": {}
+    }
+}
\ No newline at end of file
diff --git a/tests/data/placebo/test_transfer_user_delete/transfer.ListUsers_1.json b/tests/data/placebo/test_transfer_user_delete/transfer.ListUsers_1.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_transfer_user_delete/transfer.ListUsers_1.json
@@ -0,0 +1,16 @@
+{
+    "status_code": 200,
+    "data": {
+        "ServerId": "s-04070bd555b846c98",
+        "Users": [
+            {
+                "Arn": "arn:aws:transfer:us-east-1:644160558196:user/s-04070bd555b846c98/test",
+                "HomeDirectoryType": "PATH",
+                "Role": "arn:aws:iam::644160558196:role/role",
+                "SshPublicKeyCount": 0,
+                "UserName": "test"
+            }
+        ],
+        "ResponseMetadata": {}
+    }
+}
\ No newline at end of file
diff --git a/tests/test_transfer.py b/tests/test_transfer.py
new file mode 100644
--- /dev/null
+++ b/tests/test_transfer.py
@@ -0,0 +1,82 @@
+# Copyright The Cloud Custodian Authors.
+# SPDX-License-Identifier: Apache-2.0
+from .common import BaseTest
+
+
+class TestTransferServer(BaseTest):
+
+    def test_resources(self):
+        session_factory = self.replay_flight_data("test_transfer_server")
+        p = self.load_policy(
+            {"name": "transfer-server-test-describe", "resource": "transfer-server"},
+            session_factory=session_factory,
+        )
+        resources = p.run()
+        self.assertEqual(len(resources), 1)
+        self.assertEqual(resources[0]["ServerId"], "s-4a6d521483294bd79")
+        self.assertEqual(resources[0]["State"], "ONLINE")
+
+    def test_stop_server(self):
+        session_factory = self.replay_flight_data("test_transfer_server_stop")
+        p = self.load_policy(
+            {
+                "name": "transfer-server-test-stop",
+                "resource": "transfer-server",
+                "actions": [{"type": "stop"}],
+            },
+            session_factory=session_factory,
+        )
+        resources = p.run()
+        self.assertEqual(len(resources), 1)
+
+    def test_start_server(self):
+        session_factory = self.replay_flight_data("test_transfer_server_start")
+        p = self.load_policy(
+            {
+                "name": "transfer-server-test-start",
+                "resource": "transfer-server",
+                "actions": [{"type": "start"}],
+            },
+            session_factory=session_factory,
+        )
+        resources = p.run()
+        self.assertEqual(len(resources), 1)
+
+    def test_delete_server(self):
+        session_factory = self.replay_flight_data("test_transfer_server_delete")
+        p = self.load_policy(
+            {
+                "name": "transfer-server-test-delete",
+                "resource": "transfer-server",
+                "actions": [{"type": "delete"}],
+            },
+            session_factory=session_factory,
+        )
+        resources = p.run()
+        self.assertEqual(len(resources), 1)
+
+
+class TransferUser(BaseTest):
+
+    def test_resources(self):
+        session_factory = self.replay_flight_data("test_transfer_user")
+        p = self.load_policy(
+            {"name": "transfer-user-test-describe", "resource": "transfer-user"},
+            session_factory=session_factory,
+        )
+        resources = p.run()
+        self.assertEqual(len(resources), 1)
+        self.assertEqual(resources[0]["UserName"], "test")
+
+    def test_delete_user(self):
+        session_factory = self.replay_flight_data("test_transfer_user_delete")
+        p = self.load_policy(
+            {
+                "name": "transfer-user-test-delete",
+                "resource": "transfer-user",
+                "actions": [{"type": "delete"}],
+            },
+            session_factory=session_factory,
+        )
+        resources = p.run()
+        self.assertEqual(len(resources), 1)