Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion docs/docs/start/advanced.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ Next you can start configuring the connection. Either use the config file or set
| `INVENTREE_LDAP_USER_ATTR_MAP` | `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"}` |
| `INVENTREE_LDAP_ALWAYS_UPDATE_USER` | `ldap.always_update_user` | Always update the user on each login, default: `true` |
| `INVENTREE_LDAP_CACHE_TIMEOUT` | `ldap.cache_timeout` | cache timeout to reduce traffic with LDAP server, default: `3600` (1h) |
| `INVENTREE_LDAP_GROUP_SEARCH` | `ldap.group_search` | Base LDAP DN for group searching; required to enable group features |
| `INVENTREE_LDAP_GROUP_SEARCH` | `ldap.group_search` | Base LDAP DN for group searching; required to enable group features. If not set, all group-based features are disabled gracefully |
| `INVENTREE_LDAP_GROUP_OBJECT_CLASS` | `ldap.group_object_class` | The string to pass to the LDAP group search `(objectClass=<...>)`, default: `groupOfUniqueNames` |
| `INVENTREE_LDAP_MIRROR_GROUPS` | `ldap.mirror_groups` | If `True`, mirror a user's LDAP group membership in the Django database, default: `False` |
| `INVENTREE_LDAP_GROUP_TYPE_CLASS` | `ldap.group_type_class` | The group class to be imported from `django_auth_ldap.config` as a string, default: `'GroupOfUniqueNamesType'`|
Expand All @@ -74,6 +74,7 @@ Next you can start configuring the connection. Either use the config file or set
| `INVENTREE_LDAP_REQUIRE_GROUP` | `ldap.require_group` | If set, users _must_ be in this group to log in to InvenTree |
| `INVENTREE_LDAP_DENY_GROUP` | `ldap.deny_group` | If set, users _must not_ be in this group to log in to InvenTree |
| `INVENTREE_LDAP_USER_FLAGS_BY_GROUP` | `ldap.user_flags_by_group` | LDAP group to InvenTree user flag map, can be json if used as env, in yml directly specify the object. See config template for example, default: `{}` |
| `INVENTREE_LDAP_FIND_GROUP_PERMS` | `ldap.find_group_perms` | If `True`, look up LDAP group permissions during authentication. Automatically disabled when `ldap.group_search` is not set. default: `True` |

## Tracing support

Expand Down
41 changes: 34 additions & 7 deletions src/backend/InvenTree/InvenTree/setting/ldap.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,33 @@ def get_ldap_config(debug: bool = False) -> dict:
str,
)

group_search_dn = get_setting(
'INVENTREE_LDAP_GROUP_SEARCH', 'ldap.group_search'
)

find_group_perms = get_boolean_setting(
'INVENTREE_LDAP_FIND_GROUP_PERMS', 'ldap.find_group_perms', True
)

# If group search DN is not configured, group-based features cannot
# work. Disable them gracefully with a warning instead of letting
# django-auth-ldap crash at runtime with a TypeError (see #12225).
if group_search_dn is None:

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should you also check for empty string here? e.g.

if not group_search_dn:
    ...

if find_group_perms or get_setting(
'INVENTREE_LDAP_MIRROR_GROUPS', 'ldap.mirror_groups'
) or get_setting(
'INVENTREE_LDAP_REQUIRE_GROUP', 'ldap.require_group'
) or get_setting(
'INVENTREE_LDAP_DENY_GROUP', 'ldap.deny_group'
):
print(
'[LDAP] ldap.group_search is not configured; '
'disabling group-based features (find_group_perms, '
'mirror_groups, require_group, deny_group). '
'Set ldap.group_search to enable them.'
)
find_group_perms = False

ldap_config = {
'AUTH_LDAP_GLOBAL_OPTIONS': global_options,
'AUTH_LDAP_SERVER_URI': get_setting(
Expand Down Expand Up @@ -108,13 +135,13 @@ def get_ldap_config(debug: bool = False) -> dict:
),
'AUTH_LDAP_MIRROR_GROUPS': get_boolean_setting(
'INVENTREE_LDAP_MIRROR_GROUPS', 'ldap.mirror_groups', False
),
) if group_search_dn is not None else False,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

could simplify to:

) if group_search_dn else False

'AUTH_LDAP_GROUP_OBJECT_CLASS': group_object_class,
'AUTH_LDAP_GROUP_SEARCH': django_auth_ldap.config.LDAPSearch(
get_setting('INVENTREE_LDAP_GROUP_SEARCH', 'ldap.group_search'),
group_search_dn,
ldap.SCOPE_SUBTREE,
f'(objectClass={group_object_class})',
),
) if group_search_dn is not None else None,
'AUTH_LDAP_GROUP_TYPE_CLASS': group_type_class,
'AUTH_LDAP_GROUP_TYPE_CLASS_ARGS': [*group_type_class_args],
'AUTH_LDAP_GROUP_TYPE_CLASS_KWARGS': {**group_type_class_kwargs},
Expand All @@ -123,17 +150,17 @@ def get_ldap_config(debug: bool = False) -> dict:
),
'AUTH_LDAP_REQUIRE_GROUP': get_setting(
'INVENTREE_LDAP_REQUIRE_GROUP', 'ldap.require_group'
),
) if group_search_dn is not None else None,
'AUTH_LDAP_DENY_GROUP': get_setting(
'INVENTREE_LDAP_DENY_GROUP', 'ldap.deny_group'
),
) if group_search_dn is not None else None,
'AUTH_LDAP_USER_FLAGS_BY_GROUP': get_setting(
'INVENTREE_LDAP_USER_FLAGS_BY_GROUP',
'ldap.user_flags_by_group',
default_value=None,
typecast=dict,
),
'AUTH_LDAP_FIND_GROUP_PERMS': True,
) if group_search_dn is not None else None,
'AUTH_LDAP_FIND_GROUP_PERMS': find_group_perms,
}

return ldap_config
Empty file.
145 changes: 145 additions & 0 deletions src/backend/InvenTree/setting/tests/test_ldap_config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
"""Tests for LDAP configuration handling.

Verifies that missing ``ldap.group_search`` does not cause runtime crashes
and that ``find_group_perms`` is properly configurable.
"""

import importlib
from unittest import mock

import pytest


@pytest.fixture
def ldap_module():
"""Import the ldap setting module fresh for each test."""
# The module imports django_auth_ldap and ldap at function level,
# so we can import it directly without those packages installed
# as long as we mock them for the get_ldap_config call.
import InvenTree.InvenTree.setting.ldap as ldap_mod
importlib.reload(ldap_mod)
return ldap_mod


class TestLdapGroupSearchMissing:
"""Tests for graceful handling of missing ldap.group_search (see #12225)."""

def test_find_group_perms_disabled_when_group_search_missing(self, ldap_module):
"""When group_search is None, find_group_perms must be False."""
with mock.patch.object(ldap_module, 'get_setting') as gs, \
mock.patch.object(ldap_module, 'get_boolean_setting') as gbs:

# get_setting: return None for group_search, defaults for others
def gs_side_effect(env_key, yaml_key, *args, **kwargs):
if 'GROUP_SEARCH' in env_key:
return None
if 'GLOBAL_OPTIONS' in env_key:
return kwargs.get('default_value', {}) or {}
if 'GROUP_TYPE_CLASS_ARGS' in env_key:
return kwargs.get('default_value', []) if 'default_value' in kwargs else []
if 'GROUP_TYPE_CLASS_KWARGS' in env_key:
return kwargs.get('default_value', {}) or {}
return kwargs.get('default_value', None)

gs.side_effect = gs_side_effect

# get_boolean_setting: return True for find_group_perms, False for others
def gbs_side_effect(env_key, yaml_key, default):
if 'FIND_GROUP_PERMS' in env_key:
return True
return default

gbs.side_effect = gbs_side_effect

# Mock the imports inside get_ldap_config
with mock.patch('django_auth_ldap.config') as mock_config:
mock_config.LDAPSearch.return_value = mock.MagicMock()
mock_config.GroupOfUniqueNamesType.return_value = mock.MagicMock()

with mock.patch('ldap') as mock_ldap:
mock_ldap.SCOPE_SUBTREE = 2
mock_ldap.OPT_REFERRALS = 0

config = ldap_module.get_ldap_config(debug=False)

assert config['AUTH_LDAP_FIND_GROUP_PERMS'] is False
assert config['AUTH_LDAP_GROUP_SEARCH'] is None
assert config['AUTH_LDAP_MIRROR_GROUPS'] is False
assert config['AUTH_LDAP_REQUIRE_GROUP'] is None
assert config['AUTH_LDAP_DENY_GROUP'] is None
assert config['AUTH_LDAP_USER_FLAGS_BY_GROUP'] is None

def test_find_group_perms_enabled_when_group_search_set(self, ldap_module):
"""When group_search is configured, find_group_perms stays True."""
with mock.patch.object(ldap_module, 'get_setting') as gs, \
mock.patch.object(ldap_module, 'get_boolean_setting') as gbs:

def gs_side_effect(env_key, yaml_key, *args, **kwargs):
if 'GROUP_SEARCH' in env_key:
return 'ou=groups,dc=example,dc=org'
if 'GLOBAL_OPTIONS' in env_key:
return kwargs.get('default_value', {}) or {}
if 'GROUP_TYPE_CLASS_ARGS' in env_key:
return kwargs.get('default_value', []) if 'default_value' in kwargs else []
if 'GROUP_TYPE_CLASS_KWARGS' in env_key:
return kwargs.get('default_value', {}) or {}
return kwargs.get('default_value', None)

gs.side_effect = gs_side_effect

def gbs_side_effect(env_key, yaml_key, default):
if 'FIND_GROUP_PERMS' in env_key:
return True
return default

gbs.side_effect = gbs_side_effect

with mock.patch('django_auth_ldap.config') as mock_config:
mock_config.LDAPSearch.return_value = mock.MagicMock()
mock_config.GroupOfUniqueNamesType.return_value = mock.MagicMock()

with mock.patch('ldap') as mock_ldap:
mock_ldap.SCOPE_SUBTREE = 2

config = ldap_module.get_ldap_config(debug=False)

assert config['AUTH_LDAP_FIND_GROUP_PERMS'] is True
assert config['AUTH_LDAP_GROUP_SEARCH'] is not None

def test_find_group_perms_can_be_disabled_explicitly(self, ldap_module):
"""User can set find_group_perms=False even when group_search is set."""
with mock.patch.object(ldap_module, 'get_setting') as gs, \
mock.patch.object(ldap_module, 'get_boolean_setting') as gbs:

def gs_side_effect(env_key, yaml_key, *args, **kwargs):
if 'GROUP_SEARCH' in env_key:
return 'ou=groups,dc=example,dc=org'
if 'GLOBAL_OPTIONS' in env_key:
return kwargs.get('default_value', {}) or {}
if 'GROUP_TYPE_CLASS_ARGS' in env_key:
return kwargs.get('default_value', []) if 'default_value' in kwargs else []
if 'GROUP_TYPE_CLASS_KWARGS' in env_key:
return kwargs.get('default_value', {}) or {}
return kwargs.get('default_value', None)

gs.side_effect = gs_side_effect

def gbs_side_effect(env_key, yaml_key, default):
if 'FIND_GROUP_PERMS' in env_key:
return False
return default

gbs.side_effect = gbs_side_effect

with mock.patch('django_auth_ldap.config') as mock_config:
mock_config.LDAPSearch.return_value = mock.MagicMock()
mock_config.GroupOfUniqueNamesType.return_value = mock.MagicMock()

with mock.patch('ldap') as mock_ldap:
mock_ldap.SCOPE_SUBTREE = 2

config = ldap_module.get_ldap_config(debug=False)

assert config['AUTH_LDAP_FIND_GROUP_PERMS'] is False
# Group search is still configured (user might use it for other features)
assert config['AUTH_LDAP_GROUP_SEARCH'] is not None
Loading