Merge pull request #2894 from microe/add_sts_assume_role_with_saml
Add the STS call assume_role_with_saml
This commit is contained in:
commit
60bcb46729
@ -1,6 +1,7 @@
|
|||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
|
import io
|
||||||
import json
|
import json
|
||||||
import re
|
import re
|
||||||
import sys
|
import sys
|
||||||
@ -29,6 +30,7 @@ UNSIGNED_REQUESTS = {
|
|||||||
"AWSCognitoIdentityService": ("cognito-identity", "us-east-1"),
|
"AWSCognitoIdentityService": ("cognito-identity", "us-east-1"),
|
||||||
"AWSCognitoIdentityProviderService": ("cognito-idp", "us-east-1"),
|
"AWSCognitoIdentityProviderService": ("cognito-idp", "us-east-1"),
|
||||||
}
|
}
|
||||||
|
UNSIGNED_ACTIONS = {"AssumeRoleWithSAML": ("sts", "us-east-1")}
|
||||||
|
|
||||||
|
|
||||||
class DomainDispatcherApplication(object):
|
class DomainDispatcherApplication(object):
|
||||||
@ -77,9 +79,13 @@ class DomainDispatcherApplication(object):
|
|||||||
else:
|
else:
|
||||||
# Unsigned request
|
# Unsigned request
|
||||||
target = environ.get("HTTP_X_AMZ_TARGET")
|
target = environ.get("HTTP_X_AMZ_TARGET")
|
||||||
|
action = self.get_action_from_body(environ)
|
||||||
if target:
|
if target:
|
||||||
service, _ = target.split(".", 1)
|
service, _ = target.split(".", 1)
|
||||||
service, region = UNSIGNED_REQUESTS.get(service, DEFAULT_SERVICE_REGION)
|
service, region = UNSIGNED_REQUESTS.get(service, DEFAULT_SERVICE_REGION)
|
||||||
|
elif action and action in UNSIGNED_ACTIONS:
|
||||||
|
# See if we can match the Action to a known service
|
||||||
|
service, region = UNSIGNED_ACTIONS.get(action)
|
||||||
else:
|
else:
|
||||||
# S3 is the last resort when the target is also unknown
|
# S3 is the last resort when the target is also unknown
|
||||||
service, region = DEFAULT_SERVICE_REGION
|
service, region = DEFAULT_SERVICE_REGION
|
||||||
@ -130,6 +136,26 @@ class DomainDispatcherApplication(object):
|
|||||||
self.app_instances[backend] = app
|
self.app_instances[backend] = app
|
||||||
return app
|
return app
|
||||||
|
|
||||||
|
def get_action_from_body(self, environ):
|
||||||
|
body = None
|
||||||
|
try:
|
||||||
|
# AWS requests use querystrings as the body (Action=x&Data=y&...)
|
||||||
|
simple_form = environ["CONTENT_TYPE"].startswith(
|
||||||
|
"application/x-www-form-urlencoded"
|
||||||
|
)
|
||||||
|
request_body_size = int(environ["CONTENT_LENGTH"])
|
||||||
|
if simple_form and request_body_size:
|
||||||
|
body = environ["wsgi.input"].read(request_body_size).decode("utf-8")
|
||||||
|
body_dict = dict(x.split("=") for x in body.split("&"))
|
||||||
|
return body_dict["Action"]
|
||||||
|
except (KeyError, ValueError):
|
||||||
|
pass
|
||||||
|
finally:
|
||||||
|
if body:
|
||||||
|
# We've consumed the body = need to reset it
|
||||||
|
environ["wsgi.input"] = io.StringIO(body)
|
||||||
|
return None
|
||||||
|
|
||||||
def __call__(self, environ, start_response):
|
def __call__(self, environ, start_response):
|
||||||
backend_app = self.get_application(environ)
|
backend_app = self.get_application(environ)
|
||||||
return backend_app(environ, start_response)
|
return backend_app(environ, start_response)
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
from base64 import b64decode
|
||||||
import datetime
|
import datetime
|
||||||
|
import xmltodict
|
||||||
from moto.core import BaseBackend, BaseModel
|
from moto.core import BaseBackend, BaseModel
|
||||||
from moto.core.utils import iso_8601_datetime_with_milliseconds
|
from moto.core.utils import iso_8601_datetime_with_milliseconds
|
||||||
from moto.core import ACCOUNT_ID
|
from moto.core import ACCOUNT_ID
|
||||||
@ -79,5 +81,24 @@ class STSBackend(BaseBackend):
|
|||||||
def assume_role_with_web_identity(self, **kwargs):
|
def assume_role_with_web_identity(self, **kwargs):
|
||||||
return self.assume_role(**kwargs)
|
return self.assume_role(**kwargs)
|
||||||
|
|
||||||
|
def assume_role_with_saml(self, **kwargs):
|
||||||
|
del kwargs["principal_arn"]
|
||||||
|
saml_assertion_encoded = kwargs.pop("saml_assertion")
|
||||||
|
saml_assertion_decoded = b64decode(saml_assertion_encoded)
|
||||||
|
saml_assertion = xmltodict.parse(saml_assertion_decoded.decode("utf-8"))
|
||||||
|
kwargs["duration"] = int(
|
||||||
|
saml_assertion["samlp:Response"]["Assertion"]["AttributeStatement"][
|
||||||
|
"Attribute"
|
||||||
|
][2]["AttributeValue"]
|
||||||
|
)
|
||||||
|
kwargs["role_session_name"] = saml_assertion["samlp:Response"]["Assertion"][
|
||||||
|
"AttributeStatement"
|
||||||
|
]["Attribute"][0]["AttributeValue"]
|
||||||
|
kwargs["external_id"] = None
|
||||||
|
kwargs["policy"] = None
|
||||||
|
role = AssumedRole(**kwargs)
|
||||||
|
self.assumed_roles.append(role)
|
||||||
|
return role
|
||||||
|
|
||||||
|
|
||||||
sts_backend = STSBackend()
|
sts_backend = STSBackend()
|
||||||
|
@ -71,6 +71,19 @@ class TokenResponse(BaseResponse):
|
|||||||
template = self.response_template(ASSUME_ROLE_WITH_WEB_IDENTITY_RESPONSE)
|
template = self.response_template(ASSUME_ROLE_WITH_WEB_IDENTITY_RESPONSE)
|
||||||
return template.render(role=role)
|
return template.render(role=role)
|
||||||
|
|
||||||
|
def assume_role_with_saml(self):
|
||||||
|
role_arn = self.querystring.get("RoleArn")[0]
|
||||||
|
principal_arn = self.querystring.get("PrincipalArn")[0]
|
||||||
|
saml_assertion = self.querystring.get("SAMLAssertion")[0]
|
||||||
|
|
||||||
|
role = sts_backend.assume_role_with_saml(
|
||||||
|
role_arn=role_arn,
|
||||||
|
principal_arn=principal_arn,
|
||||||
|
saml_assertion=saml_assertion,
|
||||||
|
)
|
||||||
|
template = self.response_template(ASSUME_ROLE_WITH_SAML_RESPONSE)
|
||||||
|
return template.render(role=role)
|
||||||
|
|
||||||
def get_caller_identity(self):
|
def get_caller_identity(self):
|
||||||
template = self.response_template(GET_CALLER_IDENTITY_RESPONSE)
|
template = self.response_template(GET_CALLER_IDENTITY_RESPONSE)
|
||||||
|
|
||||||
@ -168,6 +181,30 @@ ASSUME_ROLE_WITH_WEB_IDENTITY_RESPONSE = """<AssumeRoleWithWebIdentityResponse x
|
|||||||
</AssumeRoleWithWebIdentityResponse>"""
|
</AssumeRoleWithWebIdentityResponse>"""
|
||||||
|
|
||||||
|
|
||||||
|
ASSUME_ROLE_WITH_SAML_RESPONSE = """<AssumeRoleWithSAMLResponse xmlns="https://sts.amazonaws.com/doc/2011-06-15/">
|
||||||
|
<AssumeRoleWithSAMLResult>
|
||||||
|
<Audience>https://signin.aws.amazon.com/saml</Audience>
|
||||||
|
<AssumedRoleUser>
|
||||||
|
<AssumedRoleId>{{ role.user_id }}</AssumedRoleId>
|
||||||
|
<Arn>{{ role.arn }}</Arn>
|
||||||
|
</AssumedRoleUser>
|
||||||
|
<Credentials>
|
||||||
|
<AccessKeyId>{{ role.access_key_id }}</AccessKeyId>
|
||||||
|
<SecretAccessKey>{{ role.secret_access_key }}</SecretAccessKey>
|
||||||
|
<SessionToken>{{ role.session_token }}</SessionToken>
|
||||||
|
<Expiration>{{ role.expiration_ISO8601 }}</Expiration>
|
||||||
|
</Credentials>
|
||||||
|
<Subject>{{ role.user_id }}</Subject>
|
||||||
|
<NameQualifier>B64EncodedStringOfHashOfIssuerAccountIdAndUserId=</NameQualifier>
|
||||||
|
<SubjectType>persistent</SubjectType>
|
||||||
|
<Issuer>http://localhost:3000/</Issuer>
|
||||||
|
</AssumeRoleWithSAMLResult>
|
||||||
|
<ResponseMetadata>
|
||||||
|
<RequestId>c6104cbe-af31-11e0-8154-cbc7ccf896c7</RequestId>
|
||||||
|
</ResponseMetadata>
|
||||||
|
</AssumeRoleWithSAMLResponse>"""
|
||||||
|
|
||||||
|
|
||||||
GET_CALLER_IDENTITY_RESPONSE = """<GetCallerIdentityResponse xmlns="https://sts.amazonaws.com/doc/2011-06-15/">
|
GET_CALLER_IDENTITY_RESPONSE = """<GetCallerIdentityResponse xmlns="https://sts.amazonaws.com/doc/2011-06-15/">
|
||||||
<GetCallerIdentityResult>
|
<GetCallerIdentityResult>
|
||||||
<Arn>{{ arn }}</Arn>
|
<Arn>{{ arn }}</Arn>
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
from base64 import b64encode
|
||||||
import json
|
import json
|
||||||
|
|
||||||
import boto
|
import boto
|
||||||
@ -103,6 +104,128 @@ def test_assume_role():
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@freeze_time("2012-01-01 12:00:00")
|
||||||
|
@mock_sts
|
||||||
|
def test_assume_role_with_saml():
|
||||||
|
client = boto3.client("sts", region_name="us-east-1")
|
||||||
|
|
||||||
|
session_name = "session-name"
|
||||||
|
policy = json.dumps(
|
||||||
|
{
|
||||||
|
"Statement": [
|
||||||
|
{
|
||||||
|
"Sid": "Stmt13690092345534",
|
||||||
|
"Action": ["S3:ListBucket"],
|
||||||
|
"Effect": "Allow",
|
||||||
|
"Resource": ["arn:aws:s3:::foobar-tester"],
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
)
|
||||||
|
role_name = "test-role"
|
||||||
|
provider_name = "TestProvFed"
|
||||||
|
user_name = "testuser"
|
||||||
|
role_input = "arn:aws:iam::{account_id}:role/{role_name}".format(
|
||||||
|
account_id=ACCOUNT_ID, role_name=role_name
|
||||||
|
)
|
||||||
|
principal_role = "arn:aws:iam:{account_id}:saml-provider/{provider_name}".format(
|
||||||
|
account_id=ACCOUNT_ID, provider_name=provider_name
|
||||||
|
)
|
||||||
|
saml_assertion = """
|
||||||
|
<?xml version="1.0"?>
|
||||||
|
<samlp:Response xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" ID="_00000000-0000-0000-0000-000000000000" Version="2.0" IssueInstant="2012-01-01T12:00:00.000Z" Destination="https://signin.aws.amazon.com/saml" Consent="urn:oasis:names:tc:SAML:2.0:consent:unspecified">
|
||||||
|
<Issuer xmlns="urn:oasis:names:tc:SAML:2.0:assertion">http://localhost/</Issuer>
|
||||||
|
<samlp:Status>
|
||||||
|
<samlp:StatusCode Value="urn:oasis:names:tc:SAML:2.0:status:Success"/>
|
||||||
|
</samlp:Status>
|
||||||
|
<Assertion xmlns="urn:oasis:names:tc:SAML:2.0:assertion" ID="_00000000-0000-0000-0000-000000000000" IssueInstant="2012-12-01T12:00:00.000Z" Version="2.0">
|
||||||
|
<Issuer>http://localhost:3000/</Issuer>
|
||||||
|
<ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
|
||||||
|
<ds:SignedInfo>
|
||||||
|
<ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
|
||||||
|
<ds:SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
|
||||||
|
<ds:Reference URI="#_00000000-0000-0000-0000-000000000000">
|
||||||
|
<ds:Transforms>
|
||||||
|
<ds:Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature"/>
|
||||||
|
<ds:Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
|
||||||
|
</ds:Transforms>
|
||||||
|
<ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
|
||||||
|
<ds:DigestValue>NTIyMzk0ZGI4MjI0ZjI5ZGNhYjkyOGQyZGQ1NTZjODViZjk5YTY4ODFjOWRjNjkyYzZmODY2ZDQ4NjlkZjY3YSAgLQo=</ds:DigestValue>
|
||||||
|
</ds:Reference>
|
||||||
|
</ds:SignedInfo>
|
||||||
|
<ds:SignatureValue>NTIyMzk0ZGI4MjI0ZjI5ZGNhYjkyOGQyZGQ1NTZjODViZjk5YTY4ODFjOWRjNjkyYzZmODY2ZDQ4NjlkZjY3YSAgLQo=</ds:SignatureValue>
|
||||||
|
<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
|
||||||
|
<ds:X509Data>
|
||||||
|
<ds:X509Certificate>NTIyMzk0ZGI4MjI0ZjI5ZGNhYjkyOGQyZGQ1NTZjODViZjk5YTY4ODFjOWRjNjkyYzZmODY2ZDQ4NjlkZjY3YSAgLQo=</ds:X509Certificate>
|
||||||
|
</ds:X509Data>
|
||||||
|
</KeyInfo>
|
||||||
|
</ds:Signature>
|
||||||
|
<Subject>
|
||||||
|
<NameID Format="urn:oasis:names:tc:SAML:2.0:nameid-format:persistent">{username}</NameID>
|
||||||
|
<SubjectConfirmation Method="urn:oasis:names:tc:SAML:2.0:cm:bearer">
|
||||||
|
<SubjectConfirmationData NotOnOrAfter="2012-01-01T13:00:00.000Z" Recipient="https://signin.aws.amazon.com/saml"/>
|
||||||
|
</SubjectConfirmation>
|
||||||
|
</Subject>
|
||||||
|
<Conditions NotBefore="2012-01-01T12:00:00.000Z" NotOnOrAfter="2012-01-01T13:00:00.000Z">
|
||||||
|
<AudienceRestriction>
|
||||||
|
<Audience>urn:amazon:webservices</Audience>
|
||||||
|
</AudienceRestriction>
|
||||||
|
</Conditions>
|
||||||
|
<AttributeStatement>
|
||||||
|
<Attribute Name="https://aws.amazon.com/SAML/Attributes/RoleSessionName">
|
||||||
|
<AttributeValue>{username}@localhost</AttributeValue>
|
||||||
|
</Attribute>
|
||||||
|
<Attribute Name="https://aws.amazon.com/SAML/Attributes/Role">
|
||||||
|
<AttributeValue>arn:aws:iam::{account_id}:saml-provider/{provider_name},arn:aws:iam::{account_id}:role/{role_name}</AttributeValue>
|
||||||
|
</Attribute>
|
||||||
|
<Attribute Name="https://aws.amazon.com/SAML/Attributes/SessionDuration">
|
||||||
|
<AttributeValue>900</AttributeValue>
|
||||||
|
</Attribute>
|
||||||
|
</AttributeStatement>
|
||||||
|
<AuthnStatement AuthnInstant="2012-01-01T12:00:00.000Z" SessionIndex="_00000000-0000-0000-0000-000000000000">
|
||||||
|
<AuthnContext>
|
||||||
|
<AuthnContextClassRef>urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport</AuthnContextClassRef>
|
||||||
|
</AuthnContext>
|
||||||
|
</AuthnStatement>
|
||||||
|
</Assertion>
|
||||||
|
</samlp:Response>""".format(
|
||||||
|
account_id=ACCOUNT_ID,
|
||||||
|
role_name=role_name,
|
||||||
|
provider_name=provider_name,
|
||||||
|
username=user_name,
|
||||||
|
).replace(
|
||||||
|
"\n", ""
|
||||||
|
)
|
||||||
|
|
||||||
|
assume_role_response = client.assume_role_with_saml(
|
||||||
|
RoleArn=role_input,
|
||||||
|
PrincipalArn=principal_role,
|
||||||
|
SAMLAssertion=b64encode(saml_assertion.encode("utf-8")).decode("utf-8"),
|
||||||
|
)
|
||||||
|
|
||||||
|
credentials = assume_role_response["Credentials"]
|
||||||
|
if not settings.TEST_SERVER_MODE:
|
||||||
|
credentials["Expiration"].isoformat().should.equal("2012-01-01T12:15:00+00:00")
|
||||||
|
credentials["SessionToken"].should.have.length_of(356)
|
||||||
|
assert credentials["SessionToken"].startswith("FQoGZXIvYXdzE")
|
||||||
|
credentials["AccessKeyId"].should.have.length_of(20)
|
||||||
|
assert credentials["AccessKeyId"].startswith("ASIA")
|
||||||
|
credentials["SecretAccessKey"].should.have.length_of(40)
|
||||||
|
|
||||||
|
assume_role_response["AssumedRoleUser"]["Arn"].should.equal(
|
||||||
|
"arn:aws:sts::{account_id}:assumed-role/{role_name}/{fed_name}@localhost".format(
|
||||||
|
account_id=ACCOUNT_ID, role_name=role_name, fed_name=user_name
|
||||||
|
)
|
||||||
|
)
|
||||||
|
assert assume_role_response["AssumedRoleUser"]["AssumedRoleId"].startswith("AROA")
|
||||||
|
assert assume_role_response["AssumedRoleUser"]["AssumedRoleId"].endswith(
|
||||||
|
":{fed_name}@localhost".format(fed_name=user_name)
|
||||||
|
)
|
||||||
|
assume_role_response["AssumedRoleUser"]["AssumedRoleId"].should.have.length_of(
|
||||||
|
21 + 1 + len("{fed_name}@localhost".format(fed_name=user_name))
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@freeze_time("2012-01-01 12:00:00")
|
@freeze_time("2012-01-01 12:00:00")
|
||||||
@mock_sts_deprecated
|
@mock_sts_deprecated
|
||||||
def test_assume_role_with_web_identity():
|
def test_assume_role_with_web_identity():
|
||||||
|
Loading…
x
Reference in New Issue
Block a user