/gimme_aws_creds/main.py

https://github.com/Nike-Inc/gimme-aws-creds · Python · 864 lines · 802 code · 4 blank · 58 comment · 8 complexity · 64be517fa38d9efe33d6805e18817cb2 MD5 · raw file

  1. #!/usr/bin/env python3
  2. """
  3. Copyright 2016-present Nike, Inc.
  4. Licensed under the Apache License, Version 2.0 (the "License");
  5. You may not use this file except in compliance with the License.
  6. You may obtain a copy of the License at
  7. http://www.apache.org/licenses/LICENSE-2.0
  8. Unless required by applicable law or agreed to in writing, software
  9. distributed under the License is distributed on an "AS IS" BASIS,
  10. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  11. See the License for the specific language governing permissions and* limitations under the License.*
  12. """
  13. # For enumerating saml roles
  14. # standard imports
  15. import configparser
  16. import json
  17. import os
  18. import re
  19. import sys
  20. # extras
  21. import boto3
  22. from botocore.exceptions import ClientError
  23. from okta.framework.ApiClient import ApiClient
  24. from okta.framework.OktaError import OktaError
  25. # local imports
  26. from . import errors, ui
  27. from .aws import AwsResolver
  28. from .config import Config
  29. from .default import DefaultResolver
  30. from .okta import OktaClient
  31. class GimmeAWSCreds(object):
  32. """
  33. This is a CLI tool that gets temporary AWS credentials
  34. from Okta based the available AWS Okta Apps and roles
  35. assigned to the user. The user is able to select the app
  36. and role from the CLI or specify them in a config file by
  37. passing --action-configure to the CLI too.
  38. gimme_aws_creds will either write the credentials to stdout
  39. or ~/.aws/credentials depending on what was specified when
  40. --action-configure was ran.
  41. Usage:
  42. -h, --help show this help message and exit
  43. --username USERNAME, -u USERNAME
  44. The username to use when logging into Okta. The
  45. username can also be set via the OKTA_USERNAME env
  46. variable. If not provided you will be prompted to
  47. enter a username.
  48. --action-configure, -c If set, will prompt user for configuration parameters
  49. and then exit.
  50. --profile PROFILE, -p PROFILE
  51. If set, the specified configuration profile will be
  52. used instead of the default.
  53. --resolve, -r If set, perfom alias resolution.
  54. --insecure, -k Allow connections to SSL sites without cert
  55. verification.
  56. --mfa-code MFA_CODE The MFA verification code to be used with SMS or TOTP
  57. authentication methods. If not provided you will be
  58. prompted to enter an MFA verification code.
  59. --remember-device, -m
  60. The MFA device will be remembered by Okta service for
  61. a limited time, otherwise, you will be prompted for it
  62. every time.
  63. --version gimme-aws-creds version
  64. Config Options:
  65. okta_org_url = Okta URL
  66. gimme_creds_server = URL of the gimme-creds-server
  67. client_id = OAuth Client id for the gimme-creds-server
  68. okta_auth_server = Server ID for the OAuth authorization server used by gimme-creds-server
  69. write_aws_creds = Option to write creds to ~/.aws/credentials
  70. cred_profile = Use DEFAULT or Role-based name as the profile in ~/.aws/credentials
  71. aws_appname = (optional) Okta AWS App Name
  72. aws_rolename = (optional) AWS Role ARN. 'ALL' will retrieve all roles, can be a CSV for multiple roles.
  73. okta_username = (optional) Okta User Name
  74. """
  75. resolver = DefaultResolver()
  76. envvar_list = [
  77. 'AWS_DEFAULT_DURATION',
  78. 'CLIENT_ID',
  79. 'CRED_PROFILE',
  80. 'GIMME_AWS_CREDS_CLIENT_ID',
  81. 'GIMME_AWS_CREDS_CRED_PROFILE',
  82. 'GIMME_AWS_CREDS_OUTPUT_FORMAT',
  83. 'OKTA_AUTH_SERVER',
  84. 'OKTA_DEVICE_TOKEN',
  85. 'OKTA_MFA_CODE',
  86. 'OKTA_PASSWORD',
  87. 'OKTA_USERNAME',
  88. ]
  89. envvar_conf_map = {
  90. 'GIMME_AWS_CREDS_CLIENT_ID': 'client_id',
  91. 'GIMME_AWS_CREDS_CRED_PROFILE': 'cred_profile',
  92. 'GIMME_AWS_CREDS_OUTPUT_FORMAT': 'output_format',
  93. 'OKTA_DEVICE_TOKEN': 'device_token',
  94. }
  95. def __init__(self, ui=ui.cli):
  96. """
  97. :type ui: ui.UserInterface
  98. """
  99. self.ui = ui
  100. self.FILE_ROOT = self.ui.HOME
  101. self.AWS_CONFIG = self.ui.environ.get(
  102. 'AWS_SHARED_CREDENTIALS_FILE',
  103. os.path.join(self.FILE_ROOT, '.aws', 'credentials')
  104. )
  105. self._cache = {}
  106. # this is modified code from https://github.com/nimbusscale/okta_aws_login
  107. def _write_aws_creds(self, profile, access_key, secret_key, token, aws_config=None):
  108. """ Writes the AWS STS token into the AWS credential file"""
  109. # Check to see if the aws creds path exists, if not create it
  110. aws_config = aws_config or self.AWS_CONFIG
  111. creds_dir = os.path.dirname(aws_config)
  112. if os.path.exists(creds_dir) is False:
  113. os.makedirs(creds_dir)
  114. config = configparser.RawConfigParser()
  115. # Read in the existing config file if it exists
  116. if os.path.isfile(aws_config):
  117. config.read(aws_config)
  118. # Put the credentials into a saml specific section instead of clobbering
  119. # the default credentials
  120. if not config.has_section(profile):
  121. config.add_section(profile)
  122. config.set(profile, 'aws_access_key_id', access_key)
  123. config.set(profile, 'aws_secret_access_key', secret_key)
  124. config.set(profile, 'aws_session_token', token)
  125. config.set(profile, 'aws_security_token', token)
  126. # Write the updated config file
  127. with open(aws_config, 'w+') as configfile:
  128. config.write(configfile)
  129. # Update file permissions to secure sensitive credentials file
  130. os.chmod(aws_config, 0o600)
  131. self.ui.result('Written profile {} to {}'.format(profile, aws_config))
  132. def write_aws_creds_from_data(self, data, aws_config=None):
  133. if not isinstance(data, dict):
  134. self.ui.warning('json line is not a dict! ' + repr(data))
  135. return
  136. aws_config = aws_config or data.get('shared_credentials_file')
  137. credentials = data.get('credentials', {})
  138. profile = data.get('profile', {})
  139. errs = []
  140. if not isinstance(profile, dict):
  141. errs.append('profile is not a dict!' + repr(profile))
  142. else:
  143. for key in ('name',):
  144. value = profile.get(key, None)
  145. if not value:
  146. errs.append('{} is not set {} in profile! {}'.format(key, repr(value), str(profile.keys())))
  147. if not isinstance(credentials, dict):
  148. errs.append('credentials are not a dict!' + repr(credentials))
  149. else:
  150. for key in ('aws_access_key_id',
  151. 'aws_secret_access_key',
  152. 'aws_session_token'):
  153. value = credentials.get(key, None)
  154. if not value:
  155. errs.append(
  156. '{} is not set {} in credentials! {}'.format(key, repr(value), str(credentials.keys())))
  157. if errs:
  158. for error in errs:
  159. self.ui.warning(error)
  160. return
  161. arn = data.get('role', {}).get('arn', '<no-arn>')
  162. self.ui.result('Saving {} as {}'.format(arn, profile['name']))
  163. self._write_aws_creds(
  164. profile['name'],
  165. credentials['aws_access_key_id'],
  166. credentials['aws_secret_access_key'],
  167. credentials['aws_session_token'],
  168. aws_config=aws_config,
  169. )
  170. @staticmethod
  171. def _get_partition_from_saml_acs(saml_acs_url):
  172. """ Determine the AWS partition by looking at the ACS endpoint URL"""
  173. if saml_acs_url == 'https://signin.aws.amazon.com/saml':
  174. return 'aws'
  175. elif saml_acs_url == 'https://signin.amazonaws.cn/saml':
  176. return 'aws-cn'
  177. elif saml_acs_url == 'https://signin.amazonaws-us-gov.com/saml':
  178. return 'aws-us-gov'
  179. else:
  180. raise errors.GimmeAWSCredsError("{} is an unknown ACS URL".format(saml_acs_url))
  181. @staticmethod
  182. def _get_sts_creds(partition, assertion, idp, role, duration=3600):
  183. """ using the assertion and arns return aws sts creds """
  184. # Use the first available region for partitions other than the public AWS
  185. session = boto3.session.Session(profile_name=None)
  186. if partition != 'aws':
  187. regions = session.get_available_regions('sts', partition)
  188. client = session.client('sts', regions[0])
  189. else:
  190. client = session.client('sts')
  191. response = client.assume_role_with_saml(
  192. RoleArn=role,
  193. PrincipalArn=idp,
  194. SAMLAssertion=assertion,
  195. DurationSeconds=duration
  196. )
  197. return response['Credentials']
  198. @staticmethod
  199. def _call_gimme_creds_server(okta_connection, gimme_creds_server_url):
  200. """ Retrieve the user's AWS accounts from the gimme_creds_server"""
  201. response = okta_connection.get(gimme_creds_server_url)
  202. # Throw an error if we didn't get any accounts back
  203. if not response.json():
  204. raise errors.GimmeAWSCredsError("No AWS accounts found.")
  205. return response.json()
  206. @staticmethod
  207. def _get_aws_account_info(okta_org_url, okta_api_key, username):
  208. """ Call the Okta User API and process the results to return
  209. just the information we need for gimme_aws_creds"""
  210. # We need access to the entire JSON response from the Okta APIs, so we need to
  211. # use the low-level ApiClient instead of UsersClient and AppInstanceClient
  212. users_client = ApiClient(okta_org_url, okta_api_key, pathname='/api/v1/users')
  213. # Get User information
  214. try:
  215. result = users_client.get_path('/{0}'.format(username))
  216. user = result.json()
  217. except OktaError as e:
  218. if e.error_code == 'E0000007':
  219. raise errors.GimmeAWSCredsError("Error: " + username + " was not found!")
  220. else:
  221. raise errors.GimmeAWSCredsError("Error: " + e.error_summary)
  222. try:
  223. # Get first page of results
  224. result = users_client.get_path('/{0}/appLinks'.format(user['id']))
  225. final_result = result.json()
  226. # Loop through other pages
  227. while 'next' in result.links:
  228. result = users_client.get(result.links['next']['url'])
  229. final_result = final_result + result.json()
  230. ui.default.info("done\n")
  231. except OktaError as e:
  232. if e.error_code == 'E0000007':
  233. raise errors.GimmeAWSCredsError("Error: No applications found for " + username)
  234. else:
  235. raise errors.GimmeAWSCredsError("Error: " + e.error_summary)
  236. # Loop through the list of apps and filter it down to just the info we need
  237. app_list = []
  238. for app in final_result:
  239. # All AWS connections have the same app name
  240. if app['appName'] == 'amazon_aws':
  241. new_app_entry = {
  242. 'id': app['id'],
  243. 'name': app['label'],
  244. 'links': {
  245. 'appLink': app['linkUrl'],
  246. 'appLogo': app['logoUrl']
  247. }
  248. }
  249. app_list.append(new_app_entry)
  250. # Throw an error if we didn't get any accounts back
  251. if not app_list:
  252. raise errors.GimmeAWSCredsError("No AWS accounts found.")
  253. return app_list
  254. @staticmethod
  255. def _parse_role_arn(arn):
  256. """ Extracts account number, path and role name from role arn string """
  257. matches = re.match(r"arn:(aws|aws-cn|aws-us-gov):iam:.*:(?P<accountid>\d{12}):role(?P<path>(/[\w/]+)?/)(?P<role>\S+)", arn)
  258. return {
  259. 'account': matches.group('accountid'),
  260. 'role': matches.group('role'),
  261. 'path': matches.group('path')
  262. }
  263. @staticmethod
  264. def _get_alias_from_friendly_name(friendly_name):
  265. """ Extracts alias from friendly name string """
  266. res = None
  267. matches = re.match(r"Account:\s(?P<alias>.+)\s\(\d{12}\)", friendly_name)
  268. if matches:
  269. res = matches.group('alias')
  270. return res
  271. def _choose_app(self, aws_info):
  272. """ gets a list of available apps and
  273. ask the user to select the app they want
  274. to assume a roles for and returns the selection
  275. """
  276. if not aws_info:
  277. return None
  278. if len(aws_info) == 1:
  279. return aws_info[0] # auto select when only 1 choice
  280. app_strs = []
  281. for i, app in enumerate(aws_info):
  282. app_strs.append('[{}] {}'.format(i, app["name"]))
  283. if app_strs:
  284. self.ui.message("Pick an app:")
  285. # print out the apps and let the user select
  286. for app in app_strs:
  287. self.ui.message(app)
  288. else:
  289. return None
  290. selection = self._get_user_int_selection(0, len(aws_info) - 1)
  291. if selection is None:
  292. raise errors.GimmeAWSCredsError("You made an invalid selection")
  293. return aws_info[int(selection)]
  294. def _get_selected_app(self, aws_appname, aws_info):
  295. """ select the application from the config file if it exists in the
  296. results from Okta. If not, present the user with a menu."""
  297. if aws_appname:
  298. for _, app in enumerate(aws_info):
  299. if app["name"] == aws_appname:
  300. return app
  301. elif app["name"] == "fakelabel":
  302. # auto select this app
  303. return app
  304. self.ui.error("ERROR: AWS account [{}] not found!".format(aws_appname))
  305. # Present the user with a list of apps to choose from
  306. return self._choose_app(aws_info)
  307. def _get_user_int_selection(self, min_int, max_int, max_retries=5):
  308. selection = None
  309. for _ in range(0, max_retries):
  310. try:
  311. selection = int(self.ui.input("Selection: "))
  312. break
  313. except ValueError:
  314. self.ui.warning('Invalid selection, must be an integer value.')
  315. if selection is None:
  316. return None
  317. # make sure the choice is valid
  318. if selection < min_int or selection > max_int:
  319. return None
  320. return selection
  321. def _get_selected_roles(self, requested_roles, aws_roles):
  322. """ select the role from the config file if it exists in the
  323. results from Okta. If not, present the user with a menu. """
  324. # 'all' is a special case - skip procesing
  325. if requested_roles == 'all':
  326. return set(role.role for role in aws_roles)
  327. # check to see if a role is in the config and look for it in the results from Okta
  328. if requested_roles:
  329. ret = set()
  330. if isinstance(requested_roles, str):
  331. requested_roles = requested_roles.split(',')
  332. for role_name in requested_roles:
  333. role_name = role_name.strip()
  334. if not role_name:
  335. continue
  336. is_regexp = len(role_name) > 2 and role_name[0] == role_name[-1] == '/'
  337. pattern = re.compile(role_name[1:-1])
  338. for aws_role in aws_roles:
  339. if aws_role.role == role_name or (is_regexp and pattern.search(aws_role.role)):
  340. ret.add(aws_role.role)
  341. if ret:
  342. return ret
  343. self.ui.error("ERROR: AWS roles [{}] not found!".format(', '.join(requested_roles)))
  344. # Present the user with a list of roles to choose from
  345. return self._choose_roles(aws_roles)
  346. def _choose_roles(self, roles):
  347. """ gets a list of available roles and
  348. asks the user to select the role they want to assume
  349. """
  350. if not roles:
  351. return set()
  352. # Check if only one role exists and return that role
  353. if len(roles) == 1:
  354. single_role = roles[0].role
  355. self.ui.info("Detected single role: {}".format(single_role))
  356. return {single_role}
  357. # Gather the roles available to the user.
  358. role_strs = self.resolver._display_role(roles)
  359. if role_strs:
  360. self.ui.message("Pick a role:")
  361. for role in role_strs:
  362. self.ui.message(role)
  363. else:
  364. return set()
  365. selections = self._get_user_int_selections_many(0, len(roles) - 1)
  366. if not selections:
  367. raise errors.GimmeAWSCredsError("You made an invalid selection")
  368. return {roles[int(selection)].role for selection in selections}
  369. def _get_user_int_selections_many(self, min_int, max_int, max_retries=5):
  370. for _ in range(max_retries):
  371. selections = set()
  372. error = False
  373. for value in self.ui.input('Selections (comma separated): ').split(','):
  374. value = value.strip()
  375. if not value:
  376. continue
  377. try:
  378. selection = int(value)
  379. except ValueError:
  380. self.ui.warning('Invalid selection {}, must be an integer value.'.format(repr(value)))
  381. error = True
  382. continue
  383. if min_int <= selection <= max_int:
  384. selections.add(value)
  385. else:
  386. self.ui.warning(
  387. 'Selection {} out of range <{}, {}>'.format(repr(selection), min_int, max_int))
  388. if error:
  389. continue
  390. if selections:
  391. return selections
  392. return set()
  393. def run(self):
  394. try:
  395. self._run()
  396. except errors.GimmeAWSCredsExitBase as exc:
  397. exc.handle()
  398. def generate_config(self):
  399. """ generates a new configuration and populates
  400. various config caches
  401. """
  402. self._cache['config'] = config = Config(gac_ui=self.ui)
  403. config.get_args()
  404. self._cache['conf_dict'] = config.get_config_dict()
  405. for value in self.envvar_list:
  406. if self.ui.environ.get(value):
  407. key = self.envvar_conf_map.get(value, value).lower()
  408. self.conf_dict[key] = self.ui.environ.get(value)
  409. # AWS Default session duration ....
  410. if self.conf_dict.get('aws_default_duration'):
  411. self.config.aws_default_duration = int(self.conf_dict['aws_default_duration'])
  412. else:
  413. self.config.aws_default_duration = 3600
  414. self.resolver = self.get_resolver()
  415. return config
  416. @property
  417. def config(self):
  418. if 'config' in self._cache:
  419. return self._cache['config']
  420. config = self.generate_config()
  421. return config
  422. @property
  423. def conf_dict(self):
  424. """
  425. :rtype: dict
  426. """
  427. # noinspection PyUnusedLocal
  428. config = self.config
  429. return self._cache['conf_dict']
  430. @property
  431. def output_format(self):
  432. return self.conf_dict.setdefault('output_format', self.config.output_format)
  433. @property
  434. def okta_org_url(self):
  435. ret = self.conf_dict.get('okta_org_url')
  436. if not ret:
  437. raise errors.GimmeAWSCredsError('No Okta organization URL in configuration. Try running --config again.')
  438. return ret
  439. @property
  440. def gimme_creds_server(self):
  441. ret = self.conf_dict.get('gimme_creds_server')
  442. if not ret:
  443. raise errors.GimmeAWSCredsError('No Gimme-Creds server URL in configuration. Try running --config again.')
  444. return ret
  445. @property
  446. def okta(self):
  447. if 'okta' in self._cache:
  448. return self._cache['okta']
  449. okta = self._cache['okta'] = OktaClient(
  450. self.ui,
  451. self.okta_org_url,
  452. self.config.verify_ssl_certs,
  453. self.device_token,
  454. )
  455. if self.config.username is not None:
  456. okta.set_username(self.config.username)
  457. elif self.conf_dict.get('okta_username'):
  458. okta.set_username(self.conf_dict['okta_username'])
  459. if self.conf_dict.get('okta_password'):
  460. okta.set_password(self.conf_dict['okta_password'])
  461. if self.conf_dict.get('preferred_mfa_type'):
  462. okta.set_preferred_mfa_type(self.conf_dict['preferred_mfa_type'])
  463. if self.config.mfa_code is not None:
  464. okta.set_mfa_code(self.config.mfa_code)
  465. elif self.conf_dict.get('okta_mfa_code'):
  466. okta.set_mfa_code(self.conf_dict.get('okta_mfa_code'))
  467. okta.set_remember_device(self.config.remember_device
  468. or self.conf_dict.get('remember_device', False))
  469. return okta
  470. def get_resolver(self):
  471. if self.config.resolve:
  472. return AwsResolver(self.config.verify_ssl_certs)
  473. elif str(self.conf_dict.get('resolve_aws_alias')) == 'True':
  474. return AwsResolver(self.config.verify_ssl_certs)
  475. return self.resolver
  476. @property
  477. def device_token(self):
  478. if self.config.action_register_device is True:
  479. self.conf_dict['device_token'] = None
  480. return self.conf_dict.get('device_token')
  481. def set_auth_session(self, auth_session):
  482. self._cache['auth_session'] = auth_session
  483. @property
  484. def auth_session(self):
  485. if 'auth_session' in self._cache:
  486. return self._cache['auth_session']
  487. auth_result = self.okta.auth_session()
  488. self.set_auth_session(auth_result)
  489. return auth_result
  490. @property
  491. def aws_results(self):
  492. if 'aws_results' in self._cache:
  493. return self._cache['aws_results']
  494. # Call the Okta APIs and proces data locally
  495. if self.gimme_creds_server == 'internal':
  496. # Okta API key is required when calling Okta APIs internally
  497. if self.config.api_key is None:
  498. raise errors.GimmeAWSCredsError('OKTA_API_KEY environment variable not found!')
  499. auth_result = self.auth_session
  500. aws_results = self._get_aws_account_info(self.okta_org_url, self.config.api_key,
  501. auth_result['username'])
  502. elif self.gimme_creds_server == 'appurl':
  503. self.auth_session
  504. # bypass lambda & API call
  505. # Apps url is required when calling with appurl
  506. if self.conf_dict.get('app_url'):
  507. self.config.app_url = self.conf_dict['app_url']
  508. if self.config.app_url is None:
  509. raise errors.GimmeAWSCredsError('app_url is not defined in your config!')
  510. # build app list
  511. aws_results = []
  512. new_app_entry = {
  513. 'id': 'fakeid', # not used anyway
  514. 'name': 'fakelabel', # not used anyway
  515. 'links': {'appLink': self.config.app_url}
  516. }
  517. aws_results.append(new_app_entry)
  518. # Use the gimme_creds_lambda service
  519. else:
  520. if not self.conf_dict.get('client_id'):
  521. raise errors.GimmeAWSCredsError('No OAuth Client ID in configuration. Try running --config again.')
  522. if not self.conf_dict.get('okta_auth_server'):
  523. raise errors.GimmeAWSCredsError(
  524. 'No OAuth Authorization server in configuration. Try running --config again.')
  525. # Authenticate with Okta and get an OAuth access token
  526. self.okta.auth_oauth(
  527. self.conf_dict['client_id'],
  528. authorization_server=self.conf_dict['okta_auth_server'],
  529. access_token=True,
  530. id_token=False,
  531. scopes=['openid']
  532. )
  533. # Add Access Tokens to Okta-protected requests
  534. self.okta.use_oauth_access_token(True)
  535. self.ui.info("Authentication Success! Calling Gimme-Creds Server...")
  536. aws_results = self._call_gimme_creds_server(self.okta, self.gimme_creds_server)
  537. self._cache['aws_results'] = aws_results
  538. return aws_results
  539. @property
  540. def aws_app(self):
  541. if 'aws_app' in self._cache:
  542. return self._cache['aws_app']
  543. self._cache['aws_app'] = aws_app = self._get_selected_app(self.conf_dict.get('aws_appname'), self.aws_results)
  544. return aws_app
  545. @property
  546. def saml_data(self):
  547. if 'saml_data' in self._cache:
  548. return self._cache['saml_data']
  549. self._cache['saml_data'] = saml_data = self.okta.get_saml_response(self.aws_app['links']['appLink'])
  550. return saml_data
  551. @property
  552. def aws_roles(self):
  553. if 'aws_roles' in self._cache:
  554. return self._cache['aws_roles']
  555. self._cache['aws_roles'] = roles = self.resolver._enumerate_saml_roles(
  556. self.saml_data['SAMLResponse'],
  557. self.saml_data['TargetUrl'],
  558. )
  559. return roles
  560. @property
  561. def aws_selected_roles(self):
  562. if 'aws_selected_roles' in self._cache:
  563. return self._cache['aws_selected_roles']
  564. selected_roles = self._get_selected_roles(self.requested_roles, self.aws_roles)
  565. self._cache['aws_aws_selected_roless'] = ret = [
  566. role
  567. for role in self.aws_roles
  568. if role.role in selected_roles
  569. ]
  570. return ret
  571. @property
  572. def requested_roles(self):
  573. if 'requested_roles' in self._cache:
  574. return self._cache['requested_roles']
  575. self._cache['requested_roles'] = requested_roles = self.config.roles or self.conf_dict.get('aws_rolename', '')
  576. return requested_roles
  577. @property
  578. def aws_partition(self):
  579. if 'aws_partition' in self._cache:
  580. return self._cache['aws_partition']
  581. self._cache['aws_partition'] = aws_partition = self._get_partition_from_saml_acs(self.saml_data['TargetUrl'])
  582. return aws_partition
  583. def prepare_data(self, role, generate_credentials=False):
  584. aws_creds = {}
  585. if generate_credentials:
  586. try:
  587. aws_creds = self._get_sts_creds(
  588. self.aws_partition,
  589. self.saml_data['SAMLResponse'],
  590. role.idp,
  591. role.role,
  592. self.config.aws_default_duration,
  593. )
  594. except ClientError as ex:
  595. if 'requested DurationSeconds exceeds the MaxSessionDuration' in ex.response['Error']['Message']:
  596. self.ui.warning(
  597. "The requested session duration was too long for this role. Falling back to 1 hour.")
  598. aws_creds = self._get_sts_creds(
  599. self.aws_partition,
  600. self.saml_data['SAMLResponse'],
  601. role.idp,
  602. role.role,
  603. 3600,
  604. )
  605. else:
  606. self.ui.error('Failed to generate credentials for {} due to {}'.format(role.role, ex))
  607. naming_data = self._parse_role_arn(role.role)
  608. # set the profile name
  609. # Note if there are multiple roles
  610. # it will be overwritten multiple times and last role wins.
  611. cred_profile = self.conf_dict['cred_profile']
  612. resolve_alias = self.conf_dict['resolve_aws_alias']
  613. include_path = self.conf_dict.get('include_path')
  614. profile_name = self.get_profile_name(cred_profile, include_path, naming_data, resolve_alias, role)
  615. return {
  616. 'shared_credentials_file': self.AWS_CONFIG,
  617. 'profile': {
  618. 'name': profile_name,
  619. 'derived_name': naming_data['role'],
  620. 'config_name': self.conf_dict.get('cred_profile', ''),
  621. },
  622. 'role': {
  623. 'arn': role.role,
  624. 'name': role.role,
  625. 'friendly_name': role.friendly_role_name,
  626. 'friendly_account_name': role.friendly_account_name,
  627. },
  628. 'credentials': {
  629. 'aws_access_key_id': aws_creds.get('AccessKeyId', ''),
  630. 'aws_secret_access_key': aws_creds.get('SecretAccessKey', ''),
  631. 'aws_session_token': aws_creds.get('SessionToken', ''),
  632. 'aws_security_token': aws_creds.get('SessionToken', ''),
  633. } if bool(aws_creds) else {}
  634. }
  635. def get_profile_name(self, cred_profile, include_path, naming_data, resolve_alias, role):
  636. if cred_profile.lower() == 'default':
  637. profile_name = 'default'
  638. elif cred_profile.lower() == 'role':
  639. profile_name = naming_data['role']
  640. elif cred_profile.lower() == 'acc-role':
  641. account = naming_data['account']
  642. role_name = naming_data['role']
  643. path = naming_data['path']
  644. if resolve_alias == 'True':
  645. account_alias = self._get_alias_from_friendly_name(role.friendly_account_name)
  646. if account_alias:
  647. account = account_alias
  648. if include_path == 'True':
  649. role_name = ''.join([path, role_name])
  650. profile_name = '-'.join([account,
  651. role_name])
  652. else:
  653. profile_name = cred_profile
  654. return profile_name
  655. def iter_selected_aws_credentials(self):
  656. results = []
  657. for role in self.aws_selected_roles:
  658. data = self.prepare_data(role, generate_credentials=True)
  659. if not data:
  660. continue
  661. results.append(data)
  662. yield data
  663. self._cache['selected_aws_credentials'] = results
  664. @property
  665. def selected_aws_credentials(self):
  666. if 'selected_aws_credentials' in self._cache:
  667. return self._cache['selected_aws_credentials']
  668. self._cache['selected_aws_credentials'] = ret = list(self.iter_selected_aws_credentials())
  669. return ret
  670. def _run(self):
  671. """ Pulling it all together to make the CLI """
  672. self.handle_action_configure()
  673. self.handle_action_register_device()
  674. self.handle_action_list_profiles()
  675. self.handle_action_store_json_creds()
  676. self.handle_action_list_roles()
  677. for data in self.iter_selected_aws_credentials():
  678. write_aws_creds = str(self.conf_dict['write_aws_creds']) == 'True'
  679. # check if write_aws_creds is true if so
  680. # get the profile name and write out the file
  681. if write_aws_creds:
  682. self.write_aws_creds_from_data(data)
  683. continue
  684. if self.output_format == 'json':
  685. self.ui.result(json.dumps(data))
  686. continue
  687. # Defaults to `export` format
  688. self.ui.result("export AWS_ROLE_ARN=" + data['role']['arn'])
  689. self.ui.result("export AWS_ACCESS_KEY_ID=" + data['credentials']['aws_access_key_id'])
  690. self.ui.result("export AWS_SECRET_ACCESS_KEY=" + data['credentials']['aws_secret_access_key'])
  691. self.ui.result("export AWS_SESSION_TOKEN=" + data['credentials']['aws_session_token'])
  692. self.ui.result("export AWS_SECURITY_TOKEN=" + data['credentials']['aws_security_token'])
  693. self.config.clean_up()
  694. def handle_action_configure(self):
  695. # Create/Update config when configure arg set
  696. if not self.config.action_configure:
  697. return
  698. self.config.update_config_file()
  699. raise errors.GimmeAWSCredsExitSuccess()
  700. def handle_action_list_profiles(self):
  701. if not self.config.action_list_profiles:
  702. return
  703. if os.path.isfile(self.config.OKTA_CONFIG):
  704. with open(self.config.OKTA_CONFIG, 'r') as okta_config:
  705. raise errors.GimmeAWSCredsExitSuccess(result=okta_config.read())
  706. raise errors.GimmeAWSCredsExitError('{} is not a file'.format(self.config.OKTA_CONFIG))
  707. def handle_action_store_json_creds(self, stream=None):
  708. if not self.config.action_store_json_creds:
  709. return
  710. stream = stream or sys.stdin
  711. for line in stream:
  712. try:
  713. data = json.loads(line)
  714. except json.JSONDecodeError:
  715. self.ui.warning('error parsing json line {}'.format(repr(line)))
  716. continue
  717. self.write_aws_creds_from_data(data)
  718. raise errors.GimmeAWSCredsExitSuccess()
  719. def handle_action_register_device(self):
  720. # Capture the Device Token and write it to the config file
  721. if self.device_token is None or self.config.action_register_device is True:
  722. if not self.config.action_register_device:
  723. self.ui.notify('\n*** No device token found in configuration file, it will be created.')
  724. self.ui.notify('*** You may be prompted for MFA more than once for this run.\n')
  725. auth_result = self.auth_session
  726. self.conf_dict['device_token'] = auth_result['device_token']
  727. self.config.write_config_file(self.conf_dict)
  728. self.okta.device_token = self.conf_dict['device_token']
  729. self.ui.notify('\nDevice token saved!\n')
  730. if self.config.action_register_device is True:
  731. raise errors.GimmeAWSCredsExitSuccess()
  732. def handle_action_list_roles(self):
  733. if self.config.action_list_roles:
  734. raise errors.GimmeAWSCredsExitSuccess(result='\n'.join(map(str, self.aws_roles)))