From f9d3f43b024f85a3679fde8adaff7c4278431cad Mon Sep 17 00:00:00 2001 From: Jacob Felknor Date: Fri, 15 Nov 2024 15:08:27 -0700 Subject: [PATCH] configurable ldap group classes (#8475) * configurable ldap group classes * remove accidental duplicate line * fix style issues --------- Co-authored-by: Matthias Mair Co-authored-by: Oliver --- .pre-commit-config.yaml | 10 +++--- docs/docs/start/advanced.md | 9 +++-- .../InvenTree/InvenTree/helpers_mixin.py | 4 +-- src/backend/InvenTree/InvenTree/settings.py | 36 ++++++++++++++++--- src/backend/InvenTree/InvenTree/tests.py | 4 +-- src/backend/InvenTree/machine/machine_type.py | 2 +- src/backend/InvenTree/machine/models.py | 2 +- src/backend/InvenTree/order/models.py | 6 ++-- src/backend/InvenTree/part/views.py | 4 +-- .../samples/integration/label_sample.py | 2 +- src/backend/InvenTree/plugin/test_plugin.py | 2 +- .../InvenTree/script/translation_stats.py | 2 +- src/backend/InvenTree/web/urls.py | 2 +- tasks.py | 4 +-- 14 files changed, 59 insertions(+), 30 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 1c5df1a003..5185cbac91 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -10,14 +10,14 @@ exclude: | )$ repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.6.0 + rev: v5.0.0 hooks: - id: trailing-whitespace - id: end-of-file-fixer - id: check-yaml - id: mixed-line-ending - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.7.0 + rev: v0.7.3 hooks: - id: ruff-format args: [--preview] @@ -28,7 +28,7 @@ repos: --preview ] - repo: https://github.com/astral-sh/uv-pre-commit - rev: 0.4.24 + rev: 0.5.1 hooks: - id: pip-compile name: pip-compile requirements-dev.in @@ -51,7 +51,7 @@ repos: args: [contrib/container/requirements.in, -o, contrib/container/requirements.txt, --python-version=3.11, --no-strip-extras, --generate-hashes] files: contrib/container/requirements\.(in|txt)$ - repo: https://github.com/Riverside-Healthcare/djLint - rev: v1.35.2 + rev: v1.36.1 hooks: - id: djlint-django - repo: https://github.com/codespell-project/codespell @@ -76,7 +76,7 @@ repos: additional_dependencies: ["@biomejs/biome@1.9.4"] files: ^src/frontend/.*\.(js|ts|tsx)$ - repo: https://github.com/gitleaks/gitleaks - rev: v8.21.0 + rev: v8.21.2 hooks: - id: gitleaks #- repo: https://github.com/jumanjihouse/pre-commit-hooks diff --git a/docs/docs/start/advanced.md b/docs/docs/start/advanced.md index 0d9032b105..7711eae331 100644 --- a/docs/docs/start/advanced.md +++ b/docs/docs/start/advanced.md @@ -27,7 +27,6 @@ The version information contains the following information extracted form the in | Target | No | ubuntu:20.04 | environment: `INVENTREE_PKG_TARGET` | | Active plugins | Yes | [{'name': 'InvenTreeBarcode', 'slug': 'inventreebarcode', 'version': '2.0.0'}] | instance | - ### Installer codes The installer code is used to identify the way InvenTree was installed. If you vendor InvenTree, you can and should set the installer code to your own value to make sure debugging goes smoothly. @@ -68,11 +67,15 @@ Next you can start configuring the connection. Either use the config file or set | `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) | | `ldap.group_search` | `INVENTREE_LDAP_GROUP_SEARCH` | Base LDAP DN for group searching; required to enable group features | +| `ldap.group_object_class` | `INVENTREE_LDAP_GROUP_OBJECT_CLASS` | The string to pass to the LDAP group search `(objectClass=<...>)`, default: `groupOfUniqueNames` | +| `ldap.mirror_groups` | `INVENTREE_LDAP_MIRROR_GROUPS` | If `True`, mirror a user's LDAP group membership in the Django database, default: `False` | +| `ldap.group_type_class` | `INVENTREE_LDAP_GROUP_TYPE_CLASS` | The group class to be imported from `django_auth_ldap.config` as a string, default: `'GroupOfUniqueNamesType'`| +| `ldap.group_type_class_args` | `INVENTREE_LDAP_GROUP_TYPE_CLASS_ARGS` | A `list` of positional args to pass to the LDAP group type class, default `[]` | +| `ldap.group_type_class_kwargs` | `INVENTREE_LDAP_GROUP_TYPE_CLASS_KWARGS` | A `dict` of keyword args to pass to the LDAP group type class, default `{'name_attr': 'cn'}` | | `ldap.require_group` | `INVENTREE_LDAP_REQUIRE_GROUP` | If set, users _must_ be in this group to log in to InvenTree | | `ldap.deny_group` | `INVENTREE_LDAP_DENY_GROUP` | If set, users _must not_ be in this group to log in to InvenTree | | `ldap.user_flags_by_group` | `INVENTREE_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: `{}` | - ## Tracing support Starting with 0.14.0 InvenTree supports sending traces, logs and metrics to OpenTelemetry compatible endpoints (both HTTP and gRPC). A [list of vendors](https://opentelemetry.io/ecosystem/vendors) is available on the project site. @@ -99,4 +102,4 @@ If your InvenTree instance is used in a multi-site environment, you can enable m | Environment Variable | Config Key | Description | Default | | --- | --- | --- | --- | | INVENTREE_SITE_MULTI | site_multi | Enable multiple sites | False | -| INVENTREE_SITE_ID | site_id | Specify a fixed site ID | *Not specified* | +| INVENTREE_SITE_ID | site_id | Specify a fixed site ID | _Not specified_ | diff --git a/src/backend/InvenTree/InvenTree/helpers_mixin.py b/src/backend/InvenTree/InvenTree/helpers_mixin.py index d3f5c06d06..fee2ace924 100644 --- a/src/backend/InvenTree/InvenTree/helpers_mixin.py +++ b/src/backend/InvenTree/InvenTree/helpers_mixin.py @@ -62,7 +62,7 @@ class ClassValidationMixin: if len(missing_attributes) > 0: errors.append( - f"did not provide the following attributes: {', '.join(missing_attributes)}" + f'did not provide the following attributes: {", ".join(missing_attributes)}' ) if len(missing_overrides) > 0: missing_overrides_list = [] @@ -75,7 +75,7 @@ class ClassValidationMixin: else: missing_overrides_list.append(base_implementation.__name__) errors.append( - f"did not override the required attributes: {', '.join(missing_overrides_list)}" + f'did not override the required attributes: {", ".join(missing_overrides_list)}' ) if len(errors) > 0: diff --git a/src/backend/InvenTree/InvenTree/settings.py b/src/backend/InvenTree/InvenTree/settings.py index e69094096c..7e294b8ac8 100644 --- a/src/backend/InvenTree/InvenTree/settings.py +++ b/src/backend/InvenTree/InvenTree/settings.py @@ -360,8 +360,8 @@ AUTHENTICATION_BACKENDS = CONFIG.get( # LDAP support LDAP_AUTH = get_boolean_setting('INVENTREE_LDAP_ENABLED', 'ldap.enabled', False) if LDAP_AUTH: + import django_auth_ldap.config import ldap - from django_auth_ldap.config import GroupOfUniqueNamesType, LDAPSearch AUTHENTICATION_BACKENDS.append('django_auth_ldap.backend.LDAPBackend') @@ -412,7 +412,7 @@ if LDAP_AUTH: AUTH_LDAP_BIND_PASSWORD = get_setting( 'INVENTREE_LDAP_BIND_PASSWORD', 'ldap.bind_password' ) - AUTH_LDAP_USER_SEARCH = LDAPSearch( + AUTH_LDAP_USER_SEARCH = django_auth_ldap.config.LDAPSearch( get_setting('INVENTREE_LDAP_SEARCH_BASE_DN', 'ldap.search_base_dn'), ldap.SCOPE_SUBTREE, str( @@ -439,12 +439,38 @@ if LDAP_AUTH: 'INVENTREE_LDAP_CACHE_TIMEOUT', 'ldap.cache_timeout', 3600, int ) - AUTH_LDAP_GROUP_SEARCH = LDAPSearch( + AUTH_LDAP_MIRROR_GROUPS = get_boolean_setting( + 'INVENTREE_LDAP_MIRROR_GROUPS', 'ldap.mirror_groups', False + ) + AUTH_LDAP_GROUP_OBJECT_CLASS = get_setting( + 'INVENTREE_LDAP_GROUP_OBJECT_CLASS', + 'ldap.group_object_class', + 'groupOfUniqueNames', + str, + ) + AUTH_LDAP_GROUP_SEARCH = django_auth_ldap.config.LDAPSearch( get_setting('INVENTREE_LDAP_GROUP_SEARCH', 'ldap.group_search'), ldap.SCOPE_SUBTREE, - '(objectClass=groupOfUniqueNames)', + f'(objectClass={AUTH_LDAP_GROUP_OBJECT_CLASS})', + ) + AUTH_LDAP_GROUP_TYPE_CLASS = get_setting( + 'INVENTREE_LDAP_GROUP_TYPE_CLASS', + 'ldap.group_type_class', + 'GroupOfUniqueNamesType', + str, + ) + AUTH_LDAP_GROUP_TYPE_CLASS_ARGS = get_setting( + 'INVENTREE_LDAP_GROUP_TYPE_CLASS_ARGS', 'ldap.group_type_class_args', [], list + ) + AUTH_LDAP_GROUP_TYPE_CLASS_KWARGS = get_setting( + 'INVENTREE_LDAP_GROUP_TYPE_CLASS_KWARGS', + 'ldap.group_type_class_kwargs', + {'name_attr': 'cn'}, + dict, + ) + AUTH_LDAP_GROUP_TYPE = getattr(django_auth_ldap.config, AUTH_LDAP_GROUP_TYPE_CLASS)( + *AUTH_LDAP_GROUP_TYPE_CLASS_ARGS, **AUTH_LDAP_GROUP_TYPE_CLASS_KWARGS ) - AUTH_LDAP_GROUP_TYPE = GroupOfUniqueNamesType(name_attr='cn') AUTH_LDAP_REQUIRE_GROUP = get_setting( 'INVENTREE_LDAP_REQUIRE_GROUP', 'ldap.require_group' ) diff --git a/src/backend/InvenTree/InvenTree/tests.py b/src/backend/InvenTree/InvenTree/tests.py index 0f17f9a3d4..bf9ad84477 100644 --- a/src/backend/InvenTree/InvenTree/tests.py +++ b/src/backend/InvenTree/InvenTree/tests.py @@ -1034,14 +1034,14 @@ class TestVersionNumber(TestCase): # Check that the current .git values work too git_hash = str( - subprocess.check_output('git rev-parse --short HEAD'.split()), 'utf-8' + subprocess.check_output(['git', 'rev-parse', '--short', 'HEAD']), 'utf-8' ).strip() # On some systems the hash is a different length, so just check the first 6 characters self.assertEqual(git_hash[:6], version.inventreeCommitHash()[:6]) d = ( - str(subprocess.check_output('git show -s --format=%ci'.split()), 'utf-8') + str(subprocess.check_output(['git', 'show', '-s', '--format=%ci']), 'utf-8') .strip() .split(' ')[0] ) diff --git a/src/backend/InvenTree/machine/machine_type.py b/src/backend/InvenTree/machine/machine_type.py index 3239f5a149..1b8bdf4133 100644 --- a/src/backend/InvenTree/machine/machine_type.py +++ b/src/backend/InvenTree/machine/machine_type.py @@ -260,7 +260,7 @@ class BaseMachineType( error_parts.append( f'{config_type.name} settings: ' + ', '.join(missing) ) - self.handle_error(f"Missing {' and '.join(error_parts)}") + self.handle_error(f'Missing {" and ".join(error_parts)}') return try: diff --git a/src/backend/InvenTree/machine/models.py b/src/backend/InvenTree/machine/models.py index c951adb59d..0e4852c64e 100755 --- a/src/backend/InvenTree/machine/models.py +++ b/src/backend/InvenTree/machine/models.py @@ -112,7 +112,7 @@ class MachineConfig(models.Model): """Get machine errors for django admin interface.""" return format_html_join( mark_safe('
'), '{}', ((str(error),) for error in self.errors) - ) or mark_safe(f"{_('No errors')}") + ) or mark_safe(f'{_("No errors")}') @admin.display(description=_('Machine status')) def get_machine_status(self): diff --git a/src/backend/InvenTree/order/models.py b/src/backend/InvenTree/order/models.py index f29097e6e8..8578974711 100644 --- a/src/backend/InvenTree/order/models.py +++ b/src/backend/InvenTree/order/models.py @@ -470,7 +470,7 @@ class PurchaseOrder(TotalPriceMixin, Order): def __str__(self): """Render a string representation of this PurchaseOrder.""" - return f"{self.reference} - {self.supplier.name if self.supplier else _('deleted')}" + return f'{self.reference} - {self.supplier.name if self.supplier else _("deleted")}' reference = models.CharField( unique=True, @@ -996,7 +996,7 @@ class SalesOrder(TotalPriceMixin, Order): def __str__(self): """Render a string representation of this SalesOrder.""" - return f"{self.reference} - {self.customer.name if self.customer else _('deleted')}" + return f'{self.reference} - {self.customer.name if self.customer else _("deleted")}' reference = models.CharField( unique=True, @@ -2194,7 +2194,7 @@ class ReturnOrder(TotalPriceMixin, Order): def __str__(self): """Render a string representation of this ReturnOrder.""" - return f"{self.reference} - {self.customer.name if self.customer else _('no customer')}" + return f'{self.reference} - {self.customer.name if self.customer else _("no customer")}' reference = models.CharField( unique=True, diff --git a/src/backend/InvenTree/part/views.py b/src/backend/InvenTree/part/views.py index 1ad7c2f31d..26f7c1fbd5 100644 --- a/src/backend/InvenTree/part/views.py +++ b/src/backend/InvenTree/part/views.py @@ -295,7 +295,7 @@ class PartImport(FileManagementFormView): # Set alerts if import_done: - alert = f"{_('Part-Import')}
{_(f'Imported {import_done} parts')}" + alert = f'{_("Part-Import")}
{_(f"Imported {import_done} parts")}' messages.success(self.request, alert) if import_error: error_text = '\n'.join([ @@ -304,7 +304,7 @@ class PartImport(FileManagementFormView): ]) messages.error( self.request, - f"{_('Some errors occurred:')}
    {error_text}
", + f'{_("Some errors occurred:")}
    {error_text}
', ) return HttpResponseRedirect(reverse('part-index')) diff --git a/src/backend/InvenTree/plugin/samples/integration/label_sample.py b/src/backend/InvenTree/plugin/samples/integration/label_sample.py index 312205314e..fe6df363ef 100644 --- a/src/backend/InvenTree/plugin/samples/integration/label_sample.py +++ b/src/backend/InvenTree/plugin/samples/integration/label_sample.py @@ -31,7 +31,7 @@ class SampleLabelPrinter(LabelPrintingMixin, InvenTreePlugin): Normally here the connection to the printer and transfer of the label would take place. """ # Test that the expected kwargs are present - print(f"Printing Label: {kwargs['filename']} (User: {kwargs['user']})") + print(f'Printing Label: {kwargs["filename"]} (User: {kwargs["user"]})') pdf_data = kwargs['pdf_data'] png_file = self.render_to_png( diff --git a/src/backend/InvenTree/plugin/test_plugin.py b/src/backend/InvenTree/plugin/test_plugin.py index 4fe03ea1c9..cc034acdee 100644 --- a/src/backend/InvenTree/plugin/test_plugin.py +++ b/src/backend/InvenTree/plugin/test_plugin.py @@ -252,7 +252,7 @@ class RegistryTests(TestCase): def test_package_loading(self): """Test that package distributed plugins work.""" # Install sample package - subprocess.check_output('pip install inventree-zapier'.split()) + subprocess.check_output(['pip', 'install', 'inventree-zapier']) # Reload to discover plugin registry.reload_plugins(full_reload=True, collect=True) diff --git a/src/backend/InvenTree/script/translation_stats.py b/src/backend/InvenTree/script/translation_stats.py index c459b1eaf1..29c1b7eac3 100644 --- a/src/backend/InvenTree/script/translation_stats.py +++ b/src/backend/InvenTree/script/translation_stats.py @@ -62,7 +62,7 @@ if __name__ == '__main__': percentage = int(covered / total * 100) if total > 0 else 0 if verbose: - print(f"| {locale.ljust(4, ' ')} : {str(percentage).rjust(4, ' ')}% |") + print(f'| {locale.ljust(4, " ")} : {str(percentage).rjust(4, " ")}% |') locales_perc[locale] = percentage diff --git a/src/backend/InvenTree/web/urls.py b/src/backend/InvenTree/web/urls.py index 17ac2abea3..6199abe264 100644 --- a/src/backend/InvenTree/web/urls.py +++ b/src/backend/InvenTree/web/urls.py @@ -18,7 +18,7 @@ class RedirectAssetView(TemplateView): def get(self, request, *args, **kwargs): """Redirect to static asset.""" return redirect( - f"{settings.STATIC_URL}web/assets/{kwargs['path']}", permanent=True + f'{settings.STATIC_URL}web/assets/{kwargs["path"]}', permanent=True ) diff --git a/tasks.py b/tasks.py index 3137b2eefe..3100764037 100644 --- a/tasks.py +++ b/tasks.py @@ -1453,7 +1453,7 @@ Then try continuing by running: invoke frontend-download --file