Esempio n. 1
0
def populate_profiles(sso_start_url, sso_region, regions, dry_run,
                      config_default, existing_config_action,
                      profile_name_components, profile_name_separator,
                      profile_name_include_region, profile_name_region_style,
                      profile_name_trim_account_name_patterns,
                      profile_name_trim_role_name_patterns,
                      profile_name_process, credential_process, force_refresh,
                      verbose):
    """Configure profiles for all accounts and roles.

    Writes a profile to your AWS config file (~/.aws/config) for every account and role you have access to,
    for the regions you specify.
    """

    configure_logging(LOGGER, verbose)

    missing = []

    try:
        instance = get_instance(
            sso_start_url,
            sso_region,
            sso_start_url_vars=DEFAULT_START_URL_VARS,
            sso_region_vars=DEFAULT_SSO_REGION_VARS,
        )
    except GetInstanceError as e:
        LOGGER.fatal(str(e))
        sys.exit(1)

    if not regions:
        for var_name in DEFAULT_REGION_VARS:
            value = os.environ.get(var_name)
            if value:
                LOGGER.debug(f"Got default region {value} from {var_name}")
                regions = [value]
                break
    if not regions:
        missing.append("--region")

    if missing:
        raise click.UsageError("Missing arguments: {}".format(
            ", ".join(missing)))

    if config_default:
        config_default = dict(v.split("=", 1) for v in config_default)
    else:
        config_default = {}

    if not profile_name_separator:
        profile_name_separator = os.environ.get(
            "AWS_CONFIGURE_SSO_DEFAULT_PROFILE_NAME_SEPARATOR"
        ) or DEFAULT_SEPARATOR

    if profile_name_process:
        profile_name_formatter = get_process_formatter(profile_name_process)
    else:
        region_format, no_region_format = generate_profile_name_format(
            profile_name_components, profile_name_separator,
            profile_name_region_style)
        LOGGER.debug(
            "Profile name format (region):    {}".format(region_format))
        LOGGER.debug(
            "Profile name format (no region): {}".format(no_region_format))
        profile_name_formatter = get_formatter(profile_name_include_region,
                                               region_format, no_region_format)
        if profile_name_trim_account_name_patterns or profile_name_trim_role_name_patterns:
            profile_name_formatter = get_trim_formatter(
                profile_name_trim_account_name_patterns,
                profile_name_trim_role_name_patterns, profile_name_formatter)

    try:
        profile_name_formatter(0,
                               account_name="foo",
                               account_id="bar",
                               role_name="baz",
                               region="us-east-1")
    except Exception as e:
        raise click.UsageError("Invalid profile name format: {}".format(e))

    session = Session()

    token_fetcher = get_token_fetcher(
        session,
        instance.region,
        interactive=True,
    )

    LOGGER.info(f"Logging in to {instance.start_url}")
    token = token_fetcher.fetch_token(instance.start_url,
                                      force_refresh=force_refresh)

    LOGGER.debug("Token: {}".format(token))

    config = botocore.config.Config(
        region_name=instance.region,
        signature_version=botocore.UNSIGNED,
    )
    client = session.create_client("sso", config=config)

    LOGGER.info("Gathering accounts and roles")
    accounts = []
    list_accounts_args = {"accessToken": token["accessToken"]}
    while True:
        response = client.list_accounts(**list_accounts_args)

        accounts.extend(response["accountList"])

        next_token = response.get("nextToken")
        if not next_token:
            break
        else:
            list_accounts_args["nextToken"] = response["nextToken"]

    LOGGER.debug("Account list: {} {}".format(len(accounts), accounts))

    configs = []
    for account in accounts:
        LOGGER.debug("Getting roles for {}".format(account["accountId"]))
        list_role_args = {
            "accessToken": token["accessToken"],
            "accountId": account["accountId"],
        }

        while True:
            response = client.list_account_roles(**list_role_args)

            for role in response["roleList"]:
                for i, region in enumerate(regions):
                    profile_name = profile_name_formatter(
                        i,
                        account_name=account["accountName"],
                        account_id=account["accountId"],
                        role_name=role["roleName"],
                        region=region,
                    )
                    configs.append(
                        ConfigParams(profile_name, account["accountName"],
                                     account["accountId"], role["roleName"],
                                     region))

            next_token = response.get("nextToken")
            if not next_token:
                break
            else:
                list_role_args["nextToken"] = response["nextToken"]

    configs.sort(key=lambda v: v.profile_name)

    LOGGER.debug("Got configs: {}".format(configs))

    if not dry_run:
        LOGGER.info("Writing {} profiles to {}".format(
            len(configs), get_config_filename(session)))

        config_writer = ConfigFileWriter()

        def write_config(profile_name, config_values):
            # discard because we're already loading the existing values
            write_values(session,
                         profile_name,
                         config_values,
                         config_file_writer=config_writer,
                         existing_config_action="discard")
    else:
        LOGGER.info("Dry run for {} profiles".format(len(configs)))

        def write_config(profile_name, config_values):
            lines = ["[profile {}]".format(process_profile_name(profile_name))]
            for key, value in config_values.items():
                lines.append("{} = {}".format(key, value))
            lines.append("")
            print("\n".join(lines))

    for config in configs:
        LOGGER.debug("Processing config: {}".format(config))

        config_values = {}
        existing_profile = False
        existing_config = {}
        if existing_config_action != "discard":
            try:
                existing_config = Session(
                    profile=config.profile_name).get_scoped_config()
                config_values.update(existing_config)
                existing_profile = True
            except ProfileNotFound:
                pass

        config_values.update({
            "sso_start_url": instance.start_url,
            "sso_region": instance.region,
            "sso_account_name": config.account_name,
            "sso_account_id": config.account_id,
            "sso_role_name": config.role_name,
            "region": config.region,
        })

        for k, v in config_default.items():
            if k in existing_config and existing_config_action in ["keep"]:
                continue
            config_values[k] = v

        if credential_process is not None:
            set_credential_process = credential_process
        elif os.environ.get(DISABLE_CREDENTIAL_PROCESS_VAR,
                            "").lower() in ["1", "true"]:
            set_credential_process = False
        else:
            set_credential_process = SET_CREDENTIAL_PROCESS_DEFAULT

        if set_credential_process:
            credential_process_name = os.environ.get(
                CREDENTIAL_PROCESS_NAME_VAR
            ) or "aws-sso-util credential-process"
            config_values[
                "credential_process"] = f"{credential_process_name} --profile {config.profile_name}"
        elif set_credential_process is False:
            config_values.pop("credential_process", None)

        config_values["sso_auto_populated"] = "true"

        LOGGER.debug("Config values for profile {}: {}".format(
            config.profile_name, config_values))

        write_config(config.profile_name, config_values)
Esempio n. 2
0
def login(
        sso_start_url,
        sso_region,
        profile,
        login_all,
        force,
        headless,
        verbose):
    """Log in to an AWS SSO instance.

    Note this only needs to be done once for a given SSO instance (i.e., start URL),
    as all profiles sharing the same start URL will share the same login.

    If only one SSO instance/start URL exists in your AWS config file,
    or you've set the environment variables AWS_DEFAULT_SSO_START_URL and AWS_DEFAULT_SSO_REGION,
    you don't need to provide a start URL or region.

    Otherwise, you can provide a full start URL, or a regex for the start URL (usually a substring will work),
    and if this uniquely identifies a start URL in your config, that will suffice.

    You can also provide a profile name with --profile to use the SSO instance from a specific profile.
    """

    if login_all is None:
        login_all = os.environ.get(LOGIN_ALL_VAR, "").lower() in ["true", "1"]

    configure_logging(LOGGER, verbose)

    instances, specifier, all_instances = find_instances(
        profile_name=profile,
        profile_source="--profile",
        start_url=sso_start_url,
        start_url_source="CLI input",
        region=sso_region,
        region_source="CLI input",
        start_url_vars=LOGIN_DEFAULT_START_URL_VARS,
        region_vars=LOGIN_DEFAULT_SSO_REGION_VARS,
    )

    if not instances:
        if all_instances:
            LOGGER.fatal((
                f"No AWS SSO config matched {specifier.to_str(region=True)} " +
                f"from {SSOInstance.to_strs(all_instances)}"))
        else:
            LOGGER.fatal("No AWS SSO config found")
        sys.exit(1)

    if len(instances) > 1 and not login_all:
        LOGGER.fatal(f"Found {len(instances)} SSO configs, please specify one or use --all: {SSOInstance.to_strs(instances)}")
        sys.exit(1)

    LOGGER.debug(f"Instances: {SSOInstance.to_strs(instances)}")

    session = botocore.session.Session()

    regions = [i.region for i in instances]
    token_fetchers = {}
    for region in regions:
        token_fetchers[region] = get_token_fetcher(session, region, interactive=True, disable_browser=headless)

    if len(instances) > 1:
        print(f"Logging in {len(instances)} AWS SSO instances")
    for instance in instances:
        print(f"Logging in {instance.start_url}")
        token_fetcher = token_fetchers[instance.region]
        try:
            token = token_fetcher.fetch_token(instance.start_url, force_refresh=force)
            LOGGER.debug(f"Token: {token}")
            expiration = token['expiresAt']
            if isinstance(expiration, str):
                expiration = parse(expiration)
            expiration_utc = expiration.astimezone(tzutc())
            expiration_str = expiration_utc.strftime(UTC_TIME_FORMAT)
            try:
                local_expiration = expiration_utc.astimezone(tzlocal())
                expiration_str = local_expiration.strftime(LOCAL_TIME_FORMAT)
                # TODO: locale-friendly string
            except:
                pass
            print(f"Login succeeded, valid until {expiration_str}")
        except PendingAuthorizationExpiredError:
            print(f"Login window expired", file=sys.stderr)
            sys.exit(2)
        except aws_error_utils.catch_aws_error("InvalidGrantException") as e:
            LOGGER.debug("Login failed; the login window may have expired", exc_info=True)
            err_info = aws_error_utils.get_aws_error_info(e)
            msg_str = f" ({err_info.message})" if err_info.message else ""
            print(f"Login failed; the login window may have expired: {err_info.code}{msg_str}", file=sys.stderr)
            sys.exit(3)
        except botocore.exceptions.ClientError as e:
            LOGGER.debug("Login failed", exc_info=True)
            err_info = aws_error_utils.get_aws_error_info(e)
            msg_str = f" ({err_info.message})" if err_info.message else ""
            print(f"Login failed: {err_info.code}{msg_str}", file=sys.stderr)
            sys.exit(4)
        except Exception as e:
            LOGGER.debug("Login failed", exc_info=True)
            print(f"Login failed: {e}", file=sys.stderr)
            sys.exit(4)