def get_factor_type() -> str:
     factor_type = prompt(f"\nPlease select your OKTA MFA Factor type. Supported Types are "
                          f"{SUPPORTED_OKTA_FACTOR_TYPES}: ",
                          completer=WordCompleter(SUPPORTED_OKTA_FACTOR_TYPES))
     Utils.stc_validate(factor_type in SUPPORTED_OKTA_FACTOR_TYPES,
                        f"You must select a factor type from: {SUPPORTED_OKTA_FACTOR_TYPES}")
     return factor_type
Exemple #2
0
    def report_error(self, command: str, e: Exception) -> None:
        """
        If the user chooses to report this exception, ship it off over to the Figgy API to let us know what went wrong!
        :param command: user's command input
        :param e: exception that was thrown
        """

        printable_exception = self.sanitize(e)
        os = Utils.get_os()

        payload = {
            'command': command,
            'os': os,
            'stacktrace': printable_exception,
            'version': VERSION
        }

        result = requests.post(FIGGY_ERROR_REPORTING_URL, json=payload)

        Utils.stc_validate(result.status_code == 200, "Unable to report this error to Figgy. Please consider "
                                                      f"opening a ticket on the figgy github repo: {FIGGY_GITHUB}")

        print(f"We are so sorry you experienced this error! This error has been anonymously reported to the Figgy "
              f"development team. \n\nIf you don't want to be prompted to report errors, you can disable the error "
              f"reporting by running `{CLI_NAME} --configure`.")
        print(f"\n\n{self.c.fg_bl}--------------------------------------------------------{self.c.rs}\n\n")
Exemple #3
0
    def select_aws_cli_profile() -> str:
        default_value = 'bastion'
        profile = input(
            f'Please input the aws_cli profile name of your first.last_programmatic user in the '
            f'MGMT account (Default: {default_value}): ') or default_value
        Utils.stc_validate(profile != '',
                           "You must input a valid aws_cli profile")

        return profile
Exemple #4
0
    def select_run_env(valid_envs: List[str]) -> RunEnv:
        input_env = prompt(f'Select a RunEnvironment: {valid_envs}: ',
                           completer=WordCompleter(valid_envs))
        Utils.stc_validate(
            input_env in valid_envs,
            f"{input_env} is not a valid Run Environment. Please select from: {valid_envs}"
        )

        return RunEnv(env=input_env)
Exemple #5
0
    def get_mfa(display_hint: bool = False,
                color: Optional[Color] = None) -> str:
        if display_hint and random.randint(0, 10) == 10:
            blue = color.fg_bl if color else ''
            rs = color.rs if color else ''
            print(
                f"{blue}Hint:{rs} Tired of typing in your MFA? Consider saving your MFA secret to your keychain and "
                f"let {CLI_NAME} securely auto-generate tokens for you. \n"
                f"{blue}More info:{rs} http://figgy.dev/docs/getting-started/install.html\n\n"
            )

        mfa = input('Please input the MFA associated with your user: '******'', "You must input a valid mfa")

        return mfa
Exemple #6
0
def main():
    """
        Entrypoint to figgy.

        Performs generic validation, then routes user down appropriate execution path based on command line parameters
    """
    arguments = sys.argv
    user = getpass.getuser()
    Utils.stc_validate(user != ROOT_USER, f"Hey! Stop trying to run {CLI_NAME} as {ROOT_USER}. That's bad!")
    original_command = ' '.join(arguments)
    sys.argv = arguments
    os.makedirs(os.path.dirname(BOTO3_CLIENT_FILE_LOCK_PATH), exist_ok=True)

    try:
        # Parse / Validate Args
        args = FiggyCLI.parse_args()
        if hasattr(args, 'debug') and args.debug:
            root_logger.setLevel(logging.INFO)
            root_logger.addHandler(stdout_handler)

        cli: FiggyCLI = FiggyCLI(args)
        command: Command = cli.get_command()
        if hasattr(args, 'info') and args.info:
            command.print_help_text()
        else:
            command.execute()

    except AssertionError as e:
        Utils.stc_error_exit(e.args[0])
    except KeyboardInterrupt:
        pass
    except SystemExit:
        pass
    except (BaseException, Exception) as e:
        try:
            error_reporter = FiggyErrorReporter(FiggySetup.stc_get_defaults(skip=True, profile=None))
            error_reporter.log_error(original_command, e)
        except Exception as e:
            print(e)
            print(f"\n\nUnable to log or report this exception. Please submit a Github issue to: {FIGGY_GITHUB}")
Exemple #7
0
    def __init__(self, args):
        """
        Initializes global shared properties
        :param args: Arguments passed in from user, collected from ArgParse
        """
        self._profile = None
        self._command_factory = None
        self._setup = None
        self._is_setup_command: bool = FiggyCLI.is_setup_command(args)
        self._utils = Utils(self.get_colors_enabled())
        self._profile = Utils.attr_if_exists(profile, args)
        self._defaults: CLIDefaults = FiggySetup.stc_get_defaults(skip=self._is_setup_command, profile=self._profile)
        self._run_env = self._defaults.run_env
        role_override = Utils.attr_if_exists(role, args)
        self._role: Role = self.get_role(args.prompt, role_override=role_override, is_setup=self._is_setup_command)

        FiggyCLI.validate_environment(self._defaults)

        if not self._is_setup_command:
            if not hasattr(args, 'env') or args.env is None:
                print(f"{EMPTY_ENV_HELP_TEXT}{self._run_env.env}\n")
            else:
                Utils.stc_validate(args.env in self._defaults.valid_envs,
                                   f'{ENV_HELP_TEXT} {self._defaults.valid_envs}. Provided: {args.env}')
                self._run_env = RunEnv(env=args.env)

        self._utils.validate(Utils.attr_exists(configure, args) or Utils.attr_exists(command, args),
                             f"No command found. Proper format is `{CLI_NAME} <resource> <command> --option(s)`")

        self._assumable_role = self.find_assumable_role(self._run_env, self._role, skip=self._is_setup_command,
                                                        profile=self._profile)

        command_name = Utils.attr_if_exists(command, args)
        resource_name = Utils.attr_if_exists(resource, args)

        found_command: CliCommand = Utils.find_command(str(command_name))
        found_resource: CliCommand = Utils.find_resource(str(resource_name))

        self._context: FiggyContext = FiggyContext(self.get_colors_enabled(), found_resource, found_command,
                                                   self._run_env, self._assumable_role, args)
 def get_profile():
     profile = input('Please input your aws profile linked to your credentials in your `bastion` account: ')
     Utils.stc_validate(profile != '', "You must input a valid profile name.")
     return profile
Exemple #9
0
    def get_password(provider: str = 'Please input') -> str:
        okta_password = getpass.getpass(f'{provider} password: '******'',
                           "You must input a valid OKTA password")

        return okta_password
Exemple #10
0
    def get_user(provider: str = 'Please input') -> str:
        okta_username = input(f'{provider} username: '******'',
                           "You must input a valid OKTA username")

        return okta_username
class BastionSessionProvider(SessionProvider):
    _MAX_ATTEMPTS = 5

    def __init__(self, defaults: CLIDefaults, context: FiggyContext):
        super().__init__(defaults, context)
        self.__id = uuid.uuid4()
        self._utils = Utils(defaults.colors_enabled)
        self.__bastion_session = boto3.session.Session(
            profile_name=self._defaults.provider_config.profile_name)
        self._ssm = None
        self._sts = None
        self._iam_client = None
        self._iam = None
        keychain_enabled = defaults.extras.get(DISABLE_KEYRING) is not True
        vault = FiggyVault(keychain_enabled=keychain_enabled,
                           secrets_mgr=self._secrets_mgr)
        self._sts_cache: CacheManager = CacheManager(
            file_override=STS_SESSION_CACHE_PATH, vault=vault)
        self._role_name_prefix = os.getenv(FIGGY_ROLE_PREFIX_OVERRIDE_ENV,
                                           FIGGY_ROLE_NAME_PREFIX)

    def __get_iam_user(self):
        self._defaults.user = self.__get_iam_resource().CurrentUser().user_name
        return self._defaults.user

    def __get_iam_resource(self):
        if not self._iam:
            self._iam = self.__bastion_session.resource('iam')

        return self._iam

    def __get_iam_client(self):
        if not self._iam_client:
            self._iam_client = self.__bastion_session.client('iam')

        return self._iam_client

    def __get_ssm(self):
        if not self._ssm:
            self._ssm = SsmDao(self.__bastion_session.client('ssm'))
        return self._ssm

    def __get_sts(self):
        if not self._sts:
            self._sts = self.__bastion_session.client('sts')
        return self._sts

    def get_mfa_serial(self) -> Optional[str]:
        response = self.__get_iam_client().list_mfa_devices(
            UserName=self._defaults.user)
        devices = response.get('MFADevices', [])
        log.info(f'Found MFA devices: {devices}.')
        return devices[0].get('SerialNumber') if devices else None

    def get_session(self,
                    env: GlobalEnvironment,
                    prompt: bool,
                    exit_on_fail=True,
                    mfa: Optional[str] = None) -> boto3.Session:
        forced = False
        log.info(
            f"Getting session for role: {env.role.role_arn} in env: {env.role.run_env.env}"
        )
        attempts = 0
        while True:
            try:
                if prompt and not forced:
                    forced = True
                    raise InvalidSessionError(
                        "Forcing new session due to prompt.")

                creds: FiggyAWSSession = self._sts_cache.get_val(
                    env.role.cache_key())

                if creds:
                    session = boto3.Session(
                        aws_access_key_id=creds.access_key,
                        aws_secret_access_key=creds.secret_key,
                        aws_session_token=creds.token,
                        region_name=env.region)

                    if creds.expires_soon(
                    ) or not self._is_valid_session(session):
                        self._utils.validate(
                            attempts < self._MAX_ATTEMPTS,
                            f"Failed to authenticate with AWS after {attempts} attempts. Exiting. "
                        )

                        attempts = attempts + 1
                        log.info(
                            "Invalid session detected in cache. Raising session error."
                        )
                        raise InvalidSessionError("Invalid Session Detected")

                    log.info("Valid bastion SSO session returned from cache.")
                    return session
                else:
                    raise InvalidSessionError(
                        "Forcing new session, cache is empty.")
            except (FileNotFoundError, NoCredentialsError,
                    InvalidSessionError) as e:
                try:
                    if self._defaults.mfa_enabled:
                        self._defaults.mfa_serial = self.get_mfa_serial()
                        color = Utils.default_colors(
                        ) if self._defaults.colors_enabled else None

                        if not mfa:
                            if self._context.command == commands.ui and not self._defaults.auto_mfa:
                                raise CannotRetrieveMFAException(
                                    "Cannot retrieve MFA, UI mode is activated."
                                )
                            else:
                                mfa = self._secrets_mgr.get_next_mfa(self._defaults.user) if self._defaults.auto_mfa else \
                                                                    Input.get_mfa(display_hint=True, color=color)

                        response = self.__get_sts().assume_role(
                            RoleArn=env.role.role_arn,
                            RoleSessionName=Utils.sanitize_session_name(
                                self._defaults.user),
                            DurationSeconds=self._defaults.session_duration,
                            SerialNumber=self._defaults.mfa_serial,
                            TokenCode=mfa)
                    else:
                        response = self.__get_sts().assume_role(
                            RoleArn=env.role.role_arn,
                            RoleSessionName=Utils.sanitize_session_name(
                                self._defaults.user),
                            DurationSeconds=self._defaults.session_duration)

                    session = FiggyAWSSession(
                        **response.get('Credentials', {}))
                    log.info(f"Got session response: {response}")
                    self._sts_cache.write(env.role.cache_key(), session)
                except (ClientError, ParamValidationError) as e:
                    if isinstance(
                            e, ParamValidationError
                    ) or "AccessDenied" == e.response['Error']['Code']:
                        if exit_on_fail:
                            self._utils.error_exit(
                                f"Error authenticating with AWS from Bastion Profile:"
                                f" {self._defaults.provider_config.profile_name}: {e}"
                            )
                    else:
                        if exit_on_fail:
                            log.error(
                                f"Failed to authenticate due to error: {e}")
                            self._utils.error_exit(
                                f"Error getting session for role: {env.role.role_arn} "
                                f"-- Are you sure you have permissions?")

                    raise e

    def get_assumable_roles(self):
        if self.is_role_session():
            user_roles = [self._defaults.role.role]
        else:
            ROLE_PATH = f'/figgy/users/{self.__get_iam_user()}/roles'
            user_roles = self.__get_ssm().get_parameter(ROLE_PATH)
            self._utils.stc_validate(
                user_roles is not None and user_roles != "[]",
                "Something is wrong with your user's configuration with Figgy. "
                "Unable to find any eligible roles for your user. Please contact your"
                " administrator.")

            user_roles = json.loads(user_roles)

        environments = self.__get_ssm().get_all_parameters(
            [PS_FIGGY_ACCOUNTS_PREFIX], option='OneLevel')
        names: List[str] = [env.get('Name') for env in environments]
        parameters = self.__get_ssm().get_parameter_values(names)
        assumable_roles: List[AssumableRole] = []

        for param in parameters:
            env_name = param.get('Name').split('/')[-1]
            account_id = param.get('Value')

            for role in user_roles:
                assumable_roles.append(
                    AssumableRole(
                        run_env=RunEnv(env=env_name, account_id=account_id),
                        role=Role(
                            role=role,
                            full_name=
                            f'{FIGGY_ROLE_NAME_PREFIX}{env_name}-{role}'),
                        account_id=account_id,
                        provider_name=Provider.AWS_BASTION.value,
                        profile=None))

        return assumable_roles

    def is_role_session(self):
        """
        For sandbox demos, where users aren't coming from user accounts, we want to skip looking up user -> role.
        :return: bool - Is this session originating from a role?
        """
        creds = self.__bastion_session.get_credentials(
        ).get_frozen_credentials()

        return hasattr(creds, 'token') and creds.token is not None

    def cleanup_session_cache(self):
        self._sts_cache.wipe_cache()