mirror of
https://git.vectorsigma.ru/public/tubearchivist.git
synced 2026-08-04 19:09:29 +00:00
Configurable LDAP user promotion to superuser or staff (#1000)
* refactor: segregated ldap and fwd auth settings into imports and added variable validations * added: LDAP users listed in configuration variables are promoted to staff or superuser
This commit is contained in:
6
backend/config/fwd_auth_settings.py
Normal file
6
backend/config/fwd_auth_settings.py
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
from os import environ
|
||||||
|
|
||||||
|
TA_AUTH_PROXY_USERNAME_HEADER = (
|
||||||
|
environ.get("TA_AUTH_PROXY_USERNAME_HEADER") or "HTTP_REMOTE_USER"
|
||||||
|
)
|
||||||
|
TA_AUTH_PROXY_LOGOUT_URL = environ.get("TA_AUTH_PROXY_LOGOUT_URL")
|
||||||
111
backend/config/ldap_settings.py
Normal file
111
backend/config/ldap_settings.py
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
from os import environ
|
||||||
|
|
||||||
|
import ldap
|
||||||
|
from django_auth_ldap.config import LDAPSearch
|
||||||
|
|
||||||
|
AUTH_LDAP_SERVER_URI = environ.get("TA_LDAP_SERVER_URI")
|
||||||
|
|
||||||
|
AUTH_LDAP_BIND_DN = environ.get("TA_LDAP_BIND_DN")
|
||||||
|
|
||||||
|
AUTH_LDAP_BIND_PASSWORD = environ.get("TA_LDAP_BIND_PASSWORD")
|
||||||
|
|
||||||
|
"""
|
||||||
|
Given Names are *_technically_* different from Personal names, as people
|
||||||
|
who change their names have different given names and personal names,
|
||||||
|
and they go by personal names. Additionally, "LastName" is actually
|
||||||
|
incorrect for many cultures, such as Korea, where the
|
||||||
|
family name comes first, and the personal name comes last.
|
||||||
|
|
||||||
|
But we all know people are going to try to guess at these, so still want
|
||||||
|
to include names that people will guess, hence using first/last as well.
|
||||||
|
"""
|
||||||
|
|
||||||
|
AUTH_LDAP_USER_ATTR_MAP_USERNAME = (
|
||||||
|
environ.get("TA_LDAP_USER_ATTR_MAP_USERNAME")
|
||||||
|
or environ.get("TA_LDAP_USER_ATTR_MAP_UID")
|
||||||
|
or "uid"
|
||||||
|
)
|
||||||
|
|
||||||
|
AUTH_LDAP_USER_ATTR_MAP_PERSONALNAME = (
|
||||||
|
environ.get("TA_LDAP_USER_ATTR_MAP_PERSONALNAME")
|
||||||
|
or environ.get("TA_LDAP_USER_ATTR_MAP_FIRSTNAME")
|
||||||
|
or environ.get("TA_LDAP_USER_ATTR_MAP_GIVENNAME")
|
||||||
|
or "givenName"
|
||||||
|
)
|
||||||
|
|
||||||
|
AUTH_LDAP_USER_ATTR_MAP_SURNAME = (
|
||||||
|
environ.get("TA_LDAP_USER_ATTR_MAP_SURNAME")
|
||||||
|
or environ.get("TA_LDAP_USER_ATTR_MAP_LASTNAME")
|
||||||
|
or environ.get("TA_LDAP_USER_ATTR_MAP_FAMILYNAME")
|
||||||
|
or "sn"
|
||||||
|
)
|
||||||
|
|
||||||
|
AUTH_LDAP_USER_ATTR_MAP_EMAIL = (
|
||||||
|
environ.get("TA_LDAP_USER_ATTR_MAP_EMAIL")
|
||||||
|
or environ.get("TA_LDAP_USER_ATTR_MAP_MAIL")
|
||||||
|
or "mail"
|
||||||
|
)
|
||||||
|
|
||||||
|
AUTH_LDAP_USER_BASE = environ.get("TA_LDAP_USER_BASE")
|
||||||
|
|
||||||
|
AUTH_LDAP_USER_FILTER = environ.get("TA_LDAP_USER_FILTER")
|
||||||
|
|
||||||
|
# pylint: disable=no-member
|
||||||
|
AUTH_LDAP_USER_SEARCH = LDAPSearch(
|
||||||
|
AUTH_LDAP_USER_BASE,
|
||||||
|
ldap.SCOPE_SUBTREE,
|
||||||
|
"(&("
|
||||||
|
+ AUTH_LDAP_USER_ATTR_MAP_USERNAME
|
||||||
|
+ "=%(user)s)"
|
||||||
|
+ AUTH_LDAP_USER_FILTER
|
||||||
|
+ ")",
|
||||||
|
)
|
||||||
|
|
||||||
|
AUTH_LDAP_USER_ATTR_MAP = {
|
||||||
|
"username": AUTH_LDAP_USER_ATTR_MAP_USERNAME,
|
||||||
|
"first_name": AUTH_LDAP_USER_ATTR_MAP_PERSONALNAME,
|
||||||
|
"last_name": AUTH_LDAP_USER_ATTR_MAP_SURNAME,
|
||||||
|
"email": AUTH_LDAP_USER_ATTR_MAP_EMAIL,
|
||||||
|
}
|
||||||
|
|
||||||
|
if bool(environ.get("TA_LDAP_DISABLE_CERT_CHECK")):
|
||||||
|
# pylint: disable=global-at-module-level
|
||||||
|
global AUTH_LDAP_GLOBAL_OPTIONS
|
||||||
|
AUTH_LDAP_GLOBAL_OPTIONS = {
|
||||||
|
ldap.OPT_X_TLS_REQUIRE_CERT: ldap.OPT_X_TLS_NEVER,
|
||||||
|
}
|
||||||
|
|
||||||
|
# Promote specific usernames to staff or superuser permission levels
|
||||||
|
_ldap_superuser_username_config = (
|
||||||
|
environ.get("TA_LDAP_PROMOTE_USERNAMES_TO_SUPERUSER") or ""
|
||||||
|
)
|
||||||
|
_ldap_superuser_usernames = []
|
||||||
|
if _ldap_superuser_username_config:
|
||||||
|
_ldap_superuser_usernames = [
|
||||||
|
u.strip() for u in _ldap_superuser_username_config.split(",")
|
||||||
|
]
|
||||||
|
|
||||||
|
_ldap_staff_username_config = (
|
||||||
|
environ.get("TA_LDAP_PROMOTE_USERNAMES_TO_STAFF") or ""
|
||||||
|
)
|
||||||
|
_ldap_staff_usernames = []
|
||||||
|
if _ldap_staff_username_config:
|
||||||
|
_ldap_staff_usernames = [
|
||||||
|
u.strip() for u in _ldap_staff_username_config.split(",")
|
||||||
|
]
|
||||||
|
|
||||||
|
if _ldap_staff_usernames or _ldap_superuser_usernames:
|
||||||
|
import django_auth_ldap.backend
|
||||||
|
|
||||||
|
def create_user(sender, user=None, ldap_user=None, **kwargs):
|
||||||
|
if user.ldap_username in _ldap_superuser_usernames and not (
|
||||||
|
user.is_superuser and user.is_staff
|
||||||
|
):
|
||||||
|
user.is_staff = True
|
||||||
|
user.is_superuser = True
|
||||||
|
user.save()
|
||||||
|
elif user.ldap_username in _ldap_staff_usernames and not user.is_staff:
|
||||||
|
user.is_staff = True
|
||||||
|
user.save()
|
||||||
|
|
||||||
|
django_auth_ldap.backend.populate_user.connect(create_user)
|
||||||
@@ -81,6 +81,7 @@ class Command(BaseCommand):
|
|||||||
"""run all commands"""
|
"""run all commands"""
|
||||||
self.stdout.write(LOGO)
|
self.stdout.write(LOGO)
|
||||||
self.stdout.write(TOPIC)
|
self.stdout.write(TOPIC)
|
||||||
|
self._additional_auth_vars_expectations()
|
||||||
self._expected_vars()
|
self._expected_vars()
|
||||||
self._unexpected_vars()
|
self._unexpected_vars()
|
||||||
self._elastic_user_overwrite()
|
self._elastic_user_overwrite()
|
||||||
@@ -89,6 +90,50 @@ class Command(BaseCommand):
|
|||||||
self._disable_static_auth()
|
self._disable_static_auth()
|
||||||
self._create_superuser()
|
self._create_superuser()
|
||||||
|
|
||||||
|
def _additional_auth_vars_expectations(self):
|
||||||
|
"""conditionally add additional expectations for auth modes"""
|
||||||
|
ldap_required_env = [
|
||||||
|
"TA_LDAP_SERVER_URI",
|
||||||
|
"TA_LDAP_BIND_DN",
|
||||||
|
"TA_LDAP_BIND_PASSWORD",
|
||||||
|
"TA_LDAP_USER_BASE",
|
||||||
|
"TA_LDAP_USER_FILTER",
|
||||||
|
]
|
||||||
|
_login_auth_mode = (
|
||||||
|
os.environ.get("TA_LOGIN_AUTH_MODE") or "single"
|
||||||
|
).casefold()
|
||||||
|
if _login_auth_mode == "local":
|
||||||
|
UNEXPECTED_ENV_VARS["TA_LDAP"] = (
|
||||||
|
"TA_LDAP is not valid with current auth mode"
|
||||||
|
)
|
||||||
|
UNEXPECTED_ENV_VARS["TA_ENABLE_AUTH_PROXY"] = (
|
||||||
|
"TA_ENABLE_AUTH_PROXY is not valid with current auth mode"
|
||||||
|
)
|
||||||
|
elif _login_auth_mode == "ldap":
|
||||||
|
EXPECTED_ENV_VARS.extend(ldap_required_env)
|
||||||
|
UNEXPECTED_ENV_VARS["TA_ENABLE_AUTH_PROXY"] = (
|
||||||
|
"TA_ENABLE_AUTH_PROXY is not valid with current auth mode"
|
||||||
|
)
|
||||||
|
elif _login_auth_mode == "forwardauth":
|
||||||
|
UNEXPECTED_ENV_VARS["TA_LDAP"] = (
|
||||||
|
"TA_LDAP is not valid with current auth mode"
|
||||||
|
)
|
||||||
|
elif _login_auth_mode == "ldap_local":
|
||||||
|
EXPECTED_ENV_VARS.extend(ldap_required_env)
|
||||||
|
UNEXPECTED_ENV_VARS["TA_ENABLE_AUTH_PROXY"] = (
|
||||||
|
"TA_ENABLE_AUTH_PROXY is not valid with current auth mode"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
if bool(os.environ.get("TA_LDAP")):
|
||||||
|
EXPECTED_ENV_VARS.extend(ldap_required_env)
|
||||||
|
UNEXPECTED_ENV_VARS["TA_ENABLE_AUTH_PROXY"] = (
|
||||||
|
"TA_ENABLE_AUTH_PROXY is not valid with current auth mode"
|
||||||
|
)
|
||||||
|
if bool(os.environ.get("TA_ENABLE_AUTH_PROXY")):
|
||||||
|
UNEXPECTED_ENV_VARS["TA_LDAP"] = (
|
||||||
|
"TA_LDAP is not valid with current auth mode"
|
||||||
|
)
|
||||||
|
|
||||||
def _expected_vars(self):
|
def _expected_vars(self):
|
||||||
"""check if expected env vars are set"""
|
"""check if expected env vars are set"""
|
||||||
self.stdout.write("[1] checking expected env vars")
|
self.stdout.write("[1] checking expected env vars")
|
||||||
|
|||||||
@@ -104,98 +104,6 @@ TEMPLATES = [
|
|||||||
|
|
||||||
WSGI_APPLICATION = "config.wsgi.application"
|
WSGI_APPLICATION = "config.wsgi.application"
|
||||||
|
|
||||||
if bool(environ.get("TA_LDAP")):
|
|
||||||
# pylint: disable=global-at-module-level
|
|
||||||
import ldap
|
|
||||||
from django_auth_ldap.config import LDAPSearch
|
|
||||||
|
|
||||||
global AUTH_LDAP_SERVER_URI
|
|
||||||
AUTH_LDAP_SERVER_URI = environ.get("TA_LDAP_SERVER_URI")
|
|
||||||
|
|
||||||
global AUTH_LDAP_BIND_DN
|
|
||||||
AUTH_LDAP_BIND_DN = environ.get("TA_LDAP_BIND_DN")
|
|
||||||
|
|
||||||
global AUTH_LDAP_BIND_PASSWORD
|
|
||||||
AUTH_LDAP_BIND_PASSWORD = environ.get("TA_LDAP_BIND_PASSWORD")
|
|
||||||
|
|
||||||
"""
|
|
||||||
Since these are new environment variables, taking the opporunity to use
|
|
||||||
more accurate env names.
|
|
||||||
Given Names are *_technically_* different from Personal names, as people
|
|
||||||
who change their names have different given names and personal names,
|
|
||||||
and they go by personal names. Additionally, "LastName" is actually
|
|
||||||
incorrect for many cultures, such as Korea, where the
|
|
||||||
family name comes first, and the personal name comes last.
|
|
||||||
|
|
||||||
But we all know people are going to try to guess at these, so still want
|
|
||||||
to include names that people will guess, hence using first/last as well.
|
|
||||||
"""
|
|
||||||
|
|
||||||
# Attribute mapping options
|
|
||||||
|
|
||||||
global AUTH_LDAP_USER_ATTR_MAP_USERNAME
|
|
||||||
AUTH_LDAP_USER_ATTR_MAP_USERNAME = (
|
|
||||||
environ.get("TA_LDAP_USER_ATTR_MAP_USERNAME")
|
|
||||||
or environ.get("TA_LDAP_USER_ATTR_MAP_UID")
|
|
||||||
or "uid"
|
|
||||||
)
|
|
||||||
|
|
||||||
global AUTH_LDAP_USER_ATTR_MAP_PERSONALNAME
|
|
||||||
AUTH_LDAP_USER_ATTR_MAP_PERSONALNAME = (
|
|
||||||
environ.get("TA_LDAP_USER_ATTR_MAP_PERSONALNAME")
|
|
||||||
or environ.get("TA_LDAP_USER_ATTR_MAP_FIRSTNAME")
|
|
||||||
or environ.get("TA_LDAP_USER_ATTR_MAP_GIVENNAME")
|
|
||||||
or "givenName"
|
|
||||||
)
|
|
||||||
|
|
||||||
global AUTH_LDAP_USER_ATTR_MAP_SURNAME
|
|
||||||
AUTH_LDAP_USER_ATTR_MAP_SURNAME = (
|
|
||||||
environ.get("TA_LDAP_USER_ATTR_MAP_SURNAME")
|
|
||||||
or environ.get("TA_LDAP_USER_ATTR_MAP_LASTNAME")
|
|
||||||
or environ.get("TA_LDAP_USER_ATTR_MAP_FAMILYNAME")
|
|
||||||
or "sn"
|
|
||||||
)
|
|
||||||
|
|
||||||
global AUTH_LDAP_USER_ATTR_MAP_EMAIL
|
|
||||||
AUTH_LDAP_USER_ATTR_MAP_EMAIL = (
|
|
||||||
environ.get("TA_LDAP_USER_ATTR_MAP_EMAIL")
|
|
||||||
or environ.get("TA_LDAP_USER_ATTR_MAP_MAIL")
|
|
||||||
or "mail"
|
|
||||||
)
|
|
||||||
|
|
||||||
global AUTH_LDAP_USER_BASE
|
|
||||||
AUTH_LDAP_USER_BASE = environ.get("TA_LDAP_USER_BASE")
|
|
||||||
|
|
||||||
global AUTH_LDAP_USER_FILTER
|
|
||||||
AUTH_LDAP_USER_FILTER = environ.get("TA_LDAP_USER_FILTER")
|
|
||||||
|
|
||||||
global AUTH_LDAP_USER_SEARCH
|
|
||||||
# pylint: disable=no-member
|
|
||||||
AUTH_LDAP_USER_SEARCH = LDAPSearch(
|
|
||||||
AUTH_LDAP_USER_BASE,
|
|
||||||
ldap.SCOPE_SUBTREE,
|
|
||||||
"(&("
|
|
||||||
+ AUTH_LDAP_USER_ATTR_MAP_USERNAME
|
|
||||||
+ "=%(user)s)"
|
|
||||||
+ AUTH_LDAP_USER_FILTER
|
|
||||||
+ ")",
|
|
||||||
)
|
|
||||||
|
|
||||||
global AUTH_LDAP_USER_ATTR_MAP
|
|
||||||
AUTH_LDAP_USER_ATTR_MAP = {
|
|
||||||
"username": AUTH_LDAP_USER_ATTR_MAP_USERNAME,
|
|
||||||
"first_name": AUTH_LDAP_USER_ATTR_MAP_PERSONALNAME,
|
|
||||||
"last_name": AUTH_LDAP_USER_ATTR_MAP_SURNAME,
|
|
||||||
"email": AUTH_LDAP_USER_ATTR_MAP_EMAIL,
|
|
||||||
}
|
|
||||||
|
|
||||||
if bool(environ.get("TA_LDAP_DISABLE_CERT_CHECK")):
|
|
||||||
global AUTH_LDAP_GLOBAL_OPTIONS
|
|
||||||
AUTH_LDAP_GLOBAL_OPTIONS = {
|
|
||||||
ldap.OPT_X_TLS_REQUIRE_CERT: ldap.OPT_X_TLS_NEVER,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
# Database
|
# Database
|
||||||
# https://docs.djangoproject.com/en/3.2/ref/settings/#databases
|
# https://docs.djangoproject.com/en/3.2/ref/settings/#databases
|
||||||
|
|
||||||
@@ -229,16 +137,6 @@ AUTH_PASSWORD_VALIDATORS = [
|
|||||||
|
|
||||||
AUTH_USER_MODEL = "user.Account"
|
AUTH_USER_MODEL = "user.Account"
|
||||||
|
|
||||||
# Forward-auth authentication
|
|
||||||
if bool(environ.get("TA_ENABLE_AUTH_PROXY")):
|
|
||||||
TA_AUTH_PROXY_USERNAME_HEADER = (
|
|
||||||
environ.get("TA_AUTH_PROXY_USERNAME_HEADER") or "HTTP_REMOTE_USER"
|
|
||||||
)
|
|
||||||
TA_AUTH_PROXY_LOGOUT_URL = environ.get("TA_AUTH_PROXY_LOGOUT_URL")
|
|
||||||
|
|
||||||
MIDDLEWARE.append("user.src.remote_user_auth.HttpRemoteUserMiddleware")
|
|
||||||
|
|
||||||
|
|
||||||
# Configure Authentication Backend Combinations
|
# Configure Authentication Backend Combinations
|
||||||
_login_auth_mode = (environ.get("TA_LOGIN_AUTH_MODE") or "single").casefold()
|
_login_auth_mode = (environ.get("TA_LOGIN_AUTH_MODE") or "single").casefold()
|
||||||
if _login_auth_mode == "local":
|
if _login_auth_mode == "local":
|
||||||
@@ -247,24 +145,33 @@ if _login_auth_mode == "local":
|
|||||||
)
|
)
|
||||||
elif _login_auth_mode == "ldap":
|
elif _login_auth_mode == "ldap":
|
||||||
AUTHENTICATION_BACKENDS = ("django_auth_ldap.backend.LDAPBackend",)
|
AUTHENTICATION_BACKENDS = ("django_auth_ldap.backend.LDAPBackend",)
|
||||||
|
from .ldap_settings import * # noqa: F403 F401
|
||||||
elif _login_auth_mode == "forwardauth":
|
elif _login_auth_mode == "forwardauth":
|
||||||
|
from .fwd_auth_settings import * # noqa: F403 F401
|
||||||
|
|
||||||
AUTHENTICATION_BACKENDS = (
|
AUTHENTICATION_BACKENDS = (
|
||||||
"django.contrib.auth.backends.RemoteUserBackend",
|
"django.contrib.auth.backends.RemoteUserBackend",
|
||||||
)
|
)
|
||||||
|
MIDDLEWARE.append("user.src.remote_user_auth.HttpRemoteUserMiddleware")
|
||||||
elif _login_auth_mode == "ldap_local":
|
elif _login_auth_mode == "ldap_local":
|
||||||
AUTHENTICATION_BACKENDS = (
|
AUTHENTICATION_BACKENDS = (
|
||||||
"django_auth_ldap.backend.LDAPBackend",
|
"django_auth_ldap.backend.LDAPBackend",
|
||||||
"django.contrib.auth.backends.ModelBackend",
|
"django.contrib.auth.backends.ModelBackend",
|
||||||
)
|
)
|
||||||
|
from .ldap_settings import * # noqa: F403 F401
|
||||||
else:
|
else:
|
||||||
# If none of these cases match, AUTHENTICATION_BACKENDS is unset, which
|
# If none of these cases match, AUTHENTICATION_BACKENDS is unset, which
|
||||||
# means the ModelBackend should be used by default
|
# means the ModelBackend should be used by default
|
||||||
if bool(environ.get("TA_LDAP")):
|
if bool(environ.get("TA_LDAP")):
|
||||||
AUTHENTICATION_BACKENDS = ("django_auth_ldap.backend.LDAPBackend",)
|
AUTHENTICATION_BACKENDS = ("django_auth_ldap.backend.LDAPBackend",)
|
||||||
|
from .ldap_settings import * # noqa: F403 F401
|
||||||
if bool(environ.get("TA_ENABLE_AUTH_PROXY")):
|
if bool(environ.get("TA_ENABLE_AUTH_PROXY")):
|
||||||
|
from .fwd_auth_settings import * # noqa: F403 F401
|
||||||
|
|
||||||
AUTHENTICATION_BACKENDS = (
|
AUTHENTICATION_BACKENDS = (
|
||||||
"django.contrib.auth.backends.RemoteUserBackend",
|
"django.contrib.auth.backends.RemoteUserBackend",
|
||||||
)
|
)
|
||||||
|
MIDDLEWARE.append("user.src.remote_user_auth.HttpRemoteUserMiddleware")
|
||||||
|
|
||||||
# Internationalization
|
# Internationalization
|
||||||
# https://docs.djangoproject.com/en/3.2/topics/i18n/
|
# https://docs.djangoproject.com/en/3.2/topics/i18n/
|
||||||
|
|||||||
Reference in New Issue
Block a user