diff --git a/.github/workflows/django.yml b/.github/workflows/django.yml new file mode 100644 index 0000000..f979d50 --- /dev/null +++ b/.github/workflows/django.yml @@ -0,0 +1,132 @@ +name: Django Testing CI + +on: + push: + branches: [ main ] + pull_request: + branches: [ main, release-* ] +env: + REGISTRY: ghcr.io + IMAGE_NAME: ${{ github.repository }} + +jobs: + build-ldap-server: + runs-on: ubuntu-latest + + permissions: + contents: read + packages: write + + steps: + - name: Checkout repository + uses: actions/checkout@v2 + + - name: Log in to the Container registry + uses: docker/login-action@f054a8b539a109f9f41c372932f1ae047eff08c9 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Extract metadata (tags, labels) for Docker + id: meta + uses: docker/metadata-action@98669ae865ea3cffbcbaa878cf57c20bbf1c6c38 + with: + images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + + - name: Build and push Docker image + uses: docker/build-push-action@ad44023a93711e3deb337508980b4b5e9bcdc5dc + with: + context: testproj/ldap_server + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + + test: + runs-on: ubuntu-latest + needs: build-ldap-server + services: + ldap: + image: ghcr.io/danyi1212/django-windowsauth:${{ github.head_ref }} + ports: + - 389:389 + - 636:636 + options: --name ldap-server + + strategy: + fail-fast: false + matrix: + python-version: + - "3.7" + - "3.8" + - "3.9" + - "3.10" + django-version: + - "2.2" # LTS + - "3.0" + - "3.1" + - "3.2" # LTS + # - "4.0" + exclude: + # Python 3.9 is compatible with django 3.0+ + - { python-version: "3.9", django-version: "2.2" } + # Python 3.10 is compatible with Django 3.2+ + - { python-version: "3.10", django-version: "2.2" } + - { python-version: "3.10", django-version: "3.0" } + - { python-version: "3.10", django-version: "3.1" } + # Django 4 is compatible with Python 3.8+ + # - { python-version: "3.7", django-version: "4.0" } + + steps: + - uses: actions/checkout@v2 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v2 + with: + python-version: ${{ matrix.python-version }} + + - name: Ping LDAP Server + uses: docker://docker + with: + args: docker exec ldap-server ldapsearch -D 'cn=admin,dc=example,dc=org' -w Adm1n! -b 'dc=example,dc=org' + + - name: Upgrade pip version + run: python -m pip install -U pip + + - name: Install Dependencies + working-directory: ./testproj + run: pip install -r requirements-github.txt + + - name: Install django ${{ matrix.django-version }} + run: python -m pip install "Django~=${{ matrix.django-version }}" + + - name: Python and Django versions + run: | + echo "Python ${{ matrix.python-version }} -> Django ${{ matrix.django-version }}" + python --version + echo "Django: `django-admin --version`" + + - name: Create logs directory + working-directory: ./testproj + run: mkdir logs + + - name: Run Tests + working-directory: ./testproj + run: python manage.py test + + pylint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v2 + - name: Install dependencies + working-directory: ./testproj + run: | + python -m pip install --upgrade pip + pip install pylint pylint-django django djangorestframework -r requirements-github.txt + - name: Analysing drf_messages with pylint + run: | + pylint windows_auth --load-plugins pylint_django --load-plugins pylint_django.checkers.migrations + - name: Analysing test project with pylint + run: | + pylint testproj/* --load-plugins pylint_django --load-plugins pylint_django.checkers.migrations --django-settings-module=testproj.settings diff --git a/.idea/django-windowsauth.iml b/.idea/django-windowsauth.iml index be58f17..051336c 100644 --- a/.idea/django-windowsauth.iml +++ b/.idea/django-windowsauth.iml @@ -1,8 +1,19 @@ + + + + + + - @@ -12,4 +23,7 @@ + + \ No newline at end of file diff --git a/.idea/runConfigurations/ldap_server_Dockerfile__Compose_Deployment.xml b/.idea/runConfigurations/ldap_server_Dockerfile__Compose_Deployment.xml new file mode 100644 index 0000000..eb9eb61 --- /dev/null +++ b/.idea/runConfigurations/ldap_server_Dockerfile__Compose_Deployment.xml @@ -0,0 +1,24 @@ + + + + + + + + + + \ No newline at end of file diff --git a/testproj/demo/tests.py b/testproj/demo/tests.py index cf6a719..7717b26 100644 --- a/testproj/demo/tests.py +++ b/testproj/demo/tests.py @@ -3,83 +3,100 @@ from django.test import TestCase, override_settings, RequestFactory from django.urls import reverse +from windows_auth.conf import wauth_settings +from windows_auth.ldap import get_ldap_manager from windows_auth.middleware import SimulateWindowsAuthMiddleware -from windows_auth.models import LDAPUser from windows_auth.settings import LDAPSettings class ModelTestCase(TestCase): + pass - def test_create_user(self): - user = LDAPUser.objects.create_user("EXAMPLE\\Administrator") - self.assertEqual(user.ldap.domain, "EXAMPLE") + # def test_create_user(self): + # user = LDAPUser.objects.create_user("EXAMPLE\\Administrator") + # self.assertEqual(user.ldap.domain, "EXAMPLE") class SettingsTestCase(TestCase): - def test_flag_settings(self): - settings = LDAPSettings( - SERVER="example.local", - SEARCH_BASE="DC=example,DC=local", - USERNAME="EXAMPLE\\django_sync", - PASSWORD="Aa123456!", - SUPERUSER_GROUPS=None, - STAFF_GROUPS=["List"], - ACTIVE_GROUPS=["Explicit"], + def test_flag_propagation(self): + ldap_settings = LDAPSettings( + SERVER="", + SEARCH_BASE="", + USERNAME="", + PASSWORD="", + SUPERUSER_GROUPS=["Super"], + STAFF_GROUPS=["Staff", "Duplicate"], + ACTIVE_GROUPS=["Active", "Duplicate"], FLAG_MAP={ - "extra": "Hello world!", + "extra": "Extra", }, ) - # check propagation - self.assertEqual(settings.get_flag_map(), { - "is_superuser": [], - "is_staff": ["List"], - "is_active": ["Explicit", "List"], - "extra": ["Hello world!"], - }) - # check without propagation - settings.PROPAGATE_GROUPS = False - self.assertEqual(settings.get_flag_map(), { - "is_superuser": [], - "is_staff": ["List"], - "is_active": ["Explicit"], - "extra": ["Hello world!"], - }) - # check unique - settings.PROPAGATE_GROUPS = True - settings.ACTIVE_GROUPS = ["Explicit", "List"] - self.assertEqual(settings.get_flag_map(), { - "is_superuser": [], - "is_staff": ["List"], - "is_active": ["Explicit", "List"], - "extra": ["Hello world!"], - }) + flag_map = ldap_settings.get_flag_map() + self.assertEqual(set(flag_map.get("is_superuser")), {"Super"}) + self.assertEqual(set(flag_map.get("is_staff")), {"Staff", "Super", "Duplicate"}) + self.assertEqual(set(flag_map.get("is_active")), {"Active", "Duplicate", "Staff", "Super"}) + self.assertEqual(set(flag_map.get("extra")), {"Extra"}) + + def test_flag_propagation_disabled(self): + ldap_settings = LDAPSettings( + SERVER="", + SEARCH_BASE="", + USERNAME="", + PASSWORD="", + SUPERUSER_GROUPS=["Super"], + STAFF_GROUPS=["Staff"], + ACTIVE_GROUPS=["Active"], + FLAG_MAP={ + "extra": "Extra", + }, + PROPAGATE_GROUPS=False, + ) + self.assertEqual( + ldap_settings.get_flag_map(), + dict( + is_superuser=["Super"], + is_staff=["Staff"], + is_active=["Active"], + extra=["Extra"], + ) + ) class MiddlewareTestCase(TestCase): + middleware_class = SimulateWindowsAuthMiddleware def setUp(self): self.factory = RequestFactory() - def pass_middleware(self, request): + @property + def middleware(self): get_response = mock.MagicMock() - middleware = SimulateWindowsAuthMiddleware(get_response) - return middleware(request) + return self.middleware_class(get_response) - @override_settings(DEBUG=True) + @override_settings(DEBUG=True, WAUTH_SIMULATE_USER="test_user") def test_auth_simulation(self): request = self.factory.get(reverse("demo:index")) - self.pass_middleware(request) - self.assertTrue(request.META.get("REMOTE_USER")) + self.middleware(request) + self.assertEqual(request.META.get("REMOTE_USER"), wauth_settings.WAUTH_SIMULATE_USER) - @override_settings(DEBUG=True) - def test_skip_simulation(self): - request = self.factory.get(reverse("demo:index"), REMOTE_ADDR="test") - self.pass_middleware(request) - self.assertEqual(request.META.get("REMOTE_USER"), "test") + # @override_settings(DEBUG=True) + # def test_skip_simulation(self, username="test_user"): + # """Keep existing REMOTE_USER header""" + # request = self.factory.get(reverse("demo:index"), REMOTE_ADDR=username) + # self.middleware(request) + # self.assertEqual(request.META.get("REMOTE_USER"), username) @override_settings(DEBUG=False) def test_bypass_auth(self): + """Bypass user simulation when not in debug""" request = self.factory.get(reverse("demo:index")) - self.pass_middleware(request) - self.assertFalse(request.META.get("REMOTE_USER")) + self.middleware(request) + self.assertEqual(request.META.get("REMOTE_USER"), None) + + +class ManagerTestCase(TestCase): + + def test_connection(self): + manager = get_ldap_manager("EXAMPLE") + self.assertTrue(manager.connection.bound) diff --git a/testproj/ldap_server/Dockerfile b/testproj/ldap_server/Dockerfile new file mode 100644 index 0000000..bbfbcbd --- /dev/null +++ b/testproj/ldap_server/Dockerfile @@ -0,0 +1,8 @@ +FROM osixia/openldap:1.5.0 +LABEL maintainer="Dan Yishai " + +ADD bootstrap /container/service/slapd/assets/config/bootstrap +ADD environment /container/environment/01-custom + +#HEALTHCHECK --interval=30s --timeout=15s --retries=10 --start-period=10s \ +# CMD ldapsearch -D 'cn=admin,dc=example,dc=org' -w Adm1n! -b 'dc=example,dc=org' || exit 1 \ No newline at end of file diff --git a/testproj/ldap_server/bootstrap/bla.txt b/testproj/ldap_server/bootstrap/bla.txt new file mode 100644 index 0000000..e69de29 diff --git a/testproj/ldap_server/environment/my-env.startup.yaml b/testproj/ldap_server/environment/my-env.startup.yaml new file mode 100644 index 0000000..117f189 --- /dev/null +++ b/testproj/ldap_server/environment/my-env.startup.yaml @@ -0,0 +1,46 @@ +# This is the default image startup configuration file +# this file defines environment variables used during the container **first start** in **startup files**. + +# This file is deleted right after startup files are processed for the first time, +# after that all these values will not be available in the container environment. +# This helps to keep your container configuration secret. +# more information : https://github.com/osixia/docker-light-baseimage + +# Required and used for new ldap server only +LDAP_ORGANISATION: Example Inc. +LDAP_DOMAIN: example.org +LDAP_ADMIN_PASSWORD: Adm1n! +LDAP_CONFIG_PASSWORD: c0nfig + +LDAP_READONLY_USER: true +LDAP_READONLY_USER_USERNAME: readonly +LDAP_READONLY_USER_PASSWORD: passwr0rd! + +# Tls +LDAP_TLS: true +LDAP_TLS_CRT_FILENAME: cert.crt +LDAP_TLS_KEY_FILENAME: cert.key +LDAP_TLS_DH_PARAM_FILENAME: dhparam.pem +LDAP_TLS_CA_CRT_FILENAME: ca.crt + +LDAP_TLS_ENFORCE: false +LDAP_TLS_CIPHER_SUITE: SECURE256:+SECURE128:-VERS-TLS-ALL:+VERS-TLS1.2:-RSA:-DHE-DSS:-CAMELLIA-128-CBC:-CAMELLIA-256-CBC +LDAP_TLS_VERIFY_CLIENT: never + +# Replication +LDAP_REPLICATION: false +# variables $LDAP_BASE_DN, $LDAP_ADMIN_PASSWORD, $LDAP_CONFIG_PASSWORD +# are automatically replaced at run time + +# if you want to add replication to an existing ldap +# adapt LDAP_REPLICATION_CONFIG_SYNCPROV and LDAP_REPLICATION_DB_SYNCPROV to your configuration +# avoid using $LDAP_BASE_DN, $LDAP_ADMIN_PASSWORD and $LDAP_CONFIG_PASSWORD variables +LDAP_REPLICATION_CONFIG_SYNCPROV: binddn="cn=admin,cn=config" bindmethod=simple credentials=$LDAP_CONFIG_PASSWORD searchbase="cn=config" type=refreshAndPersist retry="60 +" timeout=1 starttls=critical +LDAP_REPLICATION_DB_SYNCPROV: binddn="cn=admin,$LDAP_BASE_DN" bindmethod=simple credentials=$LDAP_ADMIN_PASSWORD searchbase="$LDAP_BASE_DN" type=refreshAndPersist interval=00:00:00:10 retry="60 +" timeout=1 starttls=critical +LDAP_REPLICATION_HOSTS: + - ldap://ldap.example.org # The order must be the same on all ldap servers + - ldap://ldap2.example.org + + +# Remove config after setup +LDAP_REMOVE_CONFIG_AFTER_SETUP: false diff --git a/testproj/ldap_server/environment/my-env.yaml b/testproj/ldap_server/environment/my-env.yaml new file mode 100644 index 0000000..b368f26 --- /dev/null +++ b/testproj/ldap_server/environment/my-env.yaml @@ -0,0 +1,10 @@ +# This is the default image configuration file +# These values will persists in container environment. + +# All environment variables used after the container first start +# must be defined here. +# more information : https://github.com/osixia/docker-light-baseimage + +# General container configuration +# see table 5.1 in http://www.openldap.org/doc/admin24/slapdconf2.html for the available log levels. +LDAP_LOG_LEVEL: 0 \ No newline at end of file diff --git a/testproj/requirements-github.txt b/testproj/requirements-github.txt new file mode 100644 index 0000000..4553019 --- /dev/null +++ b/testproj/requirements-github.txt @@ -0,0 +1,2 @@ +-e .. +django-debug-toolbar diff --git a/testproj/testproj/settings.py b/testproj/testproj/settings.py index 06c3312..66ef6eb 100644 --- a/testproj/testproj/settings.py +++ b/testproj/testproj/settings.py @@ -235,19 +235,20 @@ WAUTH_DOMAINS = { "EXAMPLE": LDAPSettings( - SERVER="example.local", - SEARCH_BASE="DC=example,DC=local", - USERNAME="EXAMPLE\\django_sync", - PASSWORD="Aa123456!", - USE_SSL=True, + SERVER="127.0.0.1", + SEARCH_BASE="DC=example,DC=org", + USERNAME="cn=admin,dc=example,dc=org", + PASSWORD="admin", + USE_SSL=False, COLLECT_METRICS=True, READ_ONLY=False, GROUP_MAP={ "demo": "Domain Admins", "demo2": "Domain Admins", }, - CONNECTION_OPTIONS={ - "authentication": ldap3.NTLM, - }, + USER_QUERY_FILTER={}, + # CONNECTION_OPTIONS={ + # "authentication": ldap3.NTLM, + # }, ) } diff --git a/windows_auth/apps.py b/windows_auth/apps.py index b4b5359..5ec7ed0 100644 --- a/windows_auth/apps.py +++ b/windows_auth/apps.py @@ -1,5 +1,6 @@ import atexit +from django.conf import settings from django.db import DatabaseError from ldap3.core.exceptions import LDAPException from django.apps import AppConfig @@ -13,7 +14,7 @@ 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.conf import wauth_settings from windows_auth.settings import DEFAULT_DOMAIN_SETTING from windows_auth.ldap import get_ldap_manager, close_connections @@ -23,11 +24,17 @@ def ready(self): # You can avoid this behavior by using "runserver --noreload" parameter, # or modifying the WAUTH_PRELOAD_DOMAINS setting to False. + if ( + not hasattr(settings, "WAUTH_DOMAINS") + and not getattr(settings, "WAUTH_IGNORE_SETTING_WARNINGS", False) + ): + logger.warn("The required setting WAUTH_DOMAINS is missing.") + # check about users with domain missing from settings - if not WAUTH_IGNORE_SETTING_WARNINGS and DEFAULT_DOMAIN_SETTING not in WAUTH_DOMAINS: + if not wauth_settings.WAUTH_IGNORE_SETTING_WARNINGS and DEFAULT_DOMAIN_SETTING not in wauth_settings.WAUTH_DOMAINS: try: from windows_auth.models import LDAPUser - missing_domains = LDAPUser.objects.exclude(domain__in=WAUTH_DOMAINS.keys()) + missing_domains = LDAPUser.objects.exclude(domain__in=wauth_settings.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 " @@ -37,9 +44,9 @@ def ready(self): logger.warn(e) # configure default preload domains - preload_domains = WAUTH_PRELOAD_DOMAINS + preload_domains = wauth_settings.WAUTH_PRELOAD_DOMAINS if preload_domains in (None, True): - preload_domains = list(WAUTH_DOMAINS.keys()) + preload_domains = list(wauth_settings.WAUTH_DOMAINS.keys()) if DEFAULT_DOMAIN_SETTING in preload_domains: preload_domains.remove(DEFAULT_DOMAIN_SETTING) diff --git a/windows_auth/backends.py b/windows_auth/backends.py index b314680..f3124d5 100644 --- a/windows_auth/backends.py +++ b/windows_auth/backends.py @@ -1,6 +1,6 @@ from django.contrib.auth.backends import RemoteUserBackend -from windows_auth.conf import WAUTH_USE_SPN, WAUTH_LOWERCASE_USERNAME +from windows_auth.conf import wauth_settings from windows_auth.models import LDAPUser @@ -19,12 +19,12 @@ def clean_username(self, username: str): :param username: raw REMOTE_USER header value :return: cleaned sAMAccountName value from the """ - if WAUTH_USE_SPN: + if wauth_settings.WAUTH_USE_SPN: sam_account_name, self.domain = username.rsplit("@", 2) else: self.domain, sam_account_name = username.split("\\", 2) - if WAUTH_LOWERCASE_USERNAME: + if wauth_settings.WAUTH_LOWERCASE_USERNAME: return str(sam_account_name).lower() else: return sam_account_name diff --git a/windows_auth/conf.py b/windows_auth/conf.py index fae5cee..2b21e50 100644 --- a/windows_auth/conf.py +++ b/windows_auth/conf.py @@ -1,34 +1,59 @@ -from typing import Callable, Union, Optional, Iterable +from dataclasses import dataclass, field, fields, Field +from typing import Callable, Union, Optional, Iterable, Dict, TYPE_CHECKING, Set from django.conf import settings +from django.core.signals import setting_changed +from django.dispatch import receiver from django.http import HttpResponse from django.utils import timezone -from windows_auth import logger - -if not hasattr(settings, "WAUTH_DOMAINS"): - logger.warn("The required setting WAUTH_DOMAINS is missing.") - -# Settings for each domain -WAUTH_DOMAINS: dict = getattr(settings, "WAUTH_DOMAINS", {}) - -# Expect REMOTE_USER value to be in SPN scheme -WAUTH_USE_SPN: bool = getattr(settings, "WAUTH_USE_SPN", False) -# Minimum time until automatic re-sync -WAUTH_RESYNC_DELTA: Optional[Union[str, int, timezone.timedelta]] = getattr(settings, "WAUTH_RESYNC_DELTA", - timezone.timedelta(days=1)) -# Use cache instead of model for determining re-sync -WAUTH_USE_CACHE: bool = getattr(settings, "WAUTH_USE_CACHE", False) -# Raise exception and return Error 500 when user failed to synced to domain -WAUTH_REQUIRE_RESYNC: bool = getattr(settings, "WAUTH_REQUIRE_RESYNC", settings.DEBUG or False) -# Choose custom HTTP Response to send when LDAP Sync fails -WAUTH_ERROR_RESPONSE: Optional[Union[int, HttpResponse, Callable]] = getattr(settings, "WAUTH_ERROR_RESPONSE", None) -# Lowercase the username from the REMOTE_USER. Used for correct non-case sensitive LDAP backends. -WAUTH_LOWERCASE_USERNAME: bool = getattr(settings, "WAUTH_LOWERCASE_USERNAME", True) -# Skip verification of domain settings on server startup -WAUTH_IGNORE_SETTING_WARNINGS: bool = getattr(settings, "WAUTH_IGNORE_SETTING_WARNINGS", False) -# List of domains to preload and connect during process startup -WAUTH_PRELOAD_DOMAINS: Optional[Iterable[str]] = getattr(settings, "WAUTH_PRELOAD_DOMAINS", None) -# User to impersonate when using SimulateWindowsAuthMiddleware -WAUTH_SIMULATE_USER: str = getattr(settings, "WAUTH_SIMULATE_USER", "") +if TYPE_CHECKING: + from windows_auth.settings import LDAPSettings + + +@dataclass() +class WAUTHSettings: + WAUTH_DOMAINS: Dict[str, Union["LDAPSettings"]] = field(default_factory=lambda: {}) + WAUTH_USE_SPN: bool = False + WAUTH_RESYNC_DELTA: Optional[Union[str, int, timezone.timedelta]] = timezone.timedelta(days=1) + WAUTH_USE_CACHE: bool = False + # Raise exception and return Error 500 when user failed to synced to domain + WAUTH_REQUIRE_RESYNC: bool = field(default_factory=lambda: settings.DEBUG) + WAUTH_ERROR_RESPONSE: Optional[Union[int, HttpResponse, Callable]] = None + # Lowercase the username from the REMOTE_USER. Used for connecting case-insensitive LDAP backends. + WAUTH_LOWERCASE_USERNAME: bool = True + # Skip verification of domain settings on server startup + WAUTH_IGNORE_SETTING_WARNINGS: bool = False + # List of domains to preload and connect during process startup + WAUTH_PRELOAD_DOMAINS: Optional[Iterable[str]] = None + # User to impersonate when using SimulateWindowsAuthMiddleware + WAUTH_SIMULATE_USER: str = "" + + @classmethod + def build_settings(cls): + return cls(**{ + f.name: getattr(settings, f.name) + for f in fields(cls) + if hasattr(settings, f.name) + }) + + def update_setting(self, name: str, value) -> None: + self.__setattr__(name, value) + + +if settings.configured: + wauth_settings = WAUTHSettings.build_settings() + + +@receiver(setting_changed) +def _update_setting(sender=None, setting=None, value=None, enter=None, **kwargs): + if not wauth_settings: + return + + if setting in map(lambda f: f.name, fields(WAUTHSettings)): + wauth_settings.update_setting(setting, value) + + # update bound settings + if setting == "DEBUG" and not hasattr(settings, "WAUTH_REQUIRE_RESYNC"): + wauth_settings.update_setting("WAUTH_REQUIRE_RESYNC", value) diff --git a/windows_auth/decorators.py b/windows_auth/decorators.py index a78069d..502a40e 100644 --- a/windows_auth/decorators.py +++ b/windows_auth/decorators.py @@ -1,7 +1,7 @@ from django.contrib.auth.decorators import user_passes_test from django.utils import timezone -from windows_auth.conf import WAUTH_USE_CACHE +from windows_auth.conf import wauth_settings from windows_auth.models import LDAPUser @@ -50,7 +50,7 @@ def ldap_sync_required(function=None, timedelta=None, login_url=None, allow_non_ def check_sync(user): if user.is_authenticated and LDAPUser.objects.filter(user=user).exists(): try: - if WAUTH_USE_CACHE: + if wauth_settings.WAUTH_USE_CACHE: user.ldap.sync() else: # check via database query diff --git a/windows_auth/middleware.py b/windows_auth/middleware.py index 7373704..e2f393a 100644 --- a/windows_auth/middleware.py +++ b/windows_auth/middleware.py @@ -4,8 +4,7 @@ from django.utils import timezone from windows_auth import logger -from windows_auth.conf import WAUTH_RESYNC_DELTA, WAUTH_USE_CACHE, WAUTH_REQUIRE_RESYNC, WAUTH_ERROR_RESPONSE, \ - WAUTH_SIMULATE_USER +from windows_auth.conf import wauth_settings from windows_auth.models import LDAPUser @@ -22,17 +21,17 @@ def __call__(self, request): """ if (request.user and request.user.is_authenticated - and LDAPUser.objects.filter(user=request.user).exists() and WAUTH_RESYNC_DELTA not in (None, False)): + and LDAPUser.objects.filter(user=request.user).exists() and wauth_settings.WAUTH_RESYNC_DELTA not in (None, False)): try: # convert timeout to seconds - if isinstance(WAUTH_RESYNC_DELTA, timezone.timedelta): - timeout = WAUTH_RESYNC_DELTA.total_seconds() + if isinstance(wauth_settings.WAUTH_RESYNC_DELTA, timezone.timedelta): + timeout = wauth_settings.WAUTH_RESYNC_DELTA.total_seconds() else: - timeout = int(WAUTH_RESYNC_DELTA) + timeout = int(wauth_settings.WAUTH_RESYNC_DELTA) ldap_user = LDAPUser.objects.get(user=request.user) - if WAUTH_USE_CACHE: + if wauth_settings.WAUTH_USE_CACHE: # if cache does not exist cache_key = f"wauth_resync_user_{ldap_user.user.id}" if not cache.get(cache_key): @@ -50,11 +49,11 @@ def __call__(self, request): except Exception as e: logger.exception(f"Failed to synchronize user {request.user} against LDAP") # return error response - if WAUTH_REQUIRE_RESYNC: - if isinstance(WAUTH_ERROR_RESPONSE, int): - return HttpResponse(f"Authorization Failed.", status=WAUTH_ERROR_RESPONSE) - elif callable(WAUTH_ERROR_RESPONSE): - return WAUTH_ERROR_RESPONSE(request, e) + if wauth_settings.WAUTH_REQUIRE_RESYNC: + if isinstance(wauth_settings.WAUTH_ERROR_RESPONSE, int): + return HttpResponse(f"Authorization Failed.", status=wauth_settings.WAUTH_ERROR_RESPONSE) + elif callable(wauth_settings.WAUTH_ERROR_RESPONSE): + return wauth_settings.WAUTH_ERROR_RESPONSE(request, e) else: raise e response = self.get_response(request) @@ -69,6 +68,6 @@ def __init__(self, get_response): def __call__(self, request: HttpRequest): if settings.DEBUG and not request.META.get("REMOTE_USER"): # Set remote user - request.META['REMOTE_USER'] = WAUTH_SIMULATE_USER + request.META['REMOTE_USER'] = wauth_settings.WAUTH_SIMULATE_USER return self.get_response(request) diff --git a/windows_auth/models.py b/windows_auth/models.py index 40bf340..7e74f15 100644 --- a/windows_auth/models.py +++ b/windows_auth/models.py @@ -9,7 +9,7 @@ from ldap3 import Reader, Entry, Attribute from windows_auth import logger -from windows_auth.conf import WAUTH_USE_CACHE, WAUTH_USE_SPN, WAUTH_LOWERCASE_USERNAME +from windows_auth.conf import wauth_settings from windows_auth.ldap import LDAPManager, get_ldap_manager from windows_auth.signals import ldap_user_sync from windows_auth.utils import LogExecutionTime @@ -40,7 +40,7 @@ def create_user(self, username: str) -> User: :param username: Logon username (DOMAIN\username or username@domain.com) :return: User object (not LDAPUser) """ - if WAUTH_USE_SPN: + if wauth_settings.WAUTH_USE_SPN: if "@" not in username: raise ValueError("Username must be in username@domain.com format.") @@ -51,7 +51,7 @@ def create_user(self, username: str) -> User: domain, sam_account_name = username.split("\\", 2) - if WAUTH_LOWERCASE_USERNAME: + if wauth_settings.WAUTH_LOWERCASE_USERNAME: sam_account_name = sam_account_name.lower() user = get_user_model().objects.create_user(username=sam_account_name) @@ -210,13 +210,13 @@ def sync(self) -> None: ldap_user_sync.send(self, ldap_user=ldap_user, group_reader=group_reader) # update sync time - if not WAUTH_USE_CACHE: + if not wauth_settings.WAUTH_USE_CACHE: with LogExecutionTime(f"Save LDAP User {self}"): self.last_sync = timezone.now() self.save() def __str__(self): - if WAUTH_USE_SPN: + if wauth_settings.WAUTH_USE_SPN: return f"{self.user.username}@{self.domain}" else: return f"{self.domain}\\{self.user.username}" diff --git a/windows_auth/settings.py b/windows_auth/settings.py index 8f47c17..0149b87 100644 --- a/windows_auth/settings.py +++ b/windows_auth/settings.py @@ -57,17 +57,17 @@ class LDAPSettings: @classmethod def for_domain(cls, domain: str): - from windows_auth.conf import WAUTH_DOMAINS + from windows_auth.conf import wauth_settings - if domain not in WAUTH_DOMAINS and DEFAULT_DOMAIN_SETTING not in WAUTH_DOMAINS: + if domain not in wauth_settings.WAUTH_DOMAINS and DEFAULT_DOMAIN_SETTING not in wauth_settings.WAUTH_DOMAINS: raise ImproperlyConfigured(f"Domain {domain} settings could not be found in WAUTH_DOMAINS setting.") - domain_settings = WAUTH_DOMAINS.get(domain, {}) + domain_settings = wauth_settings.WAUTH_DOMAINS.get(domain, {}) # when setting is an LDAPSetting object if isinstance(domain_settings, LDAPSettings): return domain_settings - default_settings = WAUTH_DOMAINS.get(DEFAULT_DOMAIN_SETTING, {}) + default_settings = wauth_settings.WAUTH_DOMAINS.get(DEFAULT_DOMAIN_SETTING, {}) # when domain setting if isinstance(default_settings, LDAPSettings): default_settings = asdict(default_settings)