Ejemplo n.º 1
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."
        )
Ejemplo n.º 2
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
Ejemplo n.º 3
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"
Ejemplo n.º 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}"
                )
Ejemplo n.º 5
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}'
        )
Ejemplo n.º 6
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")
Ejemplo n.º 7
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()
Ejemplo n.º 8
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
Ejemplo n.º 9
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
Ejemplo n.º 10
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."
            )
Ejemplo n.º 11
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.")