def inner(self, *args, **kwargs): if os.environ.get( AnonymousUsageTracker._DISABLE_METRICS_ENV_VAR) == "true": return function(self, *args, **kwargs) command = getattr(self, 'type', None) log.info(f'GOt command {command}') if command: command = command.name cache = CacheManager(AnonymousUsageTracker._CACHE_NAME) if hasattr(self, 'context') and hasattr( self.context, 'defaults') and self.context.defaults is not None: if isinstance(self.context.defaults, CLIDefaults): user_id = self.context.defaults.user_id else: user_id = "EmptyDefaults" else: user_id = "NoOne" last_write, metrics = cache.get( AnonymousUsageTracker._METRICS_KEY, default=FiggyMetrics(user_id=user_id)) metrics.increment_count(command) if Utils.millis_since_epoch( ) - metrics.last_report > AnonymousUsageTracker.REPORT_FREQUENCY: defaults = FiggySetup.stc_get_defaults(skip=True) if defaults and defaults.usage_tracking: # Ship it async. If it don't worky, oh well :shruggie: with ThreadPoolExecutor(max_workers=1) as pool: pool.submit(AnonymousUsageTracker.report_usage, metrics) log.info( f'Reporting anonymous usage for metrics: {metrics}' ) cache.write(AnonymousUsageTracker._METRICS_KEY, FiggyMetrics(user_id=user_id)) return function(self, *args, **kwargs) else: cache.write(AnonymousUsageTracker._METRICS_KEY, metrics) return function(self, *args, **kwargs)
class FiggySetup: """ Contains logic around setting up Figgy. Configuring user auth, etc. """ # If we ever need to add params to this constructor we'll need to better handle dependencies and do a bit of # refactoring here. def __init__(self, context: FiggyContext): self._cache_mgr = CacheManager(file_override=DEFAULTS_FILE_CACHE_PATH) self._config_mgr, self.c = ConfigManager.figgy(), Utils.default_colors() self._session_mgr = None self._session_provider = None self._secrets_mgr = SecretsManager() self._figgy_context = context def get_assumable_roles(self, defaults: CLIDefaults = None) -> List[AssumableRole]: if not defaults: defaults = self.get_defaults() return self._get_session_provider(defaults).get_assumable_roles() def _get_session_manager(self, defaults: CLIDefaults) -> SessionManager: if not self._session_mgr: self._session_mgr = SessionManager(defaults, self._get_session_provider(defaults)) return self._session_mgr def _get_session_provider(self, defaults: CLIDefaults): if not self._session_provider: self._session_provider = SessionProviderFactory(defaults, self._figgy_context).instance() return self._session_provider 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 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 def configure_preferences(self, current_defaults: CLIDefaults): updated_defaults = current_defaults updated_defaults.region = self._config_mgr.get_or_prompt(Config.Section.Figgy.AWS_REGION, Input.select_region) updated_defaults.colors_enabled = self._config_mgr.get_or_prompt(Config.Section.Figgy.COLORS_ENABLED, Input.select_enable_colors, force_prompt=True) # Defaulting to True, users will always be prompted to report or not report an error. updated_defaults.report_errors = True # Defaulting usage tracking to on, unless the user updates ~/.figgy/config to disable it. updated_defaults.usage_tracking = self._config_mgr.get_property(Config.Section.Figgy.USAGE_TRACKING, default=True) return updated_defaults def configure_extras(self, current_defaults: CLIDefaults): updated_defaults = current_defaults if os.environ.get(FIGGY_DISABLE_KEYRING) == 'true': updated_defaults.extras[DISABLE_KEYRING] = True return updated_defaults def configure_figgy_defaults(self, current_defaults: CLIDefaults): updated_defaults = current_defaults env = GlobalEnvironment(role=current_defaults.assumable_roles[0], region=current_defaults.region) session = self._get_session_manager(current_defaults).get_session(env, prompt=True) ssm = SsmDao(session.client('ssm')) default_service_ns = ssm.get_parameter(PS_FIGGY_DEFAULT_SERVICE_NS_PATH) updated_defaults.service_ns = default_service_ns updated_defaults.enabled_regions = json.loads(ssm.get_parameter(PS_FIGGY_REGIONS)) return updated_defaults 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 save_defaults(self, defaults: CLIDefaults): self._cache_mgr.write(DEFAULTS_KEY, defaults) def get_defaults(self) -> CLIDefaults: try: last_write, defaults = self._cache_mgr.get(DEFAULTS_KEY) except Exception as e: # If cache is corrupted or inaccessible, "fogetaboutit" (in italian accent) return CLIDefaults.unconfigured() return defaults if defaults else CLIDefaults.unconfigured() @staticmethod 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 @staticmethod def print_role_table(roles: List[AssumableRole]): printable_roles: Dict[int: Dict] = {} for role in roles: item = printable_roles.get(role.account_id, {}) item['env'] = role.run_env.env item['roles'] = item.get('roles', []) + [role.role.role] printable_roles[role.account_id] = item print(tabulate( [ [ f'{account_id[0:5]} [REDACTED]', printable_roles[account_id]['env'], ', '.join(printable_roles[account_id]['roles']) ] for account_id in printable_roles.keys() ], headers=['Account #', 'Environment', 'Role'], tablefmt="grid", numalign="center", stralign="left", ))