Example #1
0
    def configure_roles(self, current_defaults: CLIDefaults, run_env: RunEnv = None, role: Role = None) -> CLIDefaults:
        updated_defaults = current_defaults
        provider_factory: SessionProviderFactory = SessionProviderFactory(current_defaults, self._figgy_context)
        session_provider: SSOSessionProvider = provider_factory.instance()
        session_provider.cleanup_session_cache()

        # Get assertion and parse out account -> role -> run_env mappings.
        assumable_roles: List[AssumableRole] = session_provider.get_assumable_roles()
        print(f"\n{self.c.fg_bl}The following assumable roles were detected for user: {current_defaults.user} "
              f"- if something is missing, contact your system administrator.{self.c.rs}\n\n")

        if assumable_roles:
            self.print_role_table(assumable_roles)

        valid_envs = list(set([x.run_env.env for x in assumable_roles]))
        valid_roles = list(set([x.role.role for x in assumable_roles]))

        if not role:
            role: Role = Input.select_role(valid_roles=valid_roles)
            print("\n")

        if not run_env:
            run_env: RunEnv = Input.select_default_account(valid_envs=valid_envs)
            print("\n")
        else:
            print(f"\nYour default environment has been set to: {run_env}. Commands without the "
                  f"--{env.name} option will run against this account.")

        updated_defaults.run_env = run_env
        updated_defaults.valid_envs = valid_envs
        updated_defaults.valid_roles = valid_roles
        updated_defaults.assumable_roles = assumable_roles
        updated_defaults.role = role

        return updated_defaults
Example #2
0
    def _perform_deletions(self) -> None:
        deleted = []  # type: List[str]
        for path in self.deleted_ps_paths:
            if path in self.dirs:
                all_children = set(
                    list(
                        map(
                            lambda x: x['Name'],
                            self._ssm.get_all_parameters([path],
                                                         option='Recursive'))))
                delete_children = Input.y_n_input(
                    f"You have selected a DIRECTORY to delete: {path}. "
                    f"Do you want to delete ALL children of: {path}?",
                    default_yes=False)

                if delete_children:
                    for child in all_children:
                        if self._delete.delete_param(child):
                            deleted.append(child)
            else:
                if path not in deleted:
                    delete_it = Input.y_n_input(f"Delete {path}? ",
                                                default_yes=True)

                    if delete_it:
                        if self._delete.delete_param(path):
                            deleted.append(path)
Example #3
0
    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."
        )
Example #4
0
    def edit(self) -> None:
        """
        Allows a user to define a PS name and add or edit a parameter at that location. Uses NPYscreen editor.
        """
        key = Input.input('Please input a PS Name: ',
                          completer=self._config_completer)

        try:
            value, desc = self._ssm.get_parameter_with_description(key)
            edit_app = EditApp(key, value, desc)
            edit_app.run()

            value, desc = edit_app.value_box.value, edit_app.description_box.value
            log.info(f"Edited value: {value} - description: {desc}")

            is_secret = Input.is_secret()
            parameter_type, kms_id = SSM_SECURE_STRING if is_secret else SSM_STRING, None
            if is_secret:
                valid_keys = self._config_view.get_authorized_kms_keys()
                if len(valid_keys) > 1:
                    key_name = Input.select_kms_key(valid_keys)
                else:
                    key_name = valid_keys[0]

                kms_id = self._config_view.get_authorized_key_id(
                    key_name, self.run_env)

            if not self._utils.is_valid_input(key, f"Parameter name", True) \
                    or not self._utils.is_valid_input(value, key, True):
                self._utils.error_exit(
                    "Invalid input detected, please resolve the issue and retry."
                )

            self._ssm.set_parameter(key,
                                    value,
                                    desc,
                                    parameter_type,
                                    key_id=kms_id)
            print(f"{self.c.fg_gr}{key} saved successfully.{self.c.rs}")
        except ClientError as e:
            if "AccessDeniedException" == e.response['Error']['Code']:
                denied = "AccessDeniedException" == e.response['Error']['Code']
                if denied and "AWSKMS; Status Code: 400;" in e.response[
                        'Error']['Message']:
                    print(
                        f"\n{self.c.fg_rd}You do not have access to decrypt the value of: {key}{self.c.rs}"
                    )
                elif denied:
                    print(
                        f"\n{self.c.fg_rd}You do not have access to Parameter: {key}{self.c.rs}"
                    )
                else:
                    raise
            else:
                self._utils.error_exit(
                    f"{self.c.fg_rd}Exception caught attempting to add config: {e}{self.c.rs}"
                )
Example #5
0
    def _promote(self):
        repeat = True
        parameters: List[Dict] = []
        while repeat:
            namespace = Input.input("Please input a namespace prefix to promote:"
                               f" (i.e. {self.context.defaults.service_ns}/foo/): ", completer=self._config_completer)
            if not self._utils.is_valid_input(namespace, "namespace", notify=False):
                continue

            try:
                parameters: List[Dict] = self._source_ssm.get_all_parameters([namespace])

                if not parameters and self._source_ssm.get_parameter(namespace):
                    parameters, latest_version = self._source_ssm.get_parameter_details(namespace)
                    parameters = list(parameters)

                if parameters:
                    repeat = False
                else:
                    self._out.warn("\nNo parameters found. Try again.\n")
            except ClientError as e:
                print(f"{self.c.fg_rd}ERROR: >> {e}{self.c.rs}")
                continue

        self._out.notify(f'\nFound [[{len(parameters)}]] parameter{"s" if len(parameters) > 1 else ""} to migrate.\n')

        assumable_roles = self.context.defaults.assumable_roles
        matching_roles = list(set([x for x in assumable_roles if x.role == self.config_context.role]))
        valid_envs = set([x.run_env.env for x in matching_roles])
        valid_envs.remove(self.run_env.env)  # Remove current env, we can't promote from dev -> dev
        next_env = Input.select(f'Please select the destination environment.', valid_options=list(valid_envs))

        matching_role = [role for role in matching_roles if role.run_env == RunEnv(env=next_env)][0]
        env: GlobalEnvironment = GlobalEnvironment(role=matching_role, region=self.config_context.defaults.region)
        dest_ssm = SsmDao(self._session_mgr.get_session(env, prompt=False).client('ssm'))

        for param in parameters:
            if 'KeyId' in param:
                self._out.print(f"Skipping param: [[{param['Name']}]]. It is encrypted and cannot be migrated.")
            else:
                promote_it = Input.y_n_input(f"Would you like to promote: {param['Name']}?",
                                             default_yes=True)

                if promote_it:
                    val = self._source_ssm.get_parameter(param['Name'])
                    description = param.get('Description', "")
                    dest_ssm.set_parameter(param['Name'], val, description, SSM_STRING)
                    self._out.success(f"Successfully promoted [[{param['Name']}]] to [[{next_env}]].\r\n")
Example #6
0
    def _delete_param(self):
        """
        Prompts user for a parameter name to delete, then deletes
        """
        # Add all keys
        key, notify, delete_another = None, False, True

        while delete_another:
            key = Input.input('PS Name to Delete: ', completer=self._config_completer)
            try:
                if self.delete_param(key):
                    if key in self._config_completer.words:
                        self._config_completer.words.remove(key)
                else:
                    continue
            except ClientError as e:
                error_code = e.response['Error']['Code']
                if "AccessDeniedException" == error_code:
                    self._out.error(f"\n\nYou do not have permissions to delete config values at the path: [[{key}]]")
                    self._out.warn(f"Your role of {self.context.role} may delete keys under the following namespaces: "
                                   f"{self._cfg_view.get_authorized_namespaces()}")
                    self._out.print(f"Error message: {e.response['Error']['Message']}")
                elif "ParameterNotFound" == error_code:
                    self._out.error(f"The specified Name: [[{key}]] does not exist in the selected environment. "
                                    f"Please try again.")
                else:
                    self._out.error(f"Exception caught attempting to delete config: {e.response['Message']}")

            print()
            to_continue = input(f"Delete another? (Y/n): ")
            to_continue = to_continue if to_continue != '' else 'y'
            delete_another = to_continue.lower() == "y"
Example #7
0
    def _cleanup_parameters(self, config_keys: Set):
        """
        Prompts user for prune of stray ParameterStore names.
        Args:
            config_keys: set() -> Set of parameters that are found as defined in the figgy.json file for a svc
        """

        self._out.notify(f"Checking for stray config names.\r\n")

        # Find & Prune stray keys
        ps_keys = set(
            list(
                map(lambda x: x['Name'],
                    self._ssm.get_all_parameters([self._namespace]))))
        ps_only_keys = ps_keys.difference(config_keys)
        for key in ps_only_keys:
            selection = Input.y_n_input(
                f"{key} exists in ParameterStore but does not exist "
                f"in your config, do you want to delete it?",
                default_yes=False)

            if selection:
                self._delete_command.delete_param(key)
            else:
                self._out.notify("OK, skipping due to user selection.")
        if not ps_only_keys:
            print(f"{self.c.fg_bl}No stray keys found.{self.c.rs}")
Example #8
0
    def install_mac(self, latest_version: FiggyVersionDetails) -> bool:
        install_path = '/usr/local/bin/figgy'

        if self.upgrade_mgr.is_brew_install():
            self._out.notify_h2(f"Homebrew installation detected!")

            print(
                f"This upgrade process will not remove your brew installation but will instead unlink it. "
                f"Going forward you will no longer need homebrew to manage {CLI_NAME}. Continuing is recommended.\n"
            )

            selection = Input.y_n_input(f"Continue? ", default_yes=True)
        else:
            selection = True

        if selection:
            self.upgrade_mgr.install_onedir(install_path,
                                            latest_version.version, MAC)
            return True
        else:
            self._out.print(
                f'\n[[Auto-upgrade aborted. To upgrade through brew run:]] \n'
                f'-> brew upgrade figtools/figgy/figgy')
            self._out.warn(
                f"\n\nYou may continue to manage [[{CLI_NAME}]] through Homebrew, but doing so will "
                f"limit some upcoming functionality around canary releases, rollbacks, and dynamic "
                f"version-swapping.")
            return False
Example #9
0
    def log_error(self, command: str, e: Exception):
        os.makedirs(ERROR_LOG_DIR, exist_ok=True)

        # Do not sanitize here since this is only written to the user's local log folder.
        printable_exception = ''.join(traceback.format_exception(etype=type(e), value=e, tb=e.__traceback__))
        log_file_name = datetime.datetime.now().strftime("%Y-%m-%d-%H-%M-%S.log")

        with open(f'{ERROR_LOG_DIR}/{log_file_name}', "w+") as log:
            log.write(f'Command: {command}\n\n\n{printable_exception}')

        print(f"\n\n{self.c.fg_bl}---------------------ERROR ENCOUNTERED --------------------{self.c.rs}\n\n")
        print(f"{self.c.fg_rd}Figgy experienced the following irrecoverable error. "
              f"Please consider reporting this error.{self.c.rs}")
        print(printable_exception)

        print(f"\n\n{self.c.fg_bl}-----------------------------------------------------------{self.c.rs}\n\n")

        if self.reporting_enabled:
            ship_it = Input.y_n_input("Would you like to report this to Figgy's developer(s)?", default_yes=True)

            if ship_it:
                self.report_error(command, e)
            else:
                print(f"Error was not reported. Please consider reporting this error on our Github: "
                      f"{FIGGY_GITHUB}. Error details have been logged to this file: "
                      f"{self.c.fg_bl}~/.{CLI_NAME}/errors/{log_file_name}{self.c.rs}. Farewell! \n")
Example #10
0
    def get_role(self, prompt: bool, role_override: str = None, is_setup: bool = False) -> Role:
        """
        Returns a string of the user's selected role.

        Lookup the user's default role from the config file (created via the --configure option), an ENV variable, or
        instead prompt the user for the session.

        :param prompt: True/False - if True, users will always be prompted to input their role
        :param role_override: String representation of the role to get, regardless of defaults.
        :return: str: name of the selected role.
        """

        defaults = FiggySetup.stc_get_defaults(self._is_setup_command, profile=self._profile)
        if defaults is not None and not prompt:

            if role_override:
                if role_override in [role.role.role for role in defaults.assumable_roles] or is_setup:
                    return Role(role=role_override)
                else:
                    self._utils.error_exit(f"Invalid role override provided of: {role_override}. "
                                           f"You do not have permissions to assume this role. Contact your system "
                                           f"administrator to receive permissions then rerun `{CLI_NAME} "
                                           f"--{configure.name}`.")

            return defaults.role
        else:
            roles = self.__setup().get_assumable_roles()
            role_names = list(set([x.role.role for x in roles]))
            return Input.select_role(role_names)
Example #11
0
    def _fill_repl_conf_variables(self, repl_conf: Dict) -> Dict:
        repl_copy = {}
        all_vars = []
        for key, val in repl_conf.items():
            all_vars = all_vars + re.findall(r'\${(\w+)}', key)
            all_vars = all_vars + re.findall(r'\${(\w+)}', key)

        all_vars = set(all_vars)
        if all_vars:
            print(
                f"{self.c.fg_bl}{len(all_vars)} variables detected in: {self.c.rs}{self.c.fg_yl}"
                f"{self._config_path}{self.c.rs}\n")

        template_vals = {}
        for var in all_vars:
            print(f"Template variable: {self.c.fg_bl}{var}{self.c.rs} found.")
            input_val = Input.input(
                f"Please input a value for {self.c.fg_bl}{var}{self.c.rs}: ",
                min_length=1)
            template_vals[var] = input_val

        for key, val in repl_conf.items():
            updated_key = key
            updated_val = val

            for template_key, template_val in template_vals.items():
                updated_key = updated_key.replace(f"${{{template_key}}}",
                                                  template_val)
                updated_val = updated_val.replace(f"${{{template_key}}}",
                                                  template_val)

            repl_copy[updated_key] = updated_val
            repl_copy[updated_key] = updated_val

        return repl_copy
Example #12
0
    def _prompt_delete(self, name):
        param = self._ssm.get_parameter_encrypted(name)
        if param:
            selection = Input.y_n_input(
                f"PS Name: {name} did not exist at this restore time."
                f" Delete it? ",
                default_yes=False)

            if selection:
                self._delete.delete_param(name)
    def get_saml_assertion(self,
                           prompt: bool = False,
                           mfa: Optional[str] = None) -> str:
        """
        Lookup OKTA session from cache, if it's valid, use it, otherwise, generate new assertion with MFA
        Args:
            prompt: Used for forcing prompts of username / password and always generating a new assertion
            mfa: MFA to use for generating the new OKTA session with.
            force_new: Forces a new session, abandons one from cache
        """
        log.info(f'Getting SAML assertion. Provided MFA override: {mfa}')
        invalid_session = True
        okta = self.get_sso_session(prompt, mfa)
        failure_count = 0
        # Todo: is this an infinite loop after a request from UI with a bad MFA?
        while invalid_session:
            try:
                assertion = okta.get_assertion()
            except InvalidSessionError as e:
                if failure_count > 0:
                    print(e)
                    print(
                        "Authentication failed with SSO provider, please reauthenticate"
                        " Likely invalid MFA or Password?\r\n")
                    failure_count += 1

                log.debug(f" invalid session: {e}")
                user = self._get_user(prompt)
                password = self._get_password(user, prompt=prompt, save=True)

                if self._defaults.mfa_enabled:
                    color = Utils.default_colors(
                    ) if self._defaults.colors_enabled else None
                    mfa = self._secrets_mgr.get_next_mfa(user) if self._defaults.auto_mfa else \
                        Input.get_mfa(display_hint=True, color=color)
                else:
                    mfa = None

                primary_auth = OktaPrimaryAuth(self._defaults, password, mfa)

                try:
                    print("Trying to write session to cache...")
                    self._write_okta_session_to_cache(
                        primary_auth.get_session())
                except InvalidSessionError as e:
                    print(f"Got invalid session: {e}")
                    return self.get_saml_assertion(prompt=True)
                else:
                    return self.get_saml_assertion(prompt=True)
            else:
                assertion = base64.b64decode(assertion).decode('utf-8')
                self._saml_cache.write(SAML_ASSERTION_CACHE_KEY, assertion)
                return assertion
Example #14
0
    def get_or_prompt(self,
                      key: Enum,
                      get_it: Callable,
                      colors_enabled=Utils.is_mac(),
                      force_prompt=False,
                      desc: str = None) -> str:
        """
        Retrieves a value from the config_file based on the provided ENUM's value.
        If the value is unset, executes the get_it() method provided to retrieve the value, then returns it.

        If the user selects something _other_ than the default, overwrite the original value in the config file
        :param key: Enum representing the config value to fetch
        :param get_it: Method to execute if `config` is not in the configured config_file
        :param colors_enabled: Whether or not to enable colored output for the prompt if a config is found in config_file
        :param optional description to provide context when user is prompted.
        :return: String value from the config file, or the result of get_it()
        """
        c = TerminalFactory(colors_enabled).instance().get_colors()
        val = self.get_property(key)
        selection = None

        if val or force_prompt:
            if not force_prompt:
                print(f"\n\n{c.fg_bl}Default value found:{c.rs}")
                print(f"Key: {c.fg_gr}{key.value}{c.rs}")
                print(f"Value: {c.fg_gr}{val}{c.rs}")
                if desc:
                    print(f"Description: {desc}")

                selection = Input.y_n_input(f"Continue with `{val}`? ",
                                            default_yes=True)

            if not selection or force_prompt:
                original_val = val
                val = get_it()

                if isinstance(val, bool):
                    val = "true" if val else "false"

                if val != original_val:
                    print(
                        f"\n\nYou selected to overwrite the default. Updating default value:"
                        f"\nKey: {c.fg_gr}{key.value}{c.rs}"
                        f"\nFrom: {c.fg_yl}{original_val}{c.rs}"
                        f"\nTo: {c.fg_gr}{val}{c.rs}"
                        f"\nIn file {c.fg_bl}{self.config_file}{c.rs}")

                    self.set(key, val)

        else:
            val = get_it()

        return val
Example #15
0
    def _get_user(self, prompt: bool) -> str:
        """
        Get the user either from cache, or prompt the user.

        Returns: str -> username
        """

        defaults = self._defaults
        if defaults is not None and not prompt:
            return defaults.user
        else:
            return Input.get_user(provider=self._defaults.provider.name)
Example #16
0
    def _generate(self):
        from_config = self._utils.get_ci_config(self._from_path)
        service_name = self._utils.get_namespace(from_config).split('/')[2]
        current_ns = self._utils.get_namespace(from_config)

        base_name, version = self._get_service_name_and_version(service_name)
        base_name = base_name if not base_name.endswith(
            '-') else base_name[:-1]
        new_service_name = f'{base_name}-{version + 1}'

        new_name = Input.input(
            f'Please select a new service name, it CANNOT be: {service_name}:  ',
            default=new_service_name)
        self._utils.validate(
            new_name != service_name,
            f"You must select a new service name that differs from the one"
            f"designated in your source figgy.json file. "
            f"(NOT {service_name})")
        new_ns = f'{self.context.defaults.service_ns}/{new_name}/'

        # Update all configs destinations to leverage new namespace. Easiest to search/replace across everything.
        output_string = json.dumps(from_config)
        output_string = output_string.replace(current_ns[:-1], new_ns[:-1])
        new_config = json.loads(output_string)

        # Remove existing configs that will be replicated
        new_config[CONFIG_KEY] = []

        # Configure replicate_from block
        new_config[REPL_FROM_KEY] = {
            SOURCE_NS_KEY:
            from_config.get(REPL_FROM_KEY, {}).get(SOURCE_NS_KEY, current_ns),
            PARAMETERS_KEY:
            from_config.get(REPL_FROM_KEY, {}).get(PARAMETERS_KEY, [])
        }

        for param in from_config.get(CONFIG_KEY, []):
            new_config[REPL_FROM_KEY][PARAMETERS_KEY].append(
                self._utils.get_parameter_only(param))

        formatted_config = self._utils.format_config(new_config)
        current_dir = getcwd()
        output_file = prompt(f'Write new config here?: ',
                             default=f'{current_dir}/{new_name}-config.json')
        self._utils.is_valid_input(output_file, "output_file", True)

        with open(output_file, "w") as file:
            file.write(json.dumps(formatted_config, sort_keys=False, indent=4))

        print(
            f'{self.c.fg_gr}New config successfully generated at location: {output_file}{self.c.rs}'
        )
Example #17
0
    def configure_auth(self, current_defaults: CLIDefaults, configure_provider=True) -> CLIDefaults:
        updated_defaults = current_defaults
        if configure_provider or current_defaults.provider is Provider.UNSELECTED:
            provider: Provider = Input.select_provider()
            updated_defaults.provider = provider
        else:
            provider: Provider = current_defaults.provider

        if provider in Provider.sso_providers():
            user: str = Input.get_user(provider=provider.name)
            password: str = Input.get_password(provider=provider.name)
            self._secrets_mgr.set_password(user, password)
            updated_defaults.user = user

        try:
            mfa_enabled = Utils.parse_bool(self._config_mgr.get_or_prompt(Config.Section.Figgy.MFA_ENABLED,
                                                                          Input.select_mfa_enabled, desc=MFA_DESC))
            if mfa_enabled:
                auto_mfa = Utils.parse_bool(self._config_mgr.get_or_prompt(Config.Section.Figgy.AUTO_MFA,
                                                                           Input.select_auto_mfa, desc=AUTO_MFA_DESC))
            else:
                auto_mfa = False

        except ValueError as e:
            Utils.stc_error_exit(f"Invalid value found in figgy defaults file under "
                                 f"{Config.Section.Figgy.MFA_ENABLED.value}. It must be either 'true' or 'false'")
        else:
            updated_defaults.mfa_enabled = mfa_enabled
            updated_defaults.auto_mfa = auto_mfa

        if updated_defaults.auto_mfa:
            mfa_secret = Input.get_mfa_secret()
            self._secrets_mgr.set_mfa_secret(updated_defaults.user, mfa_secret)

        if configure_provider:
            provider_config = ProviderConfigFactory().instance(provider, mfa_enabled=updated_defaults.mfa_enabled)
            updated_defaults.provider_config = provider_config

        return updated_defaults
Example #18
0
    def _audit(self):
        audit_more = True

        while audit_more:
            ps_name = Input.input(f"Please input a PS Name : ",
                                  completer=self._config_completer)
            audit_logs = self._audit_dao.get_audit_logs(ps_name)
            result_count = len(audit_logs)
            if result_count > 0:
                self._out.print(f"\nFound [[{result_count}]] results.")
            else:
                self._out.warn(f"\nNo results found for: [[{ps_name}]]")
            for log in audit_logs:
                self._out.print(log.pretty_print())

            to_continue = input(f"Audit another? (Y/n): ")
            to_continue = to_continue if to_continue != '' else 'y'
            audit_more = to_continue.lower() == "y"
            print()
Example #19
0
    def _get_password(self,
                      user_name,
                      prompt: bool,
                      save: bool = False) -> str:
        """
        Get the password either from keyring, or prompt the user.

        Returns: str -> password
        """

        password = self._secrets_mgr.get_password(user_name)
        reset_password = not password

        if reset_password or prompt:
            password = Input.get_password(
                provider=self._defaults.provider.name)
            if reset_password or save:
                self._secrets_mgr.set_password(user_name, password)

        return password
Example #20
0
    def get_profile(self, prompt: bool) -> str:
        """Returns the user's profile.

        Checks ENV variable, if not there, checks the config file (created via the --configure option), otherwise prompts
        the user

        Args:
            prompt: True/False - if True, users will always be prompted to input their profile

        :return: str: aws profile name
        """

        if BASTION_PROFILE_ENV_NAME in os.environ and not prompt:
            return os.environ.get(BASTION_PROFILE_ENV_NAME)
        else:
            defaults: CLIDefaults = FiggySetup.stc_get_defaults(self._is_setup_command, profile=self._profile)
            if defaults is not None and not prompt:
                return defaults.provider_config.profile
            else:
                return Input.select_aws_cli_profile()
Example #21
0
    def handle_totp(self, sess):
        response_page = BeautifulSoup(sess.text, 'html.parser')
        tl = response_page.find('input', {'name': 'TL'}).get('value')
        gxf = response_page.find('input', {'name': 'gxf'}).get('value')
        challenge_url = sess.url.split("?")[0]
        challenge_id = challenge_url.split("totp/")[1]

        if self._defaults.mfa_enabled:
            color = Utils.default_colors(
            ) if self._defaults.colors_enabled else None
            mfa_token = self._secrets_mgr.get_next_mfa(self._defaults.user) if self._defaults.auto_mfa else \
                Input.get_mfa(display_hint=True, color=color)
        else:
            mfa_token = None

        if not mfa_token:
            raise ValueError(
                "MFA token required for {} but none supplied.".format(
                    self.config.username))

        payload = {
            'challengeId': challenge_id,
            'challengeType': 6,
            'continue': self.cont,
            'scc': 1,
            'sarp': 1,
            'checkedDomains': 'youtube',
            'pstMsg': 0,
            'TL': tl,
            'gxf': gxf,
            'Pin': mfa_token,
            'TrustDevice': 'on',
        }

        # Submit TOTP
        return self.post(challenge_url, data=payload)
Example #22
0
 def get_app_link() -> str:
     app_link = Input.input("Please input your OKTA AWS Application Embed Link. It's usually something like "
                      "'https://your-company.okta.com/home/amazon_aws/ASDF12351fg1/234': ")
     Utils.stc_is_valid_input(app_link, "OKTA AWS Application URL", True)
     return app_link
    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
Example #24
0
    def upgrade(self):
        latest_version: FiggyVersionDetails = self.tracker.get_version()
        install_success, upgrade_it = False, True

        if self.upgrade_mgr.is_pip_install():
            self._out.error(
                f"Figgy appears to have been installed with pip. Please upgrade [[{CLI_NAME}]] with "
                f"`pip` instead.")
            self._out.print(
                f"\n\n[[Try this command]]: pip install figgy-cli --upgrade")

            self._out.print(
                f"\n\nPip based [[{CLI_NAME}]] installations do not support automatic upgrades and "
                f"instead require pip-managed upgrades; however,  Homebrew, one-line, and manual "
                f"installations support auto-upgrade. Please consider installing figgy through one "
                f"of these other methods to take advantage of this feature. "
                f"It will save you time, help keep you up-to-date, and enable important features like "
                f"release-rollbacks and canary releases! "
                f"[[https://www.figgy.dev/docs/getting-started/install/]]")
            sys.exit(0)

        install_path = self.upgrade_mgr.install_path

        if not install_path:
            self._utils.error_exit(
                f"Unable to detect local figgy installation. Please reinstall figgy and follow one "
                f"of the recommended installation procedures.")

        if latest_version.version == VERSION:
            self._out.success(
                f'You are currently using the latest version of [[{CLI_NAME}]]: [[{VERSION}]]'
            )
            upgrade_it = False
        elif self.tracker.upgrade_available():
            self._out.notify_h2(
                f"New version: [[{latest_version.version}]] is more recent than your version: [[{VERSION}]]"
            )
            upgrade_it = True
        elif not self.tracker.cloud_version_compatible_with_upgrade():
            self._out.notify_h2(
                f"Version [[{self.tracker.get_version().version}]] of the Figgy CLI is available but your "
                f"current version of Figgy Cloud ([[{self.tracker.current_cloud_version()}]]) is not compatible."
                f" Your administrator must first update FiggyCloud to at least version: "
                f"[[{self.tracker.required_cloud_version()}]] before you can upgrade Figgy."
            )
            upgrade_it = False
        else:
            self._out.notify_h2(
                f"Your version: [[{VERSION}]] is more recent then the current recommended version "
                f"of {CLI_NAME}: [[{latest_version.version}]]")
            upgrade_it = Input.y_n_input(
                f'Would you like to revert to the current recommended version '
                f'of {CLI_NAME}?')

        if upgrade_it:
            if self._utils.is_mac():
                self._out.print(
                    f"\nMacOS auto-upgrade is supported. Performing auto-upgrade."
                )
                install_success = self.install_mac(latest_version)
            elif self._utils.is_linux():
                self._out.print(
                    f"\nLinux auto-upgrade is supported. Performing auto-upgrade."
                )
                install_success = self.install_linux(latest_version)
            elif self._utils.is_windows():
                self._out.print(
                    f"\nWindows auto-upgrade is supported. Performing auto-upgrade."
                )
                install_success = self.install_windows(latest_version)

            if install_success:
                self._out.success(
                    f"Installation successful! Exiting. Rerun `[[{CLI_NAME}]]` "
                    f"to use the latest version!")
            else:
                self._out.warn(
                    f"\nUpgrade may not have been successful. Check by re-running "
                    f"[[`{CLI_NAME}` --version]] to see if it was. If it wasn't, please reinstall [[`{CLI_NAME}`]]. "
                    f"See {INSTALL_URL}.")
Example #25
0
    def put_param(self, key=None, loop=False, display_hints=True) -> None:
        """
        Allows a user to define a PS name and add a new parameter at that named location. User will be prompted for a
        value, desc, and whether or not the parameter is a secret. If (Y) is selected for the secret, will encrypt the
        value with the appropriately mapped KMS key with the user's role.

        :param key: If specified, the user will be prompted for the specified key. Otherwise the user will be prompted
                    to specify the PS key to set.
        :param loop: Whether or not to continually loop and continue prompting the user for more keys.
        :param display_hints: Whether or not to display "Hints" to the user. You may want to turn this off if you are
                              looping and constantly calling put_param with a specified key.
        """

        value, desc, notify, put_another = True, None, False, True

        if display_hints:
            self._out.print(
                f"[[Hint:]] To upload a file's contents, pass in `file:///path/to/your/file` "
                f"in the value prompt.")

        while put_another:
            try:

                if not key:
                    lexer = PygmentsLexer(
                        FigLexer
                    ) if self.context.defaults.colors_enabled else None
                    style = style_from_pygments_cls(
                        FiggyPygment
                    ) if self.context.defaults.colors_enabled else None
                    key = Input.input(
                        'Please input a PS Name: ',
                        completer=self._config_view.get_config_completer(),
                        lexer=lexer,
                        style=style)
                    if self.parameter_is_existing_dir(key):
                        self._out.warn(
                            f'You attempted to store parameter named: {key},'
                            f' but it already exists in ParameterStore as a directory: {key}/'
                        )
                        key = None
                        continue

                if self._source_key:
                    plain_key = '/'.join(key.strip('/').split('/')[2:])
                    source_key = f'{self._source_key}/{plain_key}'
                    orig_value, orig_description = self._get.get_val_and_desc(
                        source_key)
                else:
                    orig_description = ''
                    orig_value = ''

                value = Input.input(f"Please input a value for {key}: ",
                                    default=orig_value if orig_value else '')

                if value.lower().startswith(self._FILE_PREFIX):
                    value = Utils.load_file(
                        value.replace(self._FILE_PREFIX, ""))

                existing_desc = self._ssm.get_description(key)
                desc = Input.input(
                    f"Please input an optional description: ",
                    optional=True,
                    default=existing_desc if existing_desc else
                    orig_description if orig_description else '')

                is_secret = Input.is_secret()
                parameter_type, kms_id = SSM_SECURE_STRING if is_secret else SSM_STRING, None
                if is_secret:
                    valid_keys = self._config_view.get_authorized_kms_keys()
                    if len(valid_keys) > 1:
                        key_name = Input.select_kms_key(valid_keys)
                    else:
                        key_name = valid_keys[0]

                    kms_id = self._config_view.get_authorized_key_id(
                        key_name, self.run_env)

                notify = True

                self._ssm.set_parameter(key,
                                        value,
                                        desc,
                                        parameter_type,
                                        key_id=kms_id)
                if key not in self._config_view.get_config_completer().words:
                    self._config_view.get_config_completer().words.append(key)

            except ClientError as e:
                if "AccessDeniedException" == e.response['Error']['Code']:
                    self._out.error(
                        f"\n\nYou do not have permissions to add config values at the path: [[{key}]]"
                    )
                    self._out.warn(
                        f"Your role of {self.context.role} may add keys under the following namespaces: "
                        f"{self._config_view.get_authorized_namespaces()}")
                    self._out.print(
                        f"Error message: {e.response['Error']['Message']}")
                else:
                    self._out.error(
                        f"Exception caught attempting to add config: {e}")

            print()
            if loop:
                to_continue = input(f"\nAdd another? (y/N): ")
                put_another = True if to_continue.lower() == 'y' else False
                key = None
            else:
                put_another = False
    def get_sso_session(self,
                        prompt: bool = False,
                        mfa: Optional[str] = None) -> Okta:
        """
        Pulls the last okta session from cache, if cache doesn't exist, generates a new session and writes it to cache.
        From this session, the OKTA SVC is hydrated and returned.
        Args:
            prompt: If supplied, will never get session from cache.

        Returns: Initialized Okta service.
        """
        count = 0
        while True:
            try:
                if prompt:
                    raise InvalidSessionError(
                        "Forcing new session due to prompt.")

                cached_session = self._get_session_from_cache()
                if not cached_session:
                    raise InvalidSessionError("No session found in cache.")

                okta = Okta(OktaSessionAuth(self._defaults, cached_session))
                return okta
            except (FileNotFoundError, InvalidSessionError, JSONDecodeError,
                    AttributeError) as e:
                try:
                    password = self._secrets_mgr.get_password(
                        self._defaults.user)

                    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:
                            color = Utils.default_colors(
                            ) if self._defaults.colors_enabled else None
                            mfa = self._secrets_mgr.get_next_mfa(self._defaults.user) if self._defaults.auto_mfa else \
                                Input.get_mfa(display_hint=True, color=color)

                    log.info(f"Getting OKTA primary auth with mfa: {mfa}")
                    primary_auth = OktaPrimaryAuth(self._defaults, password,
                                                   mfa)
                    self._write_okta_session_to_cache(
                        primary_auth.get_session())
                    return Okta(primary_auth)
                except InvalidSessionError as e:
                    prompt = True
                    log.error(
                        f"Caught error when authing with OKTA & caching session: {e}. "
                    )
                    time.sleep(1)
                    count += 1
                    if count > 1:
                        if self._context.command == ui:
                            raise InvalidCredentialsException(
                                "Failed OKTA authentication. Invalid user, password, or MFA."
                            )
                        else:
                            Utils.stc_error_exit(
                                "Unable to autheticate with OKTA with your provided credentials. Perhaps your"
                                f"user, password, or MFA changed? Try rerunning `{CLI_NAME} --configure` again."
                            )
Example #27
0
    def login_sandbox(self):
        """
        If user provides --role flag, skip role & env selection for a smoother user experience.
        """
        EnvironmentValidator(self._defaults).validate_environment_variables()

        Utils.wipe_vaults() or Utils.wipe_defaults(
        ) or Utils.wipe_config_cache()

        self._out.print(
            f"{self.c.fg_bl}Logging you into the Figgy Sandbox environment.{self.c.rs}"
        )
        user = Input.input("Please input a user name: ", min_length=2)
        colors = Input.select_enable_colors()

        # Prompt user for role if --role not provided
        if commands.role not in self.context.options:
            role = Input.select("\n\nPlease select a role to impersonate: ",
                                valid_options=SANDBOX_ROLES)
        else:
            role = self.context.role.role
            self._utils.validate(
                role in SANDBOX_ROLES,
                f"Provided role: >>>`{role}`<<< is not a valid sandbox role."
                f" Please choose from {SANDBOX_ROLES}")

        params = {'role': role, 'user': user}
        result = requests.get(GET_SANDBOX_CREDS_URL, params=params)

        if result.status_code != 200:
            self._utils.error_exit(
                "Unable to get temporary credentials from the Figgy sandbox. If this problem "
                f"persists please notify us on our GITHUB: {FIGGY_GITHUB}")

        data = result.json()
        response = SandboxLoginResponse(**data)
        self._aws_cfg.write_credentials(
            access_key=response.AWS_ACCESS_KEY_ID,
            secret_key=response.AWS_SECRET_ACCESS_KEY,
            token=response.AWS_SESSION_TOKEN,
            region=FIGGY_SANDBOX_REGION,
            profile_name=FIGGY_SANDBOX_PROFILE)

        defaults = CLIDefaults.sandbox(user=user, role=role, colors=colors)
        self._setup.save_defaults(defaults)

        run_env = RunEnv(
            env='dev',
            account_id=SANDBOX_DEV_ACCOUNT_ID) if self.context.role else None

        config_mgr = ConfigManager.figgy()
        config_mgr.set(Config.Section.Bastion.PROFILE, FIGGY_SANDBOX_PROFILE)
        defaults = self._setup.configure_extras(defaults)
        defaults = self._setup.configure_roles(current_defaults=defaults,
                                               role=Role(role=role),
                                               run_env=run_env)
        defaults = self._setup.configure_figgy_defaults(defaults)
        self._setup.save_defaults(defaults)

        self._out.success(
            f"\nLogin successful. Your sandbox session will last for [[1 hour]]."
        )

        self._out.print(
            f"\nIf your session expires, you may rerun `{CLI_NAME} login sandbox` to get another sandbox session. "
            f"\nAll previous figgy sessions have been disabled, you'll need to run {CLI_NAME} "
            f"--configure to leave the sandbox.")
Example #28
0
    def _restore_param(self) -> None:
        """
        Allow the user to query a parameter store entry from dynamo, so we can query + restore it, if desired.
        """

        table_entries = []

        ps_name = prompt(f"Please input PS key to restore: ",
                         completer=self._config_completer)

        if self._is_replication_destination(ps_name):
            repl_conf = self._repl.get_config_repl(ps_name)
            self._print_cannot_restore_msg(repl_conf)
            exit(0)

        self._out.notify(
            f"\n\nAttempting to retrieve all restorable values of [[{ps_name}]]"
        )
        items: List[RestoreConfig] = self._audit.get_parameter_restore_details(
            ps_name)

        if len(items) == 0:
            self._out.warn(
                "No restorable values were found for this parameter.")
            return

        for i, item in enumerate(items):
            date = time.strftime("%Y-%m-%d %H:%M:%S",
                                 time.localtime(item.ps_time / 1000))

            # we need to decrypt the value, if encrypted, in order to show it to the user
            if item.ps_key_id:
                item.ps_value = self._kms.decrypt_with_context(
                    item.ps_value,
                    {"PARAMETER_ARN": self.get_parameter_arn(item.ps_name)},
                )
            table_entries.append([i, date, item.ps_value, item.ps_user])

        self._out.print(
            tabulate(
                table_entries,
                headers=["Item #", "Time Created", "Value", "User"],
                tablefmt="grid",
                numalign="center",
                stralign="left",
            ))

        valid_options = [f'{x}' for x in range(0, len(items))]
        choice = int(
            Input.select("Select an item number to restore: ",
                         valid_options=valid_options))
        item = items[choice] if items[choice] else None

        restore = Input.y_n_input(
            f"Are you sure you want to restore item #{choice} and have it be the latest version? ",
            default_yes=False)

        if not restore:
            self._utils.warn_exit("Restore aborted.")

        key_id = None if item.ps_type == "String" else item.ps_key_id

        try:
            self._ssm.set_parameter(item.ps_name,
                                    item.ps_value,
                                    item.ps_description,
                                    item.ps_type,
                                    key_id=key_id)

            current_value = self._ssm.get_parameter(item.ps_name)
            if current_value == item.ps_value:
                self._out.success("Restore was successful")
            else:
                self._out.error(
                    "Latest version in parameter store doesn't match what we restored."
                )
                self._out.print(
                    f"Current value: [[{current_value}]].  Expected value: [[{item.ps_value}]]"
                )

        except ClientError as e:
            self._client_exception_msg(item, e)
Example #29
0
    def _restore_params_to_point_in_time(self):
        """
        Restores parameters as they were to a point-in-time as defined by the time provided by the users.
        Replays parameter history to that point-in-time so versioning remains intact.
        """

        repl_destinations = []
        ps_prefix = Input.input(
            f"Which parameter store prefix would you like to recursively restore? "
            f"(e.g., /app/demo-time): ",
            completer=self._config_completer)

        authed_nses = self._cfg_view.get_authorized_namespaces()
        valid_prefix = (
            [True for ns in authed_nses if ps_prefix.startswith(ns)]
            or [False])[0]
        self._utils.validate(
            valid_prefix,
            f"Selected namespace must begin with a 'Fig Tree' you have access to. "
            f"Such as: {authed_nses}")

        time_selected, time_converted = None, None
        try:
            time_selected = Input.input(
                "Seconds since epoch to restore latest values from: ")
            time_converted = datetime.fromtimestamp(float(time_selected))
        except ValueError as e:
            if "out of range" in e.args[0]:
                try:
                    time_converted = datetime.fromtimestamp(
                        float(time_selected) / 1000)
                except ValueError as e:
                    self._utils.error_exit(
                        "Make sure you're using a format of either seconds or milliseconds since epoch."
                    )
            elif "could not convert" in e.args[0]:
                self._utils.error_exit(
                    f"The format of this input should be seconds since epoch. (e.g., 1547647091)\n"
                    f"Try using: https://www.epochconverter.com/ to convert your date to this "
                    f"specific format.")
            else:
                self._utils.error_exit(
                    "An unexpected exception triggered: "
                    f"'{e}' while trying to convert {time_selected} to 'datetime' format."
                )

        self._utils.validate(
            time_converted is not None,
            f"`{CLI_NAME}` encountered an error parsing your input for "
            f"target rollback time.")
        keep_going = Input.y_n_input(
            f"Are you sure you want to restore all figs under {ps_prefix} values to their state at: "
            f"{time_converted}? ",
            default_yes=False)

        if not keep_going:
            self._utils.warn_exit("Aborting restore due to user selection")

        ps_history: PSHistory = self._audit.get_parameter_history_before_time(
            time_converted, ps_prefix)
        restore_count = len(ps_history.history.values())

        if len(ps_history.history.values()) == 0:
            self._utils.warn_exit(
                "No results found for time range.  Aborting.")

        last_item_name = 'Unknown'
        try:
            for item in ps_history.history.values():
                last_item_name = item.name

                if self._is_replication_destination(item.name):
                    repl_destinations.append(item.name)
                    continue

                if item.cfg_at(time_converted).ps_action == SSM_PUT:
                    cfgs_before: List[RestoreConfig] = item.cfgs_before(
                        time_converted)
                    cfg_at: RestoreConfig = item.cfg_at(time_converted)
                    ssm_value = self._ssm.get_parameter(item.name)
                    dynamo_value = self._decrypt_if_applicable(cfg_at)

                    if ssm_value != dynamo_value:
                        if ssm_value is not None:
                            self._ssm.delete_parameter(item.name)

                        for cfg in cfgs_before:
                            decrypted_value = self._decrypt_if_applicable(cfg)
                            self._out.print(
                                f"\nRestoring: [[{cfg.ps_name}]] \nValue: [[{decrypted_value}]]"
                                f"\nDescription: [[{cfg.ps_description}]]\nKMS Key: "
                                f"[[{cfg.ps_key_id if cfg.ps_key_id else '[[No KMS Key Specified]]'}]]"
                            )
                            self._out.notify(
                                f"Replaying version: [[{cfg.ps_version}]] of [[{cfg.ps_name}]]"
                            )
                            print()

                            self._ssm.set_parameter(cfg.ps_name,
                                                    decrypted_value,
                                                    cfg.ps_description,
                                                    cfg.ps_type,
                                                    key_id=cfg.ps_key_id)
                    else:
                        self._out.success(
                            f"Config: {item.name} is current. Skipping.")
                else:
                    # This item must have been a delete, which means this config didn't exist at that time.
                    self._out.print(
                        f"Checking if [[{item.name}]] exists. It was previously deleted."
                    )
                    self._prompt_delete(item.name)
        except ClientError as e:
            if "AccessDeniedException" == e.response["Error"]["Code"]:
                self._utils.error_exit(
                    f"\n\nYou do not have permissions to restore config at the path:"
                    f" [[{last_item_name}]]")
            else:
                self._utils.error_exit(
                    f"Caught error when attempting restore. {e}")

        for item in repl_destinations:
            cfg = self._repl.get_config_repl(item)
            self._print_cannot_restore_msg(cfg)

        print("\n\n")
        if not repl_destinations:
            self._out.success_h2(
                f"[[{restore_count}]] configurations restored successfully!")
        else:
            self._out.warn(
                f"\n\n[[{len(repl_destinations)}]] configurations were not restored because they are shared "
                f"from other destinations. To restore them, restore their sources."
            )
            self._out.success(
                f"{restore_count - len(repl_destinations)} configurations restored successfully."
            )