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")