diff --git a/IMPLEMENTATION_COVERAGE.md b/IMPLEMENTATION_COVERAGE.md index 2b99d2f58..86b511f0e 100644 --- a/IMPLEMENTATION_COVERAGE.md +++ b/IMPLEMENTATION_COVERAGE.md @@ -4077,6 +4077,59 @@ - [ ] verify_mac +## lakeformation +
+34% implemented + +- [ ] add_lf_tags_to_resource +- [ ] assume_decorated_role_with_saml +- [X] batch_grant_permissions +- [X] batch_revoke_permissions +- [ ] cancel_transaction +- [ ] commit_transaction +- [ ] create_data_cells_filter +- [X] create_lf_tag +- [ ] delete_data_cells_filter +- [X] delete_lf_tag +- [ ] delete_objects_on_cancel +- [X] deregister_resource +- [X] describe_resource +- [ ] describe_transaction +- [ ] extend_transaction +- [ ] get_data_cells_filter +- [X] get_data_lake_settings +- [ ] get_effective_permissions_for_path +- [X] get_lf_tag +- [ ] get_query_state +- [ ] get_query_statistics +- [ ] get_resource_lf_tags +- [ ] get_table_objects +- [ ] get_temporary_glue_partition_credentials +- [ ] get_temporary_glue_table_credentials +- [ ] get_work_unit_results +- [ ] get_work_units +- [X] grant_permissions +- [X] list_data_cells_filter +- [X] list_lf_tags +- [X] list_permissions +- [X] list_resources +- [ ] list_table_storage_optimizers +- [ ] list_transactions +- [X] put_data_lake_settings +- [X] register_resource +- [ ] remove_lf_tags_from_resource +- [X] revoke_permissions +- [ ] search_databases_by_lf_tags +- [ ] search_tables_by_lf_tags +- [ ] start_query_planning +- [ ] start_transaction +- [ ] update_data_cells_filter +- [ ] update_lf_tag +- [ ] update_resource +- [ ] update_table_objects +- [ ] update_table_storage_optimizer +
+ ## lambda
53% implemented @@ -6956,7 +7009,6 @@ - kinesis-video-webrtc-storage - kinesisanalytics - kinesisanalyticsv2 -- lakeformation - lex-models - lex-runtime - lexv2-models diff --git a/docs/docs/services/lakeformation.rst b/docs/docs/services/lakeformation.rst new file mode 100644 index 000000000..8ca355156 --- /dev/null +++ b/docs/docs/services/lakeformation.rst @@ -0,0 +1,83 @@ +.. _implementedservice_lakeformation: + +.. |start-h3| raw:: html + +

+ +.. |end-h3| raw:: html + +

+ +============= +lakeformation +============= + +|start-h3| Example usage |end-h3| + +.. sourcecode:: python + + @mock_lakeformation + def test_lakeformation_behaviour: + boto3.client("lakeformation") + ... + + + +|start-h3| Implemented features for this service |end-h3| + +- [ ] add_lf_tags_to_resource +- [ ] assume_decorated_role_with_saml +- [X] batch_grant_permissions +- [X] batch_revoke_permissions +- [ ] cancel_transaction +- [ ] commit_transaction +- [ ] create_data_cells_filter +- [X] create_lf_tag +- [ ] delete_data_cells_filter +- [X] delete_lf_tag +- [ ] delete_objects_on_cancel +- [X] deregister_resource +- [X] describe_resource +- [ ] describe_transaction +- [ ] extend_transaction +- [ ] get_data_cells_filter +- [X] get_data_lake_settings +- [ ] get_effective_permissions_for_path +- [X] get_lf_tag +- [ ] get_query_state +- [ ] get_query_statistics +- [ ] get_resource_lf_tags +- [ ] get_table_objects +- [ ] get_temporary_glue_partition_credentials +- [ ] get_temporary_glue_table_credentials +- [ ] get_work_unit_results +- [ ] get_work_units +- [X] grant_permissions +- [X] list_data_cells_filter + + This currently just returns an empty list, as the corresponding Create is not yet implemented + + +- [X] list_lf_tags +- [X] list_permissions + + No parameters have been implemented yet + + +- [X] list_resources +- [ ] list_table_storage_optimizers +- [ ] list_transactions +- [X] put_data_lake_settings +- [X] register_resource +- [ ] remove_lf_tags_from_resource +- [X] revoke_permissions +- [ ] search_databases_by_lf_tags +- [ ] search_tables_by_lf_tags +- [ ] start_query_planning +- [ ] start_transaction +- [ ] update_data_cells_filter +- [ ] update_lf_tag +- [ ] update_resource +- [ ] update_table_objects +- [ ] update_table_storage_optimizer + diff --git a/moto/__init__.py b/moto/__init__.py index b18f450ae..2ab5615e5 100644 --- a/moto/__init__.py +++ b/moto/__init__.py @@ -102,6 +102,7 @@ mock_kinesisvideoarchivedmedia = lazy_load( boto3_name="kinesis-video-archived-media", ) mock_kms = lazy_load(".kms", "mock_kms") +mock_lakeformation = lazy_load(".lakeformation", "mock_lakeformation") mock_logs = lazy_load(".logs", "mock_logs") mock_managedblockchain = lazy_load(".managedblockchain", "mock_managedblockchain") mock_mediaconnect = lazy_load(".mediaconnect", "mock_mediaconnect") diff --git a/moto/backend_index.py b/moto/backend_index.py index dccc30d37..20cee8a4b 100644 --- a/moto/backend_index.py +++ b/moto/backend_index.py @@ -92,6 +92,7 @@ backend_url_patterns = [ re.compile("https?://.*\\.kinesisvideo\\.(.+)\\.amazonaws.com"), ), ("kms", re.compile("https?://kms\\.(.+)\\.amazonaws\\.com")), + ("lakeformation", re.compile("https?://lakeformation\\.(.+)\\.amazonaws\\.com")), ("lambda", re.compile("https?://lambda\\.(.+)\\.amazonaws\\.com")), ("logs", re.compile("https?://logs\\.(.+)\\.amazonaws\\.com")), ( diff --git a/moto/iam/models.py b/moto/iam/models.py index 9bec926f3..c1d666d0c 100644 --- a/moto/iam/models.py +++ b/moto/iam/models.py @@ -1818,6 +1818,7 @@ class IAMBackend(BaseBackend): # Maybe we can enable this (and roles for other services) as part of a major release # self.create_service_linked_role( # service_name="opensearchservice.amazonaws.com", suffix="", description="" + # service_name="lakeformation.amazonaws.com" # ) def attach_role_policy(self, policy_arn: str, role_name: str) -> None: diff --git a/moto/lakeformation/__init__.py b/moto/lakeformation/__init__.py new file mode 100644 index 000000000..29c3287bc --- /dev/null +++ b/moto/lakeformation/__init__.py @@ -0,0 +1,5 @@ +"""lakeformation module initialization; sets value for base decorator.""" +from .models import lakeformation_backends +from ..core.models import base_decorator + +mock_lakeformation = base_decorator(lakeformation_backends) diff --git a/moto/lakeformation/exceptions.py b/moto/lakeformation/exceptions.py new file mode 100644 index 000000000..4e57f24cf --- /dev/null +++ b/moto/lakeformation/exceptions.py @@ -0,0 +1,7 @@ +"""Exceptions raised by the lakeformation service.""" +from moto.core.exceptions import JsonRESTError + + +class EntityNotFound(JsonRESTError): + def __init__(self) -> None: + super().__init__("EntityNotFoundException", "Entity not found") diff --git a/moto/lakeformation/models.py b/moto/lakeformation/models.py new file mode 100644 index 000000000..61005f6cf --- /dev/null +++ b/moto/lakeformation/models.py @@ -0,0 +1,173 @@ +from collections import defaultdict +from typing import Any, Dict, List + +from moto.core import BaseBackend, BackendDict, BaseModel +from moto.utilities.tagging_service import TaggingService +from .exceptions import EntityNotFound + + +class Resource(BaseModel): + def __init__(self, arn: str, role_arn: str): + self.arn = arn + self.role_arn = role_arn + + def to_dict(self) -> Dict[str, Any]: + return { + "ResourceArn": self.arn, + "RoleArn": self.role_arn, + } + + +def default_settings() -> Dict[str, Any]: + return { + "DataLakeAdmins": [], + "CreateDatabaseDefaultPermissions": [ + { + "Principal": {"DataLakePrincipalIdentifier": "IAM_ALLOWED_PRINCIPALS"}, + "Permissions": ["ALL"], + } + ], + "CreateTableDefaultPermissions": [ + { + "Principal": {"DataLakePrincipalIdentifier": "IAM_ALLOWED_PRINCIPALS"}, + "Permissions": ["ALL"], + } + ], + "TrustedResourceOwners": [], + "AllowExternalDataFiltering": False, + "ExternalDataFilteringAllowList": [], + } + + +class LakeFormationBackend(BaseBackend): + def __init__(self, region_name: str, account_id: str): + super().__init__(region_name, account_id) + self.resources: Dict[str, Resource] = dict() + self.settings: Dict[str, Dict[str, Any]] = defaultdict(default_settings) + self.grants: Dict[str, List[Dict[str, Any]]] = defaultdict(list) + self.tagger = TaggingService() + + def describe_resource(self, resource_arn: str) -> Resource: + if resource_arn not in self.resources: + raise EntityNotFound + return self.resources[resource_arn] + + def deregister_resource(self, resource_arn: str) -> None: + del self.resources[resource_arn] + + def register_resource(self, resource_arn: str, role_arn: str) -> None: + self.resources[resource_arn] = Resource(resource_arn, role_arn) + + def list_resources(self) -> List[Resource]: + return list(self.resources.values()) + + def get_data_lake_settings(self, catalog_id: str) -> Dict[str, Any]: + return self.settings[catalog_id] + + def put_data_lake_settings(self, catalog_id: str, settings: Dict[str, Any]) -> None: + self.settings[catalog_id] = settings + + def grant_permissions( + self, + catalog_id: str, + principal: Dict[str, str], + resource: Dict[str, Any], + permissions: List[str], + permissions_with_grant_options: List[str], + ) -> None: + self.grants[catalog_id].append( + { + "Principal": principal, + "Resource": resource, + "Permissions": permissions, + "PermissionsWithGrantOption": permissions_with_grant_options, + } + ) + + def revoke_permissions( + self, + catalog_id: str, + principal: Dict[str, str], + resource: Dict[str, Any], + permissions_to_revoke: List[str], + permissions_with_grant_options_to_revoke: List[str], + ) -> None: + for grant in self.grants[catalog_id]: + if grant["Principal"] == principal and grant["Resource"] == resource: + grant["Permissions"] = [ + perm + for perm in grant["Permissions"] + if perm not in permissions_to_revoke + ] + if grant.get("PermissionsWithGrantOption") is not None: + grant["PermissionsWithGrantOption"] = [ + perm + for perm in grant["PermissionsWithGrantOption"] + if perm not in permissions_with_grant_options_to_revoke + ] + self.grants[catalog_id] = [ + grant for grant in self.grants[catalog_id] if grant["Permissions"] != [] + ] + + def list_permissions(self, catalog_id: str) -> List[Dict[str, Any]]: + """ + No parameters have been implemented yet + """ + return self.grants[catalog_id] + + def create_lf_tag(self, catalog_id: str, key: str, values: List[str]) -> None: + # There is no ARN that we can use, so just create another unique identifier that's easy to recognize and reproduce + arn = f"arn:lakeformation:{catalog_id}" + tag_list = TaggingService.convert_dict_to_tags_input({key: values}) # type: ignore + self.tagger.tag_resource(arn=arn, tags=tag_list) + + def get_lf_tag(self, catalog_id: str, key: str) -> List[str]: + # There is no ARN that we can use, so just create another unique identifier that's easy to recognize and reproduce + arn = f"arn:lakeformation:{catalog_id}" + all_tags = self.tagger.get_tag_dict_for_resource(arn=arn) + return all_tags.get(key, []) # type: ignore + + def delete_lf_tag(self, catalog_id: str, key: str) -> None: + # There is no ARN that we can use, so just create another unique identifier that's easy to recognize and reproduce + arn = f"arn:lakeformation:{catalog_id}" + self.tagger.untag_resource_using_names(arn, tag_names=[key]) + + def list_lf_tags(self, catalog_id: str) -> Dict[str, str]: + # There is no ARN that we can use, so just create another unique identifier that's easy to recognize and reproduce + arn = f"arn:lakeformation:{catalog_id}" + return self.tagger.get_tag_dict_for_resource(arn=arn) + + def list_data_cells_filter(self) -> List[Dict[str, Any]]: + """ + This currently just returns an empty list, as the corresponding Create is not yet implemented + """ + return [] + + def batch_grant_permissions( + self, catalog_id: str, entries: List[Dict[str, Any]] + ) -> None: + for entry in entries: + self.grant_permissions( + catalog_id=catalog_id, + principal=entry.get("Principal"), # type: ignore[arg-type] + resource=entry.get("Resource"), # type: ignore[arg-type] + permissions=entry.get("Permissions"), # type: ignore[arg-type] + permissions_with_grant_options=entry.get("PermissionsWithGrantOptions"), # type: ignore[arg-type] + ) + + def batch_revoke_permissions( + self, catalog_id: str, entries: List[Dict[str, Any]] + ) -> None: + for entry in entries: + self.revoke_permissions( + catalog_id=catalog_id, + principal=entry.get("Principal"), # type: ignore[arg-type] + resource=entry.get("Resource"), # type: ignore[arg-type] + permissions_to_revoke=entry.get("Permissions"), # type: ignore[arg-type] + permissions_with_grant_options_to_revoke=entry.get( # type: ignore[arg-type] + "PermissionsWithGrantOptions" + ), + ) + + +lakeformation_backends = BackendDict(LakeFormationBackend, "lakeformation") diff --git a/moto/lakeformation/responses.py b/moto/lakeformation/responses.py new file mode 100644 index 000000000..4109f0265 --- /dev/null +++ b/moto/lakeformation/responses.py @@ -0,0 +1,139 @@ +"""Handles incoming lakeformation requests, invokes methods, returns responses.""" +import json + +from moto.core.responses import BaseResponse +from .models import lakeformation_backends, LakeFormationBackend + + +class LakeFormationResponse(BaseResponse): + """Handler for LakeFormation requests and responses.""" + + def __init__(self) -> None: + super().__init__(service_name="lakeformation") + + @property + def lakeformation_backend(self) -> LakeFormationBackend: + """Return backend instance specific for this region.""" + return lakeformation_backends[self.current_account][self.region] + + def describe_resource(self) -> str: + resource_arn = self._get_param("ResourceArn") + resource = self.lakeformation_backend.describe_resource( + resource_arn=resource_arn + ) + return json.dumps({"ResourceInfo": resource.to_dict()}) + + def deregister_resource(self) -> str: + resource_arn = self._get_param("ResourceArn") + self.lakeformation_backend.deregister_resource(resource_arn=resource_arn) + return "{}" + + def register_resource(self) -> str: + resource_arn = self._get_param("ResourceArn") + role_arn = self._get_param("RoleArn") + self.lakeformation_backend.register_resource( + resource_arn=resource_arn, + role_arn=role_arn, + ) + return "{}" + + def list_resources(self) -> str: + resources = self.lakeformation_backend.list_resources() + return json.dumps({"ResourceInfoList": [res.to_dict() for res in resources]}) + + def get_data_lake_settings(self) -> str: + catalog_id = self._get_param("CatalogId") or self.current_account + settings = self.lakeformation_backend.get_data_lake_settings(catalog_id) + return json.dumps({"DataLakeSettings": settings}) + + def put_data_lake_settings(self) -> str: + catalog_id = self._get_param("CatalogId") or self.current_account + settings = self._get_param("DataLakeSettings") + self.lakeformation_backend.put_data_lake_settings(catalog_id, settings) + return "{}" + + def grant_permissions(self) -> str: + catalog_id = self._get_param("CatalogId") or self.current_account + principal = self._get_param("Principal") + resource = self._get_param("Resource") + permissions = self._get_param("Permissions") + permissions_with_grant_options = self._get_param("PermissionsWithGrantOption") + self.lakeformation_backend.grant_permissions( + catalog_id=catalog_id, + principal=principal, + resource=resource, + permissions=permissions, + permissions_with_grant_options=permissions_with_grant_options, + ) + return "{}" + + def revoke_permissions(self) -> str: + catalog_id = self._get_param("CatalogId") or self.current_account + principal = self._get_param("Principal") + resource = self._get_param("Resource") + permissions = self._get_param("Permissions") + permissions_with_grant_options = ( + self._get_param("PermissionsWithGrantOption") or [] + ) + self.lakeformation_backend.revoke_permissions( + catalog_id=catalog_id, + principal=principal, + resource=resource, + permissions_to_revoke=permissions, + permissions_with_grant_options_to_revoke=permissions_with_grant_options, + ) + return "{}" + + def list_permissions(self) -> str: + catalog_id = self._get_param("CatalogId") or self.current_account + permissions = self.lakeformation_backend.list_permissions(catalog_id) + return json.dumps({"PrincipalResourcePermissions": permissions}) + + def create_lf_tag(self) -> str: + catalog_id = self._get_param("CatalogId") or self.current_account + key = self._get_param("TagKey") + values = self._get_param("TagValues") + self.lakeformation_backend.create_lf_tag(catalog_id, key, values) + return "{}" + + def get_lf_tag(self) -> str: + catalog_id = self._get_param("CatalogId") or self.current_account + key = self._get_param("TagKey") + tag_values = self.lakeformation_backend.get_lf_tag(catalog_id, key) + return json.dumps( + {"CatalogId": catalog_id, "TagKey": key, "TagValues": tag_values} + ) + + def delete_lf_tag(self) -> str: + catalog_id = self._get_param("CatalogId") or self.current_account + key = self._get_param("TagKey") + self.lakeformation_backend.delete_lf_tag(catalog_id, key) + return "{}" + + def list_lf_tags(self) -> str: + catalog_id = self._get_param("CatalogId") or self.current_account + tags = self.lakeformation_backend.list_lf_tags(catalog_id) + return json.dumps( + { + "LFTags": [ + {"CatalogId": catalog_id, "TagKey": tag, "TagValues": value} + for tag, value in tags.items() + ] + } + ) + + def list_data_cells_filter(self) -> str: + data_cells = self.lakeformation_backend.list_data_cells_filter() + return json.dumps({"DataCellsFilters": data_cells}) + + def batch_grant_permissions(self) -> str: + catalog_id = self._get_param("CatalogId") or self.current_account + entries = self._get_param("Entries") + self.lakeformation_backend.batch_grant_permissions(catalog_id, entries) + return json.dumps({"Failures": []}) + + def batch_revoke_permissions(self) -> str: + catalog_id = self._get_param("CatalogId") or self.current_account + entries = self._get_param("Entries") + self.lakeformation_backend.batch_revoke_permissions(catalog_id, entries) + return json.dumps({"Failures": []}) diff --git a/moto/lakeformation/urls.py b/moto/lakeformation/urls.py new file mode 100644 index 000000000..ed8c4e093 --- /dev/null +++ b/moto/lakeformation/urls.py @@ -0,0 +1,29 @@ +"""lakeformation base URL and path.""" +from .responses import LakeFormationResponse + +url_bases = [ + r"https?://lakeformation\.(.+)\.amazonaws\.com", +] + + +response = LakeFormationResponse() + + +url_paths = { + "{0}/DescribeResource$": response.dispatch, + "{0}/DeregisterResource$": response.dispatch, + "{0}/RegisterResource$": response.dispatch, + "{0}/ListResources$": response.dispatch, + "{0}/GetDataLakeSettings$": response.dispatch, + "{0}/PutDataLakeSettings$": response.dispatch, + "{0}/GrantPermissions$": response.dispatch, + "{0}/ListPermissions$": response.dispatch, + "{0}/RevokePermissions$": response.dispatch, + "{0}/CreateLFTag$": response.dispatch, + "{0}/GetLFTag$": response.dispatch, + "{0}/DeleteLFTag$": response.dispatch, + "{0}/ListLFTags$": response.dispatch, + "{0}/ListDataCellsFilter$": response.dispatch, + "{0}/BatchGrantPermissions$": response.dispatch, + "{0}/BatchRevokePermissions$": response.dispatch, +} diff --git a/tests/terraformtests/terraform-tests.success.txt b/tests/terraformtests/terraform-tests.success.txt index 49040b582..198eec084 100644 --- a/tests/terraformtests/terraform-tests.success.txt +++ b/tests/terraformtests/terraform-tests.success.txt @@ -342,6 +342,8 @@ kms: - TestAccKMSKey_Policy_iamServiceLinkedRole - TestAccKMSSecretDataSource - TestAccKMSSecretsDataSource +lakeformation: + - TestAccLakeFormationResource lambda: - TestAccLambdaAlias_ - TestAccLambdaLayerVersion_basic diff --git a/tests/test_lakeformation/__init__.py b/tests/test_lakeformation/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/test_lakeformation/test_lakeformation.py b/tests/test_lakeformation/test_lakeformation.py new file mode 100644 index 000000000..5083a9f5e --- /dev/null +++ b/tests/test_lakeformation/test_lakeformation.py @@ -0,0 +1,256 @@ +"""Unit tests for lakeformation-supported APIs.""" +import boto3 +import pytest + +from botocore.exceptions import ClientError +from moto import mock_lakeformation +from moto.core import DEFAULT_ACCOUNT_ID + +# See our Development Tips on writing tests for hints on how to write good tests: +# http://docs.getmoto.org/en/latest/docs/contributing/development_tips/tests.html + + +@mock_lakeformation +def test_register_resource(): + client = boto3.client("lakeformation", region_name="us-east-2") + resp = client.register_resource( + ResourceArn="some arn", + ) + + del resp["ResponseMetadata"] + assert resp == {} + + +@mock_lakeformation +def test_describe_resource(): + client = boto3.client("lakeformation", region_name="us-east-2") + client.register_resource(ResourceArn="some arn", RoleArn="role arn") + + resp = client.describe_resource(ResourceArn="some arn") + + assert resp["ResourceInfo"] == {"ResourceArn": "some arn", "RoleArn": "role arn"} + + +@mock_lakeformation +def test_deregister_resource(): + client = boto3.client("lakeformation", region_name="us-east-2") + client.register_resource(ResourceArn="some arn") + client.deregister_resource(ResourceArn="some arn") + + with pytest.raises(ClientError) as exc: + client.describe_resource(ResourceArn="some arn") + err = exc.value.response["Error"] + assert err["Code"] == "EntityNotFoundException" + + +@mock_lakeformation +def test_list_resources(): + client = boto3.client("lakeformation", region_name="us-east-2") + + resp = client.list_resources() + assert resp["ResourceInfoList"] == [] + + client.register_resource(ResourceArn="some arn") + client.register_resource(ResourceArn="another arn") + + resp = client.list_resources() + assert len(resp["ResourceInfoList"]) == 2 + + +@mock_lakeformation +def test_data_lake_settings(): + client = boto3.client("lakeformation", region_name="us-east-2") + resp = client.get_data_lake_settings() + assert resp["DataLakeSettings"] == { + "DataLakeAdmins": [], + "CreateDatabaseDefaultPermissions": [ + { + "Principal": {"DataLakePrincipalIdentifier": "IAM_ALLOWED_PRINCIPALS"}, + "Permissions": ["ALL"], + } + ], + "CreateTableDefaultPermissions": [ + { + "Principal": {"DataLakePrincipalIdentifier": "IAM_ALLOWED_PRINCIPALS"}, + "Permissions": ["ALL"], + } + ], + "TrustedResourceOwners": [], + "AllowExternalDataFiltering": False, + "ExternalDataFilteringAllowList": [], + } + + settings = {"DataLakeAdmins": [{"DataLakePrincipalIdentifier": "dlpi"}]} + client.put_data_lake_settings(DataLakeSettings=settings) + + resp = client.get_data_lake_settings() + assert resp["DataLakeSettings"] == settings + + +@mock_lakeformation +def test_list_permissions(): + client = boto3.client("lakeformation", region_name="eu-west-2") + + resp = client.grant_permissions( + Principal={"DataLakePrincipalIdentifier": "asdf"}, + Resource={"Database": {"Name": "db"}}, + Permissions=["ALL"], + PermissionsWithGrantOption=["SELECT"], + ) + + del resp["ResponseMetadata"] + assert resp == {} + + # list all + resp = client.list_permissions() + assert resp["PrincipalResourcePermissions"] == [ + { + "Principal": {"DataLakePrincipalIdentifier": "asdf"}, + "Resource": {"Database": {"Name": "db"}}, + "Permissions": ["ALL"], + "PermissionsWithGrantOption": ["SELECT"], + } + ] + + +@mock_lakeformation +def test_revoke_permissions(): + client = boto3.client("lakeformation", region_name="eu-west-2") + + client.grant_permissions( + Principal={"DataLakePrincipalIdentifier": "asdf"}, + Resource={"Database": {"Name": "db"}}, + Permissions=["SELECT", "ALTER", "DROP"], + PermissionsWithGrantOption=["SELECT", "DROP"], + ) + + resp = client.revoke_permissions( + Principal={"DataLakePrincipalIdentifier": "asdf"}, + Resource={"Database": {"Name": "db"}}, + Permissions=["DROP"], + ) + + del resp["ResponseMetadata"] + assert resp == {} + + # list all + resp = client.list_permissions() + assert resp["PrincipalResourcePermissions"] == [ + { + "Principal": {"DataLakePrincipalIdentifier": "asdf"}, + "Resource": {"Database": {"Name": "db"}}, + "Permissions": ["SELECT", "ALTER"], + "PermissionsWithGrantOption": ["SELECT", "DROP"], + } + ] + + +@mock_lakeformation +def test_lf_tags(): + client = boto3.client("lakeformation", region_name="eu-west-2") + + client.create_lf_tag(TagKey="tag1", TagValues=["1a", "1b"]) + client.create_lf_tag(TagKey="tag2", TagValues=["2a", "2b"]) + client.create_lf_tag(TagKey="tag3", TagValues=["3a", "3b"]) + + resp = client.get_lf_tag(TagKey="tag1") + assert resp["CatalogId"] == DEFAULT_ACCOUNT_ID + assert resp["TagKey"] == "tag1" + assert resp["TagValues"] == ["1a", "1b"] + + resp = client.list_lf_tags() + assert len(resp["LFTags"]) == 3 + assert { + "CatalogId": DEFAULT_ACCOUNT_ID, + "TagKey": "tag1", + "TagValues": ["1a", "1b"], + } in resp["LFTags"] + assert { + "CatalogId": DEFAULT_ACCOUNT_ID, + "TagKey": "tag2", + "TagValues": ["2a", "2b"], + } in resp["LFTags"] + assert { + "CatalogId": DEFAULT_ACCOUNT_ID, + "TagKey": "tag3", + "TagValues": ["3a", "3b"], + } in resp["LFTags"] + + client.delete_lf_tag(TagKey="tag2") + + resp = client.list_lf_tags() + assert len(resp["LFTags"]) == 2 + assert { + "CatalogId": DEFAULT_ACCOUNT_ID, + "TagKey": "tag1", + "TagValues": ["1a", "1b"], + } in resp["LFTags"] + assert { + "CatalogId": DEFAULT_ACCOUNT_ID, + "TagKey": "tag3", + "TagValues": ["3a", "3b"], + } in resp["LFTags"] + + +@mock_lakeformation +def test_list_data_cells_filter(): + client = boto3.client("lakeformation", region_name="eu-west-2") + + resp = client.list_data_cells_filter() + assert resp["DataCellsFilters"] == [] + + +@mock_lakeformation +def test_batch_revoke_permissions(): + client = boto3.client("lakeformation", region_name="eu-west-2") + + client.batch_grant_permissions( + Entries=[ + { + "Id": "id1", + "Principal": {"DataLakePrincipalIdentifier": "id1"}, + "Resource": {"Database": {"Name": "db"}}, + "Permissions": ["SELECT", "ALTER", "DROP"], + "PermissionsWithGrantOption": ["SELECT", "DROP"], + }, + { + "Id": "id2", + "Principal": {"DataLakePrincipalIdentifier": "id2"}, + "Resource": {"Database": {"Name": "db"}}, + "Permissions": ["SELECT", "ALTER", "DROP"], + "PermissionsWithGrantOption": ["SELECT", "DROP"], + }, + { + "Id": "id3", + "Principal": {"DataLakePrincipalIdentifier": "id3"}, + "Resource": {"Database": {"Name": "db"}}, + "Permissions": ["SELECT", "ALTER", "DROP"], + "PermissionsWithGrantOption": ["SELECT", "DROP"], + }, + ] + ) + + resp = client.list_permissions() + assert len(resp["PrincipalResourcePermissions"]) == 3 + + client.batch_revoke_permissions( + Entries=[ + { + "Id": "id1", + "Principal": {"DataLakePrincipalIdentifier": "id2"}, + "Resource": {"Database": {"Name": "db"}}, + "Permissions": ["SELECT", "ALTER", "DROP"], + "PermissionsWithGrantOption": ["SELECT", "DROP"], + }, + { + "Id": "id2", + "Principal": {"DataLakePrincipalIdentifier": "id3"}, + "Resource": {"Database": {"Name": "db"}}, + "Permissions": ["SELECT", "ALTER", "DROP"], + "PermissionsWithGrantOption": ["SELECT", "DROP"], + }, + ] + ) + + resp = client.list_permissions() + assert len(resp["PrincipalResourcePermissions"]) == 1