Skip to content

Commit

Permalink
cli: Better debugging of token issues in aws-sso-util check (#45)
Browse files Browse the repository at this point in the history
  • Loading branch information
benkehoe authored Dec 22, 2021
1 parent 97585c3 commit 9cfbb05
Show file tree
Hide file tree
Showing 2 changed files with 94 additions and 24 deletions.
91 changes: 77 additions & 14 deletions cli/src/aws_sso_util/check.py
Original file line number Diff line number Diff line change
@@ -1,18 +1,23 @@
import logging
import sys
import re
import pathlib
import getpass
import traceback

import click

from aws_sso_lib.sso import list_available_accounts, list_available_roles, login
from aws_sso_lib.sso import get_boto3_session, list_available_accounts, list_available_roles, login, get_token_fetcher, SSO_TOKEN_DIR
from aws_sso_lib.config import find_instances, SSOInstance
from aws_sso_lib.exceptions import AuthenticationNeededError

from .utils import configure_logging, GetInstanceError

from .login import LOGIN_DEFAULT_START_URL_VARS, LOGIN_DEFAULT_SSO_REGION_VARS
from .configure_profile import CONFIGURE_DEFAULT_START_URL_VARS, CONFIGURE_DEFAULT_SSO_REGION_VARS

import botocore

LOGGER = logging.getLogger(__name__)

def get_specifier_parts(specifier):
Expand All @@ -35,13 +40,23 @@ def get_specifier_parts(specifier):
def join_parts(parts):
return re.sub(r" (?=[,\(\)])", "", " ".join(parts))

def extract_error(e, e_type):
if isinstance(e, e_type):
return e
cause = e.__cause__ or e.__context__
if isinstance(cause, e_type):
return cause
return None

@click.command()
@click.option("--sso-start-url", "-u", metavar="URL", help="Your AWS SSO start URL")
@click.option("--sso-region", metavar="REGION", help="The AWS region your AWS SSO instance is deployed in")
@click.option("--account-id", "-a", "account")
@click.option("--account-id", "-a", "account", metavar="ACCOUNT_ID", help="Check for access to a particular account")
@click.option("--account", hidden=True)
@click.option("--role-name", "-r")
@click.option("--role-name", "-r", metavar="ROLE_NAME", help="Check for access to a particular role")
@click.option("--command", type=click.Choice(["default", "configure", "login"]), default="default")
@click.option("--instance-details", is_flag=True, default=None, help="Display details of the AWS SSO instance")
@click.option("--skip-token-check", is_flag=True, help="When not checking an account and/or role, do not check token validity")
@click.option("--force-refresh", is_flag=True, help="Re-login")
@click.option("--quiet", "-q", is_flag=True)
@click.option("--verbose", "-v", count=True)
Expand All @@ -51,6 +66,8 @@ def check(
account,
role_name,
command,
instance_details,
skip_token_check,
force_refresh,
quiet,
verbose):
Expand All @@ -72,6 +89,15 @@ def check(
start_url_vars = None
region_vars = None

if instance_details is None:
instance_details = not (account or role_name)

if skip_token_check and (account or role_name):
raise click.UsageError("Cannot specify --skip-token-check when checking an account and/or role")

if skip_token_check and force_refresh:
raise click.UsageError("Cannot specify both --force-refresh and --skip-token-check")

instances, specifier, all_instances = find_instances(
start_url=sso_start_url,
start_url_source="CLI input",
Expand Down Expand Up @@ -117,7 +143,7 @@ def check(

instance = instances[0]

if not (account or role_name):
if instance_details:
parts = [
f"AWS SSO instance",
f"start URL {instance.start_url} from {instance.start_url_source}",
Expand All @@ -142,18 +168,55 @@ def check(
if len(all_instances) > 1:
parts.append(f", from instances {SSOInstance.to_strs(all_instances, region=True)}")
LOGGER.info(join_parts(parts))
return
else:
LOGGER.info(f"AWS SSO instance: {instance.start_url} ({instance.region})")

LOGGER.info(f"AWS SSO instance: {instance.start_url} ({instance.region})")
if not account and not role_name and skip_token_check:
return

try:
token = login(instance.start_url, instance.region, force_refresh=force_refresh)
except Exception as e:
LOGGER.exception(f"Exception during login")
sys.exit(201)
print(f"Token expiration: {token['expiresAt']}")
if force_refresh:
try:
token = login(instance.start_url, instance.region, force_refresh=True)
except Exception as e:
LOGGER.exception(f"Exception during login")
sys.exit(201)
else:
try:
session = botocore.session.Session()
token_fetcher = get_token_fetcher(session, instance.region, interactive=False)
token = token_fetcher.fetch_token(instance.start_url)
except AuthenticationNeededError as e:
LOGGER.debug(traceback.format_exc())
LOGGER.error("No valid token found")
sys.exit(201)
except Exception as e:
perm_error = extract_error(e, PermissionError)
if perm_error:
coda = ""
if perm_error.filename:
msg = f"located at {perm_error.filename}"
try:
path = pathlib.Path(perm_error.filename)
owner = path.owner()
if owner != getpass.getuser():
coda = f", it is owned by {owner}"
except Exception:
pass
else:
msg = f"located in {SSO_TOKEN_DIR} by default"
LOGGER.debug(traceback.format_exc())
LOGGER.error(f"The SSO cache file ({msg}) may have the wrong permissions{coda}")
else:
LOGGER.exception(f"Exception in loading token")
os_error = extract_error(e, OSError)
if os_error and os_error.filename:
LOGGER.error(f"The SSO cache file is located at {os_error.filename}")
sys.exit(201)
LOGGER.info(f"Token expiration: {token['expiresAt']}")

if not account:
if not account and not role_name:
return
elif not account:
accounts = {}
for account_id, account_name, available_role_name in list_available_roles(instance.start_url, instance.region):
if account_id in accounts:
Expand Down Expand Up @@ -197,4 +260,4 @@ def check(
LOGGER.info(f"Access found for account {account_id} ({account_name}): {', '.join(role_names)}")

if __name__ == "__main__":
roles(prog_name="python -m aws_sso_util.check") #pylint: disable=unexpected-keyword-arg,no-value-for-parameter
check(prog_name="python -m aws_sso_util.check") #pylint: disable=unexpected-keyword-arg,no-value-for-parameter
27 changes: 17 additions & 10 deletions docs/check.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,14 @@ It can help you validate whether or not you have access to a specific account an

For use in shell scripts, the `--quiet`/`-q` flag can be specified, which will suppress all output, allowing for shell script conditionals to check the return code.

## AWS SSO instance configuration

### Overview
## AWS SSO instance configuration check

To use `aws-sso-util` commands (not including `aws-sso-util admin` commands), an AWS SSO instance must be specified.
`aws-sso-util check` always determines if a valid instance is configured.

This consists of a start URL and the region the AWS SSO instance is in (which is separate from whatever region you might be accessing).
### Overview

The AWS SSO instance consists of a start URL and the region the AWS SSO instance is in (which is separate from whatever region you might be accessing).
However, `aws-sso-util` tries to be smart about finding this value.

If you're working with a single AWS SSO instance, and you've already got a profile configured for it, it should just work.
Expand All @@ -34,25 +35,31 @@ You should consider setting the environment variables `AWS_DEFAULT_SSO_START_URL
* If a region was found in step 1, instances must match this region.
4. The resulting filtered list of instances must contain exactly one entry.

### Debugging
### Instance check
If `aws-sso-util check` cannot find a unique AWS SSO instance, it will return an error and a description of what it did find.

* If no AWS SSO instances were found at all, it will print that and exit with return code 101.
* If at least one AWS SSO instance was found, but the specifier filtered all of them, it will print the specifier and the entire set of instances, and exit with return code 102.
* If no unique AWS SSO intance was found, either because no specifier was found or because the specifier matched more than one of them, it will print all matched instances, the specifier, and the entire set of instances, and exit with return code 103.

If `aws-sso-util check` finds a unique instance, and neither `--account-id` nor `--role-name` are given, it will print the details of the instance, the specifier, and the entire set of instances, and exit with return code 0 (success).
To print out these details when also checking access to an account and/or role, use the `--instance-details` flag.

If you provide the flag `-vvv` (which turns the logging level of `aws_sso_lib` to `DEBUG`), the details of the AWS SSO instance collection and filtering process will be printed.

## AWS SSO access
## AWS SSO token check

`aws-sso-util check` attempts to load the user's AWS SSO token.
If `--force-refresh` is provided, it goes through the login process.
Otherwise, it attempts to load the cached token.
Either way, on successful retrieval of the token, the expiration is printed; on failure, it will exit with code 201.
To skip this step when not checking access to an account or role (which requires the token anyway), use the `--skip-token-check` flag.

`aws-sso-util` can check if the user has access to a particular account and/or role.
`aws-sso-util check` attempts to identify common problems with cached tokens, including permissions errors.

If the above AWS SSO instance check passed, the instance is printed.
## AWS SSO access check

If a valid token cannot be found and the user cannot be logged in, it will print an error and exit with return code 201.
Otherwise, the expiration of the token is printed.
`aws-sso-util` can check if the user has access to a particular account and/or role using the `--account-id` and `--role-name` options.

If only `--account-id` is given, `aws-sso-util check` will find if any roles are accessible in that account, and print them out.
If no roles are accessible in that account, it will print an error and exit with return code 202.
Expand Down

0 comments on commit 9cfbb05

Please sign in to comment.