moto/moto/sts/models.py

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

209 lines
7.2 KiB
Python
Raw Normal View History

2013-05-24 17:22:34 -04:00
import datetime
2022-08-13 09:49:43 +00:00
import re
from base64 import b64decode
from typing import Any, Dict, List, Optional, Tuple
import xmltodict
2023-04-26 22:20:28 +00:00
from moto.core.base_backend import BackendDict, BaseBackend
from moto.core.common_models import BaseModel
from moto.core.utils import (
get_partition_from_region,
iso_8601_datetime_with_milliseconds,
utcnow,
)
from moto.iam.models import AccessKey, iam_backends
from moto.sts.utils import (
Fix saml-assertion parsing in assume-role-with-saml (#3523) * Retrieve SAML Attribute by Name instead of relying on order which is too fragile * Handle case when SAML Attribute SessionDuration is not provided, as it is not a required attribute from SAML response When session duration not provided, AWS consider by default a duration of one hour as cited in the following documentation: "If this attribute is not present, then the credential last for one hour (the default value of the DurationSeconds parameter of the AssumeRoleWithSAML API)." https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_providers_create_saml_assertions.html#saml_role-session-duration Traceback was: [...] File "/Users/benjamin.brabant/Projects/PERSO/moto/moto/sts/responses.py", line 79, in assume_role_with_saml role = sts_backend.assume_role_with_saml( File "/Users/benjamin.brabant/Projects/PERSO/moto/moto/sts/models.py", line 99, in assume_role_with_saml role = AssumedRole(**kwargs) TypeError: __init__() missing 1 required positional argument: 'duration' * Process saml xml namespaces properly instead of relying on textual prefix that can vary between identity providers * Handle when SAML response AttributeValue xml tag contains attributes that force xmltodict to build a dictionary as for complex types instead of directly returning string value Leverage force_cdata option of xmltodict parser that always return a complex dictionary even if xml tag contains only text and no attributes. * Improve existing test_assume_role_with_saml to be coherent with other assume_role_with_saml tests and remove dead code at the same time
2020-12-08 10:08:40 +01:00
DEFAULT_STS_SESSION_DURATION,
random_assumed_role_id,
random_session_token,
2019-10-31 08:44:26 -07:00
)
2013-05-24 17:22:34 -04:00
2017-03-11 23:41:12 -05:00
class Token(BaseModel):
2023-04-26 22:20:28 +00:00
def __init__(self, duration: int, name: Optional[str] = None):
now = utcnow()
2013-05-24 17:22:34 -04:00
self.expiration = now + datetime.timedelta(seconds=duration)
self.name = name
self.policy = None
2013-05-24 17:22:34 -04:00
@property
2023-04-26 22:20:28 +00:00
def expiration_ISO8601(self) -> str:
2014-11-29 23:34:40 -05:00
return iso_8601_datetime_with_milliseconds(self.expiration)
2013-05-24 17:22:34 -04:00
2017-03-11 23:41:12 -05:00
class AssumedRole(BaseModel):
2022-08-13 09:49:43 +00:00
def __init__(
self,
2023-04-26 22:20:28 +00:00
account_id: str,
region_name: str,
2023-04-26 22:20:28 +00:00
access_key: AccessKey,
role_session_name: str,
role_arn: str,
policy: str,
duration: int,
external_id: str,
2022-08-13 09:49:43 +00:00
):
self.account_id = account_id
self.region_name = region_name
2013-05-24 17:22:34 -04:00
self.session_name = role_session_name
self.role_arn = role_arn
2013-05-24 17:22:34 -04:00
self.policy = policy
now = utcnow()
2013-05-24 17:22:34 -04:00
self.expiration = now + datetime.timedelta(seconds=duration)
self.external_id = external_id
2022-08-13 09:49:43 +00:00
self.access_key = access_key
self.access_key_id = access_key.access_key_id
self.secret_access_key = access_key.secret_access_key
self.session_token = random_session_token()
2013-05-24 17:22:34 -04:00
@property
2023-04-26 22:20:28 +00:00
def expiration_ISO8601(self) -> str:
2014-11-29 23:34:40 -05:00
return iso_8601_datetime_with_milliseconds(self.expiration)
2013-05-24 17:22:34 -04:00
@property
2023-04-26 22:20:28 +00:00
def user_id(self) -> str:
iam_backend = iam_backends[self.account_id]["global"]
try:
role_id = iam_backend.get_role_by_arn(arn=self.role_arn).id
except Exception:
role_id = "AROA" + random_assumed_role_id()
return role_id + ":" + self.session_name
@property
2023-04-26 22:20:28 +00:00
def arn(self) -> str:
partition = get_partition_from_region(self.region_name)
return f"arn:{partition}:sts::{self.account_id}:assumed-role/{self.role_arn.split('/')[-1]}/{self.session_name}"
2013-05-24 17:22:34 -04:00
class STSBackend(BaseBackend):
2023-04-26 22:20:28 +00:00
def __init__(self, region_name: str, account_id: str):
super().__init__(region_name, account_id)
2023-04-26 22:20:28 +00:00
self.assumed_roles: List[AssumedRole] = []
2023-04-26 22:20:28 +00:00
def get_session_token(self, duration: int) -> Token:
return Token(duration=duration)
2013-05-24 17:22:34 -04:00
2023-04-26 22:20:28 +00:00
def get_federation_token(self, name: Optional[str], duration: int) -> Token:
return Token(duration=duration, name=name)
2023-04-26 22:20:28 +00:00
def assume_role(
self,
region_name: str,
2023-04-26 22:20:28 +00:00
role_session_name: str,
role_arn: str,
policy: str,
duration: int,
external_id: str,
) -> AssumedRole:
2022-08-13 09:49:43 +00:00
"""
Assume an IAM Role. Note that the role does not need to exist. The ARN can point to another account, providing an opportunity to switch accounts.
"""
account_id, access_key = self._create_access_key(role=role_arn)
role = AssumedRole(
account_id=account_id,
region_name=region_name,
access_key=access_key,
role_session_name=role_session_name,
role_arn=role_arn,
policy=policy,
duration=duration,
external_id=external_id,
2022-08-13 09:49:43 +00:00
)
access_key.role_arn = role_arn
account_backend = sts_backends[account_id]["global"]
account_backend.assumed_roles.append(role)
2013-05-24 17:22:34 -04:00
return role
2023-04-26 22:20:28 +00:00
def get_assumed_role_from_access_key(
self, access_key_id: str
) -> Optional[AssumedRole]:
for assumed_role in self.assumed_roles:
if assumed_role.access_key_id == access_key_id:
return assumed_role
return None
2023-04-26 22:20:28 +00:00
def assume_role_with_web_identity(self, **kwargs: Any) -> AssumedRole:
return self.assume_role(**kwargs)
2023-04-26 22:20:28 +00:00
def assume_role_with_saml(self, **kwargs: Any) -> AssumedRole:
del kwargs["principal_arn"]
saml_assertion_encoded = kwargs.pop("saml_assertion")
saml_assertion_decoded = b64decode(saml_assertion_encoded)
Fix saml-assertion parsing in assume-role-with-saml (#3523) * Retrieve SAML Attribute by Name instead of relying on order which is too fragile * Handle case when SAML Attribute SessionDuration is not provided, as it is not a required attribute from SAML response When session duration not provided, AWS consider by default a duration of one hour as cited in the following documentation: "If this attribute is not present, then the credential last for one hour (the default value of the DurationSeconds parameter of the AssumeRoleWithSAML API)." https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_providers_create_saml_assertions.html#saml_role-session-duration Traceback was: [...] File "/Users/benjamin.brabant/Projects/PERSO/moto/moto/sts/responses.py", line 79, in assume_role_with_saml role = sts_backend.assume_role_with_saml( File "/Users/benjamin.brabant/Projects/PERSO/moto/moto/sts/models.py", line 99, in assume_role_with_saml role = AssumedRole(**kwargs) TypeError: __init__() missing 1 required positional argument: 'duration' * Process saml xml namespaces properly instead of relying on textual prefix that can vary between identity providers * Handle when SAML response AttributeValue xml tag contains attributes that force xmltodict to build a dictionary as for complex types instead of directly returning string value Leverage force_cdata option of xmltodict parser that always return a complex dictionary even if xml tag contains only text and no attributes. * Improve existing test_assume_role_with_saml to be coherent with other assume_role_with_saml tests and remove dead code at the same time
2020-12-08 10:08:40 +01:00
namespaces = {
"urn:oasis:names:tc:SAML:2.0:protocol": "samlp",
"urn:oasis:names:tc:SAML:2.0:assertion": "saml",
}
saml_assertion = xmltodict.parse(
saml_assertion_decoded.decode("utf-8"),
force_cdata=True,
process_namespaces=True,
namespaces=namespaces,
namespace_separator="|",
)
Fix saml-assertion parsing in assume-role-with-saml (#3523) * Retrieve SAML Attribute by Name instead of relying on order which is too fragile * Handle case when SAML Attribute SessionDuration is not provided, as it is not a required attribute from SAML response When session duration not provided, AWS consider by default a duration of one hour as cited in the following documentation: "If this attribute is not present, then the credential last for one hour (the default value of the DurationSeconds parameter of the AssumeRoleWithSAML API)." https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_providers_create_saml_assertions.html#saml_role-session-duration Traceback was: [...] File "/Users/benjamin.brabant/Projects/PERSO/moto/moto/sts/responses.py", line 79, in assume_role_with_saml role = sts_backend.assume_role_with_saml( File "/Users/benjamin.brabant/Projects/PERSO/moto/moto/sts/models.py", line 99, in assume_role_with_saml role = AssumedRole(**kwargs) TypeError: __init__() missing 1 required positional argument: 'duration' * Process saml xml namespaces properly instead of relying on textual prefix that can vary between identity providers * Handle when SAML response AttributeValue xml tag contains attributes that force xmltodict to build a dictionary as for complex types instead of directly returning string value Leverage force_cdata option of xmltodict parser that always return a complex dictionary even if xml tag contains only text and no attributes. * Improve existing test_assume_role_with_saml to be coherent with other assume_role_with_saml tests and remove dead code at the same time
2020-12-08 10:08:40 +01:00
2022-08-13 09:49:43 +00:00
target_role = None
saml_assertion_attributes = saml_assertion["samlp|Response"]["saml|Assertion"][
"saml|AttributeStatement"
]["saml|Attribute"]
Fix saml-assertion parsing in assume-role-with-saml (#3523) * Retrieve SAML Attribute by Name instead of relying on order which is too fragile * Handle case when SAML Attribute SessionDuration is not provided, as it is not a required attribute from SAML response When session duration not provided, AWS consider by default a duration of one hour as cited in the following documentation: "If this attribute is not present, then the credential last for one hour (the default value of the DurationSeconds parameter of the AssumeRoleWithSAML API)." https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_providers_create_saml_assertions.html#saml_role-session-duration Traceback was: [...] File "/Users/benjamin.brabant/Projects/PERSO/moto/moto/sts/responses.py", line 79, in assume_role_with_saml role = sts_backend.assume_role_with_saml( File "/Users/benjamin.brabant/Projects/PERSO/moto/moto/sts/models.py", line 99, in assume_role_with_saml role = AssumedRole(**kwargs) TypeError: __init__() missing 1 required positional argument: 'duration' * Process saml xml namespaces properly instead of relying on textual prefix that can vary between identity providers * Handle when SAML response AttributeValue xml tag contains attributes that force xmltodict to build a dictionary as for complex types instead of directly returning string value Leverage force_cdata option of xmltodict parser that always return a complex dictionary even if xml tag contains only text and no attributes. * Improve existing test_assume_role_with_saml to be coherent with other assume_role_with_saml tests and remove dead code at the same time
2020-12-08 10:08:40 +01:00
for attribute in saml_assertion_attributes:
if (
attribute["@Name"]
== "https://aws.amazon.com/SAML/Attributes/RoleSessionName"
):
kwargs["role_session_name"] = attribute["saml|AttributeValue"]["#text"]
Fix saml-assertion parsing in assume-role-with-saml (#3523) * Retrieve SAML Attribute by Name instead of relying on order which is too fragile * Handle case when SAML Attribute SessionDuration is not provided, as it is not a required attribute from SAML response When session duration not provided, AWS consider by default a duration of one hour as cited in the following documentation: "If this attribute is not present, then the credential last for one hour (the default value of the DurationSeconds parameter of the AssumeRoleWithSAML API)." https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_providers_create_saml_assertions.html#saml_role-session-duration Traceback was: [...] File "/Users/benjamin.brabant/Projects/PERSO/moto/moto/sts/responses.py", line 79, in assume_role_with_saml role = sts_backend.assume_role_with_saml( File "/Users/benjamin.brabant/Projects/PERSO/moto/moto/sts/models.py", line 99, in assume_role_with_saml role = AssumedRole(**kwargs) TypeError: __init__() missing 1 required positional argument: 'duration' * Process saml xml namespaces properly instead of relying on textual prefix that can vary between identity providers * Handle when SAML response AttributeValue xml tag contains attributes that force xmltodict to build a dictionary as for complex types instead of directly returning string value Leverage force_cdata option of xmltodict parser that always return a complex dictionary even if xml tag contains only text and no attributes. * Improve existing test_assume_role_with_saml to be coherent with other assume_role_with_saml tests and remove dead code at the same time
2020-12-08 10:08:40 +01:00
if (
attribute["@Name"]
== "https://aws.amazon.com/SAML/Attributes/SessionDuration"
):
kwargs["duration"] = int(attribute["saml|AttributeValue"]["#text"])
2022-08-13 09:49:43 +00:00
if attribute["@Name"] == "https://aws.amazon.com/SAML/Attributes/Role":
target_role = attribute["saml|AttributeValue"]["#text"].split(",")[0]
Fix saml-assertion parsing in assume-role-with-saml (#3523) * Retrieve SAML Attribute by Name instead of relying on order which is too fragile * Handle case when SAML Attribute SessionDuration is not provided, as it is not a required attribute from SAML response When session duration not provided, AWS consider by default a duration of one hour as cited in the following documentation: "If this attribute is not present, then the credential last for one hour (the default value of the DurationSeconds parameter of the AssumeRoleWithSAML API)." https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_providers_create_saml_assertions.html#saml_role-session-duration Traceback was: [...] File "/Users/benjamin.brabant/Projects/PERSO/moto/moto/sts/responses.py", line 79, in assume_role_with_saml role = sts_backend.assume_role_with_saml( File "/Users/benjamin.brabant/Projects/PERSO/moto/moto/sts/models.py", line 99, in assume_role_with_saml role = AssumedRole(**kwargs) TypeError: __init__() missing 1 required positional argument: 'duration' * Process saml xml namespaces properly instead of relying on textual prefix that can vary between identity providers * Handle when SAML response AttributeValue xml tag contains attributes that force xmltodict to build a dictionary as for complex types instead of directly returning string value Leverage force_cdata option of xmltodict parser that always return a complex dictionary even if xml tag contains only text and no attributes. * Improve existing test_assume_role_with_saml to be coherent with other assume_role_with_saml tests and remove dead code at the same time
2020-12-08 10:08:40 +01:00
if "duration" not in kwargs:
kwargs["duration"] = DEFAULT_STS_SESSION_DURATION
2023-04-26 22:20:28 +00:00
account_id, access_key = self._create_access_key(role=target_role) # type: ignore
2022-08-13 09:49:43 +00:00
kwargs["account_id"] = account_id
kwargs["region_name"] = self.region_name
2022-08-13 09:49:43 +00:00
kwargs["access_key"] = access_key
kwargs["external_id"] = None
kwargs["policy"] = None
role = AssumedRole(**kwargs)
self.assumed_roles.append(role)
return role
def get_caller_identity(
self, access_key_id: str, region: str
) -> Tuple[str, str, str]:
2022-08-13 09:49:43 +00:00
assumed_role = self.get_assumed_role_from_access_key(access_key_id)
if assumed_role:
return assumed_role.user_id, assumed_role.arn, assumed_role.account_id
iam_backend = iam_backends[self.account_id]["global"]
user = iam_backend.get_user_from_access_key_id(access_key_id)
if user:
return user.id, user.arn, user.account_id
# Default values in case the request does not use valid credentials generated by moto
partition = get_partition_from_region(region)
2022-08-13 09:49:43 +00:00
user_id = "AKIAIOSFODNN7EXAMPLE"
arn = f"arn:{partition}:sts::{self.account_id}:user/moto"
2022-08-13 09:49:43 +00:00
return user_id, arn, self.account_id
2023-04-26 22:20:28 +00:00
def _create_access_key(self, role: str) -> Tuple[str, AccessKey]:
2022-08-13 09:49:43 +00:00
account_id_match = re.search(r"arn:aws:iam::([0-9]+).+", role)
if account_id_match:
account_id = account_id_match.group(1)
else:
account_id = self.account_id
iam_backend = iam_backends[account_id]["global"]
return account_id, iam_backend.create_temp_access_key()
2017-02-23 21:37:43 -05:00
2023-03-14 18:52:07 -01:00
sts_backends = BackendDict(
STSBackend, "sts", use_boto3_regions=False, additional_regions=["global"]
)