Repository: aws/aws-cli

Issue:
aws2 configure sso barfs on null email addresses in account list
When running `aws2 configure sso`, aws2 breaks when parsing the list of returned accounts, if an account has a `null` emailAddress. Here's a sample from a run using the `--debug` switch:

```
2019-11-09 18:54:04,691 - MainThread - botocore.parsers - DEBUG - Response body:
b'{"nextToken":null,"accountList":[{"accountId":"111111111111","accountName":"foo","emailAddress":null},{"accountId":"222222222222","accountName":"bar","emailAddress":"joe.blow+bar@example.com"},{"accountId":"333333333333","accountName":"baz","emailAddress":"baz@example.com"},{"accountId":"444444444444","accountName":"quux","emailAddress":"quux-ct@example.com"},{"accountId":"555555555555","accountName":"blargh","emailAddress":"blargh-ct@example.com"},{"accountId":"666666666666","accountName":"logs","emailAddress":"logs-ct@example.com"},{"accountId":"777777777777","accountName":"blahonga","emailAddress":null},{"accountId":"888888888888","accountName":"main-ct","emailAddress":"main-ct@example.com"}]}'
2019-11-09 18:54:04,692 - MainThread - botocore.hooks - DEBUG - Event needs-retry.sso.ListAccounts: calling handler <botocore.retryhandler.RetryHandler object at 0x1052f3f50>
2019-11-09 18:54:04,692 - MainThread - botocore.retryhandler - DEBUG - No retry needed.
There are 8 AWS accounts available to you.
2019-11-09 18:54:04,711 - MainThread - awscli.clidriver - DEBUG - Exception caught in main()
Traceback (most recent call last):
  File "site-packages/awscli/clidriver.py", line 278, in main
  File "site-packages/awscli/customizations/commands.py", line 190, in __call__
  File "site-packages/awscli/customizations/commands.py", line 187, in __call__
  File "site-packages/awscli/customizations/configure/sso.py", line 291, in _run_main
  File "site-packages/awscli/customizations/configure/sso.py", line 187, in _prompt_for_account
  File "site-packages/awscli/customizations/configure/sso.py", line 171, in _handle_multiple_accounts
  File "site-packages/awscli/customizations/wizard/selectmenu.py", line 71, in select_menu
  File "site-packages/prompt_toolkit/application/application.py", line 736, in run
  File "site-packages/prompt_toolkit/application/application.py", line 710, in run
  File "site-packages/prompt_toolkit/eventloop/future.py", line 151, in result
  File "site-packages/prompt_toolkit/eventloop/coroutine.py", line 92, in step_next
  File "site-packages/prompt_toolkit/application/application.py", line 685, in _run_async2
  File "site-packages/prompt_toolkit/eventloop/coroutine.py", line 88, in step_next
  File "site-packages/prompt_toolkit/application/application.py", line 628, in _run_async
  File "site-packages/prompt_toolkit/application/application.py", line 471, in _redraw
  File "site-packages/prompt_toolkit/renderer.py", line 587, in render
  File "site-packages/prompt_toolkit/layout/containers.py", line 678, in write_to_screen
  File "site-packages/prompt_toolkit/layout/containers.py", line 722, in _draw_float
  File "site-packages/prompt_toolkit/layout/containers.py", line 1349, in preferred_width
  File "site-packages/prompt_toolkit/layout/containers.py", line 1390, in _merge_dimensions
  File "site-packages/prompt_toolkit/layout/containers.py", line 1338, in preferred_content_width
  File "site-packages/awscli/customizations/wizard/selectmenu.py", line 123, in preferred_width
  File "site-packages/awscli/customizations/wizard/selectmenu.py", line 123, in <genexpr>
  File "site-packages/awscli/customizations/wizard/selectmenu.py", line 122, in <genexpr>
  File "site-packages/awscli/customizations/configure/sso.py", line 164, in _display_account
KeyError: 'emailAddress'
2019-11-09 18:54:04,723 - MainThread - awscli.clidriver - DEBUG - Exiting with rc 255

'emailAddress'
```



Discussion:
I appear to be affected by this issue as well. Further, I'm not sure why the JSON response is showing 3 of my organization's 17 accounts with null for email addresses, when the 3 accounts indicating null for an email address clearly have an AccountEmail and SSOUserEmail address populated for them in the master account's Service Catalog Provisioned products list (our landing zone was set up through Control Tower, if that matters). Happy to look somewhere else if I'm overlooking something obvious, but I don't see any immediate differences between the 3 accounts that have null for an email address and the 14 that do not.
Ours is also a Control Tower installation. We clearly have the same issue. The code doesn't expect to ever receive a `null` email address, but it does happen. Should be easy to fix.
Interesting, not sure why this would come back as `null`. I'm also not sure if this is expected service behavior or should be considered a bug in SSO's API. Either way, I think the CLI could be a little more defensive here.

While I work on getting a fix in for this, it's still possible to manually setup the CLI profile by manually editing your `~/.aws/config` with the desired sso configuration:

```
[profile sso-profile]
sso_start_url = https://d-1234.awsapps.com/start
sso_region = us-west-2
sso_account_id = 123412341234
sso_role_name = ReadOnly
```
Then you can run `aws sso login --profile sso-profile` to login if needed.

Resolving patch:
diff --git a/awscli/customizations/configure/sso.py b/awscli/customizations/configure/sso.py
--- a/awscli/customizations/configure/sso.py
+++ b/awscli/customizations/configure/sso.py
@@ -88,6 +88,25 @@ def get_value(self, current_value, prompt_text='',
         return response
 
 
+def display_account(account):
+    """Converts an SSO account response into a display string.
+
+    All fields should be present in the API response but we've seen some
+    cases where the account name or email address is not set. Considering
+    we only need the account name and email address for display purposes
+    we should be defensive just in case they don't come back.
+    """
+    if 'accountName' not in account and 'emailAddress' not in account:
+        account_template = '{accountId}'
+    elif 'emailAddress' not in account:
+        account_template = '{accountName} ({accountId})'
+    elif 'accountName' not in account:
+        account_template = '{emailAddress} ({accountId})'
+    else:
+        account_template = '{accountName}, {emailAddress} ({accountId})'
+    return account_template.format(**account)
+
+
 class ConfigureSSOCommand(BasicCommand):
     NAME = 'sso'
     SYNOPSIS = ('aws2 configure sso [--profile profile-name]')
@@ -151,15 +170,12 @@ def _handle_single_account(self, accounts):
         uni_print(single_account_msg.format(sso_account_id))
         return sso_account_id
 
-    def _display_account(self, account):
-        return '{accountName}, {emailAddress} ({accountId})'.format(**account)
-
     def _handle_multiple_accounts(self, accounts):
         available_accounts_msg = (
             'There are {} AWS accounts available to you.\n'
         )
         uni_print(available_accounts_msg.format(len(accounts)))
-        selected_account = self._selector(accounts, self._display_account)
+        selected_account = self._selector(accounts, display_account)
         sso_account_id = selected_account['accountId']
         return sso_account_id
 


Test patch:
diff --git a/tests/unit/customizations/configure/test_sso.py b/tests/unit/customizations/configure/test_sso.py
--- a/tests/unit/customizations/configure/test_sso.py
+++ b/tests/unit/customizations/configure/test_sso.py
@@ -24,6 +24,7 @@
 from botocore.exceptions import ProfileNotFound
 
 from awscli.testutils import unittest
+from awscli.customizations.configure.sso import display_account
 from awscli.customizations.configure.sso import select_menu
 from awscli.customizations.configure.sso import PTKPrompt
 from awscli.customizations.configure.sso import ConfigureSSOCommand
@@ -394,3 +395,43 @@ def test_prompts_suggest_values(self):
             expected_cli_outputs,
         ]
         self.assert_prompt_completions(expected_completions)
+
+
+class TestDisplayAccount(unittest.TestCase):
+    def setUp(self):
+        self.account_id = '1234'
+        self.email_address = 'test@test.com'
+        self.account_name = 'FooBar'
+        self.account = {
+            'accountId': self.account_id,
+            'emailAddress': self.email_address,
+            'accountName': self.account_name,
+        }
+
+    def test_display_account_all_fields(self):
+        account_str = display_account(self.account)
+        self.assertIn(self.account_name, account_str)
+        self.assertIn(self.email_address, account_str)
+        self.assertIn(self.account_id, account_str)
+
+    def test_display_account_missing_email(self):
+        del self.account['emailAddress']
+        account_str = display_account(self.account)
+        self.assertIn(self.account_name, account_str)
+        self.assertNotIn(self.email_address, account_str)
+        self.assertIn(self.account_id, account_str)
+
+    def test_display_account_missing_name(self):
+        del self.account['accountName']
+        account_str = display_account(self.account)
+        self.assertNotIn(self.account_name, account_str)
+        self.assertIn(self.email_address, account_str)
+        self.assertIn(self.account_id, account_str)
+
+    def test_display_account_missing_name_and_email(self):
+        del self.account['accountName']
+        del self.account['emailAddress']
+        account_str = display_account(self.account)
+        self.assertNotIn(self.account_name, account_str)
+        self.assertNotIn(self.email_address, account_str)
+        self.assertIn(self.account_id, account_str)