uktrade/directory-components

View on GitHub
directory_components/janitor/management/commands/helpers.py

Summary

Maintainability
A
1 hr
Test Coverage
import difflib
from pprint import pformat
import importlib
import inspect
import re

from colors import red, green
from vulture import Vulture

from django.core.management.commands.diffsettings import module_to_dict
from django.conf import global_settings, settings


DEFAULT_UNSAFE_SETTINGS = [
    re.compile('.*?PASSWORD.*?'),
    re.compile('.*?SECRET.*?'),
    re.compile('.*?AUTHORIZATION.*?'),
    re.compile('.*?KEY.*?'),
    re.compile('.*?TOKEN.*?'),
    re.compile('.*?DSN.*?'),
]


def list_vault_paths(client, root):
    response = client.list(path=f'{root}/metadata')
    for project in response['data']['keys']:
        response = client.list(path=f'{root}/metadata/{project}')
        for environment in response['data']['keys']:
            yield f'{root}/data/{project}{environment}'


def get_secrets_wizard(client, root):
    response = client.list(path=root)
    project = prompt_user_choice(
        message=f'{root} Choose a projects:',
        options=response['data']['keys'],
    )

    response = client.list(path=f'{root}/{project}')
    environment = prompt_user_choice(
        message=f'({root}{project}) Choose an environment:',
        options=response['data']['keys'],
    )

    return get_secrets(
        client=client,
        path=f'{root}/data{project}{environment}',
    )


def prompt_user_choice(message, options):
    display = '\n'.join([f'{[i]} {option}' for i, option in enumerate(options)])
    index = int(input(f'{message}:\n\n{display}\n\n'))
    return options[index]


def clean_secrets(secrets):
    ignore_settings = getattr(
        settings,
        'DIRECTORY_COMPONENTS_VAULT_IGNORE_SETTINGS_REGEX',
        DEFAULT_UNSAFE_SETTINGS
    )
    secrets = secrets.copy()
    for key in secrets:
        for entry in ignore_settings:
            if entry.match(key):
                secrets[key] = '💀' * 5
                break
    return secrets


def get_secrets(client, path):
    response = client.read(path=path)
    return response['data']['data']


def write_secrets(client, path, secrets):
    client.write(path=path, wrap_ttl=None, data=secrets)


def diff_dicts(dict_a, dict_b):
    return difflib.ndiff(
       pformat(clean_secrets(dict_a)).splitlines(),
       pformat(clean_secrets(dict_b)).splitlines(),
    )


def colour_diff(diff):
    for line in diff:
        if line.startswith('+'):
            yield green(line)
        elif line.startswith('-'):
            yield red(line)
        else:
            yield line


class Vulture(Vulture):

    def __init__(self, *args, **kwargs):
        self.settings_keys = list(module_to_dict(settings._wrapped).keys())
        super().__init__(*args, **kwargs)

    def report(self, min_confidence=0):
        for unused_code in self.get_unused_code(min_confidence=min_confidence):
            report = unused_code.get_report()
            if 'conf/settings.py' in report:
                yield unused_code.name

    def visit_Str(self, node):
        # handle cases like getattr(settings, 'SOME_SETTING')
        name = resolve_setting_name(name=node.s, settings_keys=self.settings_keys)
        if name:
            self.used_names.add(name)
        else:
            return super().visit_Str(node)


def get_settings_source_code(settings):
    # SETTINGS_MODULE is set only when the settings are provided from settings.py otherwise
    # when settings are explicitly set via settings.configure SETTINGS_MODULE is empty
    assert settings.SETTINGS_MODULE
    return inspect.getsource(importlib.import_module(settings.SETTINGS_MODULE))


def resolve_setting_name(name, settings_keys):
    resolved_name = None
    match = next((item for item in settings_keys if item == name), None)

    # prevent matching SECRET_KEY to LIBRARY_SECRET_KEY
    if match:
        if not hasattr(global_settings, match):
            resolved_name = match
    else:
        # handle when USERNAME_REQUIRED is used in code that refers to ACCOUNT_USERNAME_REQUIRED setting
        partial_match = next((item for item in settings_keys if item.endswith(name)), None)
        if partial_match:
            # gets prefix for partial matches e.g, ACCOUNT_
            prefix = partial_match.replace(name, '')
            # avoids KEY being misidentified as LIBRARY_SECRET_KEY
            # or heaven forbid C being misidentified as DIRECTORY_CONSTANTS_URL_GREAT_DOMESTIC
            if prefix.count('_') == 1:
                resolved_name = partial_match
    return resolved_name