From 587fed5de3f168cc1e5aef2783d0fbac76addaa0 Mon Sep 17 00:00:00 2001 From: Caesar Kabalan Date: Wed, 25 Aug 2021 00:33:37 -0700 Subject: [PATCH] Add Federated Signin URL Generator Adds a signin "service" in similar fashion to the CLI's configure "service": * Using the AWS federation endpoint this command takes temporary credentials and returns a sign-in URL allowing a user to log in to the AWS Management Console using those temporary credentials. * Follows the published example code for 'Enabling custom identity broker access to the AWS console' in the AWS IAM User Guide. * The name `signin`, while generic and potentially confusing to new users, follows as close as possible to the global service naming convention similar to `iam.amazonaws.com` (`signin.aws.amazon.com`). * Resolves #4642 (feature request) --- awscli/customizations/signin/__init__.py | 17 ++ awscli/customizations/signin/exceptions.py | 36 +++ awscli/customizations/signin/signin.py | 171 ++++++++++++++ awscli/examples/signin/_description.rst | 22 ++ awscli/examples/signin/_examples.rst | 15 ++ awscli/handlers.py | 2 + tests/functional/signin/__init__.py | 12 + tests/functional/signin/test_signin.py | 72 ++++++ tests/unit/customizations/signin/__init__.py | 65 +++++ .../unit/customizations/signin/test_signin.py | 223 ++++++++++++++++++ 10 files changed, 635 insertions(+) create mode 100644 awscli/customizations/signin/__init__.py create mode 100644 awscli/customizations/signin/exceptions.py create mode 100644 awscli/customizations/signin/signin.py create mode 100644 awscli/examples/signin/_description.rst create mode 100644 awscli/examples/signin/_examples.rst create mode 100644 tests/functional/signin/__init__.py create mode 100644 tests/functional/signin/test_signin.py create mode 100644 tests/unit/customizations/signin/__init__.py create mode 100644 tests/unit/customizations/signin/test_signin.py diff --git a/awscli/customizations/signin/__init__.py b/awscli/customizations/signin/__init__.py new file mode 100644 index 000000000000..931eda530de7 --- /dev/null +++ b/awscli/customizations/signin/__init__.py @@ -0,0 +1,17 @@ +# Copyright 2021 Amazon.com, Inc. or its affiliates. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"). You +# may not use this file except in compliance with the License. A copy of +# the License is located at +# +# http://aws.amazon.com/apache2.0/ +# +# or in the "license" file accompanying this file. This file is +# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF +# ANY KIND, either express or implied. See the License for the specific +# language governing permissions and limitations under the License. +from awscli.customizations.signin.signin import SigninCommand + + +def register_signin_command(cli): + cli.register('building-command-table.main', SigninCommand.add_command) diff --git a/awscli/customizations/signin/exceptions.py b/awscli/customizations/signin/exceptions.py new file mode 100644 index 000000000000..1494230437df --- /dev/null +++ b/awscli/customizations/signin/exceptions.py @@ -0,0 +1,36 @@ +# Copyright 2021 Amazon.com, Inc. or its affiliates. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"). You +# may not use this file except in compliance with the License. A copy of +# the License is located at +# +# http://aws.amazon.com/apache2.0/ +# +# or in the "license" file accompanying this file. This file is +# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF +# ANY KIND, either express or implied. See the License for the specific +# language governing permissions and limitations under the License. + + +class SigninError(Exception): + """ Base class for all SigninErrors.""" + fmt = 'An unspecified error occurred' + + def __init__(self, **kwargs): + msg = self.fmt.format(**kwargs) + super(SigninError, self).__init__(msg) + self.kwargs = kwargs + + +class NonTemporaryCredentialsError(SigninError): + fmt = ("Error: The current profile contains long-term credentials. You may" + " only signin with temporary credentials.") + + +class SessionDurationOutOfRangeError(SigninError): + fmt = ("Error: The specified Session Duration must be 900 seconds (15" + " minutes) to 43200 seconds (12 hours).") + + +class FederationResponseError(SigninError): + fmt = "Error: AWS Federation Endpoint: {msg}" diff --git a/awscli/customizations/signin/signin.py b/awscli/customizations/signin/signin.py new file mode 100644 index 000000000000..89c4bee2b27c --- /dev/null +++ b/awscli/customizations/signin/signin.py @@ -0,0 +1,171 @@ +# Copyright 2021 Amazon.com, Inc. or its affiliates. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"). You +# may not use this file except in compliance with the License. A copy of +# the License is located at +# +# http://aws.amazon.com/apache2.0/ +# +# or in the "license" file accompanying this file. This file is +# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF +# ANY KIND, either express or implied. See the License for the specific +# language governing permissions and limitations under the License. +from awscli.customizations.commands import BasicCommand +from awscli.customizations.signin import exceptions + +import urllib.parse +import urllib.request +import urllib.error +from botocore.awsrequest import AWSRequest +from botocore.httpsession import URLLib3Session +import json +import sys + + +class SigninCommand(BasicCommand): + NAME = 'signin' + DESCRIPTION = BasicCommand.FROM_FILE() + SYNOPSIS = '' + ARG_TABLE = [ + { + 'name': 'session-duration', + 'cli_type_name': 'integer', + 'help_text': ( + "

Specifies the duration of the console session. This is" + " separate from the duration of the temporary credentials that" + " you specify using the DurationSeconds parameter of an" + " sts:AssumeRole call. You can specify a --session-duration" + " maximum value of 43200 (12 hours). If the --session-duration" + " parameter is missing, then the session defaults to the" + " duration of the credentials of the profile used for this " + " command (which defaults to one hour).

See the" + " documentation for the `sts:AssumeRole` API for details about" + " how to specify a duration using the DurationSeconds" + " parameter. The ability to create a console session that is" + " longer than one hour is intrinsic to the getSigninToken" + " operation of the federation endpoint.

" + ), + 'required': False + }, + { + 'name': 'destination-url', + 'help_text': ( + "

URL for the desired AWS console page. The browser will" + " automatically redirect to this URL after login.

To" + " provide this value you will need to set the config option" + " `cli_follow_urlparam` to false.

" + ), + 'required': False + }, + { + 'name': 'issuer-url', + 'help_text': ( + "

URL for your internal sign-in page. The browser will" + " automatically redirect to this URL after the user's session" + " expires.

To provide this value you will need to set the" + " config option `cli_follow_urlparam` to false.

" + ), + 'required': False + }, + { + 'name': 'partition', + 'help_text': ( + "

The AWS partition for the signin URLs.

" + ), + 'required': False, + 'default': 'AWS', + 'choices': ['AWS', 'AWS_CN', 'AWS_US_GOV'], + }, + ] + EXAMPLES = BasicCommand.FROM_FILE() + + def __init__(self, session, prompter=None, config_writer=None): + super(SigninCommand, self).__init__(session) + + def _run_main(self, parsed_args, parsed_globals): + # Called when invoked with no args "aws signin" + # Reference Architecture: https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_providers_enable-console-custom-url.html + + credentials = self._session.get_credentials() + if credentials.token is None: + # Temporary credentials are REQUIRED + raise exceptions.NonTemporaryCredentialsError() + + json_credentials = { + 'sessionId': credentials.access_key, + 'sessionKey': credentials.secret_key, + 'sessionToken': credentials.token + } + + partitions = { + 'AWS': 'aws.amazon.com', + 'AWS_US_GOV': 'amazonaws-us-gov.com', + 'AWS_CN': 'amazonaws.cn' + } + token_url = self._build_getsignintoken_url( + credentials=json_credentials, + partition=partitions[parsed_args.partition], + session_duration=parsed_args.session_duration + ) + + # Federation endpoint always returns a JSON object with a + # 'SigninToken' key as long as the request is properly formatted + # with an in-range SessionDuration parameter. Conveniently this + # allows us to test with invalid credentials or partitions we don't + # have credentials for. + req = URLLib3Session() + response = req.send(AWSRequest('GET', token_url).prepare()) + if response.status_code >= 400: + raise exceptions.FederationResponseError( + msg=f"HTTP Code {response.status_code}" + ) + + federation_response = response.content + try: + signin_token_json = json.loads(federation_response) + except ValueError: + raise exceptions.FederationResponseError( + msg='Malformed reponse. Not a JSON string.' + ) + + if 'SigninToken' not in signin_token_json: + raise exceptions.FederationResponseError( + msg=('Malformed reponse. JSON string does not contain key ' + 'Signintoken') + ) + + signin_url = self._build_login_url( + partition=partitions[parsed_args.partition], + signin_token=signin_token_json['SigninToken'], + destination_url=parsed_args.destination_url, + issuer_url=parsed_args.issuer_url + ) + + sys.stdout.write(signin_url + "\n") + return 0 + + @staticmethod + def _build_getsignintoken_url(credentials, partition, + session_duration=None): + string_credentials = json.dumps(credentials) + url = f"https://signin.{partition}/federation?Action=getSigninToken" + if session_duration: + if not 900 <= session_duration <= 43200: + raise exceptions.SessionDurationOutOfRangeError() + url += f"&SessionDuration={session_duration}" + url += f"&Session={urllib.parse.quote_plus(string_credentials)}" + return url + + @staticmethod + def _build_login_url(partition, signin_token, destination_url=None, + issuer_url=None): + url = f"https://signin.{partition}/federation?Action=login" + if issuer_url: + url += f"&Issuer={urllib.parse.quote_plus(issuer_url)}" + dest_url = destination_url or f"https://console.{partition}/" + url += f"&Destination={urllib.parse.quote_plus(dest_url)}" + url += f"&SigninToken={signin_token}" + return url diff --git a/awscli/examples/signin/_description.rst b/awscli/examples/signin/_description.rst new file mode 100644 index 000000000000..6876c951fb40 --- /dev/null +++ b/awscli/examples/signin/_description.rst @@ -0,0 +1,22 @@ +Generate a sign-in URL for the AWS Management Console using temporary +credentials. + +This command **MUST** be invoked with a profile containing temporary credentials. The profile may not contain long-term credentials including **aws_access_key_id** and **aws_secret_access_key**. + +This command is used to provide AWS Management Console access to a set of assumed role credentials. A typical workflow allows for a AWS IAM User without direct console access to assume a role, then run this **signin** command to generate a URL allowing sign-in to the AWS Management Console. Typically this command will be used when an AWS IAM User has an Access Key and Secret Access Key, no console login password, but access to assume a role. + +The following credential configuration also allows for transparent role assumption:: + + [my_user] + aws_access_key_id = AKIAABCDEFGHIJKLMNOP + aws_secret_access_key = ... + + [default] + role_arn = arn:aws:iam::012345678910:role/my_role + role_session_name = example-session-name + source_profile = my_user + duration_seconds = 43200 + +For more information on this process, see `Enabling custom identity broker access to the AWS console`_ in the *AWS Identity and Access Management User Guide*. + +.. _`Enabling custom identity broker access to the AWS console`: https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_providers_enable-console-custom-url.html diff --git a/awscli/examples/signin/_examples.rst b/awscli/examples/signin/_examples.rst new file mode 100644 index 000000000000..94f00fab376a --- /dev/null +++ b/awscli/examples/signin/_examples.rst @@ -0,0 +1,15 @@ +To generate an AWS Management Console signin URL with the default profile:: + + $ aws signin + +To generate an AWS Management Console signin URL with the my_role profile:: + + $ aws --profile my_role signin + +To go directly to the CloudFormation service page after login:: + + $ aws signin --destination-url https://console.aws.amazon.com/cloudformation/home + +To generate a signin link to AWS GovCloud:: + + $ aws signin --partition AWS_US_GOV diff --git a/awscli/handlers.py b/awscli/handlers.py index 0c1f8781886f..99ce4bfc7426 100644 --- a/awscli/handlers.py +++ b/awscli/handlers.py @@ -91,6 +91,7 @@ from awscli.customizations.sessionmanager import register_ssm_session from awscli.customizations.sms_voice import register_sms_voice_hide from awscli.customizations.dynamodb import register_dynamodb_paginator_fix +from awscli.customizations.signin import register_signin_command def awscli_initialize(event_handlers): @@ -183,3 +184,4 @@ def awscli_initialize(event_handlers): register_ssm_session(event_handlers) register_sms_voice_hide(event_handlers) register_dynamodb_paginator_fix(event_handlers) + register_signin_command(event_handlers) diff --git a/tests/functional/signin/__init__.py b/tests/functional/signin/__init__.py new file mode 100644 index 000000000000..3757aff59af3 --- /dev/null +++ b/tests/functional/signin/__init__.py @@ -0,0 +1,12 @@ +# Copyright 2021 Amazon.com, Inc. or its affiliates. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"). You +# may not use this file except in compliance with the License. A copy of +# the License is located at +# +# http://aws.amazon.com/apache2.0/ +# +# or in the "license" file accompanying this file. This file is +# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF +# ANY KIND, either express or implied. See the License for the specific +# language governing permissions and limitations under the License. diff --git a/tests/functional/signin/test_signin.py b/tests/functional/signin/test_signin.py new file mode 100644 index 000000000000..49651d6ad420 --- /dev/null +++ b/tests/functional/signin/test_signin.py @@ -0,0 +1,72 @@ +# Copyright 2021 Amazon.com, Inc. or its affiliates. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"). You +# may not use this file except in compliance with the License. A copy of +# the License is located at +# +# http://aws.amazon.com/apache2.0/ +# +# or in the "license" file accompanying this file. This file is +# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF +# ANY KIND, either express or implied. See the License for the specific +# language governing permissions and limitations under the License. +from awscli.testutils import FileCreator, BaseAWSCommandParamsTest, \ + create_clidriver + + +class TestSignin(BaseAWSCommandParamsTest): + prefix = 'signin' + + def setUp(self): + super(TestSignin, self).setUp() + self.environ['AWS_CONFIG_FILE'] = FileCreator().create_file( + 'config', + '[default]\ncli_follow_urlparam = false\n') + self.environ['AWS_ACCESS_KEY_ID'] = 'ASIAAAAAAAAAAAAAAAAAA' + self.environ['AWS_SECRET_ACCESS_KEY'] = 'SECRET_ACCESS_TEST_VALUE' + self.environ['AWS_SESSION_TOKEN'] = 'SESSION_TOKEN_TEST_VALUE' + self.driver = create_clidriver() + + def tearDown(self): + super(TestSignin, self).tearDown() + + def test_signin(self): + args = (' --partition AWS --destination-url' + ' https://console.aws.amazon.com/cloudformation/home' + ' --issuer-url http://sso.mycompany.com') + cmdline = self.prefix + args + + stdout, stderr, rc = self.run_cmd(cmdline, expected_rc=0) + + self.assertIn( + 'https://signin.aws.amazon.com/federation', + stdout, + msg='Signin URL does not contain the proper domain and path' + ) + + self.assertIn( + 'Action=login', + stdout, + msg='Signin URL does not contain the proper action parameter' + ) + + self.assertIn( + 'Issuer=http%3A%2F%2Fsso.mycompany.com', + stdout, + msg='Signin URL does not contain the proper issuer parameter' + ) + + self.assertIn( + ('Destination=https%3A%2F%2Fconsole.aws.amazon.com%2Fcloudformatio' + 'n%2Fhome'), + stdout, + msg='Signin URL does not contain the proper destination parameter' + ) + + self.assertIn( + ('SigninToken='), + stdout, + msg='Signin URL does not contain a signin token' + ) + + self.assertEqual(rc, 0, msg='Signin does not return a 0 exit code') diff --git a/tests/unit/customizations/signin/__init__.py b/tests/unit/customizations/signin/__init__.py new file mode 100644 index 000000000000..403369ef79a5 --- /dev/null +++ b/tests/unit/customizations/signin/__init__.py @@ -0,0 +1,65 @@ +# Copyright 2021 Amazon.com, Inc. or its affiliates. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"). You +# may not use this file except in compliance with the License. A copy of +# the License is located at +# +# http://aws.amazon.com/apache2.0/ +# +# or in the "license" file accompanying this file. This file is +# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF +# ANY KIND, either express or implied. See the License for the specific +# language governing permissions and limitations under the License. +from botocore.exceptions import ProfileNotFound + + +class FakeSession(object): + + def __init__(self, all_variables, profile_does_not_exist=False, + config_file_vars=None, environment_vars=None, + credentials=None, profile=None): + self.variables = all_variables + self.profile_does_not_exist = profile_does_not_exist + self.config = {} + if config_file_vars is None: + config_file_vars = {} + self.config_file_vars = config_file_vars + if environment_vars is None: + environment_vars = {} + self.environment_vars = environment_vars + self._credentials = credentials + self.profile = profile + + def get_credentials(self): + return self._credentials + + def get_scoped_config(self): + if self.profile_does_not_exist: + raise ProfileNotFound(profile='foo') + return self.config + + def get_config_variable(self, name, methods=None): + if name == 'credentials_file': + # The credentials_file var doesn't require a + # profile to exist. + return '~/fake_credentials_filename' + if self.profile_does_not_exist and not name == 'config_file': + raise ProfileNotFound(profile='foo') + if methods is not None: + if 'env' in methods: + return self.environment_vars.get(name) + elif 'config' in methods: + return self.config_file_vars.get(name) + else: + return self.variables.get(name) + + def emit(self, event_name, **kwargs): + pass + + def emit_first_non_none_response(self, *args, **kwargs): + pass + + def _build_profile_map(self): + if self.full_config is None: + return None + return self.full_config['profiles'] diff --git a/tests/unit/customizations/signin/test_signin.py b/tests/unit/customizations/signin/test_signin.py new file mode 100644 index 000000000000..907a7a6cdef1 --- /dev/null +++ b/tests/unit/customizations/signin/test_signin.py @@ -0,0 +1,223 @@ +# Copyright 2021 Amazon.com, Inc. or its affiliates. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"). You +# may not use this file except in compliance with the License. A copy of +# the License is located at +# +# http://aws.amazon.com/apache2.0/ +# +# or in the "license" file accompanying this file. This file is +# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF +# ANY KIND, either express or implied. See the License for the specific +# language governing permissions and limitations under the License. +from argparse import Namespace +from awscli.customizations.signin import exceptions +from awscli.customizations.signin.signin import SigninCommand +from awscli.testutils import unittest, capture_output, mock +from tests.unit.customizations.configure import FakeSession + + +class TestSigninCommand(unittest.TestCase): + + def setUp(self): + self.global_args = Namespace() + self.global_args.profile = 'default' + + def test_signin_defaults(self): + credentials = mock.Mock() + credentials.access_key = 'ASIAAAAAAAAAAAAAAAAAA' + credentials.secret_key = 'SECRET_ACCESS_TEST_VALUE' + credentials.token = 'SESSION_TOKEN_TEST_VALUE' + session = FakeSession({}, profile='default', credentials=credentials) + cmd = SigninCommand(session=session) + with capture_output() as captured: + cmd(args='', parsed_globals=self.global_args) + self.assertIn( + ('https://signin.aws.amazon.com/federation?Action=login&Destinatio' + 'n=https%3A%2F%2Fconsole.aws.amazon.com%2F&SigninToken='), + captured.stdout.getvalue() + ) + + @mock.patch('awscli.customizations.signin.signin.URLLib3Session.send') + def test_signin_with_bad_request(self, mock_send): + response = mock.Mock() + response.status_code = 400 + response.content = 'Bad Request' + mock_send.return_value = response + credentials = mock.Mock() + credentials.access_key = 'ASIAAAAAAAAAAAAAAAAAA' + credentials.secret_key = 'SECRET_ACCESS_TEST_VALUE' + credentials.token = 'SESSION_TOKEN_TEST_VALUE' + session = FakeSession({}, profile='default', credentials=credentials) + cmd = SigninCommand(session=session) + msg = "Signin doesn't raise federation response error with bad data" + with self.assertRaises(exceptions.FederationResponseError, msg=msg): + cmd( + args='', + parsed_globals=self.global_args + ) + + @mock.patch('awscli.customizations.signin.signin.URLLib3Session.send') + def test_signin_with_non_json_response(self, mock_send): + response = mock.Mock() + response.status_code = 200 + response.content = 'Some Non-JSON Content' + mock_send.return_value = response + credentials = mock.Mock() + credentials.access_key = 'ASIAAAAAAAAAAAAAAAAAA' + credentials.secret_key = 'SECRET_ACCESS_TEST_VALUE' + credentials.token = 'SESSION_TOKEN_TEST_VALUE' + session = FakeSession({}, profile='default', credentials=credentials) + cmd = SigninCommand(session=session) + msg = "Signin doesn't raise federation response error with bad data" + with self.assertRaises(exceptions.FederationResponseError, msg=msg): + cmd( + args='', + parsed_globals=self.global_args + ) + + @mock.patch('awscli.customizations.signin.signin.URLLib3Session.send') + def test_signin_with_malformed_json_response(self, mock_send): + response = mock.Mock() + response.status_code = 200 + response.content = '{"test_key": "test_value"}' + mock_send.return_value = response + credentials = mock.Mock() + credentials.access_key = 'ASIAAAAAAAAAAAAAAAAAA' + credentials.secret_key = 'SECRET_ACCESS_TEST_VALUE' + credentials.token = 'SESSION_TOKEN_TEST_VALUE' + session = FakeSession({}, profile='default', credentials=credentials) + cmd = SigninCommand(session=session) + msg = "Signin doesn't raise federation response error with bad data" + with self.assertRaises(exceptions.FederationResponseError, msg=msg): + cmd( + args='', + parsed_globals=self.global_args + ) + + def test_signin_with_user_credentials(self): + credentials = mock.Mock() + credentials.access_key = 'AKIAAAAAAAAAAAAAAAAAA' + credentials.secret_key = 'SECRET_ACCESS_TEST_VALUE' + credentials.token = None + session = FakeSession({}, profile='default', credentials=credentials) + cmd = SigninCommand(session=session) + msg = "Signin doesn't raise error when using non-temporary credentials" + with self.assertRaises(exceptions.NonTemporaryCredentialsError, + msg=msg): + cmd( + args='', + parsed_globals=self.global_args + ) + + def test_build_getsignintoken_url_default(self): + url = SigninCommand._build_getsignintoken_url( + credentials={ + 'sessionId': 'TEST_VALUE', + 'sessionKey': 'TEST_VALUE', + 'sessionToken': 'TEST_VALUE' + }, + partition='aws.amazon.com' + ) + self.assertEqual( + url, + ('https://signin.aws.amazon.com/federation?Action=getSigninToken&S' + 'ession=%7B%22sessionId%22%3A+%22TEST_VALUE%22%2C+%22sessionKey%2' + '2%3A+%22TEST_VALUE%22%2C+%22sessionToken%22%3A+%22TEST_VALUE%22%' + '7D'), + 'Improperly formatted default Signin Token URL' + ) + + def test_build_getsignintoken_url_with_duration_min(self): + url = SigninCommand._build_getsignintoken_url( + credentials={ + 'sessionId': 'TEST_VALUE', + 'sessionKey': 'TEST_VALUE', + 'sessionToken': 'TEST_VALUE' + }, + partition='aws.amazon.com', + session_duration=900 + ) + self.assertEqual( + url, + ('https://signin.aws.amazon.com/federation?Action=getSigninToken&S' + 'essionDuration=900&Session=%7B%22sessionId%22%3A+%22TEST_VALUE%2' + '2%2C+%22sessionKey%22%3A+%22TEST_VALUE%22%2C+%22sessionToken%22%' + '3A+%22TEST_VALUE%22%7D'), + 'Improperly formatted Signin Token URL with minimum duration' + ) + + def test_build_getsignintoken_url_with_duration_max(self): + url = SigninCommand._build_getsignintoken_url( + credentials={ + 'sessionId': 'TEST_VALUE', + 'sessionKey': 'TEST_VALUE', + 'sessionToken': 'TEST_VALUE' + }, + partition='aws.amazon.com', + session_duration=43200 + ) + self.assertEqual( + url, + ('https://signin.aws.amazon.com/federation?Action=getSigninToken&S' + 'essionDuration=43200&Session=%7B%22sessionId%22%3A+%22TEST_VALUE' + '%22%2C+%22sessionKey%22%3A+%22TEST_VALUE%22%2C+%22sessionToken%2' + '2%3A+%22TEST_VALUE%22%7D'), + 'Improperly formatted Signin Token URL with maximum duration' + ) + + def test_build_getsignintoken_url_with_duration_too_long(self): + msg = 'URL improperly generated with out-of-range session duration' + with self.assertRaises(exceptions.SessionDurationOutOfRangeError, + msg=msg): + SigninCommand._build_getsignintoken_url( + credentials={ + 'sessionId': 'TEST_VALUE', + 'sessionKey': 'TEST_VALUE', + 'sessionToken': 'TEST_VALUE' + }, + partition='aws.amazon.com', + session_duration=43201 + ) + + def test_build_getsignintoken_url_with_duration_too_short(self): + msg = 'URL improperly generated with out-of-range session duration' + with self.assertRaises(exceptions.SessionDurationOutOfRangeError, + msg=msg): + SigninCommand._build_getsignintoken_url( + credentials={ + 'sessionId': 'TEST_VALUE', + 'sessionKey': 'TEST_VALUE', + 'sessionToken': 'TEST_VALUE' + }, + partition='aws.amazon.com', + session_duration=899 + ) + + def test_build_login_url_default(self): + url = SigninCommand._build_login_url( + partition='aws.amazon.com', + signin_token='TOKEN_HERE' + ) + self.assertEqual( + url, + ('https://signin.aws.amazon.com/federation?Action=login&Destinatio' + 'n=https%3A%2F%2Fconsole.aws.amazon.com%2F&SigninToken=TOKEN_HERE' + ), + 'Improperly formatted default Login URL' + ) + + def test_build_login_url_with_optional_urls(self): + url = SigninCommand._build_login_url( + partition='aws.amazon.com', + signin_token='TOKEN_HERE', + destination_url='https://desturl.amazon.com/', + issuer_url='http://login.mycompany.com/' + ) + self.assertEqual( + url, + ('https://signin.aws.amazon.com/federation?Action=login&Issuer=htt' + 'p%3A%2F%2Flogin.mycompany.com%2F&Destination=https%3A%2F%2Fdestu' + 'rl.amazon.com%2F&SigninToken=TOKEN_HERE'), + 'Improperly formatted Login URL with destination and issuer URLs' + )