Merge pull request #2300 from acsbendi/master
Basic IAM authentication, authorization
This commit is contained in:
commit
69d86cbd54
14 changed files with 1460 additions and 50 deletions
|
|
@ -1,4 +1,7 @@
|
|||
from __future__ import unicode_literals
|
||||
|
||||
from .models import BaseModel, BaseBackend, moto_api_backend # flake8: noqa
|
||||
from .responses import ActionAuthenticatorMixin
|
||||
|
||||
moto_api_backends = {"global": moto_api_backend}
|
||||
set_initial_no_auth_action_count = ActionAuthenticatorMixin.set_initial_no_auth_action_count
|
||||
|
|
|
|||
363
moto/core/access_control.py
Normal file
363
moto/core/access_control.py
Normal file
|
|
@ -0,0 +1,363 @@
|
|||
"""
|
||||
This implementation is NOT complete, there are many things to improve.
|
||||
The following is a list of the most important missing features and inaccuracies.
|
||||
|
||||
TODO add support for more principals, apart from IAM users and assumed IAM roles
|
||||
TODO add support for the Resource and Condition parts of IAM policies
|
||||
TODO add support and create tests for all services in moto (for example, API Gateway is probably not supported currently)
|
||||
TODO implement service specific error messages (currently, EC2 and S3 are supported separately, everything else defaults to the errors IAM returns)
|
||||
TODO include information about the action's resource in error messages (once the Resource element in IAM policies is supported)
|
||||
TODO check all other actions that are performed by the action called by the user (for example, autoscaling:CreateAutoScalingGroup requires permission for iam:CreateServiceLinkedRole too - see https://docs.aws.amazon.com/autoscaling/ec2/userguide/control-access-using-iam.html)
|
||||
TODO add support for resource-based policies
|
||||
|
||||
"""
|
||||
|
||||
import json
|
||||
import logging
|
||||
import re
|
||||
from abc import abstractmethod, ABCMeta
|
||||
from enum import Enum
|
||||
|
||||
import six
|
||||
from botocore.auth import SigV4Auth, S3SigV4Auth
|
||||
from botocore.awsrequest import AWSRequest
|
||||
from botocore.credentials import Credentials
|
||||
from six import string_types
|
||||
|
||||
from moto.iam.models import ACCOUNT_ID, Policy
|
||||
from moto.iam import iam_backend
|
||||
from moto.core.exceptions import SignatureDoesNotMatchError, AccessDeniedError, InvalidClientTokenIdError, AuthFailureError
|
||||
from moto.s3.exceptions import (
|
||||
BucketAccessDeniedError,
|
||||
S3AccessDeniedError,
|
||||
BucketInvalidTokenError,
|
||||
S3InvalidTokenError,
|
||||
S3InvalidAccessKeyIdError,
|
||||
BucketInvalidAccessKeyIdError,
|
||||
BucketSignatureDoesNotMatchError,
|
||||
S3SignatureDoesNotMatchError
|
||||
)
|
||||
from moto.sts import sts_backend
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def create_access_key(access_key_id, headers):
|
||||
if access_key_id.startswith("AKIA") or "X-Amz-Security-Token" not in headers:
|
||||
return IAMUserAccessKey(access_key_id, headers)
|
||||
else:
|
||||
return AssumedRoleAccessKey(access_key_id, headers)
|
||||
|
||||
|
||||
class IAMUserAccessKey(object):
|
||||
|
||||
def __init__(self, access_key_id, headers):
|
||||
iam_users = iam_backend.list_users('/', None, None)
|
||||
for iam_user in iam_users:
|
||||
for access_key in iam_user.access_keys:
|
||||
if access_key.access_key_id == access_key_id:
|
||||
self._owner_user_name = iam_user.name
|
||||
self._access_key_id = access_key_id
|
||||
self._secret_access_key = access_key.secret_access_key
|
||||
if "X-Amz-Security-Token" in headers:
|
||||
raise CreateAccessKeyFailure(reason="InvalidToken")
|
||||
return
|
||||
raise CreateAccessKeyFailure(reason="InvalidId")
|
||||
|
||||
@property
|
||||
def arn(self):
|
||||
return "arn:aws:iam::{account_id}:user/{iam_user_name}".format(
|
||||
account_id=ACCOUNT_ID,
|
||||
iam_user_name=self._owner_user_name
|
||||
)
|
||||
|
||||
def create_credentials(self):
|
||||
return Credentials(self._access_key_id, self._secret_access_key)
|
||||
|
||||
def collect_policies(self):
|
||||
user_policies = []
|
||||
|
||||
inline_policy_names = iam_backend.list_user_policies(self._owner_user_name)
|
||||
for inline_policy_name in inline_policy_names:
|
||||
inline_policy = iam_backend.get_user_policy(self._owner_user_name, inline_policy_name)
|
||||
user_policies.append(inline_policy)
|
||||
|
||||
attached_policies, _ = iam_backend.list_attached_user_policies(self._owner_user_name)
|
||||
user_policies += attached_policies
|
||||
|
||||
user_groups = iam_backend.get_groups_for_user(self._owner_user_name)
|
||||
for user_group in user_groups:
|
||||
inline_group_policy_names = iam_backend.list_group_policies(user_group.name)
|
||||
for inline_group_policy_name in inline_group_policy_names:
|
||||
inline_user_group_policy = iam_backend.get_group_policy(user_group.name, inline_group_policy_name)
|
||||
user_policies.append(inline_user_group_policy)
|
||||
|
||||
attached_group_policies, _ = iam_backend.list_attached_group_policies(user_group.name)
|
||||
user_policies += attached_group_policies
|
||||
|
||||
return user_policies
|
||||
|
||||
|
||||
class AssumedRoleAccessKey(object):
|
||||
|
||||
def __init__(self, access_key_id, headers):
|
||||
for assumed_role in sts_backend.assumed_roles:
|
||||
if assumed_role.access_key_id == access_key_id:
|
||||
self._access_key_id = access_key_id
|
||||
self._secret_access_key = assumed_role.secret_access_key
|
||||
self._session_token = assumed_role.session_token
|
||||
self._owner_role_name = assumed_role.arn.split("/")[-1]
|
||||
self._session_name = assumed_role.session_name
|
||||
if headers["X-Amz-Security-Token"] != self._session_token:
|
||||
raise CreateAccessKeyFailure(reason="InvalidToken")
|
||||
return
|
||||
raise CreateAccessKeyFailure(reason="InvalidId")
|
||||
|
||||
@property
|
||||
def arn(self):
|
||||
return "arn:aws:sts::{account_id}:assumed-role/{role_name}/{session_name}".format(
|
||||
account_id=ACCOUNT_ID,
|
||||
role_name=self._owner_role_name,
|
||||
session_name=self._session_name
|
||||
)
|
||||
|
||||
def create_credentials(self):
|
||||
return Credentials(self._access_key_id, self._secret_access_key, self._session_token)
|
||||
|
||||
def collect_policies(self):
|
||||
role_policies = []
|
||||
|
||||
inline_policy_names = iam_backend.list_role_policies(self._owner_role_name)
|
||||
for inline_policy_name in inline_policy_names:
|
||||
_, inline_policy = iam_backend.get_role_policy(self._owner_role_name, inline_policy_name)
|
||||
role_policies.append(inline_policy)
|
||||
|
||||
attached_policies, _ = iam_backend.list_attached_role_policies(self._owner_role_name)
|
||||
role_policies += attached_policies
|
||||
|
||||
return role_policies
|
||||
|
||||
|
||||
class CreateAccessKeyFailure(Exception):
|
||||
|
||||
def __init__(self, reason, *args):
|
||||
super(CreateAccessKeyFailure, self).__init__(*args)
|
||||
self.reason = reason
|
||||
|
||||
|
||||
@six.add_metaclass(ABCMeta)
|
||||
class IAMRequestBase(object):
|
||||
|
||||
def __init__(self, method, path, data, headers):
|
||||
log.debug("Creating {class_name} with method={method}, path={path}, data={data}, headers={headers}".format(
|
||||
class_name=self.__class__.__name__, method=method, path=path, data=data, headers=headers))
|
||||
self._method = method
|
||||
self._path = path
|
||||
self._data = data
|
||||
self._headers = headers
|
||||
credential_scope = self._get_string_between('Credential=', ',', self._headers['Authorization'])
|
||||
credential_data = credential_scope.split('/')
|
||||
self._region = credential_data[2]
|
||||
self._service = credential_data[3]
|
||||
self._action = self._service + ":" + (self._data["Action"][0] if isinstance(self._data["Action"], list) else self._data["Action"])
|
||||
try:
|
||||
self._access_key = create_access_key(access_key_id=credential_data[0], headers=headers)
|
||||
except CreateAccessKeyFailure as e:
|
||||
self._raise_invalid_access_key(e.reason)
|
||||
|
||||
def check_signature(self):
|
||||
original_signature = self._get_string_between('Signature=', ',', self._headers['Authorization'])
|
||||
calculated_signature = self._calculate_signature()
|
||||
if original_signature != calculated_signature:
|
||||
self._raise_signature_does_not_match()
|
||||
|
||||
def check_action_permitted(self):
|
||||
policies = self._access_key.collect_policies()
|
||||
|
||||
permitted = False
|
||||
for policy in policies:
|
||||
iam_policy = IAMPolicy(policy)
|
||||
permission_result = iam_policy.is_action_permitted(self._action)
|
||||
if permission_result == PermissionResult.DENIED:
|
||||
self._raise_access_denied()
|
||||
elif permission_result == PermissionResult.PERMITTED:
|
||||
permitted = True
|
||||
|
||||
if not permitted:
|
||||
self._raise_access_denied()
|
||||
|
||||
@abstractmethod
|
||||
def _raise_signature_does_not_match(self):
|
||||
raise NotImplementedError()
|
||||
|
||||
@abstractmethod
|
||||
def _raise_access_denied(self):
|
||||
raise NotImplementedError()
|
||||
|
||||
@abstractmethod
|
||||
def _raise_invalid_access_key(self, reason):
|
||||
raise NotImplementedError()
|
||||
|
||||
@abstractmethod
|
||||
def _create_auth(self, credentials):
|
||||
raise NotImplementedError()
|
||||
|
||||
@staticmethod
|
||||
def _create_headers_for_aws_request(signed_headers, original_headers):
|
||||
headers = {}
|
||||
for key, value in original_headers.items():
|
||||
if key.lower() in signed_headers:
|
||||
headers[key] = value
|
||||
return headers
|
||||
|
||||
def _create_aws_request(self):
|
||||
signed_headers = self._get_string_between('SignedHeaders=', ',', self._headers['Authorization']).split(';')
|
||||
headers = self._create_headers_for_aws_request(signed_headers, self._headers)
|
||||
request = AWSRequest(method=self._method, url=self._path, data=self._data, headers=headers)
|
||||
request.context['timestamp'] = headers['X-Amz-Date']
|
||||
|
||||
return request
|
||||
|
||||
def _calculate_signature(self):
|
||||
credentials = self._access_key.create_credentials()
|
||||
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)
|
||||
return auth.signature(string_to_sign, request)
|
||||
|
||||
@staticmethod
|
||||
def _get_string_between(first_separator, second_separator, string):
|
||||
return string.partition(first_separator)[2].partition(second_separator)[0]
|
||||
|
||||
|
||||
class IAMRequest(IAMRequestBase):
|
||||
|
||||
def _raise_signature_does_not_match(self):
|
||||
if self._service == "ec2":
|
||||
raise AuthFailureError()
|
||||
else:
|
||||
raise SignatureDoesNotMatchError()
|
||||
|
||||
def _raise_invalid_access_key(self, _):
|
||||
if self._service == "ec2":
|
||||
raise AuthFailureError()
|
||||
else:
|
||||
raise InvalidClientTokenIdError()
|
||||
|
||||
def _create_auth(self, credentials):
|
||||
return SigV4Auth(credentials, self._service, self._region)
|
||||
|
||||
def _raise_access_denied(self):
|
||||
raise AccessDeniedError(
|
||||
user_arn=self._access_key.arn,
|
||||
action=self._action
|
||||
)
|
||||
|
||||
|
||||
class S3IAMRequest(IAMRequestBase):
|
||||
|
||||
def _raise_signature_does_not_match(self):
|
||||
if "BucketName" in self._data:
|
||||
raise BucketSignatureDoesNotMatchError(bucket=self._data["BucketName"])
|
||||
else:
|
||||
raise S3SignatureDoesNotMatchError()
|
||||
|
||||
def _raise_invalid_access_key(self, reason):
|
||||
if reason == "InvalidToken":
|
||||
if "BucketName" in self._data:
|
||||
raise BucketInvalidTokenError(bucket=self._data["BucketName"])
|
||||
else:
|
||||
raise S3InvalidTokenError()
|
||||
else:
|
||||
if "BucketName" in self._data:
|
||||
raise BucketInvalidAccessKeyIdError(bucket=self._data["BucketName"])
|
||||
else:
|
||||
raise S3InvalidAccessKeyIdError()
|
||||
|
||||
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(object):
|
||||
|
||||
def __init__(self, policy):
|
||||
if isinstance(policy, Policy):
|
||||
default_version = next(policy_version for policy_version in policy.versions if policy_version.is_default)
|
||||
policy_document = default_version.document
|
||||
elif isinstance(policy, string_types):
|
||||
policy_document = policy
|
||||
else:
|
||||
policy_document = policy["policy_document"]
|
||||
|
||||
self._policy_json = json.loads(policy_document)
|
||||
|
||||
def is_action_permitted(self, action):
|
||||
permitted = False
|
||||
if isinstance(self._policy_json["Statement"], list):
|
||||
for policy_statement in self._policy_json["Statement"]:
|
||||
iam_policy_statement = IAMPolicyStatement(policy_statement)
|
||||
permission_result = iam_policy_statement.is_action_permitted(action)
|
||||
if permission_result == PermissionResult.DENIED:
|
||||
return permission_result
|
||||
elif permission_result == PermissionResult.PERMITTED:
|
||||
permitted = True
|
||||
else: # dict
|
||||
iam_policy_statement = IAMPolicyStatement(self._policy_json["Statement"])
|
||||
return iam_policy_statement.is_action_permitted(action)
|
||||
|
||||
if permitted:
|
||||
return PermissionResult.PERMITTED
|
||||
else:
|
||||
return PermissionResult.NEUTRAL
|
||||
|
||||
|
||||
class IAMPolicyStatement(object):
|
||||
|
||||
def __init__(self, statement):
|
||||
self._statement = statement
|
||||
|
||||
def is_action_permitted(self, action):
|
||||
is_action_concerned = False
|
||||
|
||||
if "NotAction" in self._statement:
|
||||
if not self._check_element_matches("NotAction", action):
|
||||
is_action_concerned = True
|
||||
else: # Action is present
|
||||
if self._check_element_matches("Action", action):
|
||||
is_action_concerned = True
|
||||
|
||||
if is_action_concerned:
|
||||
if self._statement["Effect"] == "Allow":
|
||||
return PermissionResult.PERMITTED
|
||||
else: # Deny
|
||||
return PermissionResult.DENIED
|
||||
else:
|
||||
return PermissionResult.NEUTRAL
|
||||
|
||||
def _check_element_matches(self, statement_element, value):
|
||||
if isinstance(self._statement[statement_element], list):
|
||||
for statement_element_value in self._statement[statement_element]:
|
||||
if self._match(statement_element_value, value):
|
||||
return True
|
||||
return False
|
||||
else: # string
|
||||
return self._match(self._statement[statement_element], value)
|
||||
|
||||
@staticmethod
|
||||
def _match(pattern, string):
|
||||
pattern = pattern.replace("*", ".*")
|
||||
pattern = "^{pattern}$".format(pattern=pattern)
|
||||
return re.match(pattern, string)
|
||||
|
||||
|
||||
class PermissionResult(Enum):
|
||||
PERMITTED = 1
|
||||
DENIED = 2
|
||||
NEUTRAL = 3
|
||||
|
|
@ -65,3 +65,42 @@ class JsonRESTError(RESTError):
|
|||
|
||||
def get_body(self, *args, **kwargs):
|
||||
return self.description
|
||||
|
||||
|
||||
class SignatureDoesNotMatchError(RESTError):
|
||||
code = 403
|
||||
|
||||
def __init__(self):
|
||||
super(SignatureDoesNotMatchError, self).__init__(
|
||||
'SignatureDoesNotMatch',
|
||||
"The request signature we calculated does not match the signature you provided. Check your AWS Secret Access Key and signing method. Consult the service documentation for details.")
|
||||
|
||||
|
||||
class InvalidClientTokenIdError(RESTError):
|
||||
code = 403
|
||||
|
||||
def __init__(self):
|
||||
super(InvalidClientTokenIdError, self).__init__(
|
||||
'InvalidClientTokenId',
|
||||
"The security token included in the request is invalid.")
|
||||
|
||||
|
||||
class AccessDeniedError(RESTError):
|
||||
code = 403
|
||||
|
||||
def __init__(self, user_arn, action):
|
||||
super(AccessDeniedError, self).__init__(
|
||||
'AccessDenied',
|
||||
"User: {user_arn} is not authorized to perform: {operation}".format(
|
||||
user_arn=user_arn,
|
||||
operation=action
|
||||
))
|
||||
|
||||
|
||||
class AuthFailureError(RESTError):
|
||||
code = 401
|
||||
|
||||
def __init__(self):
|
||||
super(AuthFailureError, self).__init__(
|
||||
'AuthFailure',
|
||||
"AWS was not able to validate the provided access credentials")
|
||||
|
|
|
|||
|
|
@ -1,13 +1,17 @@
|
|||
from __future__ import unicode_literals
|
||||
|
||||
import functools
|
||||
from collections import defaultdict
|
||||
import datetime
|
||||
import json
|
||||
import logging
|
||||
import re
|
||||
import io
|
||||
import requests
|
||||
|
||||
import pytz
|
||||
|
||||
from moto.core.access_control import IAMRequest, S3IAMRequest
|
||||
from moto.core.exceptions import DryRunClientError
|
||||
|
||||
from jinja2 import Environment, DictLoader, TemplateNotFound
|
||||
|
|
@ -22,7 +26,7 @@ from werkzeug.exceptions import HTTPException
|
|||
import boto3
|
||||
from moto.compat import OrderedDict
|
||||
from moto.core.utils import camelcase_to_underscores, method_names_from_class
|
||||
|
||||
from moto import settings
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
|
@ -103,7 +107,54 @@ class _TemplateEnvironmentMixin(object):
|
|||
return self.environment.get_template(template_id)
|
||||
|
||||
|
||||
class BaseResponse(_TemplateEnvironmentMixin):
|
||||
class ActionAuthenticatorMixin(object):
|
||||
|
||||
request_count = 0
|
||||
|
||||
def _authenticate_and_authorize_action(self, iam_request_cls):
|
||||
if ActionAuthenticatorMixin.request_count >= settings.INITIAL_NO_AUTH_ACTION_COUNT:
|
||||
iam_request = iam_request_cls(method=self.method, path=self.path, data=self.data, headers=self.headers)
|
||||
iam_request.check_signature()
|
||||
iam_request.check_action_permitted()
|
||||
else:
|
||||
ActionAuthenticatorMixin.request_count += 1
|
||||
|
||||
def _authenticate_and_authorize_normal_action(self):
|
||||
self._authenticate_and_authorize_action(IAMRequest)
|
||||
|
||||
def _authenticate_and_authorize_s3_action(self):
|
||||
self._authenticate_and_authorize_action(S3IAMRequest)
|
||||
|
||||
@staticmethod
|
||||
def set_initial_no_auth_action_count(initial_no_auth_action_count):
|
||||
def decorator(function):
|
||||
def wrapper(*args, **kwargs):
|
||||
if settings.TEST_SERVER_MODE:
|
||||
response = requests.post("http://localhost:5000/moto-api/reset-auth", data=str(initial_no_auth_action_count).encode())
|
||||
original_initial_no_auth_action_count = response.json()['PREVIOUS_INITIAL_NO_AUTH_ACTION_COUNT']
|
||||
else:
|
||||
original_initial_no_auth_action_count = settings.INITIAL_NO_AUTH_ACTION_COUNT
|
||||
original_request_count = ActionAuthenticatorMixin.request_count
|
||||
settings.INITIAL_NO_AUTH_ACTION_COUNT = initial_no_auth_action_count
|
||||
ActionAuthenticatorMixin.request_count = 0
|
||||
try:
|
||||
result = function(*args, **kwargs)
|
||||
finally:
|
||||
if settings.TEST_SERVER_MODE:
|
||||
requests.post("http://localhost:5000/moto-api/reset-auth", data=str(original_initial_no_auth_action_count).encode())
|
||||
else:
|
||||
ActionAuthenticatorMixin.request_count = original_request_count
|
||||
settings.INITIAL_NO_AUTH_ACTION_COUNT = original_initial_no_auth_action_count
|
||||
return result
|
||||
|
||||
functools.update_wrapper(wrapper, function)
|
||||
wrapper.__wrapped__ = function
|
||||
return wrapper
|
||||
|
||||
return decorator
|
||||
|
||||
|
||||
class BaseResponse(_TemplateEnvironmentMixin, ActionAuthenticatorMixin):
|
||||
|
||||
default_region = 'us-east-1'
|
||||
# to extract region, use [^.]
|
||||
|
|
@ -167,6 +218,7 @@ class BaseResponse(_TemplateEnvironmentMixin):
|
|||
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
|
||||
|
|
@ -273,6 +325,13 @@ class BaseResponse(_TemplateEnvironmentMixin):
|
|||
|
||||
def call_action(self):
|
||||
headers = self.response_headers
|
||||
|
||||
try:
|
||||
self._authenticate_and_authorize_normal_action()
|
||||
except HTTPException as http_error:
|
||||
response = http_error.description, dict(status=http_error.code)
|
||||
return self._send_response(headers, response)
|
||||
|
||||
action = camelcase_to_underscores(self._get_action())
|
||||
method_names = method_names_from_class(self.__class__)
|
||||
if action in method_names:
|
||||
|
|
@ -285,16 +344,7 @@ class BaseResponse(_TemplateEnvironmentMixin):
|
|||
if isinstance(response, six.string_types):
|
||||
return 200, headers, response
|
||||
else:
|
||||
if len(response) == 2:
|
||||
body, new_headers = response
|
||||
else:
|
||||
status, new_headers, body = response
|
||||
status = new_headers.get('status', 200)
|
||||
headers.update(new_headers)
|
||||
# Cast status to string
|
||||
if "status" in headers:
|
||||
headers['status'] = str(headers['status'])
|
||||
return status, headers, body
|
||||
return self._send_response(headers, response)
|
||||
|
||||
if not action:
|
||||
return 404, headers, ''
|
||||
|
|
@ -302,6 +352,19 @@ class BaseResponse(_TemplateEnvironmentMixin):
|
|||
raise NotImplementedError(
|
||||
"The {0} action has not been implemented".format(action))
|
||||
|
||||
@staticmethod
|
||||
def _send_response(headers, response):
|
||||
if len(response) == 2:
|
||||
body, new_headers = response
|
||||
else:
|
||||
status, new_headers, body = response
|
||||
status = new_headers.get('status', 200)
|
||||
headers.update(new_headers)
|
||||
# Cast status to string
|
||||
if "status" in headers:
|
||||
headers['status'] = str(headers['status'])
|
||||
return status, headers, body
|
||||
|
||||
def _get_param(self, param_name, if_none=None):
|
||||
val = self.querystring.get(param_name)
|
||||
if val is not None:
|
||||
|
|
@ -569,6 +632,14 @@ class MotoAPIResponse(BaseResponse):
|
|||
return 200, {}, json.dumps({"status": "ok"})
|
||||
return 400, {}, json.dumps({"Error": "Need to POST to reset Moto"})
|
||||
|
||||
def reset_auth_response(self, request, full_url, headers):
|
||||
if request.method == "POST":
|
||||
previous_initial_no_auth_action_count = settings.INITIAL_NO_AUTH_ACTION_COUNT
|
||||
settings.INITIAL_NO_AUTH_ACTION_COUNT = float(request.data.decode())
|
||||
ActionAuthenticatorMixin.request_count = 0
|
||||
return 200, {}, json.dumps({"status": "ok", "PREVIOUS_INITIAL_NO_AUTH_ACTION_COUNT": str(previous_initial_no_auth_action_count)})
|
||||
return 400, {}, json.dumps({"Error": "Need to POST to reset Moto Auth"})
|
||||
|
||||
def model_data(self, request, full_url, headers):
|
||||
from moto.core.models import model_data
|
||||
|
||||
|
|
|
|||
|
|
@ -11,4 +11,5 @@ url_paths = {
|
|||
'{0}/moto-api/$': response_instance.dashboard,
|
||||
'{0}/moto-api/data.json': response_instance.model_data,
|
||||
'{0}/moto-api/reset': response_instance.reset_response,
|
||||
'{0}/moto-api/reset-auth': response_instance.reset_auth_response,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1248,5 +1248,13 @@ class IAMBackend(BaseBackend):
|
|||
return saml_provider
|
||||
raise IAMNotFoundException("SamlProvider {0} not found".format(saml_provider_arn))
|
||||
|
||||
def get_user_from_access_key_id(self, access_key_id):
|
||||
for user_name, user in self.users.items():
|
||||
access_keys = self.get_all_access_keys(user_name)
|
||||
for access_key in access_keys:
|
||||
if access_key.access_key_id == access_key_id:
|
||||
return user
|
||||
return None
|
||||
|
||||
|
||||
iam_backend = IAMBackend()
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
from __future__ import unicode_literals
|
||||
|
||||
from moto.core.responses import BaseResponse
|
||||
|
||||
from .models import iam_backend, User
|
||||
|
||||
|
||||
|
|
@ -425,11 +426,13 @@ class IamResponse(BaseResponse):
|
|||
|
||||
def get_user(self):
|
||||
user_name = self._get_param('UserName')
|
||||
if user_name:
|
||||
user = iam_backend.get_user(user_name)
|
||||
if not user_name:
|
||||
access_key_id = self.get_current_user()
|
||||
user = iam_backend.get_user_from_access_key_id(access_key_id)
|
||||
if user is None:
|
||||
user = User("default_user")
|
||||
else:
|
||||
user = User(name='default_user')
|
||||
# If no user is specific, IAM returns the current user
|
||||
user = iam_backend.get_user(user_name)
|
||||
|
||||
template = self.response_template(USER_TEMPLATE)
|
||||
return template.render(action='Get', user=user)
|
||||
|
|
@ -457,7 +460,6 @@ class IamResponse(BaseResponse):
|
|||
def create_login_profile(self):
|
||||
user_name = self._get_param('UserName')
|
||||
password = self._get_param('Password')
|
||||
password = self._get_param('Password')
|
||||
user = iam_backend.create_login_profile(user_name, password)
|
||||
|
||||
template = self.response_template(CREATE_LOGIN_PROFILE_TEMPLATE)
|
||||
|
|
|
|||
|
|
@ -199,3 +199,67 @@ 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)
|
||||
|
||||
|
||||
class S3InvalidTokenError(S3ClientError):
|
||||
code = 400
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(S3InvalidTokenError, self).__init__('InvalidToken', 'The provided token is malformed or otherwise invalid.', *args, **kwargs)
|
||||
|
||||
|
||||
class BucketInvalidTokenError(BucketError):
|
||||
code = 400
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(BucketInvalidTokenError, self).__init__('InvalidToken', 'The provided token is malformed or otherwise invalid.', *args, **kwargs)
|
||||
|
||||
|
||||
class S3InvalidAccessKeyIdError(S3ClientError):
|
||||
code = 403
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(S3InvalidAccessKeyIdError, self).__init__(
|
||||
'InvalidAccessKeyId',
|
||||
"The AWS Access Key Id you provided does not exist in our records.", *args, **kwargs)
|
||||
|
||||
|
||||
class BucketInvalidAccessKeyIdError(S3ClientError):
|
||||
code = 403
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(BucketInvalidAccessKeyIdError, self).__init__(
|
||||
'InvalidAccessKeyId',
|
||||
"The AWS Access Key Id you provided does not exist in our records.", *args, **kwargs)
|
||||
|
||||
|
||||
class S3SignatureDoesNotMatchError(S3ClientError):
|
||||
code = 403
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(S3SignatureDoesNotMatchError, self).__init__(
|
||||
'SignatureDoesNotMatch',
|
||||
"The request signature we calculated does not match the signature you provided. Check your key and signing method.", *args, **kwargs)
|
||||
|
||||
|
||||
class BucketSignatureDoesNotMatchError(S3ClientError):
|
||||
code = 403
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(BucketSignatureDoesNotMatchError, self).__init__(
|
||||
'SignatureDoesNotMatch',
|
||||
"The request signature we calculated does not match the signature you provided. Check your key and signing method.", *args, **kwargs)
|
||||
|
|
|
|||
|
|
@ -3,13 +3,14 @@ from __future__ import unicode_literals
|
|||
import re
|
||||
|
||||
import six
|
||||
|
||||
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 +26,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 +104,24 @@ 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):
|
||||
self.data["Action"] = "ListAllMyBuckets"
|
||||
self._authenticate_and_authorize_s3_action()
|
||||
|
||||
# No bucket specified. Listing all buckets
|
||||
all_buckets = self.backend.get_all_buckets()
|
||||
template = self.response_template(S3_ALL_BUCKETS)
|
||||
|
|
@ -112,11 +186,20 @@ 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
|
||||
if 'host' not in self.headers:
|
||||
self.headers['host'] = urlparse(full_url).netloc
|
||||
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:
|
||||
|
|
@ -127,8 +210,7 @@ class ResponseObject(_TemplateEnvironmentMixin):
|
|||
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)
|
||||
|
||||
|
|
@ -137,6 +219,8 @@ class ResponseObject(_TemplateEnvironmentMixin):
|
|||
# If no bucket specified, list all buckets
|
||||
return self.all_buckets()
|
||||
|
||||
self.data["BucketName"] = bucket_name
|
||||
|
||||
if hasattr(request, 'body'):
|
||||
# Boto
|
||||
body = request.body
|
||||
|
|
@ -150,20 +234,26 @@ class ResponseObject(_TemplateEnvironmentMixin):
|
|||
body = u'{0}'.format(body).encode('utf-8')
|
||||
|
||||
if method == 'HEAD':
|
||||
return self._bucket_response_head(bucket_name, headers)
|
||||
return self._bucket_response_head(bucket_name)
|
||||
elif method == 'GET':
|
||||
return self._bucket_response_get(bucket_name, querystring, headers)
|
||||
return self._bucket_response_get(bucket_name, querystring)
|
||||
elif method == 'PUT':
|
||||
return self._bucket_response_put(request, body, region_name, bucket_name, querystring, headers)
|
||||
return self._bucket_response_put(request, body, region_name, bucket_name, querystring)
|
||||
elif method == 'DELETE':
|
||||
return self._bucket_response_delete(body, bucket_name, querystring, headers)
|
||||
return self._bucket_response_delete(body, bucket_name, querystring)
|
||||
elif method == 'POST':
|
||||
return self._bucket_response_post(request, body, bucket_name, headers)
|
||||
return self._bucket_response_post(request, body, bucket_name)
|
||||
else:
|
||||
raise NotImplementedError(
|
||||
"Method {0} has not been impelemented in the S3 backend yet".format(method))
|
||||
|
||||
def _bucket_response_head(self, bucket_name, headers):
|
||||
@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):
|
||||
try:
|
||||
self.backend.get_bucket(bucket_name)
|
||||
except MissingBucket:
|
||||
|
|
@ -174,7 +264,10 @@ class ResponseObject(_TemplateEnvironmentMixin):
|
|||
return 404, {}, ""
|
||||
return 200, {}, ""
|
||||
|
||||
def _bucket_response_get(self, bucket_name, querystring, headers):
|
||||
def _bucket_response_get(self, bucket_name, querystring):
|
||||
self._set_action("BUCKET", "GET", querystring)
|
||||
self._authenticate_and_authorize_s3_action()
|
||||
|
||||
if 'uploads' in querystring:
|
||||
for unsup in ('delimiter', 'max-uploads'):
|
||||
if unsup in querystring:
|
||||
|
|
@ -333,6 +426,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)
|
||||
|
|
@ -393,9 +495,13 @@ class ResponseObject(_TemplateEnvironmentMixin):
|
|||
next_continuation_token = None
|
||||
return result_keys, is_truncated, next_continuation_token
|
||||
|
||||
def _bucket_response_put(self, request, body, region_name, bucket_name, querystring, headers):
|
||||
def _bucket_response_put(self, request, body, region_name, bucket_name, querystring):
|
||||
if not request.headers.get('Content-Length'):
|
||||
return 411, {}, "Content-Length required"
|
||||
|
||||
self._set_action("BUCKET", "PUT", querystring)
|
||||
self._authenticate_and_authorize_s3_action()
|
||||
|
||||
if 'versioning' in querystring:
|
||||
ver = re.search('<Status>([A-Za-z]+)</Status>', body.decode())
|
||||
if ver:
|
||||
|
|
@ -494,7 +600,10 @@ class ResponseObject(_TemplateEnvironmentMixin):
|
|||
template = self.response_template(S3_BUCKET_CREATE_RESPONSE)
|
||||
return 200, {}, template.render(bucket=new_bucket)
|
||||
|
||||
def _bucket_response_delete(self, body, bucket_name, querystring, headers):
|
||||
def _bucket_response_delete(self, body, bucket_name, querystring):
|
||||
self._set_action("BUCKET", "DELETE", querystring)
|
||||
self._authenticate_and_authorize_s3_action()
|
||||
|
||||
if 'policy' in querystring:
|
||||
self.backend.delete_bucket_policy(bucket_name, body)
|
||||
return 204, {}, ""
|
||||
|
|
@ -521,17 +630,20 @@ class ResponseObject(_TemplateEnvironmentMixin):
|
|||
S3_DELETE_BUCKET_WITH_ITEMS_ERROR)
|
||||
return 409, {}, template.render(bucket=removed_bucket)
|
||||
|
||||
def _bucket_response_post(self, request, body, bucket_name, headers):
|
||||
def _bucket_response_post(self, request, body, bucket_name):
|
||||
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):
|
||||
return self._bucket_response_delete_keys(request, body, bucket_name, headers)
|
||||
self.data["Action"] = "DeleteObject"
|
||||
self._authenticate_and_authorize_s3_action()
|
||||
|
||||
return self._bucket_response_delete_keys(request, body, bucket_name)
|
||||
|
||||
self.data["Action"] = "PutObject"
|
||||
self._authenticate_and_authorize_s3_action()
|
||||
|
||||
# POST to bucket-url should create file from form
|
||||
if hasattr(request, 'form'):
|
||||
|
|
@ -560,7 +672,15 @@ class ResponseObject(_TemplateEnvironmentMixin):
|
|||
|
||||
return 200, {}, ""
|
||||
|
||||
def _bucket_response_delete_keys(self, request, body, bucket_name, headers):
|
||||
@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):
|
||||
template = self.response_template(S3_DELETE_KEYS_RESPONSE)
|
||||
|
||||
keys = minidom.parseString(body).getElementsByTagName('Key')
|
||||
|
|
@ -606,6 +726,11 @@ 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
|
||||
if 'host' not in self.headers:
|
||||
self.headers['host'] = urlparse(full_url).netloc
|
||||
response_headers = {}
|
||||
try:
|
||||
response = self._key_response(request, full_url, headers)
|
||||
|
|
@ -665,14 +790,17 @@ class ResponseObject(_TemplateEnvironmentMixin):
|
|||
elif method == 'HEAD':
|
||||
return self._key_response_head(bucket_name, query, key_name, headers=request.headers)
|
||||
elif method == 'DELETE':
|
||||
return self._key_response_delete(bucket_name, query, key_name, headers)
|
||||
return self._key_response_delete(bucket_name, query, key_name)
|
||||
elif method == 'POST':
|
||||
return self._key_response_post(request, body, bucket_name, query, key_name, headers)
|
||||
return self._key_response_post(request, body, bucket_name, query, key_name)
|
||||
else:
|
||||
raise NotImplementedError(
|
||||
"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)
|
||||
self._authenticate_and_authorize_s3_action()
|
||||
|
||||
response_headers = {}
|
||||
if query.get('uploadId'):
|
||||
upload_id = query['uploadId'][0]
|
||||
|
|
@ -707,6 +835,9 @@ 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)
|
||||
self._authenticate_and_authorize_s3_action()
|
||||
|
||||
response_headers = {}
|
||||
if query.get('uploadId') and query.get('partNumber'):
|
||||
upload_id = query['uploadId'][0]
|
||||
|
|
@ -1080,7 +1211,10 @@ class ResponseObject(_TemplateEnvironmentMixin):
|
|||
config = parsed_xml['AccelerateConfiguration']
|
||||
return config['Status']
|
||||
|
||||
def _key_response_delete(self, bucket_name, query, key_name, headers):
|
||||
def _key_response_delete(self, bucket_name, query, key_name):
|
||||
self._set_action("KEY", "DELETE", query)
|
||||
self._authenticate_and_authorize_s3_action()
|
||||
|
||||
if query.get('uploadId'):
|
||||
upload_id = query['uploadId'][0]
|
||||
self.backend.cancel_multipart(bucket_name, upload_id)
|
||||
|
|
@ -1100,7 +1234,10 @@ class ResponseObject(_TemplateEnvironmentMixin):
|
|||
raise InvalidPartOrder()
|
||||
yield (pn, p.getElementsByTagName('ETag')[0].firstChild.wholeText)
|
||||
|
||||
def _key_response_post(self, request, body, bucket_name, query, key_name, headers):
|
||||
def _key_response_post(self, request, body, bucket_name, query, key_name):
|
||||
self._set_action("KEY", "POST", query)
|
||||
self._authenticate_and_authorize_s3_action()
|
||||
|
||||
if body == b'' and 'uploads' in query:
|
||||
metadata = metadata_from_headers(request.headers)
|
||||
multipart = self.backend.initiate_multipart(
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import os
|
||||
|
||||
TEST_SERVER_MODE = os.environ.get('TEST_SERVER_MODE', '0').lower() == 'true'
|
||||
INITIAL_NO_AUTH_ACTION_COUNT = float(os.environ.get('INITIAL_NO_AUTH_ACTION_COUNT', float('inf')))
|
||||
|
|
|
|||
|
|
@ -38,6 +38,9 @@ class AssumedRole(BaseModel):
|
|||
|
||||
class STSBackend(BaseBackend):
|
||||
|
||||
def __init__(self):
|
||||
self.assumed_roles = []
|
||||
|
||||
def get_session_token(self, duration):
|
||||
token = Token(duration=duration)
|
||||
return token
|
||||
|
|
@ -48,6 +51,7 @@ class STSBackend(BaseBackend):
|
|||
|
||||
def assume_role(self, **kwargs):
|
||||
role = AssumedRole(**kwargs)
|
||||
self.assumed_roles.append(role)
|
||||
return role
|
||||
|
||||
def assume_role_with_web_identity(self, **kwargs):
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue