-
Notifications
You must be signed in to change notification settings - Fork 4.1k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
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)
- Loading branch information
Showing
10 changed files
with
638 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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}" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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': ( | ||
"<p>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).</p><p>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.</p>" | ||
), | ||
'required': False | ||
}, | ||
{ | ||
'name': 'destination-url', | ||
'help_text': ( | ||
"<p>URL for the desired AWS console page. The browser will" | ||
" automatically redirect to this URL after login.</p><p>To" | ||
" provide this value you will need to set the config option" | ||
" `cli_follow_urlparam` to false.</p>" | ||
), | ||
'required': False | ||
}, | ||
{ | ||
'name': 'issuer-url', | ||
'help_text': ( | ||
"<p>URL for your internal sign-in page. The browser will" | ||
"automatically redirect to this URL after the user's session" | ||
"expires.</p><p>To provide this value you will need to set the" | ||
" config option `cli_follow_urlparam` to false.</p>" | ||
), | ||
'required': False | ||
}, | ||
{ | ||
'name': 'partition', | ||
'help_text': ( | ||
"<p>The AWS partition for the signin URLs.</p><ul>" | ||
"<li>**AWS** = aws.amazon.com</li>" | ||
"<li>**AWS_US_GOV** = amazonaws-us-gov.com</li>" | ||
"<li>**AWS_CN** = amazonaws.cn</li></ul>" | ||
), | ||
'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) | ||
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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') |
Oops, something went wrong.