diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index 22e3332e7c..ae1565e0af 100755 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -28,7 +28,9 @@ RUN apt-get update && export DEBIAN_FRONTEND=noninteractive && \ # PostgreSQL support libpq-dev \ # MySQL / MariaDB support - default-libmysqlclient-dev mariadb-client && \ + default-libmysqlclient-dev mariadb-client \ + # LDAP support + libldap2-dev libsasl2-dev && \ apt-get autoclean && apt-get autoremove # [Optional] Uncomment this line to install global node packages. diff --git a/Dockerfile b/Dockerfile index d4a341b102..84709ab034 100644 --- a/Dockerfile +++ b/Dockerfile @@ -58,7 +58,7 @@ RUN apk add --no-cache \ # Image format support libjpeg libwebp zlib \ # Weasyprint requirements : https://doc.courtbouillon.org/weasyprint/stable/first_steps.html#alpine-3-12 - py3-pip py3-pillow py3-cffi py3-brotli pango poppler-utils \ + py3-pip py3-pillow py3-cffi py3-brotli pango poppler-utils openldap \ # SQLite support sqlite \ # PostgreSQL support @@ -84,7 +84,7 @@ RUN if [ `apk --print-arch` = "armv7" ]; then \ fi RUN apk add --no-cache --virtual .build-deps \ - gcc g++ musl-dev openssl-dev libffi-dev cargo python3-dev \ + gcc g++ musl-dev openssl-dev libffi-dev cargo python3-dev openldap-dev \ # Image format dev libs jpeg-dev openjpeg-dev libwebp-dev zlib-dev \ # DB specific dev libs diff --git a/InvenTree/InvenTree/settings.py b/InvenTree/InvenTree/settings.py index 87749ce66d..acb4ffa73e 100644 --- a/InvenTree/InvenTree/settings.py +++ b/InvenTree/InvenTree/settings.py @@ -291,6 +291,63 @@ AUTHENTICATION_BACKENDS = CONFIG.get('authentication_backends', [ "sesame.backends.ModelBackend", # Magic link login django-sesame ]) +# LDAP support +LDAP_AUTH = get_boolean_setting("INVENTREE_LDAP_ENABLED", "ldap.enabled", False) +if LDAP_AUTH: + import ldap + from django_auth_ldap.config import LDAPSearch + + AUTHENTICATION_BACKENDS.append("django_auth_ldap.backend.LDAPBackend") + + # debug mode to troubleshoot configuration + LDAP_DEBUG = get_boolean_setting("INVENTREE_LDAP_DEBUG", "ldap.debug", False) + if LDAP_DEBUG: + if "loggers" not in LOGGING: + LOGGING["loggers"] = {} + LOGGING["loggers"]["django_auth_ldap"] = {"level": "DEBUG", "handlers": ["console"]} + + # get global options from dict and use ldap.OPT_* as keys and values + global_options_dict = get_setting("INVENTREE_LDAP_GLOBAL_OPTIONS", "ldap.global_options", {}, dict) + global_options = {} + for k, v in global_options_dict.items(): + # keys are always ldap.OPT_* constants + k_attr = getattr(ldap, k, None) + if not k.startswith("OPT_") or k_attr is None: + print(f"[LDAP] ldap.global_options, key '{k}' not found, skipping...") + continue + + # values can also be other strings, e.g. paths + v_attr = v + if v.startswith("OPT_"): + v_attr = getattr(ldap, v, None) + + if v_attr is None: + print(f"[LDAP] ldap.global_options, value key '{v}' not found, skipping...") + continue + + global_options[k_attr] = v_attr + AUTH_LDAP_GLOBAL_OPTIONS = global_options + if LDAP_DEBUG: + print("[LDAP] ldap.global_options =", global_options) + + AUTH_LDAP_SERVER_URI = get_setting("INVENTREE_LDAP_SERVER_URI", "ldap.server_uri") + AUTH_LDAP_START_TLS = get_boolean_setting("INVENTREE_LDAP_START_TLS", "ldap.start_tls", False) + AUTH_LDAP_BIND_DN = get_setting("INVENTREE_LDAP_BIND_DN", "ldap.bind_dn") + AUTH_LDAP_BIND_PASSWORD = get_setting("INVENTREE_LDAP_BIND_PASSWORD", "ldap.bind_password") + AUTH_LDAP_USER_SEARCH = LDAPSearch( + get_setting("INVENTREE_LDAP_SEARCH_BASE_DN", "ldap.search_base_dn"), + ldap.SCOPE_SUBTREE, + str(get_setting("INVENTREE_LDAP_SEARCH_FILTER_STR", "ldap.search_filter_str", "(uid= %(user)s)")) + ) + AUTH_LDAP_USER_DN_TEMPLATE = get_setting("INVENTREE_LDAP_USER_DN_TEMPLATE", "ldap.user_dn_template") + AUTH_LDAP_USER_ATTR_MAP = get_setting("INVENTREE_LDAP_USER_ATTR_MAP", "ldap.user_attr_map", { + 'first_name': 'givenName', + 'last_name': 'sn', + 'email': 'mail', + }, dict) + AUTH_LDAP_ALWAYS_UPDATE_USER = get_boolean_setting("INVENTREE_LDAP_ALWAYS_UPDATE_USER", "ldap.always_update_user", True) + AUTH_LDAP_CACHE_TIMEOUT = get_setting("INVENTREE_LDAP_CACHE_TIMEOUT", "ldap.cache_timeout", 3600, int) + DEBUG_TOOLBAR_ENABLED = DEBUG and get_setting('INVENTREE_DEBUG_TOOLBAR', 'debug_toolbar', False) # If the debug toolbar is enabled, add the modules diff --git a/InvenTree/config_template.yaml b/InvenTree/config_template.yaml index 8f22037da6..20213ff22f 100644 --- a/InvenTree/config_template.yaml +++ b/InvenTree/config_template.yaml @@ -233,6 +233,43 @@ remote_login_header: HTTP_REMOTE_USER # KEYCLOAK_URL: 'https://keycloak.custom/auth' # KEYCLOAK_REALM: 'master' +# Add LDAP support +# ldap: +# enabled: false +# debug: false # enable debug mode to troubleshoot ldap configuration +# server_uri: ldaps://example.org +# bind_dn: cn=admin,dc=example,dc=org +# bind_password: admin_password +# search_base_dn: cn=Users,dc=example,dc=org + +# # enable TLS encryption over the standard LDAP port, +# # see: https://django-auth-ldap.readthedocs.io/en/latest/reference.html#auth-ldap-start-tls +# # start_tls: false + +# # uncomment if you want to use direct bind, bind_dn and bin_password is not necessary then +# # user_dn_template: "uid=%(user)s,dc=example,dc=org" + +# # uncomment to set advanced global options, see https://www.python-ldap.org/en/latest/reference/ldap.html#ldap-options +# # for all available options (keys and values starting with OPT_ get automatically converted to python-ldap keys) +# # global_options: +# # OPT_X_TLS_REQUIRE_CERT: OPT_X_TLS_NEVER +# # OPT_X_TLS_CACERTFILE: /opt/inventree/ldapca.pem + +# # uncomment for advanced filter search, default: uid=%(user)s +# # search_filter_str: + +# # uncomment for advanced user attribute mapping (in the format : ) +# # user_attr_map: +# # first_name: givenName +# # last_name: sn +# # email: mail + +# # always update the user on each login, default: true +# # always_update_user: true + +# # cache timeout to reduce traffic with LDAP server, default: 3600 (1h) +# # cache_timeout: 3600 + # Customization options # Add custom messages to the login page or main interface navbar or exchange the logo # Use environment variable INVENTREE_CUSTOMIZE or INVENTREE_CUSTOM_LOGO diff --git a/docker/requirements.txt b/docker/requirements.txt index 0c149ea943..15830985a8 100644 --- a/docker/requirements.txt +++ b/docker/requirements.txt @@ -14,3 +14,7 @@ mariadb>=1.0.7,<1.1.0 # gunicorn web server gunicorn>=20.1.0 + +# LDAP required packages +django-auth-ldap # Django integration for ldap auth +python-ldap # LDAP auth support diff --git a/docs/docs/start/advanced.md b/docs/docs/start/advanced.md index 2cb230e7cd..98610c45ba 100644 --- a/docs/docs/start/advanced.md +++ b/docs/docs/start/advanced.md @@ -40,3 +40,30 @@ The installer code is used to identify the way InvenTree was installed. If you v | DIO | Installed using digital ocean marketplace[^1] | No | [^1]: Starting with fresh installs of 0.12.0 this code is set. Versions installed before 0.12.0 do not have this code set even after upgrading to 0.12.0. + +## Authentication + +### LDAP + +You can link your InvenTree server to an LDAP server. + +!!! warning "Important" + This feature is currently only available for docker installs. + +Next you can start configuring the connection. Either use the config file or set the environment variables. + +| config key | ENV Variable | Description | +| --- | --- | --- | +| `ldap.enabled` | `INVENTREE_LDAP_ENABLED` | Set this to `True` to enable LDAP. | +| `ldap.debug` | `INVENTREE_LDAP_DEBUG` | Set this to `True` to activate debug mode, useful for troubleshooting ldap configurations. | +| `ldap.server_uri` | `INVENTREE_LDAP_SERVER_URI` | LDAP Server URI, e.g. `ldaps://example.org` | +| `ldap.start_tls` | `INVENTREE_LDAP_START_TLS` | Enable TLS encryption over the standard LDAP port, [see](https://django-auth-ldap.readthedocs.io/en/latest/reference.html#auth-ldap-start-tls). (You can set TLS options via `ldap.global_options`) | +| `ldap.bind_dn` | `INVENTREE_LDAP_BIND_DN` | LDAP bind dn, e.g. `cn=admin,dc=example,dc=org` | +| `ldap.bind_password` | `INVENTREE_LDAP_BIND_PASSWORD` | LDAP bind password | +| `ldap.search_base_dn` | `INVENTREE_LDAP_SEARCH_BASE_DN` | LDAP search base dn, e.g. `cn=Users,dc=example,dc=org` | +| `ldap.user_dn_template` | `INVENTREE_LDAP_USER_DN_TEMPLATE` | use direct bind as auth user, `ldap.bind_dn` and `ldap.bin_password` is not necessary then, e.g. `uid=%(user)s,dc=example,dc=org` | +| `ldap.global_options` | `INVENTREE_LDAP_GLOBAL_OPTIONS` | set advanced options as dict, e.g. TLS settings. For a list of all available options, see [python-ldap docs](https://www.python-ldap.org/en/latest/reference/ldap.html#ldap-options). (keys and values starting with OPT_ get automatically converted to `python-ldap` keys) | +| `ldap.search_filter_str`| `INVENTREE_LDAP_SEARCH_FILTER_STR` | LDAP search filter str, default: `uid=%(user)s` | +| `ldap.user_attr_map` | `INVENTREE_LDAP_USER_ATTR_MAP` | LDAP <-> Inventree user attribute map, can be json if used as env, in yml directly specify the object. default: `{"first_name": "givenName", "last_name": "sn", "email": "mail"}` | +| `ldap.always_update_user` | `INVENTREE_LDAP_ALWAYS_UPDATE_USER` | Always update the user on each login, default: `true` | +| `ldap.cache_timeout` | `INVENTREE_LDAP_CACHE_TIMEOUT` | cache timeout to reduce traffic with LDAP server, default: `3600` (1h) |