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)