diff --git a/Dockerfile b/Dockerfile index 4347aa8..8db4cac 100644 --- a/Dockerfile +++ b/Dockerfile @@ -61,12 +61,12 @@ COPY ${NETBOX_PATH} /opt/netbox COPY docker/configuration.docker.py /opt/netbox/netbox/netbox/configuration.py -COPY configuration/gunicorn_config.py /etc/netbox/config/ +COPY docker/gunicorn_config.py /etc/netbox/ COPY docker/nginx.conf /etc/netbox-nginx/nginx.conf COPY docker/docker-entrypoint.sh /opt/netbox/docker-entrypoint.sh COPY startup_scripts/ /opt/netbox/startup_scripts/ COPY initializers/ /opt/netbox/initializers/ -COPY configuration/configuration.py /etc/netbox/config/configuration.py +COPY configuration/ /etc/netbox/config/ WORKDIR /opt/netbox/netbox @@ -79,7 +79,7 @@ ENTRYPOINT [ "/opt/netbox/docker-entrypoint.sh" ] -CMD ["gunicorn", "-c /etc/netbox/config/gunicorn_config.py", "netbox.wsgi"] +CMD ["gunicorn", "-c /etc/netbox/gunicorn_config.py", "netbox.wsgi"] LABEL ORIGINAL_TAG="" \ NETBOX_GIT_BRANCH="" \ @@ -122,4 +122,3 @@ util-linux COPY docker/ldap_config.docker.py /opt/netbox/netbox/netbox/ldap_config.py -COPY configuration/ldap_config.py /etc/netbox/config/ldap_config.py diff --git a/configuration/gunicorn_config.py b/configuration/gunicorn_config.py deleted file mode 100644 index 063d822..0000000 --- a/configuration/gunicorn_config.py +++ /dev/null @@ -1,8 +0,0 @@ -command = '/usr/bin/gunicorn' -pythonpath = '/opt/netbox/netbox' -bind = '0.0.0.0:8001' -workers = 3 -errorlog = '-' -accesslog = '-' -capture_output = False -loglevel = 'debug' diff --git a/configuration/ldap/ldap_config.py b/configuration/ldap/ldap_config.py new file mode 100644 index 0000000..da6c6fe --- /dev/null +++ b/configuration/ldap/ldap_config.py @@ -0,0 +1,84 @@ +import ldap +import os + +from django_auth_ldap.config import LDAPSearch +from importlib import import_module + +# Read secret from file +def read_secret(secret_name, default=''): + try: + f = open('/run/secrets/' + secret_name, 'r', encoding='utf-8') + except EnvironmentError: + return default + else: + with f: + return f.readline().strip() + +# Import and return the group type based on string name +def import_group_type(group_type_name): + mod = import_module('django_auth_ldap.config') + try: + return getattr(mod, group_type_name)() + except: + return None + +# Server URI +AUTH_LDAP_SERVER_URI = os.environ.get('AUTH_LDAP_SERVER_URI', '') + +# The following may be needed if you are binding to Active Directory. +AUTH_LDAP_CONNECTION_OPTIONS = { + ldap.OPT_REFERRALS: 0 +} + +# Set the DN and password for the NetBox service account. +AUTH_LDAP_BIND_DN = os.environ.get('AUTH_LDAP_BIND_DN', '') +AUTH_LDAP_BIND_PASSWORD = read_secret('auth_ldap_bind_password', os.environ.get('AUTH_LDAP_BIND_PASSWORD', '')) + +# Set a string template that describes any user’s distinguished name based on the username. +AUTH_LDAP_USER_DN_TEMPLATE = os.environ.get('AUTH_LDAP_USER_DN_TEMPLATE', None) + +# Enable STARTTLS for ldap authentication. +AUTH_LDAP_START_TLS = os.environ.get('AUTH_LDAP_START_TLS', 'False').lower() == 'true' + +# Include this setting if you want to ignore certificate errors. This might be needed to accept a self-signed cert. +# Note that this is a NetBox-specific setting which sets: +# ldap.set_option(ldap.OPT_X_TLS_REQUIRE_CERT, ldap.OPT_X_TLS_NEVER) +LDAP_IGNORE_CERT_ERRORS = os.environ.get('LDAP_IGNORE_CERT_ERRORS', 'False').lower() == 'true' + +AUTH_LDAP_USER_SEARCH_BASEDN = os.environ.get('AUTH_LDAP_USER_SEARCH_BASEDN', '') +AUTH_LDAP_USER_SEARCH_ATTR = os.environ.get('AUTH_LDAP_USER_SEARCH_ATTR', 'sAMAccountName') +AUTH_LDAP_USER_SEARCH = LDAPSearch(AUTH_LDAP_USER_SEARCH_BASEDN, + ldap.SCOPE_SUBTREE, + "(" + AUTH_LDAP_USER_SEARCH_ATTR + "=%(user)s)") + +# This search ought to return all groups to which the user belongs. django_auth_ldap uses this to determine group +# heirarchy. +AUTH_LDAP_GROUP_SEARCH_BASEDN = os.environ.get('AUTH_LDAP_GROUP_SEARCH_BASEDN', '') +AUTH_LDAP_GROUP_SEARCH_CLASS = os.environ.get('AUTH_LDAP_GROUP_SEARCH_CLASS', 'group') +AUTH_LDAP_GROUP_SEARCH = LDAPSearch(AUTH_LDAP_GROUP_SEARCH_BASEDN, ldap.SCOPE_SUBTREE, + "(objectClass=" + AUTH_LDAP_GROUP_SEARCH_CLASS + ")") +AUTH_LDAP_GROUP_TYPE = import_group_type(os.environ.get('AUTH_LDAP_GROUP_TYPE', 'GroupOfNamesType')) + +# Define a group required to login. +AUTH_LDAP_REQUIRE_GROUP = os.environ.get('AUTH_LDAP_REQUIRE_GROUP_DN', '') + +# Define special user types using groups. Exercise great caution when assigning superuser status. +AUTH_LDAP_USER_FLAGS_BY_GROUP = { + "is_active": os.environ.get('AUTH_LDAP_REQUIRE_GROUP_DN', ''), + "is_staff": os.environ.get('AUTH_LDAP_IS_ADMIN_DN', ''), + "is_superuser": os.environ.get('AUTH_LDAP_IS_SUPERUSER_DN', '') +} + +# For more granular permissions, we can map LDAP groups to Django groups. +AUTH_LDAP_FIND_GROUP_PERMS = os.environ.get('AUTH_LDAP_FIND_GROUP_PERMS', 'True').lower() == 'true' +AUTH_LDAP_MIRROR_GROUPS = os.environ.get('AUTH_LDAP_MIRROR_GROUPS', None).lower() == 'true' + +# Cache groups for one hour to reduce LDAP traffic +AUTH_LDAP_CACHE_TIMEOUT = int(os.environ.get('AUTH_LDAP_CACHE_TIMEOUT', 3600)) + +# Populate the Django user from the LDAP directory. +AUTH_LDAP_USER_ATTR_MAP = { + "first_name": os.environ.get('AUTH_LDAP_ATTR_FIRSTNAME', 'givenName'), + "last_name": os.environ.get('AUTH_LDAP_ATTR_LASTNAME', 'sn'), + "email": os.environ.get('AUTH_LDAP_ATTR_MAIL', 'mail') +} diff --git a/configuration/ldap_config.py b/configuration/ldap_config.py deleted file mode 100644 index da6c6fe..0000000 --- a/configuration/ldap_config.py +++ /dev/null @@ -1,84 +0,0 @@ -import ldap -import os - -from django_auth_ldap.config import LDAPSearch -from importlib import import_module - -# Read secret from file -def read_secret(secret_name, default=''): - try: - f = open('/run/secrets/' + secret_name, 'r', encoding='utf-8') - except EnvironmentError: - return default - else: - with f: - return f.readline().strip() - -# Import and return the group type based on string name -def import_group_type(group_type_name): - mod = import_module('django_auth_ldap.config') - try: - return getattr(mod, group_type_name)() - except: - return None - -# Server URI -AUTH_LDAP_SERVER_URI = os.environ.get('AUTH_LDAP_SERVER_URI', '') - -# The following may be needed if you are binding to Active Directory. -AUTH_LDAP_CONNECTION_OPTIONS = { - ldap.OPT_REFERRALS: 0 -} - -# Set the DN and password for the NetBox service account. -AUTH_LDAP_BIND_DN = os.environ.get('AUTH_LDAP_BIND_DN', '') -AUTH_LDAP_BIND_PASSWORD = read_secret('auth_ldap_bind_password', os.environ.get('AUTH_LDAP_BIND_PASSWORD', '')) - -# Set a string template that describes any user’s distinguished name based on the username. -AUTH_LDAP_USER_DN_TEMPLATE = os.environ.get('AUTH_LDAP_USER_DN_TEMPLATE', None) - -# Enable STARTTLS for ldap authentication. -AUTH_LDAP_START_TLS = os.environ.get('AUTH_LDAP_START_TLS', 'False').lower() == 'true' - -# Include this setting if you want to ignore certificate errors. This might be needed to accept a self-signed cert. -# Note that this is a NetBox-specific setting which sets: -# ldap.set_option(ldap.OPT_X_TLS_REQUIRE_CERT, ldap.OPT_X_TLS_NEVER) -LDAP_IGNORE_CERT_ERRORS = os.environ.get('LDAP_IGNORE_CERT_ERRORS', 'False').lower() == 'true' - -AUTH_LDAP_USER_SEARCH_BASEDN = os.environ.get('AUTH_LDAP_USER_SEARCH_BASEDN', '') -AUTH_LDAP_USER_SEARCH_ATTR = os.environ.get('AUTH_LDAP_USER_SEARCH_ATTR', 'sAMAccountName') -AUTH_LDAP_USER_SEARCH = LDAPSearch(AUTH_LDAP_USER_SEARCH_BASEDN, - ldap.SCOPE_SUBTREE, - "(" + AUTH_LDAP_USER_SEARCH_ATTR + "=%(user)s)") - -# This search ought to return all groups to which the user belongs. django_auth_ldap uses this to determine group -# heirarchy. -AUTH_LDAP_GROUP_SEARCH_BASEDN = os.environ.get('AUTH_LDAP_GROUP_SEARCH_BASEDN', '') -AUTH_LDAP_GROUP_SEARCH_CLASS = os.environ.get('AUTH_LDAP_GROUP_SEARCH_CLASS', 'group') -AUTH_LDAP_GROUP_SEARCH = LDAPSearch(AUTH_LDAP_GROUP_SEARCH_BASEDN, ldap.SCOPE_SUBTREE, - "(objectClass=" + AUTH_LDAP_GROUP_SEARCH_CLASS + ")") -AUTH_LDAP_GROUP_TYPE = import_group_type(os.environ.get('AUTH_LDAP_GROUP_TYPE', 'GroupOfNamesType')) - -# Define a group required to login. -AUTH_LDAP_REQUIRE_GROUP = os.environ.get('AUTH_LDAP_REQUIRE_GROUP_DN', '') - -# Define special user types using groups. Exercise great caution when assigning superuser status. -AUTH_LDAP_USER_FLAGS_BY_GROUP = { - "is_active": os.environ.get('AUTH_LDAP_REQUIRE_GROUP_DN', ''), - "is_staff": os.environ.get('AUTH_LDAP_IS_ADMIN_DN', ''), - "is_superuser": os.environ.get('AUTH_LDAP_IS_SUPERUSER_DN', '') -} - -# For more granular permissions, we can map LDAP groups to Django groups. -AUTH_LDAP_FIND_GROUP_PERMS = os.environ.get('AUTH_LDAP_FIND_GROUP_PERMS', 'True').lower() == 'true' -AUTH_LDAP_MIRROR_GROUPS = os.environ.get('AUTH_LDAP_MIRROR_GROUPS', None).lower() == 'true' - -# Cache groups for one hour to reduce LDAP traffic -AUTH_LDAP_CACHE_TIMEOUT = int(os.environ.get('AUTH_LDAP_CACHE_TIMEOUT', 3600)) - -# Populate the Django user from the LDAP directory. -AUTH_LDAP_USER_ATTR_MAP = { - "first_name": os.environ.get('AUTH_LDAP_ATTR_FIRSTNAME', 'givenName'), - "last_name": os.environ.get('AUTH_LDAP_ATTR_LASTNAME', 'sn'), - "email": os.environ.get('AUTH_LDAP_ATTR_MAIL', 'mail') -} diff --git a/docker/configuration.docker.py b/docker/configuration.docker.py index 733887f..3a4d2a2 100644 --- a/docker/configuration.docker.py +++ b/docker/configuration.docker.py @@ -1,10 +1,62 @@ +from os.path import abspath, isfile +from os import scandir import importlib.util import sys -try: - spec = importlib.util.spec_from_file_location('configuration', '/etc/netbox/config/configuration.py') +_CONFIG_DIR = '/etc/netbox/config/' +_MAIN_CONFIG = 'configuration' +_MODULE = 'netbox.configuration' +_loaded_configurations = [] + + +def __getattr__(name): + for config in _loaded_configurations: + try: + return getattr(config, name) + except: + pass + raise AttributeError + + +def _filename(f): + return f.name + + +def _import(module_name, path): + spec = importlib.util.spec_from_file_location('', path) module = importlib.util.module_from_spec(spec) spec.loader.exec_module(module) - sys.modules['netbox.configuration'] = module -except: - raise ImportError('') + sys.modules[module_name] = module + + _loaded_configurations.insert(0, module) + + print(f"🧬 loaded config '{path}'") + + +_main_config_path = abspath(f'{_CONFIG_DIR}/{_MAIN_CONFIG}.py') +if isfile(_main_config_path): + _import(f'{_MODULE}.configuration', _main_config_path) +else: + print(f"⚠️ Main configuration '{_main_config_path}' not found.") + +with scandir(_CONFIG_DIR) as it: + for f in sorted(it, key=_filename): + if not f.is_file(): + continue + + if f.name.startswith('__'): + continue + + if not f.name.endswith('.py'): + continue + + if f.name == f'{_MAIN_CONFIG}.py': + continue + + module_name = f"{_MODULE}.{f.name[:-len('.py')]}" + + _import(module_name, f.path) + +if len(_loaded_configurations) == 0: + print(f"‼️ No configuration files found in '{_CONFIG_DIR}'.") + raise ImportError(f"No configuration files found in '{_CONFIG_DIR}'.") diff --git a/docker/gunicorn_config.py b/docker/gunicorn_config.py new file mode 100644 index 0000000..063d822 --- /dev/null +++ b/docker/gunicorn_config.py @@ -0,0 +1,8 @@ +command = '/usr/bin/gunicorn' +pythonpath = '/opt/netbox/netbox' +bind = '0.0.0.0:8001' +workers = 3 +errorlog = '-' +accesslog = '-' +capture_output = False +loglevel = 'debug' diff --git a/docker/ldap_config.docker.py b/docker/ldap_config.docker.py index 8d82173..601ba71 100644 --- a/docker/ldap_config.docker.py +++ b/docker/ldap_config.docker.py @@ -1,10 +1,62 @@ +from os.path import abspath, isfile +from os import scandir import importlib.util import sys -try: - spec = importlib.util.spec_from_file_location('ldap_config', '/etc/netbox/config/ldap_config.py') +_CONFIG_DIR = '/etc/netbox/config/ldap/' +_MAIN_CONFIG = 'ldap_config' +_MODULE = 'netbox.configuration.ldap' +_loaded_configurations = [] + + +def __getattr__(name): + for config in _loaded_configurations: + try: + return getattr(config, name) + except: + pass + raise AttributeError + + +def _filename(f): + return f.name + + +def _import(module_name, path): + spec = importlib.util.spec_from_file_location('', path) module = importlib.util.module_from_spec(spec) spec.loader.exec_module(module) - sys.modules['netbox.ldap_config'] = module -except: - raise ImportError('') + sys.modules[module_name] = module + + _loaded_configurations.insert(0, module) + + print(f"🧬 loaded config '{path}'") + + +_main_config_path = abspath(f'{_CONFIG_DIR}/{_MAIN_CONFIG}.py') +if isfile(_main_config_path): + _import(f'{_MODULE}.configuration', _main_config_path) +else: + print(f"⚠️ Main configuration '{_main_config_path}' not found.") + +with scandir(_CONFIG_DIR) as it: + for f in sorted(it, key=_filename): + if not f.is_file(): + continue + + if f.name.startswith('__'): + continue + + if not f.name.endswith('.py'): + continue + + if f.name == f'{_MAIN_CONFIG}.py': + continue + + module_name = f"{_MODULE}.{f.name[:-len('.py')]}" + + _import(module_name, f.path) + +if len(_loaded_configurations) == 0: + print(f"‼️ No configuration files found in '{_CONFIG_DIR}'.") + raise ImportError(f"No configuration files found in '{_CONFIG_DIR}'.")