From f39bc0a376a61098fb7715f8b65c538dd1ad5c7b Mon Sep 17 00:00:00 2001 From: Patrick Decat Date: Tue, 28 Sep 2021 11:16:44 +0200 Subject: [PATCH] Allow passing additional `--console-role-arn` and `--console-external-id` parameters in conjunction with `--print-console-signin-url` --- README.md | 12 +++++++++- aws_adfs/login.py | 57 ++++++++++++++++++++++++++++++++++++++++++++--- poetry.lock | 46 ++++++++++++++++++++++++++++++++++---- pyproject.toml | 1 + 4 files changed, 108 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 0c72691e..73839ce6 100644 --- a/README.md +++ b/README.md @@ -253,6 +253,14 @@ aws-adfs integrates with: AWS_DEFAULT_REGION environmental variables instead of saving them to the aws configuration file. + --print-console-signin-url Output a URL that lets users who sign in to + your organization's network securely access + the AWS Management Console. + --console-role-arn TEXT Role to assume for use in conjunction with + --print-console-signin-url + --console-external-id TEXT External ID to pass in assume role for use + in conjunction with --print-console-signin- + url --role-arn TEXT Predefined role arn to selects, e.g. aws- adfs login --role-arn arn:aws:iam::123456789 012:role/YourSpecialRole @@ -420,4 +428,6 @@ poetry run pytest * [johan1252](https://github.com/johan1252) for: Ask for authentication method if there is no default method set in Duo Security settings * [pdecat](https://github.com/pdecat) for: Always return the same number of values from _initiate_authentication() * [mikereinhold](https://github.com/mikereinhold) for: Feature credential process -* [pdecat](https://github.com/pdecat) for: Add --username-password-command command line parameter +* [pdecat](https://github.com/pdecat) for: + * Add --username-password-command command line parameter + * Add --print-console-signin-url, --console-role-arn and --console-external-id command line parameters \ No newline at end of file diff --git a/aws_adfs/login.py b/aws_adfs/login.py index d0416be4..625f22c8 100644 --- a/aws_adfs/login.py +++ b/aws_adfs/login.py @@ -10,6 +10,7 @@ from os import environ from platform import system +import boto3 import botocore import botocore.exceptions import botocore.session @@ -97,6 +98,14 @@ is_flag=True, help='Output a URL that lets users who sign in to your organization\'s network securely access the AWS Management Console.', ) +@click.option( + "--console-role-arn", + help="Role to assume for use in conjunction with --print-console-signin-url", +) +@click.option( + "--console-external-id", + help="External ID to pass in assume role for use in conjunction with --print-console-signin-url", +) @click.option( '--role-arn', help='Predefined role arn to selects, e.g. aws-adfs login --role-arn arn:aws:iam::123456789012:role/YourSpecialRole', @@ -141,6 +150,8 @@ def login( stdout, printenv, print_console_signin_url, + console_role_arn, + console_external_id, role_arn, session_duration, no_session_cache, @@ -269,7 +280,9 @@ def login( _emit_summary(config, aws_session_duration) _print_environment_variables(aws_session_token, config) elif print_console_signin_url: - _print_console_signin_url(aws_session_token, adfs_host) + _print_console_signin_url( + aws_session_token, adfs_host, console_role_arn, console_external_id + ) else: _store(config, aws_session_token) _emit_summary(config, aws_session_duration) @@ -300,9 +313,43 @@ def _print_environment_variables(aws_session_token, config): u"""{} AWS_DEFAULT_REGION={}""".format(envcommand, config.region)) -def _print_console_signin_url(aws_session_token, adfs_host): +def _print_console_signin_url( + aws_session_token, adfs_host, console_role_arn, console_external_id +): # The steps below come from https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_providers_enable-console-custom-url.html + if console_role_arn: + # Step 2: Using the access keys for an IAM user in your AWS account, + # call "AssumeRole" to get temporary access keys for the federated user + + # Note: Calls to AWS STS AssumeRole must be signed using the access key ID + # and secret access key of an IAM user or using existing temporary credentials. + # The credentials can be in EC2 instance metadata, in environment variables, + # or in a configuration file, and will be discovered automatically by the + # client('sts') function. For more information, see the Python SDK docs: + # http://boto3.readthedocs.io/en/latest/reference/services/sts.html + # http://boto3.readthedocs.io/en/latest/reference/services/sts.html#STS.Client.assume_role + + # FIXME: use botocore instead of boto3: https://github.com/boto/botocore/blob/1.21.49/botocore/credentials.py#L766 + sts_connection = boto3.client( + "sts", + aws_access_key_id=aws_session_token["Credentials"]["AccessKeyId"], + aws_secret_access_key=aws_session_token["Credentials"]["SecretAccessKey"], + aws_session_token=aws_session_token["Credentials"]["SessionToken"], + ) + + if console_external_id: + aws_session_token = sts_connection.assume_role( + RoleArn=console_role_arn, + RoleSessionName="aws-adfs", + ExternalId=console_external_id, + ) + else: + aws_session_token = sts_connection.assume_role( + RoleArn=console_role_arn, + RoleSessionName="aws-adfs", + ) + # Step 3: Format resulting temporary credentials into JSON url_credentials = {} url_credentials['sessionId'] = aws_session_token['Credentials']['AccessKeyId'] @@ -314,7 +361,11 @@ def _print_console_signin_url(aws_session_token, adfs_host): # the sign-in action request, a 12-hour session duration, and the JSON document with temporary credentials # as parameters. request_parameters = "?Action=getSigninToken" - request_parameters += "&SessionDuration=43200" + + # https://signin.aws.amazon.com/federation endpoint returns a HTTP/1.1 400 Bad Request error with AssumeRole credentials when SessionDuration is set + if not console_role_arn: + request_parameters += "&SessionDuration=43200" + request_parameters += "&Session=" + urllib.parse.quote_plus(json_string_with_temp_credentials) request_url = "https://signin.aws.amazon.com/federation" + request_parameters r = requests.get(request_url) diff --git a/poetry.lock b/poetry.lock index 7c3ecaf7..b051d084 100644 --- a/poetry.lock +++ b/poetry.lock @@ -20,9 +20,25 @@ docs = ["furo", "sphinx", "zope.interface", "sphinx-notfound-page"] tests = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface", "cloudpickle"] tests_no_zope = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "cloudpickle"] +[[package]] +name = "boto3" +version = "1.20.50" +description = "The AWS SDK for Python" +category = "main" +optional = false +python-versions = ">= 3.6" + +[package.dependencies] +botocore = ">=1.23.50,<1.24.0" +jmespath = ">=0.7.1,<1.0.0" +s3transfer = ">=0.5.0,<0.6.0" + +[package.extras] +crt = ["botocore[crt] (>=1.21.0,<2.0a0)"] + [[package]] name = "botocore" -version = "1.23.43" +version = "1.23.50" description = "Low-level, data-driven core of boto 3." category = "main" optional = false @@ -400,6 +416,20 @@ requests = ">=2.0" [package.extras] dev = ["setuptools-version-command"] +[[package]] +name = "s3transfer" +version = "0.5.1" +description = "An Amazon S3 Transfer Manager" +category = "main" +optional = false +python-versions = ">= 3.6" + +[package.dependencies] +botocore = ">=1.12.36,<2.0a.0" + +[package.extras] +crt = ["botocore[crt] (>=1.20.29,<2.0a.0)"] + [[package]] name = "six" version = "1.16.0" @@ -460,7 +490,7 @@ testing = ["pytest (>=4.6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytes [metadata] lock-version = "1.1" python-versions = "^3.6" -content-hash = "906b6f8e325a908416badffbe5e0b36a6e645f987b15605514a604908037e122" +content-hash = "eefbe01ae6176c012e1ba9b159acf18611317cb7fedb7676bca876f371ffb6fa" [metadata.files] atomicwrites = [ @@ -471,9 +501,13 @@ attrs = [ {file = "attrs-21.4.0-py2.py3-none-any.whl", hash = "sha256:2d27e3784d7a565d36ab851fe94887c5eccd6a463168875832a1be79c82828b4"}, {file = "attrs-21.4.0.tar.gz", hash = "sha256:626ba8234211db98e869df76230a137c4c40a12d72445c45d5f5b716f076e2fd"}, ] +boto3 = [ + {file = "boto3-1.20.50-py3-none-any.whl", hash = "sha256:87994c3753ef386189a222556eaf7cb1ef63432d516b4344c7b2899ce544ad2a"}, + {file = "boto3-1.20.50.tar.gz", hash = "sha256:c62362e3105c918272a95c9f4881587c3a3c68aa5fedcd322313def3688c9f7b"}, +] botocore = [ - {file = "botocore-1.23.43-py3-none-any.whl", hash = "sha256:22c88a653a026439f2e9b0ade154fafe0eaaaea3fee6e080102d90ec4271284e"}, - {file = "botocore-1.23.43.tar.gz", hash = "sha256:f8c60dff90a7aea7f84908f0e4e778890d4f08c883d2da111c15c10d7c199102"}, + {file = "botocore-1.23.50-py3-none-any.whl", hash = "sha256:aa953d9767ff99a7aa35dde770a1405c8877cef9caf280859b94104483b8368d"}, + {file = "botocore-1.23.50.tar.gz", hash = "sha256:109d9a200f70268d5429423fd8052f6fed5e041853d6621081692ea5ad7f70c7"}, ] certifi = [ {file = "certifi-2021.10.8-py2.py3-none-any.whl", hash = "sha256:d62a0163eb4c2344ac042ab2bdf75399a71a2d8c7d47eac2e2ee91b9d6339569"}, @@ -778,6 +812,10 @@ requests-kerberos = [ requests-negotiate-sspi = [ {file = "requests_negotiate_sspi-0.5.2-py2.py3-none-any.whl", hash = "sha256:84ca9e81cfd3f2bd5eede5f8eddec1d5b58d957efac5e7fc078a3b323d296b77"}, ] +s3transfer = [ + {file = "s3transfer-0.5.1-py3-none-any.whl", hash = "sha256:25c140f5c66aa79e1ac60be50dcd45ddc59e83895f062a3aab263b870102911f"}, + {file = "s3transfer-0.5.1.tar.gz", hash = "sha256:69d264d3e760e569b78aaa0f22c97e955891cd22e32b10c51f784eeda4d9d10a"}, +] six = [ {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, diff --git a/pyproject.toml b/pyproject.toml index ae7704f8..73ef8e94 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -42,6 +42,7 @@ requests-kerberos = [ requests-negotiate-sspi= [ { version = ">=0.3.4", markers = "platform_system == 'Windows'" }, ] +boto3 = "^1.20.50" [tool.poetry.dev-dependencies] coverage = "<4"