Repository: getmoto/moto
Pull request #8999 (2025-06-21 00:23:05)

RAM - Implementing ListPermissions and some refactoring
Hello, there 😊

I'm continuing the saga to implement tests for the RAM API calls. This time, I'm implementing the `list_permissions()` method. Also, I liked the recommendation from @bblommers to list the input parameters explicitly for the responses in `responses.py` and the `models.py` files (as stated [here](https://github.com/getmoto/moto/pull/8987#discussion_r2159588118)), so this generated some refactoring. Here's a summary of the PR:
- **[NEW]**: I'm implementing the `list_permissions()` API, mocking the response with the expected AWS managed permissions in the AWS RAM service with the services. The responses are based on the services configured in the `SHAREABLE_RESOURCES` list in `models.py` file;
- **[REFACTOR]**: since the responses for this new API implementation has a considerable amount of repeated dicts, I decided to refactor the `models.py` file in order to make things cleaner in this file. I don't know if it's the general pattern we use in the repo, but I created a `utils.py` file to centralize utilities and constants for the RAM implementations to be created in the `models.py` file. In order to generate the expected AWS managed permissions, I decided to code the `format_ram_permission()` function, which can be used to create `CUSTOMER_MANAGED` permissions in the future also;
- **[REFACTOR]**: since I created the `utils.py` file to centralize constants, I decided to move the `RESOURCE_TYPES` values to a new constant called `RAM_RESOURCE_TYPES`, which I decided to format dinamically with an auxiliary constant called `_RESOURCE_TYPE_DEFINITIONS`;
- **[REFACTOR]**: I refactored the `responses.py` and `models.py` files to respectively send and receive explicit parameters. I wasn't able to implement the same for the `create_resource_share()` functions. For some reason, when I tested, when this function was called for the second time, some internal error occurred and the test got stuck in an infinite loop internally. So, I decided to leave the `create_resource_share()` function as is;
- **[TESTS]**: new unit tests were added for both the new implementation and the new `utils.py` file.

Feel free to recomend any design pattern for the implementations I added and to guide me with any recomendations for the code and the contributions :)


Patch:
diff --git a/docs/docs/services/ram.rst b/docs/docs/services/ram.rst
index 1a3b86074..c0924ac3b 100644
--- a/docs/docs/services/ram.rst
+++ b/docs/docs/services/ram.rst
@@ -34,7 +34,7 @@ ram
 - [ ] list_pending_invitation_resources
 - [ ] list_permission_associations
 - [ ] list_permission_versions
-- [ ] list_permissions
+- [X] list_permissions
 - [ ] list_principals
 - [ ] list_replace_permission_associations_work
 - [ ] list_resource_share_permissions
diff --git a/moto/ram/models.py b/moto/ram/models.py
index 6f19c4c2c..c8c15e642 100644
--- a/moto/ram/models.py
+++ b/moto/ram/models.py
@@ -1,6 +1,6 @@
 import re
 import string
-from typing import Any, Dict, List
+from typing import Any, Dict, List, Optional
 
 from moto.core.base_backend import BackendDict, BaseBackend
 from moto.core.common_models import BaseModel
@@ -13,6 +13,7 @@ from moto.ram.exceptions import (
     OperationNotPermittedException,
     UnknownResourceException,
 )
+from moto.ram.utils import AWS_MANAGED_PERMISSIONS, RAM_RESOURCE_TYPES
 from moto.utilities.utils import get_partition
 
 
@@ -147,102 +148,18 @@ class ResourceShare(BaseModel):
             "status": self.status,
         }
 
-    def update(self, **kwargs: Any) -> None:
-        self.allow_external_principals = kwargs.get(
-            "allowExternalPrincipals", self.allow_external_principals
-        )
+    def update(self, allow_external_principals: bool, name: Optional[str]) -> None:
+        self.allow_external_principals = allow_external_principals
         self.last_updated_time = utcnow()
-        self.name = kwargs.get("name", self.name)
+        self.name = name
 
 
 class ResourceAccessManagerBackend(BaseBackend):
-    RESOURCE_TYPES = [  # List of resource types based on SHAREABLE_RESOURCES
-        {
-            "resourceType": "rds:Cluster",
-            "serviceName": "rds",
-            "resourceRegionScope": "REGIONAL",
-        },
-        {
-            "resourceType": "imagebuilder:Component",
-            "serviceName": "imagebuilder",
-            "resourceRegionScope": "REGIONAL",
-        },
-        {
-            "resourceType": "networkmanager:CoreNetwork",
-            "serviceName": "networkmanager",
-            "resourceRegionScope": "GLOBAL",
-        },
-        {
-            "resourceType": "resource-groups:Group",
-            "serviceName": "resource-groups",
-            "resourceRegionScope": "REGIONAL",
-        },
-        {
-            "resourceType": "imagebuilder:Image",
-            "serviceName": "imagebuilder",
-            "resourceRegionScope": "REGIONAL",
-        },
-        {
-            "resourceType": "imagebuilder:ImageRecipe",
-            "serviceName": "imagebuilder",
-            "resourceRegionScope": "REGIONAL",
-        },
-        {
-            "resourceType": "license-manager:LicenseConfiguration",
-            "serviceName": "license-manager",
-            "resourceRegionScope": "REGIONAL",
-        },
-        {
-            "resourceType": "appmesh:Mesh",
-            "serviceName": "appmesh",
-            "resourceRegionScope": "REGIONAL",
-        },
-        {
-            "resourceType": "ec2:PrefixList",
-            "serviceName": "ec2",
-            "resourceRegionScope": "REGIONAL",
-        },
-        {
-            "resourceType": "codebuild:Project",
-            "serviceName": "codebuild",
-            "resourceRegionScope": "REGIONAL",
-        },
-        {
-            "resourceType": "codebuild:ReportGroup",
-            "serviceName": "codebuild",
-            "resourceRegionScope": "REGIONAL",
-        },
-        {
-            "resourceType": "route53resolver:ResolverRule",
-            "serviceName": "route53resolver",
-            "resourceRegionScope": "REGIONAL",
-        },
-        {
-            "resourceType": "ec2:Subnet",
-            "serviceName": "ec2",
-            "resourceRegionScope": "REGIONAL",
-        },
-        {
-            "resourceType": "ec2:TransitGatewayMulticastDomain",
-            "serviceName": "ec2",
-            "resourceRegionScope": "REGIONAL",
-        },
-        {
-            "resourceType": "glue:Database",
-            "serviceName": "glue",
-            "resourceRegionScope": "REGIONAL",
-        },
-        {
-            "resourceType": "glue:Table",
-            "serviceName": "glue",
-            "resourceRegionScope": "REGIONAL",
-        },
-        {
-            "resourceType": "glue:Catalog",
-            "serviceName": "glue",
-            "resourceRegionScope": "REGIONAL",
-        },
-    ]
+    RESOURCE_TYPES = RAM_RESOURCE_TYPES
+
+    PERMISSION_TYPES = ["ALL", "AWS", "LOCAL"]
+
+    MANAGED_PERMISSIONS = AWS_MANAGED_PERMISSIONS
 
     def __init__(self, region_name: str, account_id: str):
         super().__init__(region_name, account_id)
@@ -264,16 +181,14 @@ class ResourceAccessManagerBackend(BaseBackend):
 
         return dict(resourceShare=response)
 
-    def get_resource_shares(self, **kwargs: Any) -> Dict[str, Any]:
-        owner = kwargs["resourceOwner"]
-
-        if owner not in ["SELF", "OTHER-ACCOUNTS"]:
+    def get_resource_shares(self, resource_owner: Optional[str]) -> Dict[str, Any]:
+        if resource_owner not in ["SELF", "OTHER-ACCOUNTS"]:
             raise InvalidParameterException(
-                f"{owner} is not a valid resource owner. "
+                f"{resource_owner} is not a valid resource owner. "
                 "Specify either SELF or OTHER-ACCOUNTS and try again."
             )
 
-        if owner == "OTHER-ACCOUNTS":
+        if resource_owner == "OTHER-ACCOUNTS":
             raise NotImplementedError(
                 "Value 'OTHER-ACCOUNTS' for parameter 'resourceOwner' not implemented."
             )
@@ -282,29 +197,48 @@ class ResourceAccessManagerBackend(BaseBackend):
 
         return dict(resourceShares=resouces)
 
-    def update_resource_share(self, **kwargs: Any) -> Dict[str, Any]:
-        arn = kwargs["resourceShareArn"]
-
+    def update_resource_share(
+        self,
+        resource_share_arn: Optional[str],
+        allow_external_principals: bool,
+        name: Optional[str],
+    ) -> Dict[str, Any]:
         resource = next(
-            (resource for resource in self.resource_shares if arn == resource.arn), None
+            (
+                resource
+                for resource in self.resource_shares
+                if resource_share_arn == resource.arn
+            ),
+            None,
         )
 
         if not resource:
-            raise UnknownResourceException(f"ResourceShare {arn} could not be found.")
+            raise UnknownResourceException(
+                f"ResourceShare {resource_share_arn} could not be found."
+            )
 
-        resource.update(**kwargs)
+        resource.update(allow_external_principals, name)
         response = resource.describe()
         response.pop("featureSet")
 
         return dict(resourceShare=response)
 
-    def delete_resource_share(self, arn: str) -> Dict[str, Any]:
+    def delete_resource_share(
+        self, resource_share_arn: Optional[str]
+    ) -> Dict[str, Any]:
         resource = next(
-            (resource for resource in self.resource_shares if arn == resource.arn), None
+            (
+                resource
+                for resource in self.resource_shares
+                if resource_share_arn == resource.arn
+            ),
+            None,
         )
 
         if not resource:
-            raise UnknownResourceException(f"ResourceShare {arn} could not be found.")
+            raise UnknownResourceException(
+                f"ResourceShare {resource_share_arn} could not be found."
+            )
 
         resource.delete()
 
@@ -316,15 +250,20 @@ class ResourceAccessManagerBackend(BaseBackend):
 
         return dict(returnValue=True)
 
-    def get_resource_share_associations(self, **kwargs: Any) -> Dict[str, Any]:
-        association_type = kwargs["associationType"]
+    def get_resource_share_associations(
+        self,
+        association_type: Optional[str],
+        association_status: Optional[str],
+        resource_share_arns: List[str],
+        resource_arn: Optional[str],
+        principal: Optional[str],
+    ) -> Dict[str, Any]:
         if association_type not in ["PRINCIPAL", "RESOURCE"]:
             raise InvalidParameterException(
                 f"{association_type} is not a valid association type. "
                 "Specify either PRINCIPAL or RESOURCE and try again."
             )
 
-        association_status = kwargs.get("associationStatus")
         if association_status and association_status not in [
             "ASSOCIATING",
             "ASSOCIATED",
@@ -336,10 +275,6 @@ class ResourceAccessManagerBackend(BaseBackend):
                 f"{association_status} is not a valid association status."
             )
 
-        resource_share_arns = kwargs.get("resourceShareArns", [])
-        resource_arn = kwargs.get("resourceArn")
-        principal = kwargs.get("principal")
-
         if association_type == "PRINCIPAL" and resource_arn:
             raise InvalidParameterException(
                 "You cannot specify a resource ARN when the association type is PRINCIPAL."
@@ -393,9 +328,7 @@ class ResourceAccessManagerBackend(BaseBackend):
 
         return dict(resourceShareAssociations=associations)
 
-    def list_resource_types(self, **kwargs: Any) -> Dict[str, Any]:
-        resource_region_scope = kwargs.get("resourceRegionScope", "ALL")
-
+    def list_resource_types(self, resource_region_scope: str) -> Dict[str, Any]:
         if resource_region_scope not in ["ALL", "REGIONAL", "GLOBAL"]:
             raise InvalidParameterException(
                 f"{resource_region_scope} is not a valid resource region "
@@ -413,5 +346,47 @@ class ResourceAccessManagerBackend(BaseBackend):
 
         return dict(resourceTypes=resource_types)
 
+    def list_permissions(
+        self, resource_type: str, permission_type: str
+    ) -> Dict[str, Any]:
+        permission_types_relation = {
+            "AWS": "AWS_MANAGED",
+            "LOCAL": "CUSTOMER_MANAGED",
+        }
+
+        # Here, resourceType first partition (service) is case sensitive and
+        # last partition (type) is case insensitive
+        if resource_type and not any(
+            str(permission["resourceType"]).split(":")[0] == resource_type.split(":")[0]
+            and str(permission["resourceType"]).lower() == resource_type.lower()
+            for permission in self.MANAGED_PERMISSIONS
+        ):
+            raise InvalidParameterException(f"Invalid resource type: {resource_type}")
+
+        if resource_type:
+            permissions = [
+                permission
+                for permission in self.MANAGED_PERMISSIONS
+                if str(permission["resourceType"]).lower() == resource_type.lower()
+            ]
+        else:
+            permissions = self.MANAGED_PERMISSIONS
+
+        if permission_type not in self.PERMISSION_TYPES:
+            raise InvalidParameterException(
+                f"{permission_type} is not a valid scope. Must be one of: "
+                f"{', '.join(self.PERMISSION_TYPES)}."
+            )
+
+        if permission_type != "ALL":
+            permissions = [
+                permission
+                for permission in permissions
+                if permission_types_relation.get(permission_type)
+                == permission["permissionType"]
+            ]
+
+        return dict(permissions=permissions)
+
 
 ram_backends = BackendDict(ResourceAccessManagerBackend, "ram")
diff --git a/moto/ram/responses.py b/moto/ram/responses.py
index 7b36f71f1..1aa76b2a3 100644
--- a/moto/ram/responses.py
+++ b/moto/ram/responses.py
@@ -25,22 +25,49 @@ class ResourceAccessManagerResponse(BaseResponse):
         return json.dumps(self.ram_backend.create_resource_share(**self.request_params))
 
     def get_resource_shares(self) -> str:
-        return json.dumps(self.ram_backend.get_resource_shares(**self.request_params))
+        resource_owner = self._get_param("resourceOwner")
+        return json.dumps(self.ram_backend.get_resource_shares(resource_owner))
 
     def update_resource_share(self) -> str:
-        return json.dumps(self.ram_backend.update_resource_share(**self.request_params))
+        resource_share_arn = self._get_param("resourceShareArn")
+        allow_external_principals = self._get_param("allowExternalPrincipals", True)
+        name = self._get_param("name")
+        return json.dumps(
+            self.ram_backend.update_resource_share(
+                resource_share_arn, allow_external_principals, name
+            )
+        )
 
     def delete_resource_share(self) -> str:
-        arn = self._get_param("resourceShareArn")
-        return json.dumps(self.ram_backend.delete_resource_share(arn))
+        resource_share_arn = self._get_param("resourceShareArn")
+        return json.dumps(self.ram_backend.delete_resource_share(resource_share_arn))
 
     def enable_sharing_with_aws_organization(self) -> str:
         return json.dumps(self.ram_backend.enable_sharing_with_aws_organization())
 
     def get_resource_share_associations(self) -> str:
+        association_type = self._get_param("associationType")
+        association_status = self._get_param("associationStatus")
+        resource_share_arns = self._get_param("resourceShareArns", [])
+        resource_arn = self._get_param("resourceArn")
+        principal = self._get_param("principal")
         return json.dumps(
-            self.ram_backend.get_resource_share_associations(**self.request_params)
+            self.ram_backend.get_resource_share_associations(
+                association_type,
+                association_status,
+                resource_share_arns,
+                resource_arn,
+                principal,
+            )
         )
 
     def list_resource_types(self) -> str:
-        return json.dumps(self.ram_backend.list_resource_types(**self.request_params))
+        resource_region_scope = self._get_param("resourceRegionScope", "ALL")
+        return json.dumps(self.ram_backend.list_resource_types(resource_region_scope))
+
+    def list_permissions(self) -> str:
+        resource_type = self._get_param("resourceType")
+        permission_type = self._get_param("permissionType", "ALL")
+        return json.dumps(
+            self.ram_backend.list_permissions(resource_type, permission_type)
+        )
diff --git a/moto/ram/urls.py b/moto/ram/urls.py
index e973085d1..ee1440b2f 100644
--- a/moto/ram/urls.py
+++ b/moto/ram/urls.py
@@ -10,4 +10,5 @@ url_paths = {
     "{0}/getresourceshareassociations$": ResourceAccessManagerResponse.dispatch,
     "{0}/updateresourceshare$": ResourceAccessManagerResponse.dispatch,
     "{0}/listresourcetypes$": ResourceAccessManagerResponse.dispatch,
+    "{0}/listpermissions$": ResourceAccessManagerResponse.dispatch,
 }
diff --git a/moto/ram/utils.py b/moto/ram/utils.py
new file mode 100644
index 000000000..1de1e0c05
--- /dev/null
+++ b/moto/ram/utils.py
@@ -0,0 +1,311 @@
+from datetime import datetime, timezone
+from typing import Dict, Optional, Union
+
+
+def format_ram_permission(
+    name: str,
+    resource_type: str,
+    version: str = "1",
+    arn_prefix: str = "arn:aws:ram::aws:permission/",
+    status: str = "ATTACHABLE",
+    creation_time: Optional[str] = None,
+    last_updated_time: Optional[str] = None,
+    is_resource_type_default: bool = True,
+    permission_type: str = "AWS_MANAGED",
+    default_version: bool = True,
+) -> Dict[str, Union[str, bool]]:
+    """
+    Format a RAM (Resource Access Manager) permission dictionary with the
+    specified attributes.
+
+    Args:
+        name (str): The name of the permission.
+        resource_type (str): The type of resource the permission applies to.
+        version (str): The version of the permission. Defaults to "1".
+        arn_prefix (str): The prefix for the permission ARN. Defaults
+            to "arn:aws:ram::aws:permission/".
+        status (str): The status of the permission. Defaults to
+            "ATTACHABLE".
+        creation_time (str, optional): The creation time in UTC. If not
+            provided, uses the current time.
+        last_updated_time (str, optional): The last updated time in UTC. If
+            not provided, uses the creation time.
+        is_resource_type_default (bool): Whether this is the default
+            permission for the resource type. Defaults to True.
+        permission_type (str): The type of permission (e.g.,
+            "AWS_MANAGED"). Defaults to "AWS_MANAGED".
+        default_version (bool): Whether this is the default version.
+            Defaults to True.
+
+    Returns:
+        dict: A dictionary representing the formatted RAM permission.
+    """
+    now = datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M:%S.%f")[:-3]
+    creation_time = creation_time or now
+    last_updated_time = last_updated_time or creation_time
+    arn = f"{arn_prefix}{name}"
+    return {
+        "arn": arn,
+        "version": version,
+        "defaultVersion": default_version,
+        "name": name,
+        "resourceType": resource_type,
+        "status": status,
+        "creationTime": creation_time,
+        "lastUpdatedTime": last_updated_time,
+        "isResourceTypeDefault": is_resource_type_default,
+        "permissionType": permission_type,
+    }
+
+
+# List of tuples with resourceType, serviceName, and resourceRegionScope based
+# on the SHARED_RESOURCE_TYPES in models.py
+_RESOURCE_TYPE_DEFINITIONS = [
+    ("rds:Cluster", "rds", "REGIONAL"),
+    ("imagebuilder:Component", "imagebuilder", "REGIONAL"),
+    ("networkmanager:CoreNetwork", "networkmanager", "GLOBAL"),
+    ("resource-groups:Group", "resource-groups", "REGIONAL"),
+    ("imagebuilder:Image", "imagebuilder", "REGIONAL"),
+    ("imagebuilder:ImageRecipe", "imagebuilder", "REGIONAL"),
+    ("license-manager:LicenseConfiguration", "license-manager", "REGIONAL"),
+    ("appmesh:Mesh", "appmesh", "REGIONAL"),
+    ("ec2:PrefixList", "ec2", "REGIONAL"),
+    ("codebuild:Project", "codebuild", "REGIONAL"),
+    ("codebuild:ReportGroup", "codebuild", "REGIONAL"),
+    ("route53resolver:ResolverRule", "route53resolver", "REGIONAL"),
+    ("ec2:Subnet", "ec2", "REGIONAL"),
+    ("ec2:TransitGatewayMulticastDomain", "ec2", "REGIONAL"),
+    ("glue:Database", "glue", "REGIONAL"),
+    ("glue:Table", "glue", "REGIONAL"),
+    ("glue:Catalog", "glue", "REGIONAL"),
+]
+
+# List of dictionaries representing RAM resource types based on the definitions
+RAM_RESOURCE_TYPES = [
+    {
+        "resourceType": resource_type,
+        "serviceName": service_name,
+        "resourceRegionScope": region_scope,
+    }
+    for resource_type, service_name, region_scope in _RESOURCE_TYPE_DEFINITIONS
+]
+
+# List of AWS managed permissions for RAM, formatted using the
+# format_ram_permission function.
+AWS_MANAGED_PERMISSIONS = [
+    format_ram_permission(
+        name="AWSRAMDefaultPermissionRDSCluster",
+        resource_type="rds:Cluster",
+        creation_time="2022-06-30 17:04:16.671000",
+    ),
+    format_ram_permission(
+        name="AWSRAMDefaultPermissionImageBuilderComponent",
+        resource_type="imagebuilder:Component",
+        creation_time="2022-06-30 17:04:21.977000",
+    ),
+    format_ram_permission(
+        name="AWSRAMDefaultPermissionsNetworkManagerCoreNetwork",
+        resource_type="networkmanager:CoreNetwork",
+        version="3",
+        creation_time="2024-10-23 16:34:51.604000",
+    ),
+    format_ram_permission(
+        name="AWSRAMTransitGatewayPermissionsNetworkManagerCoreNetwork",
+        resource_type="networkmanager:CoreNetwork",
+        is_resource_type_default=False,
+        creation_time="2022-06-30 17:03:48.071000",
+    ),
+    format_ram_permission(
+        name="AWSRAMVPCPermissionsNetworkManagerCoreNetwork",
+        resource_type="networkmanager:CoreNetwork",
+        is_resource_type_default=False,
+        creation_time="2022-06-30 17:03:46.477000",
+    ),
+    format_ram_permission(
+        name="AWSRAMDefaultPermissionResourceGroup",
+        resource_type="resource-groups:Group",
+        creation_time="2022-06-30 17:04:10.914000",
+    ),
+    format_ram_permission(
+        name="AWSRAMDefaultPermissionImageBuilderImage",
+        resource_type="imagebuilder:Image",
+        creation_time="2022-06-30 17:04:21.201000",
+    ),
+    format_ram_permission(
+        name="AWSRAMDefaultPermissionImageBuilderImageRecipe",
+        resource_type="imagebuilder:ImageRecipe",
+        creation_time="2022-06-30 17:04:22.832000",
+    ),
+    format_ram_permission(
+        name="AWSRAMDefaultPermissionLicenseConfiguration",
+        resource_type="license-manager:LicenseConfiguration",
+        creation_time="2024-01-31 16:36:13.664000",
+    ),
+    format_ram_permission(
+        name="AWSRAMDefaultPermissionAppMesh",
+        resource_type="appmesh:Mesh",
+        version="3",
+        creation_time="2023-03-28 12:27:55.910000",
+    ),
+    format_ram_permission(
+        name="AWSRAMDefaultPermissionPrefixList",
+        resource_type="ec2:PrefixList",
+        creation_time="2022-06-30 17:04:20.594000",
+    ),
+    format_ram_permission(
+        name="AWSRAMDefaultPermissionCodeBuildProject",
+        resource_type="codebuild:Project",
+        creation_time="2022-06-30 17:04:17.595000",
+    ),
+    format_ram_permission(
+        name="AWSRAMDefaultPermissionCodeBuildReportGroup",
+        resource_type="codebuild:ReportGroup",
+        creation_time="2022-06-30 17:04:17.761000",
+    ),
+    format_ram_permission(
+        name="AWSRAMDefaultPermissionResolverRule",
+        resource_type="route53resolver:ResolverRule",
+        creation_time="2022-06-30 17:04:14.752000",
+    ),
+    format_ram_permission(
+        name="AWSRAMDefaultPermissionSubnet",
+        resource_type="ec2:Subnet",
+        creation_time="2024-10-30 15:11:41.358000",
+    ),
+    format_ram_permission(
+        name="AWSRAMDefaultPermissionTransitGatewayMulticastDomain",
+        resource_type="ec2:TransitGatewayMulticastDomain",
+        creation_time="2022-06-30 17:04:13.461000",
+    ),
+    format_ram_permission(
+        name="AWSRAMDefaultPermissionGlueDatabase",
+        resource_type="glue:Database",
+        creation_time="2022-06-30 17:04:25.912000",
+    ),
+    format_ram_permission(
+        name="AWSRAMLFEnabledGlueAllTablesReadWriteForDatabase",
+        resource_type="glue:Database",
+        is_resource_type_default=False,
+        creation_time="2023-06-27 17:05:24.471000",
+    ),
+    format_ram_permission(
+        name="AWSRAMLFEnabledGlueDatabaseReadWrite",
+        resource_type="glue:Database",
+        is_resource_type_default=False,
+        creation_time="2023-06-27 17:05:27.153000",
+    ),
+    format_ram_permission(
+        name="AWSRAMPermissionGlueAllTablesReadWriteForDatabase",
+        resource_type="glue:Database",
+        version="3",
+        is_resource_type_default=False,
+        creation_time="2022-10-27 14:19:43.345000",
+    ),
+    format_ram_permission(
+        name="AWSRAMPermissionGlueDatabaseReadWrite",
+        resource_type="glue:Database",
+        version="3",
+        is_resource_type_default=False,
+        creation_time="2022-10-27 14:20:08.415000",
+    ),
+    format_ram_permission(
+        name="AWSRAMPermissionGlueTableReadWriteForDatabase",
+        resource_type="glue:Database",
+        version="3",
+        is_resource_type_default=False,
+        creation_time="2022-10-27 14:19:17.062000",
+    ),
+    format_ram_permission(
+        name="AWSRAMPermissionLFTagGlueDatabaseReadWrite",
+        resource_type="glue:Database",
+        is_resource_type_default=False,
+        creation_time="2022-10-27 14:22:14.138000",
+    ),
+    format_ram_permission(
+        name="AWSRAMPermissionLFTagGlueTableReadWriteForDatabase",
+        resource_type="glue:Database",
+        is_resource_type_default=False,
+        creation_time="2022-10-27 14:21:49.920000",
+    ),
+    format_ram_permission(
+        name="AWSRAMDefaultPermissionGlueTable",
+        resource_type="glue:Table",
+        creation_time="2022-06-30 17:04:24.372000",
+    ),
+    format_ram_permission(
+        name="AWSRAMLFEnabledGlueDatabaseReadWriteForTable",
+        resource_type="glue:Table",
+        is_resource_type_default=False,
+        creation_time="2023-06-27 17:05:33.534000",
+    ),
+    format_ram_permission(
+        name="AWSRAMLFEnabledGlueTableReadWrite",
+        resource_type="glue:Table",
+        is_resource_type_default=False,
+        creation_time="2023-06-27 17:05:21.095000",
+    ),
+    format_ram_permission(
+        name="AWSRAMPermissionGlueDatabaseReadWriteForTable",
+        resource_type="glue:Table",
+        version="2",
+        is_resource_type_default=False,
+        creation_time="2022-06-30 17:04:09.034000",
+    ),
+    format_ram_permission(
+        name="AWSRAMPermissionGlueTableReadWrite",
+        resource_type="glue:Table",
+        version="2",
+        is_resource_type_default=False,
+        creation_time="2022-06-30 17:04:03.947000",
+    ),
+    format_ram_permission(
+        name="AWSRAMPermissionLFTagGlueDatabaseReadWriteForTable",
+        resource_type="glue:Table",
+        is_resource_type_default=False,
+        creation_time="2022-10-27 14:22:26.191000",
+    ),
+    format_ram_permission(
+        name="AWSRAMPermissionLFTagGlueTableReadWrite",
+        resource_type="glue:Table",
+        is_resource_type_default=False,
+        creation_time="2022-10-27 14:22:01.908000",
+    ),
+    format_ram_permission(
+        name="AWSRAMDefaultPermissionGlueCatalog",
+        resource_type="glue:Catalog",
+        creation_time="2022-06-30 17:04:26.612000",
+    ),
+    format_ram_permission(
+        name="AWSRAMPermissionGlueAllTablesReadWriteForCatalog",
+        resource_type="glue:Catalog",
+        version="3",
+        is_resource_type_default=False,
+        creation_time="2022-10-27 14:19:56.250000",
+    ),
+    format_ram_permission(
+        name="AWSRAMPermissionGlueDatabaseReadWriteForCatalog",
+        resource_type="glue:Catalog",
+        version="3",
+        is_resource_type_default=False,
+        creation_time="2022-10-27 14:20:20.322000",
+    ),
+    format_ram_permission(
+        name="AWSRAMPermissionGlueTableReadWriteForCatalog",
+        resource_type="glue:Catalog",
+        version="3",
+        is_resource_type_default=False,
+        creation_time="2022-10-27 14:19:30.644000",
+    ),
+    format_ram_permission(
+        name="AWSRAMPermissionLFTagGlueDatabaseReadWriteForCatalog",
+        resource_type="glue:Catalog",
+        is_resource_type_default=False,
+        creation_time="2022-10-27 14:22:37.435000",
+    ),
+    format_ram_permission(
+        name="AWSRAMPermissionLFTagGlueTableReadWriteForCatalog",
+        resource_type="glue:Catalog",
+        is_resource_type_default=False,
+        creation_time="2022-10-27 14:21:37.593000",
+    ),
+]


Test patch:
diff --git a/tests/test_ram/test_ram.py b/tests/test_ram/test_ram.py
index 5a3b96c9d..d3637c644 100644
--- a/tests/test_ram/test_ram.py
+++ b/tests/test_ram/test_ram.py
@@ -1,3 +1,4 @@
+import json
 import re
 import time
 from datetime import datetime
@@ -580,3 +581,74 @@ def test_list_resource_types(resource_region_scope, expect_error, error_message)
 
         assert "resourceTypes" in response
         assert response["resourceTypes"] == expected_types
+
+
+@mock_aws
+@pytest.mark.parametrize(
+    "parameters, expect_error, error_message",
+    [
+        ({}, False, None),
+        ({"resourceType": "glue:catalog"}, False, None),
+        ({"permissionType": "ALL"}, False, None),
+        ({"resourceType": "glue:catalog", "permissionType": "AWS"}, False, None),
+        (
+            {"resourceType": "gluE:catalog", "permissionType": "AWS"},
+            True,
+            "Invalid resource type: gluE:catalog",
+        ),
+        (
+            {"resourceType": "glue:catalog", "permissionType": "INVALID"},
+            True,
+            "INVALID is not a valid scope. Must be one of: ALL, AWS, LOCAL.",
+        ),
+    ],
+    ids=[
+        "default_params",
+        "valid_resource_type",
+        "valid_permission_type",
+        "valid_resource_type_and_permission_type",
+        "invalid_resource_type",
+        "invalid_permission_type",
+    ],
+)
+def test_list_permissions(parameters, expect_error, error_message):
+    client = boto3.client("ram", region_name="us-east-1")
+    permission_types_relation = {
+        "AWS": "AWS_MANAGED",
+        "LOCAL": "CUSTOMER_MANAGED",
+    }
+    resource_type = parameters.get("resourceType")
+    permission_type = parameters.get("permissionType")
+
+    # when
+    if expect_error:
+        with pytest.raises(ClientError) as e:
+            client.list_permissions(**parameters)
+        ex = e.value
+        assert ex.response["ResponseMetadata"]["HTTPStatusCode"] == 400
+        assert "InvalidParameterException" in ex.response["Error"]["Code"]
+        assert ex.response["Error"]["Message"] == error_message
+    else:
+        response = client.list_permissions(**parameters)
+
+        # then
+        expected_permissions = ResourceAccessManagerBackend.MANAGED_PERMISSIONS
+        if resource_type:
+            expected_permissions = [
+                permission
+                for permission in expected_permissions
+                if permission["resourceType"].lower() == resource_type.lower()
+            ]
+
+        if permission_type and permission_type != "ALL":
+            expected_permissions = [
+                permission
+                for permission in expected_permissions
+                if permission_types_relation.get(permission_type)
+                == permission["permissionType"]
+            ]
+
+        assert "permissions" in response
+        assert json.dumps(response["permissions"], default=str) == json.dumps(
+            expected_permissions, default=str
+        )
diff --git a/tests/test_ram/test_ram_utils.py b/tests/test_ram/test_ram_utils.py
new file mode 100644
index 000000000..4cfd3d566
--- /dev/null
+++ b/tests/test_ram/test_ram_utils.py
@@ -0,0 +1,84 @@
+import re
+
+from moto.ram.utils import (
+    AWS_MANAGED_PERMISSIONS,
+    RAM_RESOURCE_TYPES,
+    format_ram_permission,
+)
+
+
+def test_format_ram_permission_defaults():
+    name = "TestPermission"
+    resource_type = "test:Resource"
+    result = format_ram_permission(name, resource_type)
+    assert result["name"] == name
+    assert result["resourceType"] == resource_type
+    assert result["version"] == "1"
+    assert result["arn"] == f"arn:aws:ram::aws:permission/{name}"
+    assert result["status"] == "ATTACHABLE"
+    assert result["isResourceTypeDefault"] is True
+    assert result["permissionType"] == "AWS_MANAGED"
+    assert result["defaultVersion"] is True
+    # creationTime and lastUpdatedTime should be a string in the expected format
+    assert re.match(
+        r"\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d{3}", result["creationTime"]
+    )
+    assert result["creationTime"] == result["lastUpdatedTime"]
+
+
+def test_format_ram_permission_custom_values():
+    name = "CustomPermission"
+    resource_type = "custom:Type"
+    version = "2"
+    arn_prefix = "arn:aws:ram::custom:permission/"
+    status = "UNATTACHABLE"
+    creation_time = "2023-01-01 12:00:00.000"
+    last_updated_time = "2023-01-02 13:00:00.000"
+    is_resource_type_default = False
+    permission_type = "CUSTOM"
+    default_version = False
+
+    result = format_ram_permission(
+        name=name,
+        resource_type=resource_type,
+        version=version,
+        arn_prefix=arn_prefix,
+        status=status,
+        creation_time=creation_time,
+        last_updated_time=last_updated_time,
+        is_resource_type_default=is_resource_type_default,
+        permission_type=permission_type,
+        default_version=default_version,
+    )
+    assert result["name"] == name
+    assert result["resourceType"] == resource_type
+    assert result["version"] == version
+    assert result["arn"] == f"{arn_prefix}{name}"
+    assert result["status"] == status
+    assert result["creationTime"] == creation_time
+    assert result["lastUpdatedTime"] == last_updated_time
+    assert result["isResourceTypeDefault"] is False
+    assert result["permissionType"] == permission_type
+    assert result["defaultVersion"] is False
+
+
+def test_ram_resource_types_structure():
+    for entry in RAM_RESOURCE_TYPES:
+        assert "resourceType" in entry
+        assert "serviceName" in entry
+        assert "resourceRegionScope" in entry
+        assert entry["resourceRegionScope"] in ("REGIONAL", "GLOBAL")
+
+
+def test_aws_managed_permissions_structure():
+    for perm in AWS_MANAGED_PERMISSIONS:
+        assert "arn" in perm
+        assert perm["arn"].startswith("arn:aws:ram::aws:permission/")
+        assert "name" in perm
+        assert "resourceType" in perm
+        assert "creationTime" in perm
+        assert "lastUpdatedTime" in perm
+        assert "status" in perm
+        assert "permissionType" in perm
+        assert perm["permissionType"] == "AWS_MANAGED"
+        assert isinstance(perm["isResourceTypeDefault"], bool)