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