Implemented checking if S3 action is permitted.

This commit is contained in:
acsbendi 2019-07-04 16:38:43 +02:00
parent 7ec8f85438
commit 5dbec8aee5
5 changed files with 260 additions and 38 deletions

View File

@ -1,8 +1,9 @@
import json
import re
from abc import ABC, abstractmethod
from enum import Enum
from botocore.auth import SigV4Auth
from botocore.auth import SigV4Auth, S3SigV4Auth
from botocore.awsrequest import AWSRequest
from botocore.credentials import Credentials
from moto.iam.models import ACCOUNT_ID, Policy
@ -10,6 +11,7 @@ from moto.iam.models import ACCOUNT_ID, Policy
from moto.iam import iam_backend
from moto.core.exceptions import SignatureDoesNotMatchError, AccessDeniedError, InvalidClientTokenIdError
from moto.s3.exceptions import BucketAccessDeniedError, S3AccessDeniedError
ACCESS_KEY_STORE = {
"AKIAJDULPKHCC4KGTYVA": {
@ -19,7 +21,7 @@ ACCESS_KEY_STORE = {
}
class IAMRequest:
class IAMRequestBase(ABC):
def __init__(self, method, path, data, headers):
print(f"Creating {IAMRequest.__name__} with method={method}, path={path}, data={data}, headers={headers}")
@ -56,12 +58,9 @@ class IAMRequest:
if not permitted:
self._raise_access_denied(iam_user_name)
@abstractmethod
def _raise_access_denied(self, iam_user_name):
raise AccessDeniedError(
account_id=ACCOUNT_ID,
iam_user_name=iam_user_name,
action=self._action
)
pass
@staticmethod
def _collect_policies_for_iam_user(iam_user_name):
@ -87,13 +86,9 @@ class IAMRequest:
return user_policies
def _create_auth(self):
if self._access_key not in ACCESS_KEY_STORE:
raise InvalidClientTokenIdError()
secret_key = ACCESS_KEY_STORE[self._access_key]["secret_access_key"]
credentials = Credentials(self._access_key, secret_key)
return SigV4Auth(credentials, self._service, self._region)
@abstractmethod
def _create_auth(self, credentials):
pass
@staticmethod
def _create_headers_for_aws_request(signed_headers, original_headers):
@ -112,7 +107,12 @@ class IAMRequest:
return request
def _calculate_signature(self):
auth = self._create_auth()
if self._access_key not in ACCESS_KEY_STORE:
raise InvalidClientTokenIdError()
secret_key = ACCESS_KEY_STORE[self._access_key]["secret_access_key"]
credentials = Credentials(self._access_key, secret_key)
auth = self._create_auth(credentials)
request = self._create_aws_request()
canonical_request = auth.canonical_request(request)
string_to_sign = auth.string_to_sign(request, canonical_request)
@ -123,6 +123,31 @@ class IAMRequest:
return string.partition(first_separator)[2].partition(second_separator)[0]
class IAMRequest(IAMRequestBase):
def _create_auth(self, credentials):
return SigV4Auth(credentials, self._service, self._region)
def _raise_access_denied(self, iam_user_name):
raise AccessDeniedError(
account_id=ACCOUNT_ID,
iam_user_name=iam_user_name,
action=self._action
)
class S3IAMRequest(IAMRequestBase):
def _create_auth(self, credentials):
return S3SigV4Auth(credentials, self._service, self._region)
def _raise_access_denied(self, _):
if "BucketName" in self._data:
raise BucketAccessDeniedError(bucket=self._data["BucketName"])
else:
raise S3AccessDeniedError()
class IAMPolicy:
def __init__(self, policy):

View File

@ -10,7 +10,7 @@ import io
import pytz
from moto.core.authentication import IAMRequest
from moto.core.authentication import IAMRequest, S3IAMRequest
from moto.core.exceptions import DryRunClientError
from jinja2 import Environment, DictLoader, TemplateNotFound
@ -111,8 +111,7 @@ class ActionAuthenticatorMixin(object):
INITIAL_NO_AUTH_ACTION_COUNT = int(os.environ.get("INITIAL_NO_AUTH_ACTION_COUNT", 999999999))
request_count = 0
def _authenticate_action(self):
iam_request = IAMRequest(method=self.method, path=self.path, data=self.querystring, headers=self.headers)
def _authenticate_action(self, iam_request):
iam_request.check_signature()
if ActionAuthenticatorMixin.request_count >= ActionAuthenticatorMixin.INITIAL_NO_AUTH_ACTION_COUNT:
@ -120,6 +119,14 @@ class ActionAuthenticatorMixin(object):
else:
ActionAuthenticatorMixin.request_count += 1
def _authenticate_normal_action(self):
iam_request = IAMRequest(method=self.method, path=self.path, data=self.data, headers=self.headers)
self._authenticate_action(iam_request)
def _authenticate_s3_action(self):
iam_request = S3IAMRequest(method=self.method, path=self.path, data=self.data, headers=self.headers)
self._authenticate_action(iam_request)
class BaseResponse(_TemplateEnvironmentMixin, ActionAuthenticatorMixin):
@ -185,6 +192,7 @@ class BaseResponse(_TemplateEnvironmentMixin, ActionAuthenticatorMixin):
self.uri = full_url
self.path = urlparse(full_url).path
self.querystring = querystring
self.data = querystring
self.method = request.method
self.region = self.get_region_from_url(request, full_url)
self.uri_match = None
@ -293,7 +301,7 @@ class BaseResponse(_TemplateEnvironmentMixin, ActionAuthenticatorMixin):
headers = self.response_headers
try:
self._authenticate_action()
self._authenticate_normal_action()
except HTTPException as http_error:
response = http_error.description, dict(status=http_error.code)
return self._send_response(headers, response)

View File

@ -199,3 +199,17 @@ class DuplicateTagKeys(S3ClientError):
"InvalidTag",
"Cannot provide multiple Tags with the same key",
*args, **kwargs)
class S3AccessDeniedError(S3ClientError):
code = 403
def __init__(self, *args, **kwargs):
super(S3AccessDeniedError, self).__init__('AccessDenied', 'Access Denied', *args, **kwargs)
class BucketAccessDeniedError(BucketError):
code = 403
def __init__(self, *args, **kwargs):
super(BucketAccessDeniedError, self).__init__('AccessDenied', 'Access Denied', *args, **kwargs)

View File

@ -3,13 +3,15 @@ from __future__ import unicode_literals
import re
import six
from werkzeug.exceptions import HTTPException
from moto.core.utils import str_to_rfc_1123_datetime
from six.moves.urllib.parse import parse_qs, urlparse, unquote
import xmltodict
from moto.packages.httpretty.core import HTTPrettyRequest
from moto.core.responses import _TemplateEnvironmentMixin
from moto.core.responses import _TemplateEnvironmentMixin, ActionAuthenticatorMixin
from moto.core.utils import path_url
from moto.s3bucket_path.utils import bucket_name_from_url as bucketpath_bucket_name_from_url, \
@ -25,6 +27,72 @@ from xml.dom import minidom
DEFAULT_REGION_NAME = 'us-east-1'
ACTION_MAP = {
"BUCKET": {
"GET": {
"uploads": "ListBucketMultipartUploads",
"location": "GetBucketLocation",
"lifecycle": "GetLifecycleConfiguration",
"versioning": "GetBucketVersioning",
"policy": "GetBucketPolicy",
"website": "GetBucketWebsite",
"acl": "GetBucketAcl",
"tagging": "GetBucketTagging",
"logging": "GetBucketLogging",
"cors": "GetBucketCORS",
"notification": "GetBucketNotification",
"accelerate": "GetAccelerateConfiguration",
"versions": "ListBucketVersions",
"DEFAULT": "ListBucket"
},
"PUT": {
"lifecycle": "PutLifecycleConfiguration",
"versioning": "PutBucketVersioning",
"policy": "PutBucketPolicy",
"website": "PutBucketWebsite",
"acl": "PutBucketAcl",
"tagging": "PutBucketTagging",
"logging": "PutBucketLogging",
"cors": "PutBucketCORS",
"notification": "PutBucketNotification",
"accelerate": "PutAccelerateConfiguration",
"DEFAULT": "CreateBucket"
},
"DELETE": {
"lifecycle": "PutLifecycleConfiguration",
"policy": "DeleteBucketPolicy",
"tagging": "PutBucketTagging",
"cors": "PutBucketCORS",
"DEFAULT": "DeleteBucket"
}
},
"KEY": {
"GET": {
"uploadId": "ListMultipartUploadParts",
"acl": "GetObjectAcl",
"tagging": "GetObjectTagging",
"versionId": "GetObjectVersion",
"DEFAULT": "GetObject"
},
"PUT": {
"acl": "PutObjectAcl",
"tagging": "PutObjectTagging",
"DEFAULT": "PutObject"
},
"DELETE": {
"uploadId": "AbortMultipartUpload",
"versionId": "DeleteObjectVersion",
"DEFAULT": " DeleteObject"
},
"POST": {
"uploads": "PutObject",
"restore": "RestoreObject",
"uploadId": "PutObject"
}
}
}
def parse_key_name(pth):
return pth.lstrip("/")
@ -37,17 +105,27 @@ def is_delete_keys(request, path, bucket_name):
)
class ResponseObject(_TemplateEnvironmentMixin):
class ResponseObject(_TemplateEnvironmentMixin, ActionAuthenticatorMixin):
def __init__(self, backend):
super(ResponseObject, self).__init__()
self.backend = backend
self.method = ""
self.path = ""
self.data = {}
self.headers = {}
@property
def should_autoescape(self):
return True
def all_buckets(self):
def all_buckets(self, headers):
try:
self.data["Action"] = "ListAllMyBuckets"
self._authenticate_s3_action()
except HTTPException as http_error:
response = http_error.code, headers, http_error.description
return self._send_response(response)
# No bucket specified. Listing all buckets
all_buckets = self.backend.get_all_buckets()
template = self.response_template(S3_ALL_BUCKETS)
@ -112,11 +190,18 @@ class ResponseObject(_TemplateEnvironmentMixin):
return self.bucket_response(request, full_url, headers)
def bucket_response(self, request, full_url, headers):
self.method = request.method
self.path = self._get_path(request)
self.headers = request.headers
try:
response = self._bucket_response(request, full_url, headers)
except S3ClientError as s3error:
response = s3error.code, {}, s3error.description
return self._send_response(response)
@staticmethod
def _send_response(response):
if isinstance(response, six.string_types):
return 200, {}, response.encode("utf-8")
else:
@ -124,18 +209,21 @@ class ResponseObject(_TemplateEnvironmentMixin):
if not isinstance(response_content, six.binary_type):
response_content = response_content.encode("utf-8")
print(f"response_content: {response_content}")
return status_code, headers, response_content
def _bucket_response(self, request, full_url, headers):
parsed_url = urlparse(full_url)
querystring = parse_qs(parsed_url.query, keep_blank_values=True)
querystring = self._get_querystring(full_url)
method = request.method
region_name = parse_region_from_url(full_url)
bucket_name = self.parse_bucket_name_from_url(request, full_url)
if not bucket_name:
# If no bucket specified, list all buckets
return self.all_buckets()
return self.all_buckets(headers)
self.data["BucketName"] = bucket_name
if hasattr(request, 'body'):
# Boto
@ -163,6 +251,12 @@ class ResponseObject(_TemplateEnvironmentMixin):
raise NotImplementedError(
"Method {0} has not been impelemented in the S3 backend yet".format(method))
@staticmethod
def _get_querystring(full_url):
parsed_url = urlparse(full_url)
querystring = parse_qs(parsed_url.query, keep_blank_values=True)
return querystring
def _bucket_response_head(self, bucket_name, headers):
try:
self.backend.get_bucket(bucket_name)
@ -175,6 +269,14 @@ class ResponseObject(_TemplateEnvironmentMixin):
return 200, {}, ""
def _bucket_response_get(self, bucket_name, querystring, headers):
self._set_action("BUCKET", "GET", querystring)
try:
self._authenticate_s3_action()
except HTTPException as http_error:
response = http_error.code, headers, http_error.description
return self._send_response(response)
if 'uploads' in querystring:
for unsup in ('delimiter', 'max-uploads'):
if unsup in querystring:
@ -333,6 +435,15 @@ class ResponseObject(_TemplateEnvironmentMixin):
max_keys=max_keys
)
def _set_action(self, action_resource_type, method, querystring):
action_set = False
for action_in_querystring, action in ACTION_MAP[action_resource_type][method].items():
if action_in_querystring in querystring:
self.data["Action"] = action
action_set = True
if not action_set:
self.data["Action"] = ACTION_MAP[action_resource_type][method]["DEFAULT"]
def _handle_list_objects_v2(self, bucket_name, querystring):
template = self.response_template(S3_BUCKET_GET_RESPONSE_V2)
bucket = self.backend.get_bucket(bucket_name)
@ -396,6 +507,15 @@ class ResponseObject(_TemplateEnvironmentMixin):
def _bucket_response_put(self, request, body, region_name, bucket_name, querystring, headers):
if not request.headers.get('Content-Length'):
return 411, {}, "Content-Length required"
self._set_action("BUCKET", "PUT", querystring)
try:
self._authenticate_s3_action()
except HTTPException as http_error:
response = http_error.code, headers, http_error.description
return self._send_response(response)
if 'versioning' in querystring:
ver = re.search('<Status>([A-Za-z]+)</Status>', body.decode())
if ver:
@ -495,6 +615,14 @@ class ResponseObject(_TemplateEnvironmentMixin):
return 200, {}, template.render(bucket=new_bucket)
def _bucket_response_delete(self, body, bucket_name, querystring, headers):
self._set_action("BUCKET", "DELETE", querystring)
try:
self._authenticate_s3_action()
except HTTPException as http_error:
response = http_error.code, headers, http_error.description
return self._send_response(response)
if 'policy' in querystring:
self.backend.delete_bucket_policy(bucket_name, body)
return 204, {}, ""
@ -525,14 +653,27 @@ class ResponseObject(_TemplateEnvironmentMixin):
if not request.headers.get('Content-Length'):
return 411, {}, "Content-Length required"
if isinstance(request, HTTPrettyRequest):
path = request.path
else:
path = request.full_path if hasattr(request, 'full_path') else path_url(request.url)
path = self._get_path(request)
if self.is_delete_keys(request, path, bucket_name):
self.data["Action"] = "DeleteObject"
try:
self._authenticate_s3_action()
except HTTPException as http_error:
response = http_error.code, headers, http_error.description
return self._send_response(response)
return self._bucket_response_delete_keys(request, body, bucket_name, headers)
self.data["Action"] = "PutObject"
try:
self._authenticate_s3_action()
except HTTPException as http_error:
response = http_error.code, headers, http_error.description
return self._send_response(response)
# POST to bucket-url should create file from form
if hasattr(request, 'form'):
# Not HTTPretty
@ -560,6 +701,14 @@ class ResponseObject(_TemplateEnvironmentMixin):
return 200, {}, ""
@staticmethod
def _get_path(request):
if isinstance(request, HTTPrettyRequest):
path = request.path
else:
path = request.full_path if hasattr(request, 'full_path') else path_url(request.url)
return path
def _bucket_response_delete_keys(self, request, body, bucket_name, headers):
template = self.response_template(S3_DELETE_KEYS_RESPONSE)
@ -604,6 +753,9 @@ class ResponseObject(_TemplateEnvironmentMixin):
return 206, response_headers, response_content[begin:end + 1]
def key_response(self, request, full_url, headers):
self.method = request.method
self.path = self._get_path(request)
self.headers = request.headers
response_headers = {}
try:
response = self._key_response(request, full_url, headers)
@ -671,6 +823,14 @@ class ResponseObject(_TemplateEnvironmentMixin):
"Method {0} has not been implemented in the S3 backend yet".format(method))
def _key_response_get(self, bucket_name, query, key_name, headers):
self._set_action("KEY", "GET", query)
try:
self._authenticate_s3_action()
except HTTPException as http_error:
response = http_error.code, headers, http_error.description
return self._send_response(response)
response_headers = {}
if query.get('uploadId'):
upload_id = query['uploadId'][0]
@ -700,6 +860,14 @@ class ResponseObject(_TemplateEnvironmentMixin):
return 200, response_headers, key.value
def _key_response_put(self, request, body, bucket_name, query, key_name, headers):
self._set_action("KEY", "PUT", query)
try:
self._authenticate_s3_action()
except HTTPException as http_error:
response = http_error.code, headers, http_error.description
return self._send_response(response)
response_headers = {}
if query.get('uploadId') and query.get('partNumber'):
upload_id = query['uploadId'][0]
@ -1067,6 +1235,14 @@ class ResponseObject(_TemplateEnvironmentMixin):
return config['Status']
def _key_response_delete(self, bucket_name, query, key_name, headers):
self._set_action("KEY", "DELETE", query)
try:
self._authenticate_s3_action()
except HTTPException as http_error:
response = http_error.code, headers, http_error.description
return self._send_response(response)
if query.get('uploadId'):
upload_id = query['uploadId'][0]
self.backend.cancel_multipart(bucket_name, upload_id)
@ -1087,6 +1263,14 @@ class ResponseObject(_TemplateEnvironmentMixin):
yield (pn, p.getElementsByTagName('ETag')[0].firstChild.wholeText)
def _key_response_post(self, request, body, bucket_name, query, key_name, headers):
self._set_action("KEY", "POST", query)
try:
self._authenticate_s3_action()
except HTTPException as http_error:
response = http_error.code, headers, http_error.description
return self._send_response(response)
if body == b'' and 'uploads' in query:
metadata = metadata_from_headers(request.headers)
multipart = self.backend.initiate_multipart(

View File

@ -7,15 +7,6 @@ url_bases = [
r"https?://(?P<bucket_name>[a-zA-Z0-9\-_.]*)\.?s3(.*).amazonaws.com"
]
def ambiguous_response1(*args, **kwargs):
return S3ResponseInstance.ambiguous_response(*args, **kwargs)
def ambiguous_response2(*args, **kwargs):
return S3ResponseInstance.ambiguous_response(*args, **kwargs)
url_paths = {
# subdomain bucket
'{0}/$': S3ResponseInstance.bucket_response,