|
6 | 6 |
|
7 | 7 | from botocore.utils import InvalidArnException |
8 | 8 |
|
9 | | -from localstack.aws.api import RequestContext |
| 9 | +from localstack.aws.api import CommonServiceException, RequestContext |
10 | 10 | from localstack.aws.api.sns import ( |
11 | 11 | AmazonResourceName, |
12 | 12 | ConfirmSubscriptionResponse, |
| 13 | + CreatePlatformApplicationResponse, |
13 | 14 | CreateTopicResponse, |
| 15 | + GetPlatformApplicationAttributesResponse, |
14 | 16 | GetSMSAttributesResponse, |
15 | 17 | GetSubscriptionAttributesResponse, |
16 | 18 | GetTopicAttributesResponse, |
17 | 19 | InvalidParameterException, |
| 20 | + ListEndpointsByPlatformApplicationResponse, |
| 21 | + ListPlatformApplicationsResponse, |
18 | 22 | ListString, |
19 | 23 | ListSubscriptionsByTopicResponse, |
20 | 24 | ListSubscriptionsResponse, |
21 | 25 | ListTagsForResourceResponse, |
22 | 26 | ListTopicsResponse, |
23 | 27 | MapStringToString, |
24 | 28 | NotFoundException, |
| 29 | + PlatformApplication, |
25 | 30 | SetSMSAttributesResponse, |
26 | 31 | SnsApi, |
27 | 32 | String, |
|
45 | 50 | ) |
46 | 51 | from localstack.services.sns import constants as sns_constants |
47 | 52 | from localstack.services.sns.certificate import SNS_SERVER_CERT |
48 | | -from localstack.services.sns.constants import DUMMY_SUBSCRIPTION_PRINCIPAL |
| 53 | +from localstack.services.sns.constants import ( |
| 54 | + DUMMY_SUBSCRIPTION_PRINCIPAL, |
| 55 | + VALID_APPLICATION_PLATFORMS, |
| 56 | +) |
49 | 57 | from localstack.services.sns.filter import FilterPolicyValidator |
50 | 58 | from localstack.services.sns.publisher import PublishDispatcher, SnsPublishContext |
51 | 59 | from localstack.services.sns.v2.models import ( |
|
65 | 73 | get_next_page_token_from_arn, |
66 | 74 | get_region_from_subscription_token, |
67 | 75 | is_valid_e164_number, |
| 76 | + parse_and_validate_platform_application_arn, |
68 | 77 | parse_and_validate_topic_arn, |
69 | 78 | validate_subscription_attribute, |
70 | 79 | ) |
71 | | -from localstack.utils.aws.arns import get_partition, parse_arn, sns_topic_arn |
| 80 | +from localstack.utils.aws.arns import ( |
| 81 | + get_partition, |
| 82 | + parse_arn, |
| 83 | + sns_platform_application_arn, |
| 84 | + sns_topic_arn, |
| 85 | +) |
72 | 86 | from localstack.utils.collections import PaginatedList, select_from_typed_dict |
73 | 87 |
|
74 | 88 | # set up logger |
@@ -535,6 +549,115 @@ def list_subscriptions_by_topic( |
535 | 549 | response["NextToken"] = next_token |
536 | 550 | return response |
537 | 551 |
|
| 552 | + # |
| 553 | + # PlatformApplications |
| 554 | + # |
| 555 | + def create_platform_application( |
| 556 | + self, |
| 557 | + context: RequestContext, |
| 558 | + name: String, |
| 559 | + platform: String, |
| 560 | + attributes: MapStringToString, |
| 561 | + **kwargs, |
| 562 | + ) -> CreatePlatformApplicationResponse: |
| 563 | + _validate_platform_application_name(name) |
| 564 | + if platform not in VALID_APPLICATION_PLATFORMS: |
| 565 | + raise InvalidParameterException( |
| 566 | + f"Invalid parameter: Platform Reason: {platform} is not supported" |
| 567 | + ) |
| 568 | + |
| 569 | + _validate_platform_application_attributes(attributes) |
| 570 | + |
| 571 | + # attribute validation specific to create_platform_application |
| 572 | + if "PlatformCredential" in attributes and "PlatformPrincipal" not in attributes: |
| 573 | + raise InvalidParameterException( |
| 574 | + "Invalid parameter: Attributes Reason: PlatformCredential attribute provided without PlatformPrincipal" |
| 575 | + ) |
| 576 | + |
| 577 | + elif "PlatformPrincipal" in attributes and "PlatformCredential" not in attributes: |
| 578 | + raise InvalidParameterException( |
| 579 | + "Invalid parameter: Attributes Reason: PlatformPrincipal attribute provided without PlatformCredential" |
| 580 | + ) |
| 581 | + |
| 582 | + store = self.get_store(context.account_id, context.region) |
| 583 | + # We are not validating the access data here like AWS does (against ADM and the like) |
| 584 | + attributes.pop("PlatformPrincipal") |
| 585 | + attributes.pop("PlatformCredential") |
| 586 | + _attributes = {"Enabled": "true"} |
| 587 | + _attributes.update(attributes) |
| 588 | + application_arn = sns_platform_application_arn( |
| 589 | + platform_application_name=name, |
| 590 | + platform=platform, |
| 591 | + account_id=context.account_id, |
| 592 | + region_name=context.region, |
| 593 | + ) |
| 594 | + platform_application = PlatformApplication( |
| 595 | + PlatformApplicationArn=application_arn, Attributes=_attributes |
| 596 | + ) |
| 597 | + store.platform_applications[application_arn] = platform_application |
| 598 | + return CreatePlatformApplicationResponse(**platform_application) |
| 599 | + |
| 600 | + def delete_platform_application( |
| 601 | + self, context: RequestContext, platform_application_arn: String, **kwargs |
| 602 | + ) -> None: |
| 603 | + store = self.get_store(context.account_id, context.region) |
| 604 | + store.platform_applications.pop(platform_application_arn, None) |
| 605 | + |
| 606 | + def list_platform_applications( |
| 607 | + self, context: RequestContext, next_token: String | None = None, **kwargs |
| 608 | + ) -> ListPlatformApplicationsResponse: |
| 609 | + store = self.get_store(context.account_id, context.region) |
| 610 | + platform_applications = store.platform_applications.values() |
| 611 | + paginated_applications = PaginatedList(platform_applications) |
| 612 | + page, token = paginated_applications.get_page( |
| 613 | + token_generator=lambda x: get_next_page_token_from_arn(x["PlatformApplicationArn"]), |
| 614 | + page_size=100, |
| 615 | + next_token=next_token, |
| 616 | + ) |
| 617 | + |
| 618 | + response = ListPlatformApplicationsResponse(PlatformApplications=page) |
| 619 | + if token: |
| 620 | + response["NextToken"] = token |
| 621 | + return response |
| 622 | + |
| 623 | + def get_platform_application_attributes( |
| 624 | + self, context: RequestContext, platform_application_arn: String, **kwargs |
| 625 | + ) -> GetPlatformApplicationAttributesResponse: |
| 626 | + platform_application = self._get_platform_application(platform_application_arn, context) |
| 627 | + attributes = platform_application["Attributes"] |
| 628 | + return GetPlatformApplicationAttributesResponse(Attributes=attributes) |
| 629 | + |
| 630 | + def set_platform_application_attributes( |
| 631 | + self, |
| 632 | + context: RequestContext, |
| 633 | + platform_application_arn: String, |
| 634 | + attributes: MapStringToString, |
| 635 | + **kwargs, |
| 636 | + ) -> None: |
| 637 | + parse_and_validate_platform_application_arn(platform_application_arn) |
| 638 | + _validate_platform_application_attributes(attributes) |
| 639 | + |
| 640 | + platform_application = self._get_platform_application(platform_application_arn, context) |
| 641 | + platform_application["Attributes"].update(attributes) |
| 642 | + |
| 643 | + # |
| 644 | + # Platform Endpoints |
| 645 | + # |
| 646 | + |
| 647 | + def list_endpoints_by_platform_application( |
| 648 | + self, |
| 649 | + context: RequestContext, |
| 650 | + platform_application_arn: String, |
| 651 | + next_token: String | None = None, |
| 652 | + **kwargs, |
| 653 | + ) -> ListEndpointsByPlatformApplicationResponse: |
| 654 | + # TODO: stub so cleanup fixture won't fail |
| 655 | + return ListEndpointsByPlatformApplicationResponse(Endpoints=[]) |
| 656 | + |
| 657 | + # |
| 658 | + # Sms operations |
| 659 | + # |
| 660 | + |
538 | 661 | def set_sms_attributes( |
539 | 662 | self, context: RequestContext, attributes: MapStringToString, **kwargs |
540 | 663 | ) -> SetSMSAttributesResponse: |
@@ -606,6 +729,17 @@ def _get_topic(arn: str, context: RequestContext) -> Topic: |
606 | 729 | except KeyError: |
607 | 730 | raise NotFoundException("Topic does not exist") |
608 | 731 |
|
| 732 | + @staticmethod |
| 733 | + def _get_platform_application( |
| 734 | + platform_application_arn: str, context: RequestContext |
| 735 | + ) -> PlatformApplication: |
| 736 | + parse_and_validate_platform_application_arn(platform_application_arn) |
| 737 | + try: |
| 738 | + store = SnsProvider.get_store(context.account_id, context.region) |
| 739 | + return store.platform_applications[platform_application_arn] |
| 740 | + except KeyError: |
| 741 | + raise NotFoundException("PlatformApplication does not exist") |
| 742 | + |
609 | 743 |
|
610 | 744 | def _create_topic(name: str, attributes: dict, context: RequestContext) -> Topic: |
611 | 745 | topic_arn = sns_topic_arn( |
@@ -673,6 +807,28 @@ def _create_default_topic_policy(topic: Topic, context: RequestContext) -> str: |
673 | 807 | ) |
674 | 808 |
|
675 | 809 |
|
| 810 | +def _validate_platform_application_name(name: str) -> None: |
| 811 | + reason = "" |
| 812 | + if not name: |
| 813 | + reason = "cannot be empty" |
| 814 | + elif not re.match(r"^.{0,256}$", name): |
| 815 | + reason = "must be at most 256 characters long" |
| 816 | + elif not re.match(r"^[A-Za-z0-9._-]+$", name): |
| 817 | + reason = "must contain only characters 'a'-'z', 'A'-'Z', '0'-'9', '_', '-', and '.'" |
| 818 | + |
| 819 | + if reason: |
| 820 | + raise InvalidParameterException(f"Invalid parameter: {name} Reason: {reason}") |
| 821 | + |
| 822 | + |
| 823 | +def _validate_platform_application_attributes(attributes: dict) -> None: |
| 824 | + if not attributes: |
| 825 | + raise CommonServiceException( |
| 826 | + code="ValidationError", |
| 827 | + message="1 validation error detected: Value null at 'attributes' failed to satisfy constraint: Member must not be null", |
| 828 | + sender_fault=True, |
| 829 | + ) |
| 830 | + |
| 831 | + |
676 | 832 | def _validate_sms_attributes(attributes: dict) -> None: |
677 | 833 | for k, v in attributes.items(): |
678 | 834 | if k not in SMS_ATTRIBUTE_NAMES: |
|
0 commit comments