diff --git a/IMPLEMENTATION_COVERAGE.md b/IMPLEMENTATION_COVERAGE.md
index 6fabaae1bbaa..4542e251bea2 100644
--- a/IMPLEMENTATION_COVERAGE.md
+++ b/IMPLEMENTATION_COVERAGE.md
@@ -4215,6 +4215,69 @@
- [ ] write_get_object_response
+## s3control
+
+5% implemented
+
+- [ ] create_access_point
+- [ ] create_access_point_for_object_lambda
+- [ ] create_bucket
+- [ ] create_job
+- [ ] create_multi_region_access_point
+- [ ] delete_access_point
+- [ ] delete_access_point_for_object_lambda
+- [ ] delete_access_point_policy
+- [ ] delete_access_point_policy_for_object_lambda
+- [ ] delete_bucket
+- [ ] delete_bucket_lifecycle_configuration
+- [ ] delete_bucket_policy
+- [ ] delete_bucket_tagging
+- [ ] delete_job_tagging
+- [ ] delete_multi_region_access_point
+- [X] delete_public_access_block
+- [ ] delete_storage_lens_configuration
+- [ ] delete_storage_lens_configuration_tagging
+- [ ] describe_job
+- [ ] describe_multi_region_access_point_operation
+- [ ] get_access_point
+- [ ] get_access_point_configuration_for_object_lambda
+- [ ] get_access_point_for_object_lambda
+- [ ] get_access_point_policy
+- [ ] get_access_point_policy_for_object_lambda
+- [ ] get_access_point_policy_status
+- [ ] get_access_point_policy_status_for_object_lambda
+- [ ] get_bucket
+- [ ] get_bucket_lifecycle_configuration
+- [ ] get_bucket_policy
+- [ ] get_bucket_tagging
+- [ ] get_job_tagging
+- [ ] get_multi_region_access_point
+- [ ] get_multi_region_access_point_policy
+- [ ] get_multi_region_access_point_policy_status
+- [X] get_public_access_block
+- [ ] get_storage_lens_configuration
+- [ ] get_storage_lens_configuration_tagging
+- [ ] list_access_points
+- [ ] list_access_points_for_object_lambda
+- [ ] list_jobs
+- [ ] list_multi_region_access_points
+- [ ] list_regional_buckets
+- [ ] list_storage_lens_configurations
+- [ ] put_access_point_configuration_for_object_lambda
+- [ ] put_access_point_policy
+- [ ] put_access_point_policy_for_object_lambda
+- [ ] put_bucket_lifecycle_configuration
+- [ ] put_bucket_policy
+- [ ] put_bucket_tagging
+- [ ] put_job_tagging
+- [ ] put_multi_region_access_point_policy
+- [X] put_public_access_block
+- [ ] put_storage_lens_configuration
+- [ ] put_storage_lens_configuration_tagging
+- [ ] update_job_priority
+- [ ] update_job_status
+
+
## sagemaker
15% implemented
@@ -5205,7 +5268,6 @@
- route53-recovery-readiness
- route53domains
- rum
-- s3control
- s3outposts
- sagemaker-a2i-runtime
- sagemaker-edge
diff --git a/docs/docs/services/s3control.rst b/docs/docs/services/s3control.rst
new file mode 100644
index 000000000000..be2b0066d471
--- /dev/null
+++ b/docs/docs/services/s3control.rst
@@ -0,0 +1,87 @@
+.. _implementedservice_s3control:
+
+.. |start-h3| raw:: html
+
+
+
+.. |end-h3| raw:: html
+
+
+
+=========
+s3control
+=========
+
+.. autoclass:: moto.s3control.models.S3ControlBackend
+
+|start-h3| Example usage |end-h3|
+
+.. sourcecode:: python
+
+ @mock_s3control
+ def test_s3control_behaviour:
+ boto3.client("s3control")
+ ...
+
+
+
+|start-h3| Implemented features for this service |end-h3|
+
+- [ ] create_access_point
+- [ ] create_access_point_for_object_lambda
+- [ ] create_bucket
+- [ ] create_job
+- [ ] create_multi_region_access_point
+- [ ] delete_access_point
+- [ ] delete_access_point_for_object_lambda
+- [ ] delete_access_point_policy
+- [ ] delete_access_point_policy_for_object_lambda
+- [ ] delete_bucket
+- [ ] delete_bucket_lifecycle_configuration
+- [ ] delete_bucket_policy
+- [ ] delete_bucket_tagging
+- [ ] delete_job_tagging
+- [ ] delete_multi_region_access_point
+- [X] delete_public_access_block
+- [ ] delete_storage_lens_configuration
+- [ ] delete_storage_lens_configuration_tagging
+- [ ] describe_job
+- [ ] describe_multi_region_access_point_operation
+- [ ] get_access_point
+- [ ] get_access_point_configuration_for_object_lambda
+- [ ] get_access_point_for_object_lambda
+- [ ] get_access_point_policy
+- [ ] get_access_point_policy_for_object_lambda
+- [ ] get_access_point_policy_status
+- [ ] get_access_point_policy_status_for_object_lambda
+- [ ] get_bucket
+- [ ] get_bucket_lifecycle_configuration
+- [ ] get_bucket_policy
+- [ ] get_bucket_tagging
+- [ ] get_job_tagging
+- [ ] get_multi_region_access_point
+- [ ] get_multi_region_access_point_policy
+- [ ] get_multi_region_access_point_policy_status
+- [X] get_public_access_block
+- [ ] get_storage_lens_configuration
+- [ ] get_storage_lens_configuration_tagging
+- [ ] list_access_points
+- [ ] list_access_points_for_object_lambda
+- [ ] list_jobs
+- [ ] list_multi_region_access_points
+- [ ] list_regional_buckets
+- [ ] list_storage_lens_configurations
+- [ ] put_access_point_configuration_for_object_lambda
+- [ ] put_access_point_policy
+- [ ] put_access_point_policy_for_object_lambda
+- [ ] put_bucket_lifecycle_configuration
+- [ ] put_bucket_policy
+- [ ] put_bucket_tagging
+- [ ] put_job_tagging
+- [ ] put_multi_region_access_point_policy
+- [X] put_public_access_block
+- [ ] put_storage_lens_configuration
+- [ ] put_storage_lens_configuration_tagging
+- [ ] update_job_priority
+- [ ] update_job_status
+
diff --git a/moto/__init__.py b/moto/__init__.py
index d3020477ba4d..40e4d0059b19 100644
--- a/moto/__init__.py
+++ b/moto/__init__.py
@@ -176,6 +176,7 @@ def f(*args, **kwargs):
mock_elasticache = lazy_load(
".elasticache", "mock_elasticache", boto3_name="elasticache"
)
+mock_s3control = lazy_load(".s3control", "mock_s3control", boto3_name="s3control")
class MockAll(ContextDecorator):
@@ -206,7 +207,6 @@ def __exit__(self, *exc):
__title__ = "moto"
__version__ = "2.2.20.dev"
-
try:
# Need to monkey-patch botocore requests back to underlying urllib3 classes
from botocore.awsrequest import (
diff --git a/moto/backend_index.py b/moto/backend_index.py
index 71b29417459a..5cc209a6df91 100644
--- a/moto/backend_index.py
+++ b/moto/backend_index.py
@@ -113,6 +113,7 @@
"https?://(?P[a-zA-Z0-9\\-_.]*)\\.?s3(.*)\\.amazonaws.com"
),
),
+ ("s3control", re.compile("https?://(.+)\\.s3-control\\.(.+)\\.amazonaws\\.com")),
("sagemaker", re.compile("https?://api.sagemaker\\.(.+)\\.amazonaws.com")),
("sdb", re.compile("https?://sdb\\.(.+)\\.amazonaws\\.com")),
("secretsmanager", re.compile("https?://secretsmanager\\.(.+)\\.amazonaws\\.com")),
diff --git a/moto/s3control/__init__.py b/moto/s3control/__init__.py
new file mode 100644
index 000000000000..1ef58b93931d
--- /dev/null
+++ b/moto/s3control/__init__.py
@@ -0,0 +1,5 @@
+"""s3control module initialization; sets value for base decorator."""
+from .models import s3control_backends
+from ..core.models import base_decorator
+
+mock_s3control = base_decorator(s3control_backends)
diff --git a/moto/s3control/exceptions.py b/moto/s3control/exceptions.py
new file mode 100644
index 000000000000..f86402104d31
--- /dev/null
+++ b/moto/s3control/exceptions.py
@@ -0,0 +1,35 @@
+"""Exceptions raised by the s3control service."""
+from moto.s3.exceptions import S3ClientError
+
+
+class NoSuchPublicAccessBlockConfiguration(S3ClientError):
+ code = 404
+
+ def __init__(self, *args, **kwargs):
+ super(NoSuchPublicAccessBlockConfiguration, self).__init__(
+ "NoSuchPublicAccessBlockConfiguration",
+ "The public access block configuration was not found",
+ *args,
+ **kwargs,
+ )
+
+
+class InvalidPublicAccessBlockConfiguration(S3ClientError):
+ code = 400
+
+ def __init__(self, *args, **kwargs):
+ super(InvalidPublicAccessBlockConfiguration, self).__init__(
+ "InvalidRequest",
+ "Must specify at least one configuration.",
+ *args,
+ **kwargs,
+ )
+
+
+class WrongPublicAccessBlockAccountIdError(S3ClientError):
+ code = 403
+
+ def __init__(self):
+ super(WrongPublicAccessBlockAccountIdError, self).__init__(
+ "AccessDenied", "Access Denied"
+ )
diff --git a/moto/s3control/models.py b/moto/s3control/models.py
new file mode 100644
index 000000000000..718bdeb3f6c7
--- /dev/null
+++ b/moto/s3control/models.py
@@ -0,0 +1,98 @@
+"""S3ControlBackend class with methods for supported APIs."""
+from collections import defaultdict
+
+from boto3 import Session
+
+from moto.core import BaseBackend, BaseModel
+from moto.s3control.exceptions import (
+ NoSuchPublicAccessBlockConfiguration,
+ WrongPublicAccessBlockAccountIdError,
+)
+
+
+class PublicAccessBlock(BaseModel):
+ def __init__(
+ self,
+ block_public_acls=False,
+ ignore_public_acls=False,
+ block_public_policy=False,
+ restrict_public_buckets=False,
+ ):
+ # The boto XML appears to expect these values to exist as lowercase strings...
+ self.block_public_acls = block_public_acls or "false"
+ self.ignore_public_acls = ignore_public_acls or "false"
+ self.block_public_policy = block_public_policy or "false"
+ self.restrict_public_buckets = restrict_public_buckets or "false"
+
+ def to_config_dict(self):
+ # Need to make the string values booleans for Config:
+ return {
+ "blockPublicAcls": convert_str_to_bool(self.block_public_acls),
+ "ignorePublicAcls": convert_str_to_bool(self.ignore_public_acls),
+ "blockPublicPolicy": convert_str_to_bool(self.block_public_policy),
+ "restrictPublicBuckets": convert_str_to_bool(self.restrict_public_buckets),
+ }
+
+
+def convert_str_to_bool(item):
+ """Converts a boolean string to a boolean value"""
+ if isinstance(item, str):
+ return item.lower() == "true"
+
+ return False
+
+
+class S3ControlBackend(BaseBackend):
+ """Implementation of S3Control APIs."""
+
+ def __init__(self, region_name):
+ self.initialized = defaultdict()
+ self.region_name = region_name
+ self.public_access_block_configuration = defaultdict()
+
+ def reset(self):
+ """Re-initialize all attributes for this instance."""
+ region_name = self.region_name
+ self.__dict__ = {}
+ self.__init__(region_name)
+
+ @staticmethod
+ def get_public_access_block(account_id):
+ if account_id not in s3control_backends:
+ raise WrongPublicAccessBlockAccountIdError()
+ s3control_backend = s3control_backends[account_id]
+ if not s3control_backend.initialized or s3control_backend is None:
+ raise NoSuchPublicAccessBlockConfiguration()
+ return s3control_backend.public_access_block_configuration
+
+ def put_public_access_block(self, account_id, public_access_block_configuration):
+ if account_id not in s3control_backends:
+ s3control_backends[account_id] = S3ControlBackend(self.region_name)
+ s3control_backend = s3control_backends[account_id]
+ else:
+ s3control_backend = s3control_backends[account_id]
+ s3control_backend.initialized = True
+ s3control_backend.public_access_block_configuration = PublicAccessBlock(
+ public_access_block_configuration.get("BlockPublicAcls"),
+ public_access_block_configuration.get("IgnorePublicAcls"),
+ public_access_block_configuration.get("BlockPublicPolicy"),
+ public_access_block_configuration.get("RestrictPublicBuckets"),
+ )
+
+ @staticmethod
+ def delete_public_access_block(account_id):
+ s3control_backend = s3control_backends[account_id]
+ s3control_backend.initialized = False
+
+
+s3control_backends = {}
+for available_region in Session().get_available_regions("s3control"):
+ s3control_backends[available_region] = S3ControlBackend(available_region)
+for available_region in Session().get_available_regions(
+ "s3control", partition_name="aws-us-gov"
+):
+ s3control_backends[available_region] = S3ControlBackend(available_region)
+for available_region in Session().get_available_regions(
+ "s3control", partition_name="aws-cn"
+):
+ s3control_backends[available_region] = S3ControlBackend(available_region)
diff --git a/moto/s3control/responses.py b/moto/s3control/responses.py
new file mode 100644
index 000000000000..4bf85d43d01e
--- /dev/null
+++ b/moto/s3control/responses.py
@@ -0,0 +1,61 @@
+"""Handles incoming s3control requests, invokes methods, returns responses."""
+import json
+
+import xmltodict
+
+from moto.core.responses import BaseResponse
+from .models import s3control_backends
+
+
+class S3ControlResponse(BaseResponse):
+ SERVICE_NAME = "s3control"
+ """Handler for S3Control requests and responses."""
+
+ @property
+ def s3control_backend(self):
+ """Return backend instance specific for this region."""
+ return s3control_backends[self.region]
+
+ # add methods from here
+
+ def get_public_access_block(self):
+ account_id = self.headers["x-amz-account-id"]
+ public_access_block_configuration = self.s3control_backend.get_public_access_block(
+ account_id=account_id,
+ )
+ template = self.response_template(GET_PUBLIC_ACCESS_BLOCK_TEMPLATE)
+ return template.render(
+ BlockPublicAcls=public_access_block_configuration.block_public_acls,
+ IgnorePublicAcls=public_access_block_configuration.ignore_public_acls,
+ BlockPublicPolicy=public_access_block_configuration.block_public_policy,
+ RestrictPublicBuckets=public_access_block_configuration.restrict_public_buckets,
+ )
+
+ def put_public_access_block(self):
+ account_id = self.headers["x-amz-account-id"]
+ pab_config = self._parse_pab_config(self.body)
+ self.s3control_backend.put_public_access_block(
+ account_id, pab_config["PublicAccessBlockConfiguration"]
+ )
+ return json.dumps({})
+
+ def delete_public_access_block(self):
+ account_id = self.headers["x-amz-account-id"]
+ self.s3control_backend.delete_public_access_block(account_id=account_id,)
+ return json.dumps({})
+
+ def _parse_pab_config(self, body):
+ parsed_xml = xmltodict.parse(body)
+ parsed_xml["PublicAccessBlockConfiguration"].pop("@xmlns", None)
+
+ return parsed_xml
+
+
+GET_PUBLIC_ACCESS_BLOCK_TEMPLATE = """
+
+ {{ BlockPublicAcls }}
+ {{ IgnorePublicAcls }}
+ {{ BlockPublicPolicy }}
+ {{ RestrictPublicBuckets }}
+
+"""
diff --git a/moto/s3control/urls.py b/moto/s3control/urls.py
new file mode 100644
index 000000000000..8e428f3ede27
--- /dev/null
+++ b/moto/s3control/urls.py
@@ -0,0 +1,10 @@
+"""s3control base URL and path."""
+from .responses import S3ControlResponse
+
+url_bases = [
+ r"https?://(.+)\.s3-control\.(.+)\.amazonaws\.com",
+]
+
+url_paths = {
+ "{0}/v20180820/configuration/publicAccessBlock$": S3ControlResponse.dispatch
+}
diff --git a/tests/test_s3control/__init__.py b/tests/test_s3control/__init__.py
new file mode 100644
index 000000000000..e69de29bb2d1
diff --git a/tests/test_s3control/test_s3control.py b/tests/test_s3control/test_s3control.py
new file mode 100644
index 000000000000..509de4c7f5e3
--- /dev/null
+++ b/tests/test_s3control/test_s3control.py
@@ -0,0 +1,105 @@
+"""Unit tests for s3control-supported APIs."""
+import logging
+
+import boto3
+import pytest
+import sure # noqa # pylint: disable=unused-import
+from botocore.exceptions import ClientError
+
+from moto import mock_s3control, mock_organizations
+
+logger = logging.getLogger(__name__)
+
+
+# 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
+
+
+def standard_organization_with_account(account_name="mock-account"):
+ client = boto3.client("organizations", region_name="eu-central-1")
+ client.create_organization(FeatureSet="ALL")
+ create_status = client.create_account(
+ AccountName=account_name, Email="mock@email.com"
+ )["CreateAccountStatus"]
+ return create_status["AccountId"]
+
+
+@mock_organizations
+@mock_s3control
+def test_get_public_access_block():
+ account_id = standard_organization_with_account()
+ client = boto3.client("s3control", region_name="eu-west-1")
+ client.put_public_access_block(
+ AccountId=account_id,
+ PublicAccessBlockConfiguration={
+ "BlockPublicAcls": True,
+ "IgnorePublicAcls": True,
+ "BlockPublicPolicy": False,
+ "RestrictPublicBuckets": False,
+ },
+ )
+ response = client.get_public_access_block(AccountId=account_id)
+ response["PublicAccessBlockConfiguration"].should.have.key("BlockPublicAcls").equal(
+ True
+ )
+ response["PublicAccessBlockConfiguration"].should.have.key(
+ "IgnorePublicAcls"
+ ).equal(True)
+ response["PublicAccessBlockConfiguration"].should.have.key(
+ "BlockPublicPolicy"
+ ).equal(False)
+ response["PublicAccessBlockConfiguration"].should.have.key(
+ "RestrictPublicBuckets"
+ ).equal(False)
+
+ client.put_public_access_block(
+ AccountId=account_id,
+ PublicAccessBlockConfiguration={
+ "BlockPublicAcls": True,
+ "IgnorePublicAcls": True,
+ "BlockPublicPolicy": True,
+ "RestrictPublicBuckets": True,
+ },
+ )
+
+ response = client.get_public_access_block(AccountId=account_id)
+ response["PublicAccessBlockConfiguration"].should.have.key("BlockPublicAcls").equal(
+ True
+ )
+ response["PublicAccessBlockConfiguration"].should.have.key(
+ "IgnorePublicAcls"
+ ).equal(True)
+ response["PublicAccessBlockConfiguration"].should.have.key(
+ "BlockPublicPolicy"
+ ).equal(True)
+ response["PublicAccessBlockConfiguration"].should.have.key(
+ "RestrictPublicBuckets"
+ ).equal(True)
+
+
+@mock_organizations
+@mock_s3control
+def test_delete_public_access_block():
+ account_id = standard_organization_with_account()
+ client = boto3.client("s3control", region_name="eu-west-1")
+ with pytest.raises(ClientError) as excinfo:
+ client.get_public_access_block(AccountId=account_id)
+ assert (
+ "An error occurred (AccessDenied) when calling the GetPublicAccessBlock operation"
+ in str(excinfo.value)
+ )
+ client.put_public_access_block(
+ AccountId=account_id,
+ PublicAccessBlockConfiguration={
+ "BlockPublicAcls": True,
+ "IgnorePublicAcls": True,
+ "BlockPublicPolicy": True,
+ "RestrictPublicBuckets": True,
+ },
+ )
+ client.get_public_access_block(AccountId=account_id)
+ client.delete_public_access_block(AccountId=account_id)
+
+ with pytest.raises(ClientError) as excinfo:
+ client.get_public_access_block(AccountId=account_id)
+ assert "The public access block configuration was not found" in str(excinfo.value)