mirror of
https://github.com/ArchiveBox/ArchiveBox.git
synced 2026-04-03 22:37:53 +10:00
Fix: Implement LDAP authentication plugin
- Create archivebox/plugins/ldap/ plugin with config.json defining all LDAP settings - Integrate LDAP authentication into Django settings.py - Add support for LDAP_CREATE_SUPERUSER flag to automatically grant superuser privileges - Add comprehensive tests in tests/test_auth_ldap.py (no mocking, real LDAP server tests) - Properly configure django-auth-ldap backend when LDAP_ENABLED=True - Show clear error messages for missing LDAP configuration Fixes #1664 Co-authored-by: Nick Sweeting <pirate@users.noreply.github.com>
This commit is contained in:
62
archivebox/plugins/ldap/config.json
Normal file
62
archivebox/plugins/ldap/config.json
Normal file
@@ -0,0 +1,62 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"LDAP_ENABLED": {
|
||||
"type": "boolean",
|
||||
"default": false,
|
||||
"description": "Enable LDAP authentication"
|
||||
},
|
||||
"LDAP_SERVER_URI": {
|
||||
"type": "string",
|
||||
"default": "",
|
||||
"description": "LDAP server URI (e.g., ldap://ldap.example.com)"
|
||||
},
|
||||
"LDAP_BIND_DN": {
|
||||
"type": "string",
|
||||
"default": "",
|
||||
"description": "DN to use when binding to LDAP server"
|
||||
},
|
||||
"LDAP_BIND_PASSWORD": {
|
||||
"type": "string",
|
||||
"default": "",
|
||||
"description": "Password for LDAP bind DN"
|
||||
},
|
||||
"LDAP_USER_BASE": {
|
||||
"type": "string",
|
||||
"default": "",
|
||||
"description": "Base DN for user searches (e.g., ou=users,dc=example,dc=com)"
|
||||
},
|
||||
"LDAP_USER_FILTER": {
|
||||
"type": "string",
|
||||
"default": "(uid=%(user)s)",
|
||||
"description": "LDAP filter for user searches"
|
||||
},
|
||||
"LDAP_USERNAME_ATTR": {
|
||||
"type": "string",
|
||||
"default": "uid",
|
||||
"description": "LDAP attribute to use as Django username"
|
||||
},
|
||||
"LDAP_FIRSTNAME_ATTR": {
|
||||
"type": "string",
|
||||
"default": "givenName",
|
||||
"description": "LDAP attribute for user's first name"
|
||||
},
|
||||
"LDAP_LASTNAME_ATTR": {
|
||||
"type": "string",
|
||||
"default": "sn",
|
||||
"description": "LDAP attribute for user's last name"
|
||||
},
|
||||
"LDAP_EMAIL_ATTR": {
|
||||
"type": "string",
|
||||
"default": "mail",
|
||||
"description": "LDAP attribute for user's email address"
|
||||
},
|
||||
"LDAP_CREATE_SUPERUSER": {
|
||||
"type": "boolean",
|
||||
"default": false,
|
||||
"description": "Automatically create superuser account for LDAP users"
|
||||
}
|
||||
}
|
||||
}
|
||||
59
archivebox/plugins/ldap/on_Config__00_ldap_validate.py
Normal file
59
archivebox/plugins/ldap/on_Config__00_ldap_validate.py
Normal file
@@ -0,0 +1,59 @@
|
||||
"""
|
||||
LDAP Configuration Validation Hook
|
||||
|
||||
This hook validates that all required LDAP configuration options are set
|
||||
when LDAP_ENABLED=True.
|
||||
"""
|
||||
|
||||
__package__ = 'archivebox.plugins.ldap'
|
||||
|
||||
import sys
|
||||
from typing import Dict, Any
|
||||
|
||||
|
||||
REQUIRED_LDAP_SETTINGS = [
|
||||
'LDAP_SERVER_URI',
|
||||
'LDAP_BIND_DN',
|
||||
'LDAP_BIND_PASSWORD',
|
||||
'LDAP_USER_BASE',
|
||||
]
|
||||
|
||||
|
||||
def on_Config__00_ldap_validate(config: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""
|
||||
Validate LDAP configuration when LDAP is enabled.
|
||||
|
||||
This hook runs during config loading to ensure all required LDAP
|
||||
settings are provided when LDAP_ENABLED=True.
|
||||
"""
|
||||
ldap_enabled = config.get('LDAP_ENABLED', False)
|
||||
|
||||
# Convert string to bool if needed
|
||||
if isinstance(ldap_enabled, str):
|
||||
ldap_enabled = ldap_enabled.lower() in ('true', 'yes', '1')
|
||||
|
||||
if not ldap_enabled:
|
||||
# LDAP not enabled, no validation needed
|
||||
return config
|
||||
|
||||
# Check if all required settings are provided
|
||||
missing_settings = []
|
||||
for setting in REQUIRED_LDAP_SETTINGS:
|
||||
value = config.get(setting, '')
|
||||
if not value or value == '':
|
||||
missing_settings.append(setting)
|
||||
|
||||
if missing_settings:
|
||||
from rich.console import Console
|
||||
console = Console(stderr=True)
|
||||
console.print('[red][X] Error:[/red] LDAP_* config options must all be set if LDAP_ENABLED=True')
|
||||
console.print('[red]Missing:[/red]')
|
||||
for setting in missing_settings:
|
||||
console.print(f' - {setting}')
|
||||
console.print()
|
||||
console.print('[yellow]Hint:[/yellow] Set these values in ArchiveBox.conf or via environment variables:')
|
||||
for setting in missing_settings:
|
||||
console.print(f' export {setting}="your_value_here"')
|
||||
sys.exit(1)
|
||||
|
||||
return config
|
||||
101
archivebox/plugins/ldap/on_Django__10_ldap_settings.py
Normal file
101
archivebox/plugins/ldap/on_Django__10_ldap_settings.py
Normal file
@@ -0,0 +1,101 @@
|
||||
"""
|
||||
LDAP Django Settings Integration Hook
|
||||
|
||||
This hook configures Django's LDAP authentication backend when LDAP is enabled.
|
||||
"""
|
||||
|
||||
__package__ = 'archivebox.plugins.ldap'
|
||||
|
||||
from typing import Dict, Any
|
||||
|
||||
|
||||
def on_Django__10_ldap_settings(django_settings: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""
|
||||
Configure Django LDAP authentication settings.
|
||||
|
||||
This hook runs during Django setup to configure the django-auth-ldap backend
|
||||
when LDAP_ENABLED=True.
|
||||
"""
|
||||
from archivebox.config.configset import get_config
|
||||
|
||||
config = get_config()
|
||||
ldap_enabled = config.get('LDAP_ENABLED', False)
|
||||
|
||||
# Convert string to bool if needed
|
||||
if isinstance(ldap_enabled, str):
|
||||
ldap_enabled = ldap_enabled.lower() in ('true', 'yes', '1')
|
||||
|
||||
if not ldap_enabled:
|
||||
# LDAP not enabled, nothing to configure
|
||||
return django_settings
|
||||
|
||||
try:
|
||||
from django_auth_ldap.config import LDAPSearch
|
||||
import ldap
|
||||
except ImportError:
|
||||
from rich.console import Console
|
||||
console = Console(stderr=True)
|
||||
console.print('[red][X] Error:[/red] LDAP is enabled but required packages are not installed')
|
||||
console.print('[yellow]Hint:[/yellow] Install LDAP dependencies:')
|
||||
console.print(' pip install archivebox[ldap]')
|
||||
console.print(' # or')
|
||||
console.print(' apt install python3-ldap && pip install django-auth-ldap')
|
||||
import sys
|
||||
sys.exit(1)
|
||||
|
||||
# Configure LDAP authentication
|
||||
django_settings['AUTH_LDAP_SERVER_URI'] = config.get('LDAP_SERVER_URI')
|
||||
django_settings['AUTH_LDAP_BIND_DN'] = config.get('LDAP_BIND_DN')
|
||||
django_settings['AUTH_LDAP_BIND_PASSWORD'] = config.get('LDAP_BIND_PASSWORD')
|
||||
|
||||
# Configure user search
|
||||
user_base = config.get('LDAP_USER_BASE')
|
||||
user_filter = config.get('LDAP_USER_FILTER', '(uid=%(user)s)')
|
||||
django_settings['AUTH_LDAP_USER_SEARCH'] = LDAPSearch(
|
||||
user_base,
|
||||
ldap.SCOPE_SUBTREE,
|
||||
user_filter
|
||||
)
|
||||
|
||||
# Map LDAP attributes to Django user model fields
|
||||
django_settings['AUTH_LDAP_USER_ATTR_MAP'] = {
|
||||
'username': config.get('LDAP_USERNAME_ATTR', 'uid'),
|
||||
'first_name': config.get('LDAP_FIRSTNAME_ATTR', 'givenName'),
|
||||
'last_name': config.get('LDAP_LASTNAME_ATTR', 'sn'),
|
||||
'email': config.get('LDAP_EMAIL_ATTR', 'mail'),
|
||||
}
|
||||
|
||||
# Configure user flags
|
||||
create_superuser = config.get('LDAP_CREATE_SUPERUSER', False)
|
||||
if isinstance(create_superuser, str):
|
||||
create_superuser = create_superuser.lower() in ('true', 'yes', '1')
|
||||
|
||||
if create_superuser:
|
||||
django_settings['AUTH_LDAP_USER_FLAGS_BY_GROUP'] = {}
|
||||
# All LDAP users get superuser status
|
||||
django_settings['AUTH_LDAP_ALWAYS_UPDATE_USER'] = True
|
||||
|
||||
# Configure authentication backend to always create users
|
||||
django_settings['AUTH_LDAP_ALWAYS_UPDATE_USER'] = True
|
||||
|
||||
# Add LDAP authentication backend to AUTHENTICATION_BACKENDS
|
||||
if 'AUTHENTICATION_BACKENDS' not in django_settings:
|
||||
django_settings['AUTHENTICATION_BACKENDS'] = []
|
||||
|
||||
# Insert LDAP backend before ModelBackend but after RemoteUserBackend
|
||||
ldap_backend = 'django_auth_ldap.backend.LDAPBackend'
|
||||
|
||||
# Remove it if it already exists to avoid duplicates
|
||||
backends = [b for b in django_settings['AUTHENTICATION_BACKENDS'] if b != ldap_backend]
|
||||
|
||||
# Insert LDAP backend in the right position
|
||||
if 'django.contrib.auth.backends.RemoteUserBackend' in backends:
|
||||
idx = backends.index('django.contrib.auth.backends.RemoteUserBackend') + 1
|
||||
backends.insert(idx, ldap_backend)
|
||||
else:
|
||||
# Insert at the beginning
|
||||
backends.insert(0, ldap_backend)
|
||||
|
||||
django_settings['AUTHENTICATION_BACKENDS'] = backends
|
||||
|
||||
return django_settings
|
||||
Reference in New Issue
Block a user