Repository: aws/aws-cli

Issue:
[v2] `aws sso login` should not require a fully-configured profile
Currently, `aws sso login` operates on a particular profile, even [requiring that `sso_account_id` and `sso_role_name` be present in the profile](https://github.com/aws/aws-cli/blob/f2788558422dac42a5ebe37c7e5a3d24b19dee9f/awscli/customizations/sso/login.py#L32) even though it does not use them, [only fetching the token](https://github.com/aws/aws-cli/blob/f2788558422dac42a5ebe37c7e5a3d24b19dee9f/awscli/customizations/sso/utils.py#L45) (as it should, because AWS SSO-capable SDKs can use the token to get credentials for the appropriate account and role).

At the very least, `sso_account_id` and `sso_role_name` should be removed from the list of required config variables, which would allow a profile like:
```ini
[profile login]
sso_start_url = https://d-2e69cb2b10.awsapps.com/start
sso_region = us-east-2
```
and then `aws sso login --profile login` would just work without requiring a specific account and role that won't be used anyway.

This matters because not all users in an organization have the same permissions, so there's not a good way to provide them all with a single working config file to start from.

A better alternative would be to have AWS SSO configuration be explicit in the config file, perhaps with a new section type:
```ini
[sso default]
sso_start_url = https://d-2e69cb2b10.awsapps.com/start
sso_region = us-east-2
```

Or, `aws sso login` should check the configured profiles and if there's only one AWS SSO configuration (i.e., they all use the same start URL and region), it should just use that.

I've implemented the latter in [`aws-sso-util login`](https://github.com/benkehoe/aws-sso-util#logging-in-and-out).

Discussion:
lorengordon: Oh this is fantastic. I was just documenting this for our users today, and kept wondering why these arguments were necessary! Thanks!
codecov-commenter: # [Codecov](https://codecov.io/gh/aws/aws-cli/pull/6730?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aws) Report
> Merging [#6730](https://codecov.io/gh/aws/aws-cli/pull/6730?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aws) (25248cd) into [v2](https://codecov.io/gh/aws/aws-cli/commit/b3bfda4898fa28b84280e02796f940bded3e13b4?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aws) (b3bfda4) will **decrease** coverage by `0.00%`.
> The diff coverage is `n/a`.

[![Impacted file tree graph](https://codecov.io/gh/aws/aws-cli/pull/6730/graphs/tree.svg?width=650&height=150&src=pr&token=aKPbq1EbIm&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aws)](https://codecov.io/gh/aws/aws-cli/pull/6730?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aws)

```diff
@@            Coverage Diff             @@
##               v2    #6730      +/-   ##
==========================================
- Coverage   93.65%   93.65%   -0.01%     
==========================================
  Files         349      349              
  Lines       35406    35416      +10     
  Branches     5049     5054       +5     
==========================================
+ Hits        33161    33168       +7     
- Misses       1645     1648       +3     
  Partials      600      600              
```


| [Impacted Files](https://codecov.io/gh/aws/aws-cli/pull/6730?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aws) | Coverage Δ | |
|---|---|---|
| [awscli/customizations/sso/login.py](https://codecov.io/gh/aws/aws-cli/pull/6730/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aws#diff-YXdzY2xpL2N1c3RvbWl6YXRpb25zL3Nzby9sb2dpbi5weQ==) | `100.00% <ø> (ø)` | |
| [awscli/s3transfer/tasks.py](https://codecov.io/gh/aws/aws-cli/pull/6730/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aws#diff-YXdzY2xpL3MzdHJhbnNmZXIvdGFza3MucHk=) | `91.66% <0.00%> (-3.34%)` | :arrow_down: |
| [awscli/botocore/serialize.py](https://codecov.io/gh/aws/aws-cli/pull/6730/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aws#diff-YXdzY2xpL2JvdG9jb3JlL3NlcmlhbGl6ZS5weQ==) | `98.72% <0.00%> (+0.01%)` | :arrow_up: |
| [awscli/botocore/parsers.py](https://codecov.io/gh/aws/aws-cli/pull/6730/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aws#diff-YXdzY2xpL2JvdG9jb3JlL3BhcnNlcnMucHk=) | `96.58% <0.00%> (+0.03%)` | :arrow_up: |
| [awscli/botocore/credentials.py](https://codecov.io/gh/aws/aws-cli/pull/6730/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aws#diff-YXdzY2xpL2JvdG9jb3JlL2NyZWRlbnRpYWxzLnB5) | `97.52% <0.00%> (+0.10%)` | :arrow_up: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/aws/aws-cli/pull/6730?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aws).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aws)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/aws/aws-cli/pull/6730?src=pr&el=footer&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aws). Last update [b3bfda4...25248cd](https://codecov.io/gh/aws/aws-cli/pull/6730?src=pr&el=lastupdated&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aws). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aws).


Patch:
diff --git a/.changes/next-release/enhancement-SSO-16533.json b/.changes/next-release/enhancement-SSO-16533.json
new file mode 100644
index 000000000..d1b8f1fa7
--- /dev/null
+++ b/.changes/next-release/enhancement-SSO-16533.json
@@ -0,0 +1,5 @@
+{
+  "type": "enhancement",
+  "category": "SSO",
+  "description": "Fixes `#5727 <https://github.com/aws/aws-cli/issues/5727>`__. Loosens the requirements to perform an ``aws sso login`` command to just the ``sso_start_url`` and ``sso_region``."
+}
diff --git a/.changes/next-release/enhancement-protocol-77528.json b/.changes/next-release/enhancement-protocol-77528.json
deleted file mode 100644
index a14222b1b..000000000
--- a/.changes/next-release/enhancement-protocol-77528.json
+++ /dev/null
@@ -1,5 +0,0 @@
-{
-  "type": "enhancement",
-  "category": "protocol",
-  "description": "Add support for lists in headers"
-}
diff --git a/awscli/botocore/parsers.py b/awscli/botocore/parsers.py
index aae210860..10b1716c5 100644
--- a/awscli/botocore/parsers.py
+++ b/awscli/botocore/parsers.py
@@ -941,13 +941,6 @@ class BaseRestParser(ResponseParser):
             parsed = json.loads(decoded)
         return parsed
 
-    def _handle_list(self, shape, node):
-        location = shape.serialization.get('location')
-        if location == 'header' and not isinstance(node, list):
-            # List in headers may be a comma separated string as per RFC7230
-            node = [e.strip() for e in node.split(',')]
-        return super(BaseRestParser, self)._handle_list(shape, node)
-
 
 class RestJSONParser(BaseRestParser, BaseJSONParser):
 
diff --git a/awscli/botocore/serialize.py b/awscli/botocore/serialize.py
index ccc474241..864ecebef 100644
--- a/awscli/botocore/serialize.py
+++ b/awscli/botocore/serialize.py
@@ -585,9 +585,6 @@ class BaseRestSerializer(Serializer):
                 partitioned['query_string_kwargs'][key_name] = param_value
         elif location == 'header':
             shape = shape_members[param_name]
-            if not param_value and shape.type_name == 'list':
-                # Empty lists should not be set on the headers
-                return
             value = self._convert_header_value(shape, param_value)
             partitioned['headers'][key_name] = str(value)
         elif location == 'headers':
@@ -619,12 +616,6 @@ class BaseRestSerializer(Serializer):
             timestamp_format = shape.serialization.get(
                 'timestampFormat', self.HEADER_TIMESTAMP_FORMAT)
             return self._convert_timestamp_to_str(timestamp, timestamp_format)
-        elif shape.type_name == 'list':
-            converted_value = [
-                self._convert_header_value(shape.member, v)
-                for v in value if v is not None
-            ]
-            return ",".join(converted_value)
         elif is_json_value_header(shape):
             # Serialize with no spaces after separators to save space in
             # the header.
diff --git a/awscli/customizations/sso/login.py b/awscli/customizations/sso/login.py
index 1ce7d1446..1d2ab09c6 100644
--- a/awscli/customizations/sso/login.py
+++ b/awscli/customizations/sso/login.py
@@ -26,14 +26,15 @@ class LoginCommand(BasicCommand):
         'Retrieves and caches an AWS SSO access token to exchange for AWS '
         'credentials. To login, the requested profile must have first been '
         'setup using ``aws configure sso``. Each time the ``login`` command '
-        'is called, a new SSO access token will be retrieved.'
+        'is called, a new SSO access token will be retrieved. Please note '
+        'that only one login session can be active for a given SSO Start URL '
+        'and creating multiple profiles does not allow for multiple users to '
+        'be authenticated against the same SSO Start URL.'
     )
     ARG_TABLE = []
     _REQUIRED_SSO_CONFIG_VARS = [
         'sso_start_url',
         'sso_region',
-        'sso_role_name',
-        'sso_account_id',
     ]
 
     def _run_main(self, parsed_args, parsed_globals):


Test patch:
diff --git a/tests/__init__.py b/tests/__init__.py
index 1df09e60f..5e894b2f0 100644
--- a/tests/__init__.py
+++ b/tests/__init__.py
@@ -403,7 +403,7 @@ class PromptToolkitAppRunner:
             target=self._do_run_app, args=(target, args, run_context))
         try:
             thread.start()
-            self._wait_until_app_is_done_updating()
+            self._wait_until_app_is_done_rendering()
             yield run_context
         finally:
             if self._app_is_exitable():
diff --git a/tests/functional/sso/test_login.py b/tests/functional/sso/test_login.py
index 40bed8860..a5af67a1c 100644
--- a/tests/functional/sso/test_login.py
+++ b/tests/functional/sso/test_login.py
@@ -145,6 +145,21 @@ class TestLoginCommand(BaseSSOTest):
             stderr
         )
 
+    def test_login_minimal_sso_configuration(self):
+        content = (
+            '[default]\n'
+            'sso_start_url={start_url}\n'
+            'sso_region={sso_region}\n'
+        ).format(start_url=self.start_url, sso_region=self.sso_region)
+        self.set_config_file_content(content=content)
+        self.add_oidc_workflow_responses(self.access_token)
+        self.run_cmd('sso login')
+        self.assert_used_expected_sso_region(expected_region=self.sso_region)
+        self.assert_cache_contains_token(
+            start_url=self.start_url,
+            expected_token=self.access_token
+        )
+
     def test_login_partially_missing_sso_configuration(self):
         content = (
             '[default]\n'
@@ -158,6 +173,8 @@ class TestLoginCommand(BaseSSOTest):
         )
         self.assertIn('sso_region', stderr)
         self.assertNotIn('sso_start_url', stderr)
+        self.assertNotIn('sso_account_id', stderr)
+        self.assertNotIn('sso_role_name', stderr)
 
     def test_token_cache_datetime_format(self):
         self.add_oidc_workflow_responses(self.access_token)
diff --git a/tests/unit/botocore/protocols/input/rest-json.json b/tests/unit/botocore/protocols/input/rest-json.json
index 54f327dbd..644b179d6 100644
--- a/tests/unit/botocore/protocols/input/rest-json.json
+++ b/tests/unit/botocore/protocols/input/rest-json.json
@@ -2107,88 +2107,5 @@
         }
       }
     ]
-  },
-  {
-    "description": "List in header",
-    "metadata": {
-      "protocol": "rest-json",
-      "apiVersion": "2014-01-01"
-    },
-    "clientEndpoint": "https://rest-json-test.amazonaws.com",
-    "shapes": {
-      "InputShape": {
-        "type": "structure",
-        "members": {
-          "ListParam": {
-            "shape": "ListShape",
-            "location": "header",
-            "locationName": "x-amz-list-param"
-          }
-        }
-      },
-      "ListShape": {
-        "type": "list",
-        "member": {
-          "shape": "EnumType"
-        }
-      },
-      "EnumType": {
-        "type": "string",
-        "enum": ["one", "two", "three"]
-      }
-    },
-    "cases": [
-      {
-        "given": {
-          "http": {
-            "method": "POST",
-            "requestUri": "/2014-01-01/example"
-          },
-          "input": {
-            "shape": "InputShape",
-            "locationName": "OperationRequest"
-          },
-          "name": "OperationName"
-        },
-        "params": {
-          "ListParam": [
-            "one",
-            "two",
-            "three"
-          ]
-        },
-        "serialized": {
-          "method": "POST",
-          "body": "",
-          "uri": "/2014-01-01/example",
-          "headers": {
-            "x-amz-list-param": "one,two,three"
-          }
-        }
-      },
-      {
-        "given": {
-          "http": {
-            "method": "POST",
-            "requestUri": "/2014-01-01/example"
-          },
-          "input": {
-            "shape": "InputShape",
-            "locationName": "OperationRequest"
-          },
-          "name": "OperationName"
-        },
-        "params": {
-          "ListParam": []
-        },
-        "serialized": {
-          "method": "POST",
-          "body": "",
-          "uri": "/2014-01-01/example",
-          "headers": {},
-          "forbidHeaders": ["x-amz-list-param"]
-        }
-      }
-    ]
   }
 ]
diff --git a/tests/unit/botocore/protocols/input/rest-xml.json b/tests/unit/botocore/protocols/input/rest-xml.json
index cd5b98418..b5fa9567f 100644
--- a/tests/unit/botocore/protocols/input/rest-xml.json
+++ b/tests/unit/botocore/protocols/input/rest-xml.json
@@ -1888,89 +1888,5 @@
         }
       }
     ]
-  },
-  {
-    "description": "List in header",
-    "metadata": {
-      "protocol": "rest-xml",
-      "apiVersion": "2014-01-01"
-    },
-    "shapes": {
-      "InputShape": {
-        "type": "structure",
-        "members": {
-          "ListParam": {
-            "shape": "ListShape",
-            "location": "header",
-            "locationName": "x-amz-list-param"
-          }
-        }
-      },
-      "ListShape": {
-        "type": "list",
-        "member": {
-          "shape": "EnumType"
-        }
-      },
-      "EnumType": {
-        "type": "string",
-        "enum": ["one", "two", "three"]
-      }
-    },
-    "cases": [
-      {
-        "given": {
-          "http": {
-            "method": "POST",
-            "requestUri": "/2014-01-01/example"
-          },
-          "input": {
-            "shape": "InputShape",
-            "locationName": "OperationRequest",
-            "xmlNamespace": {"uri": "https://foo/"}
-          },
-          "name": "OperationName"
-        },
-        "params": {
-          "ListParam": [
-            "one",
-            "two",
-            "three"
-          ]
-        },
-        "serialized": {
-          "method": "POST",
-          "body": "",
-          "uri": "/2014-01-01/example",
-          "headers": {
-            "x-amz-list-param": "one,two,three"
-          }
-        }
-      },
-      {
-        "given": {
-          "http": {
-            "method": "POST",
-            "requestUri": "/2014-01-01/example"
-          },
-          "input": {
-            "shape": "InputShape",
-            "locationName": "OperationRequest",
-            "xmlNamespace": {"uri": "https://foo/"}
-          },
-          "name": "OperationName"
-        },
-        "params": {
-          "ListParam": []
-        },
-        "serialized": {
-          "method": "POST",
-          "body": "",
-          "uri": "/2014-01-01/example",
-          "headers": {},
-          "forbidHeaders": ["x-amz-list-param"]
-        }
-      }
-    ]
   }
 ]
diff --git a/tests/unit/botocore/protocols/output/rest-json.json b/tests/unit/botocore/protocols/output/rest-json.json
index c628f75e5..ae6223823 100644
--- a/tests/unit/botocore/protocols/output/rest-json.json
+++ b/tests/unit/botocore/protocols/output/rest-json.json
@@ -1127,135 +1127,87 @@
       }
     ]
   },
-  {
-    "description": "Tagged Unions",
-    "metadata": {
-      "protocol": "rest-json"
-    },
-    "shapes": {
-      "OutputShape": {
-        "type": "structure",
-        "members": {
-          "UnionMember": {
-            "shape": "UnionType"
-          }
-        }
-      },
-      "UnionType": {
-        "type": "structure",
-        "members": {
-          "S":{"shape":"StringType"},
-          "L": {"shape": "ListType"}
-        },
-        "union": true
-      },
-      "ListType": {
-        "type": "list",
-        "member": {
-          "shape": "StringType"
-        }
-      },
-      "StringType": {
-        "type": "string"
-      }
-    },
-    "cases": [
-      {
-        "given": {
-          "output": {
-            "shape": "OutputShape"
-          },
-          "name": "OperationName"
-        },
-        "result": {
-          "UnionMember": {"S":  "mystring"}
-        },
-        "response": {
-          "status_code": 200,
-          "headers": {},
-          "body": "{\"UnionMember\": {\"S\": \"mystring\"}}"
-        }
-      },
-      {
-        "given": {
-          "output": {
-            "shape": "OutputShape"
-          },
-          "name": "OperationName"
-        },
-        "result": {
-          "UnionMember": {"L":  ["a", "b"]}
-        },
-        "response": {
-          "status_code": 200,
-          "headers": {},
-          "body": "{\"UnionMember\": {\"L\": [\"a\", \"b\"]}}"
-        }
-      },
-      {
-        "given": {
-          "output": {
-            "shape": "OutputShape"
-          },
-          "name": "OperationName"
-        },
-        "result": {
-          "UnionMember": {"SDK_UNKNOWN_MEMBER":  {"name": "SomeUnknownMember"}}
-        },
-        "response": {
-          "status_code": 200,
-          "headers": {},
-          "body": "{\"UnionMember\": {\"SomeUnknownMember\": \"foo\"}}"
-        }
-      }
-    ]
-  },
-  {
-    "description": "List in header",
-    "metadata": {
-      "protocol": "rest-json"
-    },
-    "shapes": {
-      "OutputShape": {
-        "type": "structure",
-        "members": {
-          "ListMember": {
-            "shape": "ListShape",
-            "location": "header",
-            "locationName": "x-amz-list-member"
-          }
-        }
-      },
-      "ListShape": {
-        "type": "list",
-        "member": {
-          "shape": "EnumType"
-        }
-      },
-      "EnumType": {
-        "type": "string",
-        "enum": ["one", "two", "three"]
-      }
-    },
-    "cases": [
-      {
-        "given": {
-          "output": {
-            "shape": "OutputShape"
-          },
-          "name": "OperationName"
-        },
-        "result": {
-          "ListMember": ["one", "two", "three"]
+    {
+        "description": "Tagged Unions",
+        "metadata": {
+            "protocol": "rest-json"
+        },
+        "shapes": {
+            "OutputShape": {
+                "type": "structure",
+                "members": {
+                    "UnionMember": {
+                        "shape": "UnionType"
+                    }
+                }
+            },
+            "UnionType": {
+                "type": "structure",
+                "members": {
+                    "S":{"shape":"StringType"},
+                    "L": {"shape": "ListType"}
+                },
+                "union": true
+            },
+            "ListType": {
+                "type": "list",
+                "member": {
+                    "shape": "StringType"
+                }
+            },
+            "StringType": {
+                "type": "string"
+            }
         },
-        "response": {
-          "status_code": 200,
-          "headers": {
-              "x-amz-list-member": " one,two , three "
-          },
-          "body": ""
-        }
-      }
-    ]
-  }
+        "cases": [
+            {
+                "given": {
+                    "output": {
+                        "shape": "OutputShape"
+                    },
+                    "name": "OperationName"
+                },
+                "result": {
+                    "UnionMember": {"S":  "mystring"}
+                },
+                "response": {
+                    "status_code": 200,
+                    "headers": {},
+                    "body": "{\"UnionMember\": {\"S\": \"mystring\"}}"
+                }
+            },
+            {
+                "given": {
+                    "output": {
+                        "shape": "OutputShape"
+                    },
+                    "name": "OperationName"
+                },
+                "result": {
+                    "UnionMember": {"L":  ["a", "b"]}
+                },
+                "response": {
+                    "status_code": 200,
+                    "headers": {},
+                    "body": "{\"UnionMember\": {\"L\": [\"a\", \"b\"]}}"
+                }
+            },
+            {
+                "given": {
+                    "output": {
+                        "shape": "OutputShape"
+                    },
+                    "name": "OperationName"
+                },
+                "result": {
+                    "UnionMember": {"SDK_UNKNOWN_MEMBER":  {"name": "SomeUnknownMember"}}
+                },
+                "response": {
+                    "status_code": 200,
+                    "headers": {},
+                    "body": "{\"UnionMember\": {\"SomeUnknownMember\": \"foo\"}}"
+                }
+            }
+        ]
+    }
 ]
diff --git a/tests/unit/botocore/protocols/output/rest-xml.json b/tests/unit/botocore/protocols/output/rest-xml.json
index 8da468096..23e28b5c7 100644
--- a/tests/unit/botocore/protocols/output/rest-xml.json
+++ b/tests/unit/botocore/protocols/output/rest-xml.json
@@ -1221,53 +1221,5 @@
               }
           }
       ]
-  },
-  {
-    "description": "List in header",
-    "metadata": {
-      "protocol": "rest-xml"
-    },
-    "shapes": {
-      "OutputShape": {
-        "type": "structure",
-        "members": {
-          "ListMember": {
-            "shape": "ListShape",
-            "location": "header",
-            "locationName": "x-amz-list-member"
-          }
-        }
-      },
-      "ListShape": {
-        "type": "list",
-        "member": {
-          "shape": "EnumType"
-        }
-      },
-      "EnumType": {
-        "type": "string",
-        "enum": ["one", "two", "three"]
-      }
-    },
-    "cases": [
-      {
-        "given": {
-          "output": {
-            "shape": "OutputShape"
-          },
-          "name": "OperationName"
-        },
-        "result": {
-          "ListMember": ["one", "two", "three"]
-        },
-        "response": {
-          "status_code": 200,
-          "headers": {
-              "x-amz-list-member": " one,two , three "
-          },
-          "body": ""
-        }
-      }
-    ]
   }
 ]
diff --git a/tests/utils/botocore/__init__.py b/tests/utils/botocore/__init__.py
index 88cdb0635..ab1a3fbb3 100644
--- a/tests/utils/botocore/__init__.py
+++ b/tests/utils/botocore/__init__.py
@@ -160,10 +160,10 @@ class BaseClientDriverTest(unittest.TestCase):
 
     def setUp(self):
         self.driver = ClientDriver()
-        env = os.environ.copy()
+        env = None
         if self.INJECT_DUMMY_CREDS:
-            env['AWS_ACCESS_KEY_ID'] = 'foo'
-            env['AWS_SECRET_ACCESS_KEY'] = 'bar'
+            env = {'AWS_ACCESS_KEY_ID': 'foo',
+                   'AWS_SECRET_ACCESS_KEY': 'bar'}
         self.driver.start(env=env)
 
     def cmd(self, *args):