Skip to content

Instantly share code, notes, and snippets.

@benkehoe
Created October 5, 2022 17:17
Show Gist options
  • Select an option

  • Save benkehoe/d6c02be0c58fc65864502e9efc80af05 to your computer and use it in GitHub Desktop.

Select an option

Save benkehoe/d6c02be0c58fc65864502e9efc80af05 to your computer and use it in GitHub Desktop.

Revisions

  1. Ben Kehoe created this gist Oct 5, 2022.
    314 changes: 314 additions & 0 deletions aws_console_launcher.py
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,314 @@
    # Copyright 2022 Ben Kehoe
    #
    # 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://www.apache.org/licenses/LICENSE-2.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.

    # For launching the web console with a specific IAM Identity Center
    # account and role, including packaging that up as a shareable,
    # non-credential-based token, see:
    # https://github.com/benkehoe/aws-sso-util/blob/master/docs/console.md

    import sys
    import json
    import webbrowser
    import urllib.parse
    import os
    import argparse
    from typing import Optional

    import requests
    import boto3


    def get_logout_url(region: Optional[str] = None):
    redirect = urllib.parse.quote_plus(
    "https://aws.amazon.com/premiumsupport/knowledge-center/sign-out-account/?from_aws_sso_util_logout"
    )
    if not region or region == "us-east-1":
    return (
    f"https://signin.aws.amazon.com/oauth?Action=logout&redirect_uri={redirect}"
    )

    if region == "us-gov-east-1":
    return "https://us-gov-east-1.signin.amazonaws-us-gov.com/oauth?Action=logout"

    if region == "us-gov-west-1":
    return "https://signin.amazonaws-us-gov.com/oauth?Action=logout"

    return f"https://{region}.signin.aws.amazon.com/oauth?Action=logout&redirect_uri={redirect}"


    def get_federation_endpoint(region: Optional[str] = None):
    if not region or region == "us-east-1":
    return "https://signin.aws.amazon.com/federation"

    if region == "us-gov-east-1":
    return "https://us-gov-east-1.signin.amazonaws-us-gov.com/federation"

    if region == "us-gov-west-1":
    return "https://signin.amazonaws-us-gov.com/federation"

    return f"https://{region}.signin.aws.amazon.com/federation"


    def get_destination_base_url(region: Optional[str] = None):
    if region and region.startswith("us-gov-"):
    # TODO: regional?
    return "https://console.amazonaws-us-gov.com"
    if region:
    return f"https://{region}.console.aws.amazon.com/"
    else:
    return "https://console.aws.amazon.com/"


    def get_destination(
    path: Optional[str] = None,
    region: Optional[str] = None,
    override_region_in_destination: bool = False,
    ):
    base = get_destination_base_url(region=region)

    if path:
    stripped_path_parts = urllib.parse.urlsplit(path)[2:]
    path = urllib.parse.urlunsplit(("", "") + stripped_path_parts)
    url = urllib.parse.urljoin(base, path)
    else:
    # url = urllib.parse.urljoin(base, "/console/home")
    url = base

    if not region:
    return url

    parts = list(urllib.parse.urlsplit(url))
    query_params = urllib.parse.parse_qsl(parts[3])
    if override_region_in_destination:
    query_params = [(k, v) for k, v in query_params if k != "region"]
    query_params.append(("region", region))
    elif not any(k == "region" for k, _ in query_params):
    query_params.append(("region", region))
    query_str = urllib.parse.urlencode(query_params)
    parts[3] = query_str

    url = urllib.parse.urlunsplit(parts)

    return url


    def DurationType(value):
    value = int(value)
    if 15 < value < 720:
    raise ValueError("Duration must be between 15 and 720 minutes (inclusive)")
    return value


    def main():
    parser = argparse.ArgumentParser(description="Launch the AWS console")

    parser.add_argument(
    "--profile", metavar="PROFILE_NAME", help="A config profile to use"
    )
    parser.add_argument("--region", metavar="REGION", help="The AWS region")
    parser.add_argument(
    "--destination",
    dest="destination_path",
    metavar="PATH",
    help="Console URL path to go to",
    )

    override_region_group = parser.add_mutually_exclusive_group()
    override_region_group.add_argument(
    "--override-region-in-destination", action="store_true"
    )
    override_region_group.add_argument(
    "--keep-region-in-destination",
    dest="override_region_in_destination",
    action="store_false",
    )

    open_group = parser.add_mutually_exclusive_group()
    open_group.add_argument(
    "--open",
    dest="open_url",
    action="store_true",
    default=None,
    help="Open the login URL in a browser (the default)",
    )
    open_group.add_argument(
    "--no-open",
    dest="open_url",
    action="store_false",
    help="Do not open the login URL",
    )

    print_group = parser.add_mutually_exclusive_group()
    print_group.add_argument(
    "--print",
    dest="print_url",
    action="store_true",
    default=None,
    help="Print the login URL",
    )
    print_group.add_argument(
    "--no-print",
    dest="print_url",
    action="store_false",
    help="Do not print the login URL",
    )

    parser.add_argument(
    "--duration",
    metavar="MINUTES",
    type=DurationType,
    help="The session duration in minutes",
    )

    logout_first_group = parser.add_mutually_exclusive_group()
    logout_first_group.add_argument(
    "--logout-first",
    "-l",
    action="store_true",
    default=None,
    help="Open a logout page first",
    )
    logout_first_group.add_argument(
    "--no-logout-first",
    dest="logout_first",
    action="store_false",
    help="Do not open a logout page first",
    )

    args = parser.parse_args()

    if args.open_url is None:
    args.open_url = True

    logout_first_from_env = False
    if args.logout_first is None:
    args.logout_first = os.environ.get("AWS_CONSOLE_LOGOUT_FIRST", "").lower() in [
    "true",
    "1",
    ]
    logout_first_from_env = True

    if args.logout_first and not args.open_url:
    if logout_first_from_env:
    logout_first_value = os.environ["AWS_CONSOLE_LOGOUT_FIRST"]
    raise parser.exit(
    f"AWS_CONSOLE_LOGOUT_FIRST={logout_first_value} requires --open"
    )
    else:
    raise parser.exit("--logout-first requires --open")

    session = boto3.Session(profile_name=args.profile)

    if not args.region:
    args.region = session.region_name or os.environ.get(
    "AWS_CONSOLE_DEFAULT_REGION"
    )
    if not args.destination_path:
    args.destination_path = session._session.get_scoped_config().get(
    "web_console_destination"
    ) or os.environ.get("AWS_CONSOLE_DEFAULT_DESTINATION")

    credentials = session.get_credentials()
    if not credentials:
    parser.exit("Could not find credentials")

    federation_endpoint = get_federation_endpoint(region=args.region)
    issuer = os.environ.get("AWS_CONSOLE_DEFAULT_ISSUER")
    destination = get_destination(
    path=args.destination_path,
    region=args.region,
    override_region_in_destination=args.override_region_in_destination,
    )

    launch_console(
    session=session,
    federation_endpoint=federation_endpoint,
    destination=destination,
    region=args.region,
    open_url=args.open_url,
    print_url=args.print_url,
    duration=args.duration,
    logout_first=args.logout_first,
    issuer=issuer,
    )


    def launch_console(
    *,
    session: boto3.Session,
    federation_endpoint: str,
    destination: str,
    region: Optional[str] = None,
    open_url: Optional[bool] = None,
    print_url: Optional[bool] = None,
    duration: Optional[int] = None,
    logout_first: Optional[bool] = None,
    issuer: Optional[str] = None,
    ):
    if not issuer:
    issuer = "aws_console_launcher.py"

    read_only_credentials = session.get_credentials().get_frozen_credentials()

    session_data = {
    "sessionId": read_only_credentials.access_key,
    "sessionKey": read_only_credentials.secret_key,
    "sessionToken": read_only_credentials.token,
    }

    get_signin_token_payload = {
    "Action": "getSigninToken",
    "Session": json.dumps(session_data),
    }
    if duration is not None:
    get_signin_token_payload["SessionDuration"] = duration * 60

    response = requests.post(federation_endpoint, data=get_signin_token_payload)

    if response.status_code != 200:
    print("Could not get signin token", file=sys.stderr)
    print(response.status_code + "\n" + response.text, file=sys.stderr)
    sys.exit(2)

    token = response.json()["SigninToken"]

    get_login_url_params = {
    "Action": "login",
    "Issuer": issuer,
    "Destination": destination,
    "SigninToken": token,
    }

    request = requests.Request(
    method="GET", url=federation_endpoint, params=get_login_url_params
    )

    prepared_request = request.prepare()

    login_url = prepared_request.url

    if print_url:
    print(login_url)

    if open_url:
    if logout_first:
    logout_url = get_logout_url(region=region)
    webbrowser.open(
    logout_url, autoraise=False
    ) # &redirect_uri=https://aws.amazon.com

    webbrowser.open(login_url)


    if __name__ == "__main__":
    main()