def stc_get_defaults(skip: bool = False, profile: str = None) -> Optional[CLIDefaults]: """Lookup a user's defaults as configured by --configure option. :param skip - Boolean, if this is true, exit and return none. :param profile - AWS CLI profile to use as an override. If this is passed in all other options are ignored. :return: hydrated CLIDefaults object of default values stored in cache file or None if no cache found """ if profile: return CLIDefaults.from_profile(profile) cache_mgr = CacheManager(file_override=DEFAULTS_FILE_CACHE_PATH) try: last_write, defaults = cache_mgr.get(DEFAULTS_KEY) if not defaults: if skip: return CLIDefaults.unconfigured() else: Utils.stc_error_exit(f'{CLI_NAME} has not been configured.\n\nIf your organization has already ' f'installed Figgy Cloud, please run ' f'`{CLI_NAME} --{configure.name}`.\n\n' f'You may also provide the `--profile` flag, or log-in to our free sandbox with ' f'`figgy login sandbox` to experiment with {CLI_NAME}.') return defaults except JSONDecodeError: return None
def configure(self) -> CLIDefaults: """ Orchestrates the --configure option. Writes selections to a defaults file in user's home dir. This default files stores the following information: user: User Name for SSO integrations role: The user's preferred default role if --role is not specified env: The user's preferred eefault environment if --env is not specified valid_roles: A list of roles the user has access to based on the returned SAML assertion from the SSO provider valid_envs: A list of environments the user has access to based on the returned SAML assertion assumable_roles: Maintains a mapping of accountId -> environment name -> role name so the we can authenticate the user with the appropriate AWS accounts based on their returned SAML assertion. """ Utils.wipe_vaults() or Utils.wipe_defaults( ) or Utils.wipe_config_cache() defaults: CLIDefaults = self._setup.get_defaults() defaults = self._setup.configure_auth(defaults) defaults = self._setup.configure_extras(defaults) self._setup.save_defaults(defaults) self.c = TerminalFactory(Utils.is_mac()).instance().get_colors() defaults = self._setup.configure_roles(current_defaults=defaults) defaults = self._setup.configure_preferences(defaults) defaults = self._setup.configure_figgy_defaults(defaults) self._setup.save_defaults(defaults) print(f"\n{self.c.fg_gr}Setup successful! Enjoy figgy!{self.c.rs}") return defaults
def _put(self): value = Input.input(f"Please input a value to share: ") # Safe convert to int or float, then validate expires_in_hours = Input.input( f"Select # of hours before value auto-expires: ", default="1") expires_in_hours = Utils.safe_cast(expires_in_hours, int, expires_in_hours) expires_in_hours = Utils.safe_cast(expires_in_hours, float, expires_in_hours) self._utils.validate( isinstance(expires_in_hours, int) or isinstance(expires_in_hours, float), "You must provide a number of hours for when this secret should expire. No strings accepted." ) self._utils.validate( expires_in_hours <= 48, "You may not specify an expiration time more than 48 hours in the future." ) secret_id = self._ots.put_ots(value, expires_in_hours) self._out.print( f"\n\nTo share this secret, recipients will need the following") self._out.print(f"\n[[Secret Id]] -> {secret_id}") self._out.success( f"\n\nValue successfully stored, it will expire in {expires_in_hours} hours, or when retrieved." )
def __init__(self, command: CliCommand, context: HelpContext, figgy_context: FiggyContext): self._command = command self._context = context self._figgy_context = figgy_context self._options = context.options self._utils = Utils(False) self._setup: FiggySetup = FiggySetup(self._figgy_context)
def base_url(self) -> str: result = re.match(r'^.*https://(.*\.com)/.*', self.app_link) if result: return result.group(1) else: Utils.stc_error_exit(f"Unable to parse base url from OKTA application link: {self.app_link}, are you " f"sure this link is valid?")
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 get_session(self, env: GlobalEnvironment, prompt: bool, exit_on_fail=True, mfa: Optional[str] = None) -> boto3.Session: try: return boto3.Session(profile_name=env.role.profile) except botocore.exceptions.ProfileNotFound: Utils.stc_error_exit(f"The provided profile override of {env.role.profile} is invalid. Are you sure " f"this profile is set in your ~/.aws/credentials and ~/.aws/config files?")
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 __init__(self, source_ssm: SsmDao, config_completer_init: WordCompleter, colors_enabled: bool, config_context: ConfigContext, session_mgr: SessionManager): super().__init__(promote, colors_enabled, config_context) self.config_context = config_context self._source_ssm = source_ssm self._session_mgr = session_mgr self._config_completer = config_completer_init self._utils = Utils(colors_enabled) self._out = Output(colors_enabled)
def basic_configure(self, configure_provider=True) -> CLIDefaults: defaults: CLIDefaults = self.get_defaults() if not defaults: Utils.stc_error_exit(f"Please run {CLI_NAME} --{configure.name} to set up Figgy, " f"you've got problems friend!") else: defaults = self.configure_auth(defaults, configure_provider=configure_provider) return defaults
def __init__(self, ssm_init: SsmDao, colors_enabled: bool, config_context: ConfigContext, config_view: RBACLimitedConfigView, config_completer: WordCompleter): super().__init__(edit, colors_enabled, config_context) self._ssm = ssm_init self._config_view = config_view self._utils = Utils(colors_enabled) self._config_completer = config_completer
def __init__(self, command: CliCommand, context: MaintenanceContext, figgy_context: FiggyContext, config: Optional[ConfigService]): self._command = command self._context = context self._figgy_context = figgy_context self._options = context.options self._utils = Utils(False) self._setup: FiggySetup = FiggySetup(self._figgy_context) self._config: Optional[ConfigService] = config
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 current_cloud_version(self): if self._config: return self._config.get_fig_simple(PS_CLOUD_VERSION_PATH).value else: Utils.stc_error_exit("Before upgrading you must configure Figgy to ensure your version of " f"the Figgy CLI will be compatible your installed version of Figgy Cloud. " f"Please run `{CLI_NAME} --configure` before trying again. If you do not have an installed " f"version of Figgy Cloud, you may log into the Figgy Sandbox and try again. " f"`figgy login sandbox`. Good luck!")
def __init__(self, command: CliCommand, context: CommandContext, session_manager: SessionManager, figgy_context: FiggyContext): self._command = command self._context = context self._utils = Utils(False) self._setup: FiggySetup = FiggySetup(figgy_context) self._session_manager = session_manager
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 __init__(self, ssm_init, repl_init: ReplicationDao, config_completer_init, colors_enabled: bool, config_context: ConfigContext): super().__init__(share, colors_enabled, config_context) self._ssm = ssm_init self._repl = repl_init self._config_completer = config_completer_init self._utils = Utils(colors_enabled) self._out = Output(colors_enabled)
def __init__(self, child, extra_args=""): self.c = Utils.default_colors() self.extra_args = extra_args if child: c = Utils.default_colors() print(f"{c.fg_yl}Testing command: {child.args}{c.rs}") self._child = child self._child.logfile = sys.stdout self._child.delaybeforesend = .5
def __init__(self, maintenance_context: MaintenanceContext, config_service: Optional[ConfigService]): super().__init__(version, maintenance_context.defaults.colors_enabled, maintenance_context) self.tracker = VersionTracker(self.context.defaults, config_service) self.upgrade_mgr = UpgradeManager( maintenance_context.defaults.colors_enabled) self._utils = Utils( colors_enabled=maintenance_context.defaults.colors_enabled) self._out = Output( colors_enabled=maintenance_context.defaults.colors_enabled)
def is_setup_command(args): """ Returns True for 'special' commands that configure figgy itself or follow non-normal execution paths. Needed to skip past steps that are not necessary because figgy isn't set up yet, or to support a special use case (like sandbox logins). """ return Utils.is_set_true(configure, args) \ or Utils.command_set(sandbox, args) \ or Utils.is_set_true(version, args) \ or Utils.attr_exists(profile, args) \ or Utils.is_set_true(upgrade, args)
def __init__(self, defaults: CLIDefaults, context: FiggyContext): super().__init__(defaults, context) self._utils = Utils(defaults.colors_enabled) self._sts = boto3.client('sts') self._context = context 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._saml_cache: CacheManager = CacheManager( file_override=SAML_SESSION_CACHE_PATH, vault=vault)
def __init__(self, ssm_init: SsmDao, colors_enabled: bool, context: ConfigContext): super().__init__(validate, colors_enabled, context) self._ssm = ssm_init self._config_path = context.ci_config_path if context.ci_config_path else Utils.find_figgy_json( ) self._utils = Utils(colors_enabled) self._replication_only = context.replication_only self._errors_detected = False self.example = f"{self.c.fg_bl}{CLI_NAME} config {self.command_printable} " \ f"--env dev --config /path/to/config{self.c.rs}" self._FILE_PREFIX = "file://" self._out = Output(colors_enabled)
def __init__(self, help_context: HelpContext, figgy_setup: FiggySetup, figgy_context: FiggyContext): super().__init__(login, Utils.not_windows(), help_context) self._setup = figgy_setup self._defaults: CLIDefaults = figgy_setup.get_defaults() self._figgy_context = figgy_context self._utils = Utils(self._defaults.colors_enabled) self._aws_cfg = AWSConfig(color=self.c) self._out = Output(self._defaults.colors_enabled) self.example = f"\n\n{self.c.fg_bl}{CLI_NAME} {login.name} \n" \ f"{self.c.rs}{self.c.fg_yl} --or--{self.c.rs}\n" \ f"{self.c.fg_bl}{CLI_NAME} {login.name} {sandbox.name}{self.c.rs}"
def __init__(self, ssm: SsmDao, ddb: ConfigDao, repl_dao: ReplicationDao, context: ConfigContext, config_completer_init: WordCompleter, colors_enabled: bool, delete: Delete, args=None): super().__init__(prune, colors_enabled, context) self._ssm = ssm # type: SsmDao self._config_dao = ddb # type: ConfigDao self._repl = repl_dao self._config_completer = config_completer_init # type: WordCompleter self._utils = Utils(colors_enabled) self.example = f"{self.c.fg_bl}{CLI_NAME} config {self.command_printable} --env dev " \ f"--config /path/to/figgy.json{self.c.rs}" self._config_path = context.ci_config_path if context.ci_config_path else Utils.find_figgy_json( ) self._out = Output(colors_enabled) # If user passes in --info flag, we don't need all of this to be initialized. if not hasattr(args, info.name) or args.info is False: # Validate & parse figgy.json self._config = self._utils.get_ci_config( self._config_path) # type: Dict self._shared_names = set( self._utils.get_config_key_safe(SHARED_KEY, self._config, default=[])) # type: Set self._repl_conf = self._utils.get_config_key_safe( REPLICATION_KEY, self._config, default={}) # type: Dict self._merge_conf = self._utils.get_config_key_safe( MERGE_KEY, self._config, default={}) # type: Dict self._config_keys = set( self._utils.get_config_key_safe(CONFIG_KEY, self._config, default=[])) # type: Set self._merge_keys = set(self._merge_conf.keys()) # type: Set self._namespace = self._utils.get_namespace( self._config) # type: str self._delete_command = delete self._repl_from_conf = self._utils.get_config_key_safe( REPL_FROM_KEY, self._config, default={}) self._repl_conf = KeyUtils.merge_repl_and_repl_from_blocks( self._repl_conf, self._repl_from_conf, self._namespace) # Build list of all keys found across all config types self._all_keys = KeyUtils().find_all_expected_names( self._config_keys, self._shared_names, self._merge_conf, self._repl_conf, self._repl_from_conf, self._namespace)
def __init__(self, ssm_init: SsmDao, colors_enabled: bool, config_context: ConfigContext, config_view: RBACLimitedConfigView, get: Get): super().__init__(put, colors_enabled, config_context) self._ssm = ssm_init self._utils = Utils(colors_enabled) self._config_view = config_view self._get = get self._source_key = Utils.attr_if_exists(copy_from, config_context.args) self._out = Output(colors_enabled) self._select_name = [('class:', 'Please input a PS Name: ')] self._FILE_PREFIX = "file://"
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 __init__(self, ssm_init: SsmDao, cfg_svc: ConfigService, colors_enabled: bool, context: ConfigContext, get_command: Get, delete_command: Delete, config_view: RBACLimitedConfigView): super().__init__(browse, colors_enabled, context) self._ssm = ssm_init self._get = get_command self._cfg_svc = cfg_svc self._config_view = config_view self.selected_ps_paths = [] self.deleted_ps_paths = [] self.dirs = set() self._utils = Utils(colors_enabled) self._delete = delete_command self.prefix = context.prefix
def get_or_refresh(self, cache_key: str, refresher: Callable, *args, max_age: int = DEFAULT_REFRESH_INTERVAL) \ -> Tuple[int, Any]: assert isinstance( max_age, int ), "Invalid max_age provided for session, it must be of type <int>" last_write, val = self.get(cache_key) if Utils.millis_since_epoch() - last_write > max_age or not val: new_val = refresher(*args) self.write(cache_key, new_val) log.info(f"{cache_key} not found in cache. It was fetched.") return Utils.millis_since_epoch(), new_val else: log.info(f"Value for key: {cache_key} was found in cache.") return last_write, val
def __init__(self, keychain_enabled=True, secrets_mgr: SecretsManager = SecretsManager()): """ keychain_enabled: Stores the encryption key in the user's keychain. This will be disabled for Sandbox sessions to simplify the user experience. """ self._secrets_mgr = secrets_mgr encryption_key = DEFAULT_ENCRYPTION_KEY if keychain_enabled: encryption_key = secrets_mgr.get_encryption_key() if not encryption_key: Utils.wipe_vaults() encryption_key: str = Fernet.generate_key().decode() secrets_mgr.set_encryption_key(encryption_key) self.fernet = Fernet(encryption_key)
def install_onedir(self, install_path: str, latest_version: str, platform: str): old_path = f'{install_path}.OLD' zip_path = f"{HOME}/.figgy/figgy.zip" install_dir = f'{HOME}/.figgy/installations/{latest_version}/{str(uuid.uuid4())[:4]}' url = f'http://www.figgy.dev/releases/cli/{latest_version}/{platform.lower()}/figgy.zip' os.makedirs(os.path.dirname(install_dir), exist_ok=True) suffix = ".exe" if Utils.is_windows() else "" self._cleanup_file(zip_path) self.download_with_progress(url, zip_path) with ZipFile(zip_path, 'r') as zipObj: zipObj.extractall(install_dir) if self._utils.file_exists(old_path): os.remove(old_path) executable_path = f'{install_dir}/figgy/{CLI_NAME}{suffix}' st = os.stat(executable_path) os.chmod(executable_path, st.st_mode | stat.S_IEXEC) if self._utils.file_exists(install_path): os.rename(install_path, old_path) os.symlink(executable_path, install_path)