diff --git a/README.md b/README.md index a22e8b83..ed8e1442 100644 --- a/README.md +++ b/README.md @@ -67,6 +67,8 @@ It gets even better! Moto isn't just S3. Here's the status of the other AWS serv |---------------------------------------------------------------------------| | SQS | @mock_sqs | core endpoints done | |---------------------------------------------------------------------------| +| STS | @mock_sts | core endpoints done | +|---------------------------------------------------------------------------| ``` ### Another Example diff --git a/moto/__init__.py b/moto/__init__.py index 0548f965..49f121a3 100644 --- a/moto/__init__.py +++ b/moto/__init__.py @@ -6,3 +6,4 @@ from .ec2 import mock_ec2 from .s3 import mock_s3 from .ses import mock_ses from .sqs import mock_sqs +from .sts import mock_sts diff --git a/moto/core/utils.py b/moto/core/utils.py index 35a1e129..53418edb 100644 --- a/moto/core/utils.py +++ b/moto/core/utils.py @@ -69,3 +69,12 @@ class convert_flask_to_httpretty_response(object): # result is a status, headers, response tuple status, headers, response = result return response, status, headers + + +def iso_8601_datetime(datetime): + return datetime.strftime("%Y-%m-%dT%H:%M:%SZ") + + +def rfc_1123_datetime(datetime): + RFC1123 = '%a, %d %b %Y %H:%M:%S GMT' + return datetime.strftime(RFC1123) diff --git a/moto/s3/models.py b/moto/s3/models.py index ef39fea6..524d547e 100644 --- a/moto/s3/models.py +++ b/moto/s3/models.py @@ -2,6 +2,7 @@ import datetime import md5 from moto.core import BaseBackend +from moto.core.utils import iso_8601_datetime, rfc_1123_datetime from .utils import clean_key_name @@ -27,14 +28,13 @@ class FakeKey(object): @property def last_modified_ISO8601(self): - return self.last_modified.strftime("%Y-%m-%dT%H:%M:%SZ") + return iso_8601_datetime(self.last_modified) @property def last_modified_RFC1123(self): # Different datetime formats depending on how the key is obtained # https://github.com/boto/boto/issues/466 - RFC1123 = '%a, %d %b %Y %H:%M:%S GMT' - return self.last_modified.strftime(RFC1123) + return rfc_1123_datetime(self.last_modified) @property def metadata(self): diff --git a/moto/server.py b/moto/server.py index e0d7c60f..18980bff 100644 --- a/moto/server.py +++ b/moto/server.py @@ -8,6 +8,7 @@ from moto.ec2 import ec2_backend # flake8: noqa from moto.s3 import s3_backend # flake8: noqa from moto.ses import ses_backend # flake8: noqa from moto.sqs import sqs_backend # flake8: noqa +from moto.sts import sts_backend # flake8: noqa from moto.core.utils import convert_flask_to_httpretty_response diff --git a/moto/sts/__init__.py b/moto/sts/__init__.py new file mode 100644 index 00000000..f1ca24c7 --- /dev/null +++ b/moto/sts/__init__.py @@ -0,0 +1,2 @@ +from .models import sts_backend +mock_sts = sts_backend.decorator diff --git a/moto/sts/models.py b/moto/sts/models.py new file mode 100644 index 00000000..3a9e64e0 --- /dev/null +++ b/moto/sts/models.py @@ -0,0 +1,39 @@ +import datetime +from moto.core import BaseBackend +from moto.core.utils import iso_8601_datetime + + +class Token(object): + def __init__(self, duration): + now = datetime.datetime.now() + self.expiration = now + datetime.timedelta(seconds=duration) + + @property + def expiration_ISO8601(self): + return iso_8601_datetime(self.expiration) + + +class AssumedRole(object): + def __init__(self, role_session_name, role_arn, policy, duration, external_id): + self.session_name = role_session_name + self.arn = role_arn + self.policy = policy + now = datetime.datetime.now() + self.expiration = now + datetime.timedelta(seconds=duration) + self.external_id = external_id + + @property + def expiration_ISO8601(self): + return iso_8601_datetime(self.expiration) + + +class STSBackend(BaseBackend): + def get_session_token(self, duration): + token = Token(duration=duration) + return token + + def assume_role(self, **kwargs): + role = AssumedRole(**kwargs) + return role + +sts_backend = STSBackend() diff --git a/moto/sts/responses.py b/moto/sts/responses.py new file mode 100644 index 00000000..e97c9ec5 --- /dev/null +++ b/moto/sts/responses.py @@ -0,0 +1,67 @@ +from jinja2 import Template + +from moto.core.responses import BaseResponse +from .models import sts_backend + + +class TokenResponse(BaseResponse): + + def get_session_token(self): + duration = int(self.querystring.get('DurationSeconds', [43200])[0]) + token = sts_backend.get_session_token(duration=duration) + template = Template(GET_SESSION_TOKEN_RESPONSE) + return template.render(token=token) + + def assume_role(self): + role_session_name = self.querystring.get('RoleSessionName')[0] + role_arn = self.querystring.get('RoleArn')[0] + + policy = self.querystring.get('Policy', [None])[0] + duration = int(self.querystring.get('DurationSeconds', [3600])[0]) + external_id = self.querystring.get('ExternalId', [None])[0] + + role = sts_backend.assume_role( + role_session_name=role_session_name, + role_arn=role_arn, + policy=policy, + duration=duration, + external_id=external_id, + ) + template = Template(ASSUME_ROLE_RESPONSE) + return template.render(role=role) + + +GET_SESSION_TOKEN_RESPONSE = """ + + + AQoEXAMPLEH4aoAH0gNCAPyJxz4BlCFFxWNE1OPTgk5TthT+FvwqnKwRcOIfrRh3c/LTo6UDdyJwOOvEVPvLXCrrrUtdnniCEXAMPLE/IvU1dYUg2RVAJBanLiHb4IgRmpRV3zrkuWJOgQs8IZZaIv2BXIa2R4OlgkBN9bkUDNCJiBeb/AXlzBBko7b15fjrBs2+cTQtpZ3CYWFXG8C5zqx37wnOE49mRl/+OtkIKGO7fAE + wJalrXUtnFEMI/K7MDENG/bPxRfiCYzEXAMPLEKEY + {{ token.expiration_ISO8601 }} + AKIAIOSFODNN7EXAMPLE + + + + 58c5dbae-abef-11e0-8cfe-09039844ac7d + +""" + + +ASSUME_ROLE_RESPONSE = """ + + + BQoEXAMPLEH4aoAH0gNCAPyJxz4BlCFFxWNE1OPTgk5TthT+FvwqnKwRcOIfrRh3c/LTo6UDdyJwOOvEVPvLXCrrrUtdnniCEXAMPLE/IvU1dYUg2RVAJBanLiHb4IgRmpRV3zrkuWJOgQs8IZZaIv2BXIa2R4OlgkBN9bkUDNCJiBeb/AXlzBBko7b15fjrBs2+cTQtpZ3CYWFXG8C5zqx37wnOE49mRl/+OtkIKGO7fAE + aJalrXUtnFEMI/K7MDENG/bPxRfiCYzEXAMPLEKEY + {{ role.expiration_ISO8601 }} + AKIAIOSFODNN7EXAMPLE + + + {{ role.arn }} + ARO123EXAMPLE123:{{ role.session_name }} + + 6 + + + c6104cbe-af31-11e0-8154-cbc7ccf896c7 + +""" diff --git a/moto/sts/urls.py b/moto/sts/urls.py new file mode 100644 index 00000000..ab69fe8c --- /dev/null +++ b/moto/sts/urls.py @@ -0,0 +1,9 @@ +from .responses import TokenResponse + +url_bases = [ + "https?://sts.amazonaws.com" +] + +url_paths = { + '{0}/$': TokenResponse().dispatch, +} diff --git a/tests/test_sts/test_server.py b/tests/test_sts/test_server.py new file mode 100644 index 00000000..0e7687c7 --- /dev/null +++ b/tests/test_sts/test_server.py @@ -0,0 +1,16 @@ +import sure # flake8: noqa + +import moto.server as server + +''' +Test the different server responses +''' +server.configure_urls("sts") + + +def test_sts_get_session_token(): + test_client = server.app.test_client() + res = test_client.get('/?Action=GetSessionToken') + res.status_code.should.equal(200) + res.data.should.contain("SessionToken") + res.data.should.contain("AccessKeyId") diff --git a/tests/test_sts/test_sts.py b/tests/test_sts/test_sts.py new file mode 100644 index 00000000..0d05b613 --- /dev/null +++ b/tests/test_sts/test_sts.py @@ -0,0 +1,52 @@ +import json + +import boto +from boto.exception import BotoServerError +from freezegun import freeze_time +import sure # flake8: noqa + +from moto import mock_sts + + +@freeze_time("2012-01-01 12:00:00") +@mock_sts +def test_get_session_token(): + conn = boto.connect_sts() + token = conn.get_session_token(duration=123) + + token.expiration.should.equal('2012-01-01T12:02:03Z') + token.session_token.should.equal("AQoEXAMPLEH4aoAH0gNCAPyJxz4BlCFFxWNE1OPTgk5TthT+FvwqnKwRcOIfrRh3c/LTo6UDdyJwOOvEVPvLXCrrrUtdnniCEXAMPLE/IvU1dYUg2RVAJBanLiHb4IgRmpRV3zrkuWJOgQs8IZZaIv2BXIa2R4OlgkBN9bkUDNCJiBeb/AXlzBBko7b15fjrBs2+cTQtpZ3CYWFXG8C5zqx37wnOE49mRl/+OtkIKGO7fAE") + token.access_key.should.equal("AKIAIOSFODNN7EXAMPLE") + token.secret_key.should.equal("wJalrXUtnFEMI/K7MDENG/bPxRfiCYzEXAMPLEKEY") + + +@freeze_time("2012-01-01 12:00:00") +@mock_sts +def test_assume_role(): + conn = boto.connect_sts() + + policy = json.dumps({ + "Statement": [ + { + "Sid": "Stmt13690092345534", + "Action": [ + "S3:ListBucket" + ], + "Effect": "Allow", + "Resource": [ + "arn:aws:s3:::foobar-tester" + ] + }, + ] + }) + s3_role = "arn:aws:iam::123456789012:role/test-role" + role = conn.assume_role(s3_role, "session-name", policy, duration_seconds=123) + + credentials = role.credentials + credentials.expiration.should.equal('2012-01-01T12:02:03Z') + credentials.session_token.should.equal("BQoEXAMPLEH4aoAH0gNCAPyJxz4BlCFFxWNE1OPTgk5TthT+FvwqnKwRcOIfrRh3c/LTo6UDdyJwOOvEVPvLXCrrrUtdnniCEXAMPLE/IvU1dYUg2RVAJBanLiHb4IgRmpRV3zrkuWJOgQs8IZZaIv2BXIa2R4OlgkBN9bkUDNCJiBeb/AXlzBBko7b15fjrBs2+cTQtpZ3CYWFXG8C5zqx37wnOE49mRl/+OtkIKGO7fAE") + credentials.access_key.should.equal("AKIAIOSFODNN7EXAMPLE") + credentials.secret_key.should.equal("aJalrXUtnFEMI/K7MDENG/bPxRfiCYzEXAMPLEKEY") + + role.user.arn.should.equal("arn:aws:iam::123456789012:role/test-role") + role.user.assume_role_id.should.contain("session-name")