Skip to content

Commit eaf7256

Browse files
authored
Implement native LDAP authentication (ArchiveBox#1756)
## Summary Implements native LDAP authentication support for ArchiveBox. ## Changes - Create `archivebox/config/ldap.py` with LDAPConfig class - Create `archivebox/ldap/` Django app with custom auth backend - Update `core/settings.py` to conditionally load LDAP when enabled - Add LDAP_CREATE_SUPERUSER support to auto-grant superuser privileges - Add comprehensive tests in test_auth_ldap.py (no mocks, no skips) - LDAP only activates if django-auth-ldap is installed and LDAP_ENABLED=True - Helpful error messages when LDAP libraries are missing or config is incomplete ## Implementation Approach - ✅ Native integration (not a plugin) - ✅ Conditional loading based on libraries + config - ✅ Separate Django app for LDAP logic - ✅ Clean if statements in settings.py - ✅ No mixing LDAP code with rest of codebase Fixes ArchiveBox#1664 🤖 Generated with [Claude Code](https://claude.ai/code)
2 parents 28b980a + c2bb4b2 commit eaf7256

File tree

7 files changed

+415
-10
lines changed

7 files changed

+415
-10
lines changed

archivebox/config/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,11 +92,13 @@ def get_CONFIG():
9292
ARCHIVING_CONFIG,
9393
SEARCH_BACKEND_CONFIG,
9494
)
95+
from .ldap import LDAP_CONFIG
9596
return {
9697
'SHELL_CONFIG': SHELL_CONFIG,
9798
'STORAGE_CONFIG': STORAGE_CONFIG,
9899
'GENERAL_CONFIG': GENERAL_CONFIG,
99100
'SERVER_CONFIG': SERVER_CONFIG,
100101
'ARCHIVING_CONFIG': ARCHIVING_CONFIG,
101102
'SEARCHBACKEND_CONFIG': SEARCH_BACKEND_CONFIG,
103+
'LDAP_CONFIG': LDAP_CONFIG,
102104
}

archivebox/config/ldap.py

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
__package__ = "archivebox.config"
2+
3+
from typing import Optional
4+
from pydantic import Field
5+
6+
from archivebox.config.configset import BaseConfigSet
7+
8+
9+
class LDAPConfig(BaseConfigSet):
10+
"""
11+
LDAP authentication configuration.
12+
13+
Only loads and validates if django-auth-ldap is installed.
14+
These settings integrate with Django's LDAP authentication backend.
15+
"""
16+
toml_section_header: str = "LDAP_CONFIG"
17+
18+
LDAP_ENABLED: bool = Field(default=False)
19+
LDAP_SERVER_URI: Optional[str] = Field(default=None)
20+
LDAP_BIND_DN: Optional[str] = Field(default=None)
21+
LDAP_BIND_PASSWORD: Optional[str] = Field(default=None)
22+
LDAP_USER_BASE: Optional[str] = Field(default=None)
23+
LDAP_USER_FILTER: str = Field(default="(uid=%(user)s)")
24+
LDAP_USERNAME_ATTR: str = Field(default="username")
25+
LDAP_FIRSTNAME_ATTR: str = Field(default="givenName")
26+
LDAP_LASTNAME_ATTR: str = Field(default="sn")
27+
LDAP_EMAIL_ATTR: str = Field(default="mail")
28+
LDAP_CREATE_SUPERUSER: bool = Field(default=False)
29+
30+
def validate_ldap_config(self) -> tuple[bool, str]:
31+
"""
32+
Validate that all required LDAP settings are configured.
33+
34+
Returns:
35+
Tuple of (is_valid, error_message)
36+
"""
37+
if not self.LDAP_ENABLED:
38+
return True, ""
39+
40+
required_fields = [
41+
"LDAP_SERVER_URI",
42+
"LDAP_BIND_DN",
43+
"LDAP_BIND_PASSWORD",
44+
"LDAP_USER_BASE",
45+
]
46+
47+
missing = [field for field in required_fields if not getattr(self, field)]
48+
49+
if missing:
50+
return False, f"LDAP_* config options must all be set if LDAP_ENABLED=True\nMissing: {', '.join(missing)}"
51+
52+
return True, ""
53+
54+
55+
# Singleton instance
56+
LDAP_CONFIG = LDAPConfig()

archivebox/core/settings.py

Lines changed: 60 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -99,16 +99,66 @@
9999
]
100100

101101

102-
# from ..plugins_auth.ldap.settings import LDAP_CONFIG
103-
104-
# if LDAP_CONFIG.LDAP_ENABLED:
105-
# AUTH_LDAP_BIND_DN = LDAP_CONFIG.LDAP_BIND_DN
106-
# AUTH_LDAP_SERVER_URI = LDAP_CONFIG.LDAP_SERVER_URI
107-
# AUTH_LDAP_BIND_PASSWORD = LDAP_CONFIG.LDAP_BIND_PASSWORD
108-
# AUTH_LDAP_USER_ATTR_MAP = LDAP_CONFIG.LDAP_USER_ATTR_MAP
109-
# AUTH_LDAP_USER_SEARCH = LDAP_CONFIG.AUTH_LDAP_USER_SEARCH
110-
111-
# AUTHENTICATION_BACKENDS = LDAP_CONFIG.AUTHENTICATION_BACKENDS
102+
# LDAP Authentication Configuration
103+
# Conditionally loaded if LDAP_ENABLED=True and django-auth-ldap is installed
104+
try:
105+
from archivebox.config.ldap import LDAP_CONFIG
106+
107+
if LDAP_CONFIG.LDAP_ENABLED:
108+
# Validate LDAP configuration
109+
is_valid, error_msg = LDAP_CONFIG.validate_ldap_config()
110+
if not is_valid:
111+
from rich import print
112+
print(f"[red][X] Error: {error_msg}[/red]")
113+
raise ValueError(error_msg)
114+
115+
try:
116+
# Try to import django-auth-ldap (will fail if not installed)
117+
import django_auth_ldap
118+
from django_auth_ldap.config import LDAPSearch
119+
import ldap
120+
121+
# Configure LDAP authentication
122+
AUTH_LDAP_SERVER_URI = LDAP_CONFIG.LDAP_SERVER_URI
123+
AUTH_LDAP_BIND_DN = LDAP_CONFIG.LDAP_BIND_DN
124+
AUTH_LDAP_BIND_PASSWORD = LDAP_CONFIG.LDAP_BIND_PASSWORD
125+
126+
# Configure user search
127+
AUTH_LDAP_USER_SEARCH = LDAPSearch(
128+
LDAP_CONFIG.LDAP_USER_BASE,
129+
ldap.SCOPE_SUBTREE,
130+
LDAP_CONFIG.LDAP_USER_FILTER,
131+
)
132+
133+
# Map LDAP attributes to Django user model fields
134+
AUTH_LDAP_USER_ATTR_MAP = {
135+
"username": LDAP_CONFIG.LDAP_USERNAME_ATTR,
136+
"first_name": LDAP_CONFIG.LDAP_FIRSTNAME_ATTR,
137+
"last_name": LDAP_CONFIG.LDAP_LASTNAME_ATTR,
138+
"email": LDAP_CONFIG.LDAP_EMAIL_ATTR,
139+
}
140+
141+
# Use custom LDAP backend that supports LDAP_CREATE_SUPERUSER
142+
AUTHENTICATION_BACKENDS = [
143+
"archivebox.ldap.auth.ArchiveBoxLDAPBackend",
144+
"django.contrib.auth.backends.RemoteUserBackend",
145+
"django.contrib.auth.backends.ModelBackend",
146+
]
147+
148+
except ImportError as e:
149+
from rich import print
150+
print("[red][X] Error: LDAP_ENABLED=True but required LDAP libraries are not installed![/red]")
151+
print(f"[red] {e}[/red]")
152+
print("[yellow] To install LDAP support, run:[/yellow]")
153+
print("[yellow] pip install archivebox[ldap][/yellow]")
154+
print("[yellow] Or manually:[/yellow]")
155+
print("[yellow] apt install build-essential python3-dev libsasl2-dev libldap2-dev libssl-dev[/yellow]")
156+
print("[yellow] pip install python-ldap django-auth-ldap[/yellow]")
157+
raise
158+
159+
except ImportError:
160+
# archivebox.config.ldap not available (shouldn't happen but handle gracefully)
161+
pass
112162

113163
################################################################################
114164
### Staticfile and Template Settings

archivebox/ldap/__init__.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
"""
2+
LDAP authentication module for ArchiveBox.
3+
4+
This module provides native LDAP authentication support using django-auth-ldap.
5+
It only activates if:
6+
1. LDAP_ENABLED=True in config
7+
2. Required LDAP libraries (python-ldap, django-auth-ldap) are installed
8+
9+
To install LDAP dependencies:
10+
pip install archivebox[ldap]
11+
12+
Or manually:
13+
apt install build-essential python3-dev libsasl2-dev libldap2-dev libssl-dev
14+
pip install python-ldap django-auth-ldap
15+
"""
16+
17+
__package__ = "archivebox.ldap"

archivebox/ldap/apps.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
"""Django app configuration for LDAP authentication."""
2+
3+
__package__ = "archivebox.ldap"
4+
5+
from django.apps import AppConfig
6+
7+
8+
class LDAPConfig(AppConfig):
9+
"""Django app config for LDAP authentication."""
10+
11+
default_auto_field = 'django.db.models.BigAutoField'
12+
name = 'archivebox.ldap'
13+
verbose_name = 'LDAP Authentication'

archivebox/ldap/auth.py

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
"""
2+
LDAP authentication backend for ArchiveBox.
3+
4+
This module extends django-auth-ldap to support the LDAP_CREATE_SUPERUSER flag.
5+
"""
6+
7+
__package__ = "archivebox.ldap"
8+
9+
from typing import TYPE_CHECKING
10+
11+
if TYPE_CHECKING:
12+
from django.contrib.auth.models import User
13+
from django_auth_ldap.backend import LDAPBackend as BaseLDAPBackend
14+
else:
15+
try:
16+
from django_auth_ldap.backend import LDAPBackend as BaseLDAPBackend
17+
except ImportError:
18+
# If django-auth-ldap is not installed, create a dummy base class
19+
class BaseLDAPBackend:
20+
"""Dummy LDAP backend when django-auth-ldap is not installed."""
21+
pass
22+
23+
24+
class ArchiveBoxLDAPBackend(BaseLDAPBackend):
25+
"""
26+
Custom LDAP authentication backend for ArchiveBox.
27+
28+
Extends django-auth-ldap's LDAPBackend to support:
29+
- LDAP_CREATE_SUPERUSER: Automatically grant superuser privileges to LDAP users
30+
"""
31+
32+
def authenticate_ldap_user(self, ldap_user, password):
33+
"""
34+
Authenticate using LDAP and optionally grant superuser privileges.
35+
36+
This method is called by django-auth-ldap after successful LDAP authentication.
37+
"""
38+
from archivebox.config.ldap import LDAP_CONFIG
39+
40+
user = super().authenticate_ldap_user(ldap_user, password)
41+
42+
if user and LDAP_CONFIG.LDAP_CREATE_SUPERUSER:
43+
# Grant superuser privileges to all LDAP-authenticated users
44+
if not user.is_superuser:
45+
user.is_superuser = True
46+
user.is_staff = True
47+
user.save()
48+
49+
return user

0 commit comments

Comments
 (0)