From 0681ad90461ea90c1c2e5226fc605938994df005 Mon Sep 17 00:00:00 2001 From: Leandro Damascena Date: Thu, 23 Jan 2025 22:23:20 +0000 Subject: [PATCH 1/5] Adding TransferFamilyAuthorizer and TransferFamilyAuthorizerResponse class --- .../utilities/data_classes/__init__.py | 3 + .../data_classes/transfer_family_event.py | 190 ++++++++++++++++++ .../utilities/parser/models/__init__.py | 4 +- .../parser/models/transfer_family.py | 2 +- docs/utilities/data_classes.md | 2 + docs/utilities/parser.md | 2 +- ...ily.json => TransferFamilyAuthorizer.json} | 0 .../test_transfer_family_event.py | 142 +++++++++++++ ...sfer_family.py => test_transfer_family.py} | 14 +- 9 files changed, 348 insertions(+), 11 deletions(-) create mode 100644 aws_lambda_powertools/utilities/data_classes/transfer_family_event.py rename tests/events/{TransferFamily.json => TransferFamilyAuthorizer.json} (100%) create mode 100644 tests/unit/data_classes/required_dependencies/test_transfer_family_event.py rename tests/unit/parser/_pydantic/{test_aws_transfer_family.py => test_transfer_family.py} (66%) diff --git a/aws_lambda_powertools/utilities/data_classes/__init__.py b/aws_lambda_powertools/utilities/data_classes/__init__.py index 9952fadb9ae..8d20de7d192 100644 --- a/aws_lambda_powertools/utilities/data_classes/__init__.py +++ b/aws_lambda_powertools/utilities/data_classes/__init__.py @@ -45,6 +45,7 @@ from .ses_event import SESEvent from .sns_event import SNSEvent from .sqs_event import SQSEvent +from .transfer_family_event import TransferFamilyAuthorizer, TransferFamilyAuthorizerResponse from .vpc_lattice import VPCLatticeEvent, VPCLatticeEventV2 __all__ = [ @@ -87,4 +88,6 @@ "VPCLatticeEvent", "VPCLatticeEventV2", "CloudFormationCustomResourceEvent", + "TransferFamilyAuthorizerResponse", + "TransferFamilyAuthorizer", ] diff --git a/aws_lambda_powertools/utilities/data_classes/transfer_family_event.py b/aws_lambda_powertools/utilities/data_classes/transfer_family_event.py new file mode 100644 index 00000000000..eecc425e527 --- /dev/null +++ b/aws_lambda_powertools/utilities/data_classes/transfer_family_event.py @@ -0,0 +1,190 @@ +from __future__ import annotations + +from typing import Any, Literal + +from aws_lambda_powertools.utilities.data_classes.common import ( + DictWrapper, +) + + +class TransferFamilyAuthorizer(DictWrapper): + @property + def username(self) -> str: + """The username used for authentication""" + return self["username"] + + @property + def password(self) -> str | None: + """ + The password used for authentication. + None in case customer authenticating with certificates + """ + return self["password"] + + @property + def protocol(self) -> str: + """The protocol can be SFTP, FTP or FTPS""" + return self["protocol"] + + @property + def server_id(self) -> str: + """The AWS Transfer Family ServerID""" + return self["serverId"] + + @property + def source_ip(self) -> str: + """The customer IP used for connection""" + return self["sourceIp"] + + +class TransferFamilyAuthorizerResponse: + + def _build_authentication_response( + self, + role_arn: str, + policy: str | None = None, + home_directory: str | None = None, + home_directory_details: dict | None = None, + home_directory_type: Literal["LOGICAL", "PATH"] = "PATH", + user_gid: int | None = None, + user_uid: int | None = None, + public_keys: str | None = None, + ) -> dict[str, Any]: + + response: dict[str, Any] = {} + + if home_directory_type == "PATH": + if not home_directory: + raise ValueError("home_directory must be set when home_directory_type is PATH") + + response["HomeDirectory"] = home_directory + elif home_directory_type == "LOGICAL": + if not home_directory_details: + raise ValueError("home_directory_details must be set when home_directory_type is LOGICAL") + + response["HomeDirectoryDetails"] = [home_directory_details] + + else: + raise ValueError(f"Invalid home_directory_type: {home_directory_type}") + + if user_uid is not None: + response["PosixProfile"] = {"Gid": user_gid, "Uid": user_gid} + + if policy: + response["Policy"] = policy + + if public_keys: + response["PublicKeys"] = public_keys + + response["Role"] = role_arn + response["HomeDirectoryType"] = home_directory_type + + return response + + def build_authentication_response_efs( + self, + role_arn: str, + user_gid: int, + user_uid: int, + policy: str | None = None, + home_directory: str | None = None, + home_directory_details: dict | None = None, + home_directory_type: Literal["LOGICAL", "PATH"] = "PATH", + public_keys: str | None = None, + ) -> dict[str, Any]: + """ + Build an authentication response for AWS Transfer Family using EFS (Elastic File System). + + Parameters: + ----------- + role_arn : str + The Amazon Resource Name (ARN) of the IAM role. + user_gid : int + The group ID of the user. + user_uid : int + The user ID. + policy : str | None, optional + The IAM policy document. Defaults to None. + home_directory : str | None, optional + The home directory path. Required if home_directory_type is "PATH". Defaults to None. + home_directory_details : dict | None, optional + Details of the home directory. Required if home_directory_type is "LOGICAL". Defaults to None. + home_directory_type : Literal["LOGICAL", "PATH"], optional + The type of home directory. Must be either "LOGICAL" or "PATH". Defaults to "PATH". + public_keys : str | None, optional + The public keys associated with the user. Defaults to None. + + Returns: + -------- + dict[str, Any] + A dictionary containing the authentication response with various details such as + role ARN, policy, home directory information, and user details. + + Raises: + ------- + ValueError + If an invalid home_directory_type is provided or if required parameters are missing + for the specified home_directory_type. + """ + + return self._build_authentication_response( + role_arn=role_arn, + policy=policy, + home_directory=home_directory, + home_directory_details=home_directory_details, + home_directory_type=home_directory_type, + public_keys=public_keys, + user_gid=user_gid, + user_uid=user_uid, + ) + + def build_authentication_response_s3( + self, + role_arn: str, + policy: str | None = None, + home_directory: str | None = None, + home_directory_details: dict | None = None, + home_directory_type: Literal["LOGICAL", "PATH"] = "PATH", + public_keys: str | None = None, + ) -> dict[str, Any]: + """ + Build an authentication response for Amazon S3. + + This method constructs an authentication response tailored for S3 access, + likely by calling an internal method with the provided parameters. + + Parameters: + ----------- + role_arn : str + The Amazon Resource Name (ARN) of the IAM role for S3 access. + policy : str | None, optional + The IAM policy document for S3 access. Defaults to None. + home_directory : str | None, optional + The home directory path in S3. Required if home_directory_type is "PATH". Defaults to None. + home_directory_details : dict | None, optional + Details of the home directory in S3. Required if home_directory_type is "LOGICAL". Defaults to None. + home_directory_type : Literal["LOGICAL", "PATH"], optional + The type of home directory in S3. Must be either "LOGICAL" or "PATH". Defaults to "PATH". + public_keys : str | None, optional + The public keys associated with the user for S3 access. Defaults to None. + + Returns: + -------- + dict[str, Any] + A dictionary containing the authentication response with various details such as + role ARN, policy, home directory information, and potentially other S3-specific attributes. + + Raises: + ------- + ValueError + If an invalid home_directory_type is provided or if required parameters are missing + for the specified home_directory_type. + """ + return self._build_authentication_response( + role_arn=role_arn, + policy=policy, + home_directory=home_directory, + home_directory_details=home_directory_details, + home_directory_type=home_directory_type, + public_keys=public_keys, + ) diff --git a/aws_lambda_powertools/utilities/parser/models/__init__.py b/aws_lambda_powertools/utilities/parser/models/__init__.py index 559930af945..8d6be2f40e0 100644 --- a/aws_lambda_powertools/utilities/parser/models/__init__.py +++ b/aws_lambda_powertools/utilities/parser/models/__init__.py @@ -109,7 +109,7 @@ ) from .sns import SnsModel, SnsNotificationModel, SnsRecordModel from .sqs import SqsAttributesModel, SqsModel, SqsMsgAttributeModel, SqsRecordModel -from .transfer_family import TransferFamily +from .transfer_family import TransferFamilyAuthorizer from .vpc_lattice import VpcLatticeModel from .vpc_latticev2 import VpcLatticeV2Model @@ -180,7 +180,7 @@ "SqsAttributesModel", "S3SqsEventNotificationModel", "S3SqsEventNotificationRecordModel", - "TransferFamily", + "TransferFamilyAuthorizer", "APIGatewayProxyEventModel", "APIGatewayEventRequestContext", "APIGatewayEventAuthorizer", diff --git a/aws_lambda_powertools/utilities/parser/models/transfer_family.py b/aws_lambda_powertools/utilities/parser/models/transfer_family.py index 9ae46b0c39c..62cb49479bd 100644 --- a/aws_lambda_powertools/utilities/parser/models/transfer_family.py +++ b/aws_lambda_powertools/utilities/parser/models/transfer_family.py @@ -4,7 +4,7 @@ from pydantic.networks import IPvAnyAddress -class TransferFamily(BaseModel): +class TransferFamilyAuthorizer(BaseModel): username: str password: Optional[str] = None protocol: Literal["SFTP", "FTP", "FTPS"] diff --git a/docs/utilities/data_classes.md b/docs/utilities/data_classes.md index 4cd738fad70..5fc58566eb5 100644 --- a/docs/utilities/data_classes.md +++ b/docs/utilities/data_classes.md @@ -108,6 +108,8 @@ Log Data Event for Troubleshooting | [SES](#ses) | `SESEvent` | | [SNS](#sns) | `SNSEvent` | | [SQS](#sqs) | `SQSEvent` | +| [TransferFamilyAuthorizer] | `TransferFamilyAuthorizer` | +| [TransferFamilyAuthorizerResponse] | `TransferFamilyAuthorizerResponse` | | [VPC Lattice V2](#vpc-lattice-v2) | `VPCLatticeV2Event` | | [VPC Lattice V1](#vpc-lattice-v1) | `VPCLatticeEvent` | diff --git a/docs/utilities/parser.md b/docs/utilities/parser.md index 8ab87a303d7..a9433e123f9 100644 --- a/docs/utilities/parser.md +++ b/docs/utilities/parser.md @@ -132,7 +132,7 @@ The example above uses `SqsModel`. Other built-in models can be found below. | **SesModel** | Lambda Event Source payload for Amazon Simple Email Service | | **SnsModel** | Lambda Event Source payload for Amazon Simple Notification Service | | **SqsModel** | Lambda Event Source payload for Amazon SQS | -| **TransferFamily** | Lambda Event Source payload for AWS Transfer Family custom identity provider | +| **TransferFamilyAuthorizer** | Lambda Event Source payload for AWS Transfer Family Lambda authorizer | | **VpcLatticeModel** | Lambda Event Source payload for Amazon VPC Lattice | | **VpcLatticeV2Model** | Lambda Event Source payload for Amazon VPC Lattice v2 payload | diff --git a/tests/events/TransferFamily.json b/tests/events/TransferFamilyAuthorizer.json similarity index 100% rename from tests/events/TransferFamily.json rename to tests/events/TransferFamilyAuthorizer.json diff --git a/tests/unit/data_classes/required_dependencies/test_transfer_family_event.py b/tests/unit/data_classes/required_dependencies/test_transfer_family_event.py new file mode 100644 index 00000000000..6fa62a0dcdc --- /dev/null +++ b/tests/unit/data_classes/required_dependencies/test_transfer_family_event.py @@ -0,0 +1,142 @@ +import pytest + +from aws_lambda_powertools.utilities.data_classes.transfer_family_event import ( + TransferFamilyAuthorizer, + TransferFamilyAuthorizerResponse, +) +from tests.functional.utils import load_event + + +def test_transfer_family_event(): + raw_event = load_event("transferFamilyAuthorizer.json") + parsed_event = TransferFamilyAuthorizer(raw_event) + + assert parsed_event.username == raw_event["username"] + assert parsed_event.password == raw_event["password"] + assert parsed_event.protocol == raw_event["protocol"] + assert parsed_event.server_id == raw_event["serverId"] + assert parsed_event.source_ip == raw_event["sourceIp"] + + +@pytest.mark.parametrize("home_directory_type", ["LOGICAL", "PATH"]) +def test_build_authentication_response_s3(home_directory_type): + + # GIVEN a Authorizer response + response = TransferFamilyAuthorizerResponse() + + role_arn = "arn:aws:iam::123456789012:role/S3Access" + policy = '{"Version": "2012-10-17", "Statement": [{"Effect": "Allow", "Action": "s3:*", "Resource": "*"}]}' + home_directory = "/bucket/user" if home_directory_type == "PATH" else None + home_directory_details = ( + {"Entry": "/", "Target": "/bucket/${transfer:UserName}"} if home_directory_type == "LOGICAL" else None + ) + public_keys = "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC0g+Z" + + # WHEN building an authentication response for S3 with different home directory types + response = response.build_authentication_response_s3( + role_arn=role_arn, + policy=policy, + home_directory=home_directory, + home_directory_details=home_directory_details, + home_directory_type=home_directory_type, + public_keys=public_keys, + ) + + # THEN the authentication response is correctly built + assert isinstance(response, dict) + assert response.get("Role") == role_arn + assert response.get("Policy") == policy + assert response.get("PublicKeys") == public_keys + + if home_directory_type == "PATH": + assert response.get("HomeDirectory") == home_directory + assert "HomeDirectoryDetails" not in response + else: + assert response.get("HomeDirectoryDetails") == [home_directory_details] + assert "HomeDirectory" not in response + + +@pytest.mark.parametrize("home_directory_type", ["LOGICAL", "PATH"]) +def test_build_authentication_response_efs(home_directory_type): + + # GIVEN a Authorizer response + response = TransferFamilyAuthorizerResponse() + + role_arn = "arn:aws:iam::123456789012:role/S3Access" + home_directory = "/bucket/user" if home_directory_type == "PATH" else None + home_directory_details = ( + {"Entry": "/", "Target": "/bucket/${transfer:UserName}"} if home_directory_type == "LOGICAL" else None + ) + + # WHEN building an authentication response for EFS with different home directory types + response = response.build_authentication_response_efs( + role_arn=role_arn, + home_directory=home_directory, + home_directory_details=home_directory_details, + home_directory_type=home_directory_type, + user_gid=0, + user_uid=0, + ) + + # THEN the authentication response is correctly built + assert isinstance(response, dict) + assert response.get("Role") == role_arn + + if home_directory_type == "PATH": + assert response.get("HomeDirectory") == home_directory + assert "HomeDirectoryDetails" not in response + else: + assert response.get("HomeDirectoryDetails") == [home_directory_details] + assert "HomeDirectory" not in response + + +def test_build_authentication_missing_home_directory(): + + # GIVEN a Authorizer response + response = TransferFamilyAuthorizerResponse() + + # WHEN home_directory_details is empty and type is LOGICAL + role_arn = "arn:aws:iam::123456789012:role/S3Access" + home_directory_details = {} + home_directory_type = "LOGICAL" + + # THEN must raise an exception + with pytest.raises(ValueError): + response = response.build_authentication_response_efs( + role_arn=role_arn, + home_directory_details=home_directory_details, + home_directory_type=home_directory_type, + user_gid=0, + user_uid=0, + ) + + +def test_build_authentication_response_invalid_type(): + # GIVEN a Authorizer response + response = TransferFamilyAuthorizerResponse() + + # WHEN set an invalid home_directory_type + invalid_type = "INVALID" + + # THEN must raise an exception + with pytest.raises(ValueError): + response.build_authentication_response_s3( + role_arn="arn:aws:iam::123456789012:role/S3Access", + home_directory_type=invalid_type, + ) + + +def test_build_authentication_response_missing_required_params(): + # GIVEN a Authorizer response + response = TransferFamilyAuthorizerResponse() + + # WHEN set a PATH without home_directory + home_directory_type = "PATH" + + # THEN must raise an exception + with pytest.raises(ValueError): + response.build_authentication_response_s3( + role_arn="arn:aws:iam::123456789012:role/S3Access", + home_directory_type=home_directory_type, + # Missing required home_directory for PATH type + ) diff --git a/tests/unit/parser/_pydantic/test_aws_transfer_family.py b/tests/unit/parser/_pydantic/test_transfer_family.py similarity index 66% rename from tests/unit/parser/_pydantic/test_aws_transfer_family.py rename to tests/unit/parser/_pydantic/test_transfer_family.py index 7316dacdd8e..54341200b6a 100644 --- a/tests/unit/parser/_pydantic/test_aws_transfer_family.py +++ b/tests/unit/parser/_pydantic/test_transfer_family.py @@ -1,10 +1,10 @@ -from aws_lambda_powertools.utilities.parser.models import TransferFamily +from aws_lambda_powertools.utilities.parser.models import TransferFamilyAuthorizer from tests.functional.utils import load_event -def test_aws_transfer_family_model(): - raw_event = load_event("TransferFamily.json") - parsed_event = TransferFamily(**raw_event) +def test_transfer_family_authorizer_model(): + raw_event = load_event("TransferFamilyAuthorizer.json") + parsed_event = TransferFamilyAuthorizer(**raw_event) assert parsed_event.username == raw_event["username"] assert parsed_event.password == raw_event["password"] @@ -13,10 +13,10 @@ def test_aws_transfer_family_model(): assert str(parsed_event.source_ip) == raw_event["sourceIp"] -def test_aws_transfer_family_model_without_password(): - raw_event = load_event("TransferFamily.json") +def test_transfer_family_authorizer_model_without_password(): + raw_event = load_event("TransferFamilyAuthorizer.json") del raw_event["password"] - parsed_event = TransferFamily(**raw_event) + parsed_event = TransferFamilyAuthorizer(**raw_event) assert parsed_event.username == raw_event["username"] assert parsed_event.password is None From 2f7a9c8dbfb8df98b891d86662c335e3ed316c76 Mon Sep 17 00:00:00 2001 From: Leandro Damascena Date: Fri, 24 Jan 2025 08:37:35 +0000 Subject: [PATCH 2/5] Modify tests --- .../required_dependencies/test_transfer_family_event.py | 2 +- tests/unit/parser/_pydantic/test_transfer_family.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/unit/data_classes/required_dependencies/test_transfer_family_event.py b/tests/unit/data_classes/required_dependencies/test_transfer_family_event.py index 6fa62a0dcdc..9a74c0398f9 100644 --- a/tests/unit/data_classes/required_dependencies/test_transfer_family_event.py +++ b/tests/unit/data_classes/required_dependencies/test_transfer_family_event.py @@ -7,7 +7,7 @@ from tests.functional.utils import load_event -def test_transfer_family_event(): +def test_transfer_family_authorizer_event(): raw_event = load_event("transferFamilyAuthorizer.json") parsed_event = TransferFamilyAuthorizer(raw_event) diff --git a/tests/unit/parser/_pydantic/test_transfer_family.py b/tests/unit/parser/_pydantic/test_transfer_family.py index 54341200b6a..b5e0252ffea 100644 --- a/tests/unit/parser/_pydantic/test_transfer_family.py +++ b/tests/unit/parser/_pydantic/test_transfer_family.py @@ -3,7 +3,7 @@ def test_transfer_family_authorizer_model(): - raw_event = load_event("TransferFamilyAuthorizer.json") + raw_event = load_event("transferFamilyAuthorizer.json") parsed_event = TransferFamilyAuthorizer(**raw_event) assert parsed_event.username == raw_event["username"] @@ -14,7 +14,7 @@ def test_transfer_family_authorizer_model(): def test_transfer_family_authorizer_model_without_password(): - raw_event = load_event("TransferFamilyAuthorizer.json") + raw_event = load_event("transferFamilyAuthorizer.json") del raw_event["password"] parsed_event = TransferFamilyAuthorizer(**raw_event) From 1c37fc567bb76a5a343326e763f6490f4d76c004 Mon Sep 17 00:00:00 2001 From: Leandro Damascena Date: Fri, 24 Jan 2025 08:43:02 +0000 Subject: [PATCH 3/5] Modify tests --- tests/events/TransferFamilyAuthorizer.json | 7 ------- 1 file changed, 7 deletions(-) delete mode 100644 tests/events/TransferFamilyAuthorizer.json diff --git a/tests/events/TransferFamilyAuthorizer.json b/tests/events/TransferFamilyAuthorizer.json deleted file mode 100644 index c329071e8a5..00000000000 --- a/tests/events/TransferFamilyAuthorizer.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "username": "value", - "password": "value", - "protocol": "SFTP", - "serverId": "s-abcd123456", - "sourceIp": "192.168.0.100" -} From 99f484d87de06138d3c91b0596c1048c200a9db3 Mon Sep 17 00:00:00 2001 From: Leandro Damascena Date: Fri, 24 Jan 2025 08:43:40 +0000 Subject: [PATCH 4/5] Modify tests --- tests/events/transferFamilyAuthorizer.json | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 tests/events/transferFamilyAuthorizer.json diff --git a/tests/events/transferFamilyAuthorizer.json b/tests/events/transferFamilyAuthorizer.json new file mode 100644 index 00000000000..867c1f65209 --- /dev/null +++ b/tests/events/transferFamilyAuthorizer.json @@ -0,0 +1,7 @@ +{ + "username": "value", + "password": "value", + "protocol": "SFTP", + "serverId": "s-abcd123456", + "sourceIp": "192.168.0.100" +} From 82e7d899d030492c2f60dc867b28707928d49734 Mon Sep 17 00:00:00 2001 From: Leandro Damascena Date: Mon, 27 Jan 2025 10:29:41 +0000 Subject: [PATCH 5/5] Merging from develop --- docs/utilities/data_classes.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/utilities/data_classes.md b/docs/utilities/data_classes.md index 6212701cb91..5d102e69784 100644 --- a/docs/utilities/data_classes.md +++ b/docs/utilities/data_classes.md @@ -98,8 +98,8 @@ Each event source is linked to its corresponding GitHub file with the full set o | [SES](#ses) | `SESEvent` | [Github](https://github.com/aws-powertools/powertools-lambda-python/blob/develop/aws_lambda_powertools/utilities/data_classes/ses_event.py) | | [SNS](#sns) | `SNSEvent` | [Github](https://github.com/aws-powertools/powertools-lambda-python/blob/develop/aws_lambda_powertools/utilities/data_classes/sns_event.py) | | [SQS](#sqs) | `SQSEvent` | [Github](https://github.com/aws-powertools/powertools-lambda-python/blob/develop/aws_lambda_powertools/utilities/data_classes/sqs_event.py) | -| [TransferFamilyAuthorizer] | `TransferFamilyAuthorizer` | -| [TransferFamilyAuthorizerResponse] | `TransferFamilyAuthorizerResponse` | +| [TransferFamilyAuthorizer] | `TransferFamilyAuthorizer` | [Github](https://github.com/aws-powertools/powertools-lambda-python/blob/develop/aws_lambda_powertools/utilities/data_classes/transfer_family_event.py) | +| [TransferFamilyAuthorizerResponse] | `TransferFamilyAuthorizerResponse` | [Github](https://github.com/aws-powertools/powertools-lambda-python/blob/develop/aws_lambda_powertools/utilities/data_classes/transfer_family_event.py) | | [VPC Lattice V2](#vpc-lattice-v2) | `VPCLatticeV2Event` | [Github](https://github.com/aws-powertools/powertools-lambda-python/blob/develop/aws_lambda_powertools/utilities/data_classes/vpc_lattice.py) | | [VPC Lattice V1](#vpc-lattice-v1) | `VPCLatticeEvent` | [Github](https://github.com/aws-powertools/powertools-lambda-python/blob/develop/aws_lambda_powertools/utilities/data_classes/vpc_lattice.py) |