def __init__(self, assertion, credential_path='~/.aws', profile='default', region='us-east-1', role=None): cred_dir = os.path.expanduser(credential_path) cred_file = os.path.join(cred_dir, 'credentials') boto_logger = logging.getLogger('botocore') boto_logger.setLevel(logging.WARNING) if not os.path.exists(cred_dir): LOG.info('Creating missing AWS Credentials dir {dir} 📁'.format( dir=cred_dir)) os.makedirs(cred_dir) self.sts = boto3.client('sts') self.profile = profile self.region = region self.assertion = SamlAssertion(assertion) self.writer = Credentials(cred_file) # Populated by self.assume_role() self.creds = { 'AccessKeyId': None, 'SecretAccessKey': None, 'SessionToken': None, 'Expiration': None } self.session_token = None self.role = role self.available_roles()
def test_white_space_is_removed(self): assertion = saml_assertion([' {},{} '.format(idp_arn, dev_arn)]) assert SamlAssertion(assertion).roles() == [{ 'role': dev_arn, 'principle': idp_arn }]
def test_principle_can_be_first(self): assertion = saml_assertion(['{},{}'.format(idp_arn, dev_arn)]) assert SamlAssertion(assertion).roles() == [{ 'role': dev_arn, 'principle': idp_arn }]
def test_roles_are_extracted(self): assertion = saml_assertion(['{},{}'.format(dev_arn, idp_arn)]) assert SamlAssertion(assertion).roles() == [{ 'role': dev_arn, 'principle': idp_arn }]
def test_white_space_is_removed(self): assertion = saml_assertion([f" {idp_arn},{dev_arn} "]) assert SamlAssertion(assertion).roles() == [ { "role": dev_arn, "principle": idp_arn, }, ]
def test_principle_can_be_first(self): assertion = saml_assertion([f"{idp_arn},{dev_arn}"]) assert SamlAssertion(assertion).roles() == [ { "role": dev_arn, "principle": idp_arn, }, ]
def test_roles_are_extracted(self): assertion = saml_assertion([f"{dev_arn},{idp_arn}"]) assert SamlAssertion(assertion).roles() == [ { "role": dev_arn, "principle": idp_arn, }, ]
def __init__( self, assertion, credential_path="~/.aws", profile="default", region="us-east-1", role=None, session_duration=None, ): cred_dir = os.path.expanduser(credential_path) cred_file = os.path.join(cred_dir, "credentials") boto_logger = logging.getLogger("botocore") boto_logger.setLevel(logging.WARNING) if not os.path.exists(cred_dir): LOG.info( "Creating missing AWS Credentials dir {dir} 📁".format( dir=cred_dir, ), ) os.makedirs(cred_dir) self.profile = profile self.region = region boto3.setup_default_session() self.sts = boto3.client("sts", region_name=self.region) self.assertion = SamlAssertion(assertion) self.writer = Credentials(cred_file) # Populated by self.assume_role() self.creds = { "AccessKeyId": None, "SecretAccessKey": None, "SessionToken": None, "Expiration": None, } self.session_token = None self.role = role if session_duration: self.duration = session_duration else: self.duration = 3600 self.available_roles()
def test_multiple_roles_are_returned(self): assertion = saml_assertion([ '{},{}'.format(dev_arn, idp_arn), '{},{}'.format(qa_arn, idp2_arn) ]) assert SamlAssertion(assertion).roles() == [{ 'role': dev_arn, 'principle': idp_arn }, { 'role': qa_arn, 'principle': idp2_arn }]
def test_multiple_roles_are_returned(self): assertion = saml_assertion([ f"{dev_arn},{idp_arn}", f"{qa_arn},{idp2_arn}", ], ) assert SamlAssertion(assertion).roles() == [ { "role": dev_arn, "principle": idp_arn, }, { "role": qa_arn, "principle": idp2_arn, }, ]
class Session: """Amazon Federated Session Generator. This class is used to contact Amazon with a specific SAML Assertion and get back a set of temporary Federated credentials. These credentials are written to disk (using the Credentials object above). This object is meant to be used once -- as SAML Assertions are one-time-use objects. """ def __init__( self, assertion, credential_path="~/.aws", profile="default", region="us-east-1", role=None, session_duration=None, ): cred_dir = os.path.expanduser(credential_path) cred_file = os.path.join(cred_dir, "credentials") boto_logger = logging.getLogger("botocore") boto_logger.setLevel(logging.WARNING) if not os.path.exists(cred_dir): LOG.info( "Creating missing AWS Credentials dir {dir} 📁".format( dir=cred_dir, ), ) os.makedirs(cred_dir) self.profile = profile self.region = region boto3.setup_default_session() self.sts = boto3.client("sts", region_name=self.region) self.assertion = SamlAssertion(assertion) self.writer = Credentials(cred_file) # Populated by self.assume_role() self.creds = { "AccessKeyId": None, "SecretAccessKey": None, "SessionToken": None, "Expiration": None, } self.session_token = None self.role = role if session_duration: self.duration = session_duration else: self.duration = 3600 self.available_roles() @property def is_valid(self): """Return True if the Session is still valid. Returns: Bool """ # Consider the tokens expired when they have 10m left try: msg = "Session Expiration: {} // Now: {}".format( self.creds["Expiration"], datetime.datetime.utcnow(), ) LOG.debug(msg) offset = datetime.timedelta(seconds=600) now = datetime.datetime.utcnow() expir = datetime.datetime.strptime( str(self.creds["Expiration"]), "%Y-%m-%d %H:%M:%S+00:00", ) return (now + offset) < expir except (ValueError, TypeError): return False def available_roles(self): """Return the roles available from AWS. Returns: Tuple, list of roles as dicts and a bool that is true when multiple accounts were found """ multiple_accounts = False first_account = "" formatted_roles = [] for role in self.assertion.roles(): account = role["role"].split(":")[4] role_name = role["role"].split(":")[5].split("/")[1] formatted_roles.append( { "account": account, "role_name": role_name, "arn": role["role"], "principle": role["principle"], }, ) if first_account == "": first_account = account elif first_account != account: multiple_accounts = True if multiple_accounts: formatted_roles = self.account_ids_to_names(formatted_roles) formatted_roles = sorted( formatted_roles, key=lambda k: (k["account"], k["role_name"]), ) # set the role role index after sorting i = 0 for role in formatted_roles: role["roleIdx"] = i i = i + 1 self.roles = formatted_roles return self.roles def assume_role(self, print_only=False): """Use the SAML Assertion to actually get the credentials. Uses the supplied (one time use!) SAML Assertion to go out to Amazon and get back a set of temporary credentials. These are written out to disk and can be used for an hour before they need to be replaced. """ if self.role is None: if len(self.assertion.roles()) > 1: raise MultipleRoles self.role = 0 LOG.info("Assuming role: {}".format(self.roles[self.role]["arn"])) try: session = self.sts.assume_role_with_saml( RoleArn=self.roles[self.role]["arn"], PrincipalArn=self.roles[self.role]["principle"], SAMLAssertion=self.assertion.encode(), DurationSeconds=self.duration, ) except botocore.exceptions.ClientError: # Try again with the default duration msg = ("Error assuming session with duration " "{}. Retrying with 3600.".format(self.duration)) LOG.warning(msg) session = self.sts.assume_role_with_saml( RoleArn=self.roles[self.role]["arn"], PrincipalArn=self.roles[self.role]["principle"], SAMLAssertion=self.assertion.encode(), DurationSeconds=3600, ) self.creds = session["Credentials"] if print_only: self._print_creds() else: self._write() def _write(self): """Write out our secrets to the Credentials object.""" self.writer.add_profile( name=self.profile, region=self.region, creds=self.creds, ) LOG.info( "Current time is {time}".format( time=datetime.datetime.utcnow(), ), ) LOG.info( "Session expires at {time} ⏳".format( time=self.creds["Expiration"], ), ) def _print_creds(self): """Print out the retrieved credentials to the screen""" cred_str = "AWS_ACCESS_KEY_ID = {}\n".format(self.creds["AccessKeyId"]) cred_str = "{}AWS_SECRET_ACCESS_KEY = {}\n".format( cred_str, self.creds["SecretAccessKey"], ) cred_str = "{}AWS_SESSION_TOKEN = {}".format( cred_str, self.creds["SessionToken"], ) LOG.info(f"AWS Credentials: \n\n\n{cred_str}\n\n") def generate_aws_console_url(self, issuer): """Generate a URL for logging into the AWS console with the current session key Returns: string URL for console login """ creds = { "sessionId": self.creds["AccessKeyId"], "sessionKey": self.creds["SecretAccessKey"], "sessionToken": self.creds["SessionToken"], } params = { "Action": "getSigninToken", "SessionDuration": self.duration, "Session": json.dumps(creds), } token_url = "https://signin.aws.amazon.com/federation" resp = requests.get(token_url, params=params) token = resp.json()["SigninToken"] console_url = "https%3A//console.aws.amazon.com/" params = ("?Action=login&Issuer={}&Destination={}" "&SigninToken={}").format( issuer, console_url, token, ) url = f"https://signin.aws.amazon.com/federation{params}" return url def export_creds_to_var_string(self): """Export the current credentials as environment vaiables""" var_string = ("export AWS_ACCESS_KEY_ID={}; " "export AWS_SECRET_ACCESS_KEY={}; " "export AWS_SESSION_TOKEN={};").format( self.creds["AccessKeyId"], self.creds["SecretAccessKey"], self.creds["SessionToken"], ) return var_string def account_ids_to_names(self, roles): """Turn account IDs into user-friendly names args: roles: Dict of the roles from AWS to get the account names for Returns: Dict of account names and role names for user selection """ try: accounts = self.get_account_name_map() except Exception: msg = ("Error retrieving AWS account name/ID map. " "Falling back to just account IDs") LOG.warning(msg) return roles for role in roles: role["account"] = accounts[role["account"]] LOG.debug(f"AWS roles with friendly names: {accounts}") return roles def get_account_name_map(self): """Get the friendly to ID mappings from AWS via hacktastic HTML Returns: Dict of account numbers with names """ url = "https://signin.aws.amazon.com/saml" headers = {"Content-Type": "application/x-www-form-urlencoded"} data = {"SAMLResponse": self.assertion.encode()} resp = requests.post(url=url, headers=headers, data=data) resp.raise_for_status() return self.account_names_from_html(resp.text) @staticmethod def account_names_from_html(html): """Parse the AWS SAML login page HTML for account numbers and names Returns: Dict of the account numbers and names """ accounts = {} soup = bs4.BeautifulSoup(html, "html.parser") for account in soup.find_all("div", {"class": "saml-account-name"}): name_string = account.text a_id = re.match(r".*\((\d+)\)", name_string).group(1) a_name = re.match(r"\S+\s(\S+)", name_string).group(1) accounts[a_id] = a_name LOG.debug(f"AWS account map: {accounts}") return accounts
def test_assertion_is_encoded(self): test_str = str.encode('test encoding') assert SamlAssertion(test_str).encode() == 'dGVzdCBlbmNvZGluZw=='
class Session(object): """Amazon Federated Session Generator. This class is used to contact Amazon with a specific SAML Assertion and get back a set of temporary Federated credentials. These credentials are written to disk (using the Credentials object above). This object is meant to be used once -- as SAML Assertions are one-time-use objects. """ def __init__(self, assertion, credential_path='~/.aws', profile='default', region='us-east-1', role=None): cred_dir = os.path.expanduser(credential_path) cred_file = os.path.join(cred_dir, 'credentials') boto_logger = logging.getLogger('botocore') boto_logger.setLevel(logging.WARNING) if not os.path.exists(cred_dir): LOG.info('Creating missing AWS Credentials dir {dir} 📁'.format( dir=cred_dir)) os.makedirs(cred_dir) self.sts = boto3.client('sts') self.profile = profile self.region = region self.assertion = SamlAssertion(assertion) self.writer = Credentials(cred_file) # Populated by self.assume_role() self.creds = { 'AccessKeyId': None, 'SecretAccessKey': None, 'SessionToken': None, 'Expiration': None } self.session_token = None self.role = role self.available_roles() @property def is_valid(self): """Return True if the Session is still valid. Returns: Bool """ # Consider the tokens expired when they have 10m left try: msg = ("Session Expiration: {} // Now: {}".format( self.creds['Expiration'], datetime.datetime.utcnow())) LOG.debug(msg) offset = datetime.timedelta(seconds=600) now = datetime.datetime.utcnow() expir = datetime.datetime.strptime(str(self.creds['Expiration']), '%Y-%m-%d %H:%M:%S+00:00') return (now + offset) < expir except (ValueError, TypeError): return False def available_roles(self): """Return the roles available from AWS. Returns: Tuple, list of roles as dicts and a bool that is true when multiple accounts were found """ multiple_accounts = False first_account = '' formatted_roles = [] for role in self.assertion.roles(): account = role['role'].split(':')[4] role_name = role['role'].split(':')[5].split('/')[1] formatted_roles.append({ 'account': account, 'role_name': role_name, 'arn': role['role'], 'principle': role['principle'] }) if first_account == '': first_account = account elif first_account != account: multiple_accounts = True if multiple_accounts: formatted_roles = self.account_ids_to_names(formatted_roles) self.roles = sorted(formatted_roles, key=lambda k: (k['account'], k['role_name'])) return self.roles def assume_role(self): """Use the SAML Assertion to actually get the credentials. Uses the supplied (one time use!) SAML Assertion to go out to Amazon and get back a set of temporary credentials. These are written out to disk and can be used for an hour before they need to be replaced. """ if self.role is None: if len(self.assertion.roles()) > 1: raise MultipleRoles self.role = 0 LOG.info('Assuming role: {}'.format(self.roles[self.role]['arn'])) session = self.sts.assume_role_with_saml( RoleArn=self.roles[self.role]['arn'], PrincipalArn=self.roles[self.role]['principle'], SAMLAssertion=self.assertion.encode()) self.creds = session['Credentials'] self._write() def _write(self): """Write out our secrets to the Credentials object.""" self.writer.add_profile(name=self.profile, region=self.region, creds=self.creds) LOG.info( 'Current time is {time}'.format(time=datetime.datetime.utcnow())) LOG.info('Session expires at {time} ⏳'.format( time=self.creds['Expiration'])) def account_ids_to_names(self, roles): """Turn account IDs into user-friendly names args: roles: Dict of the roles from AWS to get the account names for Returns: Dict of account names and role names for user selection """ try: accounts = self.get_account_name_map() except Exception: msg = ('Error retreiving AWS account name/ID map. ' 'Falling back to just account IDs') LOG.warning(msg) return roles for role in roles: role['account'] = accounts[role['account']] LOG.debug("AWS roles with friendly names: {}".format(accounts)) return roles def get_account_name_map(self): """ Get the friendly to ID mappings from AWS via hacktastic HTML Returns: Dict of account numbers with names """ url = 'https://signin.aws.amazon.com/saml' headers = {'Content-Type': 'application/x-www-form-urlencoded'} data = {'SAMLResponse': self.assertion.encode()} resp = requests.post(url=url, headers=headers, data=data) resp.raise_for_status() return self.account_names_from_html(resp.text) @staticmethod def account_names_from_html(html): """ Parse the AWS SAML login page HTML for account numbers and names Returns: Dict of the account numbers and names """ accounts = {} soup = bs4.BeautifulSoup(html, 'html.parser') for account in soup.find_all('div', {'class': 'saml-account-name'}): name_string = account.text a_id = re.match(r".*\((\d+)\)", name_string).group(1) a_name = re.match(r"\S+\s(\S+)", name_string).group(1) accounts[a_id] = a_name LOG.debug("AWS account map: {}".format(accounts)) return accounts
class Session(object): """Amazon Federated Session Generator. This class is used to contact Amazon with a specific SAML Assertion and get back a set of temporary Federated credentials. These credentials are written to disk (using the Credentials object above). This object is meant to be used once -- as SAML Assertions are one-time-use objects. """ def __init__(self, assertion, credential_path='~/.aws', profile='default', region='us-east-1'): cred_dir = expanduser(credential_path) cred_file = os.path.join(cred_dir, 'credentials') boto_logger = logging.getLogger('botocore') boto_logger.setLevel(logging.WARNING) if not os.path.exists(cred_dir): LOG.info('Creating missing AWS Credentials dir {dir}'.format( dir=cred_dir)) os.makedirs(cred_dir) self.sts = boto3.client('sts') self.profile = profile self.region = region self.assertion = SamlAssertion(assertion) self.writer = Credentials(cred_file) # Populated by self.assume_role() self.creds = { 'AccessKeyId': None, 'SecretAccessKey': None, 'SessionToken': None, 'Expiration': None } self.session_token = None self.role = None @property def is_valid(self): """Return True if the Session is still valid. Takes the current time (in UTC) and compares it to the Expiration time returned by Amazon. Adds a 10 minute buffer to make sure that we start working to renew the creds far before they really expire and break. Args: now: A datetime.datetime() object (likely datetime.datetime.utcnow()) buffer: Number of seconds before the actual expiration before we start returning false. Returns: Bool """ # Consider the tokens expired when they have 10m left try: msg = ("Session Expiration: {} // Now: {}".format( self.creds['Expiration'], datetime.datetime.utcnow())) LOG.debug(msg) buffer = datetime.timedelta(seconds=600) now = datetime.datetime.utcnow() expir = datetime.datetime.strptime(str(self.creds['Expiration']), '%Y-%m-%d %H:%M:%S+00:00') return (now + buffer) < expir except (ValueError, TypeError): return False def set_role(self, role_index): """Set the role based on the supplied index value.""" self.role = self.assertion.roles()[int(role_index)] def available_roles(self): """Return the roles availble from AWS.""" return self.assertion.roles() def assume_role(self): """Use the SAML Assertion to actually get the credentials. Uses the supplied (one time use!) SAML Assertion to go out to Amazon and get back a set of temporary credentials. These are written out to disk and can be used for an hour before they need to be replaced. """ if self.role is None: try: if len(self.assertion.roles()) > 1: raise MultipleRoles self.role = self.assertion.roles()[0] except xml.etree.ElementTree.ParseError: LOG.error('Could not find any Role in the SAML assertion') LOG.error(self.assertion.__dict__) raise InvalidSaml() LOG.info('Assuming role: {}'.format(self.role['role'])) session = self.sts.assume_role_with_saml( RoleArn=self.role['role'], PrincipalArn=self.role['principle'], SAMLAssertion=self.assertion.encode()) self.creds = session['Credentials'] self._write() def _write(self): """Write out our secrets to the Credentials object.""" self.writer.add_profile(name=self.profile, region=self.region, creds=self.creds) LOG.info( 'Current time is {time}'.format(time=datetime.datetime.utcnow())) LOG.info( 'Session expires at {time}'.format(time=self.creds['Expiration']))