diff --git a/.idea/runConfigurations/check.xml b/.idea/runConfigurations/check.xml new file mode 100644 index 0000000..27dc2e2 --- /dev/null +++ b/.idea/runConfigurations/check.xml @@ -0,0 +1,23 @@ + + + + + \ No newline at end of file diff --git a/docs/source/conf.py b/docs/source/conf.py index df5432a..20f3a33 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -29,6 +29,7 @@ # ones. extensions = [ "sphinx_rtd_theme", + "sphinx.ext.autosectionlabel", ] # Add any paths that contain templates here, relative to this directory. diff --git a/docs/source/index.rst b/docs/source/index.rst index 38a6b93..8618bd1 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -51,4 +51,5 @@ Features reference/signals reference/models reference/management_commands + reference/checks reference/change_log diff --git a/docs/source/installation/publish.rst b/docs/source/installation/publish.rst index 43b8bb4..374d9a8 100644 --- a/docs/source/installation/publish.rst +++ b/docs/source/installation/publish.rst @@ -5,6 +5,8 @@ Deployment Checklist Before deploying your site to production it is important to go over some best practices and make sure your site is the **most stable and secure**. Provided here are some best practices related to ``django-windowsauth``, IIS and LDAP. +Many checks can be performed automatically using ``py manage.py check --deploy``. + .. seealso:: Check out `Django's deployment checklist `_ too. diff --git a/docs/source/reference/checks.rst b/docs/source/reference/checks.rst new file mode 100644 index 0000000..c9d3b05 --- /dev/null +++ b/docs/source/reference/checks.rst @@ -0,0 +1,108 @@ + +Checks +====== + +.. glossary:: + + wauth.E001 + Type: ``Error`` + + Required setting ``WAUTH_DOMAINS`` is not configured. :ref:`WAUTH_DOMAINS (Required)` + +.. glossary:: + + wauth.W002 + Type: ``Warning`` + + Found missing domain settings for LDAP users. + +.. glossary:: + + wauth.E003 + Type: ``Error`` + + | Unable to load LDAPUser model. + | Try running ``py manage.py migrate windows_auth``. + +.. glossary:: + + wauth.W004 + Type: ``Warning`` + + | Unable to create LDAP connection with a configured domain. + | Check your settings and the server. + +.. glossary:: + + wauth.E005 + Type: ``Error`` + + | Error while creating LDAP connection with a configured domain + | Check your settings and the server. + +.. glossary:: + + wauth.E006 + Type: ``Error`` + + You have ``windows_auth.middleware.SimulateWindowsAuthMiddleware`` in your ``MIDDLEWARE``, but you have not configured ``WAUTH_SIMULATE_USER``. :ref:`WAUTH_SIMULATE_USER` + +.. glossary:: + + wauth.W010 + Type: ``Warning``, Deploy only + + You should not have ``windows_auth.middleware.SimulateWindowsAuthMiddleware`` in your middleware in production. + :ref:`SimulateWindowsAuthMiddleware` + +.. glossary:: + + wauth.I011 + Type: ``Info``, Deploy only + + Using the database to check LDAP user last sync time is slow. + If you can, you should use cache system instead. + :ref:`WAUTH_USE_CACHE` + +.. glossary:: + + wauth.W012 + Type: ``Warning``, Deploy only + + ``USE_SSL`` is not set to True. It is recommended to use only secure LDAP connection. + :ref:`USE_SSL` + +.. glossary:: + + wauth.W013 + Type: ``Warning``, Deploy only + + You should use a stronger authentication method for you LDAP connection. + Configure ``authentication`` to SASL or NTLM in you ``CONNECTION_OPTIONS``. + :doc:`../howto/securing_ldap` + +.. glossary:: + + wauth.W014 + Type: ``Warning``, Deploy only + + You should use a dedicated bind account with the minimum permissions needed. + Your bind account has logged in to website. + :ref:`USERNAME` + +.. glossary:: + + wauth.W015 + Type: ``Warning``, Deploy only + + You should use a dedicated connection for you write operations. + Using a different connection, and even another bind account, is considered best-practice. + :ref:`READ_ONLY` + +.. glossary:: + + wauth.I020 + Type: ``Info``, Deploy only + + You should keep your site and project files on a separate disk from the OS. + :ref:`READ_ONLY` diff --git a/windows_auth/apps.py b/windows_auth/apps.py index b4b5359..322e9ad 100644 --- a/windows_auth/apps.py +++ b/windows_auth/apps.py @@ -1,9 +1,7 @@ import atexit -from django.db import DatabaseError from ldap3.core.exceptions import LDAPException from django.apps import AppConfig -from django.db.models import Count from windows_auth import logger @@ -13,7 +11,8 @@ class WindowsAuthConfig(AppConfig): default_auto_field = 'django.db.models.AutoField' def ready(self): - from windows_auth.conf import WAUTH_IGNORE_SETTING_WARNINGS, WAUTH_PRELOAD_DOMAINS, WAUTH_DOMAINS + from windows_auth import checks + from windows_auth.conf import WAUTH_PRELOAD_DOMAINS, WAUTH_DOMAINS from windows_auth.settings import DEFAULT_DOMAIN_SETTING from windows_auth.ldap import get_ldap_manager, close_connections @@ -23,19 +22,6 @@ def ready(self): # You can avoid this behavior by using "runserver --noreload" parameter, # or modifying the WAUTH_PRELOAD_DOMAINS setting to False. - # check about users with domain missing from settings - if not WAUTH_IGNORE_SETTING_WARNINGS and DEFAULT_DOMAIN_SETTING not in WAUTH_DOMAINS: - try: - from windows_auth.models import LDAPUser - missing_domains = LDAPUser.objects.exclude(domain__in=WAUTH_DOMAINS.keys()) - if missing_domains.exists(): - for result in missing_domains.values("domain").annotate(count=Count("pk")): - logger.warning(f"Settings for domain \"{result.get('domain')}\" are missing from WAUTH_DOMAINS " - f"({result.get('count')} users found)") - except DatabaseError as e: - # Table probably does not exist yet, migration is pending - logger.warn(e) - # configure default preload domains preload_domains = WAUTH_PRELOAD_DOMAINS if preload_domains in (None, True): diff --git a/windows_auth/checks.py b/windows_auth/checks.py new file mode 100644 index 0000000..c59193d --- /dev/null +++ b/windows_auth/checks.py @@ -0,0 +1,216 @@ +import os +from typing import List + +import ldap3 +from django.conf import settings +from django.core.checks import register, CheckMessage, Error, Warning, Info +from django.db import DatabaseError +from django.db.models import Count +from ldap3.core.exceptions import LDAPException + +from windows_auth import logger +from windows_auth.conf import WAUTH_IGNORE_SETTING_WARNINGS, WAUTH_DOMAINS, WAUTH_USE_CACHE, WAUTH_USE_SPN +from windows_auth.ldap import get_ldap_manager +from windows_auth.models import LDAPUser +from windows_auth.settings import DEFAULT_DOMAIN_SETTING + + +@register() +def check_widows_auth_settings(app_configs, **kwargs): + messages: List[CheckMessage] = [] + + # Require WAUTH_DOMAINS + if not hasattr(settings, "WAUTH_DOMAINS"): + messages.append( + Error( + "Required setting \"WAUTH_DOMAINS\" is not configured.", + hint="Add \"WAUTH_DOMAINS\" to your settings file.", + id="wauth.E001" + ) + ) + + # Require WAUTH_SIMULATE_USER + if 'windows_auth.middleware.SimulateWindowsAuthMiddleware' in settings.MIDDLEWARE \ + and not hasattr(settings, "WAUTH_SIMULATE_USER"): + messages.append( + Error( + "You have \'windows_auth.middleware.SimulateWindowsAuthMiddleware\' in your MIDDLEWARE, " + "but you have not configured WAUTH_SIMULATE_USER.", + id="wauth.E006", + ) + ) + + return messages + + +@register() +def check_missing_domains(app_configs, **kwargs): + messages: List[CheckMessage] = [] + + # TODO deprecate WAUTH_IGNORE_SETTING_WARNINGS + if not WAUTH_IGNORE_SETTING_WARNINGS and DEFAULT_DOMAIN_SETTING not in WAUTH_DOMAINS: + try: + from windows_auth.models import LDAPUser + missing_domains = LDAPUser.objects.exclude(domain__in=WAUTH_DOMAINS.keys()) + for result in missing_domains.values("domain").annotate(count=Count("pk")): + messages.append( + Warning( + f"Found missing domain settings for \"{result.get('domain')}\" " + f"({result.get('count')} LDAP users from this domain found in the database).", + hint=f"Add settings for \"{result.get('domain')}\" in WAUTH_DOMAINS.", + obj=result.get("domain"), + id="wauth.W002" + ) + ) + except DatabaseError as e: + logger.exception(f"Unable to load LDAPUser model: {e}") + # Table probably does not exist yet, migration is pending + messages.append( + Error( + "Unable to load LDAPUser model", + hint="Try running \"py manage.py migrate windows_auth\".", + id="wauth.E003", + ) + ) + + return messages + + +@register() +def check_ldap_domains(app_configs, **kwargs): + messages: List[CheckMessage] = [] + + for domain in WAUTH_DOMAINS.keys(): + # skip loading default domain + if domain == DEFAULT_DOMAIN_SETTING: + continue + + try: + # attempt to load LDAP connection + manager = get_ldap_manager(domain) + if not manager.connection.bound: + messages.append( + Warning( + f"Unable to create LDAP connection with domain {domain}.", + obj=domain, + id="wauth.W004", + ), + ) + except LDAPException as e: + messages.append( + Error( + f"Error while creating LDAP connection with domain {domain}: {e}", + obj=domain, + id="wauth.E005", + ), + ) + + return messages + + +@register(deploy=True) +def check_simulate_wauth(app_configs, **kwargs): + messages: List[CheckMessage] = [] + + if 'windows_auth.middleware.SimulateWindowsAuthMiddleware' in settings.MIDDLEWARE: + messages.append( + Warning( + "You should not have \'windows_auth.middleware.SimulateWindowsAuthMiddleware\' " + "in your middleware in production.", + id="wauth.W010" + ) + ) + + return messages + + +@register(deploy=True) +def check_deployment_settings(app_configs, **kwargs): + messages: List[CheckMessage] = [] + + # check cache system enabled + if not WAUTH_USE_CACHE: + messages.append( + Info( + "Using the database to check LDAP user last sync time is slow. " + "If you can, you should use cache system instead.", + hint="Set WAUTH_USE_CACHE to True", + id="wauth.I011", + ) + ) + + # check LDAP settings + for domain in WAUTH_DOMAINS.keys(): + # skip loading default domain + if domain == DEFAULT_DOMAIN_SETTING: + continue + + manager = get_ldap_manager(domain) + + # check SSL + if not manager.settings.USE_SSL: + messages.append( + Warning( + "USE_SSL is not set to True. It is recommended to use only secure LDAP connection.", + obj=domain, + id="wauth.W012", + ) + ) + + # check authentication method + if not manager.settings.CONNECTION_OPTIONS.get("authentication") in (ldap3.SASL, ldap3.NTLM): + messages.append( + Warning( + "You should use a stronger authentication method for you LDAP connection.", + hint="Configure \"authentication\" to SASL or NTLM in you CONNECTION_OPTIONS.", + obj=domain, + id="wauth.W013" + ) + ) + + # check bind user + if WAUTH_USE_SPN: + username, user_domain = manager.settings.USERNAME.rsplit("@", 2) + else: + user_domain, username = manager.settings.USERNAME.split("\\", 2) + + if LDAPUser.objects.filter(user__username=username, domain=user_domain).exists(): + messages.append( + Warning( + "You should use a dedicated bind account with the minimum permissions needed.", + hint="Your bind account has logged in to website.", + obj=domain, + id="wauth.W014", + ) + ) + + # check dedicated writeable connections + if not manager.settings.READ_ONLY and LDAPUser.objects.filter(domain=domain).exists(): + messages.append( + Warning( + "You should use a dedicated connection for you write operations. " + "Using a different connection, and even another bind account, is considered best-practice.", + obj=domain, + id="wauth.W015", + ) + ) + + return messages + + +@register(deploy=True) +def check_deployment_setup(app_configs, **kwargs): + messages: List[CheckMessage] = [] + + # check project drive is not OS drive + system_drive = os.environ.get('SYSTEMDRIVE') + current_drive, path = os.path.splitdrive(__file__) + if system_drive == current_drive: + messages.append( + Info( + "You should keep your site and project files on a separate disk from the OS.", + id="wauth.I020", + ) + ) + + return messages