inventree / InvenTree

Open Source Inventory Management System
https://docs.inventree.org
MIT License
4.32k stars 781 forks source link

[HOW] Connecting to LDAP-Server by specifying certificate of a private CA #6161

Closed Nudelsalad closed 10 months ago

Nudelsalad commented 10 months ago

Deployment Method

Describe the problem*

Hi, after spending hours of trying to setup the LDAP for Inventree I thought I'll give it a shot here.

This is my config:

INVENTREE_LDAP_ENABLED=True
INVENTREE_LDAP_DEBUG=True
INVENTREE_LDAP_SERVER_URI=ldaps://ldap.int.example.com
INVENTREE_LDAP_BIND_DN="uid=ldap-read-access,cn=sysaccounts,cn=etc,dc=int,dc=example,dc=com"
INVENTREE_LDAP_START_TLS=True
INVENTREE_LDAP_BIND_PASSWORD=[redacted]
INVENTREE_LDAP_SEARCH_BASE_DN="dc=int,dc=example,dc=com"
INVENTREE_LDAP_GLOBAL_OPTIONS= {"OPT_X_TLS_CACERTFILE":"/home/inventree/data/cert.pem"}

With this config I receive the following error: Caught LDAPError while authenticating nudelsalad: OPERATIONS_ERROR({'result': 1, 'desc': 'Operations error', 'ctrls': [], 'info': 'SSL connection already established.'})

I tried adding "OPT_X_TLS_NEWCTX":"0" to the Global_Options to apply TLS settings to internal TLS context. According to Link Value 0 creates a new client-side context

Modifying INVENTREE_LDAP_GLOBAL_OPTIONS to

INVENTREE_LDAP_GLOBAL_OPTIONS= {"OPT_X_TLS_CACERTFILE":"/home/inventree/data/cert.pem","OPT_X_TLS_NEWCTX":"0"}

gives me an internal server error, it seems to somehow authenticate... -> see attached log

The TypeError: 'str' object cannot be interpreted as an integer somhow makes me wondering

Running latest version of inventree in docker setup (my other services running in container can authenticate) Any help appreciated

Steps to Reproduce

As described above

Relevant log output

'str' object cannot be interpreted as an integer while authenticating nudelsalad
Internal Server Error: /accounts/login/
Traceback (most recent call last):
  File "/root/.local/lib/python3.10/site-packages/django/core/handlers/exception.py", line 47, in inner
    response = get_response(request)
  File "/root/.local/lib/python3.10/site-packages/django/core/handlers/base.py", line 181, in _get_response
    response = wrapped_callback(request, *callback_args, **callback_kwargs)
  File "/root/.local/lib/python3.10/site-packages/django/views/generic/base.py", line 70, in view
    return self.dispatch(request, *args, **kwargs)
  File "/root/.local/lib/python3.10/site-packages/django/utils/decorators.py", line 43, in _wrapper
    return bound_method(*args, **kwargs)
  File "/root/.local/lib/python3.10/site-packages/django/views/decorators/debug.py", line 89, in sensitive_post_parameters_wrapper
    return view(request, *args, **kwargs)
  File "/root/.local/lib/python3.10/site-packages/django/utils/decorators.py", line 43, in _wrapper
    return bound_method(*args, **kwargs)
  File "/root/.local/lib/python3.10/site-packages/django/views/decorators/cache.py", line 44, in _wrapped_view_func
    response = view_func(request, *args, **kwargs)
  File "/root/.local/lib/python3.10/site-packages/allauth/account/views.py", line 164, in dispatch
    return super(LoginView, self).dispatch(request, *args, **kwargs)
  File "/root/.local/lib/python3.10/site-packages/allauth/account/views.py", line 91, in dispatch
    response = super(RedirectAuthenticatedUserMixin, self).dispatch(
  File "/root/.local/lib/python3.10/site-packages/django/views/generic/base.py", line 98, in dispatch
    return handler(request, *args, **kwargs)
  File "/root/.local/lib/python3.10/site-packages/allauth/account/views.py", line 118, in post
    if form.is_valid():
  File "/root/.local/lib/python3.10/site-packages/django/forms/forms.py", line 175, in is_valid
    return self.is_bound and not self.errors
  File "/root/.local/lib/python3.10/site-packages/django/forms/forms.py", line 170, in errors
    self.full_clean()
  File "/root/.local/lib/python3.10/site-packages/django/forms/forms.py", line 373, in full_clean
    self._clean_form()
  File "/root/.local/lib/python3.10/site-packages/django/forms/forms.py", line 400, in _clean_form
    cleaned_data = self.clean()
  File "/root/.local/lib/python3.10/site-packages/allauth/account/forms.py", line 186, in clean
    user = get_adapter(self.request).authenticate(self.request, **credentials)
  File "/root/.local/lib/python3.10/site-packages/allauth/account/adapter.py", line 668, in authenticate
    user = authenticate(request, **credentials)
  File "/root/.local/lib/python3.10/site-packages/django/views/decorators/debug.py", line 42, in sensitive_variables_wrapper
    return func(*func_args, **func_kwargs)
  File "/root/.local/lib/python3.10/site-packages/django/contrib/auth/__init__.py", line 76, in authenticate
    user = backend.authenticate(request, **credentials)
  File "/root/.local/lib/python3.10/site-packages/django_auth_ldap/backend.py", line 148, in authenticate
    user = self.authenticate_ldap_user(ldap_user, password)
  File "/root/.local/lib/python3.10/site-packages/django_auth_ldap/backend.py", line 206, in authenticate_ldap_user
    return ldap_user.authenticate(password)
  File "/root/.local/lib/python3.10/site-packages/django_auth_ldap/backend.py", line 348, in authenticate
    self._authenticate_user_dn(password)
  File "/root/.local/lib/python3.10/site-packages/django_auth_ldap/backend.py", line 478, in _authenticate_user_dn
    if self.dn is None:
  File "/root/.local/lib/python3.10/site-packages/django_auth_ldap/backend.py", line 443, in dn
    self._load_user_dn()
  File "/root/.local/lib/python3.10/site-packages/django_auth_ldap/backend.py", line 516, in _load_user_dn
    self._user_dn = self._search_for_user_dn()
  File "/root/.local/lib/python3.10/site-packages/django_auth_ldap/backend.py", line 551, in _search_for_user_dn
    return cache.get_or_set(
  File "/root/.local/lib/python3.10/site-packages/django/core/cache/backends/base.py", line 173, in get_or_set
    default = default()
  File "/root/.local/lib/python3.10/site-packages/django_auth_ldap/backend.py", line 539, in _search_for_user
    results = search.execute(self.connection, {"user": self._username})
  File "/root/.local/lib/python3.10/site-packages/django_auth_ldap/backend.py", line 465, in connection
    self._bind()
  File "/root/.local/lib/python3.10/site-packages/django_auth_ldap/backend.py", line 829, in _bind
    self._bind_as(self.settings.BIND_DN, self.settings.BIND_PASSWORD, sticky=True)
  File "/root/.local/lib/python3.10/site-packages/django_auth_ldap/backend.py", line 841, in _bind_as
    self._get_connection().simple_bind_s(bind_dn, bind_password)
  File "/root/.local/lib/python3.10/site-packages/django_auth_ldap/backend.py", line 854, in _get_connection
    self._connection = self.backend.ldap.initialize(uri, bytes_mode=False)
  File "/root/.local/lib/python3.10/site-packages/django_auth_ldap/backend.py", line 126, in ldap
    self._ldap = _LDAPConfig.get_ldap(options)
  File "/root/.local/lib/python3.10/site-packages/django_auth_ldap/config.py", line 116, in get_ldap
    ldap.set_option(opt, value)
  File "/root/.local/lib/python3.10/site-packages/ldap/functions.py", line 112, in set_option
    return _ldap_function_call(None,_ldap.set_option,option,invalue)
  File "/root/.local/lib/python3.10/site-packages/ldap/functions.py", line 52, in _ldap_function_call
    result = func(*args,**kwargs)
TypeError: 'str' object cannot be interpreted as an integer
matmair commented 10 months ago

I think this is a casting issue @Nudelsalad - try changing the setting to this INVENTREE_LDAP_GLOBAL_OPTIONS= {"OPT_X_TLS_CACERTFILE":"/home/inventree/data/cert.pem","OPT_X_TLS_NEWCTX":0}

From the code base over at python-ldap it seem like OPT_X_TLS_NEWCTX is assumed to be an int not str.

Nudelsalad commented 10 months ago

Hi @matmair appreciate your help and thanks for pointing out the obious,

Setting global options to: INVENTREE_LDAP_GLOBAL_OPTIONS= {"OPT_X_TLS_CACERTFILE":"/home/inventree/data/cert.pem","OPT_X_TLS_NEWCTX":0} leads to

Loading config file : /home/inventree/data/config.yaml
Python version 3.10.13 - /usr/local/bin/python
Traceback (most recent call last):
  File "/root/.local/bin/gunicorn", line 8, in <module>
    sys.exit(run())
  File "/root/.local/lib/python3.10/site-packages/gunicorn/app/wsgiapp.py", line 67, in run
    WSGIApplication("%(prog)s [OPTIONS] [APP_MODULE]").run()
  File "/root/.local/lib/python3.10/site-packages/gunicorn/app/base.py", line 236, in run
    super().run()
  File "/root/.local/lib/python3.10/site-packages/gunicorn/app/base.py", line 72, in run
    Arbiter(self).run()
  File "/root/.local/lib/python3.10/site-packages/gunicorn/arbiter.py", line 58, in __init__
    self.setup(app)
  File "/root/.local/lib/python3.10/site-packages/gunicorn/arbiter.py", line 118, in setup
    self.app.wsgi()
  File "/root/.local/lib/python3.10/site-packages/gunicorn/app/base.py", line 67, in wsgi
    self.callable = self.load()
  File "/root/.local/lib/python3.10/site-packages/gunicorn/app/wsgiapp.py", line 58, in load
    return self.load_wsgiapp()
  File "/root/.local/lib/python3.10/site-packages/gunicorn/app/wsgiapp.py", line 48, in load_wsgiapp
    return util.import_app(self.app_uri)
  File "/root/.local/lib/python3.10/site-packages/gunicorn/util.py", line 371, in import_app
    mod = importlib.import_module(module)
  File "/usr/local/lib/python3.10/importlib/__init__.py", line 126, in import_module
    return _bootstrap._gcd_import(name[level:], package, level)
  File "<frozen importlib._bootstrap>", line 1050, in _gcd_import
  File "<frozen importlib._bootstrap>", line 1027, in _find_and_load
  File "<frozen importlib._bootstrap>", line 1006, in _find_and_load_unlocked
  File "<frozen importlib._bootstrap>", line 688, in _load_unlocked
  File "<frozen importlib._bootstrap_external>", line 883, in exec_module
  File "<frozen importlib._bootstrap>", line 241, in _call_with_frames_removed
  File "/home/inventree/InvenTree/InvenTree/wsgi.py", line 15, in <module>
    application = get_wsgi_application()  # pragma: no cover
  File "/root/.local/lib/python3.10/site-packages/django/core/wsgi.py", line 12, in get_wsgi_application
    django.setup(set_prefix=False)
  File "/root/.local/lib/python3.10/site-packages/django/__init__.py", line 19, in setup
    configure_logging(settings.LOGGING_CONFIG, settings.LOGGING)
  File "/root/.local/lib/python3.10/site-packages/django/conf/__init__.py", line 82, in __getattr__
    self._setup(name)
  File "/root/.local/lib/python3.10/site-packages/django/conf/__init__.py", line 69, in _setup
    self._wrapped = Settings(settings_module)
  File "/root/.local/lib/python3.10/site-packages/django/conf/__init__.py", line 170, in __init__
    mod = importlib.import_module(self.SETTINGS_MODULE)
  File "/usr/local/lib/python3.10/importlib/__init__.py", line 126, in import_module
    return _bootstrap._gcd_import(name[level:], package, level)
  File "<frozen importlib._bootstrap>", line 1050, in _gcd_import
  File "<frozen importlib._bootstrap>", line 1027, in _find_and_load
  File "<frozen importlib._bootstrap>", line 1006, in _find_and_load_unlocked
  File "<frozen importlib._bootstrap>", line 688, in _load_unlocked
  File "<frozen importlib._bootstrap_external>", line 883, in exec_module
  File "<frozen importlib._bootstrap>", line 241, in _call_with_frames_removed
  File "/home/inventree/InvenTree/InvenTree/settings.py", line 334, in <module>
    if v.startswith("OPT_"):
AttributeError: 'int' object has no attribute 'startswith'

I already tried playing arround with some values, however setting the global options to:

INVENTREE_LDAP_GLOBAL_OPTIONS= {"OPT_X_TLS_REQUIRE_CERT":"OPT_X_TLS_NEVER"}

gives me pip error of the installed plugins into inventree with pip. Disabling these plugins temporarely leads to:

Caught LDAPError while authenticating nudelsalad: OPERATIONS_ERROR({'result': 1, 'desc': 'Operations error', 'ctrls': [], 'info': 'SSL connection already established.'})

The SSL connection already established info lead me to this post in Stackoverflow. Event though this config seems redundant 34 people upvoted it and trying to set the proposed values leads me to multiple Attribute Errors. Removing succesively the values leads to SSL connection already established

Even though turning SSL off is not the admired solution it also does not kinda work. Thanks in advance

matmair commented 10 months ago

@Nudelsalad are you trying to use TLS and SSL at the same time? That would not work

Nudelsalad commented 10 months ago

Hi @matmair thanks for your time, I guess I need to dig deeper into the network materia. However for those having a similar issue there are two options that worked out for me: First option: Mounting certificate of private CA into container and specifying it in the config:

INVENTREE_LDAP_ENABLED=True
INVENTREE_LDAP_SERVER_URI=ldaps://ldapsrv.example.com
INVENTREE_LDAP_BIND_DN="uid=ldap-read-access,cn=sysaccounts,cn=etc,dc=example,dc=com"
INVENTREE_LDAP_BIND_PASSWORD=[redacted by nudelsalat]
INVENTREE_LDAP_SEARCH_BASE_DN="dc=example,dc=com"
INVENTREE_LDAP_GLOBAL_OPTIONS= {"OPT_X_TLS_CACERTFILE":"/home/inventree/data/cert.pem"}

Second option:

INVENTREE_LDAP_ENABLED=True
INVENTREE_LDAP_SERVER_URI=ldap://ldapsrv.example.com
INVENTREE_LDAP_BIND_DN="uid=ldap-read-access,cn=sysaccounts,cn=etc,dc=example,dc=com"
(INVENTREE_LDAP_START_TLS=True)
INVENTREE_LDAP_BIND_PASSWORD=[redacted by nudelsalat]
INVENTREE_LDAP_SEARCH_BASE_DN="dc=example,dc=com"
INVENTREE_LDAP_GLOBAL_OPTIONS= {"OPT_X_TLS_REQUIRE_CERT":"OPT_X_TLS_NEVER"}

I would like to know now which one is "safer" so I can close this issue but thanks in advance @matmair!

matmair commented 10 months ago

@Nudelsalad mounting the ca public cert and using it for LDAPS is certainly the safer option as the traffic is encrypted and harder to sniff.