From d9cb1f2d353ccc8d0ef77f5a98fc2d8f4ca82252 Mon Sep 17 00:00:00 2001 From: acsbendi Date: Wed, 21 Aug 2019 10:45:36 +0200 Subject: [PATCH 1/7] Implemented returning random assumed role ID. --- moto/sts/models.py | 3 ++- moto/sts/responses.py | 2 +- moto/sts/utils.py | 20 +++++++++++++++----- tests/test_sts/test_sts.py | 30 +++++++++++++++++------------- 4 files changed, 35 insertions(+), 20 deletions(-) diff --git a/moto/sts/models.py b/moto/sts/models.py index e437575b..ff9de84b 100644 --- a/moto/sts/models.py +++ b/moto/sts/models.py @@ -2,7 +2,7 @@ from __future__ import unicode_literals import datetime from moto.core import BaseBackend, BaseModel from moto.core.utils import iso_8601_datetime_with_milliseconds -from moto.sts.utils import random_access_key_id, random_secret_access_key, random_session_token +from moto.sts.utils import random_access_key_id, random_secret_access_key, random_session_token, random_assumed_role_id class Token(BaseModel): @@ -30,6 +30,7 @@ class AssumedRole(BaseModel): self.access_key_id = "ASIA" + random_access_key_id() self.secret_access_key = random_secret_access_key() self.session_token = random_session_token() + self.assumed_role_id = "AROA" + random_assumed_role_id() @property def expiration_ISO8601(self): diff --git a/moto/sts/responses.py b/moto/sts/responses.py index 24ec181b..c04d1636 100644 --- a/moto/sts/responses.py +++ b/moto/sts/responses.py @@ -91,7 +91,7 @@ ASSUME_ROLE_RESPONSE = """ Date: Wed, 21 Aug 2019 12:20:35 +0200 Subject: [PATCH 2/7] Implemented get-caller-identity returning real data depending on the access key used. --- moto/sts/models.py | 12 ++++++- moto/sts/responses.py | 34 ++++++++++++++----- tests/test_sts/test_sts.py | 67 +++++++++++++++++++++++++++++++++----- 3 files changed, 96 insertions(+), 17 deletions(-) diff --git a/moto/sts/models.py b/moto/sts/models.py index ff9de84b..29526006 100644 --- a/moto/sts/models.py +++ b/moto/sts/models.py @@ -22,7 +22,7 @@ class AssumedRole(BaseModel): def __init__(self, role_session_name, role_arn, policy, duration, external_id): self.session_name = role_session_name - self.arn = role_arn + self.arn = role_arn + "/" + role_session_name self.policy = policy now = datetime.datetime.utcnow() self.expiration = now + datetime.timedelta(seconds=duration) @@ -36,6 +36,10 @@ class AssumedRole(BaseModel): def expiration_ISO8601(self): return iso_8601_datetime_with_milliseconds(self.expiration) + @property + def user_id(self): + return self.assumed_role_id + ":" + self.session_name + class STSBackend(BaseBackend): @@ -55,5 +59,11 @@ class STSBackend(BaseBackend): self.assumed_roles.append(role) return role + def get_assumed_role_from_access_key(self, access_key_id): + for assumed_role in self.assumed_roles: + if assumed_role.access_key_id == access_key_id: + return assumed_role + return None + sts_backend = STSBackend() diff --git a/moto/sts/responses.py b/moto/sts/responses.py index c04d1636..2dbe0dc1 100644 --- a/moto/sts/responses.py +++ b/moto/sts/responses.py @@ -1,6 +1,8 @@ from __future__ import unicode_literals from moto.core.responses import BaseResponse +from moto.iam.models import ACCOUNT_ID +from moto.iam import iam_backend from .models import sts_backend @@ -19,7 +21,7 @@ class TokenResponse(BaseResponse): token = sts_backend.get_federation_token( duration=duration, name=name, policy=policy) template = self.response_template(GET_FEDERATION_TOKEN_RESPONSE) - return template.render(token=token) + return template.render(token=token, account_id=ACCOUNT_ID) def assume_role(self): role_session_name = self.querystring.get('RoleSessionName')[0] @@ -41,7 +43,23 @@ class TokenResponse(BaseResponse): def get_caller_identity(self): template = self.response_template(GET_CALLER_IDENTITY_RESPONSE) - return template.render() + + # Default values in case the request does not use valid credentials generated by moto + user_id = "AKIAIOSFODNN7EXAMPLE" + arn = "arn:aws:sts::{account_id}:user/moto".format(account_id=ACCOUNT_ID) + + access_key_id = self.get_current_user() + assumed_role = sts_backend.get_assumed_role_from_access_key(access_key_id) + if assumed_role: + user_id = assumed_role.user_id + arn = assumed_role.arn + + user = iam_backend.get_user_from_access_key_id(access_key_id) + if user: + user_id = user.id + arn = user.arn + + return template.render(account_id=ACCOUNT_ID, user_id=user_id, arn=arn) GET_SESSION_TOKEN_RESPONSE = """ @@ -69,8 +87,8 @@ GET_FEDERATION_TOKEN_RESPONSE = """ - arn:aws:sts::123456789012:user/moto - AKIAIOSFODNN7EXAMPLE - 123456789012 + {{ arn }} + {{ user_id }} + {{ account_id }} c6104cbe-af31-11e0-8154-cbc7ccf896c7 diff --git a/tests/test_sts/test_sts.py b/tests/test_sts/test_sts.py index 3cc44b99..49fc1f2b 100644 --- a/tests/test_sts/test_sts.py +++ b/tests/test_sts/test_sts.py @@ -6,7 +6,8 @@ import boto3 from freezegun import freeze_time import sure # noqa -from moto import mock_sts, mock_sts_deprecated +from moto import mock_sts, mock_sts_deprecated, mock_iam +from moto.iam.models import ACCOUNT_ID @freeze_time("2012-01-01 12:00:00") @@ -26,7 +27,8 @@ def test_get_session_token(): @mock_sts_deprecated def test_get_federation_token(): conn = boto.connect_sts() - token = conn.get_federation_token(duration=123, name="Bob") + token_name = "Bob" + token = conn.get_federation_token(duration=123, name=token_name) token.credentials.expiration.should.equal('2012-01-01T12:02:03.000Z') token.credentials.session_token.should.equal( @@ -35,8 +37,8 @@ def test_get_federation_token(): token.credentials.secret_key.should.equal( "wJalrXUtnFEMI/K7MDENG/bPxRfiCYzEXAMPLEKEY") token.federated_user_arn.should.equal( - "arn:aws:sts::123456789012:federated-user/Bob") - token.federated_user_id.should.equal("123456789012:Bob") + "arn:aws:sts::{account_id}:federated-user/{token_name}".format(account_id=ACCOUNT_ID, token_name=token_name)) + token.federated_user_id.should.equal(str(ACCOUNT_ID) + ":" + token_name) @freeze_time("2012-01-01 12:00:00") @@ -72,17 +74,66 @@ def test_assume_role(): assert credentials['AccessKeyId'].startswith("ASIA") credentials['SecretAccessKey'].should.have.length_of(40) - assume_role_response['AssumedRoleUser']['Arn'].should.equal("arn:aws:iam::123456789012:role/test-role") + assume_role_response['AssumedRoleUser']['Arn'].should.equal("arn:aws:iam::123456789012:role/test-role/" + session_name) assert assume_role_response['AssumedRoleUser']['AssumedRoleId'].startswith("AROA") assert assume_role_response['AssumedRoleUser']['AssumedRoleId'].endswith(":" + session_name) assume_role_response['AssumedRoleUser']['AssumedRoleId'].should.have.length_of(21 + 1 + len(session_name)) @mock_sts -def test_get_caller_identity(): +def test_get_caller_identity_with_default_credentials(): identity = boto3.client( "sts", region_name='us-east-1').get_caller_identity() - identity['Arn'].should.equal('arn:aws:sts::123456789012:user/moto') + identity['Arn'].should.equal('arn:aws:sts::{account_id}:user/moto'.format(account_id=ACCOUNT_ID)) identity['UserId'].should.equal('AKIAIOSFODNN7EXAMPLE') - identity['Account'].should.equal('123456789012') + identity['Account'].should.equal(str(ACCOUNT_ID)) + + +@mock_sts +@mock_iam +def test_get_caller_identity_with_iam_user_credentials(): + iam_client = boto3.client("iam", region_name='us-east-1') + iam_user_name = "new-user" + iam_user = iam_client.create_user(UserName=iam_user_name)['User'] + access_key = iam_client.create_access_key(UserName=iam_user_name)['AccessKey'] + + identity = boto3.client( + "sts", region_name='us-east-1', aws_access_key_id=access_key['AccessKeyId'], + aws_secret_access_key=access_key['SecretAccessKey']).get_caller_identity() + + identity['Arn'].should.equal(iam_user['Arn']) + identity['UserId'].should.equal(iam_user['UserId']) + identity['Account'].should.equal(str(ACCOUNT_ID)) + + +@mock_sts +@mock_iam +def test_get_caller_identity_with_assumed_role_credentials(): + iam_client = boto3.client("iam", region_name='us-east-1') + sts_client = boto3.client("sts", region_name='us-east-1') + iam_role_name = "new-user" + trust_policy_document = { + "Version": "2012-10-17", + "Statement": { + "Effect": "Allow", + "Principal": {"AWS": "arn:aws:iam::{account_id}:root".format(account_id=ACCOUNT_ID)}, + "Action": "sts:AssumeRole" + } + } + iam_role_arn = iam_client.role_arn = iam_client.create_role( + RoleName=iam_role_name, + AssumeRolePolicyDocument=json.dumps(trust_policy_document) + )['Role']['Arn'] + session_name = "new-session" + assumed_role = sts_client.assume_role(RoleArn=iam_role_arn, + RoleSessionName=session_name) + access_key = assumed_role['Credentials'] + + identity = boto3.client( + "sts", region_name='us-east-1', aws_access_key_id=access_key['AccessKeyId'], + aws_secret_access_key=access_key['SecretAccessKey']).get_caller_identity() + + identity['Arn'].should.equal(assumed_role['AssumedRoleUser']['Arn']) + identity['UserId'].should.equal(assumed_role['AssumedRoleUser']['AssumedRoleId']) + identity['Account'].should.equal(str(ACCOUNT_ID)) From 27fdbb7736961cf83522b902fc256cd1ecffc06c Mon Sep 17 00:00:00 2001 From: acsbendi Date: Wed, 21 Aug 2019 12:57:45 +0200 Subject: [PATCH 3/7] Derive ARN of AssumedRoles from its role ARN and session name. --- moto/core/access_control.py | 2 +- moto/sts/models.py | 6 +++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/moto/core/access_control.py b/moto/core/access_control.py index 800b7550..c64acf20 100644 --- a/moto/core/access_control.py +++ b/moto/core/access_control.py @@ -106,7 +106,7 @@ class AssumedRoleAccessKey(object): 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._owner_role_name = assumed_role.role_arn.split("/")[-1] self._session_name = assumed_role.session_name if headers["X-Amz-Security-Token"] != self._session_token: raise CreateAccessKeyFailure(reason="InvalidToken") diff --git a/moto/sts/models.py b/moto/sts/models.py index 8ff6d983..a471b527 100644 --- a/moto/sts/models.py +++ b/moto/sts/models.py @@ -22,7 +22,7 @@ class AssumedRole(BaseModel): def __init__(self, role_session_name, role_arn, policy, duration, external_id): self.session_name = role_session_name - self.arn = role_arn + "/" + role_session_name + self.role_arn = role_arn self.policy = policy now = datetime.datetime.utcnow() self.expiration = now + datetime.timedelta(seconds=duration) @@ -40,6 +40,10 @@ class AssumedRole(BaseModel): def user_id(self): return self.assumed_role_id + ":" + self.session_name + @property + def arn(self): + return self.role_arn + "/" + self.session_name + class STSBackend(BaseBackend): From 6bdbd0dbc87108c968dcfe1066a87564a93b7984 Mon Sep 17 00:00:00 2001 From: acsbendi Date: Wed, 21 Aug 2019 13:17:58 +0200 Subject: [PATCH 4/7] Fixed a broken test case and parameterized account ID in STS tests. --- tests/test_sts/test_sts.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/tests/test_sts/test_sts.py b/tests/test_sts/test_sts.py index f61fa3e0..efbe8314 100644 --- a/tests/test_sts/test_sts.py +++ b/tests/test_sts/test_sts.py @@ -66,7 +66,7 @@ def test_assume_role(): }, ] }) - s3_role = "arn:aws:iam::123456789012:role/test-role" + s3_role = "arn:aws:iam::{account_id}:role/test-role".format(account_id=ACCOUNT_ID) assume_role_response = client.assume_role(RoleArn=s3_role, RoleSessionName=session_name, Policy=policy, DurationSeconds=900) @@ -78,7 +78,7 @@ def test_assume_role(): assert credentials['AccessKeyId'].startswith("ASIA") credentials['SecretAccessKey'].should.have.length_of(40) - assume_role_response['AssumedRoleUser']['Arn'].should.equal("arn:aws:iam::123456789012:role/test-role/" + session_name) + assume_role_response['AssumedRoleUser']['Arn'].should.equal(s3_role + "/" + session_name) assert assume_role_response['AssumedRoleUser']['AssumedRoleId'].startswith("AROA") assert assume_role_response['AssumedRoleUser']['AssumedRoleId'].endswith(":" + session_name) assume_role_response['AssumedRoleUser']['AssumedRoleId'].should.have.length_of(21 + 1 + len(session_name)) @@ -103,9 +103,10 @@ def test_assume_role_with_web_identity(): }, ] }) - s3_role = "arn:aws:iam::123456789012:role/test-role" + s3_role = "arn:aws:iam::{account_id}:role/test-role".format(account_id=ACCOUNT_ID) + session_name = "session-name" role = conn.assume_role_with_web_identity( - s3_role, "session-name", policy, duration_seconds=123) + s3_role, session_name, policy, duration_seconds=123) credentials = role.credentials credentials.expiration.should.equal('2012-01-01T12:02:03.000Z') @@ -115,7 +116,7 @@ def test_assume_role_with_web_identity(): assert credentials.access_key.startswith("ASIA") credentials.secret_key.should.have.length_of(40) - role.user.arn.should.equal("arn:aws:iam::123456789012:role/test-role") + role.user.arn.should.equal(s3_role + "/" + session_name) role.user.assume_role_id.should.contain("session-name") From 3012740699f794454d546bce71bbb9306342a58e Mon Sep 17 00:00:00 2001 From: acsbendi Date: Wed, 21 Aug 2019 19:47:12 +0200 Subject: [PATCH 5/7] Fixed AssumedRole ARN. --- moto/sts/models.py | 7 ++++++- tests/test_sts/test_sts.py | 12 ++++++++---- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/moto/sts/models.py b/moto/sts/models.py index a471b527..c2ff7a8d 100644 --- a/moto/sts/models.py +++ b/moto/sts/models.py @@ -2,6 +2,7 @@ from __future__ import unicode_literals import datetime from moto.core import BaseBackend, BaseModel from moto.core.utils import iso_8601_datetime_with_milliseconds +from moto.iam.models import ACCOUNT_ID from moto.sts.utils import random_access_key_id, random_secret_access_key, random_session_token, random_assumed_role_id @@ -42,7 +43,11 @@ class AssumedRole(BaseModel): @property def arn(self): - return self.role_arn + "/" + self.session_name + return "arn:aws:sts::{account_id}:assumed-role/{role_name}/{session_name}".format( + account_id=ACCOUNT_ID, + role_name=self.role_arn.split("/")[-1], + session_name=self.session_name + ) class STSBackend(BaseBackend): diff --git a/tests/test_sts/test_sts.py b/tests/test_sts/test_sts.py index efbe8314..ac7c4ea1 100644 --- a/tests/test_sts/test_sts.py +++ b/tests/test_sts/test_sts.py @@ -66,7 +66,8 @@ def test_assume_role(): }, ] }) - s3_role = "arn:aws:iam::{account_id}:role/test-role".format(account_id=ACCOUNT_ID) + role_name = "test-role" + s3_role = "arn:aws:iam::{account_id}:role/{role_name}".format(account_id=ACCOUNT_ID, role_name=role_name) assume_role_response = client.assume_role(RoleArn=s3_role, RoleSessionName=session_name, Policy=policy, DurationSeconds=900) @@ -78,7 +79,8 @@ def test_assume_role(): assert credentials['AccessKeyId'].startswith("ASIA") credentials['SecretAccessKey'].should.have.length_of(40) - assume_role_response['AssumedRoleUser']['Arn'].should.equal(s3_role + "/" + session_name) + assume_role_response['AssumedRoleUser']['Arn'].should.equal("arn:aws:sts::{account_id}:assumed-role/{role_name}/{session_name}".format( + account_id=ACCOUNT_ID, role_name=role_name, session_name=session_name)) assert assume_role_response['AssumedRoleUser']['AssumedRoleId'].startswith("AROA") assert assume_role_response['AssumedRoleUser']['AssumedRoleId'].endswith(":" + session_name) assume_role_response['AssumedRoleUser']['AssumedRoleId'].should.have.length_of(21 + 1 + len(session_name)) @@ -103,7 +105,8 @@ def test_assume_role_with_web_identity(): }, ] }) - s3_role = "arn:aws:iam::{account_id}:role/test-role".format(account_id=ACCOUNT_ID) + role_name = "test-role" + s3_role = "arn:aws:iam::{account_id}:role/{role_name}".format(account_id=ACCOUNT_ID, role_name=role_name) session_name = "session-name" role = conn.assume_role_with_web_identity( s3_role, session_name, policy, duration_seconds=123) @@ -116,7 +119,8 @@ def test_assume_role_with_web_identity(): assert credentials.access_key.startswith("ASIA") credentials.secret_key.should.have.length_of(40) - role.user.arn.should.equal(s3_role + "/" + session_name) + role.user.arn.should.equal("arn:aws:sts::{account_id}:assumed-role/{role_name}/{session_name}".format( + account_id=ACCOUNT_ID, role_name=role_name, session_name=session_name)) role.user.assume_role_id.should.contain("session-name") From addb63108124c5e2b6c4062aa834c5a958f94e25 Mon Sep 17 00:00:00 2001 From: acsbendi Date: Thu, 22 Aug 2019 11:06:42 +0200 Subject: [PATCH 6/7] Skip checking the expiration of AssumedRole in server mode. --- tests/test_sts/test_sts.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/test_sts/test_sts.py b/tests/test_sts/test_sts.py index ac7c4ea1..b047a8d1 100644 --- a/tests/test_sts/test_sts.py +++ b/tests/test_sts/test_sts.py @@ -9,7 +9,7 @@ from nose.tools import assert_raises import sure # noqa -from moto import mock_sts, mock_sts_deprecated, mock_iam +from moto import mock_sts, mock_sts_deprecated, mock_iam, settings from moto.iam.models import ACCOUNT_ID from moto.sts.responses import MAX_FEDERATION_TOKEN_POLICY_LENGTH @@ -72,7 +72,8 @@ def test_assume_role(): Policy=policy, DurationSeconds=900) credentials = assume_role_response['Credentials'] - credentials['Expiration'].isoformat().should.equal('2012-01-01T12:15:00+00:00') + 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) From cf2dae0ce8866f67ba088b36bafe3ec6c9827e1c Mon Sep 17 00:00:00 2001 From: acsbendi Date: Thu, 22 Aug 2019 18:09:52 +0200 Subject: [PATCH 7/7] Calling sts:GetCallerIdentity is always allowed. --- moto/core/access_control.py | 2 ++ tests/test_core/test_auth.py | 21 +++++++++++++++++++++ 2 files changed, 23 insertions(+) diff --git a/moto/core/access_control.py b/moto/core/access_control.py index c64acf20..3fb11eeb 100644 --- a/moto/core/access_control.py +++ b/moto/core/access_control.py @@ -172,6 +172,8 @@ class IAMRequestBase(object): self._raise_signature_does_not_match() def check_action_permitted(self): + if self._action == 'sts:GetCallerIdentity': # always allowed, even if there's an explicit Deny for it + return True policies = self._access_key.collect_policies() permitted = False diff --git a/tests/test_core/test_auth.py b/tests/test_core/test_auth.py index 3a1107ea..00229f80 100644 --- a/tests/test_core/test_auth.py +++ b/tests/test_core/test_auth.py @@ -273,6 +273,27 @@ def test_access_denied_with_denying_policy(): ) +@set_initial_no_auth_action_count(3) +@mock_sts +def test_get_caller_identity_allowed_with_denying_policy(): + user_name = 'test-user' + inline_policy_document = { + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Deny", + "Action": "sts:GetCallerIdentity", + "Resource": "*" + } + ] + } + access_key = create_user_with_access_key_and_inline_policy(user_name, inline_policy_document) + client = boto3.client('sts', region_name='us-east-1', + aws_access_key_id=access_key['AccessKeyId'], + aws_secret_access_key=access_key['SecretAccessKey']) + client.get_caller_identity().should.be.a(dict) + + @set_initial_no_auth_action_count(3) @mock_ec2 def test_allowed_with_wildcard_action():