Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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