S3: Allow specific resource in IAM access policy (#6637)
This commit is contained in:
parent
3682cc633b
commit
4179de8a61
@ -128,7 +128,9 @@ class _TemplateEnvironmentMixin(object):
|
|||||||
class ActionAuthenticatorMixin(object):
|
class ActionAuthenticatorMixin(object):
|
||||||
request_count: ClassVar[int] = 0
|
request_count: ClassVar[int] = 0
|
||||||
|
|
||||||
def _authenticate_and_authorize_action(self, iam_request_cls: type) -> None:
|
def _authenticate_and_authorize_action(
|
||||||
|
self, iam_request_cls: type, resource: str = "*"
|
||||||
|
) -> None:
|
||||||
if (
|
if (
|
||||||
ActionAuthenticatorMixin.request_count
|
ActionAuthenticatorMixin.request_count
|
||||||
>= settings.INITIAL_NO_AUTH_ACTION_COUNT
|
>= settings.INITIAL_NO_AUTH_ACTION_COUNT
|
||||||
@ -145,7 +147,7 @@ class ActionAuthenticatorMixin(object):
|
|||||||
headers=self.headers, # type: ignore[attr-defined]
|
headers=self.headers, # type: ignore[attr-defined]
|
||||||
)
|
)
|
||||||
iam_request.check_signature()
|
iam_request.check_signature()
|
||||||
iam_request.check_action_permitted()
|
iam_request.check_action_permitted(resource)
|
||||||
else:
|
else:
|
||||||
ActionAuthenticatorMixin.request_count += 1
|
ActionAuthenticatorMixin.request_count += 1
|
||||||
|
|
||||||
@ -154,10 +156,15 @@ class ActionAuthenticatorMixin(object):
|
|||||||
|
|
||||||
self._authenticate_and_authorize_action(IAMRequest)
|
self._authenticate_and_authorize_action(IAMRequest)
|
||||||
|
|
||||||
def _authenticate_and_authorize_s3_action(self) -> None:
|
def _authenticate_and_authorize_s3_action(
|
||||||
|
self, bucket_name: Optional[str] = None, key_name: Optional[str] = None
|
||||||
|
) -> None:
|
||||||
|
arn = f"{bucket_name or '*'}/{key_name}" if key_name else (bucket_name or "*")
|
||||||
|
resource = f"arn:aws:s3:::{arn}"
|
||||||
|
|
||||||
from moto.iam.access_control import S3IAMRequest
|
from moto.iam.access_control import S3IAMRequest
|
||||||
|
|
||||||
self._authenticate_and_authorize_action(S3IAMRequest)
|
self._authenticate_and_authorize_action(S3IAMRequest, resource)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def set_initial_no_auth_action_count(initial_no_auth_action_count: int) -> Callable[..., Callable[..., TYPE_RESPONSE]]: # type: ignore[misc]
|
def set_initial_no_auth_action_count(initial_no_auth_action_count: int) -> Callable[..., Callable[..., TYPE_RESPONSE]]: # type: ignore[misc]
|
||||||
|
@ -216,7 +216,7 @@ class IAMRequestBase(object, metaclass=ABCMeta):
|
|||||||
if original_signature != calculated_signature:
|
if original_signature != calculated_signature:
|
||||||
self._raise_signature_does_not_match()
|
self._raise_signature_does_not_match()
|
||||||
|
|
||||||
def check_action_permitted(self) -> None:
|
def check_action_permitted(self, resource: str) -> None:
|
||||||
if (
|
if (
|
||||||
self._action == "sts:GetCallerIdentity"
|
self._action == "sts:GetCallerIdentity"
|
||||||
): # always allowed, even if there's an explicit Deny for it
|
): # always allowed, even if there's an explicit Deny for it
|
||||||
@ -226,7 +226,7 @@ class IAMRequestBase(object, metaclass=ABCMeta):
|
|||||||
permitted = False
|
permitted = False
|
||||||
for policy in policies:
|
for policy in policies:
|
||||||
iam_policy = IAMPolicy(policy)
|
iam_policy = IAMPolicy(policy)
|
||||||
permission_result = iam_policy.is_action_permitted(self._action)
|
permission_result = iam_policy.is_action_permitted(self._action, resource)
|
||||||
if permission_result == PermissionResult.DENIED:
|
if permission_result == PermissionResult.DENIED:
|
||||||
self._raise_access_denied()
|
self._raise_access_denied()
|
||||||
elif permission_result == PermissionResult.PERMITTED:
|
elif permission_result == PermissionResult.PERMITTED:
|
||||||
|
@ -353,7 +353,7 @@ class S3Response(BaseResponse):
|
|||||||
self, bucket_name: str, querystring: Dict[str, Any]
|
self, bucket_name: str, querystring: Dict[str, Any]
|
||||||
) -> TYPE_RESPONSE:
|
) -> TYPE_RESPONSE:
|
||||||
self._set_action("BUCKET", "HEAD", querystring)
|
self._set_action("BUCKET", "HEAD", querystring)
|
||||||
self._authenticate_and_authorize_s3_action()
|
self._authenticate_and_authorize_s3_action(bucket_name=bucket_name)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
bucket = self.backend.head_bucket(bucket_name)
|
bucket = self.backend.head_bucket(bucket_name)
|
||||||
@ -415,7 +415,7 @@ class S3Response(BaseResponse):
|
|||||||
self, headers: Dict[str, str], bucket_name: str
|
self, headers: Dict[str, str], bucket_name: str
|
||||||
) -> TYPE_RESPONSE:
|
) -> TYPE_RESPONSE:
|
||||||
# Return 200 with the headers from the bucket CORS configuration
|
# Return 200 with the headers from the bucket CORS configuration
|
||||||
self._authenticate_and_authorize_s3_action()
|
self._authenticate_and_authorize_s3_action(bucket_name=bucket_name)
|
||||||
try:
|
try:
|
||||||
bucket = self.backend.head_bucket(bucket_name)
|
bucket = self.backend.head_bucket(bucket_name)
|
||||||
except MissingBucket:
|
except MissingBucket:
|
||||||
@ -477,7 +477,7 @@ class S3Response(BaseResponse):
|
|||||||
self, bucket_name: str, querystring: Dict[str, Any]
|
self, bucket_name: str, querystring: Dict[str, Any]
|
||||||
) -> Union[str, TYPE_RESPONSE]:
|
) -> Union[str, TYPE_RESPONSE]:
|
||||||
self._set_action("BUCKET", "GET", querystring)
|
self._set_action("BUCKET", "GET", querystring)
|
||||||
self._authenticate_and_authorize_s3_action()
|
self._authenticate_and_authorize_s3_action(bucket_name=bucket_name)
|
||||||
|
|
||||||
if "object-lock" in querystring:
|
if "object-lock" in querystring:
|
||||||
(
|
(
|
||||||
@ -822,7 +822,7 @@ class S3Response(BaseResponse):
|
|||||||
return 411, {}, "Content-Length required"
|
return 411, {}, "Content-Length required"
|
||||||
|
|
||||||
self._set_action("BUCKET", "PUT", querystring)
|
self._set_action("BUCKET", "PUT", querystring)
|
||||||
self._authenticate_and_authorize_s3_action()
|
self._authenticate_and_authorize_s3_action(bucket_name=bucket_name)
|
||||||
|
|
||||||
if "object-lock" in querystring:
|
if "object-lock" in querystring:
|
||||||
config = self._lock_config_from_body()
|
config = self._lock_config_from_body()
|
||||||
@ -1008,7 +1008,7 @@ class S3Response(BaseResponse):
|
|||||||
self, bucket_name: str, querystring: Dict[str, Any]
|
self, bucket_name: str, querystring: Dict[str, Any]
|
||||||
) -> TYPE_RESPONSE:
|
) -> TYPE_RESPONSE:
|
||||||
self._set_action("BUCKET", "DELETE", querystring)
|
self._set_action("BUCKET", "DELETE", querystring)
|
||||||
self._authenticate_and_authorize_s3_action()
|
self._authenticate_and_authorize_s3_action(bucket_name=bucket_name)
|
||||||
|
|
||||||
if "policy" in querystring:
|
if "policy" in querystring:
|
||||||
self.backend.delete_bucket_policy(bucket_name)
|
self.backend.delete_bucket_policy(bucket_name)
|
||||||
@ -1059,7 +1059,7 @@ class S3Response(BaseResponse):
|
|||||||
if self.is_delete_keys():
|
if self.is_delete_keys():
|
||||||
self.data["Action"] = "DeleteObject"
|
self.data["Action"] = "DeleteObject"
|
||||||
try:
|
try:
|
||||||
self._authenticate_and_authorize_s3_action()
|
self._authenticate_and_authorize_s3_action(bucket_name=bucket_name)
|
||||||
return self._bucket_response_delete_keys(bucket_name)
|
return self._bucket_response_delete_keys(bucket_name)
|
||||||
except BucketAccessDeniedError:
|
except BucketAccessDeniedError:
|
||||||
return self._bucket_response_delete_keys(
|
return self._bucket_response_delete_keys(
|
||||||
@ -1067,7 +1067,7 @@ class S3Response(BaseResponse):
|
|||||||
)
|
)
|
||||||
|
|
||||||
self.data["Action"] = "PutObject"
|
self.data["Action"] = "PutObject"
|
||||||
self._authenticate_and_authorize_s3_action()
|
self._authenticate_and_authorize_s3_action(bucket_name=bucket_name)
|
||||||
|
|
||||||
# POST to bucket-url should create file from form
|
# POST to bucket-url should create file from form
|
||||||
form = request.form
|
form = request.form
|
||||||
@ -1359,7 +1359,9 @@ class S3Response(BaseResponse):
|
|||||||
headers: Dict[str, Any],
|
headers: Dict[str, Any],
|
||||||
) -> TYPE_RESPONSE:
|
) -> TYPE_RESPONSE:
|
||||||
self._set_action("KEY", "GET", query)
|
self._set_action("KEY", "GET", query)
|
||||||
self._authenticate_and_authorize_s3_action()
|
self._authenticate_and_authorize_s3_action(
|
||||||
|
bucket_name=bucket_name, key_name=key_name
|
||||||
|
)
|
||||||
|
|
||||||
response_headers = self._get_cors_headers_other(headers, bucket_name)
|
response_headers = self._get_cors_headers_other(headers, bucket_name)
|
||||||
if query.get("uploadId"):
|
if query.get("uploadId"):
|
||||||
@ -1476,7 +1478,9 @@ class S3Response(BaseResponse):
|
|||||||
key_name: str,
|
key_name: str,
|
||||||
) -> TYPE_RESPONSE:
|
) -> TYPE_RESPONSE:
|
||||||
self._set_action("KEY", "PUT", query)
|
self._set_action("KEY", "PUT", query)
|
||||||
self._authenticate_and_authorize_s3_action()
|
self._authenticate_and_authorize_s3_action(
|
||||||
|
bucket_name=bucket_name, key_name=key_name
|
||||||
|
)
|
||||||
|
|
||||||
response_headers = self._get_cors_headers_other(request.headers, bucket_name)
|
response_headers = self._get_cors_headers_other(request.headers, bucket_name)
|
||||||
if query.get("uploadId") and query.get("partNumber"):
|
if query.get("uploadId") and query.get("partNumber"):
|
||||||
@ -1748,7 +1752,9 @@ class S3Response(BaseResponse):
|
|||||||
headers: Dict[str, Any],
|
headers: Dict[str, Any],
|
||||||
) -> TYPE_RESPONSE:
|
) -> TYPE_RESPONSE:
|
||||||
self._set_action("KEY", "HEAD", query)
|
self._set_action("KEY", "HEAD", query)
|
||||||
self._authenticate_and_authorize_s3_action()
|
self._authenticate_and_authorize_s3_action(
|
||||||
|
bucket_name=bucket_name, key_name=key_name
|
||||||
|
)
|
||||||
|
|
||||||
response_headers: Dict[str, Any] = {}
|
response_headers: Dict[str, Any] = {}
|
||||||
version_id = query.get("versionId", [None])[0]
|
version_id = query.get("versionId", [None])[0]
|
||||||
@ -2147,7 +2153,9 @@ class S3Response(BaseResponse):
|
|||||||
self, headers: Any, bucket_name: str, query: Dict[str, Any], key_name: str
|
self, headers: Any, bucket_name: str, query: Dict[str, Any], key_name: str
|
||||||
) -> TYPE_RESPONSE:
|
) -> TYPE_RESPONSE:
|
||||||
self._set_action("KEY", "DELETE", query)
|
self._set_action("KEY", "DELETE", query)
|
||||||
self._authenticate_and_authorize_s3_action()
|
self._authenticate_and_authorize_s3_action(
|
||||||
|
bucket_name=bucket_name, key_name=key_name
|
||||||
|
)
|
||||||
|
|
||||||
if query.get("uploadId"):
|
if query.get("uploadId"):
|
||||||
upload_id = query["uploadId"][0]
|
upload_id = query["uploadId"][0]
|
||||||
@ -2188,7 +2196,9 @@ class S3Response(BaseResponse):
|
|||||||
key_name: str,
|
key_name: str,
|
||||||
) -> TYPE_RESPONSE:
|
) -> TYPE_RESPONSE:
|
||||||
self._set_action("KEY", "POST", query)
|
self._set_action("KEY", "POST", query)
|
||||||
self._authenticate_and_authorize_s3_action()
|
self._authenticate_and_authorize_s3_action(
|
||||||
|
bucket_name=bucket_name, key_name=key_name
|
||||||
|
)
|
||||||
|
|
||||||
encryption = request.headers.get("x-amz-server-side-encryption")
|
encryption = request.headers.get("x-amz-server-side-encryption")
|
||||||
kms_key_id = request.headers.get("x-amz-server-side-encryption-aws-kms-key-id")
|
kms_key_id = request.headers.get("x-amz-server-side-encryption-aws-kms-key-id")
|
||||||
|
@ -766,3 +766,68 @@ def test_s3_invalid_token_with_temporary_credentials():
|
|||||||
assert err["Code"] == "InvalidToken"
|
assert err["Code"] == "InvalidToken"
|
||||||
assert ex.value.response["ResponseMetadata"]["HTTPStatusCode"] == 400
|
assert ex.value.response["ResponseMetadata"]["HTTPStatusCode"] == 400
|
||||||
assert err["Message"] == "The provided token is malformed or otherwise invalid."
|
assert err["Message"] == "The provided token is malformed or otherwise invalid."
|
||||||
|
|
||||||
|
|
||||||
|
@set_initial_no_auth_action_count(3)
|
||||||
|
@mock_s3
|
||||||
|
@mock_iam
|
||||||
|
def test_allow_bucket_access_using_resource_arn() -> None:
|
||||||
|
user_name = "test-user"
|
||||||
|
policy_doc = {
|
||||||
|
"Version": "2012-10-17",
|
||||||
|
"Statement": [
|
||||||
|
{
|
||||||
|
"Action": ["s3:*"],
|
||||||
|
"Effect": "Allow",
|
||||||
|
"Resource": "arn:aws:s3:::my_bucket",
|
||||||
|
"Sid": "BucketLevelGrants",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
access_key = create_user_with_access_key_and_inline_policy(user_name, policy_doc)
|
||||||
|
|
||||||
|
s3_client = boto3.client(
|
||||||
|
"s3",
|
||||||
|
region_name="us-east-1",
|
||||||
|
aws_access_key_id=access_key["AccessKeyId"],
|
||||||
|
aws_secret_access_key=access_key["SecretAccessKey"],
|
||||||
|
)
|
||||||
|
|
||||||
|
s3_client.create_bucket(Bucket="my_bucket")
|
||||||
|
with pytest.raises(ClientError):
|
||||||
|
s3_client.create_bucket(Bucket="my_bucket2")
|
||||||
|
|
||||||
|
s3_client.head_bucket(Bucket="my_bucket")
|
||||||
|
with pytest.raises(ClientError):
|
||||||
|
s3_client.head_bucket(Bucket="my_bucket2")
|
||||||
|
|
||||||
|
|
||||||
|
@set_initial_no_auth_action_count(3)
|
||||||
|
@mock_s3
|
||||||
|
@mock_iam
|
||||||
|
def test_allow_key_access_using_resource_arn() -> None:
|
||||||
|
user_name = "test-user"
|
||||||
|
policy_doc = {
|
||||||
|
"Version": "2012-10-17",
|
||||||
|
"Statement": [
|
||||||
|
{
|
||||||
|
"Action": ["s3:*"],
|
||||||
|
"Effect": "Allow",
|
||||||
|
"Resource": ["arn:aws:s3:::my_bucket", "arn:aws:s3:::*/keyname"],
|
||||||
|
"Sid": "KeyLevelGrants",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
access_key = create_user_with_access_key_and_inline_policy(user_name, policy_doc)
|
||||||
|
|
||||||
|
s3_client = boto3.client(
|
||||||
|
"s3",
|
||||||
|
region_name="us-east-1",
|
||||||
|
aws_access_key_id=access_key["AccessKeyId"],
|
||||||
|
aws_secret_access_key=access_key["SecretAccessKey"],
|
||||||
|
)
|
||||||
|
|
||||||
|
s3_client.create_bucket(Bucket="my_bucket")
|
||||||
|
s3_client.put_object(Bucket="my_bucket", Key="keyname", Body=b"test")
|
||||||
|
with pytest.raises(ClientError):
|
||||||
|
s3_client.put_object(Bucket="my_bucket", Key="unknown", Body=b"test")
|
||||||
|
Loading…
Reference in New Issue
Block a user