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
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")
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
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)
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
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}")
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
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
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()