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)
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)