From e093533811a878967b88c989a0a8014bab25d127 Mon Sep 17 00:00:00 2001 From: "Jens W. Klein" Date: Wed, 2 Feb 2022 11:00:47 +0100 Subject: [PATCH 01/74] basic refactoring for plone 6 --- .travis.yml | 39 --------- CHANGES.rst | 5 +- TODO.rst | 2 +- buildout-5.1.x.cfg | 5 -- buildout-5.2.x.cfg | 5 -- ldap.cfg | 42 ---------- requirements-5.1.x.txt | 1 - requirements-5.2.x.txt | 1 - setup.cfg | 81 +++++++++++++++++- setup.py | 83 +------------------ src/pas/__init__.py | 9 +- src/pas/plugins/__init__.py | 9 +- src/pas/plugins/ldap/__init__.py | 10 +-- src/pas/plugins/ldap/cache.py | 8 +- .../plugins/ldap/plonecontrolpanel/cache.py | 2 +- .../ldap/plonecontrolpanel/configure.zcml | 10 +-- .../ldap/plonecontrolpanel/controlpanel.py | 2 +- .../profiles/base/registry.xml | 8 -- .../{base => default}/componentregistry.xml | 0 .../{plone5 => default}/controlpanel.xml | 0 .../{base => default}/ldapsettings.xml | 0 .../profiles/{base => default}/metadata.xml | 0 .../profiles/{plone5 => default}/registry.xml | 10 ++- .../profiles/plone5/metadata.xml | 7 -- .../ldap/plonecontrolpanel/upgrades.py | 2 +- src/pas/plugins/ldap/plugin.py | 8 +- src/pas/plugins/ldap/properties.py | 6 +- src/pas/plugins/ldap/setuphandlers.py | 2 +- src/pas/plugins/ldap/sheet.py | 4 +- src/pas/plugins/ldap/testing.py | 25 ++---- src/pas/plugins/ldap/tests/test_doctests.py | 2 +- src/pas/plugins/ldap/tests/test_plugin.py | 2 +- src/pas/plugins/ldap/zmi/manage_plugin.py | 2 +- 33 files changed, 128 insertions(+), 264 deletions(-) delete mode 100644 .travis.yml delete mode 100644 buildout-5.1.x.cfg delete mode 100644 buildout-5.2.x.cfg delete mode 100644 ldap.cfg delete mode 100644 requirements-5.1.x.txt delete mode 100644 requirements-5.2.x.txt delete mode 100644 src/pas/plugins/ldap/plonecontrolpanel/profiles/base/registry.xml rename src/pas/plugins/ldap/plonecontrolpanel/profiles/{base => default}/componentregistry.xml (100%) rename src/pas/plugins/ldap/plonecontrolpanel/profiles/{plone5 => default}/controlpanel.xml (100%) rename src/pas/plugins/ldap/plonecontrolpanel/profiles/{base => default}/ldapsettings.xml (100%) rename src/pas/plugins/ldap/plonecontrolpanel/profiles/{base => default}/metadata.xml (100%) rename src/pas/plugins/ldap/plonecontrolpanel/profiles/{plone5 => default}/registry.xml (66%) delete mode 100644 src/pas/plugins/ldap/plonecontrolpanel/profiles/plone5/metadata.xml diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 749b104..0000000 --- a/.travis.yml +++ /dev/null @@ -1,39 +0,0 @@ -language: python -sudo: false -addons: - apt: - packages: - - libssl-dev - - libdb-dev -cache: - pip: true - directories: - - eggs - - downloads - - openldap -matrix: - fast_finish: true - include: - - python: "2.7" - env: PLONE_VERSION=5.1.x - - python: "2.7" - env: PLONE_VERSION=5.2.x - - python: "3.6" - env: PLONE_VERSION=5.2.x - - python: "3.7" - env: PLONE_VERSION=5.2.x - dist: xenial -sudo: true -install: - - pip install -r requirements-${PLONE_VERSION}.txt - - buildout -Nc buildout-${PLONE_VERSION}.cfg buildout:download-cache=downloads code-analysis:return-status-codes=True "parts=test code-analysis coverage test-coverage testldap" annotate - - buildout -Nc buildout-${PLONE_VERSION}.cfg buildout:download-cache=downloads code-analysis:return-status-codes=True "parts=test code-analysis coverage test-coverage testldap" -script: - - bin/code-analysis - - bin/test -after_success: - - bin/createcoverage - - bin/pip install coverage - - bin/python -m coverage.pickle2json - - pip install coveralls - - coveralls \ No newline at end of file diff --git a/CHANGES.rst b/CHANGES.rst index 28fae20..8f62b18 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -2,10 +2,11 @@ History ======= -1.8.3 (unreleased) +1.9.0 (unreleased) ------------------ -- Nothing changed yet. +- Drop support for Plone 5/ Python < 3.9 + [jensens] 1.8.2 (2022-10-31) diff --git a/TODO.rst b/TODO.rst index 4a8f0bd..ef07018 100644 --- a/TODO.rst +++ b/TODO.rst @@ -5,7 +5,7 @@ TODO See also `Issue-Tracker `_ -Milestone 2.0 +Milestone 3.0 ------------- - remove portrait monkey patch diff --git a/buildout-5.1.x.cfg b/buildout-5.1.x.cfg deleted file mode 100644 index f1cb536..0000000 --- a/buildout-5.1.x.cfg +++ /dev/null @@ -1,5 +0,0 @@ -[buildout] -extends = - base.cfg - https://dist.plone.org/release/5.1-latest/versions.cfg - versions.cfg diff --git a/buildout-5.2.x.cfg b/buildout-5.2.x.cfg deleted file mode 100644 index 438e3a8..0000000 --- a/buildout-5.2.x.cfg +++ /dev/null @@ -1,5 +0,0 @@ -[buildout] -extends = - base.cfg - https://dist.plone.org/release/5.2-latest/versions.cfg - versions.cfg diff --git a/ldap.cfg b/ldap.cfg deleted file mode 100644 index 1f1f634..0000000 --- a/ldap.cfg +++ /dev/null @@ -1,42 +0,0 @@ -[buildout] -parts += - python-ldap - testldap - -[openldap] -# this build needs (on debian based systems): -# apt-get install libssl-dev -recipe = zc.recipe.cmmi>=2.0.0 -url = https://www.openldap.org/software/download/OpenLDAP/openldap-release/openldap-2.4.49.tgz -extra_options = --with-tls --enable-slapd=yes --enable-overlays --disable-bdb --disable-hdb CPPFLAGS=-D_GNU_SOURCE -shared = true - - -[python-ldap] -recipe = zc.recipe.egg:custom -egg = python-ldap -include-dirs = - ${openldap:location}/include -library-dirs = - ${openldap:location}/lib -rpath = - ${openldap:location}/lib - - -[testenv] -LDAP_ADD_BIN = ${openldap:location}/bin/ldapadd -LDAP_DELETE_BIN = ${openldap:location}/bin/ldapdelete -SLAPD_BIN = ${openldap:location}/libexec/slapd -SLAPD_URIS = ldap://127.0.0.1:12345 - - -[testldap] -recipe = zc.recipe.egg:script -eggs = - node.ext.ldap[test] -initialization = - import os - os.environ['SLAPD_BIN'] = '${testenv:SLAPD_BIN}' - os.environ['SLAPD_URIS'] = '${testenv:SLAPD_URIS}' - os.environ['LDAP_DELETE_BIN'] = '${testenv:LDAP_DELETE_BIN}' - os.environ['LDAP_ADD_BIN'] = '${testenv:LDAP_ADD_BIN}' diff --git a/requirements-5.1.x.txt b/requirements-5.1.x.txt deleted file mode 100644 index 196bcda..0000000 --- a/requirements-5.1.x.txt +++ /dev/null @@ -1 +0,0 @@ --r https://dist.plone.org/release/5.1-latest/requirements.txt \ No newline at end of file diff --git a/requirements-5.2.x.txt b/requirements-5.2.x.txt deleted file mode 100644 index 6fbe573..0000000 --- a/requirements-5.2.x.txt +++ /dev/null @@ -1 +0,0 @@ --r https://dist.plone.org/release/5.2-latest/requirements.txt \ No newline at end of file diff --git a/setup.cfg b/setup.cfg index 6a52b8a..4f29950 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,11 +1,86 @@ +[metadata] +name = pas.plugins.ldap +version=2.0.0.dev0 +description=LDAP/AD Plugin for Plone/Zope PluggableAuthService (users+groups) +long_description = file: README.rst, CHANGES.rst +keywords = zope pas plone ldap authentication plugin users groups +author = BlueDynamics Alliance +author_email = dev@bluedynamics.com +license = GPLv2 +license_files = LICENSE.rst +url = https://github.com/collective/pas.plugins.ldap/ +project_urls = + ChangeLog = https://github.com/collective/pas.plugins.ldap/blob/master/CHANGES.rst + Issue Tracker = https://github.com/collective/pas.plugins.ldap/issues + Source Code = https://github.com/collective/pas.plugins.ldap +classifiers = + Development Status :: 5 - Production/Stable + Environment :: Web Environment + Framework :: Plone :: 6.0 + Framework :: Plone :: Addon + Framework :: Plone + Framework :: Zope :: 5 + Framework :: Zope + License :: OSI Approved :: GNU General Public License v2 (GPLv2) + Operating System :: OS Independent + Programming Language :: Python :: 3.9 + Programming Language :: Python :: 3.10 + Programming Language :: Python :: 3.11 + Topic :: System :: Systems Administration :: Authentication/Directory :: LDAP + Programming Language :: Python + +[options] +setup_requires = setuptools +install_requires = + bda.cache + five.globalrequest + node.ext.ldap>=1.0rc1 + odict + plone.registry + Products.CMFCore + Products.GenericSetup + Products.PlonePAS + Products.PluggableAuthService + Products.statusmessages + python-ldap>=3.4.0 + setuptools + yafowil.plone>=4.0.0a3 + yafowil.widget.array + yafowil.widget.dict + yafowil.yaml + zope.globalrequest + Zope>=5 + +include_package_data = True +zip_safe = False +namespace_packages = + pas + pas.plugins + +package_dir = + = src +packages = find: + +[options.packages.find] +where = + src + +[options.entry_points] +z3c.autoinclude.plugin = + target = plone + +[options.extras_require] +test = + plone.testing + +plone = + Products.CMFPlone + [isort] -# https://github.com/timothycrosley/isort/wiki/isort-Settings -# https://github.com/plone/plone.recipe.codeanalysis profile = black force_alphabetical_sort=True force_single_line = True lines_after_imports = 2 -line_length = 200 [zest.releaser] create-wheel = yes diff --git a/setup.py b/setup.py index c2eb5bd..6068493 100644 --- a/setup.py +++ b/setup.py @@ -1,84 +1,3 @@ -from setuptools import find_packages from setuptools import setup -import os - - -version = "1.8.3.dev0" -shortdesc = "LDAP/AD Plugin for Plone/Zope PluggableAuthService (users+groups)" -longdesc = open(os.path.join(os.path.dirname(__file__), "README.rst")).read() -longdesc += open(os.path.join(os.path.dirname(__file__), "TODO.rst")).read() -longdesc += open(os.path.join(os.path.dirname(__file__), "CHANGES.rst")).read() -longdesc += open(os.path.join(os.path.dirname(__file__), "LICENSE.rst")).read() - - -setup( - name="pas.plugins.ldap", - version=version, - description=shortdesc, - long_description=longdesc, - classifiers=[ - "Development Status :: 5 - Production/Stable", - "Environment :: Web Environment", - "Framework :: Plone :: 5.1", - "Framework :: Plone :: 5.2", - "Framework :: Plone :: Addon", - "Framework :: Plone", - "Framework :: Zope :: 2", - "Framework :: Zope :: 4", - "Framework :: Zope", - "License :: OSI Approved :: GNU General Public License v2 (GPLv2)", - "Operating System :: OS Independent", - "Programming Language :: Python :: 2.7", - "Programming Language :: Python :: 3.6", - "Programming Language :: Python :: 3.7", - "Programming Language :: Python", - "Topic :: System :: Systems Administration :: Authentication/Directory :: LDAP", - ], - keywords="zope pas plone ldap authentication plugin", - author="BlueDynamics Alliance", - author_email="dev@bluedynamics.com", - url="https://github.com/collective/pas.plugins.ldap/", - license="GPLv2", - packages=find_packages("src"), - package_dir={"": "src"}, - namespace_packages=["pas", "pas.plugins"], - include_package_data=True, - zip_safe=False, - install_requires=[ - "AccessControl>=3.0", - "Acquisition", - "bda.cache", - "five.globalrequest", - "node", - "node.ext.ldap>=1.1", - "odict", - "plone.registry", - "Products.CMFCore", - "Products.GenericSetup", - "Products.PlonePAS", - "Products.PluggableAuthService", - "Products.statusmessages", - "python-ldap>=3.2.0", - "setuptools", - "six", - "yafowil>=2.3.1", - "yafowil.plone>=4.0.0a3", - "yafowil.widget.array", - "yafowil.widget.dict", - "yafowil.yaml", - "zope.component", - "zope.globalrequest", - "zope.i18nmessageid", - "zope.interface", - "zope.traversing", - ], - extras_require={ - "test": ["plone.testing", "zope.configuration"], - "plone": ["Plone"], - }, - entry_points=""" - [z3c.autoinclude.plugin] - target = plone - """, -) +setup() diff --git a/src/pas/__init__.py b/src/pas/__init__.py index ca12a73..5284146 100644 --- a/src/pas/__init__.py +++ b/src/pas/__init__.py @@ -1,8 +1 @@ -# -*- coding: utf-8 -*- -# See http://peak.telecommunity.com/DevCenter/setuptools#namespace-packages -try: - __import__("pkg_resources").declare_namespace(__name__) -except ImportError: - from pkgutil import extend_path - - __path__ = extend_path(__path__, __name__) +__import__("pkg_resources").declare_namespace(__name__) diff --git a/src/pas/plugins/__init__.py b/src/pas/plugins/__init__.py index ca12a73..5284146 100644 --- a/src/pas/plugins/__init__.py +++ b/src/pas/plugins/__init__.py @@ -1,8 +1 @@ -# -*- coding: utf-8 -*- -# See http://peak.telecommunity.com/DevCenter/setuptools#namespace-packages -try: - __import__("pkg_resources").declare_namespace(__name__) -except ImportError: - from pkgutil import extend_path - - __path__ = extend_path(__path__, __name__) +__import__("pkg_resources").declare_namespace(__name__) diff --git a/src/pas/plugins/ldap/__init__.py b/src/pas/plugins/ldap/__init__.py index cd63eb0..0249a2b 100644 --- a/src/pas/plugins/ldap/__init__.py +++ b/src/pas/plugins/ldap/__init__.py @@ -1,10 +1,10 @@ # -*- coding: utf-8 -*- from AccessControl.Permissions import add_user_folders -from pas.plugins.ldap import monkey # noqa -from pas.plugins.ldap.plugin import LDAPPlugin -from pas.plugins.ldap.plugin import manage_addLDAPPlugin -from pas.plugins.ldap.plugin import manage_addLDAPPluginForm -from pas.plugins.ldap.plugin import zmidir +from . import monkey # noqa +from . import LDAPPlugin +from . import manage_addLDAPPlugin +from . import manage_addLDAPPluginForm +from . import zmidir from Products.PluggableAuthService import registerMultiPlugin import os diff --git a/src/pas/plugins/ldap/cache.py b/src/pas/plugins/ldap/cache.py index 74df4c1..9865038 100644 --- a/src/pas/plugins/ldap/cache.py +++ b/src/pas/plugins/ldap/cache.py @@ -3,10 +3,10 @@ from bda.cache import Memcached from bda.cache import NullCache from node.ext.ldap.interfaces import ICacheProviderFactory -from pas.plugins.ldap.interfaces import ICacheSettingsRecordProvider -from pas.plugins.ldap.interfaces import ILDAPPlugin -from pas.plugins.ldap.interfaces import IPluginCacheHandler -from pas.plugins.ldap.interfaces import VALUE_NOT_CACHED +from .interfaces import ICacheSettingsRecordProvider +from .interfaces import ILDAPPlugin +from .interfaces import IPluginCacheHandler +from .interfaces import VALUE_NOT_CACHED from zope.component import adapter from zope.component import queryUtility from zope.globalrequest import getRequest diff --git a/src/pas/plugins/ldap/plonecontrolpanel/cache.py b/src/pas/plugins/ldap/plonecontrolpanel/cache.py index 500a284..89ea514 100644 --- a/src/pas/plugins/ldap/plonecontrolpanel/cache.py +++ b/src/pas/plugins/ldap/plonecontrolpanel/cache.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -from pas.plugins.ldap.interfaces import ICacheSettingsRecordProvider +from ..interfaces import ICacheSettingsRecordProvider from persistent import Persistent from plone.registry import field from plone.registry import Record diff --git a/src/pas/plugins/ldap/plonecontrolpanel/configure.zcml b/src/pas/plugins/ldap/plonecontrolpanel/configure.zcml index e261fa1..1bef934 100644 --- a/src/pas/plugins/ldap/plonecontrolpanel/configure.zcml +++ b/src/pas/plugins/ldap/plonecontrolpanel/configure.zcml @@ -7,18 +7,10 @@ - - - diff --git a/src/pas/plugins/ldap/plonecontrolpanel/controlpanel.py b/src/pas/plugins/ldap/plonecontrolpanel/controlpanel.py index d8daccc..48f1753 100644 --- a/src/pas/plugins/ldap/plonecontrolpanel/controlpanel.py +++ b/src/pas/plugins/ldap/plonecontrolpanel/controlpanel.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -from pas.plugins.ldap.properties import BasePropertiesForm +from ..properties import BasePropertiesForm from Products.CMFCore.interfaces import ISiteRoot from Products.CMFPlone.resources import add_bundle_on_request from Products.statusmessages.interfaces import IStatusMessage diff --git a/src/pas/plugins/ldap/plonecontrolpanel/profiles/base/registry.xml b/src/pas/plugins/ldap/plonecontrolpanel/profiles/base/registry.xml deleted file mode 100644 index 02403ee..0000000 --- a/src/pas/plugins/ldap/plonecontrolpanel/profiles/base/registry.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - servers, delimited by space - - 127.0.0.1:11211 - - \ No newline at end of file diff --git a/src/pas/plugins/ldap/plonecontrolpanel/profiles/base/componentregistry.xml b/src/pas/plugins/ldap/plonecontrolpanel/profiles/default/componentregistry.xml similarity index 100% rename from src/pas/plugins/ldap/plonecontrolpanel/profiles/base/componentregistry.xml rename to src/pas/plugins/ldap/plonecontrolpanel/profiles/default/componentregistry.xml diff --git a/src/pas/plugins/ldap/plonecontrolpanel/profiles/plone5/controlpanel.xml b/src/pas/plugins/ldap/plonecontrolpanel/profiles/default/controlpanel.xml similarity index 100% rename from src/pas/plugins/ldap/plonecontrolpanel/profiles/plone5/controlpanel.xml rename to src/pas/plugins/ldap/plonecontrolpanel/profiles/default/controlpanel.xml diff --git a/src/pas/plugins/ldap/plonecontrolpanel/profiles/base/ldapsettings.xml b/src/pas/plugins/ldap/plonecontrolpanel/profiles/default/ldapsettings.xml similarity index 100% rename from src/pas/plugins/ldap/plonecontrolpanel/profiles/base/ldapsettings.xml rename to src/pas/plugins/ldap/plonecontrolpanel/profiles/default/ldapsettings.xml diff --git a/src/pas/plugins/ldap/plonecontrolpanel/profiles/base/metadata.xml b/src/pas/plugins/ldap/plonecontrolpanel/profiles/default/metadata.xml similarity index 100% rename from src/pas/plugins/ldap/plonecontrolpanel/profiles/base/metadata.xml rename to src/pas/plugins/ldap/plonecontrolpanel/profiles/default/metadata.xml diff --git a/src/pas/plugins/ldap/plonecontrolpanel/profiles/plone5/registry.xml b/src/pas/plugins/ldap/plonecontrolpanel/profiles/default/registry.xml similarity index 66% rename from src/pas/plugins/ldap/plonecontrolpanel/profiles/plone5/registry.xml rename to src/pas/plugins/ldap/plonecontrolpanel/profiles/default/registry.xml index 535ceab..28c7a7d 100644 --- a/src/pas/plugins/ldap/plonecontrolpanel/profiles/plone5/registry.xml +++ b/src/pas/plugins/ldap/plonecontrolpanel/profiles/default/registry.xml @@ -1,5 +1,13 @@ + + + + servers, delimited by space + + 127.0.0.1:11211 + + @@ -16,4 +24,4 @@ True - + \ No newline at end of file diff --git a/src/pas/plugins/ldap/plonecontrolpanel/profiles/plone5/metadata.xml b/src/pas/plugins/ldap/plonecontrolpanel/profiles/plone5/metadata.xml deleted file mode 100644 index 87d38b6..0000000 --- a/src/pas/plugins/ldap/plonecontrolpanel/profiles/plone5/metadata.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - 2 - - profile-pas.plugins.ldap.plonecontrolpanel:install-base - - diff --git a/src/pas/plugins/ldap/plonecontrolpanel/upgrades.py b/src/pas/plugins/ldap/plonecontrolpanel/upgrades.py index 033e0f6..17afc93 100644 --- a/src/pas/plugins/ldap/plonecontrolpanel/upgrades.py +++ b/src/pas/plugins/ldap/plonecontrolpanel/upgrades.py @@ -14,7 +14,7 @@ def remove_persistent_import_step_from_base_profile(context): A bit double, but then it works cleanly, both within Plone and outside of Plone. """ - from pas.plugins.ldap.setuphandlers import remove_persistent_import_step + from ..setuphandlers import remove_persistent_import_step remove_persistent_import_step(context) context.setLastVersionForProfile("pas.plugins.ldap:default", "2") diff --git a/src/pas/plugins/ldap/plugin.py b/src/pas/plugins/ldap/plugin.py index b2fd2cb..8f12f87 100644 --- a/src/pas/plugins/ldap/plugin.py +++ b/src/pas/plugins/ldap/plugin.py @@ -6,10 +6,10 @@ from node.ext.ldap.interfaces import ILDAPProps from node.ext.ldap.interfaces import ILDAPUsersConfig from node.ext.ldap.ugm import Ugm -from pas.plugins.ldap.cache import get_plugin_cache -from pas.plugins.ldap.interfaces import ILDAPPlugin -from pas.plugins.ldap.interfaces import VALUE_NOT_CACHED -from pas.plugins.ldap.sheet import LDAPUserPropertySheet +from .cache import get_plugin_cache +from .interfaces import ILDAPPlugin +from .interfaces import VALUE_NOT_CACHED +from .sheet import LDAPUserPropertySheet from Products.PageTemplates.PageTemplateFile import PageTemplateFile from Products.PlonePAS import interfaces as plonepas_interfaces from Products.PlonePAS.plugins.group import PloneGroup diff --git a/src/pas/plugins/ldap/properties.py b/src/pas/plugins/ldap/properties.py index ce2edf5..2f3b19e 100644 --- a/src/pas/plugins/ldap/properties.py +++ b/src/pas/plugins/ldap/properties.py @@ -9,9 +9,9 @@ from node.ext.ldap.scope import SUBTREE from node.ext.ldap.ugm import Ugm from odict import odict -from pas.plugins.ldap.defaults import DEFAULTS -from pas.plugins.ldap.interfaces import ICacheSettingsRecordProvider -from pas.plugins.ldap.interfaces import ILDAPPlugin +from .defaults import DEFAULTS +from .interfaces import ICacheSettingsRecordProvider +from .interfaces import ILDAPPlugin from Products.Five import BrowserView from yafowil import loader # noqa: F401 from yafowil.base import ExtractionError diff --git a/src/pas/plugins/ldap/setuphandlers.py b/src/pas/plugins/ldap/setuphandlers.py index b80acfb..df1cf5f 100644 --- a/src/pas/plugins/ldap/setuphandlers.py +++ b/src/pas/plugins/ldap/setuphandlers.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -from pas.plugins.ldap.plugin import LDAPPlugin +from .plugin import LDAPPlugin from zope.component.hooks import getSite diff --git a/src/pas/plugins/ldap/sheet.py b/src/pas/plugins/ldap/sheet.py index 2771c4d..6adcd6d 100644 --- a/src/pas/plugins/ldap/sheet.py +++ b/src/pas/plugins/ldap/sheet.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- from Acquisition import aq_base -from node.ext.ldap.interfaces import ILDAPGroupsConfig -from node.ext.ldap.interfaces import ILDAPUsersConfig +from .interfaces import ILDAPGroupsConfig +from .interfaces import ILDAPUsersConfig from Products.PlonePAS.interfaces.propertysheets import IMutablePropertySheet from Products.PluggableAuthService.UserPropertySheet import UserPropertySheet from zope.globalrequest import getRequest diff --git a/src/pas/plugins/ldap/testing.py b/src/pas/plugins/ldap/testing.py index 28b7f32..3f50478 100644 --- a/src/pas/plugins/ldap/testing.py +++ b/src/pas/plugins/ldap/testing.py @@ -4,10 +4,10 @@ from node.ext.ldap.interfaces import ILDAPGroupsConfig from node.ext.ldap.interfaces import ILDAPProps from node.ext.ldap.interfaces import ILDAPUsersConfig -from pas.plugins.ldap.cache import cacheProviderFactory -from pas.plugins.ldap.interfaces import ICacheSettingsRecordProvider -from pas.plugins.ldap.plonecontrolpanel.cache import CacheSettingsRecordProvider -from pas.plugins.ldap.properties import LDAPProps +from .cache import cacheProviderFactory +from .interfaces import ICacheSettingsRecordProvider +from .plonecontrolpanel.cache import CacheSettingsRecordProvider +from .properties import LDAPProps from plone.registry import Registry from plone.registry.interfaces import IRegistry from plone.testing import Layer @@ -18,16 +18,10 @@ from zope.component import provideUtility from zope.interface import implementer from zope.interface import Interface - - -try: - # plone 5.x with PlonePAS >=5.0 - from Products.PlonePAS.setuphandlers import migrate_root_uf - from Products.PlonePAS.setuphandlers import registerPluginTypes -except ImportError: - # plone 4.x with PlonePAS <5.0 - from Products.PlonePAS.Extensions.Install import migrate_root_uf - from Products.PlonePAS.Extensions.Install import registerPluginTypes +from Products.PlonePAS.setuphandlers import migrate_root_uf +from Products.PlonePAS.setuphandlers import registerPluginTypes +from zope.configuration import xmlconfig +from zope.dottedname.resolve import resolve SITE_OWNER_NAME = SITE_OWNER_PASSWORD = "admin" @@ -94,9 +88,6 @@ def setUpZCML(self): """ # Load dependent products's ZCML - from zope.configuration import xmlconfig - from zope.dottedname.resolve import resolve - def loadAll(filename): for p, config in self.products: if not config["loadZCML"]: diff --git a/src/pas/plugins/ldap/tests/test_doctests.py b/src/pas/plugins/ldap/tests/test_doctests.py index f4a4e93..9c61239 100644 --- a/src/pas/plugins/ldap/tests/test_doctests.py +++ b/src/pas/plugins/ldap/tests/test_doctests.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -from pas.plugins.ldap.testing import PASLDAPLayer +from ..testing import PASLDAPLayer from plone.testing import layered from plone.testing import z2 diff --git a/src/pas/plugins/ldap/tests/test_plugin.py b/src/pas/plugins/ldap/tests/test_plugin.py index 8964f59..d298356 100644 --- a/src/pas/plugins/ldap/tests/test_plugin.py +++ b/src/pas/plugins/ldap/tests/test_plugin.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -from pas.plugins.ldap.testing import PASLDAP_FIXTURE +from ..testing import PASLDAP_FIXTURE from Products.PlonePAS.plugins.ufactory import PloneUser import unittest diff --git a/src/pas/plugins/ldap/zmi/manage_plugin.py b/src/pas/plugins/ldap/zmi/manage_plugin.py index 97e0989..3c39d5f 100644 --- a/src/pas/plugins/ldap/zmi/manage_plugin.py +++ b/src/pas/plugins/ldap/zmi/manage_plugin.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -from pas.plugins.ldap.properties import BasePropertiesForm +from ..properties import BasePropertiesForm class ManageLDAPPlugin(BasePropertiesForm): From fa5cdb8aa48c4aaa796778e35772bde514c820e8 Mon Sep 17 00:00:00 2001 From: "Jens W. Klein" Date: Wed, 2 Feb 2022 11:03:49 +0100 Subject: [PATCH 02/74] automated pyupgrade find . -name "*.py" -exec pyupgrade --py3-only --py37-plus {} + --- src/pas/plugins/ldap/__init__.py | 1 - src/pas/plugins/ldap/cache.py | 16 ++++++------ src/pas/plugins/ldap/defaults.py | 1 - src/pas/plugins/ldap/interfaces.py | 1 - src/pas/plugins/ldap/monkey.py | 3 +-- .../ldap/plonecontrolpanel/__init__.py | 7 +++--- .../plugins/ldap/plonecontrolpanel/cache.py | 5 ++-- .../ldap/plonecontrolpanel/controlpanel.py | 5 ++-- .../ldap/plonecontrolpanel/exportimport.py | 7 +++--- .../ldap/plonecontrolpanel/inspector.py | 6 ++--- .../ldap/plonecontrolpanel/upgrades.py | 3 --- src/pas/plugins/ldap/plugin.py | 25 ++++++++----------- src/pas/plugins/ldap/properties.py | 13 +++++----- src/pas/plugins/ldap/setuphandlers.py | 1 - src/pas/plugins/ldap/sheet.py | 1 - src/pas/plugins/ldap/testing.py | 3 +-- src/pas/plugins/ldap/tests/__init__.py | 1 - src/pas/plugins/ldap/tests/test_doctests.py | 1 - src/pas/plugins/ldap/tests/test_plugin.py | 3 +-- src/pas/plugins/ldap/zmi/__init__.py | 1 - src/pas/plugins/ldap/zmi/manage_plugin.py | 1 - 21 files changed, 38 insertions(+), 67 deletions(-) diff --git a/src/pas/plugins/ldap/__init__.py b/src/pas/plugins/ldap/__init__.py index 0249a2b..9cf037d 100644 --- a/src/pas/plugins/ldap/__init__.py +++ b/src/pas/plugins/ldap/__init__.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- from AccessControl.Permissions import add_user_folders from . import monkey # noqa from . import LDAPPlugin diff --git a/src/pas/plugins/ldap/cache.py b/src/pas/plugins/ldap/cache.py index 9865038..b11a174 100644 --- a/src/pas/plugins/ldap/cache.py +++ b/src/pas/plugins/ldap/cache.py @@ -1,5 +1,3 @@ -# -*- coding: utf-8 -*- - from bda.cache import Memcached from bda.cache import NullCache from node.ext.ldap.interfaces import ICacheProviderFactory @@ -22,7 +20,7 @@ class PasLdapMemcached(Memcached): def __init__(self, servers): self._servers = servers - super(PasLdapMemcached, self).__init__(servers) + super().__init__(servers) @property def servers(self): @@ -32,18 +30,18 @@ def disconnect_all(self): self._client.disconnect_all() def __repr__(self): - return "<{0} {1}>".format(self.__class__.__name__, self.servers) + return f"<{self.__class__.__name__} {self.servers}>" @implementer(ICacheProviderFactory) -class cacheProviderFactory(object): +class cacheProviderFactory: # memcache factory for node.ext.ldap _thread_local = threading.local() @property def _key(self): - return "_v_{0}_PasLdapMemcached".format(self.__class__.__name__) + return f"_v_{self.__class__.__name__}_PasLdapMemcached" @property def servers(self): @@ -96,7 +94,7 @@ def get_plugin_cache(context): @implementer(IPluginCacheHandler) -class NullPluginCache(object): +class NullPluginCache: def __init__(self, context): self.context = context @@ -108,12 +106,12 @@ def set(self, value): @implementer(IPluginCacheHandler) -class RequestPluginCache(object): +class RequestPluginCache: def __init__(self, context): self.context = context def _key(self): - return "_v_ldap_ugm_{0}_".format(self.context.getId()) + return f"_v_ldap_ugm_{self.context.getId()}_" def get(self): request = getRequest() diff --git a/src/pas/plugins/ldap/defaults.py b/src/pas/plugins/ldap/defaults.py index 67796f3..425fe48 100644 --- a/src/pas/plugins/ldap/defaults.py +++ b/src/pas/plugins/ldap/defaults.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- from node.ext.ldap.scope import ONELEVEL diff --git a/src/pas/plugins/ldap/interfaces.py b/src/pas/plugins/ldap/interfaces.py index c96985a..f25cc41 100644 --- a/src/pas/plugins/ldap/interfaces.py +++ b/src/pas/plugins/ldap/interfaces.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- from zope.interface import Interface diff --git a/src/pas/plugins/ldap/monkey.py b/src/pas/plugins/ldap/monkey.py index 46c03f9..4da75cf 100644 --- a/src/pas/plugins/ldap/monkey.py +++ b/src/pas/plugins/ldap/monkey.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # TEMPORARY MONKEY PATCH # until this is changed upstream! from Acquisition import aq_inner @@ -46,7 +45,7 @@ def getPortraitFromSheet(context, userid): @implementer(ITraversable) -class PortraitTraverser(object): +class PortraitTraverser: def __init__(self, context, request=None): self.context = context self.request = request diff --git a/src/pas/plugins/ldap/plonecontrolpanel/__init__.py b/src/pas/plugins/ldap/plonecontrolpanel/__init__.py index c2af1ad..548c5b5 100644 --- a/src/pas/plugins/ldap/plonecontrolpanel/__init__.py +++ b/src/pas/plugins/ldap/plonecontrolpanel/__init__.py @@ -1,15 +1,14 @@ -# -*- coding: utf-8 -*- from Products.CMFPlone.interfaces.installable import INonInstallable from zope.interface import implementer @implementer(INonInstallable) -class HiddenProfiles(object): +class HiddenProfiles: """This hides zope2 profile from the quick installer tool and plone cpanel""" _hidden = [ - u"pas.plugins.ldap:default", - u"pas.plugins.ldap.plonecontrolpanel:install-base", + "pas.plugins.ldap:default", + "pas.plugins.ldap.plonecontrolpanel:install-base", ] def getNonInstallableProducts(self): diff --git a/src/pas/plugins/ldap/plonecontrolpanel/cache.py b/src/pas/plugins/ldap/plonecontrolpanel/cache.py index 89ea514..2717b8a 100644 --- a/src/pas/plugins/ldap/plonecontrolpanel/cache.py +++ b/src/pas/plugins/ldap/plonecontrolpanel/cache.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- from ..interfaces import ICacheSettingsRecordProvider from persistent import Persistent from plone.registry import field @@ -11,7 +10,7 @@ REGKEY = "pas.plugins.ldap.memcached" -class NullRecord(object): +class NullRecord: value = "" @@ -25,6 +24,6 @@ def __call__(self): records = registry.records if REGKEY not in records: # init if not exist - value = field.TextLine(title=u"servers, delimited by space") + value = field.TextLine(title="servers, delimited by space") records[REGKEY] = Record(value) return records[REGKEY] diff --git a/src/pas/plugins/ldap/plonecontrolpanel/controlpanel.py b/src/pas/plugins/ldap/plonecontrolpanel/controlpanel.py index 48f1753..17e6a69 100644 --- a/src/pas/plugins/ldap/plonecontrolpanel/controlpanel.py +++ b/src/pas/plugins/ldap/plonecontrolpanel/controlpanel.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- from ..properties import BasePropertiesForm from Products.CMFCore.interfaces import ISiteRoot from Products.CMFPlone.resources import add_bundle_on_request @@ -16,7 +15,7 @@ def getPortal(): class LDAPControlPanel(BasePropertiesForm): def __init__(self, context, request): - super(LDAPControlPanel, self).__init__(context, request) + super().__init__(context, request) add_bundle_on_request(request, "yafowil") def next(self, request): @@ -33,4 +32,4 @@ def plugin(self): def save(self, widget, data): BasePropertiesForm.save(self, widget, data) messages = IStatusMessage(self.request) - messages.addStatusMessage(_(u"LDAP Settings saved."), type="info") + messages.addStatusMessage(_("LDAP Settings saved."), type="info") diff --git a/src/pas/plugins/ldap/plonecontrolpanel/exportimport.py b/src/pas/plugins/ldap/plonecontrolpanel/exportimport.py index 43eb9aa..3316839 100644 --- a/src/pas/plugins/ldap/plonecontrolpanel/exportimport.py +++ b/src/pas/plugins/ldap/plonecontrolpanel/exportimport.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- from BTrees.OOBTree import OOBTree from Products.GenericSetup.interfaces import IBody from Products.GenericSetup.utils import XMLAdapterBase @@ -14,7 +13,7 @@ def _get_import_export_handler(context): pasldap = aclu.pasldap handler = queryMultiAdapter((pasldap, context), IBody) if handler is not None: - handler.filename = "%s%s" % (handler.name, handler.suffix) + handler.filename = f"{handler.name}{handler.suffix}" return handler logger.warning("Can't find handler for ldap settings") @@ -91,7 +90,7 @@ def _setDataAndType(self, data, node): node.setAttribute("type", "string") else: self._logger.warning( - "Invalid type {0:s} found for key {1:s} on export, skipped.".format( + "Invalid type {:s} found for key {:s} on export, skipped.".format( type(data), data ) ) @@ -130,7 +129,7 @@ def _getDataByType(self, node): data = str(data) else: self._logger.warning( - "Invalid type {0:s} found on import, skipped.".format(vtype) + f"Invalid type {vtype:s} found on import, skipped." ) data = None return data diff --git a/src/pas/plugins/ldap/plonecontrolpanel/inspector.py b/src/pas/plugins/ldap/plonecontrolpanel/inspector.py index d58feea..1622b56 100644 --- a/src/pas/plugins/ldap/plonecontrolpanel/inspector.py +++ b/src/pas/plugins/ldap/plonecontrolpanel/inspector.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- from node.ext.ldap import LDAPNode from node.ext.ldap.interfaces import ILDAPGroupsConfig from node.ext.ldap.interfaces import ILDAPProps @@ -9,11 +8,10 @@ from zope.component import getUtility import json -import six def safe_encode(val): - if isinstance(val, six.text_type): + if isinstance(val, str): return val.encode("utf-8") return val @@ -55,7 +53,7 @@ def node_attributes(self): if not node.attrs.is_binary(key): ret[safe_unicode(key)] = safe_unicode(val) else: - ret[safe_unicode(key)] = "(Binary Data with {0} Bytes)".format( + ret[safe_unicode(key)] = "(Binary Data with {} Bytes)".format( len(val) ) except UnicodeDecodeError: diff --git a/src/pas/plugins/ldap/plonecontrolpanel/upgrades.py b/src/pas/plugins/ldap/plonecontrolpanel/upgrades.py index 17afc93..24cbce5 100644 --- a/src/pas/plugins/ldap/plonecontrolpanel/upgrades.py +++ b/src/pas/plugins/ldap/plonecontrolpanel/upgrades.py @@ -1,6 +1,3 @@ -# -*- coding: utf-8 -*- - - def remove_persistent_import_step_from_base_profile(context): """Remove broken persistent import step from base profile. diff --git a/src/pas/plugins/ldap/plugin.py b/src/pas/plugins/ldap/plugin.py index 8f12f87..16b8109 100644 --- a/src/pas/plugins/ldap/plugin.py +++ b/src/pas/plugins/ldap/plugin.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- from AccessControl import ClassSecurityInfo from AccessControl.class_init import InitializeClass from BTrees import OOBTree @@ -17,7 +16,6 @@ from Products.PluggableAuthService.permissions import ManageGroups from Products.PluggableAuthService.permissions import ManageUsers from Products.PluggableAuthService.plugins.BasePlugin import BasePlugin -from six.moves import map from zope.interface import implementer import ldap @@ -30,10 +28,7 @@ logger = logging.getLogger("pas.plugins.ldap") zmidir = os.path.join(os.path.dirname(__file__), "zmi") -if six.PY2: - process_time = time.clock -else: - process_time = time.process_time +process_time = time.process_time LDAP_ERROR_LOG_TIMEOUT = float( os.environ.get("PAS_PLUGINS_LDAP_ERROR_LOG_TIMEOUT", 300.0) @@ -66,7 +61,7 @@ def _wrapper(self, *args, **kwargs): waiting = time.time() - self._v_ldaperror_timeout if waiting < LDAP_ERROR_LOG_TIMEOUT: logger.debug( - "{0}: retry wait {1:0.5f} of {2:0.0f}s -> {3}".format( + "{}: retry wait {:0.5f} of {:0.0f}s -> {}".format( prefix, waiting, LDAP_ERROR_LOG_TIMEOUT, @@ -79,7 +74,7 @@ def _wrapper(self, *args, **kwargs): start = process_time() result = original_method(self, *args, **kwargs) delta_t = process_time() - start - msg = "Call of {0!r} took {1:0.4f}s".format(original_method, delta_t) + msg = f"Call of {original_method!r} took {delta_t:0.4f}s" if delta_t < LDAP_LONG_RUNNING_LOG_THRESHOLD: logger.debug(msg) else: @@ -90,12 +85,12 @@ def _wrapper(self, *args, **kwargs): except ldap.LDAPError as e: self._v_ldaperror_msg = str(e) self._v_ldaperror_timeout = time.time() - logger.exception("LDAPError in {0}".format(prefix)) + logger.exception(f"LDAPError in {prefix}") return default except Exception as e: self._v_ldaperror_msg = str(e) self._v_ldaperror_timeout = time.time() - logger.exception("Error in {0}".format(prefix)) + logger.exception(f"Error in {prefix}") return default return _wrapper @@ -396,7 +391,7 @@ def enumerateUsers( return default # XXX: sort_by in node.ext.ldap if login: - if not isinstance(login, six.string_types): + if not isinstance(login, str): # XXX raise NotImplementedError("sequence is not supported yet.") kw["login"] = login @@ -404,7 +399,7 @@ def enumerateUsers( if "login" in kw and "name" in kw: del kw["name"] if id: - if not isinstance(id, six.string_types): + if not isinstance(id, str): # XXX raise NotImplementedError("sequence is not supported yet.") kw["id"] = id @@ -558,7 +553,7 @@ def getPropertiesForUser(self, user_or_group, request=None): if not self.is_plugin_active(pas_interfaces.IPropertiesPlugin): return default ugid = user_or_group.getId() - if not isinstance(ugid, six.text_type): + if not isinstance(ugid, str): ugid = ugid.decode("utf-8") try: if self.enumerateUsers(id=ugid) or self.enumerateGroups(id=ugid): @@ -611,7 +606,7 @@ def doChangeUser(self, user_id, password, **kw): try: self.users.passwd(user_id, None, password) except KeyError: - msg = "{0:s} is not an LDAP user.".format(user_id) + msg = f"{user_id:s} is not an LDAP user." logger.warn(msg) raise RuntimeError(msg) @@ -673,7 +668,7 @@ def getGroupById(self, group_id): return default if group_id is None: return None - if not isinstance(group_id, six.text_type): + if not isinstance(group_id, str): group_id = group_id.decode("utf8") groups = self.groups if not groups or group_id not in list(groups.keys()): diff --git a/src/pas/plugins/ldap/properties.py b/src/pas/plugins/ldap/properties.py index 2f3b19e..7a7bbef 100644 --- a/src/pas/plugins/ldap/properties.py +++ b/src/pas/plugins/ldap/properties.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- from node.ext.ldap.interfaces import ILDAPGroupsConfig from node.ext.ldap.interfaces import ILDAPProps from node.ext.ldap.interfaces import ILDAPUsersConfig @@ -88,7 +87,7 @@ def form(self): if not controller.next: return controller.rendered self.request.RESPONSE.redirect(controller.next) - return u"" + return "" def save(self, widget, data): props = ILDAPProps(self.plugin) @@ -241,7 +240,7 @@ def _setter(context, value): @implementer(ILDAPProps) @adapter(ILDAPPlugin) -class LDAPProps(object): +class LDAPProps: def __init__(self, plugin): self.plugin = plugin @@ -270,7 +269,7 @@ def memcached(self): if recordProvider is not None: record = recordProvider() return record.value - return u"feature not available" + return "feature not available" @memcached.setter def memcached(self, value): @@ -279,7 +278,7 @@ def memcached(self, value): record = recordProvider() record.value = value else: - return u"feature not available" + return "feature not available" binary_attributes = BINARY_DEFAULTS multivalued_attributes = MULTIVALUED_DEFAULTS @@ -287,7 +286,7 @@ def memcached(self, value): @implementer(ILDAPUsersConfig) @adapter(ILDAPPlugin) -class UsersConfig(object): +class UsersConfig: def __init__(self, plugin): self.plugin = plugin @@ -317,7 +316,7 @@ def expiresUnit(self): @implementer(ILDAPGroupsConfig) @adapter(ILDAPPlugin) -class GroupsConfig(object): +class GroupsConfig: def __init__(self, plugin): self.plugin = plugin diff --git a/src/pas/plugins/ldap/setuphandlers.py b/src/pas/plugins/ldap/setuphandlers.py index df1cf5f..74127d4 100644 --- a/src/pas/plugins/ldap/setuphandlers.py +++ b/src/pas/plugins/ldap/setuphandlers.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- from .plugin import LDAPPlugin from zope.component.hooks import getSite diff --git a/src/pas/plugins/ldap/sheet.py b/src/pas/plugins/ldap/sheet.py index 6adcd6d..6c5c6ec 100644 --- a/src/pas/plugins/ldap/sheet.py +++ b/src/pas/plugins/ldap/sheet.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- from Acquisition import aq_base from .interfaces import ILDAPGroupsConfig from .interfaces import ILDAPUsersConfig diff --git a/src/pas/plugins/ldap/testing.py b/src/pas/plugins/ldap/testing.py index 3f50478..bba0c8b 100644 --- a/src/pas/plugins/ldap/testing.py +++ b/src/pas/plugins/ldap/testing.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- from node.ext.ldap import testing as ldaptesting from node.ext.ldap.interfaces import ICacheProviderFactory from node.ext.ldap.interfaces import ILDAPGroupsConfig @@ -97,7 +96,7 @@ def loadAll(filename): xmlconfig.file( filename, package, context=self["configurationContext"] ) - except IOError: + except OSError: pass loadAll("meta.zcml") diff --git a/src/pas/plugins/ldap/tests/__init__.py b/src/pas/plugins/ldap/tests/__init__.py index 40a96af..e69de29 100644 --- a/src/pas/plugins/ldap/tests/__init__.py +++ b/src/pas/plugins/ldap/tests/__init__.py @@ -1 +0,0 @@ -# -*- coding: utf-8 -*- diff --git a/src/pas/plugins/ldap/tests/test_doctests.py b/src/pas/plugins/ldap/tests/test_doctests.py index 9c61239..861c8db 100644 --- a/src/pas/plugins/ldap/tests/test_doctests.py +++ b/src/pas/plugins/ldap/tests/test_doctests.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- from ..testing import PASLDAPLayer from plone.testing import layered from plone.testing import z2 diff --git a/src/pas/plugins/ldap/tests/test_plugin.py b/src/pas/plugins/ldap/tests/test_plugin.py index d298356..21d7b25 100644 --- a/src/pas/plugins/ldap/tests/test_plugin.py +++ b/src/pas/plugins/ldap/tests/test_plugin.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- from ..testing import PASLDAP_FIXTURE from Products.PlonePAS.plugins.ufactory import PloneUser @@ -68,7 +67,7 @@ def test_IGroupEnumerationPlugin_id(self): [[("id", "group2"), ("pluginid", "pasldap")]], ) self.assertEqual( - sorted([_["id"] for _ in self.ldap.enumerateGroups(id="group*")]), + sorted(_["id"] for _ in self.ldap.enumerateGroups(id="group*")), [ "group0", "group1", diff --git a/src/pas/plugins/ldap/zmi/__init__.py b/src/pas/plugins/ldap/zmi/__init__.py index 40a96af..e69de29 100644 --- a/src/pas/plugins/ldap/zmi/__init__.py +++ b/src/pas/plugins/ldap/zmi/__init__.py @@ -1 +0,0 @@ -# -*- coding: utf-8 -*- diff --git a/src/pas/plugins/ldap/zmi/manage_plugin.py b/src/pas/plugins/ldap/zmi/manage_plugin.py index 3c39d5f..b6a7893 100644 --- a/src/pas/plugins/ldap/zmi/manage_plugin.py +++ b/src/pas/plugins/ldap/zmi/manage_plugin.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- from ..properties import BasePropertiesForm From a3ea9b1d6dbfae9282e8a38ac65f36f684a97faa Mon Sep 17 00:00:00 2001 From: "Jens W. Klein" Date: Wed, 9 Feb 2022 12:41:52 +0100 Subject: [PATCH 03/74] updates for plone 6, also make file based install --- .github/workflows/tests.yaml | 36 ++ .gitignore | 28 +- Makefile | 343 ++++++++++++++++++ README_MAKE.md | 78 ++++ build_ldap.sh | 11 + constraints.txt | 1 + instance.yaml | 17 + requirements.txt | 6 + sources.ini | 45 +++ src/pas/plugins/ldap/__init__.py | 10 +- src/pas/plugins/ldap/cache.py | 6 +- src/pas/plugins/ldap/cache_volatile.zcml | 3 +- src/pas/plugins/ldap/configure.zcml | 57 +-- .../ldap/plonecontrolpanel/configure.zcml | 117 +++--- .../ldap/plonecontrolpanel/exportimport.py | 4 +- .../profiles/default/componentregistry.xml | 7 +- .../profiles/default/controlpanel.xml | 7 +- .../profiles/default/ldapsettings.xml | 152 +++++--- .../profiles/default/registry.xml | 2 +- src/pas/plugins/ldap/plugin.py | 10 +- src/pas/plugins/ldap/properties.py | 6 +- src/pas/plugins/ldap/sheet.py | 4 +- src/pas/plugins/ldap/testing.py | 23 +- src/pas/plugins/ldap/tests/test_plugin.py | 4 +- src/pas/plugins/ldap/zmi/configure.zcml | 19 +- 25 files changed, 812 insertions(+), 184 deletions(-) create mode 100644 .github/workflows/tests.yaml create mode 100644 Makefile create mode 100644 README_MAKE.md create mode 100755 build_ldap.sh create mode 100644 constraints.txt create mode 100644 instance.yaml create mode 100644 requirements.txt create mode 100644 sources.ini diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml new file mode 100644 index 0000000..4d3f9c4 --- /dev/null +++ b/.github/workflows/tests.yaml @@ -0,0 +1,36 @@ +name: Test the pas.plugins.ldap code +on: + push + +jobs: + build: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + python: + - "3.8" + - "3.9" + plone: + - "6.0.0a3" + + steps: + - uses: actions/checkout@v2 + + - name: Install xmllint + run: sudo apt-get install -y slapd ldap-utils + + - name: Setup Plone ${{ matrix.plone }} with Python ${{ matrix.python }} + id: setup + uses: plone/setup-plone@v1.0.0 + with: + python-version: ${{ matrix.python }} + plone-version: ${{ matrix.plone }} + + - name: Install package + run: | + make VENV=off install + + - name: Run tests + run: | + make VENV=off test-ignore-warnings diff --git a/.gitignore b/.gitignore index b40829b..94a6894 100644 --- a/.gitignore +++ b/.gitignore @@ -1,25 +1,27 @@ -*.pyc +__pycache__ .*.cfg .coverage -.mrsd +.installed.txt .python-version -/*eggs/ +*-dev.txt +*-mxdev.txt +*.egg-info +*.mo +*.pyc +*.pyo +/.make-sentinels/ /.project /.pydevproject /.Python /.settings/ /.vscode/ -/bin +/*eggs/ /coverage/ -/devsrc/ /dist/ /htmlcov/ -/include/ -/lib/ -/local/ -/parts/ -/share/ -/src/*.egg-info -/var/ -/buildout.cfg +/instance/ /pip-selfcheck.json +/sources/ +/src/*.egg-info +/venv/ +/.openldap/ diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..5bc86d4 --- /dev/null +++ b/Makefile @@ -0,0 +1,343 @@ +# Makefile to configure and run Plone instance + +############################################################################## +# SETUP MAKE + +## Defensive settings for make: https://tech.davis-hansson.com/p/make/ +SHELL:=bash +.ONESHELL: +# for Makefile debugging purposes add -x to the .SHELLFLAGS +.SHELLFLAGS:=-eu -o pipefail -O inherit_errexit -c +.SILENT: +.DELETE_ON_ERROR: +MAKEFLAGS+=--warn-undefined-variables +MAKEFLAGS+=--no-builtin-rules + +# Colors +# OK=Green, warn=yellow, error=red +ifeq ($(TERM),) +# no colors if not in terminal + MARK_COLOR= + OK_COLOR= + WARN_COLOR= + ERROR_COLOR= + NO_COLOR= +else + MARK_COLOR=`tput setaf 6` + OK_COLOR=`tput setaf 2` + WARN_COLOR=`tput setaf 3` + ERROR_COLOR=`tput setaf 1` + NO_COLOR=`tput sgr0` +endif + +############################################################################## +# SETTINGS AND VARIABLE +# adjust to your project needs +PROJECT_NAME=pas.plugins.ldap +IMAGE_NAME=${PROJECT_NAME} +CONSTRAINTS=constraints.txt +PIP_REQUIREMENTS_IN_FILE=requirements.txt +ADDONBASE=./ +ADDONFOLDER=${ADDONBASE}src/ +# it is possible to define an alternative YAML file for the the instance.set default i +INSTANCE_YAML?=instance.yaml +INSTANCE_FOLDER?=instance + +PIP_PARAMS= --pre + +############################################################################## +# targets and prerequisites +# target has to be one file, otherwise step gets executes for each file separate +PREPARE_PREREQUISITES=${PIP_REQUIREMENTS_IN_FILE} ${CONSTRAINTS} sources.ini ${ADDONBASE}setup.cfg +PREPARE_TARGET=requirements-mxdev.txt +INSTALL_PREREQUSISTES=${PREPARE_TARGET} +INSTALL_TARGET=.installed.txt +INSTANCE_PREREQUISITES=${INSTALL_TARGET} ${INSTANCE_YAML} +INSTANCE_TARGET=${INSTANCE_FOLDER}/etc/zope.ini ${INSTANCE_FOLDER}/etc/zope.conf ${INSTANCE_FOLDER}/etc/site.zcml +TEST_PREREQUISITES=${INSTALL_TARGET} +RUN_PREREQUISITES=${INSTANCE_TARGET} + +############################################################################## +# CONVINIENCE + +# install and run +.PHONY: all # full install, test and run +all:style testrun + +# Add the following 'help' target to your Makefile +# And add help text after each target name starting with '\#\#' +.PHONY: help +help: ## This help message + @echo "${OK_COLOR}This is the Makefile for ${WARN_COLOR}${PROJECT_NAME}${NO_COLOR}" + @echo + @echo "${WARN_COLOR}Additional parameters:${NO_COLOR}" + @echo "${MARK_COLOR}PYTHON${NO_COLOR}: Python interpreter to be used (default: python3)" + @echo "${MARK_COLOR}VENV${NO_COLOR}: [on|off] wether to create a Python virtual environment or not (default: on)"s + @echo "${MARK_COLOR}VENV_FOLDER${NO_COLOR}: location of the virtual environment (default: ./venv)" + @echo + @echo "${WARN_COLOR}Targets:${NO_COLOR}" + @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}' + +############################################################################## +# targets and prerequisites +# target has to be one file, otherwise step gets executes for each file separate +PREPARE_PREREQUISITES=${PIP_REQUIREMENTS_IN_FILE} ${CONSTRAINTS} sources.ini ${ADDONBASE}setup.cfg +PREPARE_TARGET=requirements-mxdev.txt +INSTALL_PREREQUSISTES=${PREPARE_TARGET} +INSTALL_TARGET=.installed.txt +INSTANCE_PREREQUISITES=${INSTALL_TARGET} ${INSTANCE_YAML} +INSTANCE_TARGET=${INSTANCE_FOLDER}/etc/zope.ini ${INSTANCE_FOLDER}/etc/zope.conf ${INSTANCE_FOLDER}/etc/site.zcml +TEST_PREREQUISITES=${INSTALL_TARGET} +RUN_PREREQUISITES=${INSTANCE_TARGET} + +############################################################################## +# BASE + +SENTINELFOLDER=.make-sentinels/ +SENTINEL=${SENTINELFOLDER}ABOUT.txt +${SENTINEL}: + @mkdir -p ${SENTINELFOLDER} + @echo "Sentinels for the Makefile process." > ${SENTINEL} + +# PYTHON, VENV, PIP +# venv and pybin +PYTHON?=python3 +VENV?=on +ifeq ("${VENV}", "on") + VENV_FOLDER?=./venv + PYBIN=${VENV_FOLDER}/bin/ +else + VENV_FOLDER?= + ifneq ("${VENV_FOLDER}", "") + PYBIN=${VENV_FOLDER}/bin/ + PYTHON=${PYBIN}python + else + PYBIN= + endif +endif + +# installed? +ifeq (, $(shell which $(PYTHON) )) + $(error "PYTHON=$(PYTHON) not found in $(PATH)") +endif + +# version ok? +PYTHON_VERSION_MIN=3.7 +PYTHON_VERSION_OK=$(shell $(PYTHON) -c 'import sys; print(int(float("%d.%d"% sys.version_info[0:2]) >= float($(PYTHON_VERSION_MIN))))' ) +ifeq ($(PYTHON_VERSION_OK),0) + $(error "Need python $(PYTHON_VERSION) >= $(PYTHON_VERSION_MIN)") +endif + +VENV_SENTINEL=${SENTINELFOLDER}venv.sentinel +${VENV_SENTINEL}: ${SENTINEL} +ifeq ("${VENV}", "on") + @echo "$(OK_COLOR)Setup Python Virtual Environment under '${VENV_FOLDER}' $(NO_COLOR)" + @${PYTHON} -m venv ${VENV_FOLDER} +else + @echo "$(OK_COLOR)Use current local or global Python: `which ${PYTHON}` $(NO_COLOR)" +endif + @touch ${VENV_SENTINEL} + +PIP_SENTINEL=${SENTINELFOLDER}pip.sentinel +${PIP_SENTINEL}: ${VENV_SENTINEL} ${CONSTRAINTS} ${SENTINEL} + @echo "$(OK_COLOR)Install pip$(NO_COLOR)" + @${PYBIN}pip install -U "pip>=22.0.2" wheel setuptools + @touch ${PIP_SENTINEL} + +############################################################################## +# MXDEV + +MXDEV_SENTINEL=${SENTINELFOLDER}pip-mxdev.sentinel +${MXDEV_SENTINEL}: ${PIP_SENTINEL} + @echo "$(OK_COLOR)Install mxdev$(NO_COLOR)" + @${PYBIN}pip install "mxdev>=2.0.0" + @touch ${MXDEV_SENTINEL} + +.PHONY: prepare +prepare: ${PREPARE_TARGET} ## prepare soures and dependencies + +${PREPARE_PREREQUISITES}: + @touch $@ + +${PREPARE_TARGET}: ${MXDEV_SENTINEL} ${PREPARE_PREREQUISITES} + @echo "$(OK_COLOR)Prepare sources and dependencies$(NO_COLOR)" + @${PYBIN}mxdev -c sources.ini + +.PHONY: install +install: ${INSTALL_TARGET} ## pip install all dependencies and scripts + +${INSTALL_TARGET}: ${PREPARE_TARGET} + @echo "$(OK_COLOR)Install dependencies and scripts$(NO_COLOR)" + @${PYBIN}pip install -r ${PREPARE_TARGET} ${PIP_PARAMS} + @${PYBIN}pip freeze >${INSTALL_TARGET} + +############################################################################## +# INSTANCE + +COOKIECUTTER_SENTINEL=${SENTINELFOLDER}pip-cookiecutter.sentinel +${COOKIECUTTER_SENTINEL}: + @echo "$(OK_COLOR)Install cookiecutter$(NO_COLOR)" + @${PYBIN}pip install git+https://github.com/cookiecutter/cookiecutter.git#egg=cookiecutter + @touch ${COOKIECUTTER_SENTINEL} + +${INSTANCE_YAML}: + @touch ${INSTANCE_YAML} + +.PHONY: instance +instance: ${INSTANCE_TARGET} ## create configuration for an zope (plone) instance + +${INSTANCE_TARGET}: ${INSTANCE_PREREQUISITES} ${COOKIECUTTER_SENTINEL} ${INSTANCE_YAML} + @echo "$(OK_COLOR)Create Plone/Zope configuration from ${INSTANCE_YAML} at ${INSTANCE_FOLDER}$(NO_COLOR)" + @${PYBIN}cookiecutter -f --no-input --config-file ${INSTANCE_YAML} https://github.com/bluedynamics/cookiecutter-zope-instance +# @${PYBIN}cookiecutter -f --no-input --config-file ${INSTANCE_YAML} devsrc/cookiecutter-zope-instance +############################################################################## +# TESTING + +TESTRUNNER_SENTINEL=${SENTINELFOLDER}pip-testrunner.sentinel +${TESTRUNNER_SENTINEL}: ${PIP_SENTINEL} + @echo "$(OK_COLOR)Install zope.testrunner$(NO_COLOR)" + @${PYBIN}pip install zope.testrunner + @touch ${TESTRUNNER_SENTINEL} + +LDAPRELEASE=2.4.59 +BASE_DIR=. +OPENLDAP_BASE_DIR=${BASE_DIR}/.openldap +OPENLDAP_DOWNLOAD_DIR=${OPENLDAP_BASE_DIR}/downloads +OPENLDAP_ARCHIVE=${OPENLDAP_DOWNLOAD_DIR}/openldap-${LDAPRELEASE}.tgz +${OPENLDAP_ARCHIVE}: + @echo "$(OK_COLOR)Download openldap-${LDAPRELEASE}.tgz to ${OPENLDAP_DOWNLOAD_DIR} as ${OPENLDAP_ARCHIVE}$(NO_COLOR)" + @mkdir -p ${OPENLDAP_DOWNLOAD_DIR} + @curl -o ${OPENLDAP_ARCHIVE} https://www.openldap.org/software/download/OpenLDAP/openldap-release/openldap-${LDAPRELEASE}.tgz + +OPENLDAP_INSTALL_DIR=${OPENLDAP_BASE_DIR}/inst +OPENLDAP_BIN_DIR=${OPENLDAP_INSTALL_DIR}/bin +OPENLDAP_SLAPD=${OPENLDAP_INSTALL_DIR}/libexec/slapd +${OPENLDAP_SLAPD}: ${OPENLDAP_ARCHIVE} + @echo "$(OK_COLOR)Unpack ${OPENLDAP_ARCHIVE}$(NO_COLOR)" + @mkdir -p ${OPENLDAP_INSTALL_DIR} + @tar xf ${OPENLDAP_ARCHIVE} -C ${OPENLDAP_BASE_DIR} + @echo "$(OK_COLOR)Build OpenLDAP ${LDAPRELEASE}$(NO_COLOR)" + @./build_ldap.sh ${OPENLDAP_BASE_DIR}/openldap-${LDAPRELEASE} ${shell realpath ${OPENLDAP_INSTALL_DIR}} + +.PHONY: test +test: ${TEST_PREREQUISITES} ${TESTRUNNER_SENTINEL} ${OPENLDAP_SLAPD} ## run tests + @echo "$(OK_COLOR)Run addon tests$(NO_COLOR)" + @SLAPD_BIN=${OPENLDAP_SLAPD} LDAP_ADD_BIN=${OPENLDAP_BIN_DIR}/ldapadd LDAP_DELETE_BIN=${OPENLDAP_BIN_DIR}/ldapdelete SLAPD_URIS=ldap://127.0.0.1:12345 ${PYBIN}zope-testrunner --auto-color --auto-progress --test-path=${ADDONFOLDER} + +.PHONY: test-ignore-warnings +test-ignore-warnings: ${TEST_PREREQUISITES} ${TESTRUNNER_SENTINEL} ${OPENLDAP_SLAPD} ## run tests (hide warnings) + @echo "$(OK_COLOR)Run addon tests$(NO_COLOR)" + @SLAPD_BIN=${OPENLDAP_SLAPD} LDAP_ADD_BIN=${OPENLDAP_BIN_DIR}/ldapadd LDAP_DELETE_BIN=${OPENLDAP_BIN_DIR}/ldapdelete SLAPD_URIS=ldap://127.0.0.1:12345 PYTHONWARNINGS=ignore ${PYBIN}zope-testrunner --auto-color --auto-progress --test-path=${ADDONFOLDER} + +############################################################################## +# CODE FORMATTING + +BLACK_SENTINEL=${SENTINELFOLDER}pip-black.sentinel +${BLACK_SENTINEL}: ${PREPARE_TARGET} + @echo "$(OK_COLOR)Install black$(NO_COLOR)" + @${PYBIN}pip install black + @touch ${BLACK_SENTINEL} + +ISORT_SENTINEL=${SENTINELFOLDER}pip-isort.sentinel +${ISORT_SENTINEL}: ${PREPARE_TARGET} + @echo "$(OK_COLOR)Install isort$(NO_COLOR)" + @${PYBIN}pip install isort + @touch ${ISORT_SENTINEL} + +ZPRETTY_SENTINEL=${SENTINELFOLDER}pip-zpretty.sentinel +${ZPRETTY_SENTINEL}: ${PREPARE_TARGET} + @echo "$(OK_COLOR)Install zpretty$(NO_COLOR)" + @${PYBIN}pip install "zpretty>=2.2.0" + @touch ${ZPRETTY_SENTINEL} + +.PHONY: apply-style-black +apply-style-black: ${BLACK_SENTINEL} ## apply/format code style black (to Python files) + @echo "$(OK_COLOR)Apply style black rules to code in ${ADDONFOLDER}/*$(NO_COLOR)" + @${PYBIN}black ${ADDONFOLDER} + +.PHONY: apply-style-isort +apply-style-isort: ${ISORT_SENTINEL} ## apply/format code style isort (sorted imports in Python files) + @echo "$(OK_COLOR)Apply style isort rules to code in ${ADDONFOLDER}/*$(NO_COLOR)" + @${PYBIN}isort ${ADDONFOLDER} + +.PHONY: apply-style-zpretty +apply-style-zpretty: ${ZPRETTY_SENTINEL} ## apply/format code style zpretty (to XML/ZCML files) + @echo "$(OK_COLOR)Apply style zpretty rules to code in ${ADDONFOLDER}/*$(NO_COLOR)" + @find ${ADDONFOLDER} -name '*.zcml' -exec ${PYBIN}zpretty -iz {} + + @find ${ADDONFOLDER} -name "*.xml"|grep -v locales|xargs ${PYBIN}zpretty -ix + +.PHONY: style ## apply code styles black, isort and zpretty +style: apply-style-black apply-style-isort apply-style-zpretty + +.PHONY: format ## alias for "style" +FORMATTING: style + +.PHONY: lint-black +lint-black: ${BLACK_SENTINEL} ## lint code-style black (to Python files) + @echo "$(OK_COLOR)Lint black rules to code in ${ADDONFOLDER}/*$(NO_COLOR)" + @${PYBIN}black --check ${ADDONFOLDER} + +.PHONY: lint-isort +lint-isort: ${ISORT_SENTINEL} ## lint code-style isort (sorted imports in Python files) + @echo "$(OK_COLOR)Lint style isort rules to code in ${ADDONFOLDER}/*$(NO_COLOR)" + @${PYBIN}isort --check-only ${ADDONFOLDER} + +.PHONY: lint-zpretty +lint-zpretty: ${ZPRETTY_SENTINEL} ## lint code-style zpretty (to XML/ZCML files) + @echo "$(OK_COLOR)Lint style zpretty rules to code in ${ADDONFOLDER}/*$(NO_COLOR)" + @find ${ADDONFOLDER} -name '*.zcml' -exec ${PYBIN}zpretty --check -z {} + + @find ${ADDONFOLDER} -name '*.xml'|grep -v locales|xargs ${PYBIN}zpretty --check -x + +.PHONY: lint ## lint all: check if complies with code-styles black, isort and zpretty +lint: lint-black lint-isort lint-zpretty + +############################################################################## +# RUN + +.PHONY: run +run: ${RUN_PREREQUISITES} ## run/start Plone + @echo "$(OK_COLOR)Run Plone$(NO_COLOR)" + @${PYBIN}runwsgi -v instance/etc/zope.ini + +############################################################################## +# CLEAN +.PHONY: clean-venv +clean-venv: ## remove Python virtual environment +ifeq ("${VENV}", "on") + @echo "$(OK_COLOR)Remove Virtualenv.$(NO_COLOR)" + rm -rf ${VENV_FOLDER} ${SENTINELFOLDER}/pip*.sentinel ${VENV_SENTINEL} +else: + @echo "$(OK_WARN)No self-created Python virtualenv at '${VENV_FOLDER}'! Nothing to do.$(NO_COLOR)" +endif + +.PHONY: clean-pyc +clean-pyc: ## remove Python file artifacts + @echo "$(OK_COLOR)Remove Python file artifacts (like byte-code) of code in current directory.$(NO_COLOR)" + find . -name '*.pyc' -exec rm -f {} + + find . -name '*.pyo' -exec rm -f {} + + find . -name '*~' -exec rm -f {} + + find . -name '__pycache__' -exec rm -fr {} + + +.PHONY: clean-make +clean-make: ## remove make artifact @echo "$(OK_COLOR)Remove Plone/Zope configuration (keeps data) and sentinel files.$(NO_COLOR)" + rm -rf ${INSTALL_PREREQUSISTES} ${INSTANCE_TARGET} ${SENTINELFOLDER} constraints-mxdev.txt + +.PHONY: clean-instance +clean-instance: ## remove instance configuration (keeps data) + @echo "$(OK_COLOR)Remove Plone/Zope configuration (keeps data) and sentinel files.$(NO_COLOR)" + rm -f ${INSTANCE_TARGET} + +.PHONY: clean +clean: clean-venv clean-pyc clean-make clean-instance ## clean all (except local database and pip installed packages) + +############################################################################## +# DOCKER/CONTAINER + +# this needs a Dockerfile, which is not provided by plone-kickstarter +.PHONY: build-image +build-image: ## Build Docker Image +ifneq ("$(wildcard Dockerfile)", "") + @docker build . -t $(IMAGE_NAME) -f Dockerfile +else + @echo "$(ERROR_COLOR)A 'Dockerfile' is required to build an image.$(NO_COLOR)" +endif diff --git a/README_MAKE.md b/README_MAKE.md new file mode 100644 index 0000000..f398b94 --- /dev/null +++ b/README_MAKE.md @@ -0,0 +1,78 @@ +# Information about the Makefile + +How to use the pip, mxdev, cookiecutter-zope-instance and make based install. + +## Usage + +On the commandline, execute the ``make`` command. +Without any options, make will run all steps. + +```bash +make run +``` + +All options are printed with + +```bash +make help +``` + +```text +clean remove instance configuration (keeps data) +help This help message +install pip install all dependencies and scripts +instance create configuration for an zope (plone) instance +prepare prepare soures and dependencies +run run Plone +``` + +Order for ``make run`` is: *prepare*, *install*, *instance*, *run*. + +The Makefile is built to detect changes. +At the first ``make run`` all steps are excuted. +Subsequent calls are only starting the applicaton server in the *run* step. +If one of the input file is changed, steps needed to take those changes into effect are executed again.\ + +## Python + +The Makefile support different modes of Python: + +1. Create new virtualenv under `./venv` (default) from a global Python 3. `python3` is expected to be in the PATH. +2. Like (1), but the `VENV_FOLDER` is passed to every *make* call: `make VENV_FOLDER=./some_folder/ install`. +3. `make VENV=off install`: Direct usage of current configure Python 3 environment. + Like if one uses *pyenv* or another already activated virtual environment, or in CI if the environment is alredy isolated. +4. Like (3), but the environment is not activated, so we need to point *make* to the location with `make VENV=off VENV_FOLDER=~/myenv/myproject_venv` or alike. + +**Attention:** if those paramters are used, they *must* be passed to every make call! + +**Hint:** Edit the `Makefile` and look for `VENV?=on` (which sets the default). And `VENV_FOLDER?=` (look for the if before) and adjust to your needs. + +## Files + +Initially the files below were generated by `plone-kickstarter`. +They are meant to be modified for your needs. + +They aim to ease development and deployment of *Plone 6+* + +`constraints.txt` + Version pins for your project, used by *pip*. +`README_MAKE.md` + (this file) +`instance.yaml` + Zope/Plone application server configuration. Used by *cookiecutter-zope-instance* +`Makefile` + The configuration for *make* +`requirement.txt` + The core requirements. +`sources.ini` + *mxdev* is used to develop with sources from VCS like Git. + If you need sources from git, add them here. + +## Tools + +The configuration here uses: + +- `make` +- [pip](https://pip.pypa.io/en/stable/) +- [mxdev](https://pypi.org/project/mxdev) +- [cookiecutter-zope-instance](https://github.com/bluedynamics/cookiecutter-zope-instance/) diff --git a/build_ldap.sh b/build_ldap.sh new file mode 100755 index 0000000..2321df0 --- /dev/null +++ b/build_ldap.sh @@ -0,0 +1,11 @@ +#!/bin/bash + +cd $1 + +./configure --with-tls --enable-slapd=yes --enable-overlays CPPFLAGS=-D_GNU_SOURCE --prefix=$2 +make clean +make depend +make -j4 +make install + +cd - \ No newline at end of file diff --git a/constraints.txt b/constraints.txt new file mode 100644 index 0000000..72436a6 --- /dev/null +++ b/constraints.txt @@ -0,0 +1 @@ +-c https://dist.plone.org/release/6.0.0a3/constraints.txt diff --git a/instance.yaml b/instance.yaml new file mode 100644 index 0000000..448b03e --- /dev/null +++ b/instance.yaml @@ -0,0 +1,17 @@ +--- +# This is a cookiecutter configuration context file for +# +# cookiecutter-zope-instance +# +# available options are documented at +# https://github.com/bluedynamics/cookiecutter-zope-instance/ +# +# read also README_MAKE.md in this folder +# +default_context: + wsgi_fast_listen: localhost:8080 + initial_user_name: admin + initial_user_password: admin + debug_mode: on + load_zcml: {package_includes: ['pas.plugins.ldap']} + db_storage: direct diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..17178b7 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,6 @@ +-c constraints.txt +waitress_fastlisten +-e .[test] + +# until https://github.com/plone/plone.restapi/issues/1321 is solved +plone.app.iterate diff --git a/sources.ini b/sources.ini new file mode 100644 index 0000000..e8d245d --- /dev/null +++ b/sources.ini @@ -0,0 +1,45 @@ +[settings] +version-overrides = + setuptools==60.6.0 + wheel==0.37.1 + zope.i18n==4.9.0 + pip==22.0.2 + +#github = git+https://github.com +github = git+ssh://git@github.com + +# [node] +# url = ${settings:github}/conestack/node.git +# branch = master + +# [node.ext.ldap] +# url = ${settings:github}/conestack/node.ext.ldap.git` +# branch = master + +# [node.ext.ugm] +# url = ${settings:github}/conestack/node.ext.ugm.git +# branch = master + +[yafowil] +url = ${settings:github}/conestack/yafowil.git +branch = 3.0 + +[yafowil.bootstrap] +url = ${settings:github}/conestack/yafowil.bootstrap.git +branch = 2.0 + +[yafowil.plone] +url = ${settings:github}/bluedynamics/yafowil.plone.git +branch = bootstrap-plone6 + +[yafowil.widget.array] +url = ${settings:github}/conestack/yafowil.widget.array.git +branch = 2.0 + +[yafowil.widget.dict] +url = ${settings:github}/conestack/yafowil.widget.dict.git +branch = 2.0 + +[node.ext.ldap] +url = ${settings:github}/conestack/node.ext.ldap.git +branch = master diff --git a/src/pas/plugins/ldap/__init__.py b/src/pas/plugins/ldap/__init__.py index 9cf037d..8b5d5ee 100644 --- a/src/pas/plugins/ldap/__init__.py +++ b/src/pas/plugins/ldap/__init__.py @@ -1,9 +1,9 @@ -from AccessControl.Permissions import add_user_folders from . import monkey # noqa -from . import LDAPPlugin -from . import manage_addLDAPPlugin -from . import manage_addLDAPPluginForm -from . import zmidir +from .plugin import LDAPPlugin +from .plugin import manage_addLDAPPlugin +from .plugin import manage_addLDAPPluginForm +from .plugin import zmidir +from AccessControl.Permissions import add_user_folders from Products.PluggableAuthService import registerMultiPlugin import os diff --git a/src/pas/plugins/ldap/cache.py b/src/pas/plugins/ldap/cache.py index b11a174..ee98492 100644 --- a/src/pas/plugins/ldap/cache.py +++ b/src/pas/plugins/ldap/cache.py @@ -1,10 +1,10 @@ -from bda.cache import Memcached -from bda.cache import NullCache -from node.ext.ldap.interfaces import ICacheProviderFactory from .interfaces import ICacheSettingsRecordProvider from .interfaces import ILDAPPlugin from .interfaces import IPluginCacheHandler from .interfaces import VALUE_NOT_CACHED +from bda.cache import Memcached +from bda.cache import NullCache +from node.ext.ldap.interfaces import ICacheProviderFactory from zope.component import adapter from zope.component import queryUtility from zope.globalrequest import getRequest diff --git a/src/pas/plugins/ldap/cache_volatile.zcml b/src/pas/plugins/ldap/cache_volatile.zcml index 52a2e0b..38398e5 100644 --- a/src/pas/plugins/ldap/cache_volatile.zcml +++ b/src/pas/plugins/ldap/cache_volatile.zcml @@ -1,5 +1,4 @@ - + - servers, delimited by space + servers, delimited by space 127.0.0.1:11211 diff --git a/src/pas/plugins/ldap/properties.py b/src/pas/plugins/ldap/properties.py index 783caa3..0a7514b 100644 --- a/src/pas/plugins/ldap/properties.py +++ b/src/pas/plugins/ldap/properties.py @@ -11,6 +11,7 @@ from node.ext.ldap.scope import SUBTREE from node.ext.ldap.ugm import Ugm from odict import odict +from pas.plugins.ldap import _ from Products.Five import BrowserView from yafowil import loader # noqa: F401 from yafowil.base import ExtractionError @@ -19,7 +20,6 @@ from yafowil.yaml import parse_from_YAML from zope.component import adapter from zope.component import queryUtility -from zope.i18nmessageid import MessageFactory from zope.interface import implementer import ldap @@ -27,7 +27,6 @@ logger = logging.getLogger("pas.plugins.ldap") -_ = MessageFactory("pas.plugins.ldap") _marker = dict() diff --git a/src/pas/plugins/ldap/properties.yaml b/src/pas/plugins/ldap/properties.yaml index c082540..4ed979d 100644 --- a/src/pas/plugins/ldap/properties.yaml +++ b/src/pas/plugins/ldap/properties.yaml @@ -7,7 +7,7 @@ widgets: - server: factory: "*userpassanon:fieldset" props: - legend: LDAP Server Settings + legend: i18n:lbl_ldap_server_settings:LDAP Server Settings class: formPanel custom: userpassanon: @@ -17,81 +17,81 @@ widgets: factory: '#field:text' value: expr:context.props.uri props: - label: Connection URI - help: "Example: ldap://127.0.0.1:12345" - required: No URI defined + label: i18n:lbl_connection_uri:Connection URI + help: i18n:help_connection_uri:Example, the protocol is ldap, the IP address 127.0.0.1 and the port 12345 + required: i18n:msg_connection_uri:No URI defined - anonymous: factory: '#field:checkbox' value: expr:context.anonymous default: False props: - label: Anonymous Connection? + label: i18n:lbl_anonymous_connection:Anonymous Connection? - conn_timeout: factory: '#field:number' value: expr:context.props.conn_timeout props: - label: LDAP connection timeout in seconds + label: i18n:lbl_ldap_connection_timeout_in_seconds:LDAP connection timeout in seconds datatype: integer - op_timeout: factory: '#field:number' value: expr:context.props.op_timeout props: - label: LDAP operation timeout in seconds + label: i18n:lbl_ldap_operation_timeout_in_seconds:LDAP operation timeout in seconds datatype: integer - user: factory: '#field:text' value: expr:context.props.user props: - label: Manager User + label: i18n:lbl_manager_user:Manager User - password: factory: '#field:password' value: expr:context.props.password props: - label: Manager Password + label: i18n:lbl_manager_password:Manager Password - ignore_cert: factory: '#field:checkbox' value: expr:context.props.ignore_cert props: - label: Ignore certificate check? - help: If set on authenticate a failing certificate chain check including CA is ignored. + label: i18n:lbl_ignore_certificate_check:Ignore certificate check? + help: i18n:help_ignore_certificate_check:If set on authenticate a failing certificate chain check including CA is ignored. - page_size: factory: '#field:number' value: expr:context.props.page_size props: - label: Page Size - help: Maximum page size, number of results to query the server at once for. + label: i18n:lbl_page_size:Page Size + help: i18n:help_page_size:Maximum page size, number of results to query the server at once for. datatype: integer min: 1 - required_message: 'Page size must be given.' + required_message: i18n:msg_page_size:'Page size must be given.' - users: factory: fieldset props: - legend: Users Settings + legend: i18n:lbl_users_settings:Users Settings class: formPanel widgets: - dn: factory: '#field:text' value: expr:context.users.baseDN props: - label: Users container DN - required: No Users DN defined + label: i18n:lbl_users_container_dn:Users container DN + required: i18n:msg_users_container_dn:No Users DN defined - scope: factory: '#field:select' value: expr:str(context.users.scope) props: - label: Users search scope + label: i18n:lbl_users_search_scope:Users search scope vocabulary: expr:context.scope_vocab - query: factory: '#field:text' value: expr:context.users.queryFilter props: - label: Users search query filter + label: i18n:lbl_users_search_query_filter:Users search query filter - object_classes: factory: '#array' value: expr:context.users.objectClasses props: - label: Object classes for User creation - array.label: Object class + label: i18n:lbl_object_classes_for_user_creation:Object classes for User creation + array.label: i18n:lbl_object_class:Object class widgets: - oc: factory: field:text @@ -99,19 +99,19 @@ widgets: factory: '#field:checkbox' value: expr:context.users.memberOfSupport props: - label: memberOf attribute supported? + label: i18n:lbl_memberOf_attribute_supported:memberOf attribute supported? - recursiveGroups: factory: '#field:checkbox' value: expr:context.users.recursiveGroups props: - label: Support recursive/nested groups? - help: If your LDAP/AD supports it this will use LDAP_MATCHING_RULE_IN_CHAIN. By default only AD supports this. + label: i18n:lbl_support_recursive_nested_groups:Support recursive/nested groups? + help: i18n:help_support_recursive_nested_groups:If your LDAP/AD supports it this will use LDAP_MATCHING_RULE_IN_CHAIN. By default only AD supports this. - memberOfExternalGroupDNs: factory: '#array' value: expr:context.users.memberOfExternalGroupDNs props: - array.help: "Group DNs outside of the groups base DN are ignored, except if listed here" - array.label: memberOf external allowed Group DNs + array.help: i18n:help_memberOf_external_allowed_group_dns:"Group DNs outside of the groups base DN are ignored, except if listed here" + array.label: i18n:lbl_memberOf_external_allowed_group_dns:memberOf external allowed Group DNs widgets: - dn: factory: field:text @@ -119,66 +119,66 @@ widgets: factory: '#field:checkbox' value: expr:context.users.account_expiration props: - label: User Accounts expires? + label: i18n:lbl_user_accounts_expires:User Accounts expires? - expires_attr: factory: '#field:text' value: expr:context.users.expiresAttr props: - label: Attribute containing expiration Time + label: i18n:lbl_attribute_containing_expiration_time:Attribute containing expiration Time - expires_unit: factory: '#field:select' value: expr:context.users.expiresUnit props: - label: Account expiration unit + label: i18n:lbl_account_expiration_unit:Account expiration unit vocabulary: expr:((0, 'Days since Epoch'), (1, 'Seconds since epoch')) - aliases_attrmap: factory: '#field:dict' value: expr:context.users_attrmap props: - label: User attribute aliases - required: User attribute aliases values are mandatory + label: i18n:lbl_user_attribute_aliases:User attribute aliases + required: i18n:msg_user_attribute_aliases:User attribute aliases values are mandatory static: True head: - key: Reserved Key - value: LDAP Attribute + key: i18n:head_reserved_key:Reserved Key + value: i18n:head_ldap_attribute:LDAP Attribute - propsheet_attrmap: factory: '#field:dict' value: expr:context.users_propsheet_attrmap props: - label: User Property-Sheet Attributes + label: i18n:lbl_user_property-sheet_attributes:User Property-Sheet Attributes head: - key: Name on Sheet - value: LDAP Attribute + key: i18n:head_name_on_sheet:Name on Sheet + value: i18n:head_ldap_attribute:LDAP Attribute - groups: factory: fieldset props: - legend: Groups Settings + legend: i18n:lbl_groups_settings:Groups Settings class: formPanel widgets: - dn: factory: '#field:text' value: expr:context.groups.baseDN props: - label: Groups container DN - required: No Groups DN defined + label: i18n:lbl_groups_container_dn:Groups container DN + required: i18n:msg_groups_container_dn:No Groups DN defined - scope: factory: '#field:select' value: expr:str(context.groups.scope) props: - label: Groups search scope + label: i18n:lbl_groups_search_scope:Groups search scope vocabulary: expr:context.scope_vocab - query: factory: '#field:text' value: expr:context.groups.queryFilter props: - label: Groups search query filter + label: i18n:lbl_groups_search_query_filter:Groups search query filter - object_classes: factory: '#array' value: expr:context.groups.objectClasses props: - label: Object classes for Groups - help: "One of those is mandatory: groupOfNames, groupOfUniqueNames, posixGroup, group" - array.label: Object class + label: i18n:lbl_object_classes_for_groups:Object classes for Groups + help: i18n:help_object_classes_for_groups:"One of those is mandatory like as one the following options groupOfNames, groupOfUniqueNames, posixGroup, group" + array.label: i18n:lbl_object_class:Object class widgets: - oc: factory: field:text @@ -186,49 +186,49 @@ widgets: factory: '#field:checkbox' value: expr:context.groups.memberOfSupport props: - label: memberOf attribute supported? + label: i18n:lbl_memberOf_attribute_supported:memberOf attribute supported? - aliases_attrmap: factory: '#field:dict' value: expr:context.groups_attrmap props: - label: Group attribute aliases - required: Group attribute aliases values are mandatory + label: i18n:lbl_group_attribute_aliases:Group attribute aliases + required: i18n:msg_group_attribute_aliases:Group attribute aliases values are mandatory static: True head: - key: Reserved key - value: LDAP attr name + key: i18n:head_reserved_key:Reserved key + value: i18n:head_ldap_attr_name:LDAP attr name - propsheet_attrmap: factory: '#field:dict' value: expr:context.groups_propsheet_attrmap props: - label: Group Property-Sheet Attributes + label: i18n:lbl_group_property-sheet_attributes:Group Property-Sheet Attributes head: - key: Name on Sheet - value: LDAP Attribute + key: i18n:head_name_on_sheet:Name on Sheet + value: i18n:head_ldap_attribute:LDAP Attribute - cache: factory: fieldset props: - legend: Cache Settings + legend: i18n:lbl_cache_settings:Cache Settings class: formPanel widgets: - cache: factory: '#field:checkbox' value: expr:context.props.cache props: - label: Cache LDAP queries + label: i18n:lbl_cache_ldap_queries:Cache LDAP queries - memcached: factory: '#field:text' value: expr:context.props.memcached props: - label: Memcached Server to use - help: global - same server for all ldap plugins + label: i18n:lbl_memcached_server_to_use:Memcached Server to use + help: i18n:help_memcached_server_to_use:global - same server for all ldap plugins field.class: memcached field datatype: unicode - timeout: factory: '#field:number' value: expr:context.props.timeout props: - label: Cache timeout in seconds + label: i18n:lbl_cache_timeout_in_seconds:Cache timeout in seconds datatype: integer - save: factory: submit @@ -237,5 +237,5 @@ widgets: expression: True handler: context.save next: context.next - label: Save + label: i18n:lbl_save:Save class: submit-widget button-field context diff --git a/src/pas/plugins/ldap/zmi/add_plugin.pt b/src/pas/plugins/ldap/zmi/add_plugin.pt index fee0d16..971207c 100644 --- a/src/pas/plugins/ldap/zmi/add_plugin.pt +++ b/src/pas/plugins/ldap/zmi/add_plugin.pt @@ -1,25 +1,25 @@

Header

-

Add LDAP plugin

+

Add LDAP plugin

-

+

Add users and groups from LDAP using the pas.plugins.ldap plugin.

- + - + diff --git a/src/pas/plugins/ldap/zmi/manage_plugin.pt b/src/pas/plugins/ldap/zmi/manage_plugin.pt index bf54109..3ad9ff2 100644 --- a/src/pas/plugins/ldap/zmi/manage_plugin.pt +++ b/src/pas/plugins/ldap/zmi/manage_plugin.pt @@ -22,9 +22,10 @@ } -
+
-

Connection Test

+

Connection Test

@@ -34,10 +35,10 @@

-

Manage LDAP/AD plugin properties for id id

+

Manage LDAP/AD plugin properties for id id

-

- Set properties for users and groups from LDAP/ActiveDirectory using the pas.plugins.ldap plugin. +

+ Set properties for users and groups from LDAP/ActiveDirectory using the pas.plugins.ldap plugin.

form From 04efa9417e7bd62216ebde3a28efc2ec0384389c Mon Sep 17 00:00:00 2001 From: "Leonardo J. Caballero G" Date: Sun, 22 Jun 2025 01:27:04 +0200 Subject: [PATCH 25/74] Reorganize the use of the logging library --- src/pas/plugins/ldap/__init__.py | 3 +++ src/pas/plugins/ldap/plonecontrolpanel/exportimport.py | 7 ++++--- src/pas/plugins/ldap/properties.py | 5 +---- 3 files changed, 8 insertions(+), 7 deletions(-) diff --git a/src/pas/plugins/ldap/__init__.py b/src/pas/plugins/ldap/__init__.py index d9ed91b..874b91d 100644 --- a/src/pas/plugins/ldap/__init__.py +++ b/src/pas/plugins/ldap/__init__.py @@ -8,12 +8,15 @@ from Products.PluggableAuthService import registerMultiPlugin from zope.i18nmessageid import MessageFactory +import logging import os PACKAGE_NAME = "pas.plugins.ldap" _ = MessageFactory(PACKAGE_NAME) +logger = logging.getLogger(PACKAGE_NAME) + def initialize(context): registerMultiPlugin(LDAPPlugin.meta_type) diff --git a/src/pas/plugins/ldap/plonecontrolpanel/exportimport.py b/src/pas/plugins/ldap/plonecontrolpanel/exportimport.py index 5ba4d25..eb88069 100644 --- a/src/pas/plugins/ldap/plonecontrolpanel/exportimport.py +++ b/src/pas/plugins/ldap/plonecontrolpanel/exportimport.py @@ -1,13 +1,14 @@ from BTrees.OOBTree import OOBTree from Products.GenericSetup.interfaces import IBody from Products.GenericSetup.utils import XMLAdapterBase +from pas.plugins.ldap import PACKAGE_NAME from zope.component import queryMultiAdapter from zope.interface import implementer def _get_import_export_handler(context): aclu = context.getSite().acl_users - logger = context.getLogger("pas.plugins.ldap") + logger = context.getLogger(PACKAGE_NAME) if "pasldap" not in aclu.objectIds(): return pasldap = aclu.pasldap @@ -19,7 +20,7 @@ def _get_import_export_handler(context): def import_settings(context): - logger = context.getLogger("pas.plugins.ldap") + logger = context.getLogger(PACKAGE_NAME) handler = _get_import_export_handler(context) if not handler: return @@ -36,7 +37,7 @@ def export_settings(context): return body = handler.body if body is None: - logger = context.getLogger("pas.plugins.ldap") + logger = context.getLogger(PACKAGE_NAME) logger.warning("Problem to get ldap settings.") return context.writeDataFile(handler.filename, body, handler.mime_type) diff --git a/src/pas/plugins/ldap/properties.py b/src/pas/plugins/ldap/properties.py index 0a7514b..0f51b5a 100644 --- a/src/pas/plugins/ldap/properties.py +++ b/src/pas/plugins/ldap/properties.py @@ -11,7 +11,7 @@ from node.ext.ldap.scope import SUBTREE from node.ext.ldap.ugm import Ugm from odict import odict -from pas.plugins.ldap import _ +from pas.plugins.ldap import _, logger from Products.Five import BrowserView from yafowil import loader # noqa: F401 from yafowil.base import ExtractionError @@ -23,11 +23,8 @@ from zope.interface import implementer import ldap -import logging -logger = logging.getLogger("pas.plugins.ldap") - _marker = dict() From 203524e93e02d7c310da835b7b16a4d11d70dfc2 Mon Sep 17 00:00:00 2001 From: "Leonardo J. Caballero G" Date: Sun, 22 Jun 2025 01:30:48 +0200 Subject: [PATCH 26/74] Updated CHANGES file #131 --- CHANGES.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 8f62b18..6edc9af 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,9 @@ History 1.9.0 (unreleased) ------------------ +- Added the initial i18n support #131 + [macagua] + - Drop support for Plone 5/ Python < 3.9 [jensens] From f7d194ed02dabdf9587d8a0d8e978c87b07dd8f9 Mon Sep 17 00:00:00 2001 From: "Leonardo J. Caballero G" Date: Sun, 22 Jun 2025 01:31:21 +0200 Subject: [PATCH 27/74] Updated the i18n script #131 --- src/pas/plugins/ldap/locales/__main__.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/pas/plugins/ldap/locales/__main__.py b/src/pas/plugins/ldap/locales/__main__.py index 4462d00..6ca0d07 100644 --- a/src/pas/plugins/ldap/locales/__main__.py +++ b/src/pas/plugins/ldap/locales/__main__.py @@ -15,7 +15,6 @@ locale_path = Path(__file__).parent.resolve() target_path = locale_path.parent.resolve() -#domains = [path.name[:-4] for path in locale_path.glob("*.pot")] domain = "pas.plugins.ldap" i18ndude = "uvx i18ndude" From 5fc47c1d28fd4a9acee48928ebc1378b52179232 Mon Sep 17 00:00:00 2001 From: "Leonardo J. Caballero G" Date: Sun, 22 Jun 2025 01:39:10 +0200 Subject: [PATCH 28/74] Added Spanish translation #132 --- CHANGES.rst | 3 + .../es/LC_MESSAGES/pas.plugins.ldap.po | 405 ++++++++++++++++++ 2 files changed, 408 insertions(+) create mode 100644 src/pas/plugins/ldap/locales/es/LC_MESSAGES/pas.plugins.ldap.po diff --git a/CHANGES.rst b/CHANGES.rst index 6edc9af..d8f3a02 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,9 @@ History 1.9.0 (unreleased) ------------------ +- Added Spanish translation #132 + [macagua] + - Added the initial i18n support #131 [macagua] diff --git a/src/pas/plugins/ldap/locales/es/LC_MESSAGES/pas.plugins.ldap.po b/src/pas/plugins/ldap/locales/es/LC_MESSAGES/pas.plugins.ldap.po new file mode 100644 index 0000000..e6f8479 --- /dev/null +++ b/src/pas/plugins/ldap/locales/es/LC_MESSAGES/pas.plugins.ldap.po @@ -0,0 +1,405 @@ +# Translation of pas.plugins.ldap.pot to Spanish +# Leonardo J. Caballero G. , 2025. +msgid "" +msgstr "" +"Project-Id-Version: pas.plugins.ldap\n" +"POT-Creation-Date: 2025-06-21 20:09+0000\n" +"PO-Revision-Date: 2025-06-21 19:34+0200\n" +"Last-Translator: Leonardo J. Caballero G. \n" +"Language-Team: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=utf-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"Language-Code: en\n" +"Language-Name: English\n" +"Preferred-Encodings: utf-8 latin1\n" +"Domain: pas.plugins.ldap\n" +"Language: es\n" +"X-Generator: Poedit 3.6\n" +"X-Is-Fallback-For: es-ar es-bo es-cl es-co es-cr es-do es-ec es-es es-sv es-gt es-hn es-mx es-ni es-pa es-py es-pe es-pr es-us es-uy es-ve\n" + +#: pas/plugins/ldap/plonecontrolpanel/controlpanel.pt:16 +#: pas/plugins/ldap/zmi/manage_plugin.pt:28 +msgid "Connection Test" +msgstr "Prueba de conexión" + +#: pas/plugins/ldap/properties.py:223 +msgid "Exception in Groups; " +msgstr "Excepción en Grupos; " + +#: pas/plugins/ldap/properties.py:216 +msgid "Exception in Users; " +msgstr "Excepción en Usuarios; " + +#: pas/plugins/ldap/configure.zcml:34 +msgid "Extension profile for pas.plugins.ldap Zope Base." +msgstr "Perfil de extensión base de Zope para pas.plugins.ldap." + +#: pas/plugins/ldap/plonecontrolpanel/inspector.pt:18 +msgid "Groups" +msgstr "Grupos" + +#: pas/plugins/ldap/plonecontrolpanel/inspector.pt:11 +msgid "LDAP Child Inspector" +msgstr "Inspector secundario LDAP" + +#: pas/plugins/ldap/plonecontrolpanel/controlpanel.pt:25 +msgid "LDAP Inspector" +msgstr "Inspector LDAP" + +#: pas/plugins/ldap/configure.zcml:34 +msgid "LDAP Plugin for PAS - Zope 2 Base Installation" +msgstr "Plugin LDAP para PAS - Instalación base de Zope 2" + +#: pas/plugins/ldap/plonecontrolpanel/controlpanel.py:31 +msgid "LDAP Settings saved." +msgstr "Configuración de LDAP guardada." + +#: pas/plugins/ldap/properties.py:220 +msgid "LDAP Users ok, but groups not; " +msgstr "Los usuarios de LDAP están bien, pero los grupos no; " + +#: pas/plugins/ldap/properties.py:213 +msgid "LDAP users; " +msgstr "Usuarios de LDAP; " + +#: pas/plugins/ldap/plonecontrolpanel/profiles/default/controlpanel.xml +msgid "LDAP/ AD Support" +msgstr "Soporte con LDAP/AD" + +#: pas/plugins/ldap/plonecontrolpanel/controlpanel.pt:14 +msgid "LDAP/ Active Directory Configuration" +msgstr "Configuración de LDAP/Active Directory" + +#: pas/plugins/ldap/plonecontrolpanel/configure.zcml:19 +msgid "LDAP/ Active Directory Support" +msgstr "Soporte con LDAP/Active Directory" + +#: pas/plugins/ldap/zmi/manage_plugin.pt:38 +msgid "Manage LDAP/AD plugin properties for id ${plugin_properties_id}" +msgstr "Administre las propiedades del complemento LDAP / AD para id $ {plugin_properties_id}" + +#: pas/plugins/ldap/plonecontrolpanel/inspector.pt:23 +msgid "No search base selected" +msgstr "No se ha seleccionado ninguna base de búsqueda" + +#: pas/plugins/ldap/properties.py:204 +msgid "Non-LDAP error while getting ILDAPGroupsConfig!" +msgstr "¡Error no LDAP al obtener ILDAPGroupsConfig!" + +#: pas/plugins/ldap/properties.py:192 +msgid "Non-LDAP error while getting ILDAPProps!" +msgstr "¡Error no LDAP al obtener ILDAPProps!" + +#: pas/plugins/ldap/properties.py:198 +msgid "Non-LDAP error while getting ILDAPUsersConfig!" +msgstr "¡Error no LDAP al obtener ILDAPUsersConfig!" + +#: pas/plugins/ldap/properties.py:180 +msgid "Password is required for non-anonymous connections." +msgstr "La contraseña es necesaria para las conexiones no anónimas." + +#: pas/plugins/ldap/plonecontrolpanel/inspector.pt:13 +msgid "Select search base" +msgstr "Seleccione la base de búsqueda" + +#: pas/plugins/ldap/properties.py:211 +msgid "Server Down" +msgstr "Servidor caído" + +#: pas/plugins/ldap/zmi/manage_plugin.pt:41 +msgid "Set properties for users and groups from LDAP/ActiveDirectory using the ${plugin_name} plugin." +msgstr "Establezca las propiedades de los usuarios y grupos de LDAP/ActiveDirectory mediante el complemento ${plugin_name}." + +#: pas/plugins/ldap/plonecontrolpanel/controlpanel.pt:34 +msgid "Settings" +msgstr "Ajustes" + +#: pas/plugins/ldap/properties.py:185 +msgid "User/Password are required if not anonymous." +msgstr "El usuario/contraseña son obligatorios si no son anónimos." + +#: pas/plugins/ldap/properties.py:174 +msgid "Username is required for non-anonymous connections." +msgstr "El nombre de usuario es necesario para las conexiones no anónimas." + +#: pas/plugins/ldap/plonecontrolpanel/inspector.pt:17 +msgid "Users" +msgstr "Usuarios" + +#. Default: LDAP attr name +#: pas/plugins/ldap/properties.yaml:199 +msgid "head_ldap_attr_name" +msgstr "Nombre de atributo LDAP" + +#. Default: LDAP Attribute +#: pas/plugins/ldap/properties.yaml:143 +msgid "head_ldap_attribute" +msgstr "Atributo LDAP" + +#. Default: Name on Sheet +#: pas/plugins/ldap/properties.yaml:150 +msgid "head_name_on_sheet" +msgstr "Nombre en la hoja" + +#. Default: Reserved Key +#: pas/plugins/ldap/properties.yaml:142 +#, fuzzy +msgid "head_reserved_key" +msgstr "Clave reservada" + +#. Default: Example, the protocol is ldap, the IP address 127.0.0.1 and the +#. port 12345 +#: pas/plugins/ldap/properties.yaml:21 +msgid "help_connection_uri" +msgstr "Ejemplo, el protocolo es ldap, la dirección IP es 127.0.0.1 y el puerto es 12345" + +#. Default: If set on authenticate a failing certificate chain check including +#. CA is ignored. +#: pas/plugins/ldap/properties.yaml:56 +msgid "help_ignore_certificate_check" +msgstr "Si se establece en autenticar una comprobación de cadena de certificado fallido incluyendo CA se ignora." + +#. Default: "Group DNs outside of the groups base DN are ignored, except if +#. listed here" +#: pas/plugins/ldap/properties.yaml:113 +msgid "help_memberOf_external_allowed_group_dns" +msgstr "Los DN de grupo que no sean el DN base del grupo se ignoran, excepto si se indican aquí" + +#. Default: global - same server for all ldap plugins +#: pas/plugins/ldap/properties.yaml:224 +msgid "help_memcached_server_to_use" +msgstr "global - mismo servidor para todos los plugins ldap" + +#. Default: "One of those is mandatory like as one the following options +#. groupOfNames, groupOfUniqueNames, posixGroup, group" +#: pas/plugins/ldap/properties.yaml:180 +msgid "help_object_classes_for_groups" +msgstr "Uno de ellos es obligatorio como una de las siguientes opciones groupOfNames, groupOfUniqueNames, posixGroup y group" + +#. Default: Maximum page size, number of results to query the server at once +#. for. +#: pas/plugins/ldap/properties.yaml:62 +msgid "help_page_size" +msgstr "Tamaño máximo de la página, número de resultados que se pueden consultar a la vez en el servidor." + +#. Default: If your LDAP/AD supports it this will use +#. LDAP_MATCHING_RULE_IN_CHAIN. By default only AD supports this. +#: pas/plugins/ldap/properties.yaml:108 +msgid "help_support_recursive_nested_groups" +msgstr "Si su LDAP/AD lo admite, utilizará LDAP_MATCHING_RULE_IN_CHAIN. Por defecto sólo AD lo soporta." + +#. Default: Account expiration unit +#: pas/plugins/ldap/properties.yaml:132 +msgid "lbl_account_expiration_unit" +msgstr "Unidad de caducidad de la cuenta" + +#. Default: Anonymous Connection? +#: pas/plugins/ldap/properties.yaml:28 +msgid "lbl_anonymous_connection" +msgstr "¿Conexión anónima?" + +#. Default: Attribute containing expiration Time +#: pas/plugins/ldap/properties.yaml:127 +msgid "lbl_attribute_containing_expiration_time" +msgstr "Atributo que contiene la hora de expiración" + +#. Default: Cache LDAP queries +#: pas/plugins/ldap/properties.yaml:218 +msgid "lbl_cache_ldap_queries" +msgstr "Caché de consultas LDAP" + +#. Default: Cache Settings +#: pas/plugins/ldap/properties.yaml:211 +msgid "lbl_cache_settings" +msgstr "Configuración de la caché" + +#. Default: Cache timeout in seconds +#: pas/plugins/ldap/properties.yaml:231 +msgid "lbl_cache_timeout_in_seconds" +msgstr "Tiempo de espera de la caché en segundos" + +#. Default: Connection URI +#: pas/plugins/ldap/properties.yaml:20 +msgid "lbl_connection_uri" +msgstr "URI de conexión" + +#. Default: Group attribute aliases +#: pas/plugins/ldap/properties.yaml:194 +msgid "lbl_group_attribute_aliases" +msgstr "Alias de atributos de grupo" + +#. Default: Group Property-Sheet Attributes +#: pas/plugins/ldap/properties.yaml:204 +msgid "lbl_group_property-sheet_attributes" +msgstr "Atributos de la hoja de propiedades de grupo" + +#. Default: Groups container DN +#: pas/plugins/ldap/properties.yaml:162 +msgid "lbl_groups_container_dn" +msgstr "DN del contenedor de Grupos" + +#. Default: Groups search query filter +#: pas/plugins/ldap/properties.yaml:174 +msgid "lbl_groups_search_query_filter" +msgstr "Filtro de búsqueda de grupos" + +#. Default: Groups search scope +#: pas/plugins/ldap/properties.yaml:168 +msgid "lbl_groups_search_scope" +msgstr "Ámbito de búsqueda de grupos" + +#. Default: Groups Settings +#: pas/plugins/ldap/properties.yaml:155 +msgid "lbl_groups_settings" +msgstr "Configuración de grupos" + +#. Default: Ignore certificate check? +#: pas/plugins/ldap/properties.yaml:55 +msgid "lbl_ignore_certificate_check" +msgstr "¿Ignorar la comprobación del certificado?" + +#. Default: LDAP connection timeout in seconds +#: pas/plugins/ldap/properties.yaml:33 +msgid "lbl_ldap_connection_timeout_in_seconds" +msgstr "Tiempo de espera de la conexión LDAP en segundos" + +#. Default: LDAP operation timeout in seconds +#: pas/plugins/ldap/properties.yaml:39 +msgid "lbl_ldap_operation_timeout_in_seconds" +msgstr "Tiempo de espera de la operación LDAP en segundos" + +#. Default: LDAP Server Settings +#: pas/plugins/ldap/properties.yaml:10 +msgid "lbl_ldap_server_settings" +msgstr "Configuración del servidor LDAP" + +#. Default: Manager Password +#: pas/plugins/ldap/properties.yaml:50 +msgid "lbl_manager_password" +msgstr "Contraseña de administrador" + +#. Default: Manager User +#: pas/plugins/ldap/properties.yaml:45 +msgid "lbl_manager_user" +msgstr "Usuario de administrador" + +#. Default: memberOf attribute supported? +#: pas/plugins/ldap/properties.yaml:102 +msgid "lbl_memberOf_attribute_supported" +msgstr "¿se admite el atributo memberOf?" + +#. Default: memberOf external allowed Group DNs +#: pas/plugins/ldap/properties.yaml:114 +msgid "lbl_memberOf_external_allowed_group_dns" +msgstr "DNs de grupo permitidos memberOf externos" + +#. Default: Memcached Server to use +#: pas/plugins/ldap/properties.yaml:223 +msgid "lbl_memcached_server_to_use" +msgstr "Servidor Memcached a utilizar" + +#. Default: Object class +#: pas/plugins/ldap/properties.yaml:94 +msgid "lbl_object_class" +msgstr "Clase de objeto" + +#. Default: Object classes for Groups +#: pas/plugins/ldap/properties.yaml:179 +msgid "lbl_object_classes_for_groups" +msgstr "Clases de objetos para Grupos" + +#. Default: Object classes for User creation +#: pas/plugins/ldap/properties.yaml:93 +msgid "lbl_object_classes_for_user_creation" +msgstr "Clases de objetos para la creación de usuarios" + +#. Default: Page Size +#: pas/plugins/ldap/properties.yaml:61 +msgid "lbl_page_size" +msgstr "Tamaño de página" + +#. Default: Save +#: pas/plugins/ldap/properties.yaml:240 +msgid "lbl_save" +msgstr "Guardar" + +#. Default: Support recursive/nested groups? +#: pas/plugins/ldap/properties.yaml:107 +msgid "lbl_support_recursive_nested_groups" +msgstr "¿Admite grupos recursivos/anidados?" + +#. Default: User Accounts expires? +#: pas/plugins/ldap/properties.yaml:122 +msgid "lbl_user_accounts_expires" +msgstr "¿Caducan las cuentas de usuario?" + +#. Default: User attribute aliases +#: pas/plugins/ldap/properties.yaml:138 +msgid "lbl_user_attribute_aliases" +msgstr "Alias de atributos de usuario" + +#. Default: User Property-Sheet Attributes +#: pas/plugins/ldap/properties.yaml:148 +msgid "lbl_user_property-sheet_attributes" +msgstr "Atributos de la hoja de propiedades del usuario" + +#. Default: Users container DN +#: pas/plugins/ldap/properties.yaml:76 +msgid "lbl_users_container_dn" +msgstr "DN del contenedor de usuarios" + +#. Default: Users search query filter +#: pas/plugins/ldap/properties.yaml:88 +msgid "lbl_users_search_query_filter" +msgstr "Filtro de consulta de búsqueda de usuarios" + +#. Default: Users search scope +#: pas/plugins/ldap/properties.yaml:82 +msgid "lbl_users_search_scope" +msgstr "Ámbito de búsqueda de los usuarios" + +#. Default: Users Settings +#: pas/plugins/ldap/properties.yaml:69 +msgid "lbl_users_settings" +msgstr "Configuración de usuarios" + +#. Default: No URI defined +#: pas/plugins/ldap/properties.yaml:22 +msgid "msg_connection_uri" +msgstr "No hay URI definido" + +#. Default: Group attribute aliases values are mandatory +#: pas/plugins/ldap/properties.yaml:195 +msgid "msg_group_attribute_aliases" +msgstr "Los valores de los alias de atributos de grupo son obligatorios" + +#. Default: No Groups DN defined +#: pas/plugins/ldap/properties.yaml:163 +msgid "msg_groups_container_dn" +msgstr "No hay grupos DN definidos" + +#. Default: 'Page size must be given.' +#: pas/plugins/ldap/properties.yaml:65 +msgid "msg_page_size" +msgstr "'Debe indicarse el tamaño de la página.'" + +#. Default: User attribute aliases values are mandatory +#: pas/plugins/ldap/properties.yaml:139 +msgid "msg_user_attribute_aliases" +msgstr "Los valores de los alias de atributos de usuario son obligatorios" + +#. Default: No Users DN defined +#: pas/plugins/ldap/properties.yaml:77 +msgid "msg_users_container_dn" +msgstr "No se ha definido ningún DN de usuario" + +#: pas/plugins/ldap/plonecontrolpanel/configure.zcml:19 +msgid "pas.plugins.ldap support for users and groups from ldap/active directory." +msgstr "Soporte para usuarios y grupos de LDAP/Active Directory con el complemento pas.plugins.ldap." + +#: pas/plugins/ldap/plonecontrolpanel/cache.py:28 +#: pas/plugins/ldap/plonecontrolpanel/profiles/default/registry.xml +msgid "servers, delimited by space" +msgstr "servidores, delimitados por el espacio" From 78959985b6bd7b1db7884f8fa6105198b75df09c Mon Sep 17 00:00:00 2001 From: "Jens W. Klein" Date: Mon, 23 Jun 2025 19:51:56 +0200 Subject: [PATCH 29/74] mxmake update, remove ldap --- Makefile | 341 ++++++++++++++++++++++++++++--------------------------- 1 file changed, 172 insertions(+), 169 deletions(-) diff --git a/Makefile b/Makefile index 408b8d7..dd7c090 100644 --- a/Makefile +++ b/Makefile @@ -10,8 +10,6 @@ #: core.packages #: core.sources #: i18n.gettext -#: ldap.openldap -#: ldap.python-ldap #: qa.black #: qa.coverage #: qa.isort @@ -29,7 +27,7 @@ DEPLOY_TARGETS?= # target to be executed when calling `make run` # No default value. -RUN_TARGET?= +RUN_TARGET?=zope-start # Additional files and folders to remove when running clean target # No default value. @@ -40,36 +38,39 @@ CLEAN_FS?= # Default: include.mk INCLUDE_MAKEFILE?=include.mk -## ldap.openldap - -# OpenLDAP version to download -# Default: 2.4.59 -OPENLDAP_VERSION?=2.4.59 - -# OpenLDAP base download URL -# Default: https://www.openldap.org/software/download/OpenLDAP/openldap-release/ -OPENLDAP_URL?=https://www.openldap.org/software/download/OpenLDAP/openldap-release/ - -# Build directory for OpenLDAP -# Default: $(shell echo $(realpath .))/openldap -OPENLDAP_DIR?=$(shell echo $(realpath .))/openldap - -# Build environment for OpenLDAP -# Default: PATH=/usr/local/bin:/usr/bin:/bin -OPENLDAP_ENV?=PATH=/usr/local/bin:/usr/bin:/bin +# Optional additional directories to be added to PATH in format +# `/path/to/dir/:/path/to/other/dir`. Gets inserted first, thus gets searched +# first. +# No default value. +EXTRA_PATH?= ## core.mxenv -# Python interpreter to use. +# Primary Python interpreter to use. It is used to create the +# virtual environment if `VENV_ENABLED` and `VENV_CREATE` are set to `true`. # Default: python3 -PYTHON_BIN?=python3 +PRIMARY_PYTHON?=3.12 # Minimum required Python version. -# Default: 3.7 -PYTHON_MIN_VERSION?=3.9 +# Default: 3.9 +PYTHON_MIN_VERSION?=3.10 + +# Install packages using the given package installer method. +# Supported are `pip` and `uv`. If uv is used, its global availability is +# checked. Otherwise, it is installed, either in the virtual environment or +# using the `PRIMARY_PYTHON`, dependent on the `VENV_ENABLED` setting. If +# `VENV_ENABLED` and uv is selected, uv is used to create the virtual +# environment. +# Default: pip +PYTHON_PACKAGE_INSTALLER?=uv + +# Flag whether to use a global installed 'uv' or install +# it in the virtual environment. +# Default: false +MXENV_UV_GLOBAL?=true # Flag whether to use virtual environment. If `false`, the -# interpreter according to `PYTHON_BIN` found in `PATH` is used. +# interpreter according to `PRIMARY_PYTHON` found in `PATH` is used. # Default: true VENV_ENABLED?=true @@ -84,7 +85,7 @@ VENV_CREATE?=true # target folder for the virtual environment. If `VENV_ENABLED` is `true` and # `VENV_CREATE` is false it is expected to point to an existing virtual # environment. If `VENV_ENABLED` is `false` it is ignored. -# Default: venv +# Default: .venv VENV_FOLDER?=venv # mxdev to install in virtual environment. @@ -119,6 +120,13 @@ BLACK_SRC?=src # Default: mx.ini PROJECT_CONFIG?=mx.ini +## core.packages + +# Allow prerelease and development versions. +# By default, the package installer only finds stable versions. +# Default: false +PACKAGES_ALLOW_PRERELEASES?=false + ## qa.test # The command which gets executed. Defaults to the location the @@ -152,6 +160,10 @@ ZOPE_CONFIGURATION_FILE?=instance.yaml # Default: https://github.com/plone/cookiecutter-zope-instance ZOPE_TEMPLATE?=https://github.com/plone/cookiecutter-zope-instance +# cookiecutter branch, tag or commit to checkout from the ZOPE_TEMPLATE. If empty, `--checkout` is not passed to cookiecutter. +# Default: main +ZOPE_TEMPLATE_CHECKOUT?=main + # The Zope folder "instance" will be generated relative to this existing folder. # Default: . ZOPE_BASE_FOLDER?=. @@ -160,6 +172,14 @@ ZOPE_BASE_FOLDER?=. # Default: No Default ZOPE_SCRIPTNAME?=No Default +# user name to create +# Default: No Default +ZOPE_USER_NAME?=No Default + +# user name to create +# Default: No Default +ZOPE_USER_PASSWORD?=No Default + ## i18n.gettext # Path of directory containing the message catalogs. @@ -170,7 +190,7 @@ GETTEXT_LOCALES_PATH?=locale # No default value. GETTEXT_DOMAIN?= -# List of language identifiers. +# Space separated list of language identifiers. # No default value. GETTEXT_LANGUAGES?= @@ -183,8 +203,11 @@ DIRTY_TARGETS?= CLEAN_TARGETS?= PURGE_TARGETS?= CHECK_TARGETS?= +TYPECHECK_TARGETS?= FORMAT_TARGETS?= +export PATH:=$(if $(EXTRA_PATH),$(EXTRA_PATH):,)$(PATH) + # Defensive settings for make: https://tech.davis-hansson.com/p/make/ SHELL:=bash .ONESHELL: @@ -201,90 +224,66 @@ MXMAKE_FOLDER?=.mxmake # Sentinel files SENTINEL_FOLDER?=$(MXMAKE_FOLDER)/sentinels SENTINEL?=$(SENTINEL_FOLDER)/about.txt -$(SENTINEL): +$(SENTINEL): $(firstword $(MAKEFILE_LIST)) @mkdir -p $(SENTINEL_FOLDER) @echo "Sentinels for the Makefile process." > $(SENTINEL) -############################################################################## -# openldap -############################################################################## - -# case `system.dependencies` domain is included -SYSTEM_DEPENDENCIES+=libdb-dev libsasl2-dev - -OPENLDAP_TARGET:=$(SENTINEL_FOLDER)/openldap.sentinel -$(OPENLDAP_TARGET): $(SENTINEL) - @echo "Building openldap server in '$(OPENLDAP_DIR)'" - @test -d $(OPENLDAP_DIR) || curl -o openldap-$(OPENLDAP_VERSION).tgz \ - $(OPENLDAP_URL)/openldap-$(OPENLDAP_VERSION).tgz - @test -d $(OPENLDAP_DIR) || tar xf openldap-$(OPENLDAP_VERSION).tgz - @test -d $(OPENLDAP_DIR) || rm openldap-$(OPENLDAP_VERSION).tgz - @test -d $(OPENLDAP_DIR) || mv openldap-$(OPENLDAP_VERSION) $(OPENLDAP_DIR) - @env -i -C $(OPENLDAP_DIR) $(OPENLDAP_ENV) bash -c \ - './configure \ - --with-tls \ - --enable-slapd=yes \ - --enable-overlays \ - --prefix=$(OPENLDAP_DIR) \ - && make depend \ - && make -j4 \ - && make install' - @touch $(OPENLDAP_TARGET) - -.PHONY: openldap -openldap: $(OPENLDAP_TARGET) - -.PHONY: openldap-dirty -openldap-dirty: - @test -d $(OPENLDAP_DIR) \ - && env -i -C $(OPENLDAP_DIR) $(OPENLDAP_ENV) bash -c 'make clean' - @rm -f $(OPENLDAP_TARGET) - -.PHONY: openldap-clean -openldap-clean: - @rm -f $(OPENLDAP_TARGET) - @rm -rf $(OPENLDAP_DIR) - -INSTALL_TARGETS+=openldap -DIRTY_TARGETS+=openldap-dirty -CLEAN_TARGETS+=openldap-clean - ############################################################################## # mxenv ############################################################################## -# Check if given Python is installed -ifeq (,$(shell which $(PYTHON_BIN))) -$(error "PYTHON=$(PYTHON_BIN) not found in $(PATH)") -endif +OS?= -# Check if given Python version is ok -PYTHON_VERSION_OK=$(shell $(PYTHON_BIN) -c "import sys; print((int(sys.version_info[0]), int(sys.version_info[1])) >= tuple(map(int, '$(PYTHON_MIN_VERSION)'.split('.'))))") -ifeq ($(PYTHON_VERSION_OK),0) -$(error "Need Python >= $(PYTHON_MIN_VERSION)") +# Determine the executable path +ifeq ("$(VENV_ENABLED)", "true") +export VIRTUAL_ENV=$(abspath $(VENV_FOLDER)) +ifeq ("$(OS)", "Windows_NT") +VENV_EXECUTABLE_FOLDER=$(VIRTUAL_ENV)/Scripts +else +VENV_EXECUTABLE_FOLDER=$(VIRTUAL_ENV)/bin endif - -# Check if venv folder is configured if venv is enabled -ifeq ($(shell [[ "$(VENV_ENABLED)" == "true" && "$(VENV_FOLDER)" == "" ]] && echo "true"),"true") -$(error "VENV_FOLDER must be configured if VENV_ENABLED is true") +export PATH:=$(VENV_EXECUTABLE_FOLDER):$(PATH) +MXENV_PYTHON=python +else +MXENV_PYTHON=$(PRIMARY_PYTHON) endif -# determine the executable path -ifeq ("$(VENV_ENABLED)", "true") -MXENV_PATH=$(VENV_FOLDER)/bin/ +# Determine the package installer +ifeq ("$(PYTHON_PACKAGE_INSTALLER)","uv") +PYTHON_PACKAGE_COMMAND=uv pip else -MXENV_PATH= +PYTHON_PACKAGE_COMMAND=$(MXENV_PYTHON) -m pip endif MXENV_TARGET:=$(SENTINEL_FOLDER)/mxenv.sentinel $(MXENV_TARGET): $(SENTINEL) + @$(PRIMARY_PYTHON) -c "import sys; vi = sys.version_info; sys.exit(1 if (int(vi[0]), int(vi[1])) >= tuple(map(int, '$(PYTHON_MIN_VERSION)'.split('.'))) else 0)" \ + && echo "Need Python >= $(PYTHON_MIN_VERSION)" && exit 1 || : + @[[ "$(VENV_ENABLED)" == "true" && "$(VENV_FOLDER)" == "" ]] \ + && echo "VENV_FOLDER must be configured if VENV_ENABLED is true" && exit 1 || : + @[[ "$(VENV_ENABLED)$(PYTHON_PACKAGE_INSTALLER)" == "falseuv" ]] \ + && echo "Package installer uv does not work with a global Python interpreter." && exit 1 || : ifeq ("$(VENV_ENABLED)", "true") - @echo "Setup Python Virtual Environment under '$(VENV_FOLDER)'" - @$(PYTHON_BIN) -m venv $(VENV_FOLDER) +ifeq ("$(VENV_CREATE)", "true") +ifeq ("$(PYTHON_PACKAGE_INSTALLER)$(MXENV_UV_GLOBAL)","uvtrue") + @echo "Setup Python Virtual Environment using package 'uv' at '$(VENV_FOLDER)'" + @uv venv -p $(PRIMARY_PYTHON) --seed $(VENV_FOLDER) +else + @echo "Setup Python Virtual Environment using module 'venv' at '$(VENV_FOLDER)'" + @$(PRIMARY_PYTHON) -m venv $(VENV_FOLDER) + @$(MXENV_PYTHON) -m ensurepip -U +endif endif - @$(MXENV_PATH)pip install -U pip setuptools wheel - @$(MXENV_PATH)pip install -U $(MXDEV) - @$(MXENV_PATH)pip install -U $(MXMAKE) +else + @echo "Using system Python interpreter" +endif +ifeq ("$(PYTHON_PACKAGE_INSTALLER)$(MXENV_UV_GLOBAL)","uvfalse") + @echo "Install uv" + @$(MXENV_PYTHON) -m pip install uv +endif + @$(PYTHON_PACKAGE_COMMAND) install -U pip setuptools wheel + @echo "Install/Update MXStack Python packages" + @$(PYTHON_PACKAGE_COMMAND) install -U $(MXDEV) $(MXMAKE) @touch $(MXENV_TARGET) .PHONY: mxenv @@ -297,10 +296,12 @@ mxenv-dirty: .PHONY: mxenv-clean mxenv-clean: mxenv-dirty ifeq ("$(VENV_ENABLED)", "true") +ifeq ("$(VENV_CREATE)", "true") @rm -rf $(VENV_FOLDER) +endif else - @$(MXENV_PATH)pip uninstall -y $(MXDEV) - @$(MXENV_PATH)pip uninstall -y $(MXMAKE) + @$(PYTHON_PACKAGE_COMMAND) uninstall -y $(MXDEV) + @$(PYTHON_PACKAGE_COMMAND) uninstall -y $(MXMAKE) endif INSTALL_TARGETS+=mxenv @@ -314,18 +315,18 @@ CLEAN_TARGETS+=mxenv-clean ZPRETTY_TARGET:=$(SENTINEL_FOLDER)/zpretty.sentinel $(ZPRETTY_TARGET): $(MXENV_TARGET) @echo "Install zpretty" - @$(MXENV_PATH)pip install zpretty + @$(PYTHON_PACKAGE_COMMAND) install zpretty @touch $(ZPRETTY_TARGET) .PHONY: zpretty-check zpretty-check: $(ZPRETTY_TARGET) @echo "Run zpretty check in: $(ZPRETTY_SRC)" - @find $(ZPRETTY_SRC) -name '*.zcml' -or -name '*.xml' -exec $(MXENV_PATH)zpretty --check {} + + @find $(ZPRETTY_SRC) -name '*.zcml' -or -name '*.xml' -exec zpretty --check {} + .PHONY: zpretty-format zpretty-format: $(ZPRETTY_TARGET) @echo "Run zpretty format in: $(ZPRETTY_SRC)" - @find $(ZPRETTY_SRC) -name '*.zcml' -or -name '*.xml' -exec $(MXENV_PATH)zpretty -i {} + + @find $(ZPRETTY_SRC) -name '*.zcml' -or -name '*.xml' -exec zpretty -i {} + .PHONY: zpretty-dirty zpretty-dirty: @@ -333,7 +334,7 @@ zpretty-dirty: .PHONY: zpretty-clean zpretty-clean: zpretty-dirty - @test -e $(MXENV_PATH)pip && $(MXENV_PATH)pip uninstall -y zpretty || : + @test -e $(MXENV_PYTHON) && $(MXENV_PYTHON) -m pip uninstall -y zpretty || : INSTALL_TARGETS+=$(ZPRETTY_TARGET) CHECK_TARGETS+=zpretty-check @@ -348,18 +349,18 @@ CLEAN_TARGETS+=zpretty-clean ISORT_TARGET:=$(SENTINEL_FOLDER)/isort.sentinel $(ISORT_TARGET): $(MXENV_TARGET) @echo "Install isort" - @$(MXENV_PATH)pip install isort + @$(PYTHON_PACKAGE_COMMAND) install isort @touch $(ISORT_TARGET) .PHONY: isort-check isort-check: $(ISORT_TARGET) @echo "Run isort check" - @$(MXENV_PATH)isort --check $(ISORT_SRC) + @isort --check $(ISORT_SRC) .PHONY: isort-format isort-format: $(ISORT_TARGET) @echo "Run isort format" - @$(MXENV_PATH)isort $(ISORT_SRC) + @isort $(ISORT_SRC) .PHONY: isort-dirty isort-dirty: @@ -367,7 +368,7 @@ isort-dirty: .PHONY: isort-clean isort-clean: isort-dirty - @test -e $(MXENV_PATH)pip && $(MXENV_PATH)pip uninstall -y isort || : + @test -e $(MXENV_PYTHON) && $(MXENV_PYTHON) -m pip uninstall -y isort || : INSTALL_TARGETS+=$(ISORT_TARGET) CHECK_TARGETS+=isort-check @@ -382,18 +383,18 @@ CLEAN_TARGETS+=isort-clean BLACK_TARGET:=$(SENTINEL_FOLDER)/black.sentinel $(BLACK_TARGET): $(MXENV_TARGET) @echo "Install Black" - @$(MXENV_PATH)pip install black + @$(PYTHON_PACKAGE_COMMAND) install black @touch $(BLACK_TARGET) .PHONY: black-check black-check: $(BLACK_TARGET) @echo "Run black checks" - @$(MXENV_PATH)black --check $(BLACK_SRC) + @black --check $(BLACK_SRC) .PHONY: black-format black-format: $(BLACK_TARGET) @echo "Run black format" - @$(MXENV_PATH)black $(BLACK_SRC) + @black $(BLACK_SRC) .PHONY: black-dirty black-dirty: @@ -401,7 +402,7 @@ black-dirty: .PHONY: black-clean black-clean: black-dirty - @test -e $(MXENV_PATH)pip && $(MXENV_PATH)pip uninstall -y black || : + @test -e $(MXENV_PYTHON) && $(MXENV_PYTHON) -m pip uninstall -y black || : INSTALL_TARGETS+=$(BLACK_TARGET) CHECK_TARGETS+=black-check @@ -409,44 +410,14 @@ FORMAT_TARGETS+=black-format DIRTY_TARGETS+=black-dirty CLEAN_TARGETS+=black-clean -############################################################################## -# python-ldap -############################################################################## - -PYTHON_LDAP_TARGET:=$(SENTINEL_FOLDER)/python-ldap.sentinel -$(PYTHON_LDAP_TARGET): $(MXENV_TARGET) $(OPENLDAP_TARGET) - @$(MXENV_PATH)pip install \ - --force-reinstall \ - --global-option=build_ext \ - --global-option="-I$(OPENLDAP_DIR)/include" \ - --global-option="-L$(OPENLDAP_DIR)/lib" \ - --global-option="-R$(OPENLDAP_DIR)/lib" \ - python-ldap - @touch $(PYTHON_LDAP_TARGET) - -.PHONY: python-ldap -python-ldap: $(PYTHON_LDAP_TARGET) - -.PHONY: python-ldap-dirty -python-ldap-dirty: - @rm -f $(PYTHON_LDAP_TARGET) - -.PHONY: python-ldap-clean -python-ldap-clean: python-ldap-dirty - @test -e $(MXENV_PATH)pip && $(MXENV_PATH)pip uninstall -y python-ldap || : - -INSTALL_TARGETS+=python-ldap -DIRTY_TARGETS+=python-ldap-dirty -CLEAN_TARGETS+=python-ldap-clean - ############################################################################## # sources ############################################################################## SOURCES_TARGET:=$(SENTINEL_FOLDER)/sources.sentinel -$(SOURCES_TARGET): $(MXENV_TARGET) +$(SOURCES_TARGET): $(PROJECT_CONFIG) $(MXENV_TARGET) @echo "Checkout project sources" - @$(MXENV_PATH)mxdev -o -c $(PROJECT_CONFIG) + @mxdev -o -c $(PROJECT_CONFIG) @touch $(SOURCES_TARGET) .PHONY: sources @@ -476,13 +447,11 @@ MXMAKE_FILES?=$(MXMAKE_FOLDER)/files # set environment variables for mxmake define set_mxfiles_env - @export MXMAKE_MXENV_PATH=$(1) - @export MXMAKE_FILES=$(2) + @export MXMAKE_FILES=$(1) endef # unset environment variables for mxmake define unset_mxfiles_env - @unset MXMAKE_MXENV_PATH @unset MXMAKE_FILES endef @@ -499,9 +468,10 @@ FILES_TARGET:=requirements-mxdev.txt $(FILES_TARGET): $(PROJECT_CONFIG) $(MXENV_TARGET) $(SOURCES_TARGET) $(LOCAL_PACKAGE_FILES) @echo "Create project files" @mkdir -p $(MXMAKE_FILES) - $(call set_mxfiles_env,$(MXENV_PATH),$(MXMAKE_FILES)) - @$(MXENV_PATH)mxdev -n -c $(PROJECT_CONFIG) - $(call unset_mxfiles_env,$(MXENV_PATH),$(MXMAKE_FILES)) + $(call set_mxfiles_env,$(MXMAKE_FILES)) + @mxdev -n -c $(PROJECT_CONFIG) + $(call unset_mxfiles_env) + @test -e $(MXMAKE_FILES)/pip.conf && cp $(MXMAKE_FILES)/pip.conf $(VENV_FOLDER)/pip.conf || : @touch $(FILES_TARGET) .PHONY: mxfiles @@ -529,11 +499,21 @@ ADDITIONAL_SOURCES_TARGETS?= INSTALLED_PACKAGES=$(MXMAKE_FILES)/installed.txt +ifeq ("$(PACKAGES_ALLOW_PRERELEASES)","true") +ifeq ("$(PYTHON_PACKAGE_INSTALLER)","uv") +PACKAGES_PRERELEASES=--prerelease=allow +else +PACKAGES_PRERELEASES=--pre +endif +else +PACKAGES_PRERELEASES= +endif + PACKAGES_TARGET:=$(INSTALLED_PACKAGES) $(PACKAGES_TARGET): $(FILES_TARGET) $(ADDITIONAL_SOURCES_TARGETS) @echo "Install python packages" - @$(MXENV_PATH)pip install -r $(FILES_TARGET) - @$(MXENV_PATH)pip freeze > $(INSTALLED_PACKAGES) + @$(PYTHON_PACKAGE_COMMAND) install $(PACKAGES_PRERELEASES) -r $(FILES_TARGET) + @$(PYTHON_PACKAGE_COMMAND) freeze > $(INSTALLED_PACKAGES) @touch $(PACKAGES_TARGET) .PHONY: packages @@ -546,8 +526,8 @@ packages-dirty: .PHONY: packages-clean packages-clean: @test -e $(FILES_TARGET) \ - && test -e $(MXENV_PATH)pip \ - && $(MXENV_PATH)pip uninstall -y -r $(FILES_TARGET) \ + && test -e $(MXENV_PYTHON) \ + && $(MXENV_PYTHON) -m pip uninstall -y -r $(FILES_TARGET) \ || : @rm -f $(PACKAGES_TARGET) @@ -562,14 +542,14 @@ CLEAN_TARGETS+=packages-clean TEST_TARGET:=$(SENTINEL_FOLDER)/test.sentinel $(TEST_TARGET): $(MXENV_TARGET) @echo "Install $(TEST_REQUIREMENTS)" - @$(MXENV_PATH)pip install $(TEST_REQUIREMENTS) + @$(PYTHON_PACKAGE_COMMAND) install $(TEST_REQUIREMENTS) @touch $(TEST_TARGET) .PHONY: test test: $(FILES_TARGET) $(SOURCES_TARGET) $(PACKAGES_TARGET) $(TEST_TARGET) $(TEST_DEPENDENCY_TARGETS) - @echo "Run tests" - @test -z "$(TEST_COMMAND)" && echo "No test command defined" - @test -z "$(TEST_COMMAND)" || bash -c "$(TEST_COMMAND)" + @test -z "$(TEST_COMMAND)" && echo "No test command defined" && exit 1 || : + @echo "Run tests using $(TEST_COMMAND)" + @/usr/bin/env bash -c "$(TEST_COMMAND)" .PHONY: test-dirty test-dirty: @@ -577,7 +557,7 @@ test-dirty: .PHONY: test-clean test-clean: test-dirty - @test -e $(MXENV_PATH)pip && $(MXENV_PATH)pip uninstall -y $(TEST_REQUIREMENTS) || : + @test -e $(MXENV_PYTHON) && $(MXENV_PYTHON) -m pip uninstall -y $(TEST_REQUIREMENTS) || : @rm -rf .pytest_cache INSTALL_TARGETS+=$(TEST_TARGET) @@ -591,14 +571,14 @@ DIRTY_TARGETS+=test-dirty COVERAGE_TARGET:=$(SENTINEL_FOLDER)/coverage.sentinel $(COVERAGE_TARGET): $(TEST_TARGET) @echo "Install Coverage" - @$(MXENV_PATH)pip install -U coverage + @$(PYTHON_PACKAGE_COMMAND) install -U coverage @touch $(COVERAGE_TARGET) .PHONY: coverage coverage: $(FILES_TARGET) $(SOURCES_TARGET) $(PACKAGES_TARGET) $(COVERAGE_TARGET) - @echo "Run coverage" - @test -z "$(COVERAGE_COMMAND)" && echo "No coverage command defined" - @test -z "$(COVERAGE_COMMAND)" || bash -c "$(COVERAGE_COMMAND)" + @test -z "$(COVERAGE_COMMAND)" && echo "No coverage command defined" && exit 1 || : + @echo "Run coverage using $(COVERAGE_COMMAND)" + @/usr/bin/env bash -c "$(COVERAGE_COMMAND)" .PHONY: coverage-dirty coverage-dirty: @@ -606,7 +586,7 @@ coverage-dirty: .PHONY: coverage-clean coverage-clean: coverage-dirty - @test -e $(MXENV_PATH)pip && $(MXENV_PATH)pip uninstall -y coverage || : + @test -e $(MXENV_PYTHON) && $(MXENV_PYTHON) -m pip uninstall -y coverage || : @rm -rf .coverage htmlcov INSTALL_TARGETS+=$(COVERAGE_TARGET) @@ -620,16 +600,19 @@ CLEAN_TARGETS+=coverage-clean COOKIECUTTER_TARGET:=$(SENTINEL_FOLDER)/cookiecutter.sentinel $(COOKIECUTTER_TARGET): $(MXENV_TARGET) @echo "Install cookiecutter" - @$(MXENV_PATH)pip install "cookiecutter>=2.1.1" + @$(PYTHON_PACKAGE_COMMAND) install "cookiecutter>=2.6.0" @touch $(COOKIECUTTER_TARGET) +.PHONY: cookiecutter +cookiecutter: $(COOKIECUTTER_TARGET) + .PHONY: cookiecutter-dirty cookiecutter-dirty: @rm -f $(COOKIECUTTER_TARGET) .PHONY: cookiecutter-clean cookiecutter-clean: cookiecutter-dirty - @test -e $(MXENV_PATH)pip && $(MXENV_PATH)pip uninstall -y cookiecutter || : + @test -e $(MXENV_PYTHON) && $(MXENV_PYTHON) -m pip uninstall -y cookiecutter || : @rm -f $(COOKIECUTTER_TARGET) DIRTY_TARGETS+=cookiecutter-dirty @@ -641,31 +624,43 @@ CLEAN_TARGETS+=cookiecutter-clean ZOPE_INSTANCE_FOLDER:=$(ZOPE_BASE_FOLDER)/instance ZOPE_INSTANCE_TARGET:=$(ZOPE_INSTANCE_FOLDER)/etc/zope.ini $(ZOPE_INSTANCE_FOLDER)/etc/zope.conf $(ZOPE_INSTANCE_FOLDER)/etc/site.zcml +ZOPE_RUN_TARGET:=$(ZOPE_INSTANCE_TARGET) $(PACKAGES_TARGET) + +ifeq (,$(ZOPE_TEMPLATE_CHECKOUT)) + ZOPE_COOKIECUTTER_TEMPLATE_OPTIONS= +else + ZOPE_COOKIECUTTER_TEMPLATE_OPTIONS=--checkout $(ZOPE_TEMPLATE_CHECKOUT) +endif ${ZOPE_CONFIGURATION_FILE}: @touch ${ZOPE_CONFIGURATION_FILE} $(ZOPE_INSTANCE_TARGET): $(COOKIECUTTER_TARGET) $(ZOPE_CONFIGURATION_FILE) @echo Create Plone/Zope configuration from $(ZOPE_TEMPLATE) to $(ZOPE_INSTANCE_FOLDER) - @$(MXENV_PATH)cookiecutter -f --no-input --config-file $(ZOPE_CONFIGURATION_FILE) --output-dir $(ZOPE_BASE_FOLDER) $(ZOPE_TEMPLATE) + @cookiecutter -f --no-input ${ZOPE_COOKIECUTTER_TEMPLATE_OPTIONS} --config-file $(ZOPE_CONFIGURATION_FILE) --output-dir $(ZOPE_BASE_FOLDER) $(ZOPE_TEMPLATE) .PHONY: zope-instance -zope-instance: $(ZOPE_INSTANCE_TARGET) $(SOURCES) +zope-instance: $(ZOPE_INSTANCE_TARGET) $(SOURCES_TARGET) .PHONY: zope-start -zope-start: $(ZOPE_INSTANCE_TARGET) $(PACKAGES_TARGET) +zope-start: $(ZOPE_RUN_TARGET) @echo "Start Zope/Plone with configuration in $(ZOPE_INSTANCE_FOLDER)" - @$(MXENV_PATH)runwsgi -v "$(ZOPE_INSTANCE_FOLDER)/etc/zope.ini" + @runwsgi -v "$(ZOPE_INSTANCE_FOLDER)/etc/zope.ini" .PHONY: zope-debug -zope-debug: $(ZOPE_INSTANCE_TARGET) $(PACKAGES_TARGET) +zope-debug: $(ZOPE_RUN_TARGET) @echo "Start Zope/Plone with configuration in $(ZOPE_INSTANCE_FOLDER)" - @$(MXENV_PATH)zconsole debug "$(ZOPE_INSTANCE_FOLDER)/etc/zope.ini" + @zconsole debug "$(ZOPE_INSTANCE_FOLDER)/etc/zope.conf" .PHONY: zope-runscript -zope-runscript: $(ZOPE_INSTANCE_TARGET) $(PACKAGES_TARGET) +zope-runscript: $(ZOPE_RUN_TARGET) @echo "Run Zope/Plone Console Script $(ZOPE_SCRIPTNAME) in $(ZOPE_INSTANCE_FOLDER)" - @$(MXENV_PATH)zconsole run "$(ZOPE_INSTANCE_FOLDER)/etc/zope.ini" $(ZOPE_SCRIPTNAME) + @zconsole run "$(ZOPE_INSTANCE_FOLDER)/etc/zope.conf" $(ZOPE_SCRIPTNAME) + +.PHONY: zope-adduser +zope-adduser: $(ZOPE_RUN_TARGET) + @echo "Run Zope addzopeuser to create an emergency user '$(ZOPE_USER_NAME)' with role 'Manager'" + @addzopeuser -c "$(ZOPE_INSTANCE_FOLDER)/etc/zope.conf" $(ZOPE_USER_NAME) $(ZOPE_USER_PASSWORD) .PHONY: zope-dirty zope-dirty: @@ -683,6 +678,7 @@ zope-purge: zope-dirty INSTALL_TARGETS+=zope-instance DIRTY_TARGETS+=zope-dirty CLEAN_TARGETS+=zope-clean +PURGE_TARGETS+=zope-purge ############################################################################## # gettext @@ -727,6 +723,10 @@ gettext-compile: "$(GETTEXT_LOCALES_PATH)/$$lang/LC_MESSAGES/$(GETTEXT_DOMAIN).po"; \ done +############################################################################## +# Custom includes +############################################################################## + -include $(INCLUDE_MAKEFILE) ############################################################################## @@ -768,5 +768,8 @@ runtime-clean: .PHONY: check check: $(CHECK_TARGETS) +.PHONY: typecheck +typecheck: $(TYPECHECK_TARGETS) + .PHONY: format format: $(FORMAT_TARGETS) From 3b2cdd2e68493e620d02fdd2ee403f9b4bdf030a Mon Sep 17 00:00:00 2001 From: "Jens W. Klein" Date: Mon, 23 Jun 2025 19:55:31 +0200 Subject: [PATCH 30/74] fix merge --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 6068493..fc1f76c 100644 --- a/setup.py +++ b/setup.py @@ -1,3 +1,3 @@ from setuptools import setup -setup() +setup() \ No newline at end of file From 94ea8d29b91e2ea45c0d3ea5516c301d4d809b4c Mon Sep 17 00:00:00 2001 From: "Jens W. Klein" Date: Mon, 23 Jun 2025 19:55:53 +0200 Subject: [PATCH 31/74] update some version for p6 --- setup.cfg | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/setup.cfg b/setup.cfg index 4f29950..58205ce 100644 --- a/setup.cfg +++ b/setup.cfg @@ -16,16 +16,17 @@ project_urls = classifiers = Development Status :: 5 - Production/Stable Environment :: Web Environment - Framework :: Plone :: 6.0 + Framework :: Plone :: 6.1 Framework :: Plone :: Addon Framework :: Plone Framework :: Zope :: 5 Framework :: Zope License :: OSI Approved :: GNU General Public License v2 (GPLv2) Operating System :: OS Independent - Programming Language :: Python :: 3.9 Programming Language :: Python :: 3.10 Programming Language :: Python :: 3.11 + Programming Language :: Python :: 3.12 + Programming Language :: Python :: 3.13 Topic :: System :: Systems Administration :: Authentication/Directory :: LDAP Programming Language :: Python @@ -42,11 +43,12 @@ install_requires = Products.PlonePAS Products.PluggableAuthService Products.statusmessages + plone.api python-ldap>=3.4.0 setuptools - yafowil.plone>=4.0.0a3 - yafowil.widget.array - yafowil.widget.dict + yafowil.plone>=5.0.0a1 + yafowil.widget.array>=2.0a1 + yafowil.widget.dict>=2.0a1 yafowil.yaml zope.globalrequest Zope>=5 From fec62c6183b3ba34b09381e96f1f534d2941f549 Mon Sep 17 00:00:00 2001 From: "Jens W. Klein" Date: Mon, 23 Jun 2025 19:56:11 +0200 Subject: [PATCH 32/74] pin yafowil --- requirements.txt | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/requirements.txt b/requirements.txt index 17178b7..110e0f8 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,3 +4,7 @@ waitress_fastlisten # until https://github.com/plone/plone.restapi/issues/1321 is solved plone.app.iterate +yafowil.plone==5.0.0a2 +yafowil.bootstrap==2.0.0a1 +yafowil.widget.array==2.0a1 +yafowil.widget.dict==2.0a1 \ No newline at end of file From a0d2f2a2f5973ef4865121258444cc047eca3068 Mon Sep 17 00:00:00 2001 From: "Jens W. Klein" Date: Mon, 23 Jun 2025 19:56:37 +0200 Subject: [PATCH 33/74] bump to 6.1 --- constraints.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/constraints.txt b/constraints.txt index dde5b0e..55f014e 100644 --- a/constraints.txt +++ b/constraints.txt @@ -1 +1 @@ --c https://dist.plone.org/release/6.0.5/constraints.txt +-c https://dist.plone.org/release/6.1.2/constraints.txt From 0b89dc4036ae6ff6548da8b20569bc4cd09222ca Mon Sep 17 00:00:00 2001 From: "Jens W. Klein" Date: Mon, 23 Jun 2025 19:56:57 +0200 Subject: [PATCH 34/74] use yafowil releases --- mx.ini | 58 +++++++++++++++++++++++++++++----------------------------- 1 file changed, 29 insertions(+), 29 deletions(-) diff --git a/mx.ini b/mx.ini index 04df64d..f1a3dd5 100644 --- a/mx.ini +++ b/mx.ini @@ -35,32 +35,32 @@ environment = env # url = ${settings:github}/conestack/node.ext.ugm.git # branch = master -[yafowil] -url = ${settings:github}/conestack/yafowil.git -pushurl = ${settings:github-push}:conestack/yafowil.git -branch = master - -[yafowil.bootstrap] -url = ${settings:github}/conestack/yafowil.bootstrap.git -pushurl = ${settings:github-push}:conestack/yafowil.bootstrap.git -branch = 2.0 - -[yafowil.plone] -url = ${settings:github}/bluedynamics/yafowil.plone.git -pushurl = ${settings:github}:bluedynamics/yafowil.plone.git -branch = master - -[yafowil.widget.array] -url = ${settings:github}/conestack/yafowil.widget.array.git -pushurl = ${settings:github-push}:conestack/yafowil.widget.array.git -branch = 2.0 - -[yafowil.widget.dict] -url = ${settings:github}/conestack/yafowil.widget.dict.git -pushurl = ${settings:github-push}:conestack/yafowil.widget.dict.git -branch = 2.0 - -[node.ext.ldap] -url = ${settings:github}/conestack/node.ext.ldap.git -pushurl = ${settings:github-push}:conestack/node.ext.ldap.git -branch = master +# [yafowil] +# url = ${settings:github}/conestack/yafowil.git +# pushurl = ${settings:github-push}:conestack/yafowil.git +# branch = master + +# [yafowil.bootstrap] +# url = ${settings:github}/conestack/yafowil.bootstrap.git +# pushurl = ${settings:github-push}:conestack/yafowil.bootstrap.git +# branch = 2.0 + +# [yafowil.plone] +# url = ${settings:github}/bluedynamics/yafowil.plone.git +# pushurl = ${settings:github}:bluedynamics/yafowil.plone.git +# branch = master + +# [yafowil.widget.array] +# url = ${settings:github}/conestack/yafowil.widget.array.git +# pushurl = ${settings:github-push}:conestack/yafowil.widget.array.git +# branch = 2.0 + +# [yafowil.widget.dict] +# url = ${settings:github}/conestack/yafowil.widget.dict.git +# pushurl = ${settings:github-push}:conestack/yafowil.widget.dict.git +# branch = 2.0 + +# [node.ext.ldap] +# url = ${settings:github}/conestack/node.ext.ldap.git +# pushurl = ${settings:github-push}:conestack/node.ext.ldap.git +# branch = master From f622dc199179cdcbcc1cfc4d6f3ac4827c26f670 Mon Sep 17 00:00:00 2001 From: "Jens W. Klein" Date: Mon, 23 Jun 2025 19:57:23 +0200 Subject: [PATCH 35/74] try to update action to run matrix right --- .github/workflows/tests.yaml | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index cec88d7..3d3f63f 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -9,27 +9,32 @@ jobs: fail-fast: false matrix: python: - - "3.9" - "3.10" - "3.11" + - "3.12" + - "3.13" plone: - - "6.0.5" + - "6.0.14" + - "6.1.2" steps: - uses: actions/checkout@v2 - name: Install system packages - run: sudo apt-get install -y libsasl2-dev libssl-dev libdb-dev libldap2-dev + run: | + sudo apt-get update -y + sudo apt-get install -y slapd - name: Setup Plone ${{ matrix.plone }} with Python ${{ matrix.python }} id: setup - uses: plone/setup-plone@v2.0.0 + uses: plone/setup-plone@v3.0.0 with: python-version: ${{ matrix.python }} plone-version: ${{ matrix.plone }} - name: Install package run: | + sed -i "s#\(-c https://dist.plone.org/release/\)[^/]\+\(/constraints.txt\)#\1${{ matrix.plone }}\2#" requirements.txt make VENV=off install - name: Run tests From f667363870ef8fbb5e85a4662d5d28f40ab4c5eb Mon Sep 17 00:00:00 2001 From: "Jens W. Klein" Date: Mon, 23 Jun 2025 20:01:22 +0200 Subject: [PATCH 36/74] exclude and install uv --- .github/workflows/tests.yaml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index 3d3f63f..b1761f7 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -16,12 +16,16 @@ jobs: plone: - "6.0.14" - "6.1.2" + exclude: + - python: "3.13" + plone: "6.0.14" steps: - uses: actions/checkout@v2 - name: Install system packages run: | + curl -LsSf https://astral.sh/uv/install.sh | sh sudo apt-get update -y sudo apt-get install -y slapd From 6408f3b7f0b25bde71936678e6d8d65e7f6632c4 Mon Sep 17 00:00:00 2001 From: "Jens W. Klein" Date: Mon, 23 Jun 2025 20:05:01 +0200 Subject: [PATCH 37/74] python-ldap - no wheel --- .github/workflows/tests.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index b1761f7..db8215b 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -27,7 +27,7 @@ jobs: run: | curl -LsSf https://astral.sh/uv/install.sh | sh sudo apt-get update -y - sudo apt-get install -y slapd + sudo apt-get install -y libsasl2-dev libssl-dev libdb-dev libldap2-dev slapd - name: Setup Plone ${{ matrix.plone }} with Python ${{ matrix.python }} id: setup From c40cb486464d0c2ecd98597b16f89fcbf9e7bcad Mon Sep 17 00:00:00 2001 From: "Jens W. Klein" Date: Mon, 23 Jun 2025 20:49:42 +0200 Subject: [PATCH 38/74] no openldap here --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index dd7c090..456bd84 100644 --- a/Makefile +++ b/Makefile @@ -141,7 +141,7 @@ TEST_REQUIREMENTS?=zope.testrunner # Additional make targets the test target depends on. # No default value. -TEST_DEPENDENCY_TARGETS?=openldap +TEST_DEPENDENCY_TARGETS?= ## qa.coverage From fb28699eed2a9355e0e013a00bb312e32304c553 Mon Sep 17 00:00:00 2001 From: "Jens W. Klein" Date: Mon, 23 Jun 2025 21:08:26 +0200 Subject: [PATCH 39/74] update readme --- README_MAKE.md | 38 +++++++++++++++++--------------------- 1 file changed, 17 insertions(+), 21 deletions(-) diff --git a/README_MAKE.md b/README_MAKE.md index f398b94..bf4c8ed 100644 --- a/README_MAKE.md +++ b/README_MAKE.md @@ -1,36 +1,37 @@ # Information about the Makefile -How to use the pip, mxdev, cookiecutter-zope-instance and make based install. +How to use the uv, mxdev, cookiecutter-zope-instance and make based install. ## Usage +Install [uv](https://docs.astral.sh/uv/getting-started/installation/). + On the commandline, execute the ``make`` command. -Without any options, make will run all steps. +Without any options, make will run nothing, so pass in a command. + +Run the Zope-Server: ```bash make run ``` -All options are printed with +Run all tests: ```bash -make help +make test ``` -```text -clean remove instance configuration (keeps data) -help This help message -install pip install all dependencies and scripts -instance create configuration for an zope (plone) instance -prepare prepare soures and dependencies -run run Plone +All options are printed with + +```bash +make help ``` -Order for ``make run`` is: *prepare*, *install*, *instance*, *run*. +``make run`` resolves dependencies in order like: *prepare*, *install*, *instance*, *run*. The Makefile is built to detect changes. -At the first ``make run`` all steps are excuted. -Subsequent calls are only starting the applicaton server in the *run* step. +At the first ``make run`` all steps are executed. +Subsequent calls are only starting the application server in the *run* step. If one of the input file is changed, steps needed to take those changes into effect are executed again.\ ## Python @@ -49,11 +50,6 @@ The Makefile support different modes of Python: ## Files -Initially the files below were generated by `plone-kickstarter`. -They are meant to be modified for your needs. - -They aim to ease development and deployment of *Plone 6+* - `constraints.txt` Version pins for your project, used by *pip*. `README_MAKE.md` @@ -64,7 +60,7 @@ They aim to ease development and deployment of *Plone 6+* The configuration for *make* `requirement.txt` The core requirements. -`sources.ini` +`mx.ini` *mxdev* is used to develop with sources from VCS like Git. If you need sources from git, add them here. @@ -73,6 +69,6 @@ They aim to ease development and deployment of *Plone 6+* The configuration here uses: - `make` -- [pip](https://pip.pypa.io/en/stable/) +- [uv](https://docs.astral.sh/uv/getting-started/installation/) - [mxdev](https://pypi.org/project/mxdev) - [cookiecutter-zope-instance](https://github.com/bluedynamics/cookiecutter-zope-instance/) From 92de68ad25406b8a58207e5c320bb6c0a1e79f63 Mon Sep 17 00:00:00 2001 From: "Jens W. Klein" Date: Mon, 23 Jun 2025 21:10:43 +0200 Subject: [PATCH 40/74] pyproejct.tom, hatchling and pep420 namespaces --- pyproject.toml | 88 +++++++++++++++++++ setup.cfg | 88 ------------------- setup.py | 3 - src/pas/__init__.py | 1 - src/pas/plugins/__init__.py | 1 - .../ldap/plonecontrolpanel/__init__.py | 18 ---- src/pas/plugins/ldap/tests/__init__.py | 0 7 files changed, 88 insertions(+), 111 deletions(-) create mode 100644 pyproject.toml delete mode 100644 setup.cfg delete mode 100644 setup.py delete mode 100644 src/pas/__init__.py delete mode 100644 src/pas/plugins/__init__.py delete mode 100644 src/pas/plugins/ldap/plonecontrolpanel/__init__.py delete mode 100644 src/pas/plugins/ldap/tests/__init__.py diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..b14111f --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,88 @@ +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[project] +name = "pas-plugins-ldap" +version = "2.0.0.dev0" +description = "LDAP/AD Plugin for Plone/Zope PluggableAuthService (users+groups)" +readme = "README.rst" +license = { text = "GPL 2.0" } +authors = [ + { name = "BlueDynamics Alliance", email = "dev@bluedynamics.com" }, +] +keywords = [ + "authentication", + "groups", + "ldap", + "pas", + "plone", + "plugin", + "users", + "zope", +] +classifiers = [ + "Development Status :: 5 - Production/Stable", + "Environment :: Web Environment", + "Framework :: Plone", + "Framework :: Plone :: 6.1", + "Framework :: Plone :: Addon", + "Framework :: Zope", + "Framework :: Zope :: 5", + "License :: OSI Approved :: GNU General Public License v2 (GPLv2)", + "Operating System :: OS Independent", + "Programming Language :: Python", + "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Topic :: System :: Systems Administration :: Authentication/Directory :: LDAP", +] +dependencies = [ + "bda.cache", + "five.globalrequest", + "node.ext.ldap>=1.0rc1", + "odict", + "plone.api", + "plone.registry", + "Products.CMFCore", + "Products.GenericSetup", + "Products.PlonePAS", + "Products.PluggableAuthService", + "Products.statusmessages", + "python-ldap>=3.4.0", + "setuptools", + "yafowil.plone>=5.0.0a1", + "yafowil.widget.array>=2.0a1", + "yafowil.widget.dict>=2.0a1", + "yafowil.yaml", + "zope.globalrequest", + "Zope>=5", +] + +[project.optional-dependencies] +plone = [ + "Products.CMFPlone", +] +test = [ + "plone.testing", +] + +[project.entry-points."z3c.autoinclude.plugin"] +target = "plone" + +[project.urls] +ChangeLog = "https://github.com/collective/pas.plugins.ldap/blob/master/CHANGES.rst" +Homepage = "https://github.com/collective/pas.plugins.ldap/" +"Issue Tracker" = "https://github.com/collective/pas.plugins.ldap/issues" +"Source Code" = "https://github.com/collective/pas.plugins.ldap" + +[tool.hatch.build.targets.wheel] +packages = [ + " src/pas_plugins_ldap", +] + +[tool.hatch.build.targets.sdist] +include = [ + "/ src", +] diff --git a/setup.cfg b/setup.cfg deleted file mode 100644 index 58205ce..0000000 --- a/setup.cfg +++ /dev/null @@ -1,88 +0,0 @@ -[metadata] -name = pas.plugins.ldap -version=2.0.0.dev0 -description=LDAP/AD Plugin for Plone/Zope PluggableAuthService (users+groups) -long_description = file: README.rst, CHANGES.rst -keywords = zope pas plone ldap authentication plugin users groups -author = BlueDynamics Alliance -author_email = dev@bluedynamics.com -license = GPLv2 -license_files = LICENSE.rst -url = https://github.com/collective/pas.plugins.ldap/ -project_urls = - ChangeLog = https://github.com/collective/pas.plugins.ldap/blob/master/CHANGES.rst - Issue Tracker = https://github.com/collective/pas.plugins.ldap/issues - Source Code = https://github.com/collective/pas.plugins.ldap -classifiers = - Development Status :: 5 - Production/Stable - Environment :: Web Environment - Framework :: Plone :: 6.1 - Framework :: Plone :: Addon - Framework :: Plone - Framework :: Zope :: 5 - Framework :: Zope - License :: OSI Approved :: GNU General Public License v2 (GPLv2) - Operating System :: OS Independent - Programming Language :: Python :: 3.10 - Programming Language :: Python :: 3.11 - Programming Language :: Python :: 3.12 - Programming Language :: Python :: 3.13 - Topic :: System :: Systems Administration :: Authentication/Directory :: LDAP - Programming Language :: Python - -[options] -setup_requires = setuptools -install_requires = - bda.cache - five.globalrequest - node.ext.ldap>=1.0rc1 - odict - plone.registry - Products.CMFCore - Products.GenericSetup - Products.PlonePAS - Products.PluggableAuthService - Products.statusmessages - plone.api - python-ldap>=3.4.0 - setuptools - yafowil.plone>=5.0.0a1 - yafowil.widget.array>=2.0a1 - yafowil.widget.dict>=2.0a1 - yafowil.yaml - zope.globalrequest - Zope>=5 - -include_package_data = True -zip_safe = False -namespace_packages = - pas - pas.plugins - -package_dir = - = src -packages = find: - -[options.packages.find] -where = - src - -[options.entry_points] -z3c.autoinclude.plugin = - target = plone - -[options.extras_require] -test = - plone.testing - -plone = - Products.CMFPlone - -[isort] -profile = black -force_alphabetical_sort=True -force_single_line = True -lines_after_imports = 2 - -[zest.releaser] -create-wheel = yes diff --git a/setup.py b/setup.py deleted file mode 100644 index fc1f76c..0000000 --- a/setup.py +++ /dev/null @@ -1,3 +0,0 @@ -from setuptools import setup - -setup() \ No newline at end of file diff --git a/src/pas/__init__.py b/src/pas/__init__.py deleted file mode 100644 index 5284146..0000000 --- a/src/pas/__init__.py +++ /dev/null @@ -1 +0,0 @@ -__import__("pkg_resources").declare_namespace(__name__) diff --git a/src/pas/plugins/__init__.py b/src/pas/plugins/__init__.py deleted file mode 100644 index 5284146..0000000 --- a/src/pas/plugins/__init__.py +++ /dev/null @@ -1 +0,0 @@ -__import__("pkg_resources").declare_namespace(__name__) diff --git a/src/pas/plugins/ldap/plonecontrolpanel/__init__.py b/src/pas/plugins/ldap/plonecontrolpanel/__init__.py deleted file mode 100644 index 548c5b5..0000000 --- a/src/pas/plugins/ldap/plonecontrolpanel/__init__.py +++ /dev/null @@ -1,18 +0,0 @@ -from Products.CMFPlone.interfaces.installable import INonInstallable -from zope.interface import implementer - - -@implementer(INonInstallable) -class HiddenProfiles: - """This hides zope2 profile from the quick installer tool and plone cpanel""" - - _hidden = [ - "pas.plugins.ldap:default", - "pas.plugins.ldap.plonecontrolpanel:install-base", - ] - - def getNonInstallableProducts(self): - return self._hidden - - def getNonInstallableProfiles(self): - return self._hidden diff --git a/src/pas/plugins/ldap/tests/__init__.py b/src/pas/plugins/ldap/tests/__init__.py deleted file mode 100644 index e69de29..0000000 From 59263371c511e051f23dd633ed2c7b51776f223a Mon Sep 17 00:00:00 2001 From: "Jens W. Klein" Date: Mon, 23 Jun 2025 21:13:24 +0200 Subject: [PATCH 41/74] old stuff removed --- base.cfg | 107 -------------------------------------------------- build_ldap.sh | 11 ------ 2 files changed, 118 deletions(-) delete mode 100644 base.cfg delete mode 100755 build_ldap.sh diff --git a/base.cfg b/base.cfg deleted file mode 100644 index 9c67af6..0000000 --- a/base.cfg +++ /dev/null @@ -1,107 +0,0 @@ -[buildout] -extends = ldap.cfg -parts += - zopeomelette - ploneomelette - coverage - test - test-coverage - releaser - code-analysis - vscode - -develop = . -unzip = true - -[code-analysis] -recipe = plone.recipe.codeanalysis -directory = ${buildout:directory}/src -flake8-ignore = C901,E241,E501 -flake8-max-complexity = 20 -clean-lines = False -imports = True -debug-statements = True -utf8-header = True - -[instance] -recipe = plone.recipe.zope2instance -user = admin:admin -http-address = 8080 -debug-mode = on -verbose-security = off -deprecation-warnings = on -blob-storage = var/blobstorage - -eggs = - ${python-ldap:egg} - pas.plugins.ldap - pdbpp - -zcml = - pas.plugins.ldap - -[plone] -recipe = plone.recipe.zope2instance -user = admin:admin -http-address = 8081 -debug-mode = on -verbose-security = off -deprecation-warnings = on -blob-storage = var/blobstorage - -eggs = - ${python-ldap:egg} - pas.plugins.ldap[plone] - pdbpp - -zcml = - pas.plugins.ldap - -[releaser] -recipe = zc.recipe.egg -eggs = zest.releaser[recommended] - -[test] -recipe = zc.recipe.testrunner -eggs = - ${python-ldap:egg} - pas.plugins.ldap[test] - -environment = testenv -defaults = ['--auto-color', '--auto-progress'] - -[coverage] -recipe = zc.recipe.egg -eggs = coverage - -[test-coverage] -recipe = collective.recipe.template -input = inline: - #!/bin/bash - ${buildout:directory}/bin/coverage run --source=${buildout:directory}/src/pas/plugins/ldap bin/test - ${buildout:directory}/bin/coverage html - ${buildout:directory}/bin/coverage report -m --fail-under=90 - # Fail (exit status 1) if coverage returns exit status 2 (this happens - # when test coverage is below 100%. -output = ${buildout:directory}/bin/test-coverage -mode = 755 - -[zopeomelette] -recipe = collective.recipe.omelette -eggs = - ${instance:eggs} -ignore-develop = true - -[ploneomelette] -recipe = collective.recipe.omelette -eggs = - ${plone:eggs} -ignore-develop = true - -[vscode] -recipe = collective.recipe.vscode -eggs = ${test:eggs} -flake8-enabled = false -black-enabled = true -generate-envfile = true - diff --git a/build_ldap.sh b/build_ldap.sh deleted file mode 100755 index 2321df0..0000000 --- a/build_ldap.sh +++ /dev/null @@ -1,11 +0,0 @@ -#!/bin/bash - -cd $1 - -./configure --with-tls --enable-slapd=yes --enable-overlays CPPFLAGS=-D_GNU_SOURCE --prefix=$2 -make clean -make depend -make -j4 -make install - -cd - \ No newline at end of file From bdf14e77cec8e9c656f18483700fd276d9b1898c Mon Sep 17 00:00:00 2001 From: "Jens W. Klein" Date: Mon, 23 Jun 2025 21:13:41 +0200 Subject: [PATCH 42/74] typo --- README_MAKE.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README_MAKE.md b/README_MAKE.md index bf4c8ed..784ab9f 100644 --- a/README_MAKE.md +++ b/README_MAKE.md @@ -32,7 +32,7 @@ make help The Makefile is built to detect changes. At the first ``make run`` all steps are executed. Subsequent calls are only starting the application server in the *run* step. -If one of the input file is changed, steps needed to take those changes into effect are executed again.\ +If one of the input file is changed, steps needed to take those changes into effect are executed again. ## Python From 9d5fdcdf4cf0f74087df1cd6bf9ea5de39fef98d Mon Sep 17 00:00:00 2001 From: "Jens W. Klein" Date: Mon, 23 Jun 2025 22:23:45 +0200 Subject: [PATCH 43/74] accidentially deleted --- .../ldap/plonecontrolpanel/__init__.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 src/pas/plugins/ldap/plonecontrolpanel/__init__.py diff --git a/src/pas/plugins/ldap/plonecontrolpanel/__init__.py b/src/pas/plugins/ldap/plonecontrolpanel/__init__.py new file mode 100644 index 0000000..698012e --- /dev/null +++ b/src/pas/plugins/ldap/plonecontrolpanel/__init__.py @@ -0,0 +1,19 @@ +from Products.CMFPlone.interfaces.installable import INonInstallable +from zope.interface import implementer + + +@implementer(INonInstallable) +class HiddenProfiles: + """This hides zope2 profile from the quick installer tool and plone cpanel""" + + _hidden = [ + "pas.plugins.ldap:default", + "pas.plugins.ldap.plonecontrolpanel:install-base", + ] + + def getNonInstallableProducts(self): + return self._hidden + + def getNonInstallableProfiles(self): + return self._hidden + From 614270279d74690927013e9be112106a97395b1c Mon Sep 17 00:00:00 2001 From: "Jens W. Klein" Date: Mon, 23 Jun 2025 22:24:20 +0200 Subject: [PATCH 44/74] move tests and make them run (and fail) --- .gitignore | 3 +- CHANGES.rst | 5 ++ Makefile | 2 +- mx.ini | 4 +- pyproject.toml | 38 +++++++------- src/pas/plugins/ldap/tests/test_doctests.py | 49 ------------------- {src/pas/plugins/ldap => tests}/cache.rst | 0 tests/conftest.py | 15 ++++++ .../pas/plugins/ldap => tests}/properties.rst | 0 tests/test_doctests.py | 33 +++++++++++++ .../ldap/tests => tests}/test_plugin.py | 2 +- {src/pas/plugins/ldap => tests}/testing.py | 9 ++-- 12 files changed, 84 insertions(+), 76 deletions(-) delete mode 100644 src/pas/plugins/ldap/tests/test_doctests.py rename {src/pas/plugins/ldap => tests}/cache.rst (100%) create mode 100644 tests/conftest.py rename {src/pas/plugins/ldap => tests}/properties.rst (100%) create mode 100644 tests/test_doctests.py rename {src/pas/plugins/ldap/tests => tests}/test_plugin.py (99%) rename {src/pas/plugins/ldap => tests}/testing.py (92%) diff --git a/.gitignore b/.gitignore index 65a4f26..78c3c45 100644 --- a/.gitignore +++ b/.gitignore @@ -14,6 +14,7 @@ __pycache__ /.pydevproject /.Python /.settings/ +/.venv/ /.vscode/ /*eggs/ /coverage/ @@ -24,5 +25,3 @@ __pycache__ /pip-selfcheck.json /sources/ /src/*.egg-info -/venv/ -/.openldap/ diff --git a/CHANGES.rst b/CHANGES.rst index 8f62b18..4d1f3a9 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -2,6 +2,11 @@ History ======= +2.0.0 (unreleased) +------------------ + +- todo + 1.9.0 (unreleased) ------------------ diff --git a/Makefile b/Makefile index 456bd84..f72a9f9 100644 --- a/Makefile +++ b/Makefile @@ -86,7 +86,7 @@ VENV_CREATE?=true # `VENV_CREATE` is false it is expected to point to an existing virtual # environment. If `VENV_ENABLED` is `false` it is ignored. # Default: .venv -VENV_FOLDER?=venv +VENV_FOLDER?=.venv # mxdev to install in virtual environment. # Default: mxdev diff --git a/mx.ini b/mx.ini index f1a3dd5..c39694f 100644 --- a/mx.ini +++ b/mx.ini @@ -9,11 +9,13 @@ mxmake-templates = run-tests run-coverage -mxmake-test-runner = zope-testrunner +# mxmake-test-runner = pytest +# environment variables # environment variables [mxmake-env] # VAR = value +testpaths = tests [mxmake-run-tests] environment = env diff --git a/pyproject.toml b/pyproject.toml index b14111f..8d61d6a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,16 +1,10 @@ -[build-system] -requires = ["hatchling"] -build-backend = "hatchling.build" - [project] -name = "pas-plugins-ldap" +name = "pas.plugins.ldap" version = "2.0.0.dev0" description = "LDAP/AD Plugin for Plone/Zope PluggableAuthService (users+groups)" readme = "README.rst" license = { text = "GPL 2.0" } -authors = [ - { name = "BlueDynamics Alliance", email = "dev@bluedynamics.com" }, -] +authors = [{ name = "BlueDynamics Alliance", email = "dev@bluedynamics.com" }] keywords = [ "authentication", "groups", @@ -61,12 +55,8 @@ dependencies = [ ] [project.optional-dependencies] -plone = [ - "Products.CMFPlone", -] -test = [ - "plone.testing", -] +plone = ["Products.CMFPlone"] +test = ["plone.testing", "pytest-plone"] [project.entry-points."z3c.autoinclude.plugin"] target = "plone" @@ -77,12 +67,24 @@ Homepage = "https://github.com/collective/pas.plugins.ldap/" "Issue Tracker" = "https://github.com/collective/pas.plugins.ldap/issues" "Source Code" = "https://github.com/collective/pas.plugins.ldap" +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + [tool.hatch.build.targets.wheel] -packages = [ - " src/pas_plugins_ldap", -] +packages = ["src/pas"] [tool.hatch.build.targets.sdist] include = [ - "/ src", + "/src", ] + +[tool.pytest.ini_options] +minversion = "6.0" +testpaths = [ + "tests", +] + +[tool.isort] +profile = "plone" + diff --git a/src/pas/plugins/ldap/tests/test_doctests.py b/src/pas/plugins/ldap/tests/test_doctests.py deleted file mode 100644 index 861c8db..0000000 --- a/src/pas/plugins/ldap/tests/test_doctests.py +++ /dev/null @@ -1,49 +0,0 @@ -from ..testing import PASLDAPLayer -from plone.testing import layered -from plone.testing import z2 - -import doctest -import pprint -import re -import six -import unittest - - -optionflags = doctest.NORMALIZE_WHITESPACE | doctest.ELLIPSIS -optionflags = optionflags | doctest.REPORT_ONLY_FIRST_FAILURE - - -class Py23DocChecker(doctest.OutputChecker): - def check_output(self, want, got, optionflags): - if want != got and six.PY2: - # if running on py2, ignore any "u" prefixes in the output - got = re.sub("(\\W|^)u'(.*?)'", "\\1'\\2'", got) - got = re.sub('(\\W|^)u"(.*?)"', '\\1"\\2"', got) - # also ignore "b" prefixes in the expected output - want = re.sub("b'(.*?)'", "'\\1'", want) - # we get 'ldap.' prefixes on python 3, e.g. - # ldap.UNWILLING_TO_PERFORM - want = want.lstrip("ldap.") - return doctest.OutputChecker.check_output(self, want, got, optionflags) - - -TESTFILES = [("../properties.rst", PASLDAPLayer), ("../cache.rst", PASLDAPLayer)] - - -def test_suite(): - suite = unittest.TestSuite() - suite.addTests( - [ - layered( - doctest.DocFileSuite( - docfile, - globs={"pprint": pprint.pprint, "z2": z2}, - optionflags=optionflags, - checker=Py23DocChecker(), - ), - layer=layer(), - ) - for docfile, layer in TESTFILES - ] - ) - return suite diff --git a/src/pas/plugins/ldap/cache.rst b/tests/cache.rst similarity index 100% rename from src/pas/plugins/ldap/cache.rst rename to tests/cache.rst diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..198462d --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,15 @@ +from testing import PASLDAP_FIXTURE +from pytest_plone import fixtures_factory + + +pytest_plugins = ["pytest_plone"] + + +globals().update( + fixtures_factory( + ( + (PASLDAP_FIXTURE, "ldap"), + + ) + ) +) \ No newline at end of file diff --git a/src/pas/plugins/ldap/properties.rst b/tests/properties.rst similarity index 100% rename from src/pas/plugins/ldap/properties.rst rename to tests/properties.rst diff --git a/tests/test_doctests.py b/tests/test_doctests.py new file mode 100644 index 0000000..9f433a3 --- /dev/null +++ b/tests/test_doctests.py @@ -0,0 +1,33 @@ +from testing import PASLDAPLayer +from plone.testing import layered +from plone.testing import zope + +import doctest +import pprint +import re +import six +import unittest + + +optionflags = doctest.NORMALIZE_WHITESPACE | doctest.ELLIPSIS +optionflags = optionflags | doctest.REPORT_ONLY_FIRST_FAILURE + +TESTFILES = [("properties.rst", PASLDAPLayer), ("cache.rst", PASLDAPLayer)] + + +def test_suite(): + suite = unittest.TestSuite() + suite.addTests( + [ + layered( + doctest.DocFileSuite( + docfile, + globs={"pprint": pprint.pprint, "z2": zope}, + optionflags=optionflags, + ), + layer=layer(), + ) + for docfile, layer in TESTFILES + ] + ) + return suite diff --git a/src/pas/plugins/ldap/tests/test_plugin.py b/tests/test_plugin.py similarity index 99% rename from src/pas/plugins/ldap/tests/test_plugin.py rename to tests/test_plugin.py index 26dae63..9ff094b 100644 --- a/src/pas/plugins/ldap/tests/test_plugin.py +++ b/tests/test_plugin.py @@ -1,4 +1,4 @@ -from ..testing import PASLDAP_FIXTURE +from testing import PASLDAP_FIXTURE from Products.PlonePAS.plugins.ufactory import PloneUser import unittest diff --git a/src/pas/plugins/ldap/testing.py b/tests/testing.py similarity index 92% rename from src/pas/plugins/ldap/testing.py rename to tests/testing.py index 579338a..f7ea0a8 100644 --- a/src/pas/plugins/ldap/testing.py +++ b/tests/testing.py @@ -1,7 +1,8 @@ -from .cache import cacheProviderFactory -from .interfaces import ICacheSettingsRecordProvider -from .plonecontrolpanel.cache import CacheSettingsRecordProvider -from .properties import LDAPProps +from pas.plugins.ldap.cache import cacheProviderFactory +from pas.plugins.ldap.cache import cacheProviderFactory +from pas.plugins.ldap.interfaces import ICacheSettingsRecordProvider +from pas.plugins.ldap.plonecontrolpanel.cache import CacheSettingsRecordProvider +from pas.plugins.ldap.properties import LDAPProps from node.ext.ldap import testing as ldaptesting from node.ext.ldap.interfaces import ICacheProviderFactory from node.ext.ldap.interfaces import ILDAPGroupsConfig From 12f66712adc15f4462702366a22f962422c4b010 Mon Sep 17 00:00:00 2001 From: "Jens W. Klein" Date: Mon, 23 Jun 2025 22:34:17 +0200 Subject: [PATCH 45/74] re-add openldap --- Makefile | 65 +++++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 64 insertions(+), 1 deletion(-) diff --git a/Makefile b/Makefile index f72a9f9..9de22de 100644 --- a/Makefile +++ b/Makefile @@ -10,6 +10,7 @@ #: core.packages #: core.sources #: i18n.gettext +#: ldap.openldap #: qa.black #: qa.coverage #: qa.isort @@ -44,6 +45,24 @@ INCLUDE_MAKEFILE?=include.mk # No default value. EXTRA_PATH?= +## ldap.openldap + +# OpenLDAP version to download +# Default: 2.4.59 +OPENLDAP_VERSION?=2.4.59 + +# OpenLDAP base download URL +# Default: https://www.openldap.org/software/download/OpenLDAP/openldap-release/ +OPENLDAP_URL?=https://www.openldap.org/software/download/OpenLDAP/openldap-release/ + +# Build directory for OpenLDAP +# Default: $(shell echo $(realpath .))/openldap +OPENLDAP_DIR?=$(shell echo $(realpath .))/openldap + +# Build environment for OpenLDAP +# Default: PATH=/usr/local/bin:/usr/bin:/bin +OPENLDAP_ENV?=PATH=/usr/local/bin:/usr/bin:/bin + ## core.mxenv # Primary Python interpreter to use. It is used to create the @@ -141,7 +160,7 @@ TEST_REQUIREMENTS?=zope.testrunner # Additional make targets the test target depends on. # No default value. -TEST_DEPENDENCY_TARGETS?= +TEST_DEPENDENCY_TARGETS?=openldap ## qa.coverage @@ -228,6 +247,50 @@ $(SENTINEL): $(firstword $(MAKEFILE_LIST)) @mkdir -p $(SENTINEL_FOLDER) @echo "Sentinels for the Makefile process." > $(SENTINEL) +############################################################################## +# openldap +############################################################################## + +# case `system.dependencies` domain is included +SYSTEM_DEPENDENCIES+=libdb-dev libsasl2-dev + +OPENLDAP_TARGET:=$(SENTINEL_FOLDER)/openldap.sentinel +$(OPENLDAP_TARGET): $(SENTINEL) + @echo "Building openldap server in '$(OPENLDAP_DIR)'" + @test -d $(OPENLDAP_DIR) || curl -o openldap-$(OPENLDAP_VERSION).tgz \ + $(OPENLDAP_URL)/openldap-$(OPENLDAP_VERSION).tgz + @test -d $(OPENLDAP_DIR) || tar xf openldap-$(OPENLDAP_VERSION).tgz + @test -d $(OPENLDAP_DIR) || rm openldap-$(OPENLDAP_VERSION).tgz + @test -d $(OPENLDAP_DIR) || mv openldap-$(OPENLDAP_VERSION) $(OPENLDAP_DIR) + @env -i -C $(OPENLDAP_DIR) $(OPENLDAP_ENV) bash -c \ + './configure \ + --with-tls \ + --enable-slapd=yes \ + --enable-overlays \ + --prefix=$(OPENLDAP_DIR) \ + && make depend \ + && make -j4 \ + && make install' + @touch $(OPENLDAP_TARGET) + +.PHONY: openldap +openldap: $(OPENLDAP_TARGET) + +.PHONY: openldap-dirty +openldap-dirty: + @test -d $(OPENLDAP_DIR) \ + && env -i -C $(OPENLDAP_DIR) $(OPENLDAP_ENV) bash -c 'make clean' + @rm -f $(OPENLDAP_TARGET) + +.PHONY: openldap-clean +openldap-clean: + @rm -f $(OPENLDAP_TARGET) + @rm -rf $(OPENLDAP_DIR) + +INSTALL_TARGETS+=openldap +DIRTY_TARGETS+=openldap-dirty +CLEAN_TARGETS+=openldap-clean + ############################################################################## # mxenv ############################################################################## From cba6f0501c042363b6a9fec3bf429beb545c7885 Mon Sep 17 00:00:00 2001 From: "Jens W. Klein" Date: Mon, 23 Jun 2025 22:54:53 +0200 Subject: [PATCH 46/74] stuff for openldap --- .github/workflows/tests.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index db8215b..c80eee4 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -27,7 +27,7 @@ jobs: run: | curl -LsSf https://astral.sh/uv/install.sh | sh sudo apt-get update -y - sudo apt-get install -y libsasl2-dev libssl-dev libdb-dev libldap2-dev slapd + sudo apt-get install -y build-essential libsasl2-dev libssl-dev libdb-dev libldap2-dev - name: Setup Plone ${{ matrix.plone }} with Python ${{ matrix.python }} id: setup From 462cb357e6bf0e2847a11ab98ac185711a1f894a Mon Sep 17 00:00:00 2001 From: "Jens W. Klein" Date: Mon, 23 Jun 2025 23:00:15 +0200 Subject: [PATCH 47/74] remove deprecationwarning --- src/pas/plugins/ldap/plonecontrolpanel/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pas/plugins/ldap/plonecontrolpanel/__init__.py b/src/pas/plugins/ldap/plonecontrolpanel/__init__.py index 698012e..cbcc105 100644 --- a/src/pas/plugins/ldap/plonecontrolpanel/__init__.py +++ b/src/pas/plugins/ldap/plonecontrolpanel/__init__.py @@ -1,4 +1,4 @@ -from Products.CMFPlone.interfaces.installable import INonInstallable +from plone.base.interfaces import INonInstallable from zope.interface import implementer From 2d4e95538b753c123b7f71a8207e4895daadbca8 Mon Sep 17 00:00:00 2001 From: "Jens W. Klein" Date: Tue, 24 Jun 2025 00:51:41 +0200 Subject: [PATCH 48/74] mxmake update --- Makefile | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/Makefile b/Makefile index 9de22de..8771d2c 100644 --- a/Makefile +++ b/Makefile @@ -67,6 +67,9 @@ OPENLDAP_ENV?=PATH=/usr/local/bin:/usr/bin:/bin # Primary Python interpreter to use. It is used to create the # virtual environment if `VENV_ENABLED` and `VENV_CREATE` are set to `true`. +# If global `uv` is used, this value is passed as `--python VALUE` to the venv creation. +# uv then downloads the Python interpreter if it is not available. +# for more on this feature read the [uv python documentation](https://docs.astral.sh/uv/concepts/python-versions/) # Default: python3 PRIMARY_PYTHON?=3.12 @@ -320,8 +323,12 @@ endif MXENV_TARGET:=$(SENTINEL_FOLDER)/mxenv.sentinel $(MXENV_TARGET): $(SENTINEL) +ifneq ("$(PYTHON_PACKAGE_INSTALLER)$(MXENV_UV_GLOBAL)","uvfalse") @$(PRIMARY_PYTHON) -c "import sys; vi = sys.version_info; sys.exit(1 if (int(vi[0]), int(vi[1])) >= tuple(map(int, '$(PYTHON_MIN_VERSION)'.split('.'))) else 0)" \ && echo "Need Python >= $(PYTHON_MIN_VERSION)" && exit 1 || : +else + @echo "Use Python $(PYTHON_MIN_VERSION) over uv" +endif @[[ "$(VENV_ENABLED)" == "true" && "$(VENV_FOLDER)" == "" ]] \ && echo "VENV_FOLDER must be configured if VENV_ENABLED is true" && exit 1 || : @[[ "$(VENV_ENABLED)$(PYTHON_PACKAGE_INSTALLER)" == "falseuv" ]] \ From 196ea43cc1feeab3096dbc2019cb940fdfebf388 Mon Sep 17 00:00:00 2001 From: "Jens W. Klein" Date: Tue, 24 Jun 2025 01:04:05 +0200 Subject: [PATCH 49/74] set SLAPD_BIN for test runner --- mx.ini | 1 + 1 file changed, 1 insertion(+) diff --git a/mx.ini b/mx.ini index c39694f..b1144c4 100644 --- a/mx.ini +++ b/mx.ini @@ -16,6 +16,7 @@ mxmake-templates = [mxmake-env] # VAR = value testpaths = tests +SLAPD_BIN = ./openldap/sbin/slapd [mxmake-run-tests] environment = env From 311995694eb2678cfca974cf88cdb0457b3824e9 Mon Sep 17 00:00:00 2001 From: "Jens W. Klein" Date: Tue, 24 Jun 2025 01:23:27 +0200 Subject: [PATCH 50/74] fix LDAP build and suage --- Makefile | 4 ++-- mx.ini | 4 +++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/Makefile b/Makefile index 8771d2c..730d85d 100644 --- a/Makefile +++ b/Makefile @@ -49,7 +49,7 @@ EXTRA_PATH?= # OpenLDAP version to download # Default: 2.4.59 -OPENLDAP_VERSION?=2.4.59 +OPENLDAP_VERSION?=2.6.9 # OpenLDAP base download URL # Default: https://www.openldap.org/software/download/OpenLDAP/openldap-release/ @@ -263,13 +263,13 @@ $(OPENLDAP_TARGET): $(SENTINEL) @test -d $(OPENLDAP_DIR) || curl -o openldap-$(OPENLDAP_VERSION).tgz \ $(OPENLDAP_URL)/openldap-$(OPENLDAP_VERSION).tgz @test -d $(OPENLDAP_DIR) || tar xf openldap-$(OPENLDAP_VERSION).tgz - @test -d $(OPENLDAP_DIR) || rm openldap-$(OPENLDAP_VERSION).tgz @test -d $(OPENLDAP_DIR) || mv openldap-$(OPENLDAP_VERSION) $(OPENLDAP_DIR) @env -i -C $(OPENLDAP_DIR) $(OPENLDAP_ENV) bash -c \ './configure \ --with-tls \ --enable-slapd=yes \ --enable-overlays \ + --without-systemd \ --prefix=$(OPENLDAP_DIR) \ && make depend \ && make -j4 \ diff --git a/mx.ini b/mx.ini index b1144c4..fba898b 100644 --- a/mx.ini +++ b/mx.ini @@ -16,7 +16,9 @@ mxmake-templates = [mxmake-env] # VAR = value testpaths = tests -SLAPD_BIN = ./openldap/sbin/slapd +SLAPD_BIN = ./openldap/libexec/slapd +LDAP_ADD_BIN = ./openldap/bin/ldapadd +LDAP_DELETE_BIN = ./openldap/bin/ldapdelete [mxmake-run-tests] environment = env From 0988299386d67f696a88c37aed91eeda0a5d9a48 Mon Sep 17 00:00:00 2001 From: "Jens W. Klein" Date: Tue, 24 Jun 2025 08:13:27 +0200 Subject: [PATCH 51/74] add changelog --- CHANGES.rst | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/CHANGES.rst b/CHANGES.rst index ca511d4..715e307 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,7 +5,26 @@ History 2.0.0 (unreleased) ------------------ -- todo +- Target Plone 6.x only. Drop support for Python < 3.10. + [jensens] + +- Updated package metadata to use `pyproject.toml` and drop `setuptools`. + [jensens] + +- Use mxmake exclusively for development and testing. + [jensens] + +- Refactor test setup to use pytest as runner. + [jensens] + +- Increase PAS_PLUGINS_LDAP_OPT_TIMEOUT t + +- Add i18n support and Spanish translation. + [macagua] + +- Remove five.globalrequest dependency. + [cillianderoiste] + 1.9.0 (unreleased) ------------------ From cdee0790146e39121bf060e0c5ced672795d7d48 Mon Sep 17 00:00:00 2001 From: "Jens W. Klein" Date: Tue, 24 Jun 2025 10:22:57 +0200 Subject: [PATCH 52/74] make format --- src/pas/plugins/ldap/__init__.py | 2 ++ src/pas/plugins/ldap/cache.py | 2 +- src/pas/plugins/ldap/locales/__main__.py | 4 ++-- src/pas/plugins/ldap/plonecontrolpanel/__init__.py | 1 - src/pas/plugins/ldap/plonecontrolpanel/cache.py | 2 +- src/pas/plugins/ldap/plonecontrolpanel/controlpanel.py | 2 +- src/pas/plugins/ldap/plonecontrolpanel/exportimport.py | 2 +- .../plonecontrolpanel/profiles/default/controlpanel.xml | 3 ++- .../plonecontrolpanel/profiles/default/ldapsettings.xml | 2 +- .../ldap/plonecontrolpanel/profiles/default/metadata.xml | 2 +- .../ldap/plonecontrolpanel/profiles/default/registry.xml | 5 +++-- src/pas/plugins/ldap/plugin.py | 8 ++------ src/pas/plugins/ldap/profile/metadata.xml | 2 +- src/pas/plugins/ldap/properties.py | 5 +++-- 14 files changed, 21 insertions(+), 21 deletions(-) diff --git a/src/pas/plugins/ldap/__init__.py b/src/pas/plugins/ldap/__init__.py index 874b91d..fdaa9f2 100644 --- a/src/pas/plugins/ldap/__init__.py +++ b/src/pas/plugins/ldap/__init__.py @@ -1,4 +1,5 @@ """Init and utils.""" + from . import monkey # noqa from .plugin import LDAPPlugin from .plugin import manage_addLDAPPlugin @@ -11,6 +12,7 @@ import logging import os + PACKAGE_NAME = "pas.plugins.ldap" _ = MessageFactory(PACKAGE_NAME) diff --git a/src/pas/plugins/ldap/cache.py b/src/pas/plugins/ldap/cache.py index f90f708..bd1e5ae 100644 --- a/src/pas/plugins/ldap/cache.py +++ b/src/pas/plugins/ldap/cache.py @@ -1,4 +1,3 @@ -import re from .interfaces import ICacheSettingsRecordProvider from .interfaces import ILDAPPlugin from .interfaces import IPluginCacheHandler @@ -11,6 +10,7 @@ from zope.globalrequest import getRequest from zope.interface import implementer +import re import threading import time diff --git a/src/pas/plugins/ldap/locales/__main__.py b/src/pas/plugins/ldap/locales/__main__.py index 6ca0d07..c4b7125 100644 --- a/src/pas/plugins/ldap/locales/__main__.py +++ b/src/pas/plugins/ldap/locales/__main__.py @@ -26,8 +26,8 @@ def i18n_script_setup(): """Setup the i18n scripts""" - cmd_i18ndude = ("uvx i18ndude") - cmd_lingua = ("uvx lingua") + cmd_i18ndude = "uvx i18ndude" + cmd_lingua = "uvx lingua" subprocess.call(cmd_i18ndude, shell=True) # noQA: S602 subprocess.call(cmd_lingua, shell=True) # noQA: S602 diff --git a/src/pas/plugins/ldap/plonecontrolpanel/__init__.py b/src/pas/plugins/ldap/plonecontrolpanel/__init__.py index cbcc105..28d4f7f 100644 --- a/src/pas/plugins/ldap/plonecontrolpanel/__init__.py +++ b/src/pas/plugins/ldap/plonecontrolpanel/__init__.py @@ -16,4 +16,3 @@ def getNonInstallableProducts(self): def getNonInstallableProfiles(self): return self._hidden - diff --git a/src/pas/plugins/ldap/plonecontrolpanel/cache.py b/src/pas/plugins/ldap/plonecontrolpanel/cache.py index aafeb90..f752ed0 100644 --- a/src/pas/plugins/ldap/plonecontrolpanel/cache.py +++ b/src/pas/plugins/ldap/plonecontrolpanel/cache.py @@ -1,6 +1,6 @@ from ..interfaces import ICacheSettingsRecordProvider -from persistent import Persistent from pas.plugins.ldap import _ +from persistent import Persistent from plone.registry import field from plone.registry import Record from plone.registry.interfaces import IRegistry diff --git a/src/pas/plugins/ldap/plonecontrolpanel/controlpanel.py b/src/pas/plugins/ldap/plonecontrolpanel/controlpanel.py index 51acb01..c947f4a 100644 --- a/src/pas/plugins/ldap/plonecontrolpanel/controlpanel.py +++ b/src/pas/plugins/ldap/plonecontrolpanel/controlpanel.py @@ -1,8 +1,8 @@ from ..properties import BasePropertiesForm +from pas.plugins.ldap import _ from Products.CMFCore.interfaces import ISiteRoot from Products.CMFPlone.resources import add_bundle_on_request from Products.statusmessages.interfaces import IStatusMessage -from pas.plugins.ldap import _ from zope.component import getUtility diff --git a/src/pas/plugins/ldap/plonecontrolpanel/exportimport.py b/src/pas/plugins/ldap/plonecontrolpanel/exportimport.py index eb88069..cb398fe 100644 --- a/src/pas/plugins/ldap/plonecontrolpanel/exportimport.py +++ b/src/pas/plugins/ldap/plonecontrolpanel/exportimport.py @@ -1,7 +1,7 @@ from BTrees.OOBTree import OOBTree +from pas.plugins.ldap import PACKAGE_NAME from Products.GenericSetup.interfaces import IBody from Products.GenericSetup.utils import XMLAdapterBase -from pas.plugins.ldap import PACKAGE_NAME from zope.component import queryMultiAdapter from zope.interface import implementer diff --git a/src/pas/plugins/ldap/plonecontrolpanel/profiles/default/controlpanel.xml b/src/pas/plugins/ldap/plonecontrolpanel/profiles/default/controlpanel.xml index d47af62..b6b493c 100644 --- a/src/pas/plugins/ldap/plonecontrolpanel/profiles/default/controlpanel.xml +++ b/src/pas/plugins/ldap/plonecontrolpanel/profiles/default/controlpanel.xml @@ -2,7 +2,8 @@ + i18n:domain="pas.plugins.ldap" +> + + 1 diff --git a/src/pas/plugins/ldap/plonecontrolpanel/profiles/default/registry.xml b/src/pas/plugins/ldap/plonecontrolpanel/profiles/default/registry.xml index 563fe75..2ad9dda 100644 --- a/src/pas/plugins/ldap/plonecontrolpanel/profiles/default/registry.xml +++ b/src/pas/plugins/ldap/plonecontrolpanel/profiles/default/registry.xml @@ -1,6 +1,7 @@ - + + i18n:domain="pas.plugins.ldap" +> diff --git a/src/pas/plugins/ldap/plugin.py b/src/pas/plugins/ldap/plugin.py index 790783f..bc4488b 100644 --- a/src/pas/plugins/ldap/plugin.py +++ b/src/pas/plugins/ldap/plugin.py @@ -36,12 +36,8 @@ LDAP_LONG_RUNNING_LOG_THRESHOLD = float( os.environ.get("PAS_PLUGINS_LDAP_LONG_RUNNING_LOG_THRESHOLD", 5.0) ) -OPT_NETWORK_TIMEOUT = float( - os.environ.get("PAS_PLUGINS_LDAP_OPT_NETWORK_TIMEOUT", 1.0) -) -OPT_TIMEOUT = float( - os.environ.get("PAS_PLUGINS_LDAP_OPT_TIMEOUT", 30.0) -) +OPT_NETWORK_TIMEOUT = float(os.environ.get("PAS_PLUGINS_LDAP_OPT_NETWORK_TIMEOUT", 1.0)) +OPT_TIMEOUT = float(os.environ.get("PAS_PLUGINS_LDAP_OPT_TIMEOUT", 30.0)) # initial connection timeout ldap.set_option(ldap.OPT_NETWORK_TIMEOUT, OPT_NETWORK_TIMEOUT) diff --git a/src/pas/plugins/ldap/profile/metadata.xml b/src/pas/plugins/ldap/profile/metadata.xml index d02ea5c..58133d6 100644 --- a/src/pas/plugins/ldap/profile/metadata.xml +++ b/src/pas/plugins/ldap/profile/metadata.xml @@ -1,4 +1,4 @@ - + 2 diff --git a/src/pas/plugins/ldap/properties.py b/src/pas/plugins/ldap/properties.py index 0f51b5a..4e3cfb3 100644 --- a/src/pas/plugins/ldap/properties.py +++ b/src/pas/plugins/ldap/properties.py @@ -11,7 +11,8 @@ from node.ext.ldap.scope import SUBTREE from node.ext.ldap.ugm import Ugm from odict import odict -from pas.plugins.ldap import _, logger +from pas.plugins.ldap import _ +from pas.plugins.ldap import logger from Products.Five import BrowserView from yafowil import loader # noqa: F401 from yafowil.base import ExtractionError @@ -205,7 +206,7 @@ def connection_test(self): return False, msg + str(e) try: ugm = Ugm("test", props=props, ucfg=users, gcfg=groups) - ugm.users.authenticate('foo', 'bar') + ugm.users.authenticate("foo", "bar") except ldap.SERVER_DOWN: return False, _("Server Down") except ldap.LDAPError as e: From 1314f6070f849551e47d44d0c87002fd9ab1211d Mon Sep 17 00:00:00 2001 From: "Jens W. Klein" Date: Mon, 27 Apr 2026 11:07:56 +0200 Subject: [PATCH 53/74] fix CI: regen Makefile via mxmake, pass matrix python, drop deprecated load_zcml - Regenerate Makefile with `uvx mxmake update` (mxmake 2.2.0). New Makefile fixes the mxdev offline-mode error in CI: SOURCES_TARGET now runs online to populate `.mxdev_cache` before FILES_TARGET reads it offline. - Workflow: pass `PRIMARY_PYTHON=\${{ matrix.python }}` to make so the matrix Python is actually used. The previous `VENV=off` had no effect (the variable is `VENV_ENABLED`), so uv created the venv with system Python 3.12 regardless of the matrix value. - instance.yaml: replace deprecated `load_zcml: {package_includes: [...]}` with `zcml_package_includes: [...]` (cookiecutter-zope-instance >=2.0). - .gitignore: ignore generated `/.mxdev_cache/`. --- .github/workflows/tests.yaml | 4 +- .gitignore | 1 + Makefile | 109 +++++++++++++++++++++++++++-------- instance.yaml | 2 +- 4 files changed, 89 insertions(+), 27 deletions(-) diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index c80eee4..ce39e4a 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -39,8 +39,8 @@ jobs: - name: Install package run: | sed -i "s#\(-c https://dist.plone.org/release/\)[^/]\+\(/constraints.txt\)#\1${{ matrix.plone }}\2#" requirements.txt - make VENV=off install + make PRIMARY_PYTHON=${{ matrix.python }} install - name: Run tests run: | - make VENV=off test + make PRIMARY_PYTHON=${{ matrix.python }} test diff --git a/.gitignore b/.gitignore index 78c3c45..7a16b2f 100644 --- a/.gitignore +++ b/.gitignore @@ -9,6 +9,7 @@ __pycache__ *.mo *.pyc *.pyo +/.mxdev_cache/ /.mxmake/ /.project /.pydevproject diff --git a/Makefile b/Makefile index 730d85d..0138dd5 100644 --- a/Makefile +++ b/Makefile @@ -45,6 +45,13 @@ INCLUDE_MAKEFILE?=include.mk # No default value. EXTRA_PATH?= +# Path to Python project relative to Makefile (repository root). +# Leave empty if Python project is in the same directory as Makefile. +# For monorepo setups, set to subdirectory name (e.g., `backend`). +# Future-proofed for multi-language monorepos (e.g., PROJECT_PATH_NODEJS). +# No default value. +PROJECT_PATH_PYTHON?= + ## ldap.openldap # OpenLDAP version to download @@ -71,25 +78,26 @@ OPENLDAP_ENV?=PATH=/usr/local/bin:/usr/bin:/bin # uv then downloads the Python interpreter if it is not available. # for more on this feature read the [uv python documentation](https://docs.astral.sh/uv/concepts/python-versions/) # Default: python3 -PRIMARY_PYTHON?=3.12 +PRIMARY_PYTHON?=3.14 # Minimum required Python version. -# Default: 3.9 +# Default: 3.10 PYTHON_MIN_VERSION?=3.10 # Install packages using the given package installer method. -# Supported are `pip` and `uv`. If uv is used, its global availability is -# checked. Otherwise, it is installed, either in the virtual environment or -# using the `PRIMARY_PYTHON`, dependent on the `VENV_ENABLED` setting. If -# `VENV_ENABLED` and uv is selected, uv is used to create the virtual -# environment. +# Supported are `pip` and `uv`. When `uv` is selected, a global installation +# is auto-detected and used if available. Otherwise, uv is installed in the +# virtual environment or using `PRIMARY_PYTHON`, depending on the +# `VENV_ENABLED` setting. # Default: pip PYTHON_PACKAGE_INSTALLER?=uv -# Flag whether to use a global installed 'uv' or install -# it in the virtual environment. -# Default: false -MXENV_UV_GLOBAL?=true +# Python version for UV to install/use when creating virtual +# environments with global UV. Passed to `uv venv -p VALUE`. Supports version +# specs like `3.11`, `3.14`, `cpython@3.14`. Defaults to PRIMARY_PYTHON value +# for backward compatibility. +# Default: $(PRIMARY_PYTHON) +UV_PYTHON?=$(PRIMARY_PYTHON) # Flag whether to use virtual environment. If `false`, the # interpreter according to `PRIMARY_PYTHON` found in `PATH` is used. @@ -230,6 +238,9 @@ FORMAT_TARGETS?= export PATH:=$(if $(EXTRA_PATH),$(EXTRA_PATH):,)$(PATH) +# Helper variable: adds trailing slash to PROJECT_PATH_PYTHON only if non-empty +PYTHON_PROJECT_PREFIX=$(if $(PROJECT_PATH_PYTHON),$(PROJECT_PATH_PYTHON)/,) + # Defensive settings for make: https://tech.davis-hansson.com/p/make/ SHELL:=bash .ONESHELL: @@ -263,13 +274,13 @@ $(OPENLDAP_TARGET): $(SENTINEL) @test -d $(OPENLDAP_DIR) || curl -o openldap-$(OPENLDAP_VERSION).tgz \ $(OPENLDAP_URL)/openldap-$(OPENLDAP_VERSION).tgz @test -d $(OPENLDAP_DIR) || tar xf openldap-$(OPENLDAP_VERSION).tgz + @test -d $(OPENLDAP_DIR) || rm openldap-$(OPENLDAP_VERSION).tgz @test -d $(OPENLDAP_DIR) || mv openldap-$(OPENLDAP_VERSION) $(OPENLDAP_DIR) @env -i -C $(OPENLDAP_DIR) $(OPENLDAP_ENV) bash -c \ './configure \ --with-tls \ --enable-slapd=yes \ --enable-overlays \ - --without-systemd \ --prefix=$(OPENLDAP_DIR) \ && make depend \ && make -j4 \ @@ -314,30 +325,61 @@ else MXENV_PYTHON=$(PRIMARY_PYTHON) endif -# Determine the package installer +# Determine the package installer with non-interactive flags ifeq ("$(PYTHON_PACKAGE_INSTALLER)","uv") -PYTHON_PACKAGE_COMMAND=uv pip +PYTHON_PACKAGE_COMMAND=uv pip --no-progress else PYTHON_PACKAGE_COMMAND=$(MXENV_PYTHON) -m pip endif +# Auto-detect global uv availability (simple existence check) +ifeq ("$(PYTHON_PACKAGE_INSTALLER)","uv") +UV_AVAILABLE:=$(shell command -v uv >/dev/null 2>&1 && echo "true" || echo "false") +else +UV_AVAILABLE:=false +endif + +# Determine installation strategy +# depending on the PYTHON_PACKAGE_INSTALLER and UV_AVAILABLE +# - both vars can be false or +# - one of them can be true, +# - but never boths. +USE_GLOBAL_UV:=$(shell [[ "$(PYTHON_PACKAGE_INSTALLER)" == "uv" && "$(UV_AVAILABLE)" == "true" ]] && echo "true" || echo "false") +USE_LOCAL_UV:=$(shell [[ "$(PYTHON_PACKAGE_INSTALLER)" == "uv" && "$(UV_AVAILABLE)" == "false" ]] && echo "true" || echo "false") + +# Check if global UV is outdated (non-blocking warning) +ifeq ("$(USE_GLOBAL_UV)","true") +UV_OUTDATED:=$(shell uv self update --dry-run 2>&1 | grep -q "Would update" && echo "true" || echo "false") +else +UV_OUTDATED:=false +endif + MXENV_TARGET:=$(SENTINEL_FOLDER)/mxenv.sentinel $(MXENV_TARGET): $(SENTINEL) -ifneq ("$(PYTHON_PACKAGE_INSTALLER)$(MXENV_UV_GLOBAL)","uvfalse") + # Validation: Check Python version if not using global uv +ifneq ("$(USE_GLOBAL_UV)","true") @$(PRIMARY_PYTHON) -c "import sys; vi = sys.version_info; sys.exit(1 if (int(vi[0]), int(vi[1])) >= tuple(map(int, '$(PYTHON_MIN_VERSION)'.split('.'))) else 0)" \ && echo "Need Python >= $(PYTHON_MIN_VERSION)" && exit 1 || : else - @echo "Use Python $(PYTHON_MIN_VERSION) over uv" + @echo "Using global uv for Python $(UV_PYTHON)" endif + # Validation: Check VENV_FOLDER is set if venv enabled @[[ "$(VENV_ENABLED)" == "true" && "$(VENV_FOLDER)" == "" ]] \ && echo "VENV_FOLDER must be configured if VENV_ENABLED is true" && exit 1 || : - @[[ "$(VENV_ENABLED)$(PYTHON_PACKAGE_INSTALLER)" == "falseuv" ]] \ + # Validation: Check uv not used with system Python + @[[ "$(VENV_ENABLED)" == "false" && "$(PYTHON_PACKAGE_INSTALLER)" == "uv" ]] \ && echo "Package installer uv does not work with a global Python interpreter." && exit 1 || : + # Warning: Notify if global UV is outdated +ifeq ("$(UV_OUTDATED)","true") + @echo "WARNING: A newer version of uv is available. Run 'uv self update' to upgrade." +endif + + # Create virtual environment ifeq ("$(VENV_ENABLED)", "true") ifeq ("$(VENV_CREATE)", "true") -ifeq ("$(PYTHON_PACKAGE_INSTALLER)$(MXENV_UV_GLOBAL)","uvtrue") - @echo "Setup Python Virtual Environment using package 'uv' at '$(VENV_FOLDER)'" - @uv venv -p $(PRIMARY_PYTHON) --seed $(VENV_FOLDER) +ifeq ("$(USE_GLOBAL_UV)","true") + @echo "Setup Python Virtual Environment using global uv at '$(VENV_FOLDER)'" + @uv venv --allow-existing --no-progress -p $(UV_PYTHON) --seed $(VENV_FOLDER) else @echo "Setup Python Virtual Environment using module 'venv' at '$(VENV_FOLDER)'" @$(PRIMARY_PYTHON) -m venv $(VENV_FOLDER) @@ -347,10 +389,14 @@ endif else @echo "Using system Python interpreter" endif -ifeq ("$(PYTHON_PACKAGE_INSTALLER)$(MXENV_UV_GLOBAL)","uvfalse") - @echo "Install uv" + + # Install uv locally if needed +ifeq ("$(USE_LOCAL_UV)","true") + @echo "Install uv in virtual environment" @$(MXENV_PYTHON) -m pip install uv endif + + # Install/upgrade core packages @$(PYTHON_PACKAGE_COMMAND) install -U pip setuptools wheel @echo "Install/Update MXStack Python packages" @$(PYTHON_PACKAGE_COMMAND) install -U $(MXDEV) $(MXMAKE) @@ -382,6 +428,11 @@ CLEAN_TARGETS+=mxenv-clean # zpretty ############################################################################## +# Adjust ZPRETTY_SRC to respect PROJECT_PATH_PYTHON if still at default +ifeq ($(ZPRETTY_SRC),src) +ZPRETTY_SRC:=$(PYTHON_PROJECT_PREFIX)src +endif + ZPRETTY_TARGET:=$(SENTINEL_FOLDER)/zpretty.sentinel $(ZPRETTY_TARGET): $(MXENV_TARGET) @echo "Install zpretty" @@ -416,6 +467,11 @@ CLEAN_TARGETS+=zpretty-clean # isort ############################################################################## +# Adjust ISORT_SRC to respect PROJECT_PATH_PYTHON if still at default +ifeq ($(ISORT_SRC),src) +ISORT_SRC:=$(PYTHON_PROJECT_PREFIX)src +endif + ISORT_TARGET:=$(SENTINEL_FOLDER)/isort.sentinel $(ISORT_TARGET): $(MXENV_TARGET) @echo "Install isort" @@ -450,6 +506,11 @@ CLEAN_TARGETS+=isort-clean # black ############################################################################## +# Adjust BLACK_SRC to respect PROJECT_PATH_PYTHON if still at default +ifeq ($(BLACK_SRC),src) +BLACK_SRC:=$(PYTHON_PROJECT_PREFIX)src +endif + BLACK_TARGET:=$(SENTINEL_FOLDER)/black.sentinel $(BLACK_TARGET): $(MXENV_TARGET) @echo "Install Black" @@ -487,7 +548,7 @@ CLEAN_TARGETS+=black-clean SOURCES_TARGET:=$(SENTINEL_FOLDER)/sources.sentinel $(SOURCES_TARGET): $(PROJECT_CONFIG) $(MXENV_TARGET) @echo "Checkout project sources" - @mxdev -o -c $(PROJECT_CONFIG) + @mxdev -f -c $(PROJECT_CONFIG) @touch $(SOURCES_TARGET) .PHONY: sources @@ -532,7 +593,7 @@ else @echo "[settings]" > $(PROJECT_CONFIG) endif -LOCAL_PACKAGE_FILES:=$(wildcard pyproject.toml setup.cfg setup.py requirements.txt constraints.txt) +LOCAL_PACKAGE_FILES:=$(wildcard $(PYTHON_PROJECT_PREFIX)pyproject.toml $(PYTHON_PROJECT_PREFIX)setup.cfg $(PYTHON_PROJECT_PREFIX)setup.py $(PYTHON_PROJECT_PREFIX)requirements.txt $(PYTHON_PROJECT_PREFIX)constraints.txt) FILES_TARGET:=requirements-mxdev.txt $(FILES_TARGET): $(PROJECT_CONFIG) $(MXENV_TARGET) $(SOURCES_TARGET) $(LOCAL_PACKAGE_FILES) diff --git a/instance.yaml b/instance.yaml index 448b03e..e716937 100644 --- a/instance.yaml +++ b/instance.yaml @@ -13,5 +13,5 @@ default_context: initial_user_name: admin initial_user_password: admin debug_mode: on - load_zcml: {package_includes: ['pas.plugins.ldap']} + zcml_package_includes: ['pas.plugins.ldap'] db_storage: direct From ef37599460e9d2c141b720076b6f80f14e21f092 Mon Sep 17 00:00:00 2001 From: "Jens W. Klein" Date: Mon, 27 Apr 2026 11:19:51 +0200 Subject: [PATCH 54/74] fix tests: hardcode roles default in test fixture `ldaptesting.props` (upstream node.ext.ldap LDAPProps) has no `roles` attribute, so reading it raised AttributeError. Inside the `_ldap_props` property getter this gets re-wrapped by Python as `AttributeError: _ldap_props`, which cascaded to the 25 test failures. Hardcode the test fixture to `["Member"]` (the same value as `DEFAULTS["server.roles"]`). --- tests/testing.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/testing.py b/tests/testing.py index d0f92ca..bf92924 100644 --- a/tests/testing.py +++ b/tests/testing.py @@ -34,7 +34,7 @@ def ldapprops(context): props.uri = ldaptesting.props.uri props.user = ldaptesting.props.user - props.roles = ldaptesting.props.roles + props.roles = ["Member"] props.password = ldaptesting.props.password props.cache = ldaptesting.props.cache props.page_size = ldaptesting.props.page_size From b37a8ec79201e2bb5288e0d47185db68d2525e11 Mon Sep 17 00:00:00 2001 From: "Leonardo J. Caballero G" Date: Wed, 29 Apr 2026 23:24:43 +0200 Subject: [PATCH 55/74] Updated the README file --- README.rst | 37 +++++++++++++++++++------------------ 1 file changed, 19 insertions(+), 18 deletions(-) diff --git a/README.rst b/README.rst index 2c9ab12..9116089 100644 --- a/README.rst +++ b/README.rst @@ -4,17 +4,17 @@ .. image:: https://coveralls.io/repos/collective/pas.plugins.ldap/badge.svg?branch=master&service=github :target: https://coveralls.io/github/collective/pas.plugins.ldap?branch=master -This is a `LDAP `_ Plugin for the `Zope `_ `Pluggable Authentication Service (PAS) `_. +This is a `LDAP `_ Plugin for the `Zope `_ `Pluggable Authentication Service (PAS) `_. It provides users and/or groups from an LDAP directory. -It works in a plain Zope even if it depends on `PlonePAS `_. +It works in a plain Zope even if it depends on `PlonePAS `_. If `Plone `_ is installed an integration layer with a setup-profile and a plone-controlpanel page is available. -``pas.plugins.ldap`` is **not** releated to the old LDAPUserFolder/ LDAPMultiPlugins and the packages (i.e. PloneLDAP) stacked on top of it in any way. +``pas.plugins.ldap`` is **not** releated to the old `LDAPUserFolder `_/ `LDAPMultiPlugins `_ and the packages (i.e. `PloneLDAP `_) stacked on top of it in any way. -It is based on **node.ext.ldap**, an almost framework independent LDAP stack. +It is based on **`node.ext.ldap `_**, an almost framework independent LDAP stack. For now users and groups can't be added or deleted. Properties on both are read/write. @@ -53,9 +53,9 @@ Add to the instance section of your buildout: ... pas.plugins.ldap -Run buildout. Restart Zope. +Run ``buildout``. Restart Zope. -Browse to your ``acl_users`` folder and add an LDAP-Plugin. +Browse to your ``acl_users`` folder and add an ``LDAP-Plugin``. Configure it using the settings form and activate its features with the ``activate`` tab. @@ -63,7 +63,7 @@ Configure it using the settings form and activate its features with the ``activa Plone ----- -Add to the instance section of your buildout: +Add to the instance section of your ``buildout``: .. code-block:: ini @@ -71,11 +71,11 @@ Add to the instance section of your buildout: ... pas.plugins.ldap -Run buildout. Restart Plone. +Run ``buildout``. Restart Plone. -Then go to the Plone control-panel, select ``extensions`` and install the LDAP Plugin. +Then go to the Plone control-panel, select ``Addons`` and install the ``LDAP/ Active Directory Support``. -A new LDAP Settings icon appear on the left. Click it and configure the plugin there. +A new ``LDAP Settings`` icon appear on the left. Click it and configure the plugin there. To use an own integration-profile, add to the profiles ``metadata.xml`` file: @@ -89,7 +89,7 @@ To use an own integration-profile, add to the profiles ``metadata.xml`` file: ... Additionally ldap settings can be exported and imported with ``portal_setup``. -You can place the exported ``ldapsettings.xml`` in your integration profile, so it will be imported with your next install again. +You can place the exported ``ldapsettings.xml`` file in your integration profile, so it will be imported with your next install again. **Warning:** @@ -108,12 +108,12 @@ LDAP as an external service might be down, non-responsive or slow. This package logs such events to raise awareness. There are two environment variables to control the logging of LDAP-errors: -PAS_PLUGINS_LDAP_ERROR_LOG_TIMEOUT +``PAS_PLUGINS_LDAP_ERROR_LOG_TIMEOUT`` First LDAP-error is logged, further errors ignored until the given number of seconds have passed. This supresses flooding logs if LDAP is down. Default: 300.0 (time in seconds, float). -PAS_PLUGINS_LDAP_LONG_RUNNING_LOG_THRESHOLD +``PAS_PLUGINS_LDAP_LONG_RUNNING_LOG_THRESHOLD`` Log long running LDAP/PAS operations. If a PAS operation takes longer than he given number of seconds, log it as error. Default: 5 (time in seconds, float). @@ -124,11 +124,11 @@ Timeouts Global LDAP timeouts are set and controlled by two environment variables: -PAS_PLUGINS_LDAP_OPT_NETWORK_TIMEOUT +``PAS_PLUGINS_LDAP_OPT_NETWORK_TIMEOUT`` Connection timeout. Default: 1.0s -PAS_PLUGINS_LDAP_OPT_TIMEOUT +``PAS_PLUGINS_LDAP_OPT_TIMEOUT`` Overall timeout. Default: 30.0s @@ -142,7 +142,7 @@ Caching By **default** the LDAP-queries are **not cached**. -A **must have** for a production environment is having `memcached `_ server configured as LDAP query cache. +A **must have** for a production environment is having `memcached `_ server configured as LDAP query cache. Cache at least for ~6 seconds, so a page load with all its resources is covered also in worst case. @@ -175,7 +175,7 @@ It may work under Windows if ``python-ldap`` is installed properly and recognize This package works fine for several 10000 users or groups, **unless you list users**. This is not that much a problem for small amount of users. -There is room for future optimization in the underlying `node.ext.ldap `_. +There is room for future optimization in the underlying `node.ext.ldap `_. Source Code @@ -185,7 +185,7 @@ If you want to help with the development (improvement, update, bug-fixing, ...) The code is located in the `GitHub Collective `_. -You can clone it or `get access to the GitHub Collective `_ and work directly on the project. +You can clone it or `get access to the GitHub Collective `_ and work directly on the project. Maintainers are Robert Niederreiter, Jens Klein and the `BlueDynamics Alliance `_ developer team. @@ -202,3 +202,4 @@ Contributors - Daniel Widerin - Johannes Raggam - Luca Fabbri +- Leonardo J. Caballero G. From 585d7ecf0c1dcdbb2e27083ad98bc6aa51675773 Mon Sep 17 00:00:00 2001 From: "Leonardo J. Caballero G" Date: Wed, 29 Apr 2026 23:25:46 +0200 Subject: [PATCH 56/74] Added the LICENSE.GPL file --- LICENSE.GPL | 339 ++++++++++++++++++++++++++++++++++++++++++++++++++++ README.rst | 6 + 2 files changed, 345 insertions(+) create mode 100644 LICENSE.GPL diff --git a/LICENSE.GPL b/LICENSE.GPL new file mode 100644 index 0000000..89e08fb --- /dev/null +++ b/LICENSE.GPL @@ -0,0 +1,339 @@ + GNU GENERAL PUBLIC LICENSE + Version 2, June 1991 + + Copyright (C) 1989, 1991 Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +License is intended to guarantee your freedom to share and change free +software--to make sure the software is free for all its users. This +General Public License applies to most of the Free Software +Foundation's software and to any other program whose authors commit to +using it. (Some other Free Software Foundation software is covered by +the GNU Lesser General Public License instead.) You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +this service if you wish), that you receive source code or can get it +if you want it, that you can change the software or use pieces of it +in new free programs; and that you know you can do these things. + + To protect your rights, we need to make restrictions that forbid +anyone to deny you these rights or to ask you to surrender the rights. +These restrictions translate to certain responsibilities for you if you +distribute copies of the software, or if you modify it. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must give the recipients all the rights that +you have. You must make sure that they, too, receive or can get the +source code. And you must show them these terms so they know their +rights. + + We protect your rights with two steps: (1) copyright the software, and +(2) offer you this license which gives you legal permission to copy, +distribute and/or modify the software. + + Also, for each author's protection and ours, we want to make certain +that everyone understands that there is no warranty for this free +software. If the software is modified by someone else and passed on, we +want its recipients to know that what they have is not the original, so +that any problems introduced by others will not reflect on the original +authors' reputations. + + Finally, any free program is threatened constantly by software +patents. We wish to avoid the danger that redistributors of a free +program will individually obtain patent licenses, in effect making the +program proprietary. To prevent this, we have made it clear that any +patent must be licensed for everyone's free use or not licensed at all. + + The precise terms and conditions for copying, distribution and +modification follow. + + GNU GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License applies to any program or other work which contains +a notice placed by the copyright holder saying it may be distributed +under the terms of this General Public License. The "Program", below, +refers to any such program or work, and a "work based on the Program" +means either the Program or any derivative work under copyright law: +that is to say, a work containing the Program or a portion of it, +either verbatim or with modifications and/or translated into another +language. (Hereinafter, translation is included without limitation in +the term "modification".) Each licensee is addressed as "you". + +Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running the Program is not restricted, and the output from the Program +is covered only if its contents constitute a work based on the +Program (independent of having been made by running the Program). +Whether that is true depends on what the Program does. + + 1. You may copy and distribute verbatim copies of the Program's +source code as you receive it, in any medium, provided that you +conspicuously and appropriately publish on each copy an appropriate +copyright notice and disclaimer of warranty; keep intact all the +notices that refer to this License and to the absence of any warranty; +and give any other recipients of the Program a copy of this License +along with the Program. + +You may charge a fee for the physical act of transferring a copy, and +you may at your option offer warranty protection in exchange for a fee. + + 2. You may modify your copy or copies of the Program or any portion +of it, thus forming a work based on the Program, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) You must cause the modified files to carry prominent notices + stating that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in + whole or in part contains or is derived from the Program or any + part thereof, to be licensed as a whole at no charge to all third + parties under the terms of this License. + + c) If the modified program normally reads commands interactively + when run, you must cause it, when started running for such + interactive use in the most ordinary way, to print or display an + announcement including an appropriate copyright notice and a + notice that there is no warranty (or else, saying that you provide + a warranty) and that users may redistribute the program under + these conditions, and telling the user how to view a copy of this + License. (Exception: if the Program itself is interactive but + does not normally print such an announcement, your work based on + the Program is not required to print an announcement.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Program, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Program, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Program. + +In addition, mere aggregation of another work not based on the Program +with the Program (or with a work based on the Program) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may copy and distribute the Program (or a work based on it, +under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you also do one of the following: + + a) Accompany it with the complete corresponding machine-readable + source code, which must be distributed under the terms of Sections + 1 and 2 above on a medium customarily used for software interchange; or, + + b) Accompany it with a written offer, valid for at least three + years, to give any third party, for a charge no more than your + cost of physically performing source distribution, a complete + machine-readable copy of the corresponding source code, to be + distributed under the terms of Sections 1 and 2 above on a medium + customarily used for software interchange; or, + + c) Accompany it with the information you received as to the offer + to distribute corresponding source code. (This alternative is + allowed only for noncommercial distribution and only if you + received the program in object code or executable form with such + an offer, in accord with Subsection b above.) + +The source code for a work means the preferred form of the work for +making modifications to it. For an executable work, complete source +code means all the source code for all modules it contains, plus any +associated interface definition files, plus the scripts used to +control compilation and installation of the executable. However, as a +special exception, the source code distributed need not include +anything that is normally distributed (in either source or binary +form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component +itself accompanies the executable. + +If distribution of executable or object code is made by offering +access to copy from a designated place, then offering equivalent +access to copy the source code from the same place counts as +distribution of the source code, even though third parties are not +compelled to copy the source along with the object code. + + 4. You may not copy, modify, sublicense, or distribute the Program +except as expressly provided under this License. Any attempt +otherwise to copy, modify, sublicense or distribute the Program is +void, and will automatically terminate your rights under this License. +However, parties who have received copies, or rights, from you under +this License will not have their licenses terminated so long as such +parties remain in full compliance. + + 5. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Program or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Program (or any work based on the +Program), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Program or works based on it. + + 6. Each time you redistribute the Program (or any work based on the +Program), the recipient automatically receives a license from the +original licensor to copy, distribute or modify the Program subject to +these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties to +this License. + + 7. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Program at all. For example, if a patent +license would not permit royalty-free redistribution of the Program by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under +any particular circumstance, the balance of the section is intended to +apply and the section as a whole is intended to apply in other +circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system, which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 8. If the distribution and/or use of the Program is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Program under this License +may add an explicit geographical distribution limitation excluding +those countries, so that distribution is permitted only in or among +countries not thus excluded. In such case, this License incorporates +the limitation as if written in the body of this License. + + 9. The Free Software Foundation may publish revised and/or new versions +of the General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + +Each version is given a distinguishing version number. If the Program +specifies a version number of this License which applies to it and "any +later version", you have the option of following the terms and conditions +either of that version or of any later version published by the Free +Software Foundation. If the Program does not specify a version number of +this License, you may choose any version ever published by the Free Software +Foundation. + + 10. If you wish to incorporate parts of the Program into other free +programs whose distribution conditions are different, write to the author +to ask for permission. For software which is copyrighted by the Free +Software Foundation, write to the Free Software Foundation; we sometimes +make exceptions for this. Our decision will be guided by the two goals +of preserving the free status of all derivatives of our free software and +of promoting the sharing and reuse of software generally. + + NO WARRANTY + + 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY +FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN +OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES +PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED +OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS +TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE +PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, +REPAIR OR CORRECTION. + + 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR +REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, +INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING +OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED +TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY +YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER +PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE +POSSIBILITY OF SUCH DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along + with this program; if not, write to the Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +Also add information on how to contact you by electronic and paper mail. + +If the program is interactive, make it output a short notice like this +when it starts in an interactive mode: + + Gnomovision version 69, Copyright (C) year name of author + Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, the commands you use may +be called something other than `show w' and `show c'; they could even be +mouse-clicks or menu items--whatever suits your program. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the program, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the program + `Gnomovision' (which makes passes at compilers) written by James Hacker. + + , 1 April 1989 + Ty Coon, President of Vice + +This General Public License does not permit incorporating your program into +proprietary programs. If your program is a subroutine library, you may +consider it more useful to permit linking proprietary applications with the +library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. diff --git a/README.rst b/README.rst index 9116089..870cedd 100644 --- a/README.rst +++ b/README.rst @@ -203,3 +203,9 @@ Contributors - Johannes Raggam - Luca Fabbri - Leonardo J. Caballero G. + + +License +======= + +The project is licensed under the GPLv2. From 2d52f0c957fd4b7b39e014b44f017de926565b66 Mon Sep 17 00:00:00 2001 From: "Leonardo J. Caballero G" Date: Thu, 30 Apr 2026 09:56:19 +0200 Subject: [PATCH 57/74] Added more improvements about i18n support and updated Spanish translation #131 --- .../en/LC_MESSAGES/pas.plugins.ldap.po | 238 ++++++++++++----- .../es/LC_MESSAGES/pas.plugins.ldap.po | 251 +++++++++++++----- src/pas/plugins/ldap/locales/merge-lingua.pot | 190 +++++++++---- .../plugins/ldap/locales/pas.plugins.ldap.pot | 238 ++++++++++++----- src/pas/plugins/ldap/properties.py | 13 +- src/pas/plugins/ldap/properties.yaml | 32 +-- 6 files changed, 687 insertions(+), 275 deletions(-) diff --git a/src/pas/plugins/ldap/locales/en/LC_MESSAGES/pas.plugins.ldap.po b/src/pas/plugins/ldap/locales/en/LC_MESSAGES/pas.plugins.ldap.po index 8f9691e..fe5b148 100644 --- a/src/pas/plugins/ldap/locales/en/LC_MESSAGES/pas.plugins.ldap.po +++ b/src/pas/plugins/ldap/locales/en/LC_MESSAGES/pas.plugins.ldap.po @@ -1,7 +1,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" -"POT-Creation-Date: 2025-06-21 20:09+0000\n" +"POT-Creation-Date: 2026-04-29 16:56+0000\n" "PO-Revision-Date: YEAR-MO-DA HO:MI +ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -14,20 +14,32 @@ msgstr "" "Preferred-Encodings: utf-8 latin1\n" "Domain: DOMAIN\n" +#: pas/plugins/ldap/properties.py:37 +msgid "BASE" +msgstr "" + #: pas/plugins/ldap/plonecontrolpanel/controlpanel.pt:16 #: pas/plugins/ldap/zmi/manage_plugin.pt:28 msgid "Connection Test" msgstr "" -#: pas/plugins/ldap/properties.py:223 +#: pas/plugins/ldap/properties.py:254 +msgid "Connection, users- and groups-access tested successfully." +msgstr "" + +#: pas/plugins/ldap/properties.py:43 +msgid "Days since Epoch" +msgstr "" + +#: pas/plugins/ldap/properties.py:253 msgid "Exception in Groups; " msgstr "" -#: pas/plugins/ldap/properties.py:216 +#: pas/plugins/ldap/properties.py:246 msgid "Exception in Users; " msgstr "" -#: pas/plugins/ldap/configure.zcml:34 +#: pas/plugins/ldap/configure.zcml:29 msgid "Extension profile for pas.plugins.ldap Zope Base." msgstr "" @@ -43,19 +55,19 @@ msgstr "" msgid "LDAP Inspector" msgstr "" -#: pas/plugins/ldap/configure.zcml:34 +#: pas/plugins/ldap/configure.zcml:29 msgid "LDAP Plugin for PAS - Zope 2 Base Installation" msgstr "" -#: pas/plugins/ldap/plonecontrolpanel/controlpanel.py:31 +#: pas/plugins/ldap/plonecontrolpanel/controlpanel.py:43 msgid "LDAP Settings saved." msgstr "" -#: pas/plugins/ldap/properties.py:220 +#: pas/plugins/ldap/properties.py:250 msgid "LDAP Users ok, but groups not; " msgstr "" -#: pas/plugins/ldap/properties.py:213 +#: pas/plugins/ldap/properties.py:243 msgid "LDAP users; " msgstr "" @@ -79,27 +91,39 @@ msgstr "" msgid "No search base selected" msgstr "" -#: pas/plugins/ldap/properties.py:204 +#: pas/plugins/ldap/properties.py:234 msgid "Non-LDAP error while getting ILDAPGroupsConfig!" msgstr "" -#: pas/plugins/ldap/properties.py:192 +#: pas/plugins/ldap/properties.py:222 msgid "Non-LDAP error while getting ILDAPProps!" msgstr "" -#: pas/plugins/ldap/properties.py:198 +#: pas/plugins/ldap/properties.py:228 msgid "Non-LDAP error while getting ILDAPUsersConfig!" msgstr "" -#: pas/plugins/ldap/properties.py:180 +#: pas/plugins/ldap/properties.py:38 +msgid "ONELEVEL" +msgstr "" + +#: pas/plugins/ldap/properties.py:204 msgid "Password is required for non-anonymous connections." msgstr "" +#: pas/plugins/ldap/properties.py:39 +msgid "SUBTREE" +msgstr "" + +#: pas/plugins/ldap/properties.py:44 +msgid "Seconds since epoch" +msgstr "" + #: pas/plugins/ldap/plonecontrolpanel/inspector.pt:13 msgid "Select search base" msgstr "" -#: pas/plugins/ldap/properties.py:211 +#: pas/plugins/ldap/properties.py:241 msgid "Server Down" msgstr "" @@ -111,11 +135,19 @@ msgstr "" msgid "Settings" msgstr "" -#: pas/plugins/ldap/properties.py:185 +#: pas/plugins/ldap/plonecontrolpanel/configure.zcml:27 +msgid "Uninstall LDAP Plugin for PAS" +msgstr "" + +#: pas/plugins/ldap/plonecontrolpanel/configure.zcml:27 +msgid "Uninstalls the pas.plugins.ldap add-on." +msgstr "" + +#: pas/plugins/ldap/properties.py:209 msgid "User/Password are required if not anonymous." msgstr "" -#: pas/plugins/ldap/properties.py:174 +#: pas/plugins/ldap/properties.py:198 msgid "Username is required for non-anonymous connections." msgstr "" @@ -124,22 +156,22 @@ msgid "Users" msgstr "" #. Default: LDAP attr name -#: pas/plugins/ldap/properties.yaml:199 +#: pas/plugins/ldap/properties.yaml:243 msgid "head_ldap_attr_name" msgstr "" #. Default: LDAP Attribute -#: pas/plugins/ldap/properties.yaml:143 +#: pas/plugins/ldap/properties.yaml:187 msgid "head_ldap_attribute" msgstr "" #. Default: Name on Sheet -#: pas/plugins/ldap/properties.yaml:150 +#: pas/plugins/ldap/properties.yaml:194 msgid "head_name_on_sheet" msgstr "" #. Default: Reserved key -#: pas/plugins/ldap/properties.yaml:142 +#: pas/plugins/ldap/properties.yaml:186 msgid "head_reserved_key" msgstr "" @@ -151,41 +183,80 @@ msgstr "" #. Default: If set on authenticate a failing certificate chain check including #. CA is ignored. -#: pas/plugins/ldap/properties.yaml:56 +#: pas/plugins/ldap/properties.yaml:58 msgid "help_ignore_certificate_check" msgstr "" +#. Default: The timeout period, in seconds, for waiting for a connection to be +#. established with the LDAP server. +#: pas/plugins/ldap/properties.yaml:34 +msgid "help_ldap_connection_timeout_in_seconds" +msgstr "" + +#. Default: The timeout in seconds for an operation such as a search or update +#. to complete. If no timeout is required, use -1 as the value. +#: pas/plugins/ldap/properties.yaml:41 +msgid "help_ldap_operation_timeout_in_seconds" +msgstr "" + #. Default: "Group DNs outside of the groups base DN are ignored, except if #. listed here" -#: pas/plugins/ldap/properties.yaml:113 +#: pas/plugins/ldap/properties.yaml:156 msgid "help_memberOf_external_allowed_group_dns" msgstr "" #. Default: global - same server for all ldap plugins -#: pas/plugins/ldap/properties.yaml:224 +#: pas/plugins/ldap/properties.yaml:268 msgid "help_memcached_server_to_use" msgstr "" #. Default: "One of those is mandatory like as one the following options #. groupOfNames, groupOfUniqueNames, posixGroup, group" -#: pas/plugins/ldap/properties.yaml:180 +#: pas/plugins/ldap/properties.yaml:224 msgid "help_object_classes_for_groups" msgstr "" #. Default: Maximum page size, number of results to query the server at once #. for. -#: pas/plugins/ldap/properties.yaml:62 +#: pas/plugins/ldap/properties.yaml:94 msgid "help_page_size" msgstr "" #. Default: If your LDAP/AD supports it this will use #. LDAP_MATCHING_RULE_IN_CHAIN. By default only AD supports this. -#: pas/plugins/ldap/properties.yaml:108 +#: pas/plugins/ldap/properties.yaml:151 msgid "help_support_recursive_nested_groups" msgstr "" +#. Default: If set, the LDAP server certificate is checked against the CA +#. certificates in folder. +#: pas/plugins/ldap/properties.yaml:76 +msgid "help_tls_cacertdir" +msgstr "" + +#. Default: If set, the LDAP server certificate is checked against the CA +#. certificate file. +#: pas/plugins/ldap/properties.yaml:70 +msgid "help_tls_cacertfile" +msgstr "" + +#. Default: If set, the client certificate is sent to the server. +#: pas/plugins/ldap/properties.yaml:82 +msgid "help_tls_clcertfile" +msgstr "" + +#. Default: If set, the client certificate is sent to the server. +#: pas/plugins/ldap/properties.yaml:88 +msgid "help_tls_clkeyfile" +msgstr "" + +#. Default: If set, the connection is upgraded to TLS. +#: pas/plugins/ldap/properties.yaml:64 +msgid "help_use_tls_connection" +msgstr "" + #. Default: Account expiration unit -#: pas/plugins/ldap/properties.yaml:132 +#: pas/plugins/ldap/properties.yaml:175 msgid "lbl_account_expiration_unit" msgstr "" @@ -195,22 +266,22 @@ msgid "lbl_anonymous_connection" msgstr "" #. Default: Attribute containing expiration Time -#: pas/plugins/ldap/properties.yaml:127 +#: pas/plugins/ldap/properties.yaml:170 msgid "lbl_attribute_containing_expiration_time" msgstr "" #. Default: Cache LDAP queries -#: pas/plugins/ldap/properties.yaml:218 +#: pas/plugins/ldap/properties.yaml:262 msgid "lbl_cache_ldap_queries" msgstr "" #. Default: Cache Settings -#: pas/plugins/ldap/properties.yaml:211 +#: pas/plugins/ldap/properties.yaml:255 msgid "lbl_cache_settings" msgstr "" #. Default: Cache timeout in seconds -#: pas/plugins/ldap/properties.yaml:231 +#: pas/plugins/ldap/properties.yaml:275 msgid "lbl_cache_timeout_in_seconds" msgstr "" @@ -220,47 +291,47 @@ msgid "lbl_connection_uri" msgstr "" #. Default: Group attribute aliases -#: pas/plugins/ldap/properties.yaml:194 +#: pas/plugins/ldap/properties.yaml:238 msgid "lbl_group_attribute_aliases" msgstr "" #. Default: Group Property-Sheet Attributes -#: pas/plugins/ldap/properties.yaml:204 +#: pas/plugins/ldap/properties.yaml:248 msgid "lbl_group_property-sheet_attributes" msgstr "" #. Default: Groups container DN -#: pas/plugins/ldap/properties.yaml:162 +#: pas/plugins/ldap/properties.yaml:206 msgid "lbl_groups_container_dn" msgstr "" #. Default: Groups search query filter -#: pas/plugins/ldap/properties.yaml:174 +#: pas/plugins/ldap/properties.yaml:218 msgid "lbl_groups_search_query_filter" msgstr "" #. Default: Groups search scope -#: pas/plugins/ldap/properties.yaml:168 +#: pas/plugins/ldap/properties.yaml:212 msgid "lbl_groups_search_scope" msgstr "" #. Default: Groups Settings -#: pas/plugins/ldap/properties.yaml:155 +#: pas/plugins/ldap/properties.yaml:199 msgid "lbl_groups_settings" msgstr "" #. Default: Ignore certificate check? -#: pas/plugins/ldap/properties.yaml:55 +#: pas/plugins/ldap/properties.yaml:57 msgid "lbl_ignore_certificate_check" msgstr "" -#. Default: LDAP connection timeout in seconds +#. Default: LDAP connection timeout in seconds. #: pas/plugins/ldap/properties.yaml:33 msgid "lbl_ldap_connection_timeout_in_seconds" msgstr "" -#. Default: LDAP operation timeout in seconds -#: pas/plugins/ldap/properties.yaml:39 +#. Default: LDAP operation timeout in seconds. +#: pas/plugins/ldap/properties.yaml:40 msgid "lbl_ldap_operation_timeout_in_seconds" msgstr "" @@ -270,92 +341,131 @@ msgid "lbl_ldap_server_settings" msgstr "" #. Default: Manager Password -#: pas/plugins/ldap/properties.yaml:50 +#: pas/plugins/ldap/properties.yaml:52 msgid "lbl_manager_password" msgstr "" #. Default: Manager User -#: pas/plugins/ldap/properties.yaml:45 +#: pas/plugins/ldap/properties.yaml:47 msgid "lbl_manager_user" msgstr "" #. Default: memberOf attribute supported? -#: pas/plugins/ldap/properties.yaml:102 +#: pas/plugins/ldap/properties.yaml:135 msgid "lbl_memberOf_attribute_supported" msgstr "" #. Default: memberOf external allowed Group DNs -#: pas/plugins/ldap/properties.yaml:114 +#: pas/plugins/ldap/properties.yaml:157 msgid "lbl_memberOf_external_allowed_group_dns" msgstr "" #. Default: Memcached Server to use -#: pas/plugins/ldap/properties.yaml:223 +#: pas/plugins/ldap/properties.yaml:267 msgid "lbl_memcached_server_to_use" msgstr "" #. Default: Object class -#: pas/plugins/ldap/properties.yaml:94 +#: pas/plugins/ldap/properties.yaml:126 msgid "lbl_object_class" msgstr "" #. Default: Object classes for Groups -#: pas/plugins/ldap/properties.yaml:179 +#: pas/plugins/ldap/properties.yaml:223 msgid "lbl_object_classes_for_groups" msgstr "" #. Default: Object classes for User creation -#: pas/plugins/ldap/properties.yaml:93 +#: pas/plugins/ldap/properties.yaml:125 msgid "lbl_object_classes_for_user_creation" msgstr "" #. Default: Page Size -#: pas/plugins/ldap/properties.yaml:61 +#: pas/plugins/ldap/properties.yaml:93 msgid "lbl_page_size" msgstr "" +#. Default: Role +#: pas/plugins/ldap/properties.yaml:141 +msgid "lbl_role" +msgstr "" + +#. Default: Roles acquired +#: pas/plugins/ldap/properties.yaml:140 +msgid "lbl_roles_acquired" +msgstr "" + #. Default: Save -#: pas/plugins/ldap/properties.yaml:240 +#: pas/plugins/ldap/properties.yaml:284 msgid "lbl_save" msgstr "" #. Default: Support recursive/nested groups? -#: pas/plugins/ldap/properties.yaml:107 +#: pas/plugins/ldap/properties.yaml:150 msgid "lbl_support_recursive_nested_groups" msgstr "" +#. Default: Path to folder with CA certificate files for TLS communication +#. (OPT_X_TLS_CACERTDIR) +#: pas/plugins/ldap/properties.yaml:75 +msgid "lbl_tls_cacertdir" +msgstr "" + +#. Default: Path to CA certificate file for TLS communication +#. (OPT_X_TLS_CACERTFILE) +#: pas/plugins/ldap/properties.yaml:69 +msgid "lbl_tls_cacertfile" +msgstr "" + +#. Default: Path to client certificate file for TLS communication +#. (OPT_X_TLS_CERTFILE). Requires tls_clkeyfile +#: pas/plugins/ldap/properties.yaml:81 +msgid "lbl_tls_clcertfile" +msgstr "" + +#. Default: Path to client certificate key for TLS communication +#. (OPT_X_TLS_KEYFILE). Requires tls_clcertfile +#: pas/plugins/ldap/properties.yaml:87 +msgid "lbl_tls_clkeyfile" +msgstr "" + +#. Default: Use TLS connection +#: pas/plugins/ldap/properties.yaml:63 +msgid "lbl_use_tls_connection" +msgstr "" + #. Default: User Accounts expires? -#: pas/plugins/ldap/properties.yaml:122 +#: pas/plugins/ldap/properties.yaml:165 msgid "lbl_user_accounts_expires" msgstr "" #. Default: User attribute aliases -#: pas/plugins/ldap/properties.yaml:138 +#: pas/plugins/ldap/properties.yaml:182 msgid "lbl_user_attribute_aliases" msgstr "" #. Default: User Property-Sheet Attributes -#: pas/plugins/ldap/properties.yaml:148 +#: pas/plugins/ldap/properties.yaml:192 msgid "lbl_user_property-sheet_attributes" msgstr "" #. Default: Users container DN -#: pas/plugins/ldap/properties.yaml:76 +#: pas/plugins/ldap/properties.yaml:108 msgid "lbl_users_container_dn" msgstr "" #. Default: Users search query filter -#: pas/plugins/ldap/properties.yaml:88 +#: pas/plugins/ldap/properties.yaml:120 msgid "lbl_users_search_query_filter" msgstr "" #. Default: Users search scope -#: pas/plugins/ldap/properties.yaml:82 +#: pas/plugins/ldap/properties.yaml:114 msgid "lbl_users_search_scope" msgstr "" #. Default: Users Settings -#: pas/plugins/ldap/properties.yaml:69 +#: pas/plugins/ldap/properties.yaml:101 msgid "lbl_users_settings" msgstr "" @@ -365,27 +475,27 @@ msgid "msg_connection_uri" msgstr "" #. Default: Group attribute aliases values are mandatory -#: pas/plugins/ldap/properties.yaml:195 +#: pas/plugins/ldap/properties.yaml:239 msgid "msg_group_attribute_aliases" msgstr "" #. Default: No Groups DN defined -#: pas/plugins/ldap/properties.yaml:163 +#: pas/plugins/ldap/properties.yaml:207 msgid "msg_groups_container_dn" msgstr "" #. Default: 'Page size must be given.' -#: pas/plugins/ldap/properties.yaml:65 +#: pas/plugins/ldap/properties.yaml:97 msgid "msg_page_size" msgstr "" #. Default: User attribute aliases values are mandatory -#: pas/plugins/ldap/properties.yaml:139 +#: pas/plugins/ldap/properties.yaml:183 msgid "msg_user_attribute_aliases" msgstr "" #. Default: No Users DN defined -#: pas/plugins/ldap/properties.yaml:77 +#: pas/plugins/ldap/properties.yaml:109 msgid "msg_users_container_dn" msgstr "" @@ -393,7 +503,7 @@ msgstr "" msgid "pas.plugins.ldap support for users and groups from ldap/active directory." msgstr "" -#: pas/plugins/ldap/plonecontrolpanel/cache.py:28 +#: pas/plugins/ldap/plonecontrolpanel/cache.py:31 #: pas/plugins/ldap/plonecontrolpanel/profiles/default/registry.xml msgid "servers, delimited by space" msgstr "" diff --git a/src/pas/plugins/ldap/locales/es/LC_MESSAGES/pas.plugins.ldap.po b/src/pas/plugins/ldap/locales/es/LC_MESSAGES/pas.plugins.ldap.po index e6f8479..3806b53 100644 --- a/src/pas/plugins/ldap/locales/es/LC_MESSAGES/pas.plugins.ldap.po +++ b/src/pas/plugins/ldap/locales/es/LC_MESSAGES/pas.plugins.ldap.po @@ -1,12 +1,13 @@ # Translation of pas.plugins.ldap.pot to Spanish -# Leonardo J. Caballero G. , 2025. +# Leonardo J. Caballero G. , 2025, 2026. msgid "" msgstr "" "Project-Id-Version: pas.plugins.ldap\n" -"POT-Creation-Date: 2025-06-21 20:09+0000\n" -"PO-Revision-Date: 2025-06-21 19:34+0200\n" +"POT-Creation-Date: 2026-04-29 16:56+0000\n" +"PO-Revision-Date: 2026-04-29 19:02+0200\n" "Last-Translator: Leonardo J. Caballero G. \n" "Language-Team: \n" +"Language: es\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" @@ -15,24 +16,35 @@ msgstr "" "Language-Name: English\n" "Preferred-Encodings: utf-8 latin1\n" "Domain: pas.plugins.ldap\n" -"Language: es\n" -"X-Generator: Poedit 3.6\n" +"X-Generator: Poedit 3.9\n" "X-Is-Fallback-For: es-ar es-bo es-cl es-co es-cr es-do es-ec es-es es-sv es-gt es-hn es-mx es-ni es-pa es-py es-pe es-pr es-us es-uy es-ve\n" +#: pas/plugins/ldap/properties.py:37 +msgid "BASE" +msgstr "BASE" + #: pas/plugins/ldap/plonecontrolpanel/controlpanel.pt:16 #: pas/plugins/ldap/zmi/manage_plugin.pt:28 msgid "Connection Test" msgstr "Prueba de conexión" -#: pas/plugins/ldap/properties.py:223 +#: pas/plugins/ldap/properties.py:254 +msgid "Connection, users- and groups-access tested successfully." +msgstr "Se ha comprobado correctamente el acceso a la conexión, a los usuarios y a los grupos." + +#: pas/plugins/ldap/properties.py:43 +msgid "Days since Epoch" +msgstr "Días desde el inicio del período" + +#: pas/plugins/ldap/properties.py:253 msgid "Exception in Groups; " msgstr "Excepción en Grupos; " -#: pas/plugins/ldap/properties.py:216 +#: pas/plugins/ldap/properties.py:246 msgid "Exception in Users; " msgstr "Excepción en Usuarios; " -#: pas/plugins/ldap/configure.zcml:34 +#: pas/plugins/ldap/configure.zcml:29 msgid "Extension profile for pas.plugins.ldap Zope Base." msgstr "Perfil de extensión base de Zope para pas.plugins.ldap." @@ -48,19 +60,19 @@ msgstr "Inspector secundario LDAP" msgid "LDAP Inspector" msgstr "Inspector LDAP" -#: pas/plugins/ldap/configure.zcml:34 +#: pas/plugins/ldap/configure.zcml:29 msgid "LDAP Plugin for PAS - Zope 2 Base Installation" msgstr "Plugin LDAP para PAS - Instalación base de Zope 2" -#: pas/plugins/ldap/plonecontrolpanel/controlpanel.py:31 +#: pas/plugins/ldap/plonecontrolpanel/controlpanel.py:43 msgid "LDAP Settings saved." msgstr "Configuración de LDAP guardada." -#: pas/plugins/ldap/properties.py:220 +#: pas/plugins/ldap/properties.py:250 msgid "LDAP Users ok, but groups not; " msgstr "Los usuarios de LDAP están bien, pero los grupos no; " -#: pas/plugins/ldap/properties.py:213 +#: pas/plugins/ldap/properties.py:243 msgid "LDAP users; " msgstr "Usuarios de LDAP; " @@ -84,27 +96,39 @@ msgstr "Administre las propiedades del complemento LDAP / AD para id $ {plugin_p msgid "No search base selected" msgstr "No se ha seleccionado ninguna base de búsqueda" -#: pas/plugins/ldap/properties.py:204 +#: pas/plugins/ldap/properties.py:234 msgid "Non-LDAP error while getting ILDAPGroupsConfig!" msgstr "¡Error no LDAP al obtener ILDAPGroupsConfig!" -#: pas/plugins/ldap/properties.py:192 +#: pas/plugins/ldap/properties.py:222 msgid "Non-LDAP error while getting ILDAPProps!" msgstr "¡Error no LDAP al obtener ILDAPProps!" -#: pas/plugins/ldap/properties.py:198 +#: pas/plugins/ldap/properties.py:228 msgid "Non-LDAP error while getting ILDAPUsersConfig!" msgstr "¡Error no LDAP al obtener ILDAPUsersConfig!" -#: pas/plugins/ldap/properties.py:180 +#: pas/plugins/ldap/properties.py:38 +msgid "ONELEVEL" +msgstr "UN NIVEL" + +#: pas/plugins/ldap/properties.py:204 msgid "Password is required for non-anonymous connections." msgstr "La contraseña es necesaria para las conexiones no anónimas." +#: pas/plugins/ldap/properties.py:39 +msgid "SUBTREE" +msgstr "SUBÁRBOL" + +#: pas/plugins/ldap/properties.py:44 +msgid "Seconds since epoch" +msgstr "Segundos desde el inicio del período" + #: pas/plugins/ldap/plonecontrolpanel/inspector.pt:13 msgid "Select search base" msgstr "Seleccione la base de búsqueda" -#: pas/plugins/ldap/properties.py:211 +#: pas/plugins/ldap/properties.py:241 msgid "Server Down" msgstr "Servidor caído" @@ -116,11 +140,19 @@ msgstr "Establezca las propiedades de los usuarios y grupos de LDAP/ActiveDirect msgid "Settings" msgstr "Ajustes" -#: pas/plugins/ldap/properties.py:185 +#: pas/plugins/ldap/plonecontrolpanel/configure.zcml:27 +msgid "Uninstall LDAP Plugin for PAS" +msgstr "Desinstalar el complemento LDAP para PAS" + +#: pas/plugins/ldap/plonecontrolpanel/configure.zcml:27 +msgid "Uninstalls the pas.plugins.ldap add-on." +msgstr "Desinstala el complemento pas.plugins.ldap." + +#: pas/plugins/ldap/properties.py:209 msgid "User/Password are required if not anonymous." msgstr "El usuario/contraseña son obligatorios si no son anónimos." -#: pas/plugins/ldap/properties.py:174 +#: pas/plugins/ldap/properties.py:198 msgid "Username is required for non-anonymous connections." msgstr "El nombre de usuario es necesario para las conexiones no anónimas." @@ -129,23 +161,22 @@ msgid "Users" msgstr "Usuarios" #. Default: LDAP attr name -#: pas/plugins/ldap/properties.yaml:199 +#: pas/plugins/ldap/properties.yaml:243 msgid "head_ldap_attr_name" msgstr "Nombre de atributo LDAP" #. Default: LDAP Attribute -#: pas/plugins/ldap/properties.yaml:143 +#: pas/plugins/ldap/properties.yaml:187 msgid "head_ldap_attribute" msgstr "Atributo LDAP" #. Default: Name on Sheet -#: pas/plugins/ldap/properties.yaml:150 +#: pas/plugins/ldap/properties.yaml:194 msgid "head_name_on_sheet" msgstr "Nombre en la hoja" #. Default: Reserved Key -#: pas/plugins/ldap/properties.yaml:142 -#, fuzzy +#: pas/plugins/ldap/properties.yaml:186 msgid "head_reserved_key" msgstr "Clave reservada" @@ -157,41 +188,80 @@ msgstr "Ejemplo, el protocolo es ldap, la dirección IP es 127.0.0.1 y el puerto #. Default: If set on authenticate a failing certificate chain check including #. CA is ignored. -#: pas/plugins/ldap/properties.yaml:56 +#: pas/plugins/ldap/properties.yaml:58 msgid "help_ignore_certificate_check" msgstr "Si se establece en autenticar una comprobación de cadena de certificado fallido incluyendo CA se ignora." +#. Default: The timeout period, in seconds, for waiting for a connection to be +#. established with the LDAP server. +#: pas/plugins/ldap/properties.yaml:34 +msgid "help_ldap_connection_timeout_in_seconds" +msgstr "El tiempo de espera en segundos para esperar por una conexión sea establecida a servidor LDAP." + +#. Default: The timeout in seconds for an operation such as a search or update +#. to complete. If no timeout is required, use -1 as the value. +#: pas/plugins/ldap/properties.yaml:41 +msgid "help_ldap_operation_timeout_in_seconds" +msgstr "El tiempo de espera en segundos para una operación como una búsqueda o actualizar en completarse. Si se debe utilizar sin tiempo de espera, usar -1 como valor." + #. Default: "Group DNs outside of the groups base DN are ignored, except if #. listed here" -#: pas/plugins/ldap/properties.yaml:113 +#: pas/plugins/ldap/properties.yaml:156 msgid "help_memberOf_external_allowed_group_dns" msgstr "Los DN de grupo que no sean el DN base del grupo se ignoran, excepto si se indican aquí" #. Default: global - same server for all ldap plugins -#: pas/plugins/ldap/properties.yaml:224 +#: pas/plugins/ldap/properties.yaml:268 msgid "help_memcached_server_to_use" msgstr "global - mismo servidor para todos los plugins ldap" #. Default: "One of those is mandatory like as one the following options #. groupOfNames, groupOfUniqueNames, posixGroup, group" -#: pas/plugins/ldap/properties.yaml:180 +#: pas/plugins/ldap/properties.yaml:224 msgid "help_object_classes_for_groups" msgstr "Uno de ellos es obligatorio como una de las siguientes opciones groupOfNames, groupOfUniqueNames, posixGroup y group" #. Default: Maximum page size, number of results to query the server at once #. for. -#: pas/plugins/ldap/properties.yaml:62 +#: pas/plugins/ldap/properties.yaml:94 msgid "help_page_size" msgstr "Tamaño máximo de la página, número de resultados que se pueden consultar a la vez en el servidor." #. Default: If your LDAP/AD supports it this will use #. LDAP_MATCHING_RULE_IN_CHAIN. By default only AD supports this. -#: pas/plugins/ldap/properties.yaml:108 +#: pas/plugins/ldap/properties.yaml:151 msgid "help_support_recursive_nested_groups" msgstr "Si su LDAP/AD lo admite, utilizará LDAP_MATCHING_RULE_IN_CHAIN. Por defecto sólo AD lo soporta." +#. Default: If set, the LDAP server certificate is checked against the CA +#. certificates in folder. +#: pas/plugins/ldap/properties.yaml:76 +msgid "help_tls_cacertdir" +msgstr "Si se establece, el certificado del servidor LDAP se verifica con los certificados CA en la carpeta." + +#. Default: If set, the LDAP server certificate is checked against the CA +#. certificate file. +#: pas/plugins/ldap/properties.yaml:70 +msgid "help_tls_cacertfile" +msgstr "Si se establece, el certificado del servidor LDAP se verifica con el archivo de certificados CA." + +#. Default: If set, the client certificate is sent to the server. +#: pas/plugins/ldap/properties.yaml:82 +msgid "help_tls_clcertfile" +msgstr "Si se establece, el certificado del cliente se envía al servidor." + +#. Default: If set, the client certificate is sent to the server. +#: pas/plugins/ldap/properties.yaml:88 +msgid "help_tls_clkeyfile" +msgstr "Si se establece, la clave del certificado del cliente se envía al servidor." + +#. Default: If set, the connection is upgraded to TLS. +#: pas/plugins/ldap/properties.yaml:64 +msgid "help_use_tls_connection" +msgstr "Si se establece, la conexión se actualiza a TLS." + #. Default: Account expiration unit -#: pas/plugins/ldap/properties.yaml:132 +#: pas/plugins/ldap/properties.yaml:175 msgid "lbl_account_expiration_unit" msgstr "Unidad de caducidad de la cuenta" @@ -201,22 +271,22 @@ msgid "lbl_anonymous_connection" msgstr "¿Conexión anónima?" #. Default: Attribute containing expiration Time -#: pas/plugins/ldap/properties.yaml:127 +#: pas/plugins/ldap/properties.yaml:170 msgid "lbl_attribute_containing_expiration_time" msgstr "Atributo que contiene la hora de expiración" #. Default: Cache LDAP queries -#: pas/plugins/ldap/properties.yaml:218 +#: pas/plugins/ldap/properties.yaml:262 msgid "lbl_cache_ldap_queries" msgstr "Caché de consultas LDAP" #. Default: Cache Settings -#: pas/plugins/ldap/properties.yaml:211 +#: pas/plugins/ldap/properties.yaml:255 msgid "lbl_cache_settings" msgstr "Configuración de la caché" #. Default: Cache timeout in seconds -#: pas/plugins/ldap/properties.yaml:231 +#: pas/plugins/ldap/properties.yaml:275 msgid "lbl_cache_timeout_in_seconds" msgstr "Tiempo de espera de la caché en segundos" @@ -226,49 +296,49 @@ msgid "lbl_connection_uri" msgstr "URI de conexión" #. Default: Group attribute aliases -#: pas/plugins/ldap/properties.yaml:194 +#: pas/plugins/ldap/properties.yaml:238 msgid "lbl_group_attribute_aliases" msgstr "Alias de atributos de grupo" #. Default: Group Property-Sheet Attributes -#: pas/plugins/ldap/properties.yaml:204 +#: pas/plugins/ldap/properties.yaml:248 msgid "lbl_group_property-sheet_attributes" msgstr "Atributos de la hoja de propiedades de grupo" #. Default: Groups container DN -#: pas/plugins/ldap/properties.yaml:162 +#: pas/plugins/ldap/properties.yaml:206 msgid "lbl_groups_container_dn" msgstr "DN del contenedor de Grupos" #. Default: Groups search query filter -#: pas/plugins/ldap/properties.yaml:174 +#: pas/plugins/ldap/properties.yaml:218 msgid "lbl_groups_search_query_filter" msgstr "Filtro de búsqueda de grupos" #. Default: Groups search scope -#: pas/plugins/ldap/properties.yaml:168 +#: pas/plugins/ldap/properties.yaml:212 msgid "lbl_groups_search_scope" msgstr "Ámbito de búsqueda de grupos" #. Default: Groups Settings -#: pas/plugins/ldap/properties.yaml:155 +#: pas/plugins/ldap/properties.yaml:199 msgid "lbl_groups_settings" msgstr "Configuración de grupos" #. Default: Ignore certificate check? -#: pas/plugins/ldap/properties.yaml:55 +#: pas/plugins/ldap/properties.yaml:57 msgid "lbl_ignore_certificate_check" msgstr "¿Ignorar la comprobación del certificado?" -#. Default: LDAP connection timeout in seconds +#. Default: LDAP connection timeout in seconds. #: pas/plugins/ldap/properties.yaml:33 msgid "lbl_ldap_connection_timeout_in_seconds" -msgstr "Tiempo de espera de la conexión LDAP en segundos" +msgstr "Tiempo de espera de la conexión LDAP en segundos." -#. Default: LDAP operation timeout in seconds -#: pas/plugins/ldap/properties.yaml:39 +#. Default: LDAP operation timeout in seconds. +#: pas/plugins/ldap/properties.yaml:40 msgid "lbl_ldap_operation_timeout_in_seconds" -msgstr "Tiempo de espera de la operación LDAP en segundos" +msgstr "Tiempo de espera de la operación LDAP en segundos." #. Default: LDAP Server Settings #: pas/plugins/ldap/properties.yaml:10 @@ -276,92 +346,131 @@ msgid "lbl_ldap_server_settings" msgstr "Configuración del servidor LDAP" #. Default: Manager Password -#: pas/plugins/ldap/properties.yaml:50 +#: pas/plugins/ldap/properties.yaml:52 msgid "lbl_manager_password" msgstr "Contraseña de administrador" #. Default: Manager User -#: pas/plugins/ldap/properties.yaml:45 +#: pas/plugins/ldap/properties.yaml:47 msgid "lbl_manager_user" msgstr "Usuario de administrador" #. Default: memberOf attribute supported? -#: pas/plugins/ldap/properties.yaml:102 +#: pas/plugins/ldap/properties.yaml:135 msgid "lbl_memberOf_attribute_supported" msgstr "¿se admite el atributo memberOf?" #. Default: memberOf external allowed Group DNs -#: pas/plugins/ldap/properties.yaml:114 +#: pas/plugins/ldap/properties.yaml:157 msgid "lbl_memberOf_external_allowed_group_dns" msgstr "DNs de grupo permitidos memberOf externos" #. Default: Memcached Server to use -#: pas/plugins/ldap/properties.yaml:223 +#: pas/plugins/ldap/properties.yaml:267 msgid "lbl_memcached_server_to_use" msgstr "Servidor Memcached a utilizar" #. Default: Object class -#: pas/plugins/ldap/properties.yaml:94 +#: pas/plugins/ldap/properties.yaml:126 msgid "lbl_object_class" msgstr "Clase de objeto" #. Default: Object classes for Groups -#: pas/plugins/ldap/properties.yaml:179 +#: pas/plugins/ldap/properties.yaml:223 msgid "lbl_object_classes_for_groups" msgstr "Clases de objetos para Grupos" #. Default: Object classes for User creation -#: pas/plugins/ldap/properties.yaml:93 +#: pas/plugins/ldap/properties.yaml:125 msgid "lbl_object_classes_for_user_creation" msgstr "Clases de objetos para la creación de usuarios" #. Default: Page Size -#: pas/plugins/ldap/properties.yaml:61 +#: pas/plugins/ldap/properties.yaml:93 msgid "lbl_page_size" msgstr "Tamaño de página" +#. Default: Role +#: pas/plugins/ldap/properties.yaml:141 +msgid "lbl_role" +msgstr "Rol" + +#. Default: Roles acquired +#: pas/plugins/ldap/properties.yaml:140 +msgid "lbl_roles_acquired" +msgstr "Roles adquiridos" + #. Default: Save -#: pas/plugins/ldap/properties.yaml:240 +#: pas/plugins/ldap/properties.yaml:284 msgid "lbl_save" msgstr "Guardar" #. Default: Support recursive/nested groups? -#: pas/plugins/ldap/properties.yaml:107 +#: pas/plugins/ldap/properties.yaml:150 msgid "lbl_support_recursive_nested_groups" msgstr "¿Admite grupos recursivos/anidados?" +#. Default: Path to folder with CA certificate files for TLS communication +#. (OPT_X_TLS_CACERTDIR) +#: pas/plugins/ldap/properties.yaml:75 +msgid "lbl_tls_cacertdir" +msgstr "Ruta a la carpeta con archivos de certificados CA para la comunicación TLS (OPT_X_TLS_CACERTDIR)" + +#. Default: Path to CA certificate file for TLS communication +#. (OPT_X_TLS_CACERTFILE) +#: pas/plugins/ldap/properties.yaml:69 +msgid "lbl_tls_cacertfile" +msgstr "Ruta al archivo de certificados CA para la comunicación TLS (OPT_X_TLS_CACERTFILE)" + +#. Default: Path to client certificate file for TLS communication +#. (OPT_X_TLS_CERTFILE). Requires tls_clkeyfile +#: pas/plugins/ldap/properties.yaml:81 +msgid "lbl_tls_clcertfile" +msgstr "Ruta al archivo de certificado del cliente para la comunicación TLS (OPT_X_TLS_CERTFILE). Requiere tls_clkeyfile" + +#. Default: Path to client certificate key for TLS communication +#. (OPT_X_TLS_KEYFILE). Requires tls_clcertfile +#: pas/plugins/ldap/properties.yaml:87 +msgid "lbl_tls_clkeyfile" +msgstr "Ruta a la clave del certificado del cliente para la comunicación TLS (OPT_X_TLS_KEYFILE). Requiere tls_clcertfile" + +#. Default: Use TLS connection +#: pas/plugins/ldap/properties.yaml:63 +msgid "lbl_use_tls_connection" +msgstr "Usar conexión TLS" + #. Default: User Accounts expires? -#: pas/plugins/ldap/properties.yaml:122 +#: pas/plugins/ldap/properties.yaml:165 msgid "lbl_user_accounts_expires" msgstr "¿Caducan las cuentas de usuario?" #. Default: User attribute aliases -#: pas/plugins/ldap/properties.yaml:138 +#: pas/plugins/ldap/properties.yaml:182 msgid "lbl_user_attribute_aliases" msgstr "Alias de atributos de usuario" #. Default: User Property-Sheet Attributes -#: pas/plugins/ldap/properties.yaml:148 +#: pas/plugins/ldap/properties.yaml:192 msgid "lbl_user_property-sheet_attributes" msgstr "Atributos de la hoja de propiedades del usuario" #. Default: Users container DN -#: pas/plugins/ldap/properties.yaml:76 +#: pas/plugins/ldap/properties.yaml:108 msgid "lbl_users_container_dn" msgstr "DN del contenedor de usuarios" #. Default: Users search query filter -#: pas/plugins/ldap/properties.yaml:88 +#: pas/plugins/ldap/properties.yaml:120 msgid "lbl_users_search_query_filter" msgstr "Filtro de consulta de búsqueda de usuarios" #. Default: Users search scope -#: pas/plugins/ldap/properties.yaml:82 +#: pas/plugins/ldap/properties.yaml:114 msgid "lbl_users_search_scope" msgstr "Ámbito de búsqueda de los usuarios" #. Default: Users Settings -#: pas/plugins/ldap/properties.yaml:69 +#: pas/plugins/ldap/properties.yaml:101 msgid "lbl_users_settings" msgstr "Configuración de usuarios" @@ -371,27 +480,27 @@ msgid "msg_connection_uri" msgstr "No hay URI definido" #. Default: Group attribute aliases values are mandatory -#: pas/plugins/ldap/properties.yaml:195 +#: pas/plugins/ldap/properties.yaml:239 msgid "msg_group_attribute_aliases" msgstr "Los valores de los alias de atributos de grupo son obligatorios" #. Default: No Groups DN defined -#: pas/plugins/ldap/properties.yaml:163 +#: pas/plugins/ldap/properties.yaml:207 msgid "msg_groups_container_dn" msgstr "No hay grupos DN definidos" #. Default: 'Page size must be given.' -#: pas/plugins/ldap/properties.yaml:65 +#: pas/plugins/ldap/properties.yaml:97 msgid "msg_page_size" msgstr "'Debe indicarse el tamaño de la página.'" #. Default: User attribute aliases values are mandatory -#: pas/plugins/ldap/properties.yaml:139 +#: pas/plugins/ldap/properties.yaml:183 msgid "msg_user_attribute_aliases" msgstr "Los valores de los alias de atributos de usuario son obligatorios" #. Default: No Users DN defined -#: pas/plugins/ldap/properties.yaml:77 +#: pas/plugins/ldap/properties.yaml:109 msgid "msg_users_container_dn" msgstr "No se ha definido ningún DN de usuario" @@ -399,7 +508,7 @@ msgstr "No se ha definido ningún DN de usuario" msgid "pas.plugins.ldap support for users and groups from ldap/active directory." msgstr "Soporte para usuarios y grupos de LDAP/Active Directory con el complemento pas.plugins.ldap." -#: pas/plugins/ldap/plonecontrolpanel/cache.py:28 +#: pas/plugins/ldap/plonecontrolpanel/cache.py:31 #: pas/plugins/ldap/plonecontrolpanel/profiles/default/registry.xml msgid "servers, delimited by space" msgstr "servidores, delimitados por el espacio" diff --git a/src/pas/plugins/ldap/locales/merge-lingua.pot b/src/pas/plugins/ldap/locales/merge-lingua.pot index 2da9871..3263eab 100644 --- a/src/pas/plugins/ldap/locales/merge-lingua.pot +++ b/src/pas/plugins/ldap/locales/merge-lingua.pot @@ -1,12 +1,12 @@ # # SOME DESCRIPTIVE TITLE # This file is distributed under the same license as the PACKAGE package. -# FIRST AUTHOR , 2025. +# FIRST AUTHOR , 2026. #, fuzzy msgid "" msgstr "" "Project-Id-Version: PACKAGE 1.0\n" -"POT-Creation-Date: 2025-06-21 21:08+0200\n" +"POT-Creation-Date: 2026-04-29 18:56+0200\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -42,249 +42,327 @@ msgstr "" msgid "lbl_anonymous_connection" msgstr "" -#. Default: LDAP connection timeout in seconds +#. Default: LDAP connection timeout in seconds. #: /home/plone/project/backend/sources/pas.plugins.ldap/src/pas/plugins/ldap/properties.yaml:33 msgid "lbl_ldap_connection_timeout_in_seconds" msgstr "" -#. Default: LDAP operation timeout in seconds -#: /home/plone/project/backend/sources/pas.plugins.ldap/src/pas/plugins/ldap/properties.yaml:39 +#. Default: The timeout period, in seconds, for waiting for a connection to be +#. established with the LDAP server. +#: /home/plone/project/backend/sources/pas.plugins.ldap/src/pas/plugins/ldap/properties.yaml:34 +msgid "help_ldap_connection_timeout_in_seconds" +msgstr "" + +#. Default: LDAP operation timeout in seconds. +#: /home/plone/project/backend/sources/pas.plugins.ldap/src/pas/plugins/ldap/properties.yaml:40 msgid "lbl_ldap_operation_timeout_in_seconds" msgstr "" +#. Default: The timeout in seconds for an operation such as a search or update +#. to complete. If no timeout is required, use -1 as the value. +#: /home/plone/project/backend/sources/pas.plugins.ldap/src/pas/plugins/ldap/properties.yaml:41 +msgid "help_ldap_operation_timeout_in_seconds" +msgstr "" + #. Default: Manager User -#: /home/plone/project/backend/sources/pas.plugins.ldap/src/pas/plugins/ldap/properties.yaml:45 +#: /home/plone/project/backend/sources/pas.plugins.ldap/src/pas/plugins/ldap/properties.yaml:47 msgid "lbl_manager_user" msgstr "" #. Default: Manager Password -#: /home/plone/project/backend/sources/pas.plugins.ldap/src/pas/plugins/ldap/properties.yaml:50 +#: /home/plone/project/backend/sources/pas.plugins.ldap/src/pas/plugins/ldap/properties.yaml:52 msgid "lbl_manager_password" msgstr "" #. Default: Ignore certificate check? -#: /home/plone/project/backend/sources/pas.plugins.ldap/src/pas/plugins/ldap/properties.yaml:55 +#: /home/plone/project/backend/sources/pas.plugins.ldap/src/pas/plugins/ldap/properties.yaml:57 msgid "lbl_ignore_certificate_check" msgstr "" #. Default: If set on authenticate a failing certificate chain check including #. CA is ignored. -#: /home/plone/project/backend/sources/pas.plugins.ldap/src/pas/plugins/ldap/properties.yaml:56 +#: /home/plone/project/backend/sources/pas.plugins.ldap/src/pas/plugins/ldap/properties.yaml:58 msgid "help_ignore_certificate_check" msgstr "" +#. Default: Use TLS connection +#: /home/plone/project/backend/sources/pas.plugins.ldap/src/pas/plugins/ldap/properties.yaml:63 +msgid "lbl_use_tls_connection" +msgstr "" + +#. Default: If set, the connection is upgraded to TLS. +#: /home/plone/project/backend/sources/pas.plugins.ldap/src/pas/plugins/ldap/properties.yaml:64 +msgid "help_use_tls_connection" +msgstr "" + +#. Default: Path to CA certificate file for TLS communication +#. (OPT_X_TLS_CACERTFILE) +#: /home/plone/project/backend/sources/pas.plugins.ldap/src/pas/plugins/ldap/properties.yaml:69 +msgid "lbl_tls_cacertfile" +msgstr "" + +#. Default: If set, the LDAP server certificate is checked against the CA +#. certificate file. +#: /home/plone/project/backend/sources/pas.plugins.ldap/src/pas/plugins/ldap/properties.yaml:70 +msgid "help_tls_cacertfile" +msgstr "" + +#. Default: Path to folder with CA certificate files for TLS communication +#. (OPT_X_TLS_CACERTDIR) +#: /home/plone/project/backend/sources/pas.plugins.ldap/src/pas/plugins/ldap/properties.yaml:75 +msgid "lbl_tls_cacertdir" +msgstr "" + +#. Default: If set, the LDAP server certificate is checked against the CA +#. certificates in folder. +#: /home/plone/project/backend/sources/pas.plugins.ldap/src/pas/plugins/ldap/properties.yaml:76 +msgid "help_tls_cacertdir" +msgstr "" + +#. Default: Path to client certificate file for TLS communication +#. (OPT_X_TLS_CERTFILE). Requires tls_clkeyfile +#: /home/plone/project/backend/sources/pas.plugins.ldap/src/pas/plugins/ldap/properties.yaml:81 +msgid "lbl_tls_clcertfile" +msgstr "" + +#. Default: If set, the client certificate is sent to the server. +#: /home/plone/project/backend/sources/pas.plugins.ldap/src/pas/plugins/ldap/properties.yaml:82 +msgid "help_tls_clcertfile" +msgstr "" + +#. Default: Path to client certificate key for TLS communication +#. (OPT_X_TLS_KEYFILE). Requires tls_clcertfile +#: /home/plone/project/backend/sources/pas.plugins.ldap/src/pas/plugins/ldap/properties.yaml:87 +msgid "lbl_tls_clkeyfile" +msgstr "" + +#. Default: If set, the client certificate is sent to the server. +#: /home/plone/project/backend/sources/pas.plugins.ldap/src/pas/plugins/ldap/properties.yaml:88 +msgid "help_tls_clkeyfile" +msgstr "" + #. Default: Page Size -#: /home/plone/project/backend/sources/pas.plugins.ldap/src/pas/plugins/ldap/properties.yaml:61 +#: /home/plone/project/backend/sources/pas.plugins.ldap/src/pas/plugins/ldap/properties.yaml:93 msgid "lbl_page_size" msgstr "" #. Default: Maximum page size, number of results to query the server at once #. for. -#: /home/plone/project/backend/sources/pas.plugins.ldap/src/pas/plugins/ldap/properties.yaml:62 +#: /home/plone/project/backend/sources/pas.plugins.ldap/src/pas/plugins/ldap/properties.yaml:94 msgid "help_page_size" msgstr "" #. Default: 'Page size must be given.' -#: /home/plone/project/backend/sources/pas.plugins.ldap/src/pas/plugins/ldap/properties.yaml:65 +#: /home/plone/project/backend/sources/pas.plugins.ldap/src/pas/plugins/ldap/properties.yaml:97 msgid "msg_page_size" msgstr "" #. Default: Users Settings -#: /home/plone/project/backend/sources/pas.plugins.ldap/src/pas/plugins/ldap/properties.yaml:69 +#: /home/plone/project/backend/sources/pas.plugins.ldap/src/pas/plugins/ldap/properties.yaml:101 msgid "lbl_users_settings" msgstr "" #. Default: Users container DN -#: /home/plone/project/backend/sources/pas.plugins.ldap/src/pas/plugins/ldap/properties.yaml:76 +#: /home/plone/project/backend/sources/pas.plugins.ldap/src/pas/plugins/ldap/properties.yaml:108 msgid "lbl_users_container_dn" msgstr "" #. Default: No Users DN defined -#: /home/plone/project/backend/sources/pas.plugins.ldap/src/pas/plugins/ldap/properties.yaml:77 +#: /home/plone/project/backend/sources/pas.plugins.ldap/src/pas/plugins/ldap/properties.yaml:109 msgid "msg_users_container_dn" msgstr "" #. Default: Users search scope -#: /home/plone/project/backend/sources/pas.plugins.ldap/src/pas/plugins/ldap/properties.yaml:82 +#: /home/plone/project/backend/sources/pas.plugins.ldap/src/pas/plugins/ldap/properties.yaml:114 msgid "lbl_users_search_scope" msgstr "" #. Default: Users search query filter -#: /home/plone/project/backend/sources/pas.plugins.ldap/src/pas/plugins/ldap/properties.yaml:88 +#: /home/plone/project/backend/sources/pas.plugins.ldap/src/pas/plugins/ldap/properties.yaml:120 msgid "lbl_users_search_query_filter" msgstr "" #. Default: Object classes for User creation -#: /home/plone/project/backend/sources/pas.plugins.ldap/src/pas/plugins/ldap/properties.yaml:93 +#: /home/plone/project/backend/sources/pas.plugins.ldap/src/pas/plugins/ldap/properties.yaml:125 msgid "lbl_object_classes_for_user_creation" msgstr "" #. Default: Object class -#: /home/plone/project/backend/sources/pas.plugins.ldap/src/pas/plugins/ldap/properties.yaml:94 -#: /home/plone/project/backend/sources/pas.plugins.ldap/src/pas/plugins/ldap/properties.yaml:181 +#: /home/plone/project/backend/sources/pas.plugins.ldap/src/pas/plugins/ldap/properties.yaml:126 +#: /home/plone/project/backend/sources/pas.plugins.ldap/src/pas/plugins/ldap/properties.yaml:225 msgid "lbl_object_class" msgstr "" #. Default: memberOf attribute supported? -#: /home/plone/project/backend/sources/pas.plugins.ldap/src/pas/plugins/ldap/properties.yaml:102 -#: /home/plone/project/backend/sources/pas.plugins.ldap/src/pas/plugins/ldap/properties.yaml:189 +#: /home/plone/project/backend/sources/pas.plugins.ldap/src/pas/plugins/ldap/properties.yaml:135 +#: /home/plone/project/backend/sources/pas.plugins.ldap/src/pas/plugins/ldap/properties.yaml:233 msgid "lbl_memberOf_attribute_supported" msgstr "" +#. Default: Roles acquired +#: /home/plone/project/backend/sources/pas.plugins.ldap/src/pas/plugins/ldap/properties.yaml:140 +msgid "lbl_roles_acquired" +msgstr "" + +#. Default: Role +#: /home/plone/project/backend/sources/pas.plugins.ldap/src/pas/plugins/ldap/properties.yaml:141 +msgid "lbl_role" +msgstr "" + #. Default: Support recursive/nested groups? -#: /home/plone/project/backend/sources/pas.plugins.ldap/src/pas/plugins/ldap/properties.yaml:107 +#: /home/plone/project/backend/sources/pas.plugins.ldap/src/pas/plugins/ldap/properties.yaml:150 msgid "lbl_support_recursive_nested_groups" msgstr "" #. Default: If your LDAP/AD supports it this will use #. LDAP_MATCHING_RULE_IN_CHAIN. By default only AD supports this. -#: /home/plone/project/backend/sources/pas.plugins.ldap/src/pas/plugins/ldap/properties.yaml:108 +#: /home/plone/project/backend/sources/pas.plugins.ldap/src/pas/plugins/ldap/properties.yaml:151 msgid "help_support_recursive_nested_groups" msgstr "" #. Default: "Group DNs outside of the groups base DN are ignored, except if #. listed here" -#: /home/plone/project/backend/sources/pas.plugins.ldap/src/pas/plugins/ldap/properties.yaml:113 +#: /home/plone/project/backend/sources/pas.plugins.ldap/src/pas/plugins/ldap/properties.yaml:156 msgid "help_memberOf_external_allowed_group_dns" msgstr "" #. Default: memberOf external allowed Group DNs -#: /home/plone/project/backend/sources/pas.plugins.ldap/src/pas/plugins/ldap/properties.yaml:114 +#: /home/plone/project/backend/sources/pas.plugins.ldap/src/pas/plugins/ldap/properties.yaml:157 msgid "lbl_memberOf_external_allowed_group_dns" msgstr "" #. Default: User Accounts expires? -#: /home/plone/project/backend/sources/pas.plugins.ldap/src/pas/plugins/ldap/properties.yaml:122 +#: /home/plone/project/backend/sources/pas.plugins.ldap/src/pas/plugins/ldap/properties.yaml:165 msgid "lbl_user_accounts_expires" msgstr "" #. Default: Attribute containing expiration Time -#: /home/plone/project/backend/sources/pas.plugins.ldap/src/pas/plugins/ldap/properties.yaml:127 +#: /home/plone/project/backend/sources/pas.plugins.ldap/src/pas/plugins/ldap/properties.yaml:170 msgid "lbl_attribute_containing_expiration_time" msgstr "" #. Default: Account expiration unit -#: /home/plone/project/backend/sources/pas.plugins.ldap/src/pas/plugins/ldap/properties.yaml:132 +#: /home/plone/project/backend/sources/pas.plugins.ldap/src/pas/plugins/ldap/properties.yaml:175 msgid "lbl_account_expiration_unit" msgstr "" #. Default: User attribute aliases -#: /home/plone/project/backend/sources/pas.plugins.ldap/src/pas/plugins/ldap/properties.yaml:138 +#: /home/plone/project/backend/sources/pas.plugins.ldap/src/pas/plugins/ldap/properties.yaml:182 msgid "lbl_user_attribute_aliases" msgstr "" #. Default: User attribute aliases values are mandatory -#: /home/plone/project/backend/sources/pas.plugins.ldap/src/pas/plugins/ldap/properties.yaml:139 +#: /home/plone/project/backend/sources/pas.plugins.ldap/src/pas/plugins/ldap/properties.yaml:183 msgid "msg_user_attribute_aliases" msgstr "" #. Default: Reserved Key #. Default: Reserved key -#: /home/plone/project/backend/sources/pas.plugins.ldap/src/pas/plugins/ldap/properties.yaml:142 -#: /home/plone/project/backend/sources/pas.plugins.ldap/src/pas/plugins/ldap/properties.yaml:198 +#: /home/plone/project/backend/sources/pas.plugins.ldap/src/pas/plugins/ldap/properties.yaml:186 +#: /home/plone/project/backend/sources/pas.plugins.ldap/src/pas/plugins/ldap/properties.yaml:242 msgid "head_reserved_key" msgstr "" #. Default: LDAP Attribute -#: /home/plone/project/backend/sources/pas.plugins.ldap/src/pas/plugins/ldap/properties.yaml:143 -#: /home/plone/project/backend/sources/pas.plugins.ldap/src/pas/plugins/ldap/properties.yaml:151 -#: /home/plone/project/backend/sources/pas.plugins.ldap/src/pas/plugins/ldap/properties.yaml:207 +#: /home/plone/project/backend/sources/pas.plugins.ldap/src/pas/plugins/ldap/properties.yaml:187 +#: /home/plone/project/backend/sources/pas.plugins.ldap/src/pas/plugins/ldap/properties.yaml:195 +#: /home/plone/project/backend/sources/pas.plugins.ldap/src/pas/plugins/ldap/properties.yaml:251 msgid "head_ldap_attribute" msgstr "" #. Default: User Property-Sheet Attributes -#: /home/plone/project/backend/sources/pas.plugins.ldap/src/pas/plugins/ldap/properties.yaml:148 +#: /home/plone/project/backend/sources/pas.plugins.ldap/src/pas/plugins/ldap/properties.yaml:192 msgid "lbl_user_property-sheet_attributes" msgstr "" #. Default: Name on Sheet -#: /home/plone/project/backend/sources/pas.plugins.ldap/src/pas/plugins/ldap/properties.yaml:150 -#: /home/plone/project/backend/sources/pas.plugins.ldap/src/pas/plugins/ldap/properties.yaml:206 +#: /home/plone/project/backend/sources/pas.plugins.ldap/src/pas/plugins/ldap/properties.yaml:194 +#: /home/plone/project/backend/sources/pas.plugins.ldap/src/pas/plugins/ldap/properties.yaml:250 msgid "head_name_on_sheet" msgstr "" #. Default: Groups Settings -#: /home/plone/project/backend/sources/pas.plugins.ldap/src/pas/plugins/ldap/properties.yaml:155 +#: /home/plone/project/backend/sources/pas.plugins.ldap/src/pas/plugins/ldap/properties.yaml:199 msgid "lbl_groups_settings" msgstr "" #. Default: Groups container DN -#: /home/plone/project/backend/sources/pas.plugins.ldap/src/pas/plugins/ldap/properties.yaml:162 +#: /home/plone/project/backend/sources/pas.plugins.ldap/src/pas/plugins/ldap/properties.yaml:206 msgid "lbl_groups_container_dn" msgstr "" #. Default: No Groups DN defined -#: /home/plone/project/backend/sources/pas.plugins.ldap/src/pas/plugins/ldap/properties.yaml:163 +#: /home/plone/project/backend/sources/pas.plugins.ldap/src/pas/plugins/ldap/properties.yaml:207 msgid "msg_groups_container_dn" msgstr "" #. Default: Groups search scope -#: /home/plone/project/backend/sources/pas.plugins.ldap/src/pas/plugins/ldap/properties.yaml:168 +#: /home/plone/project/backend/sources/pas.plugins.ldap/src/pas/plugins/ldap/properties.yaml:212 msgid "lbl_groups_search_scope" msgstr "" #. Default: Groups search query filter -#: /home/plone/project/backend/sources/pas.plugins.ldap/src/pas/plugins/ldap/properties.yaml:174 +#: /home/plone/project/backend/sources/pas.plugins.ldap/src/pas/plugins/ldap/properties.yaml:218 msgid "lbl_groups_search_query_filter" msgstr "" #. Default: Object classes for Groups -#: /home/plone/project/backend/sources/pas.plugins.ldap/src/pas/plugins/ldap/properties.yaml:179 +#: /home/plone/project/backend/sources/pas.plugins.ldap/src/pas/plugins/ldap/properties.yaml:223 msgid "lbl_object_classes_for_groups" msgstr "" #. Default: "One of those is mandatory like as one the following options #. groupOfNames, groupOfUniqueNames, posixGroup, group" -#: /home/plone/project/backend/sources/pas.plugins.ldap/src/pas/plugins/ldap/properties.yaml:180 +#: /home/plone/project/backend/sources/pas.plugins.ldap/src/pas/plugins/ldap/properties.yaml:224 msgid "help_object_classes_for_groups" msgstr "" #. Default: Group attribute aliases -#: /home/plone/project/backend/sources/pas.plugins.ldap/src/pas/plugins/ldap/properties.yaml:194 +#: /home/plone/project/backend/sources/pas.plugins.ldap/src/pas/plugins/ldap/properties.yaml:238 msgid "lbl_group_attribute_aliases" msgstr "" #. Default: Group attribute aliases values are mandatory -#: /home/plone/project/backend/sources/pas.plugins.ldap/src/pas/plugins/ldap/properties.yaml:195 +#: /home/plone/project/backend/sources/pas.plugins.ldap/src/pas/plugins/ldap/properties.yaml:239 msgid "msg_group_attribute_aliases" msgstr "" #. Default: LDAP attr name -#: /home/plone/project/backend/sources/pas.plugins.ldap/src/pas/plugins/ldap/properties.yaml:199 +#: /home/plone/project/backend/sources/pas.plugins.ldap/src/pas/plugins/ldap/properties.yaml:243 msgid "head_ldap_attr_name" msgstr "" #. Default: Group Property-Sheet Attributes -#: /home/plone/project/backend/sources/pas.plugins.ldap/src/pas/plugins/ldap/properties.yaml:204 +#: /home/plone/project/backend/sources/pas.plugins.ldap/src/pas/plugins/ldap/properties.yaml:248 msgid "lbl_group_property-sheet_attributes" msgstr "" #. Default: Cache Settings -#: /home/plone/project/backend/sources/pas.plugins.ldap/src/pas/plugins/ldap/properties.yaml:211 +#: /home/plone/project/backend/sources/pas.plugins.ldap/src/pas/plugins/ldap/properties.yaml:255 msgid "lbl_cache_settings" msgstr "" #. Default: Cache LDAP queries -#: /home/plone/project/backend/sources/pas.plugins.ldap/src/pas/plugins/ldap/properties.yaml:218 +#: /home/plone/project/backend/sources/pas.plugins.ldap/src/pas/plugins/ldap/properties.yaml:262 msgid "lbl_cache_ldap_queries" msgstr "" #. Default: Memcached Server to use -#: /home/plone/project/backend/sources/pas.plugins.ldap/src/pas/plugins/ldap/properties.yaml:223 +#: /home/plone/project/backend/sources/pas.plugins.ldap/src/pas/plugins/ldap/properties.yaml:267 msgid "lbl_memcached_server_to_use" msgstr "" #. Default: global - same server for all ldap plugins -#: /home/plone/project/backend/sources/pas.plugins.ldap/src/pas/plugins/ldap/properties.yaml:224 +#: /home/plone/project/backend/sources/pas.plugins.ldap/src/pas/plugins/ldap/properties.yaml:268 msgid "help_memcached_server_to_use" msgstr "" #. Default: Cache timeout in seconds -#: /home/plone/project/backend/sources/pas.plugins.ldap/src/pas/plugins/ldap/properties.yaml:231 +#: /home/plone/project/backend/sources/pas.plugins.ldap/src/pas/plugins/ldap/properties.yaml:275 msgid "lbl_cache_timeout_in_seconds" msgstr "" #. Default: Save -#: /home/plone/project/backend/sources/pas.plugins.ldap/src/pas/plugins/ldap/properties.yaml:240 +#: /home/plone/project/backend/sources/pas.plugins.ldap/src/pas/plugins/ldap/properties.yaml:284 msgid "lbl_save" msgstr "" diff --git a/src/pas/plugins/ldap/locales/pas.plugins.ldap.pot b/src/pas/plugins/ldap/locales/pas.plugins.ldap.pot index 413d683..1e7f558 100644 --- a/src/pas/plugins/ldap/locales/pas.plugins.ldap.pot +++ b/src/pas/plugins/ldap/locales/pas.plugins.ldap.pot @@ -4,7 +4,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" -"POT-Creation-Date: 2025-06-21 20:09+0000\n" +"POT-Creation-Date: 2026-04-29 16:56+0000\n" "PO-Revision-Date: YEAR-MO-DA HO:MI +ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -17,20 +17,32 @@ msgstr "" "Preferred-Encodings: utf-8 latin1\n" "Domain: pas.plugins.ldap\n" +#: pas/plugins/ldap/properties.py:37 +msgid "BASE" +msgstr "" + #: pas/plugins/ldap/plonecontrolpanel/controlpanel.pt:16 #: pas/plugins/ldap/zmi/manage_plugin.pt:28 msgid "Connection Test" msgstr "" -#: pas/plugins/ldap/properties.py:223 +#: pas/plugins/ldap/properties.py:254 +msgid "Connection, users- and groups-access tested successfully." +msgstr "" + +#: pas/plugins/ldap/properties.py:43 +msgid "Days since Epoch" +msgstr "" + +#: pas/plugins/ldap/properties.py:253 msgid "Exception in Groups; " msgstr "" -#: pas/plugins/ldap/properties.py:216 +#: pas/plugins/ldap/properties.py:246 msgid "Exception in Users; " msgstr "" -#: pas/plugins/ldap/configure.zcml:34 +#: pas/plugins/ldap/configure.zcml:29 msgid "Extension profile for pas.plugins.ldap Zope Base." msgstr "" @@ -46,19 +58,19 @@ msgstr "" msgid "LDAP Inspector" msgstr "" -#: pas/plugins/ldap/configure.zcml:34 +#: pas/plugins/ldap/configure.zcml:29 msgid "LDAP Plugin for PAS - Zope 2 Base Installation" msgstr "" -#: pas/plugins/ldap/plonecontrolpanel/controlpanel.py:31 +#: pas/plugins/ldap/plonecontrolpanel/controlpanel.py:43 msgid "LDAP Settings saved." msgstr "" -#: pas/plugins/ldap/properties.py:220 +#: pas/plugins/ldap/properties.py:250 msgid "LDAP Users ok, but groups not; " msgstr "" -#: pas/plugins/ldap/properties.py:213 +#: pas/plugins/ldap/properties.py:243 msgid "LDAP users; " msgstr "" @@ -82,27 +94,39 @@ msgstr "" msgid "No search base selected" msgstr "" -#: pas/plugins/ldap/properties.py:204 +#: pas/plugins/ldap/properties.py:234 msgid "Non-LDAP error while getting ILDAPGroupsConfig!" msgstr "" -#: pas/plugins/ldap/properties.py:192 +#: pas/plugins/ldap/properties.py:222 msgid "Non-LDAP error while getting ILDAPProps!" msgstr "" -#: pas/plugins/ldap/properties.py:198 +#: pas/plugins/ldap/properties.py:228 msgid "Non-LDAP error while getting ILDAPUsersConfig!" msgstr "" -#: pas/plugins/ldap/properties.py:180 +#: pas/plugins/ldap/properties.py:38 +msgid "ONELEVEL" +msgstr "" + +#: pas/plugins/ldap/properties.py:204 msgid "Password is required for non-anonymous connections." msgstr "" +#: pas/plugins/ldap/properties.py:39 +msgid "SUBTREE" +msgstr "" + +#: pas/plugins/ldap/properties.py:44 +msgid "Seconds since epoch" +msgstr "" + #: pas/plugins/ldap/plonecontrolpanel/inspector.pt:13 msgid "Select search base" msgstr "" -#: pas/plugins/ldap/properties.py:211 +#: pas/plugins/ldap/properties.py:241 msgid "Server Down" msgstr "" @@ -114,11 +138,19 @@ msgstr "" msgid "Settings" msgstr "" -#: pas/plugins/ldap/properties.py:185 +#: pas/plugins/ldap/plonecontrolpanel/configure.zcml:27 +msgid "Uninstall LDAP Plugin for PAS" +msgstr "" + +#: pas/plugins/ldap/plonecontrolpanel/configure.zcml:27 +msgid "Uninstalls the pas.plugins.ldap add-on." +msgstr "" + +#: pas/plugins/ldap/properties.py:209 msgid "User/Password are required if not anonymous." msgstr "" -#: pas/plugins/ldap/properties.py:174 +#: pas/plugins/ldap/properties.py:198 msgid "Username is required for non-anonymous connections." msgstr "" @@ -127,23 +159,23 @@ msgid "Users" msgstr "" #. Default: LDAP attr name -#: pas/plugins/ldap/properties.yaml:199 +#: pas/plugins/ldap/properties.yaml:243 msgid "head_ldap_attr_name" msgstr "" #. Default: LDAP Attribute -#: pas/plugins/ldap/properties.yaml:143 +#: pas/plugins/ldap/properties.yaml:187 msgid "head_ldap_attribute" msgstr "" #. Default: Name on Sheet -#: pas/plugins/ldap/properties.yaml:150 +#: pas/plugins/ldap/properties.yaml:194 msgid "head_name_on_sheet" msgstr "" #. Default: Reserved Key #. Default: Reserved key -#: pas/plugins/ldap/properties.yaml:142 +#: pas/plugins/ldap/properties.yaml:186 msgid "head_reserved_key" msgstr "" @@ -155,41 +187,80 @@ msgstr "" #. Default: If set on authenticate a failing certificate chain check including #. CA is ignored. -#: pas/plugins/ldap/properties.yaml:56 +#: pas/plugins/ldap/properties.yaml:58 msgid "help_ignore_certificate_check" msgstr "" +#. Default: The timeout period, in seconds, for waiting for a connection to be +#. established with the LDAP server. +#: pas/plugins/ldap/properties.yaml:34 +msgid "help_ldap_connection_timeout_in_seconds" +msgstr "" + +#. Default: The timeout in seconds for an operation such as a search or update +#. to complete. If no timeout is required, use -1 as the value. +#: pas/plugins/ldap/properties.yaml:41 +msgid "help_ldap_operation_timeout_in_seconds" +msgstr "" + #. Default: "Group DNs outside of the groups base DN are ignored, except if #. listed here" -#: pas/plugins/ldap/properties.yaml:113 +#: pas/plugins/ldap/properties.yaml:156 msgid "help_memberOf_external_allowed_group_dns" msgstr "" #. Default: global - same server for all ldap plugins -#: pas/plugins/ldap/properties.yaml:224 +#: pas/plugins/ldap/properties.yaml:268 msgid "help_memcached_server_to_use" msgstr "" #. Default: "One of those is mandatory like as one the following options #. groupOfNames, groupOfUniqueNames, posixGroup, group" -#: pas/plugins/ldap/properties.yaml:180 +#: pas/plugins/ldap/properties.yaml:224 msgid "help_object_classes_for_groups" msgstr "" #. Default: Maximum page size, number of results to query the server at once #. for. -#: pas/plugins/ldap/properties.yaml:62 +#: pas/plugins/ldap/properties.yaml:94 msgid "help_page_size" msgstr "" #. Default: If your LDAP/AD supports it this will use #. LDAP_MATCHING_RULE_IN_CHAIN. By default only AD supports this. -#: pas/plugins/ldap/properties.yaml:108 +#: pas/plugins/ldap/properties.yaml:151 msgid "help_support_recursive_nested_groups" msgstr "" +#. Default: If set, the LDAP server certificate is checked against the CA +#. certificates in folder. +#: pas/plugins/ldap/properties.yaml:76 +msgid "help_tls_cacertdir" +msgstr "" + +#. Default: If set, the LDAP server certificate is checked against the CA +#. certificate file. +#: pas/plugins/ldap/properties.yaml:70 +msgid "help_tls_cacertfile" +msgstr "" + +#. Default: If set, the client certificate is sent to the server. +#: pas/plugins/ldap/properties.yaml:82 +msgid "help_tls_clcertfile" +msgstr "" + +#. Default: If set, the client certificate is sent to the server. +#: pas/plugins/ldap/properties.yaml:88 +msgid "help_tls_clkeyfile" +msgstr "" + +#. Default: If set, the connection is upgraded to TLS. +#: pas/plugins/ldap/properties.yaml:64 +msgid "help_use_tls_connection" +msgstr "" + #. Default: Account expiration unit -#: pas/plugins/ldap/properties.yaml:132 +#: pas/plugins/ldap/properties.yaml:175 msgid "lbl_account_expiration_unit" msgstr "" @@ -199,22 +270,22 @@ msgid "lbl_anonymous_connection" msgstr "" #. Default: Attribute containing expiration Time -#: pas/plugins/ldap/properties.yaml:127 +#: pas/plugins/ldap/properties.yaml:170 msgid "lbl_attribute_containing_expiration_time" msgstr "" #. Default: Cache LDAP queries -#: pas/plugins/ldap/properties.yaml:218 +#: pas/plugins/ldap/properties.yaml:262 msgid "lbl_cache_ldap_queries" msgstr "" #. Default: Cache Settings -#: pas/plugins/ldap/properties.yaml:211 +#: pas/plugins/ldap/properties.yaml:255 msgid "lbl_cache_settings" msgstr "" #. Default: Cache timeout in seconds -#: pas/plugins/ldap/properties.yaml:231 +#: pas/plugins/ldap/properties.yaml:275 msgid "lbl_cache_timeout_in_seconds" msgstr "" @@ -224,47 +295,47 @@ msgid "lbl_connection_uri" msgstr "" #. Default: Group attribute aliases -#: pas/plugins/ldap/properties.yaml:194 +#: pas/plugins/ldap/properties.yaml:238 msgid "lbl_group_attribute_aliases" msgstr "" #. Default: Group Property-Sheet Attributes -#: pas/plugins/ldap/properties.yaml:204 +#: pas/plugins/ldap/properties.yaml:248 msgid "lbl_group_property-sheet_attributes" msgstr "" #. Default: Groups container DN -#: pas/plugins/ldap/properties.yaml:162 +#: pas/plugins/ldap/properties.yaml:206 msgid "lbl_groups_container_dn" msgstr "" #. Default: Groups search query filter -#: pas/plugins/ldap/properties.yaml:174 +#: pas/plugins/ldap/properties.yaml:218 msgid "lbl_groups_search_query_filter" msgstr "" #. Default: Groups search scope -#: pas/plugins/ldap/properties.yaml:168 +#: pas/plugins/ldap/properties.yaml:212 msgid "lbl_groups_search_scope" msgstr "" #. Default: Groups Settings -#: pas/plugins/ldap/properties.yaml:155 +#: pas/plugins/ldap/properties.yaml:199 msgid "lbl_groups_settings" msgstr "" #. Default: Ignore certificate check? -#: pas/plugins/ldap/properties.yaml:55 +#: pas/plugins/ldap/properties.yaml:57 msgid "lbl_ignore_certificate_check" msgstr "" -#. Default: LDAP connection timeout in seconds +#. Default: LDAP connection timeout in seconds. #: pas/plugins/ldap/properties.yaml:33 msgid "lbl_ldap_connection_timeout_in_seconds" msgstr "" -#. Default: LDAP operation timeout in seconds -#: pas/plugins/ldap/properties.yaml:39 +#. Default: LDAP operation timeout in seconds. +#: pas/plugins/ldap/properties.yaml:40 msgid "lbl_ldap_operation_timeout_in_seconds" msgstr "" @@ -274,92 +345,131 @@ msgid "lbl_ldap_server_settings" msgstr "" #. Default: Manager Password -#: pas/plugins/ldap/properties.yaml:50 +#: pas/plugins/ldap/properties.yaml:52 msgid "lbl_manager_password" msgstr "" #. Default: Manager User -#: pas/plugins/ldap/properties.yaml:45 +#: pas/plugins/ldap/properties.yaml:47 msgid "lbl_manager_user" msgstr "" #. Default: memberOf attribute supported? -#: pas/plugins/ldap/properties.yaml:102 +#: pas/plugins/ldap/properties.yaml:135 msgid "lbl_memberOf_attribute_supported" msgstr "" #. Default: memberOf external allowed Group DNs -#: pas/plugins/ldap/properties.yaml:114 +#: pas/plugins/ldap/properties.yaml:157 msgid "lbl_memberOf_external_allowed_group_dns" msgstr "" #. Default: Memcached Server to use -#: pas/plugins/ldap/properties.yaml:223 +#: pas/plugins/ldap/properties.yaml:267 msgid "lbl_memcached_server_to_use" msgstr "" #. Default: Object class -#: pas/plugins/ldap/properties.yaml:94 +#: pas/plugins/ldap/properties.yaml:126 msgid "lbl_object_class" msgstr "" #. Default: Object classes for Groups -#: pas/plugins/ldap/properties.yaml:179 +#: pas/plugins/ldap/properties.yaml:223 msgid "lbl_object_classes_for_groups" msgstr "" #. Default: Object classes for User creation -#: pas/plugins/ldap/properties.yaml:93 +#: pas/plugins/ldap/properties.yaml:125 msgid "lbl_object_classes_for_user_creation" msgstr "" #. Default: Page Size -#: pas/plugins/ldap/properties.yaml:61 +#: pas/plugins/ldap/properties.yaml:93 msgid "lbl_page_size" msgstr "" +#. Default: Role +#: pas/plugins/ldap/properties.yaml:141 +msgid "lbl_role" +msgstr "" + +#. Default: Roles acquired +#: pas/plugins/ldap/properties.yaml:140 +msgid "lbl_roles_acquired" +msgstr "" + #. Default: Save -#: pas/plugins/ldap/properties.yaml:240 +#: pas/plugins/ldap/properties.yaml:284 msgid "lbl_save" msgstr "" #. Default: Support recursive/nested groups? -#: pas/plugins/ldap/properties.yaml:107 +#: pas/plugins/ldap/properties.yaml:150 msgid "lbl_support_recursive_nested_groups" msgstr "" +#. Default: Path to folder with CA certificate files for TLS communication +#. (OPT_X_TLS_CACERTDIR) +#: pas/plugins/ldap/properties.yaml:75 +msgid "lbl_tls_cacertdir" +msgstr "" + +#. Default: Path to CA certificate file for TLS communication +#. (OPT_X_TLS_CACERTFILE) +#: pas/plugins/ldap/properties.yaml:69 +msgid "lbl_tls_cacertfile" +msgstr "" + +#. Default: Path to client certificate file for TLS communication +#. (OPT_X_TLS_CERTFILE). Requires tls_clkeyfile +#: pas/plugins/ldap/properties.yaml:81 +msgid "lbl_tls_clcertfile" +msgstr "" + +#. Default: Path to client certificate key for TLS communication +#. (OPT_X_TLS_KEYFILE). Requires tls_clcertfile +#: pas/plugins/ldap/properties.yaml:87 +msgid "lbl_tls_clkeyfile" +msgstr "" + +#. Default: Use TLS connection +#: pas/plugins/ldap/properties.yaml:63 +msgid "lbl_use_tls_connection" +msgstr "" + #. Default: User Accounts expires? -#: pas/plugins/ldap/properties.yaml:122 +#: pas/plugins/ldap/properties.yaml:165 msgid "lbl_user_accounts_expires" msgstr "" #. Default: User attribute aliases -#: pas/plugins/ldap/properties.yaml:138 +#: pas/plugins/ldap/properties.yaml:182 msgid "lbl_user_attribute_aliases" msgstr "" #. Default: User Property-Sheet Attributes -#: pas/plugins/ldap/properties.yaml:148 +#: pas/plugins/ldap/properties.yaml:192 msgid "lbl_user_property-sheet_attributes" msgstr "" #. Default: Users container DN -#: pas/plugins/ldap/properties.yaml:76 +#: pas/plugins/ldap/properties.yaml:108 msgid "lbl_users_container_dn" msgstr "" #. Default: Users search query filter -#: pas/plugins/ldap/properties.yaml:88 +#: pas/plugins/ldap/properties.yaml:120 msgid "lbl_users_search_query_filter" msgstr "" #. Default: Users search scope -#: pas/plugins/ldap/properties.yaml:82 +#: pas/plugins/ldap/properties.yaml:114 msgid "lbl_users_search_scope" msgstr "" #. Default: Users Settings -#: pas/plugins/ldap/properties.yaml:69 +#: pas/plugins/ldap/properties.yaml:101 msgid "lbl_users_settings" msgstr "" @@ -369,27 +479,27 @@ msgid "msg_connection_uri" msgstr "" #. Default: Group attribute aliases values are mandatory -#: pas/plugins/ldap/properties.yaml:195 +#: pas/plugins/ldap/properties.yaml:239 msgid "msg_group_attribute_aliases" msgstr "" #. Default: No Groups DN defined -#: pas/plugins/ldap/properties.yaml:163 +#: pas/plugins/ldap/properties.yaml:207 msgid "msg_groups_container_dn" msgstr "" #. Default: 'Page size must be given.' -#: pas/plugins/ldap/properties.yaml:65 +#: pas/plugins/ldap/properties.yaml:97 msgid "msg_page_size" msgstr "" #. Default: User attribute aliases values are mandatory -#: pas/plugins/ldap/properties.yaml:139 +#: pas/plugins/ldap/properties.yaml:183 msgid "msg_user_attribute_aliases" msgstr "" #. Default: No Users DN defined -#: pas/plugins/ldap/properties.yaml:77 +#: pas/plugins/ldap/properties.yaml:109 msgid "msg_users_container_dn" msgstr "" @@ -397,7 +507,7 @@ msgstr "" msgid "pas.plugins.ldap support for users and groups from ldap/active directory." msgstr "" -#: pas/plugins/ldap/plonecontrolpanel/cache.py:28 +#: pas/plugins/ldap/plonecontrolpanel/cache.py:31 #: pas/plugins/ldap/plonecontrolpanel/profiles/default/registry.xml msgid "servers, delimited by space" msgstr "" diff --git a/src/pas/plugins/ldap/properties.py b/src/pas/plugins/ldap/properties.py index b50b2ab..7a35103 100644 --- a/src/pas/plugins/ldap/properties.py +++ b/src/pas/plugins/ldap/properties.py @@ -25,15 +25,18 @@ import ldap - _marker = dict() class BasePropertiesForm(BrowserView): scope_vocab = [ - (str(BASE), "BASE"), - (str(ONELEVEL), "ONELEVEL"), - (str(SUBTREE), "SUBTREE"), + (str(BASE), _("BASE")), + (str(ONELEVEL), _("ONELEVEL")), + (str(SUBTREE), _("SUBTREE")), + ] + account_expiration_unit_vocab = [ + (int(0), _("Days since Epoch")), + (int(1), _("Seconds since epoch")), ] static_attrs_users = ["rdn", "id", "login"] static_attrs_groups = ["rdn", "id"] @@ -225,7 +228,7 @@ def connection_test(self): except Exception as e: logger.exception("Non-LDAP error while connection test!") return False, _("Exception in Groups; ") + str(e) - return True, "Connection, users- and groups-access tested successfully." + return True, _("Connection, users- and groups-access tested successfully.") def propproxy(ckey): diff --git a/src/pas/plugins/ldap/properties.yaml b/src/pas/plugins/ldap/properties.yaml index bdd617d..f79b36e 100644 --- a/src/pas/plugins/ldap/properties.yaml +++ b/src/pas/plugins/ldap/properties.yaml @@ -30,13 +30,15 @@ widgets: factory: '#field:number' value: expr:context.props.conn_timeout props: - label: i18n:lbl_ldap_connection_timeout_in_seconds:LDAP connection timeout in seconds + label: i18n:lbl_ldap_connection_timeout_in_seconds:LDAP connection timeout in seconds. + help: i18n:help_ldap_connection_timeout_in_seconds:The timeout period, in seconds, for waiting for a connection to be established with the LDAP server. datatype: integer - op_timeout: factory: '#field:number' value: expr:context.props.op_timeout props: - label: i18n:lbl_ldap_operation_timeout_in_seconds:LDAP operation timeout in seconds + label: i18n:lbl_ldap_operation_timeout_in_seconds:LDAP operation timeout in seconds. + help: i18n:help_ldap_operation_timeout_in_seconds:The timeout in seconds for an operation such as a search or update to complete. If no timeout is required, use -1 as the value. datatype: integer - user: factory: '#field:text' @@ -58,32 +60,32 @@ widgets: factory: '#field:checkbox' value: expr:context.props.start_tls props: - label: Use TLS connection - help: If set, the connection is upgraded to TLS. + label: i18n:lbl_use_tls_connection:Use TLS connection + help: i18n:help_use_tls_connection:If set, the connection is upgraded to TLS. - tls_cacertfile: factory: '#field:text' value: expr:context.props.tls_cacertfile props: - label: Path to CA certificate file for TLS communication (OPT_X_TLS_CACERTFILE) - help: If set, the LDAP server certificate is checked against the CA certificate file. + label: i18n:lbl_tls_cacertfile:Path to CA certificate file for TLS communication (OPT_X_TLS_CACERTFILE) + help: i18n:help_tls_cacertfile:If set, the LDAP server certificate is checked against the CA certificate file. - tls_cacertdir: factory: '#field:text' value: expr:context.props.tls_cacertdir props: - label: Path to folder with CA certificate files for TLS communication (OPT_X_TLS_CACERTDIR) - help: If set, the LDAP server certificate is checked against the CA certificates in folder. + label: i18n:lbl_tls_cacertdir:Path to folder with CA certificate files for TLS communication (OPT_X_TLS_CACERTDIR) + help: i18n:help_tls_cacertdir:If set, the LDAP server certificate is checked against the CA certificates in folder. - tls_clcertfile: factory: '#field:text' value: expr:context.props.tls_clcertfile props: - label: Path to client certificate file for TLS communication (OPT_X_TLS_CERTFILE). Requires tls_clkeyfile - help: If set, the client certificate is sent to the server. + label: i18n:lbl_tls_clcertfile:Path to client certificate file for TLS communication (OPT_X_TLS_CERTFILE). Requires tls_clkeyfile + help: i18n:help_tls_clcertfile:If set, the client certificate is sent to the server. - tls_clkeyfile: factory: '#field:text' value: expr:context.props.tls_clkeyfile props: - label: Path to client certificate key for TLS communication (OPT_X_TLS_KEYFILE). Requires tls_clcertfile - help: If set, the client certificate is sent to the server. + label: i18n:lbl_tls_clkeyfile:Path to client certificate key for TLS communication (OPT_X_TLS_KEYFILE). Requires tls_clcertfile + help: i18n:help_tls_clkeyfile:If set, the client certificate is sent to the server. - page_size: factory: '#field:number' value: expr:context.props.page_size @@ -135,8 +137,8 @@ widgets: factory: '#array' value: expr:context.props.roles props: - label: Roles aquired - array.label: Roles acquired + label: i18n:lbl_roles_acquired:Roles acquired + array.label: i18n:lbl_role:Role widgets: - roles: factory: field:text @@ -171,7 +173,7 @@ widgets: value: expr:context.users.expiresUnit props: label: i18n:lbl_account_expiration_unit:Account expiration unit - vocabulary: expr:((0, 'Days since Epoch'), (1, 'Seconds since epoch')) + vocabulary: expr:context.account_expiration_unit_vocab - aliases_attrmap: factory: '#field:dict' value: expr:context.users_attrmap From 373d126afe1c0bc0305617b0c3c1a0380af45daf Mon Sep 17 00:00:00 2001 From: "Leonardo J. Caballero G" Date: Thu, 30 Apr 2026 10:16:45 +0200 Subject: [PATCH 58/74] Added more improvements about QA validations --- src/pas/plugins/ldap/__init__.py | 1 - src/pas/plugins/ldap/cache.py | 49 ++++++++- src/pas/plugins/ldap/defaults.py | 3 +- src/pas/plugins/ldap/interfaces.py | 2 + src/pas/plugins/ldap/locales/__main__.py | 5 +- src/pas/plugins/ldap/monkey.py | 9 ++ .../plugins/ldap/plonecontrolpanel/cache.py | 7 +- .../ldap/plonecontrolpanel/controlpanel.py | 19 ++++ .../ldap/plonecontrolpanel/exportimport.py | 43 +++++++- .../ldap/plonecontrolpanel/inspector.py | 9 ++ .../profiles/uninstall/controlpanel.xml | 5 +- .../profiles/uninstall/registry.xml | 9 +- .../ldap/plonecontrolpanel/setuphandlers.py | 16 +-- src/pas/plugins/ldap/plugin.py | 24 ++++- src/pas/plugins/ldap/properties.py | 102 ++++++++++++++++-- src/pas/plugins/ldap/setuphandlers.py | 5 +- src/pas/plugins/ldap/sheet.py | 13 ++- 17 files changed, 292 insertions(+), 29 deletions(-) diff --git a/src/pas/plugins/ldap/__init__.py b/src/pas/plugins/ldap/__init__.py index fdaa9f2..9d2c3fb 100644 --- a/src/pas/plugins/ldap/__init__.py +++ b/src/pas/plugins/ldap/__init__.py @@ -12,7 +12,6 @@ import logging import os - PACKAGE_NAME = "pas.plugins.ldap" _ = MessageFactory(PACKAGE_NAME) diff --git a/src/pas/plugins/ldap/cache.py b/src/pas/plugins/ldap/cache.py index bd1e5ae..1fb0dc0 100644 --- a/src/pas/plugins/ldap/cache.py +++ b/src/pas/plugins/ldap/cache.py @@ -1,3 +1,5 @@ +"""Cache management for the LDAP plugin.""" + from .interfaces import ICacheSettingsRecordProvider from .interfaces import ILDAPPlugin from .interfaces import IPluginCacheHandler @@ -16,6 +18,11 @@ class PasLdapMemcached(Memcached): + """Memcached client for LDAP plugin. + + Args: + servers: The list of memcached servers. + """ _servers = None @@ -25,9 +32,11 @@ def __init__(self, servers): @property def servers(self): + """Get the list of memcached servers.""" return self._servers def disconnect_all(self): + """Disconnect all memcached connections.""" self._client.disconnect_all() def __repr__(self): @@ -36,16 +45,23 @@ def __repr__(self): @implementer(ICacheProviderFactory) class cacheProviderFactory: - # memcache factory for node.ext.ldap + """Cache provider factory for LDAP plugin. + + memcache factory for node.ext.ldap + """ + # thread local to store memcached instance per thread for thread safety _thread_local = threading.local() @property def _key(self): + """Key for storing the memcached instance on the thread local.""" return f"_v_{self.__class__.__name__}_PasLdapMemcached" @property def servers(self): + """Get the list of memcached servers from the cache settings + record provider.""" recordProvider = queryUtility(ICacheSettingsRecordProvider) if not recordProvider: return "" @@ -55,6 +71,7 @@ def servers(self): @property def cache(self): + """Get the cache instance for the current thread.""" servers = self.servers if not servers: return NullCache() @@ -85,6 +102,7 @@ def __call__(self): def get_plugin_cache(context): + """Get the plugin cache for the given context.""" if not context.plugin_caching: # bypass for testing return NullPluginCache(context) @@ -96,23 +114,35 @@ def get_plugin_cache(context): @implementer(IPluginCacheHandler) class NullPluginCache: + """Null plugin cache for LDAP plugin.""" + def __init__(self, context): self.context = context def get(self): + """Get the value from the cache, always returning VALUE_NOT_CACHED.""" return VALUE_NOT_CACHED def set(self, value): + """Set the value in the cache, does nothing for NullPluginCache.""" pass @implementer(IPluginCacheHandler) class RequestPluginCache: + """Request-based plugin cache for LDAP plugin. + + Args: + context: The context for the cache. + """ + def __init__(self, context): self.context = context self._key = f"_v_ldap_ugm_{self.context.getId()}_" def getRootRequest(self): + """Get the root request from the current request.""" + def parent_request(current_request): preq = current_request.get("PARENT_REQUEST", None) if preq: @@ -122,14 +152,18 @@ def parent_request(current_request): return parent_request(getRequest()) def get(self): + """Get the value from the cache by looking it up on the request.""" return (self.getRootRequest() or {}).get(self._key, VALUE_NOT_CACHED) def set(self, value): + """Set the value in the cache by storing it on the request.""" request = self.getRootRequest() if request is not None: request[self._key] = value def invalidate(self): + """Invalidate the cache by removing the cached value + from the request.""" request = self.getRootRequest() if request and self._key in list(request.keys()): del request[self._key] @@ -140,7 +174,16 @@ def invalidate(self): @adapter(ILDAPPlugin) class VolatilePluginCache(RequestPluginCache): + """Volatile plugin ºcache for LDAP plugin. + + Args: + RequestPluginCache (object): Request-based plugin cache for + LDAP plugin. + """ + def get(self): + """Get the value from the cache by looking it up on the request and + checking if it is still valid based on the max age.""" try: cachetime, value = getattr(self.context, self._key) except AttributeError: @@ -150,9 +193,13 @@ def get(self): return value def set(self, value): + """Set the value in the cache by storing it on the context + with a timestamp.""" setattr(self.context, self._key, (time.time(), value)) def invalidate(self): + """Invalidate the cache by removing the cached value + from the context.""" try: delattr(self.context, self._key) except AttributeError: diff --git a/src/pas/plugins/ldap/defaults.py b/src/pas/plugins/ldap/defaults.py index a069785..4ac3ae8 100644 --- a/src/pas/plugins/ldap/defaults.py +++ b/src/pas/plugins/ldap/defaults.py @@ -1,5 +1,6 @@ -from node.ext.ldap.scope import ONELEVEL +"""Default configuration values for the LDAP plugin.""" +from node.ext.ldap.scope import ONELEVEL DEFAULTS = { "server.uri": "ldap://127.0.0.1:12345", diff --git a/src/pas/plugins/ldap/interfaces.py b/src/pas/plugins/ldap/interfaces.py index f25cc41..d7adaf9 100644 --- a/src/pas/plugins/ldap/interfaces.py +++ b/src/pas/plugins/ldap/interfaces.py @@ -1,3 +1,5 @@ +"""Interfaces for the LDAP plugin.""" + from zope.interface import Interface diff --git a/src/pas/plugins/ldap/locales/__main__.py b/src/pas/plugins/ldap/locales/__main__.py index c4b7125..ad5ca75 100644 --- a/src/pas/plugins/ldap/locales/__main__.py +++ b/src/pas/plugins/ldap/locales/__main__.py @@ -6,7 +6,6 @@ import re import subprocess - logger = logging.getLogger("i18n") logger.setLevel(logging.DEBUG) @@ -55,7 +54,7 @@ def locale_folder_setup(domain: str): def _rebuild(domain: str): - """Rebuild pot file + """Rebuild the pot file from the source code Args: domain (str): locale domain application @@ -106,7 +105,7 @@ def _sync(domain: str): def main(): """Main application function""" if domain: - logger.info(f"Updating translations for {domain}") + logger.info("Updating translations for %s", domain) i18n_script_setup() locale_folder_setup(domain) _rebuild(domain) diff --git a/src/pas/plugins/ldap/monkey.py b/src/pas/plugins/ldap/monkey.py index 4da75cf..c0b75b6 100644 --- a/src/pas/plugins/ldap/monkey.py +++ b/src/pas/plugins/ldap/monkey.py @@ -1,3 +1,5 @@ +"""Monkey patch to support user portraits stored in property sheets.""" + # TEMPORARY MONKEY PATCH # until this is changed upstream! from Acquisition import aq_inner @@ -13,7 +15,10 @@ class PortraitImage(Image): + """An image class for user portraits.""" + def getPhysicalPath(self): + """Get the physical path for the portrait image.""" parent = aq_parent(aq_inner(self)) trav = "++portrait++%s" % self.id() if not hasattr(parent, "getPhysicalPath"): @@ -22,6 +27,7 @@ def getPhysicalPath(self): def getPortraitFromSheet(context, userid): + """Get the portrait image for a user from their property sheet.""" mtool = getToolByName(context, "portal_membership") member = mtool.getMemberById(userid) if not member: @@ -46,11 +52,14 @@ def getPortraitFromSheet(context, userid): @implementer(ITraversable) class PortraitTraverser: + """A traverser for user portraits.""" + def __init__(self, context, request=None): self.context = context self.request = request def traverse(self, userid, subpath): + """Traverse to the portrait image for a user.""" return getPortraitFromSheet(self.context, userid).__of__(self.context) diff --git a/src/pas/plugins/ldap/plonecontrolpanel/cache.py b/src/pas/plugins/ldap/plonecontrolpanel/cache.py index f752ed0..7015df3 100644 --- a/src/pas/plugins/ldap/plonecontrolpanel/cache.py +++ b/src/pas/plugins/ldap/plonecontrolpanel/cache.py @@ -1,3 +1,5 @@ +"""Cache settings record provider for plone registry""" + from ..interfaces import ICacheSettingsRecordProvider from pas.plugins.ldap import _ from persistent import Persistent @@ -7,16 +9,19 @@ from zope.component import queryUtility from zope.interface import implementer - REGKEY = "pas.plugins.ldap.memcached" class NullRecord: + """A null record that returns an empty string for its value.""" + value = "" @implementer(ICacheSettingsRecordProvider) class CacheSettingsRecordProvider(Persistent): + """Provides a registry record for LDAP cache settings.""" + def __call__(self): registry = queryUtility(IRegistry) if not registry: diff --git a/src/pas/plugins/ldap/plonecontrolpanel/controlpanel.py b/src/pas/plugins/ldap/plonecontrolpanel/controlpanel.py index c947f4a..a9ab22c 100644 --- a/src/pas/plugins/ldap/plonecontrolpanel/controlpanel.py +++ b/src/pas/plugins/ldap/plonecontrolpanel/controlpanel.py @@ -1,3 +1,5 @@ +"""Control panel for LDAP plugin""" + from ..properties import BasePropertiesForm from pas.plugins.ldap import _ from Products.CMFCore.interfaces import ISiteRoot @@ -7,15 +9,26 @@ def getPortal(): + """Get the Plone portal object.""" return getUtility(ISiteRoot) class LDAPControlPanel(BasePropertiesForm): + """Control panel for managing LDAP settings.""" + def __init__(self, context, request): super().__init__(context, request) add_bundle_on_request(request, "yafowil") def next(self, request): + """Next + + Args: + request (object): Request object + + Returns: + str: Absolute URL String + """ return f"{self.context.absolute_url()}/plone_ldapcontrolpanel" @property @@ -26,6 +39,12 @@ def plugin(self): return aclu.pasldap def save(self, widget, data): + """Save the LDAP setting. + + Args: + widget (Widget): Widget instance + data (Data): Data extracted from the form + """ super().save(widget, data) messages = IStatusMessage(self.request) messages.addStatusMessage(_("LDAP Settings saved."), type="info") diff --git a/src/pas/plugins/ldap/plonecontrolpanel/exportimport.py b/src/pas/plugins/ldap/plonecontrolpanel/exportimport.py index cb398fe..c018f3d 100644 --- a/src/pas/plugins/ldap/plonecontrolpanel/exportimport.py +++ b/src/pas/plugins/ldap/plonecontrolpanel/exportimport.py @@ -1,3 +1,5 @@ +"""Import and export handlers for LDAP settings in Plone's control panel.""" + from BTrees.OOBTree import OOBTree from pas.plugins.ldap import PACKAGE_NAME from Products.GenericSetup.interfaces import IBody @@ -7,8 +9,19 @@ def _get_import_export_handler(context): + """Get the import/export handler for LDAP settings. + + Args: + context (object): The context providing access to the site and logging. + + Returns: + IBody: The import/export handler for LDAP settings, or None + if not found. + """ + # get the acl_users from the site aclu = context.getSite().acl_users logger = context.getLogger(PACKAGE_NAME) + # check if the LDAP plugin is installed if "pasldap" not in aclu.objectIds(): return pasldap = aclu.pasldap @@ -20,6 +33,12 @@ def _get_import_export_handler(context): def import_settings(context): + """Import LDAP settings from an XML file. + + Args: + context (object): The import context, which provides access + to the site and logging. + """ logger = context.getLogger(PACKAGE_NAME) handler = _get_import_export_handler(context) if not handler: @@ -32,6 +51,12 @@ def import_settings(context): def export_settings(context): + """Export LDAP settings to an XML file. + + Args: + context (object): The export context, which provides access + to the site and logging. + """ handler = _get_import_export_handler(context) if not handler: return @@ -45,16 +70,23 @@ def export_settings(context): @implementer(IBody) class LDAPPluginXMLAdapter(XMLAdapterBase): - """import pas groups from ldap config.""" + """Import PAS groups from LDAP config.""" name = "ldapsettings" def _exportNode(self): + """Export LDAP settings to an XML node. + + Returns: + xml.dom.minidom.Element: The XML node representing + the LDAP settings. + """ node = self._getObjectNode("object") self._setDataAndType(self.context.settings, node) return node def _importNode(self, node): + """Import LDAP settings from an XML node.""" data = self._getDataByType(node) if not data: self._logger.error("data is empty") @@ -63,6 +95,12 @@ def _importNode(self, node): self.context.settings[key] = data[key] def _setDataAndType(self, data, node): + """Set the data and type attributes for an XML node. + + Args: + data (any): The data to be set. + node (xml.dom.minidom.Element): The XML node to set the data on. + """ if isinstance(data, (tuple, list)): node.setAttribute("type", "list") for value in data: @@ -100,6 +138,7 @@ def _setDataAndType(self, data, node): node.appendChild(child) def _getDataByType(self, node): + """Get the data from an XML node based on its type attribute.""" vtype = node.getAttribute("type") if vtype == "list": data = list() @@ -115,7 +154,7 @@ def _getDataByType(self, node): continue key = element.getAttribute("key") if key is None: - self._logger.warning("No key found for dict on import, " "skipped.") + self._logger.warning("No key found for dict on import, skipped.") continue data.update({key: self._getDataByType(element)}) return data diff --git a/src/pas/plugins/ldap/plonecontrolpanel/inspector.py b/src/pas/plugins/ldap/plonecontrolpanel/inspector.py index 1622b56..10ed6a8 100644 --- a/src/pas/plugins/ldap/plonecontrolpanel/inspector.py +++ b/src/pas/plugins/ldap/plonecontrolpanel/inspector.py @@ -1,3 +1,5 @@ +"""View for inspecting LDAP directory structure and attributes for debugging.""" + from node.ext.ldap import LDAPNode from node.ext.ldap.interfaces import ILDAPGroupsConfig from node.ext.ldap.interfaces import ILDAPProps @@ -11,12 +13,16 @@ def safe_encode(val): + """Encode a value to bytes if it's a string, otherwise return it as is.""" if isinstance(val, str): return val.encode("utf-8") return val class LDAPInspector(BrowserView): + """A view to inspect the LDAP directory structure and attributes for + debugging purposes.""" + @property def plugin(self): portal = getUtility(ISiteRoot) @@ -26,9 +32,11 @@ def plugin(self): @property def props(self): + """Get the LDAP properties from the plugin.""" return ILDAPProps(self.plugin) def users_children(self): + """Get the children of the LDAP users container.""" users = ILDAPUsersConfig(self.plugin) return self.children(users.baseDN) @@ -37,6 +45,7 @@ def groups_children(self): return self.children(groups.baseDN) def node_attributes(self): + """Get the attributes of the LDAP node specified by the DN in the request.""" dn = self.request["dn"] base = self.request["base"] if base == "users": diff --git a/src/pas/plugins/ldap/plonecontrolpanel/profiles/uninstall/controlpanel.xml b/src/pas/plugins/ldap/plonecontrolpanel/profiles/uninstall/controlpanel.xml index 513aee9..9facb4f 100644 --- a/src/pas/plugins/ldap/plonecontrolpanel/profiles/uninstall/controlpanel.xml +++ b/src/pas/plugins/ldap/plonecontrolpanel/profiles/uninstall/controlpanel.xml @@ -1,6 +1,7 @@ - + + remove="True" + > diff --git a/src/pas/plugins/ldap/plonecontrolpanel/profiles/uninstall/registry.xml b/src/pas/plugins/ldap/plonecontrolpanel/profiles/uninstall/registry.xml index 7e43441..43caa16 100644 --- a/src/pas/plugins/ldap/plonecontrolpanel/profiles/uninstall/registry.xml +++ b/src/pas/plugins/ldap/plonecontrolpanel/profiles/uninstall/registry.xml @@ -1,9 +1,14 @@ + - + - + diff --git a/src/pas/plugins/ldap/plonecontrolpanel/setuphandlers.py b/src/pas/plugins/ldap/plonecontrolpanel/setuphandlers.py index 75c6996..b461bba 100644 --- a/src/pas/plugins/ldap/plonecontrolpanel/setuphandlers.py +++ b/src/pas/plugins/ldap/plonecontrolpanel/setuphandlers.py @@ -1,8 +1,9 @@ -# -*- coding: utf-8 -*- -from zope.component.hooks import getSite +"""Setup handlers for the LDAP plugin.""" + from pas.plugins.ldap.plugin import LDAPPlugin -import logging +from zope.component.hooks import getSite +import logging logger = logging.getLogger(__name__) @@ -11,14 +12,17 @@ def _removePlugin(pas, PLUGIN_ID="pasldap"): + """Remove the LDAP plugin from the given PAS instance.""" + # get the list of installed plugins installed = pas.objectIds() + # check if plugin is installed if PLUGIN_ID not in installed: return TITLE + " already uninstalled." plugin = getattr(pas, PLUGIN_ID) if not isinstance(plugin, LDAPPlugin): logger.warning( - "Uninstall aborted. PAS plugin %s is not an LDAPPlugin.", - PLUGIN_ID) + "Uninstall aborted. PAS plugin %s is not an LDAPPlugin.", PLUGIN_ID + ) for info in pas.plugins.listPluginTypeInfo(): interface = info["interface"] if not interface.providedBy(plugin): @@ -33,7 +37,7 @@ def _removePlugin(pas, PLUGIN_ID="pasldap"): def uninstall(context): + """Uninstall pas.plugins.ldap.""" site = getSite() pas = site.acl_users _removePlugin(pas) - \ No newline at end of file diff --git a/src/pas/plugins/ldap/plugin.py b/src/pas/plugins/ldap/plugin.py index 8385f9c..5691c54 100644 --- a/src/pas/plugins/ldap/plugin.py +++ b/src/pas/plugins/ldap/plugin.py @@ -1,3 +1,5 @@ +"""LDAP plugin for Plone PAS.""" + from .cache import get_plugin_cache from .interfaces import ILDAPPlugin from .interfaces import VALUE_NOT_CACHED @@ -24,7 +26,6 @@ import six import time - logger = logging.getLogger("pas.plugins.ldap") zmidir = os.path.join(os.path.dirname(__file__), "zmi") @@ -63,7 +64,10 @@ def ldap_error_handler(prefix, default=None): """decorator, deals with non-working LDAP""" def _decorator(original_method, *args, **kwargs): + """Decorator that wraps a method to handle LDAP errors gracefully.""" + def _wrapper(self, *args, **kwargs): + """Wrapper function that handles LDAP errors and logs them.""" # look if error is in timeout phase if hasattr(self, "_v_ldaperror_timeout"): waiting = time.time() - self._v_ldaperror_timeout @@ -145,6 +149,7 @@ def init_settings(self): @security.private def is_plugin_active(self, iface): + """Check if the plugin is active for a given interface.""" pas = self._getPAS() ids = pas.plugins.listPluginIds(iface) return self.getId() in ids @@ -152,18 +157,22 @@ def is_plugin_active(self, iface): @property @security.private def groups_enabled(self): + """Check if group enumeration is enabled.""" return self.groups is not None @property @security.private def users_enabled(self): + """Check if user enumeration is enabled.""" return self.users is not None @property def _ldap_props(self): + """Get the LDAP properties from the plugin.""" return ILDAPProps(self) def _ugm(self): + """Get the user and group management object, using caching if enabled.""" plugin_cache = get_plugin_cache(self) ugm = plugin_cache.get() if ugm is not VALUE_NOT_CACHED: @@ -178,17 +187,20 @@ def _ugm(self): @ldap_error_handler("groups") @security.private def groups(self): + """Get the groups from the LDAP plugin.""" return self._ugm().groups @property @ldap_error_handler("users") @security.private def users(self): + """Get the users from the LDAP plugin.""" return self._ugm().users @property @security.protected(ManageUsers) def ldaperror(self): + """Get the current LDAP error message, if any.""" if hasattr(self, "_v_ldaperror_msg"): waiting = time.time() - self._v_ldaperror_timeout if waiting < LDAP_ERROR_LOG_TIMEOUT: @@ -197,6 +209,7 @@ def ldaperror(self): @security.public # really public?? def reset(self): + """Reset the plugin cache, if caching is enabled.""" # XXX flush caches pass @@ -438,6 +451,15 @@ def enumerateUsers( # pas_interfaces.plugins.IRolesPlugin # def getRolesForPrincipal(self, principal, request=None): + """Get the roles for a principal. + + Args: + principal (object): The principal object. + request (object, optional): The request object. Defaults to None. + + Returns: + tuple: A tuple of roles for the principal. + """ default = () users = self.users if not users: diff --git a/src/pas/plugins/ldap/properties.py b/src/pas/plugins/ldap/properties.py index 7a35103..9a29e7a 100644 --- a/src/pas/plugins/ldap/properties.py +++ b/src/pas/plugins/ldap/properties.py @@ -1,3 +1,5 @@ +"""Properties and configuration for the LDAP plugin.""" + from .defaults import DEFAULTS from .interfaces import ICacheSettingsRecordProvider from .interfaces import ILDAPPlugin @@ -29,11 +31,18 @@ class BasePropertiesForm(BrowserView): + """Base class for LDAP properties forms.""" + + # scope vocabulary, used in the form to provide options for the LDAP search + # scope. The values represent the respective LDAP search scope constants. scope_vocab = [ (str(BASE), _("BASE")), (str(ONELEVEL), _("ONELEVEL")), (str(SUBTREE), _("SUBTREE")), ] + # account expiration unit vocabulary, used in the form to provide options + # for the expiration unit of user accounts. The values represent the number + # of seconds in the respective unit. account_expiration_unit_vocab = [ (int(0), _("Days since Epoch")), (int(1), _("Seconds since epoch")), @@ -43,16 +52,24 @@ class BasePropertiesForm(BrowserView): @property def plugin(self): + """Get the LDAP plugin instance.""" raise NotImplementedError() def next(self, request): + """Get the next URL for redirection after form submission.""" raise NotImplementedError() @property def action(self): + """Get the form action URL.""" return self.next({}) def form(self): + """Render the LDAP properties form. + + Returns: + str: Rendered HTML of the form + """ # make configuration data available on form context try: self.props = ILDAPProps(self.plugin) @@ -90,6 +107,12 @@ def form(self): return "" def save(self, widget, data): + """Save the LDAP properties form. + + Args: + widget (Widget): Widget instance + data (Data): Data extracted from the form + """ props = ILDAPProps(self.plugin) users = ILDAPUsersConfig(self.plugin) groups = ILDAPGroupsConfig(self.plugin) @@ -115,11 +138,11 @@ def fetch(name, default=UNSET): props.user = "" props.password = "" props.ignore_cert = fetch("server.ignore_cert") - props.start_tls = fetch('server.start_tls') - props.tls_cacertfile = fetch('server.tls_cacertfile') - props.tls_cacertdir = fetch('server.tls_cacertdir') - props.tls_clcertfile = fetch('server.tls_clcertfile') - props.tls_clkeyfile = fetch('server.tls_clkeyfile') + props.start_tls = fetch("server.start_tls") + props.tls_cacertfile = fetch("server.tls_cacertfile") + props.tls_cacertdir = fetch("server.tls_cacertdir") + props.tls_clcertfile = fetch("server.tls_clcertfile") + props.tls_clkeyfile = fetch("server.tls_clkeyfile") # TODO: later # props.retry_max = fetch(at('server.retry_max') # props.retry_delay = fetch('server.retry_delay') @@ -130,10 +153,12 @@ def fetch(name, default=UNSET): props.memcached = fetch("cache.memcached") props.timeout = fetch("cache.timeout") - props.roles = fetch("users.roles") # a server wide variable, but related to user + props.roles = fetch( + "users.roles" + ) # a server wide variable, but related to user users.baseDN = fetch("users.dn") - # build attrmap from static keys and dynamic keys inputs + # build attrmap from static keys and dynamic keys inputs users.attrmap = odict() users.attrmap.update(fetch("users.aliases_attrmap")) users_propsheet_attrmap = fetch("users.propsheet_attrmap") @@ -173,6 +198,13 @@ def fetch(name, default=UNSET): groups.memberOfExternalGroupDNs = [] def userpassanon_extractor(self, widget, data): + """Extract the user, password and anonymous values from + the form data. + + Args: + widget (Widget): Widget instance + data (Data): Data extracted from the form + """ if not data.extracted or data["anonymous"].extracted: return data.extracted has_error = False @@ -193,6 +225,12 @@ def userpassanon_extractor(self, widget, data): return data.extracted def connection_test(self): + """Test the LDAP connection. + + Returns: + tuple: A tuple containing a boolean indicating success and + a message string. + """ try: props = ILDAPProps(self.plugin) except Exception as e: @@ -232,11 +270,31 @@ def connection_test(self): def propproxy(ckey): + """Create a property proxy for LDAP plugin settings. + + Args: + ckey (str): The key for the LDAP plugin setting. + """ + def _getter(context): + """Get a property proxy for LDAP plugin settings + + Args: + context (object): Context object + + Returns: + object: The value of the LDAP plugin setting for the given key. + """ value = context.plugin.settings.get(ckey, DEFAULTS[ckey]) return value def _setter(context, value): + """Set a property proxy for LDAP plugin settings + + Args: + context (object): Context object + value (object): Value to set for the LDAP plugin setting. + """ context.plugin.settings[ckey] = value return property(_getter, _setter) @@ -245,6 +303,8 @@ def _setter(context, value): @implementer(ILDAPProps) @adapter(ILDAPPlugin) class LDAPProps: + """Properties for LDAP plugin.""" + def __init__(self, plugin): self.plugin = plugin @@ -272,6 +332,12 @@ def __init__(self, plugin): @property def memcached(self): + """Get the memcached setting. + + Returns: + str: The memcached setting value or a message if the feature + is not available. + """ recordProvider = queryUtility(ICacheSettingsRecordProvider) if recordProvider is not None: record = recordProvider() @@ -280,6 +346,14 @@ def memcached(self): @memcached.setter def memcached(self, value): + """Set the memcached setting. + + Args: + value (object): Value to set for memcached setting. + + Returns: + str: The result of setting the memcached value. + """ recordProvider = queryUtility(ICacheSettingsRecordProvider) if recordProvider is not None: record = recordProvider() @@ -294,6 +368,8 @@ def memcached(self, value): @implementer(ILDAPUsersConfig) @adapter(ILDAPPlugin) class UsersConfig: + """Configuration for LDAP users.""" + def __init__(self, plugin): self.plugin = plugin @@ -314,16 +390,28 @@ def __init__(self, plugin): @property def expiresAttr(self): + """Expires attribute + + Returns: + str: The expiration attribute. + """ return self.account_expiration and self._expiresAttr or None @property def expiresUnit(self): + """Expires unit + + Returns: + int: The expiration unit. + """ return self.account_expiration and self._expiresUnit or 0 @implementer(ILDAPGroupsConfig) @adapter(ILDAPPlugin) class GroupsConfig: + """Configuration for LDAP groups.""" + def __init__(self, plugin): self.plugin = plugin diff --git a/src/pas/plugins/ldap/setuphandlers.py b/src/pas/plugins/ldap/setuphandlers.py index 74127d4..3e9822c 100644 --- a/src/pas/plugins/ldap/setuphandlers.py +++ b/src/pas/plugins/ldap/setuphandlers.py @@ -1,7 +1,8 @@ +"""Setup handlers for the LDAP plugin.""" + from .plugin import LDAPPlugin from zope.component.hooks import getSite - TITLE = "LDAP plugin (pas.plugins.ldap)" @@ -26,6 +27,7 @@ def remove_persistent_import_step(context): def _addPlugin(pas, pluginid="pasldap"): + """Add the LDAP plugin to the given PAS instance.""" installed = pas.objectIds() if pluginid in installed: return TITLE + " already installed." @@ -43,6 +45,7 @@ def _addPlugin(pas, pluginid="pasldap"): def post_install(context): + """Post install script for pas.plugins.ldap.""" site = getSite() pas = site.acl_users _addPlugin(pas) diff --git a/src/pas/plugins/ldap/sheet.py b/src/pas/plugins/ldap/sheet.py index 142c7ef..e13a50b 100644 --- a/src/pas/plugins/ldap/sheet.py +++ b/src/pas/plugins/ldap/sheet.py @@ -1,3 +1,5 @@ +"""Property sheet for LDAP users and groups.""" + from Acquisition import aq_base from node.ext.ldap.interfaces import ILDAPGroupsConfig from node.ext.ldap.interfaces import ILDAPUsersConfig @@ -8,12 +10,18 @@ import logging - logger = logging.getLogger("pas.plugins.ldap") @implementer(IMutablePropertySheet) class LDAPUserPropertySheet(UserPropertySheet): + """A property sheet for LDAP users and groups. + + This property sheet is used to store the properties of LDAP users and + groups. It is used by the LDAP plugin to store the properties of LDAP + users and groups in the PAS user and group objects. + """ + def __init__(self, principal, plugin): """Instanciate LDAPUserPropertySheet. @@ -59,9 +67,11 @@ def _get_ldap_principal(self): return ldap_principals[self._ldapprincipal_id] def canWriteProperty(self, obj, id): + """Check if the property can be written to.""" return id in self._properties def setProperty(self, obj, id, value): + """Set a property value.""" assert id in self._properties ldapprincipal = self._get_ldap_principal() self._properties[id] = ldapprincipal.attrs[id] = value @@ -72,6 +82,7 @@ def setProperty(self, obj, id, value): logger.error("LDAPUserPropertySheet.setProperty: %s" % str(e)) def setProperties(self, obj, mapping): + """Set multiple property values.""" for id in mapping: assert id in self._properties ldapprincipal = self._get_ldap_principal() From 6ab8beb1173aa813c2b616a5d122751feca243d4 Mon Sep 17 00:00:00 2001 From: "Leonardo J. Caballero G" Date: Thu, 30 Apr 2026 10:23:07 +0200 Subject: [PATCH 59/74] Updated the README file --- README.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.rst b/README.rst index 870cedd..c78879c 100644 --- a/README.rst +++ b/README.rst @@ -12,9 +12,9 @@ It works in a plain Zope even if it depends on `PlonePAS `_ is installed an integration layer with a setup-profile and a plone-controlpanel page is available. -``pas.plugins.ldap`` is **not** releated to the old `LDAPUserFolder `_/ `LDAPMultiPlugins `_ and the packages (i.e. `PloneLDAP `_) stacked on top of it in any way. +``pas.plugins.ldap`` is **not** releated to the old `LDAPUserFolder `_ / `LDAPMultiPlugins `_ and the packages (i.e. `PloneLDAP `_) stacked on top of it in any way. -It is based on **`node.ext.ldap `_**, an almost framework independent LDAP stack. +It is based on `node.ext.ldap `_, an almost framework independent LDAP stack. For now users and groups can't be added or deleted. Properties on both are read/write. From 7a4d0565ca1255a48a94c43cd78597ad79288304 Mon Sep 17 00:00:00 2001 From: "Leonardo J. Caballero G" Date: Fri, 1 May 2026 18:09:02 +0200 Subject: [PATCH 60/74] Updated the README file --- README.rst | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/README.rst b/README.rst index c78879c..a9704d6 100644 --- a/README.rst +++ b/README.rst @@ -1,3 +1,16 @@ + +.. image:: https://img.shields.io/pypi/v/pas.plugins.ldap.svg + :target: https://pypi.python.org/pypi/pas.plugins.ldap + :alt: Latest PyPI version + +.. image:: https://img.shields.io/pypi/dm/pas.plugins.ldap.svg + :target: https://pypi.python.org/pypi/pas.plugins.ldap + :alt: Number of PyPI downloads + +.. image:: https://github.com/conestack/pas.plugins.ldap/actions/workflows/tests.yml/badge.svg + :target: https://github.com/conestack/pas.plugins.ldap/actions/workflows/tests.yml + :alt: Test pas.plugins.ldap + .. image:: https://secure.travis-ci.org/collective/pas.plugins.ldap.png :target: http://travis-ci.org/collective/pas.plugins.ldap From f3cd8dc7ed4432645d5555b54364a8dc106d8062 Mon Sep 17 00:00:00 2001 From: "Leonardo J. Caballero G" Date: Fri, 1 May 2026 18:11:25 +0200 Subject: [PATCH 61/74] Updated the README file --- README.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.rst b/README.rst index a9704d6..e6cbb98 100644 --- a/README.rst +++ b/README.rst @@ -7,8 +7,8 @@ :target: https://pypi.python.org/pypi/pas.plugins.ldap :alt: Number of PyPI downloads -.. image:: https://github.com/conestack/pas.plugins.ldap/actions/workflows/tests.yml/badge.svg - :target: https://github.com/conestack/pas.plugins.ldap/actions/workflows/tests.yml +.. image:: https://github.com/collective/pas.plugins.ldap/actions/workflows/tests.yml/badge.svg + :target: https://github.com/collective/pas.plugins.ldap/actions/workflows/tests.yml :alt: Test pas.plugins.ldap .. image:: https://secure.travis-ci.org/collective/pas.plugins.ldap.png From eea9c746a86ccad2fe74152f6c1e361f2ab44b1b Mon Sep 17 00:00:00 2001 From: "Leonardo J. Caballero G" Date: Fri, 1 May 2026 18:16:10 +0200 Subject: [PATCH 62/74] Updated the README file --- README.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.rst b/README.rst index e6cbb98..f37ead3 100644 --- a/README.rst +++ b/README.rst @@ -7,9 +7,9 @@ :target: https://pypi.python.org/pypi/pas.plugins.ldap :alt: Number of PyPI downloads -.. image:: https://github.com/collective/pas.plugins.ldap/actions/workflows/tests.yml/badge.svg - :target: https://github.com/collective/pas.plugins.ldap/actions/workflows/tests.yml - :alt: Test pas.plugins.ldap +.. image:: https://github.com/collective/pas.plugins.ldap/actions/workflows/tests.yaml/badge.svg + :target: https://github.com/collective/pas.plugins.ldap/actions/workflows/tests.yaml + :alt: Test the pas.plugins.ldap code .. image:: https://secure.travis-ci.org/collective/pas.plugins.ldap.png :target: http://travis-ci.org/collective/pas.plugins.ldap From 00b0df3fd54c67ba63c9e59c3c00f3612c894042 Mon Sep 17 00:00:00 2001 From: "Leonardo J. Caballero G" Date: Sat, 2 May 2026 11:54:16 +0200 Subject: [PATCH 63/74] Updated the README file --- README.rst | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/README.rst b/README.rst index f37ead3..be904d7 100644 --- a/README.rst +++ b/README.rst @@ -11,9 +11,6 @@ :target: https://github.com/collective/pas.plugins.ldap/actions/workflows/tests.yaml :alt: Test the pas.plugins.ldap code -.. image:: https://secure.travis-ci.org/collective/pas.plugins.ldap.png - :target: http://travis-ci.org/collective/pas.plugins.ldap - .. image:: https://coveralls.io/repos/collective/pas.plugins.ldap/badge.svg?branch=master&service=github :target: https://coveralls.io/github/collective/pas.plugins.ldap?branch=master @@ -72,6 +69,7 @@ Browse to your ``acl_users`` folder and add an ``LDAP-Plugin``. Configure it using the settings form and activate its features with the ``activate`` tab. +---- Plone ----- @@ -86,9 +84,9 @@ Add to the instance section of your ``buildout``: Run ``buildout``. Restart Plone. -Then go to the Plone control-panel, select ``Addons`` and install the ``LDAP/ Active Directory Support``. +Then go to the Plone control-panel, select ``Addons`` and install the ``LDAP / Active Directory Support``. -A new ``LDAP Settings`` icon appear on the left. Click it and configure the plugin there. +So, you can navigate to ``Site Setup`` > ``Users`` > ``LDAP / AD Support`` and click it and configure the plugin there. To use an own integration-profile, add to the profiles ``metadata.xml`` file: From cafbfdd929034a97a517c8d1ac2ce856341a7a25 Mon Sep 17 00:00:00 2001 From: "Leonardo J. Caballero G" Date: Sat, 2 May 2026 12:21:41 +0200 Subject: [PATCH 64/74] Added more improvements about i18n support #131 --- .../en/LC_MESSAGES/pas.plugins.ldap.po | 36 ++++++++++------- .../es/LC_MESSAGES/pas.plugins.ldap.po | 40 +++++++++++-------- src/pas/plugins/ldap/locales/merge-lingua.pot | 2 +- .../plugins/ldap/locales/pas.plugins.ldap.pot | 28 ++++++------- .../ldap/plonecontrolpanel/configure.zcml | 2 +- .../profiles/default/controlpanel.xml | 2 +- src/pas/plugins/ldap/properties.py | 4 +- src/pas/plugins/ldap/properties.yaml | 2 +- src/pas/plugins/ldap/zmi/manage_plugin.pt | 2 +- 9 files changed, 67 insertions(+), 51 deletions(-) diff --git a/src/pas/plugins/ldap/locales/en/LC_MESSAGES/pas.plugins.ldap.po b/src/pas/plugins/ldap/locales/en/LC_MESSAGES/pas.plugins.ldap.po index fe5b148..abc8bd4 100644 --- a/src/pas/plugins/ldap/locales/en/LC_MESSAGES/pas.plugins.ldap.po +++ b/src/pas/plugins/ldap/locales/en/LC_MESSAGES/pas.plugins.ldap.po @@ -1,7 +1,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" -"POT-Creation-Date: 2026-04-29 16:56+0000\n" +"POT-Creation-Date: 2026-05-01 23:50+0000\n" "PO-Revision-Date: YEAR-MO-DA HO:MI +ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -47,6 +47,18 @@ msgstr "" msgid "Groups" msgstr "" +#: pas/plugins/ldap/plonecontrolpanel/profiles/default/controlpanel.xml +msgid "LDAP / AD Support" +msgstr "" + +#: pas/plugins/ldap/plonecontrolpanel/controlpanel.pt:12 +msgid "LDAP / Active Directory Configuration" +msgstr "" + +#: pas/plugins/ldap/plonecontrolpanel/configure.zcml:19 +msgid "LDAP / Active Directory Support" +msgstr "" + #: pas/plugins/ldap/plonecontrolpanel/inspector.pt:11 msgid "LDAP Child Inspector" msgstr "" @@ -71,20 +83,8 @@ msgstr "" msgid "LDAP users; " msgstr "" -#: pas/plugins/ldap/plonecontrolpanel/profiles/default/controlpanel.xml -msgid "LDAP/ AD Support" -msgstr "" - -#: pas/plugins/ldap/plonecontrolpanel/controlpanel.pt:14 -msgid "LDAP/ Active Directory Configuration" -msgstr "" - -#: pas/plugins/ldap/plonecontrolpanel/configure.zcml:19 -msgid "LDAP/ Active Directory Support" -msgstr "" - #: pas/plugins/ldap/zmi/manage_plugin.pt:38 -msgid "Manage LDAP/AD plugin properties for id ${plugin_properties_id}" +msgid "Manage LDAP/AD plugin properties for the '${plugin_properties_id}' id." msgstr "" #: pas/plugins/ldap/plonecontrolpanel/inspector.pt:23 @@ -155,6 +155,14 @@ msgstr "" msgid "Users" msgstr "" +#: pas/plugins/ldap/plonecontrolpanel/controlpanel.pt:17 +msgid "You can configure an LDAP connection in this control panel. You can use a standard LDAP server or a Microsoft Active Directory server." +msgstr "" + +#: pas/plugins/ldap/properties.py:353 +msgid "feature not available" +msgstr "" + #. Default: LDAP attr name #: pas/plugins/ldap/properties.yaml:243 msgid "head_ldap_attr_name" diff --git a/src/pas/plugins/ldap/locales/es/LC_MESSAGES/pas.plugins.ldap.po b/src/pas/plugins/ldap/locales/es/LC_MESSAGES/pas.plugins.ldap.po index 3806b53..53afd48 100644 --- a/src/pas/plugins/ldap/locales/es/LC_MESSAGES/pas.plugins.ldap.po +++ b/src/pas/plugins/ldap/locales/es/LC_MESSAGES/pas.plugins.ldap.po @@ -3,8 +3,8 @@ msgid "" msgstr "" "Project-Id-Version: pas.plugins.ldap\n" -"POT-Creation-Date: 2026-04-29 16:56+0000\n" -"PO-Revision-Date: 2026-04-29 19:02+0200\n" +"POT-Creation-Date: 2026-05-01 23:50+0000\n" +"PO-Revision-Date: 2026-05-02 01:51+0200\n" "Last-Translator: Leonardo J. Caballero G. \n" "Language-Team: \n" "Language: es\n" @@ -52,6 +52,18 @@ msgstr "Perfil de extensión base de Zope para pas.plugins.ldap." msgid "Groups" msgstr "Grupos" +#: pas/plugins/ldap/plonecontrolpanel/profiles/default/controlpanel.xml +msgid "LDAP / AD Support" +msgstr "Soporte con LDAP/AD" + +#: pas/plugins/ldap/plonecontrolpanel/controlpanel.pt:12 +msgid "LDAP / Active Directory Configuration" +msgstr "Configuración de LDAP / Active Directory" + +#: pas/plugins/ldap/plonecontrolpanel/configure.zcml:19 +msgid "LDAP / Active Directory Support" +msgstr "Soporte con LDAP/Active Directory" + #: pas/plugins/ldap/plonecontrolpanel/inspector.pt:11 msgid "LDAP Child Inspector" msgstr "Inspector secundario LDAP" @@ -76,21 +88,9 @@ msgstr "Los usuarios de LDAP están bien, pero los grupos no; " msgid "LDAP users; " msgstr "Usuarios de LDAP; " -#: pas/plugins/ldap/plonecontrolpanel/profiles/default/controlpanel.xml -msgid "LDAP/ AD Support" -msgstr "Soporte con LDAP/AD" - -#: pas/plugins/ldap/plonecontrolpanel/controlpanel.pt:14 -msgid "LDAP/ Active Directory Configuration" -msgstr "Configuración de LDAP/Active Directory" - -#: pas/plugins/ldap/plonecontrolpanel/configure.zcml:19 -msgid "LDAP/ Active Directory Support" -msgstr "Soporte con LDAP/Active Directory" - #: pas/plugins/ldap/zmi/manage_plugin.pt:38 -msgid "Manage LDAP/AD plugin properties for id ${plugin_properties_id}" -msgstr "Administre las propiedades del complemento LDAP / AD para id $ {plugin_properties_id}" +msgid "Manage LDAP/AD plugin properties for the '${plugin_properties_id}' id." +msgstr "Administre las propiedades del complemento LDAP / AD para el id '${plugin_properties_id}'." #: pas/plugins/ldap/plonecontrolpanel/inspector.pt:23 msgid "No search base selected" @@ -160,6 +160,14 @@ msgstr "El nombre de usuario es necesario para las conexiones no anónimas." msgid "Users" msgstr "Usuarios" +#: pas/plugins/ldap/plonecontrolpanel/controlpanel.pt:17 +msgid "You can configure an LDAP connection in this control panel. You can use a standard LDAP server or a Microsoft Active Directory server." +msgstr "En este panel de control se puede configurar una conexión LDAP. Puede utilizar un servidor LDAP estándar o un servidor de Microsoft Active Directory." + +#: pas/plugins/ldap/properties.py:353 +msgid "feature not available" +msgstr "función no disponible" + #. Default: LDAP attr name #: pas/plugins/ldap/properties.yaml:243 msgid "head_ldap_attr_name" diff --git a/src/pas/plugins/ldap/locales/merge-lingua.pot b/src/pas/plugins/ldap/locales/merge-lingua.pot index 3263eab..8eb44b7 100644 --- a/src/pas/plugins/ldap/locales/merge-lingua.pot +++ b/src/pas/plugins/ldap/locales/merge-lingua.pot @@ -6,7 +6,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE 1.0\n" -"POT-Creation-Date: 2026-04-29 18:56+0200\n" +"POT-Creation-Date: 2026-05-02 01:33+0200\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" diff --git a/src/pas/plugins/ldap/locales/pas.plugins.ldap.pot b/src/pas/plugins/ldap/locales/pas.plugins.ldap.pot index 1e7f558..9de723e 100644 --- a/src/pas/plugins/ldap/locales/pas.plugins.ldap.pot +++ b/src/pas/plugins/ldap/locales/pas.plugins.ldap.pot @@ -4,7 +4,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" -"POT-Creation-Date: 2026-04-29 16:56+0000\n" +"POT-Creation-Date: 2026-05-01 23:50+0000\n" "PO-Revision-Date: YEAR-MO-DA HO:MI +ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -50,6 +50,18 @@ msgstr "" msgid "Groups" msgstr "" +#: pas/plugins/ldap/plonecontrolpanel/profiles/default/controlpanel.xml +msgid "LDAP / AD Support" +msgstr "" + +#: pas/plugins/ldap/plonecontrolpanel/controlpanel.pt:12 +msgid "LDAP / Active Directory Configuration" +msgstr "" + +#: pas/plugins/ldap/plonecontrolpanel/configure.zcml:19 +msgid "LDAP / Active Directory Support" +msgstr "" + #: pas/plugins/ldap/plonecontrolpanel/inspector.pt:11 msgid "LDAP Child Inspector" msgstr "" @@ -74,20 +86,8 @@ msgstr "" msgid "LDAP users; " msgstr "" -#: pas/plugins/ldap/plonecontrolpanel/profiles/default/controlpanel.xml -msgid "LDAP/ AD Support" -msgstr "" - -#: pas/plugins/ldap/plonecontrolpanel/controlpanel.pt:14 -msgid "LDAP/ Active Directory Configuration" -msgstr "" - -#: pas/plugins/ldap/plonecontrolpanel/configure.zcml:19 -msgid "LDAP/ Active Directory Support" -msgstr "" - #: pas/plugins/ldap/zmi/manage_plugin.pt:38 -msgid "Manage LDAP/AD plugin properties for id ${plugin_properties_id}" +msgid "Manage LDAP/AD plugin properties for the '${plugin_properties_id}' id." msgstr "" #: pas/plugins/ldap/plonecontrolpanel/inspector.pt:23 diff --git a/src/pas/plugins/ldap/plonecontrolpanel/configure.zcml b/src/pas/plugins/ldap/plonecontrolpanel/configure.zcml index f4dba7a..b4b452a 100644 --- a/src/pas/plugins/ldap/plonecontrolpanel/configure.zcml +++ b/src/pas/plugins/ldap/plonecontrolpanel/configure.zcml @@ -12,7 +12,7 @@ -

Manage LDAP/AD plugin properties for id id

+

Manage LDAP/AD plugin properties for the 'id' id.

Set properties for users and groups from LDAP/ActiveDirectory using the pas.plugins.ldap plugin. From 395652656aa38f23596b40aecf37247bc12e55de Mon Sep 17 00:00:00 2001 From: "Leonardo J. Caballero G" Date: Sat, 2 May 2026 13:17:14 +0200 Subject: [PATCH 65/74] Added the CONTRIBUTORS file --- CONTRIBUTORS.rst | 10 ++++++++++ README.rst | 12 +++--------- 2 files changed, 13 insertions(+), 9 deletions(-) create mode 100644 CONTRIBUTORS.rst diff --git a/CONTRIBUTORS.rst b/CONTRIBUTORS.rst new file mode 100644 index 0000000..4d0783c --- /dev/null +++ b/CONTRIBUTORS.rst @@ -0,0 +1,10 @@ +Contributors +============ + +- Jens W. Klein +- Robert Niederrreiter +- Florian Friesdorf +- Daniel Widerin +- Johannes Raggam +- Luca Fabbri +- Leonardo J. Caballero G. diff --git a/README.rst b/README.rst index be904d7..efe0a9a 100644 --- a/README.rst +++ b/README.rst @@ -203,18 +203,12 @@ Maintainers are Robert Niederreiter, Jens Klein and the `BlueDynamics Alliance < We appreciate any contribution and if a release is needed to be done on pypi, please just contact one of us: `dev@bluedynamics dot com `_ - Contributors ============ -- Jens W. Klein -- Robert Niederrreiter -- Florian Friesdorf -- Daniel Widerin -- Johannes Raggam -- Luca Fabbri -- Leonardo J. Caballero G. - +For a list of all contributors to this project, please checkout the following resources: +- The `CONTRIBUTORS file `_. +- The `Contributors page on GitHub `_. License ======= From 55f3ba1c0e1d5e74b9db5c23613dfe451f4548b8 Mon Sep 17 00:00:00 2001 From: "Leonardo J. Caballero G" Date: Sat, 2 May 2026 13:17:41 +0200 Subject: [PATCH 66/74] Updated the README file --- README.rst | 61 ++++++++++++++++++++++++++++++++++++++++-------------- 1 file changed, 46 insertions(+), 15 deletions(-) diff --git a/README.rst b/README.rst index efe0a9a..39dceaa 100644 --- a/README.rst +++ b/README.rst @@ -16,28 +16,44 @@ This is a `LDAP `_ Plugin for the `Zope `_ `Pluggable Authentication Service (PAS) `_. -It provides users and/or groups from an LDAP directory. +``pas.plugins.ldap`` is **not** releated to the old `LDAPUserFolder `_ / `LDAPMultiPlugins `_ and the packages (i.e. `PloneLDAP `_) stacked on top of it in any way. -It works in a plain Zope even if it depends on `PlonePAS `_. +It is based on `node.ext.ldap `_, an almost framework independent LDAP stack. -If `Plone `_ is installed an integration layer with a setup-profile and a plone-controlpanel page is available. -``pas.plugins.ldap`` is **not** releated to the old `LDAPUserFolder `_ / `LDAPMultiPlugins `_ and the packages (i.e. `PloneLDAP `_) stacked on top of it in any way. +Features +-------- -It is based on `node.ext.ldap `_, an almost framework independent LDAP stack. +- If `Plone `_ is installed an integration layer with a `setup profile` and a `Plone Controlpanel` page is available. +- It works in a plain Zope even if it depends on `PlonePAS `_. +- LDAP authentication and authorization for users and groups. +- It provides users and/or groups from an LDAP directory. +- LDAP properties for users and groups, which can be used in the rest of the system as well. +- For now users and groups can't be added or deleted. Properties on both are read/write. -For now users and groups can't be added or deleted. Properties on both are read/write. -See section *TODO*. +TODO +---- + +For a detailed list of TODO tasks to this project, please checkout the `TODO file `_. + +Translations +============ + +This product has been translated into +- English +- Spanish Installation ============ +This package supports Zope applications and Plone sites using Volto and Classic UI. + Dependencies ------------ -This package depends on ``python-ldap``. +This package depends on `python-ldap `_ package. To build it correctly you need to have some development libraries included in your system. @@ -47,6 +63,7 @@ On a Debian-based installation use: sudo apt install python-dev libldap2-dev libsasl2-dev libssl-dev +---- Zope ---- @@ -65,9 +82,21 @@ Add to the instance section of your buildout: Run ``buildout``. Restart Zope. -Browse to your ``acl_users`` folder and add an ``LDAP-Plugin``. +Browse to your ``acl_users`` folder and add an ``LDAP Plugin`` object. + +Configure it using the ``LDAP Settings`` form and choose the functionality this +``LDAP Plugin`` will perform with the ``Activate`` tab. -Configure it using the settings form and activate its features with the ``activate`` tab. +- `Authentication` (``authenticateCredentials``) +- `Group_Enumeration` (``enumerateGroups``) +- `Group_Introspection` (``getGroupById``) +- `Group_Management` (``addGroup``) +- `Groups` (``getGroupsForPrincipal``) +- `Properties` (``getPropertiesForUser``) +- `Roles` (``getRolesForPrincipal``) +- `User_Adder` (``doAddUser``) +- `User_Enumeration` (``enumerateUsers``) +- `User_Management` (``doChangeUser``) ---- @@ -99,7 +128,7 @@ To use an own integration-profile, add to the profiles ``metadata.xml`` file: ... -Additionally ldap settings can be exported and imported with ``portal_setup``. +Additionally the ``LDAP Settings`` can be exported and imported with ``portal_setup`` tool. You can place the exported ``ldapsettings.xml`` file in your integration profile, so it will be imported with your next install again. **Warning:** @@ -108,6 +137,7 @@ You can place the exported ``ldapsettings.xml`` file in your integration profile But anonymous bindings are possible. +---- Logging ------- @@ -129,6 +159,7 @@ There are two environment variables to control the logging of LDAP-errors: If a PAS operation takes longer than he given number of seconds, log it as error. Default: 5 (time in seconds, float). +---- Timeouts -------- @@ -143,8 +174,9 @@ Global LDAP timeouts are set and controlled by two environment variables: Overall timeout. Default: 30.0s -See details in python-ldap documentation: OPT_NETWORK_TIMEOUT and OPT_TIMEOUT. +See details in `python-ldap `_ documentation: OPT_NETWORK_TIMEOUT and OPT_TIMEOUT. +---- Caching ------- @@ -162,6 +194,7 @@ The UGM tree is cached by default on the request, that means its built up every There is an alternative adapter available which will cache the ugm tree as volatile attribute (``_v_...``) on the persistent plugin. Volatile attributes are not persisted in the ZODB. + If the plugin object vanishes from ZODB cache the atrribute is gone. The volatile plugin cache can be activated by loading its zcml with ```_ is installed properly and recognized by buildout. This package works fine for several 10000 users or groups, **unless you list users**. This is not that much a problem for small amount of users. There is room for future optimization in the underlying `node.ext.ldap `_. - Source Code =========== From 592d7f524c8fdf5bb7cf76cddda17fda955dbd8b Mon Sep 17 00:00:00 2001 From: "Leonardo J. Caballero G" Date: Sat, 2 May 2026 13:48:25 +0200 Subject: [PATCH 67/74] Updated the README file --- README.rst | 34 ++++++++++++++++++++++++---------- 1 file changed, 24 insertions(+), 10 deletions(-) diff --git a/README.rst b/README.rst index 39dceaa..03261f5 100644 --- a/README.rst +++ b/README.rst @@ -68,7 +68,7 @@ On a Debian-based installation use: Zope ---- -Add to the instance section of your buildout: +Install ``pas.plugins.ldap`` by adding it to the ``instance`` section of your ``buildout``: .. code-block:: ini @@ -80,9 +80,9 @@ Add to the instance section of your buildout: ... pas.plugins.ldap -Run ``buildout``. Restart Zope. +Run ``buildout`` and then restart the ``Zope`` instance. -Browse to your ``acl_users`` folder and add an ``LDAP Plugin`` object. +Then browse to your ``acl_users`` folder and add an ``LDAP Plugin`` object. Configure it using the ``LDAP Settings`` form and choose the functionality this ``LDAP Plugin`` will perform with the ``Activate`` tab. @@ -103,7 +103,8 @@ Configure it using the ``LDAP Settings`` form and choose the functionality this Plone ----- -Add to the instance section of your ``buildout``: +To install ``pas.plugins.ldap`` in a Plone site, you need by adding it to the +``instance`` section of your ``buildout``: .. code-block:: ini @@ -111,7 +112,7 @@ Add to the instance section of your ``buildout``: ... pas.plugins.ldap -Run ``buildout``. Restart Plone. +Run ``buildout`` and then restart the ``Plone`` instance. Then go to the Plone control-panel, select ``Addons`` and install the ``LDAP / Active Directory Support``. @@ -225,14 +226,19 @@ Source Code If you want to help with the development (improvement, update, bug-fixing, ...) of ``pas.plugins.ldap`` this is a great idea! -The code is located in the `GitHub Collective `_. +- The code is located in the `GitHub Collective `_. + You can clone it or `get access to the GitHub Collective `_ and work directly on the project. -You can clone it or `get access to the GitHub Collective `_ and work directly on the project. +Authors +------- -Maintainers are Robert Niederreiter, Jens Klein and the `BlueDynamics Alliance `_ developer team. +This product was developed by `BlueDynamics Alliance `_ team. -We appreciate any contribution and if a release is needed to be done on pypi, please just contact one of us: -`dev@bluedynamics dot com `_ +.. image:: https://bluedynamics.com/++theme++bda.theme/static/bda-media/bda-logo.svg + :target: https://bluedynamics.com/ + :alt: BlueDynamics Alliance + +Maintainers are Robert Niederreiter, Jens Klein and the `BlueDynamics Alliance `_ developer team. Contributors ============ @@ -241,6 +247,14 @@ For a list of all contributors to this project, please checkout the following re - The `CONTRIBUTORS file `_. - The `Contributors page on GitHub `_. +Support +======= + +We appreciate any contribution and if a release is needed to be done on pypi, please just contact one of us: +`dev@bluedynamics dot com `_. + +- If you are having issues, please let us know at our `issue tracker `_. + License ======= From 7d185d8baa3d5c3277a59a1eb91e90699f6201e3 Mon Sep 17 00:00:00 2001 From: "Leonardo J. Caballero G" Date: Sat, 2 May 2026 13:55:29 +0200 Subject: [PATCH 68/74] Updated the README file --- README.rst | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/README.rst b/README.rst index 03261f5..4084cf1 100644 --- a/README.rst +++ b/README.rst @@ -22,7 +22,7 @@ It is based on `node.ext.ldap `_, an al Features --------- +======== - If `Plone `_ is installed an integration layer with a `setup profile` and a `Plone Controlpanel` page is available. - It works in a plain Zope even if it depends on `PlonePAS `_. @@ -31,7 +31,6 @@ Features - LDAP properties for users and groups, which can be used in the rest of the system as well. - For now users and groups can't be added or deleted. Properties on both are read/write. - TODO ---- @@ -227,7 +226,8 @@ Source Code If you want to help with the development (improvement, update, bug-fixing, ...) of ``pas.plugins.ldap`` this is a great idea! - The code is located in the `GitHub Collective `_. - You can clone it or `get access to the GitHub Collective `_ and work directly on the project. + +- You can clone it or `get access to the GitHub Collective `_ and work directly on the project. Authors ------- @@ -240,13 +240,6 @@ This product was developed by `BlueDynamics Alliance Maintainers are Robert Niederreiter, Jens Klein and the `BlueDynamics Alliance `_ developer team. -Contributors -============ - -For a list of all contributors to this project, please checkout the following resources: -- The `CONTRIBUTORS file `_. -- The `Contributors page on GitHub `_. - Support ======= @@ -255,6 +248,15 @@ We appreciate any contribution and if a release is needed to be done on pypi, pl - If you are having issues, please let us know at our `issue tracker `_. +Contributors +============ + +For a list of all contributors to this project, please checkout the following resources: + +- The `CONTRIBUTORS file `_. + +- The `Contributors page on GitHub `_. + License ======= From 34886321dd72baacb408498f0e9bbe0d31b2cb2e Mon Sep 17 00:00:00 2001 From: "Leonardo J. Caballero G" Date: Sat, 2 May 2026 19:47:36 +0200 Subject: [PATCH 69/74] Added more improvments about the i18n support, also a new UX for the 'LDAP / Active Directory Configuration' Plone control panel #131 #113 --- .../en/LC_MESSAGES/pas.plugins.ldap.po | 183 +++++++++-------- .../es/LC_MESSAGES/pas.plugins.ldap.po | 185 +++++++++-------- src/pas/plugins/ldap/locales/merge-lingua.pot | 147 +++++++------- .../plugins/ldap/locales/pas.plugins.ldap.pot | 188 +++++++++--------- src/pas/plugins/ldap/monkey.py | 2 +- .../ldap/plonecontrolpanel/__init__.py | 4 +- .../ldap/plonecontrolpanel/controlpanel.pt | 48 +++-- .../ldap/plonecontrolpanel/controlpanel.py | 29 +-- .../ldap/plonecontrolpanel/inspector.py | 2 + .../ldap/plonecontrolpanel/setuphandlers.py | 1 + src/pas/plugins/ldap/plugin.py | 6 +- src/pas/plugins/ldap/properties.py | 39 ++-- src/pas/plugins/ldap/properties.yaml | 4 +- src/pas/plugins/ldap/zmi/manage_plugin.pt | 2 +- 14 files changed, 426 insertions(+), 414 deletions(-) diff --git a/src/pas/plugins/ldap/locales/en/LC_MESSAGES/pas.plugins.ldap.po b/src/pas/plugins/ldap/locales/en/LC_MESSAGES/pas.plugins.ldap.po index abc8bd4..a81730c 100644 --- a/src/pas/plugins/ldap/locales/en/LC_MESSAGES/pas.plugins.ldap.po +++ b/src/pas/plugins/ldap/locales/en/LC_MESSAGES/pas.plugins.ldap.po @@ -1,7 +1,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" -"POT-Creation-Date: 2026-05-01 23:50+0000\n" +"POT-Creation-Date: 2026-05-02 16:13+0000\n" "PO-Revision-Date: YEAR-MO-DA HO:MI +ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -14,28 +14,27 @@ msgstr "" "Preferred-Encodings: utf-8 latin1\n" "Domain: DOMAIN\n" -#: pas/plugins/ldap/properties.py:37 +#: pas/plugins/ldap/properties.py:41 msgid "BASE" msgstr "" -#: pas/plugins/ldap/plonecontrolpanel/controlpanel.pt:16 #: pas/plugins/ldap/zmi/manage_plugin.pt:28 msgid "Connection Test" msgstr "" -#: pas/plugins/ldap/properties.py:254 +#: pas/plugins/ldap/properties.py:279 msgid "Connection, users- and groups-access tested successfully." msgstr "" -#: pas/plugins/ldap/properties.py:43 +#: pas/plugins/ldap/properties.py:49 msgid "Days since Epoch" msgstr "" -#: pas/plugins/ldap/properties.py:253 +#: pas/plugins/ldap/properties.py:278 msgid "Exception in Groups; " msgstr "" -#: pas/plugins/ldap/properties.py:246 +#: pas/plugins/ldap/properties.py:271 msgid "Exception in Users; " msgstr "" @@ -63,7 +62,7 @@ msgstr "" msgid "LDAP Child Inspector" msgstr "" -#: pas/plugins/ldap/plonecontrolpanel/controlpanel.pt:25 +#: pas/plugins/ldap/plonecontrolpanel/controlpanel.pt:30 msgid "LDAP Inspector" msgstr "" @@ -71,15 +70,15 @@ msgstr "" msgid "LDAP Plugin for PAS - Zope 2 Base Installation" msgstr "" -#: pas/plugins/ldap/plonecontrolpanel/controlpanel.py:43 +#: pas/plugins/ldap/plonecontrolpanel/controlpanel.py:51 msgid "LDAP Settings saved." msgstr "" -#: pas/plugins/ldap/properties.py:250 +#: pas/plugins/ldap/properties.py:275 msgid "LDAP Users ok, but groups not; " msgstr "" -#: pas/plugins/ldap/properties.py:243 +#: pas/plugins/ldap/properties.py:268 msgid "LDAP users; " msgstr "" @@ -91,31 +90,31 @@ msgstr "" msgid "No search base selected" msgstr "" -#: pas/plugins/ldap/properties.py:234 +#: pas/plugins/ldap/properties.py:259 msgid "Non-LDAP error while getting ILDAPGroupsConfig!" msgstr "" -#: pas/plugins/ldap/properties.py:222 +#: pas/plugins/ldap/properties.py:247 msgid "Non-LDAP error while getting ILDAPProps!" msgstr "" -#: pas/plugins/ldap/properties.py:228 +#: pas/plugins/ldap/properties.py:253 msgid "Non-LDAP error while getting ILDAPUsersConfig!" msgstr "" -#: pas/plugins/ldap/properties.py:38 +#: pas/plugins/ldap/properties.py:42 msgid "ONELEVEL" msgstr "" -#: pas/plugins/ldap/properties.py:204 +#: pas/plugins/ldap/properties.py:229 msgid "Password is required for non-anonymous connections." msgstr "" -#: pas/plugins/ldap/properties.py:39 +#: pas/plugins/ldap/properties.py:43 msgid "SUBTREE" msgstr "" -#: pas/plugins/ldap/properties.py:44 +#: pas/plugins/ldap/properties.py:50 msgid "Seconds since epoch" msgstr "" @@ -123,7 +122,7 @@ msgstr "" msgid "Select search base" msgstr "" -#: pas/plugins/ldap/properties.py:241 +#: pas/plugins/ldap/properties.py:266 msgid "Server Down" msgstr "" @@ -131,10 +130,6 @@ msgstr "" msgid "Set properties for users and groups from LDAP/ActiveDirectory using the ${plugin_name} plugin." msgstr "" -#: pas/plugins/ldap/plonecontrolpanel/controlpanel.pt:34 -msgid "Settings" -msgstr "" - #: pas/plugins/ldap/plonecontrolpanel/configure.zcml:27 msgid "Uninstall LDAP Plugin for PAS" msgstr "" @@ -143,11 +138,11 @@ msgstr "" msgid "Uninstalls the pas.plugins.ldap add-on." msgstr "" -#: pas/plugins/ldap/properties.py:209 +#: pas/plugins/ldap/properties.py:234 msgid "User/Password are required if not anonymous." msgstr "" -#: pas/plugins/ldap/properties.py:198 +#: pas/plugins/ldap/properties.py:223 msgid "Username is required for non-anonymous connections." msgstr "" @@ -159,351 +154,351 @@ msgstr "" msgid "You can configure an LDAP connection in this control panel. You can use a standard LDAP server or a Microsoft Active Directory server." msgstr "" -#: pas/plugins/ldap/properties.py:353 +#: pas/plugins/ldap/properties.py:354 msgid "feature not available" msgstr "" #. Default: LDAP attr name -#: pas/plugins/ldap/properties.yaml:243 +#: pas/plugins/ldap/properties.yaml:244 msgid "head_ldap_attr_name" msgstr "" #. Default: LDAP Attribute -#: pas/plugins/ldap/properties.yaml:187 +#: pas/plugins/ldap/properties.yaml:188 msgid "head_ldap_attribute" msgstr "" #. Default: Name on Sheet -#: pas/plugins/ldap/properties.yaml:194 +#: pas/plugins/ldap/properties.yaml:195 msgid "head_name_on_sheet" msgstr "" -#. Default: Reserved key -#: pas/plugins/ldap/properties.yaml:186 +#. Default: Reserved Key +#: pas/plugins/ldap/properties.yaml:187 msgid "head_reserved_key" msgstr "" #. port 12345 #. Default: Example, the protocol is ldap, the IP address 127.0.0.1 and the -#: pas/plugins/ldap/properties.yaml:21 +#: pas/plugins/ldap/properties.yaml:23 msgid "help_connection_uri" msgstr "" #. Default: If set on authenticate a failing certificate chain check including #. CA is ignored. -#: pas/plugins/ldap/properties.yaml:58 +#: pas/plugins/ldap/properties.yaml:60 msgid "help_ignore_certificate_check" msgstr "" #. Default: The timeout period, in seconds, for waiting for a connection to be #. established with the LDAP server. -#: pas/plugins/ldap/properties.yaml:34 +#: pas/plugins/ldap/properties.yaml:36 msgid "help_ldap_connection_timeout_in_seconds" msgstr "" #. Default: The timeout in seconds for an operation such as a search or update #. to complete. If no timeout is required, use -1 as the value. -#: pas/plugins/ldap/properties.yaml:41 +#: pas/plugins/ldap/properties.yaml:43 msgid "help_ldap_operation_timeout_in_seconds" msgstr "" #. Default: "Group DNs outside of the groups base DN are ignored, except if #. listed here" -#: pas/plugins/ldap/properties.yaml:156 +#: pas/plugins/ldap/properties.yaml:158 msgid "help_memberOf_external_allowed_group_dns" msgstr "" #. Default: global - same server for all ldap plugins -#: pas/plugins/ldap/properties.yaml:268 +#: pas/plugins/ldap/properties.yaml:269 msgid "help_memcached_server_to_use" msgstr "" #. Default: "One of those is mandatory like as one the following options #. groupOfNames, groupOfUniqueNames, posixGroup, group" -#: pas/plugins/ldap/properties.yaml:224 +#: pas/plugins/ldap/properties.yaml:225 msgid "help_object_classes_for_groups" msgstr "" #. Default: Maximum page size, number of results to query the server at once #. for. -#: pas/plugins/ldap/properties.yaml:94 +#: pas/plugins/ldap/properties.yaml:96 msgid "help_page_size" msgstr "" #. Default: If your LDAP/AD supports it this will use #. LDAP_MATCHING_RULE_IN_CHAIN. By default only AD supports this. -#: pas/plugins/ldap/properties.yaml:151 +#: pas/plugins/ldap/properties.yaml:153 msgid "help_support_recursive_nested_groups" msgstr "" #. Default: If set, the LDAP server certificate is checked against the CA #. certificates in folder. -#: pas/plugins/ldap/properties.yaml:76 +#: pas/plugins/ldap/properties.yaml:78 msgid "help_tls_cacertdir" msgstr "" #. Default: If set, the LDAP server certificate is checked against the CA #. certificate file. -#: pas/plugins/ldap/properties.yaml:70 +#: pas/plugins/ldap/properties.yaml:72 msgid "help_tls_cacertfile" msgstr "" #. Default: If set, the client certificate is sent to the server. -#: pas/plugins/ldap/properties.yaml:82 +#: pas/plugins/ldap/properties.yaml:84 msgid "help_tls_clcertfile" msgstr "" #. Default: If set, the client certificate is sent to the server. -#: pas/plugins/ldap/properties.yaml:88 +#: pas/plugins/ldap/properties.yaml:90 msgid "help_tls_clkeyfile" msgstr "" #. Default: If set, the connection is upgraded to TLS. -#: pas/plugins/ldap/properties.yaml:64 +#: pas/plugins/ldap/properties.yaml:66 msgid "help_use_tls_connection" msgstr "" #. Default: Account expiration unit -#: pas/plugins/ldap/properties.yaml:175 +#: pas/plugins/ldap/properties.yaml:177 msgid "lbl_account_expiration_unit" msgstr "" #. Default: Anonymous Connection? -#: pas/plugins/ldap/properties.yaml:28 +#: pas/plugins/ldap/properties.yaml:30 msgid "lbl_anonymous_connection" msgstr "" #. Default: Attribute containing expiration Time -#: pas/plugins/ldap/properties.yaml:170 +#: pas/plugins/ldap/properties.yaml:172 msgid "lbl_attribute_containing_expiration_time" msgstr "" #. Default: Cache LDAP queries -#: pas/plugins/ldap/properties.yaml:262 +#: pas/plugins/ldap/properties.yaml:263 msgid "lbl_cache_ldap_queries" msgstr "" #. Default: Cache Settings -#: pas/plugins/ldap/properties.yaml:255 +#: pas/plugins/ldap/properties.yaml:256 msgid "lbl_cache_settings" msgstr "" #. Default: Cache timeout in seconds -#: pas/plugins/ldap/properties.yaml:275 +#: pas/plugins/ldap/properties.yaml:276 msgid "lbl_cache_timeout_in_seconds" msgstr "" #. Default: Connection URI -#: pas/plugins/ldap/properties.yaml:20 +#: pas/plugins/ldap/properties.yaml:22 msgid "lbl_connection_uri" msgstr "" #. Default: Group attribute aliases -#: pas/plugins/ldap/properties.yaml:238 +#: pas/plugins/ldap/properties.yaml:239 msgid "lbl_group_attribute_aliases" msgstr "" #. Default: Group Property-Sheet Attributes -#: pas/plugins/ldap/properties.yaml:248 +#: pas/plugins/ldap/properties.yaml:249 msgid "lbl_group_property-sheet_attributes" msgstr "" #. Default: Groups container DN -#: pas/plugins/ldap/properties.yaml:206 +#: pas/plugins/ldap/properties.yaml:207 msgid "lbl_groups_container_dn" msgstr "" #. Default: Groups search query filter -#: pas/plugins/ldap/properties.yaml:218 +#: pas/plugins/ldap/properties.yaml:219 msgid "lbl_groups_search_query_filter" msgstr "" #. Default: Groups search scope -#: pas/plugins/ldap/properties.yaml:212 +#: pas/plugins/ldap/properties.yaml:213 msgid "lbl_groups_search_scope" msgstr "" #. Default: Groups Settings -#: pas/plugins/ldap/properties.yaml:199 +#: pas/plugins/ldap/properties.yaml:200 msgid "lbl_groups_settings" msgstr "" #. Default: Ignore certificate check? -#: pas/plugins/ldap/properties.yaml:57 +#: pas/plugins/ldap/properties.yaml:59 msgid "lbl_ignore_certificate_check" msgstr "" #. Default: LDAP connection timeout in seconds. -#: pas/plugins/ldap/properties.yaml:33 +#: pas/plugins/ldap/properties.yaml:35 msgid "lbl_ldap_connection_timeout_in_seconds" msgstr "" #. Default: LDAP operation timeout in seconds. -#: pas/plugins/ldap/properties.yaml:40 +#: pas/plugins/ldap/properties.yaml:42 msgid "lbl_ldap_operation_timeout_in_seconds" msgstr "" #. Default: LDAP Server Settings -#: pas/plugins/ldap/properties.yaml:10 +#: pas/plugins/ldap/properties.yaml:12 msgid "lbl_ldap_server_settings" msgstr "" #. Default: Manager Password -#: pas/plugins/ldap/properties.yaml:52 +#: pas/plugins/ldap/properties.yaml:54 msgid "lbl_manager_password" msgstr "" #. Default: Manager User -#: pas/plugins/ldap/properties.yaml:47 +#: pas/plugins/ldap/properties.yaml:49 msgid "lbl_manager_user" msgstr "" #. Default: memberOf attribute supported? -#: pas/plugins/ldap/properties.yaml:135 +#: pas/plugins/ldap/properties.yaml:137 msgid "lbl_memberOf_attribute_supported" msgstr "" #. Default: memberOf external allowed Group DNs -#: pas/plugins/ldap/properties.yaml:157 +#: pas/plugins/ldap/properties.yaml:159 msgid "lbl_memberOf_external_allowed_group_dns" msgstr "" #. Default: Memcached Server to use -#: pas/plugins/ldap/properties.yaml:267 +#: pas/plugins/ldap/properties.yaml:268 msgid "lbl_memcached_server_to_use" msgstr "" #. Default: Object class -#: pas/plugins/ldap/properties.yaml:126 +#: pas/plugins/ldap/properties.yaml:128 msgid "lbl_object_class" msgstr "" #. Default: Object classes for Groups -#: pas/plugins/ldap/properties.yaml:223 +#: pas/plugins/ldap/properties.yaml:224 msgid "lbl_object_classes_for_groups" msgstr "" #. Default: Object classes for User creation -#: pas/plugins/ldap/properties.yaml:125 +#: pas/plugins/ldap/properties.yaml:127 msgid "lbl_object_classes_for_user_creation" msgstr "" #. Default: Page Size -#: pas/plugins/ldap/properties.yaml:93 +#: pas/plugins/ldap/properties.yaml:95 msgid "lbl_page_size" msgstr "" #. Default: Role -#: pas/plugins/ldap/properties.yaml:141 +#: pas/plugins/ldap/properties.yaml:143 msgid "lbl_role" msgstr "" #. Default: Roles acquired -#: pas/plugins/ldap/properties.yaml:140 +#: pas/plugins/ldap/properties.yaml:142 msgid "lbl_roles_acquired" msgstr "" #. Default: Save -#: pas/plugins/ldap/properties.yaml:284 +#: pas/plugins/ldap/properties.yaml:285 msgid "lbl_save" msgstr "" #. Default: Support recursive/nested groups? -#: pas/plugins/ldap/properties.yaml:150 +#: pas/plugins/ldap/properties.yaml:152 msgid "lbl_support_recursive_nested_groups" msgstr "" #. Default: Path to folder with CA certificate files for TLS communication #. (OPT_X_TLS_CACERTDIR) -#: pas/plugins/ldap/properties.yaml:75 +#: pas/plugins/ldap/properties.yaml:77 msgid "lbl_tls_cacertdir" msgstr "" #. Default: Path to CA certificate file for TLS communication #. (OPT_X_TLS_CACERTFILE) -#: pas/plugins/ldap/properties.yaml:69 +#: pas/plugins/ldap/properties.yaml:71 msgid "lbl_tls_cacertfile" msgstr "" #. Default: Path to client certificate file for TLS communication #. (OPT_X_TLS_CERTFILE). Requires tls_clkeyfile -#: pas/plugins/ldap/properties.yaml:81 +#: pas/plugins/ldap/properties.yaml:83 msgid "lbl_tls_clcertfile" msgstr "" #. Default: Path to client certificate key for TLS communication #. (OPT_X_TLS_KEYFILE). Requires tls_clcertfile -#: pas/plugins/ldap/properties.yaml:87 +#: pas/plugins/ldap/properties.yaml:89 msgid "lbl_tls_clkeyfile" msgstr "" #. Default: Use TLS connection -#: pas/plugins/ldap/properties.yaml:63 +#: pas/plugins/ldap/properties.yaml:65 msgid "lbl_use_tls_connection" msgstr "" #. Default: User Accounts expires? -#: pas/plugins/ldap/properties.yaml:165 +#: pas/plugins/ldap/properties.yaml:167 msgid "lbl_user_accounts_expires" msgstr "" #. Default: User attribute aliases -#: pas/plugins/ldap/properties.yaml:182 +#: pas/plugins/ldap/properties.yaml:183 msgid "lbl_user_attribute_aliases" msgstr "" #. Default: User Property-Sheet Attributes -#: pas/plugins/ldap/properties.yaml:192 +#: pas/plugins/ldap/properties.yaml:193 msgid "lbl_user_property-sheet_attributes" msgstr "" #. Default: Users container DN -#: pas/plugins/ldap/properties.yaml:108 +#: pas/plugins/ldap/properties.yaml:110 msgid "lbl_users_container_dn" msgstr "" #. Default: Users search query filter -#: pas/plugins/ldap/properties.yaml:120 +#: pas/plugins/ldap/properties.yaml:122 msgid "lbl_users_search_query_filter" msgstr "" #. Default: Users search scope -#: pas/plugins/ldap/properties.yaml:114 +#: pas/plugins/ldap/properties.yaml:116 msgid "lbl_users_search_scope" msgstr "" #. Default: Users Settings -#: pas/plugins/ldap/properties.yaml:101 +#: pas/plugins/ldap/properties.yaml:103 msgid "lbl_users_settings" msgstr "" #. Default: No URI defined -#: pas/plugins/ldap/properties.yaml:22 +#: pas/plugins/ldap/properties.yaml:24 msgid "msg_connection_uri" msgstr "" #. Default: Group attribute aliases values are mandatory -#: pas/plugins/ldap/properties.yaml:239 +#: pas/plugins/ldap/properties.yaml:240 msgid "msg_group_attribute_aliases" msgstr "" #. Default: No Groups DN defined -#: pas/plugins/ldap/properties.yaml:207 +#: pas/plugins/ldap/properties.yaml:208 msgid "msg_groups_container_dn" msgstr "" #. Default: 'Page size must be given.' -#: pas/plugins/ldap/properties.yaml:97 +#: pas/plugins/ldap/properties.yaml:99 msgid "msg_page_size" msgstr "" #. Default: User attribute aliases values are mandatory -#: pas/plugins/ldap/properties.yaml:183 +#: pas/plugins/ldap/properties.yaml:184 msgid "msg_user_attribute_aliases" msgstr "" #. Default: No Users DN defined -#: pas/plugins/ldap/properties.yaml:109 +#: pas/plugins/ldap/properties.yaml:111 msgid "msg_users_container_dn" msgstr "" @@ -511,7 +506,7 @@ msgstr "" msgid "pas.plugins.ldap support for users and groups from ldap/active directory." msgstr "" -#: pas/plugins/ldap/plonecontrolpanel/cache.py:31 +#: pas/plugins/ldap/plonecontrolpanel/cache.py:33 #: pas/plugins/ldap/plonecontrolpanel/profiles/default/registry.xml msgid "servers, delimited by space" msgstr "" diff --git a/src/pas/plugins/ldap/locales/es/LC_MESSAGES/pas.plugins.ldap.po b/src/pas/plugins/ldap/locales/es/LC_MESSAGES/pas.plugins.ldap.po index 53afd48..e26d031 100644 --- a/src/pas/plugins/ldap/locales/es/LC_MESSAGES/pas.plugins.ldap.po +++ b/src/pas/plugins/ldap/locales/es/LC_MESSAGES/pas.plugins.ldap.po @@ -3,11 +3,10 @@ msgid "" msgstr "" "Project-Id-Version: pas.plugins.ldap\n" -"POT-Creation-Date: 2026-05-01 23:50+0000\n" -"PO-Revision-Date: 2026-05-02 01:51+0200\n" +"POT-Creation-Date: 2026-05-02 16:13+0000\n" +"PO-Revision-Date: 2026-05-02 16:32+0200\n" "Last-Translator: Leonardo J. Caballero G. \n" "Language-Team: \n" -"Language: es\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" @@ -16,31 +15,31 @@ msgstr "" "Language-Name: English\n" "Preferred-Encodings: utf-8 latin1\n" "Domain: pas.plugins.ldap\n" +"Language: es\n" "X-Generator: Poedit 3.9\n" "X-Is-Fallback-For: es-ar es-bo es-cl es-co es-cr es-do es-ec es-es es-sv es-gt es-hn es-mx es-ni es-pa es-py es-pe es-pr es-us es-uy es-ve\n" -#: pas/plugins/ldap/properties.py:37 +#: pas/plugins/ldap/properties.py:41 msgid "BASE" msgstr "BASE" -#: pas/plugins/ldap/plonecontrolpanel/controlpanel.pt:16 #: pas/plugins/ldap/zmi/manage_plugin.pt:28 msgid "Connection Test" msgstr "Prueba de conexión" -#: pas/plugins/ldap/properties.py:254 +#: pas/plugins/ldap/properties.py:279 msgid "Connection, users- and groups-access tested successfully." msgstr "Se ha comprobado correctamente el acceso a la conexión, a los usuarios y a los grupos." -#: pas/plugins/ldap/properties.py:43 +#: pas/plugins/ldap/properties.py:49 msgid "Days since Epoch" msgstr "Días desde el inicio del período" -#: pas/plugins/ldap/properties.py:253 +#: pas/plugins/ldap/properties.py:278 msgid "Exception in Groups; " msgstr "Excepción en Grupos; " -#: pas/plugins/ldap/properties.py:246 +#: pas/plugins/ldap/properties.py:271 msgid "Exception in Users; " msgstr "Excepción en Usuarios; " @@ -68,7 +67,7 @@ msgstr "Soporte con LDAP/Active Directory" msgid "LDAP Child Inspector" msgstr "Inspector secundario LDAP" -#: pas/plugins/ldap/plonecontrolpanel/controlpanel.pt:25 +#: pas/plugins/ldap/plonecontrolpanel/controlpanel.pt:30 msgid "LDAP Inspector" msgstr "Inspector LDAP" @@ -76,15 +75,15 @@ msgstr "Inspector LDAP" msgid "LDAP Plugin for PAS - Zope 2 Base Installation" msgstr "Plugin LDAP para PAS - Instalación base de Zope 2" -#: pas/plugins/ldap/plonecontrolpanel/controlpanel.py:43 +#: pas/plugins/ldap/plonecontrolpanel/controlpanel.py:51 msgid "LDAP Settings saved." msgstr "Configuración de LDAP guardada." -#: pas/plugins/ldap/properties.py:250 +#: pas/plugins/ldap/properties.py:275 msgid "LDAP Users ok, but groups not; " msgstr "Los usuarios de LDAP están bien, pero los grupos no; " -#: pas/plugins/ldap/properties.py:243 +#: pas/plugins/ldap/properties.py:268 msgid "LDAP users; " msgstr "Usuarios de LDAP; " @@ -96,31 +95,31 @@ msgstr "Administre las propiedades del complemento LDAP / AD para el id '${plugi msgid "No search base selected" msgstr "No se ha seleccionado ninguna base de búsqueda" -#: pas/plugins/ldap/properties.py:234 +#: pas/plugins/ldap/properties.py:259 msgid "Non-LDAP error while getting ILDAPGroupsConfig!" msgstr "¡Error no LDAP al obtener ILDAPGroupsConfig!" -#: pas/plugins/ldap/properties.py:222 +#: pas/plugins/ldap/properties.py:247 msgid "Non-LDAP error while getting ILDAPProps!" msgstr "¡Error no LDAP al obtener ILDAPProps!" -#: pas/plugins/ldap/properties.py:228 +#: pas/plugins/ldap/properties.py:253 msgid "Non-LDAP error while getting ILDAPUsersConfig!" msgstr "¡Error no LDAP al obtener ILDAPUsersConfig!" -#: pas/plugins/ldap/properties.py:38 +#: pas/plugins/ldap/properties.py:42 msgid "ONELEVEL" msgstr "UN NIVEL" -#: pas/plugins/ldap/properties.py:204 +#: pas/plugins/ldap/properties.py:229 msgid "Password is required for non-anonymous connections." msgstr "La contraseña es necesaria para las conexiones no anónimas." -#: pas/plugins/ldap/properties.py:39 +#: pas/plugins/ldap/properties.py:43 msgid "SUBTREE" msgstr "SUBÁRBOL" -#: pas/plugins/ldap/properties.py:44 +#: pas/plugins/ldap/properties.py:50 msgid "Seconds since epoch" msgstr "Segundos desde el inicio del período" @@ -128,7 +127,7 @@ msgstr "Segundos desde el inicio del período" msgid "Select search base" msgstr "Seleccione la base de búsqueda" -#: pas/plugins/ldap/properties.py:241 +#: pas/plugins/ldap/properties.py:266 msgid "Server Down" msgstr "Servidor caído" @@ -136,10 +135,6 @@ msgstr "Servidor caído" msgid "Set properties for users and groups from LDAP/ActiveDirectory using the ${plugin_name} plugin." msgstr "Establezca las propiedades de los usuarios y grupos de LDAP/ActiveDirectory mediante el complemento ${plugin_name}." -#: pas/plugins/ldap/plonecontrolpanel/controlpanel.pt:34 -msgid "Settings" -msgstr "Ajustes" - #: pas/plugins/ldap/plonecontrolpanel/configure.zcml:27 msgid "Uninstall LDAP Plugin for PAS" msgstr "Desinstalar el complemento LDAP para PAS" @@ -148,11 +143,11 @@ msgstr "Desinstalar el complemento LDAP para PAS" msgid "Uninstalls the pas.plugins.ldap add-on." msgstr "Desinstala el complemento pas.plugins.ldap." -#: pas/plugins/ldap/properties.py:209 +#: pas/plugins/ldap/properties.py:234 msgid "User/Password are required if not anonymous." msgstr "El usuario/contraseña son obligatorios si no son anónimos." -#: pas/plugins/ldap/properties.py:198 +#: pas/plugins/ldap/properties.py:223 msgid "Username is required for non-anonymous connections." msgstr "El nombre de usuario es necesario para las conexiones no anónimas." @@ -164,351 +159,351 @@ msgstr "Usuarios" msgid "You can configure an LDAP connection in this control panel. You can use a standard LDAP server or a Microsoft Active Directory server." msgstr "En este panel de control se puede configurar una conexión LDAP. Puede utilizar un servidor LDAP estándar o un servidor de Microsoft Active Directory." -#: pas/plugins/ldap/properties.py:353 +#: pas/plugins/ldap/properties.py:354 msgid "feature not available" msgstr "función no disponible" #. Default: LDAP attr name -#: pas/plugins/ldap/properties.yaml:243 +#: pas/plugins/ldap/properties.yaml:244 msgid "head_ldap_attr_name" msgstr "Nombre de atributo LDAP" #. Default: LDAP Attribute -#: pas/plugins/ldap/properties.yaml:187 +#: pas/plugins/ldap/properties.yaml:188 msgid "head_ldap_attribute" msgstr "Atributo LDAP" #. Default: Name on Sheet -#: pas/plugins/ldap/properties.yaml:194 +#: pas/plugins/ldap/properties.yaml:195 msgid "head_name_on_sheet" msgstr "Nombre en la hoja" #. Default: Reserved Key -#: pas/plugins/ldap/properties.yaml:186 +#: pas/plugins/ldap/properties.yaml:187 msgid "head_reserved_key" msgstr "Clave reservada" #. Default: Example, the protocol is ldap, the IP address 127.0.0.1 and the #. port 12345 -#: pas/plugins/ldap/properties.yaml:21 +#: pas/plugins/ldap/properties.yaml:23 msgid "help_connection_uri" msgstr "Ejemplo, el protocolo es ldap, la dirección IP es 127.0.0.1 y el puerto es 12345" #. Default: If set on authenticate a failing certificate chain check including #. CA is ignored. -#: pas/plugins/ldap/properties.yaml:58 +#: pas/plugins/ldap/properties.yaml:60 msgid "help_ignore_certificate_check" msgstr "Si se establece en autenticar una comprobación de cadena de certificado fallido incluyendo CA se ignora." #. Default: The timeout period, in seconds, for waiting for a connection to be #. established with the LDAP server. -#: pas/plugins/ldap/properties.yaml:34 +#: pas/plugins/ldap/properties.yaml:36 msgid "help_ldap_connection_timeout_in_seconds" msgstr "El tiempo de espera en segundos para esperar por una conexión sea establecida a servidor LDAP." #. Default: The timeout in seconds for an operation such as a search or update #. to complete. If no timeout is required, use -1 as the value. -#: pas/plugins/ldap/properties.yaml:41 +#: pas/plugins/ldap/properties.yaml:43 msgid "help_ldap_operation_timeout_in_seconds" msgstr "El tiempo de espera en segundos para una operación como una búsqueda o actualizar en completarse. Si se debe utilizar sin tiempo de espera, usar -1 como valor." #. Default: "Group DNs outside of the groups base DN are ignored, except if #. listed here" -#: pas/plugins/ldap/properties.yaml:156 +#: pas/plugins/ldap/properties.yaml:158 msgid "help_memberOf_external_allowed_group_dns" msgstr "Los DN de grupo que no sean el DN base del grupo se ignoran, excepto si se indican aquí" #. Default: global - same server for all ldap plugins -#: pas/plugins/ldap/properties.yaml:268 +#: pas/plugins/ldap/properties.yaml:269 msgid "help_memcached_server_to_use" msgstr "global - mismo servidor para todos los plugins ldap" #. Default: "One of those is mandatory like as one the following options #. groupOfNames, groupOfUniqueNames, posixGroup, group" -#: pas/plugins/ldap/properties.yaml:224 +#: pas/plugins/ldap/properties.yaml:225 msgid "help_object_classes_for_groups" msgstr "Uno de ellos es obligatorio como una de las siguientes opciones groupOfNames, groupOfUniqueNames, posixGroup y group" #. Default: Maximum page size, number of results to query the server at once #. for. -#: pas/plugins/ldap/properties.yaml:94 +#: pas/plugins/ldap/properties.yaml:96 msgid "help_page_size" msgstr "Tamaño máximo de la página, número de resultados que se pueden consultar a la vez en el servidor." #. Default: If your LDAP/AD supports it this will use #. LDAP_MATCHING_RULE_IN_CHAIN. By default only AD supports this. -#: pas/plugins/ldap/properties.yaml:151 +#: pas/plugins/ldap/properties.yaml:153 msgid "help_support_recursive_nested_groups" msgstr "Si su LDAP/AD lo admite, utilizará LDAP_MATCHING_RULE_IN_CHAIN. Por defecto sólo AD lo soporta." #. Default: If set, the LDAP server certificate is checked against the CA #. certificates in folder. -#: pas/plugins/ldap/properties.yaml:76 +#: pas/plugins/ldap/properties.yaml:78 msgid "help_tls_cacertdir" msgstr "Si se establece, el certificado del servidor LDAP se verifica con los certificados CA en la carpeta." #. Default: If set, the LDAP server certificate is checked against the CA #. certificate file. -#: pas/plugins/ldap/properties.yaml:70 +#: pas/plugins/ldap/properties.yaml:72 msgid "help_tls_cacertfile" msgstr "Si se establece, el certificado del servidor LDAP se verifica con el archivo de certificados CA." #. Default: If set, the client certificate is sent to the server. -#: pas/plugins/ldap/properties.yaml:82 +#: pas/plugins/ldap/properties.yaml:84 msgid "help_tls_clcertfile" msgstr "Si se establece, el certificado del cliente se envía al servidor." #. Default: If set, the client certificate is sent to the server. -#: pas/plugins/ldap/properties.yaml:88 +#: pas/plugins/ldap/properties.yaml:90 msgid "help_tls_clkeyfile" msgstr "Si se establece, la clave del certificado del cliente se envía al servidor." #. Default: If set, the connection is upgraded to TLS. -#: pas/plugins/ldap/properties.yaml:64 +#: pas/plugins/ldap/properties.yaml:66 msgid "help_use_tls_connection" msgstr "Si se establece, la conexión se actualiza a TLS." #. Default: Account expiration unit -#: pas/plugins/ldap/properties.yaml:175 +#: pas/plugins/ldap/properties.yaml:177 msgid "lbl_account_expiration_unit" msgstr "Unidad de caducidad de la cuenta" #. Default: Anonymous Connection? -#: pas/plugins/ldap/properties.yaml:28 +#: pas/plugins/ldap/properties.yaml:30 msgid "lbl_anonymous_connection" msgstr "¿Conexión anónima?" #. Default: Attribute containing expiration Time -#: pas/plugins/ldap/properties.yaml:170 +#: pas/plugins/ldap/properties.yaml:172 msgid "lbl_attribute_containing_expiration_time" msgstr "Atributo que contiene la hora de expiración" #. Default: Cache LDAP queries -#: pas/plugins/ldap/properties.yaml:262 +#: pas/plugins/ldap/properties.yaml:263 msgid "lbl_cache_ldap_queries" msgstr "Caché de consultas LDAP" #. Default: Cache Settings -#: pas/plugins/ldap/properties.yaml:255 +#: pas/plugins/ldap/properties.yaml:256 msgid "lbl_cache_settings" msgstr "Configuración de la caché" #. Default: Cache timeout in seconds -#: pas/plugins/ldap/properties.yaml:275 +#: pas/plugins/ldap/properties.yaml:276 msgid "lbl_cache_timeout_in_seconds" msgstr "Tiempo de espera de la caché en segundos" #. Default: Connection URI -#: pas/plugins/ldap/properties.yaml:20 +#: pas/plugins/ldap/properties.yaml:22 msgid "lbl_connection_uri" msgstr "URI de conexión" #. Default: Group attribute aliases -#: pas/plugins/ldap/properties.yaml:238 +#: pas/plugins/ldap/properties.yaml:239 msgid "lbl_group_attribute_aliases" msgstr "Alias de atributos de grupo" #. Default: Group Property-Sheet Attributes -#: pas/plugins/ldap/properties.yaml:248 +#: pas/plugins/ldap/properties.yaml:249 msgid "lbl_group_property-sheet_attributes" msgstr "Atributos de la hoja de propiedades de grupo" #. Default: Groups container DN -#: pas/plugins/ldap/properties.yaml:206 +#: pas/plugins/ldap/properties.yaml:207 msgid "lbl_groups_container_dn" msgstr "DN del contenedor de Grupos" #. Default: Groups search query filter -#: pas/plugins/ldap/properties.yaml:218 +#: pas/plugins/ldap/properties.yaml:219 msgid "lbl_groups_search_query_filter" msgstr "Filtro de búsqueda de grupos" #. Default: Groups search scope -#: pas/plugins/ldap/properties.yaml:212 +#: pas/plugins/ldap/properties.yaml:213 msgid "lbl_groups_search_scope" msgstr "Ámbito de búsqueda de grupos" #. Default: Groups Settings -#: pas/plugins/ldap/properties.yaml:199 +#: pas/plugins/ldap/properties.yaml:200 msgid "lbl_groups_settings" msgstr "Configuración de grupos" #. Default: Ignore certificate check? -#: pas/plugins/ldap/properties.yaml:57 +#: pas/plugins/ldap/properties.yaml:59 msgid "lbl_ignore_certificate_check" msgstr "¿Ignorar la comprobación del certificado?" #. Default: LDAP connection timeout in seconds. -#: pas/plugins/ldap/properties.yaml:33 +#: pas/plugins/ldap/properties.yaml:35 msgid "lbl_ldap_connection_timeout_in_seconds" msgstr "Tiempo de espera de la conexión LDAP en segundos." #. Default: LDAP operation timeout in seconds. -#: pas/plugins/ldap/properties.yaml:40 +#: pas/plugins/ldap/properties.yaml:42 msgid "lbl_ldap_operation_timeout_in_seconds" msgstr "Tiempo de espera de la operación LDAP en segundos." #. Default: LDAP Server Settings -#: pas/plugins/ldap/properties.yaml:10 +#: pas/plugins/ldap/properties.yaml:12 msgid "lbl_ldap_server_settings" msgstr "Configuración del servidor LDAP" #. Default: Manager Password -#: pas/plugins/ldap/properties.yaml:52 +#: pas/plugins/ldap/properties.yaml:54 msgid "lbl_manager_password" msgstr "Contraseña de administrador" #. Default: Manager User -#: pas/plugins/ldap/properties.yaml:47 +#: pas/plugins/ldap/properties.yaml:49 msgid "lbl_manager_user" msgstr "Usuario de administrador" #. Default: memberOf attribute supported? -#: pas/plugins/ldap/properties.yaml:135 +#: pas/plugins/ldap/properties.yaml:137 msgid "lbl_memberOf_attribute_supported" msgstr "¿se admite el atributo memberOf?" #. Default: memberOf external allowed Group DNs -#: pas/plugins/ldap/properties.yaml:157 +#: pas/plugins/ldap/properties.yaml:159 msgid "lbl_memberOf_external_allowed_group_dns" msgstr "DNs de grupo permitidos memberOf externos" #. Default: Memcached Server to use -#: pas/plugins/ldap/properties.yaml:267 +#: pas/plugins/ldap/properties.yaml:268 msgid "lbl_memcached_server_to_use" msgstr "Servidor Memcached a utilizar" #. Default: Object class -#: pas/plugins/ldap/properties.yaml:126 +#: pas/plugins/ldap/properties.yaml:128 msgid "lbl_object_class" msgstr "Clase de objeto" #. Default: Object classes for Groups -#: pas/plugins/ldap/properties.yaml:223 +#: pas/plugins/ldap/properties.yaml:224 msgid "lbl_object_classes_for_groups" msgstr "Clases de objetos para Grupos" #. Default: Object classes for User creation -#: pas/plugins/ldap/properties.yaml:125 +#: pas/plugins/ldap/properties.yaml:127 msgid "lbl_object_classes_for_user_creation" msgstr "Clases de objetos para la creación de usuarios" #. Default: Page Size -#: pas/plugins/ldap/properties.yaml:93 +#: pas/plugins/ldap/properties.yaml:95 msgid "lbl_page_size" msgstr "Tamaño de página" #. Default: Role -#: pas/plugins/ldap/properties.yaml:141 +#: pas/plugins/ldap/properties.yaml:143 msgid "lbl_role" msgstr "Rol" #. Default: Roles acquired -#: pas/plugins/ldap/properties.yaml:140 +#: pas/plugins/ldap/properties.yaml:142 msgid "lbl_roles_acquired" msgstr "Roles adquiridos" #. Default: Save -#: pas/plugins/ldap/properties.yaml:284 +#: pas/plugins/ldap/properties.yaml:285 msgid "lbl_save" msgstr "Guardar" #. Default: Support recursive/nested groups? -#: pas/plugins/ldap/properties.yaml:150 +#: pas/plugins/ldap/properties.yaml:152 msgid "lbl_support_recursive_nested_groups" msgstr "¿Admite grupos recursivos/anidados?" #. Default: Path to folder with CA certificate files for TLS communication #. (OPT_X_TLS_CACERTDIR) -#: pas/plugins/ldap/properties.yaml:75 +#: pas/plugins/ldap/properties.yaml:77 msgid "lbl_tls_cacertdir" msgstr "Ruta a la carpeta con archivos de certificados CA para la comunicación TLS (OPT_X_TLS_CACERTDIR)" #. Default: Path to CA certificate file for TLS communication #. (OPT_X_TLS_CACERTFILE) -#: pas/plugins/ldap/properties.yaml:69 +#: pas/plugins/ldap/properties.yaml:71 msgid "lbl_tls_cacertfile" msgstr "Ruta al archivo de certificados CA para la comunicación TLS (OPT_X_TLS_CACERTFILE)" #. Default: Path to client certificate file for TLS communication #. (OPT_X_TLS_CERTFILE). Requires tls_clkeyfile -#: pas/plugins/ldap/properties.yaml:81 +#: pas/plugins/ldap/properties.yaml:83 msgid "lbl_tls_clcertfile" msgstr "Ruta al archivo de certificado del cliente para la comunicación TLS (OPT_X_TLS_CERTFILE). Requiere tls_clkeyfile" #. Default: Path to client certificate key for TLS communication #. (OPT_X_TLS_KEYFILE). Requires tls_clcertfile -#: pas/plugins/ldap/properties.yaml:87 +#: pas/plugins/ldap/properties.yaml:89 msgid "lbl_tls_clkeyfile" msgstr "Ruta a la clave del certificado del cliente para la comunicación TLS (OPT_X_TLS_KEYFILE). Requiere tls_clcertfile" #. Default: Use TLS connection -#: pas/plugins/ldap/properties.yaml:63 +#: pas/plugins/ldap/properties.yaml:65 msgid "lbl_use_tls_connection" msgstr "Usar conexión TLS" #. Default: User Accounts expires? -#: pas/plugins/ldap/properties.yaml:165 +#: pas/plugins/ldap/properties.yaml:167 msgid "lbl_user_accounts_expires" msgstr "¿Caducan las cuentas de usuario?" #. Default: User attribute aliases -#: pas/plugins/ldap/properties.yaml:182 +#: pas/plugins/ldap/properties.yaml:183 msgid "lbl_user_attribute_aliases" msgstr "Alias de atributos de usuario" #. Default: User Property-Sheet Attributes -#: pas/plugins/ldap/properties.yaml:192 +#: pas/plugins/ldap/properties.yaml:193 msgid "lbl_user_property-sheet_attributes" msgstr "Atributos de la hoja de propiedades del usuario" #. Default: Users container DN -#: pas/plugins/ldap/properties.yaml:108 +#: pas/plugins/ldap/properties.yaml:110 msgid "lbl_users_container_dn" msgstr "DN del contenedor de usuarios" #. Default: Users search query filter -#: pas/plugins/ldap/properties.yaml:120 +#: pas/plugins/ldap/properties.yaml:122 msgid "lbl_users_search_query_filter" msgstr "Filtro de consulta de búsqueda de usuarios" #. Default: Users search scope -#: pas/plugins/ldap/properties.yaml:114 +#: pas/plugins/ldap/properties.yaml:116 msgid "lbl_users_search_scope" msgstr "Ámbito de búsqueda de los usuarios" #. Default: Users Settings -#: pas/plugins/ldap/properties.yaml:101 +#: pas/plugins/ldap/properties.yaml:103 msgid "lbl_users_settings" msgstr "Configuración de usuarios" #. Default: No URI defined -#: pas/plugins/ldap/properties.yaml:22 +#: pas/plugins/ldap/properties.yaml:24 msgid "msg_connection_uri" msgstr "No hay URI definido" #. Default: Group attribute aliases values are mandatory -#: pas/plugins/ldap/properties.yaml:239 +#: pas/plugins/ldap/properties.yaml:240 msgid "msg_group_attribute_aliases" msgstr "Los valores de los alias de atributos de grupo son obligatorios" #. Default: No Groups DN defined -#: pas/plugins/ldap/properties.yaml:207 +#: pas/plugins/ldap/properties.yaml:208 msgid "msg_groups_container_dn" msgstr "No hay grupos DN definidos" #. Default: 'Page size must be given.' -#: pas/plugins/ldap/properties.yaml:97 +#: pas/plugins/ldap/properties.yaml:99 msgid "msg_page_size" msgstr "'Debe indicarse el tamaño de la página.'" #. Default: User attribute aliases values are mandatory -#: pas/plugins/ldap/properties.yaml:183 +#: pas/plugins/ldap/properties.yaml:184 msgid "msg_user_attribute_aliases" msgstr "Los valores de los alias de atributos de usuario son obligatorios" #. Default: No Users DN defined -#: pas/plugins/ldap/properties.yaml:109 +#: pas/plugins/ldap/properties.yaml:111 msgid "msg_users_container_dn" msgstr "No se ha definido ningún DN de usuario" @@ -516,7 +511,7 @@ msgstr "No se ha definido ningún DN de usuario" msgid "pas.plugins.ldap support for users and groups from ldap/active directory." msgstr "Soporte para usuarios y grupos de LDAP/Active Directory con el complemento pas.plugins.ldap." -#: pas/plugins/ldap/plonecontrolpanel/cache.py:31 +#: pas/plugins/ldap/plonecontrolpanel/cache.py:33 #: pas/plugins/ldap/plonecontrolpanel/profiles/default/registry.xml msgid "servers, delimited by space" msgstr "servidores, delimitados por el espacio" diff --git a/src/pas/plugins/ldap/locales/merge-lingua.pot b/src/pas/plugins/ldap/locales/merge-lingua.pot index 8eb44b7..d56924a 100644 --- a/src/pas/plugins/ldap/locales/merge-lingua.pot +++ b/src/pas/plugins/ldap/locales/merge-lingua.pot @@ -6,7 +6,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE 1.0\n" -"POT-Creation-Date: 2026-05-02 01:33+0200\n" +"POT-Creation-Date: 2026-05-02 18:13+0200\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -17,352 +17,351 @@ msgstr "" "Generated-By: Lingua 4.15.0\n" #. Default: LDAP Server Settings -#: /home/plone/project/backend/sources/pas.plugins.ldap/src/pas/plugins/ldap/properties.yaml:10 +#: /home/plone/project/backend/sources/pas.plugins.ldap/src/pas/plugins/ldap/properties.yaml:12 msgid "lbl_ldap_server_settings" msgstr "" #. Default: Connection URI -#: /home/plone/project/backend/sources/pas.plugins.ldap/src/pas/plugins/ldap/properties.yaml:20 +#: /home/plone/project/backend/sources/pas.plugins.ldap/src/pas/plugins/ldap/properties.yaml:22 msgid "lbl_connection_uri" msgstr "" #. Default: Example, the protocol is ldap, the IP address 127.0.0.1 and the #. port 12345 -#: /home/plone/project/backend/sources/pas.plugins.ldap/src/pas/plugins/ldap/properties.yaml:21 +#: /home/plone/project/backend/sources/pas.plugins.ldap/src/pas/plugins/ldap/properties.yaml:23 msgid "help_connection_uri" msgstr "" #. Default: No URI defined -#: /home/plone/project/backend/sources/pas.plugins.ldap/src/pas/plugins/ldap/properties.yaml:22 +#: /home/plone/project/backend/sources/pas.plugins.ldap/src/pas/plugins/ldap/properties.yaml:24 msgid "msg_connection_uri" msgstr "" #. Default: Anonymous Connection? -#: /home/plone/project/backend/sources/pas.plugins.ldap/src/pas/plugins/ldap/properties.yaml:28 +#: /home/plone/project/backend/sources/pas.plugins.ldap/src/pas/plugins/ldap/properties.yaml:30 msgid "lbl_anonymous_connection" msgstr "" #. Default: LDAP connection timeout in seconds. -#: /home/plone/project/backend/sources/pas.plugins.ldap/src/pas/plugins/ldap/properties.yaml:33 +#: /home/plone/project/backend/sources/pas.plugins.ldap/src/pas/plugins/ldap/properties.yaml:35 msgid "lbl_ldap_connection_timeout_in_seconds" msgstr "" #. Default: The timeout period, in seconds, for waiting for a connection to be #. established with the LDAP server. -#: /home/plone/project/backend/sources/pas.plugins.ldap/src/pas/plugins/ldap/properties.yaml:34 +#: /home/plone/project/backend/sources/pas.plugins.ldap/src/pas/plugins/ldap/properties.yaml:36 msgid "help_ldap_connection_timeout_in_seconds" msgstr "" #. Default: LDAP operation timeout in seconds. -#: /home/plone/project/backend/sources/pas.plugins.ldap/src/pas/plugins/ldap/properties.yaml:40 +#: /home/plone/project/backend/sources/pas.plugins.ldap/src/pas/plugins/ldap/properties.yaml:42 msgid "lbl_ldap_operation_timeout_in_seconds" msgstr "" #. Default: The timeout in seconds for an operation such as a search or update #. to complete. If no timeout is required, use -1 as the value. -#: /home/plone/project/backend/sources/pas.plugins.ldap/src/pas/plugins/ldap/properties.yaml:41 +#: /home/plone/project/backend/sources/pas.plugins.ldap/src/pas/plugins/ldap/properties.yaml:43 msgid "help_ldap_operation_timeout_in_seconds" msgstr "" #. Default: Manager User -#: /home/plone/project/backend/sources/pas.plugins.ldap/src/pas/plugins/ldap/properties.yaml:47 +#: /home/plone/project/backend/sources/pas.plugins.ldap/src/pas/plugins/ldap/properties.yaml:49 msgid "lbl_manager_user" msgstr "" #. Default: Manager Password -#: /home/plone/project/backend/sources/pas.plugins.ldap/src/pas/plugins/ldap/properties.yaml:52 +#: /home/plone/project/backend/sources/pas.plugins.ldap/src/pas/plugins/ldap/properties.yaml:54 msgid "lbl_manager_password" msgstr "" #. Default: Ignore certificate check? -#: /home/plone/project/backend/sources/pas.plugins.ldap/src/pas/plugins/ldap/properties.yaml:57 +#: /home/plone/project/backend/sources/pas.plugins.ldap/src/pas/plugins/ldap/properties.yaml:59 msgid "lbl_ignore_certificate_check" msgstr "" #. Default: If set on authenticate a failing certificate chain check including #. CA is ignored. -#: /home/plone/project/backend/sources/pas.plugins.ldap/src/pas/plugins/ldap/properties.yaml:58 +#: /home/plone/project/backend/sources/pas.plugins.ldap/src/pas/plugins/ldap/properties.yaml:60 msgid "help_ignore_certificate_check" msgstr "" #. Default: Use TLS connection -#: /home/plone/project/backend/sources/pas.plugins.ldap/src/pas/plugins/ldap/properties.yaml:63 +#: /home/plone/project/backend/sources/pas.plugins.ldap/src/pas/plugins/ldap/properties.yaml:65 msgid "lbl_use_tls_connection" msgstr "" #. Default: If set, the connection is upgraded to TLS. -#: /home/plone/project/backend/sources/pas.plugins.ldap/src/pas/plugins/ldap/properties.yaml:64 +#: /home/plone/project/backend/sources/pas.plugins.ldap/src/pas/plugins/ldap/properties.yaml:66 msgid "help_use_tls_connection" msgstr "" #. Default: Path to CA certificate file for TLS communication #. (OPT_X_TLS_CACERTFILE) -#: /home/plone/project/backend/sources/pas.plugins.ldap/src/pas/plugins/ldap/properties.yaml:69 +#: /home/plone/project/backend/sources/pas.plugins.ldap/src/pas/plugins/ldap/properties.yaml:71 msgid "lbl_tls_cacertfile" msgstr "" #. Default: If set, the LDAP server certificate is checked against the CA #. certificate file. -#: /home/plone/project/backend/sources/pas.plugins.ldap/src/pas/plugins/ldap/properties.yaml:70 +#: /home/plone/project/backend/sources/pas.plugins.ldap/src/pas/plugins/ldap/properties.yaml:72 msgid "help_tls_cacertfile" msgstr "" #. Default: Path to folder with CA certificate files for TLS communication #. (OPT_X_TLS_CACERTDIR) -#: /home/plone/project/backend/sources/pas.plugins.ldap/src/pas/plugins/ldap/properties.yaml:75 +#: /home/plone/project/backend/sources/pas.plugins.ldap/src/pas/plugins/ldap/properties.yaml:77 msgid "lbl_tls_cacertdir" msgstr "" #. Default: If set, the LDAP server certificate is checked against the CA #. certificates in folder. -#: /home/plone/project/backend/sources/pas.plugins.ldap/src/pas/plugins/ldap/properties.yaml:76 +#: /home/plone/project/backend/sources/pas.plugins.ldap/src/pas/plugins/ldap/properties.yaml:78 msgid "help_tls_cacertdir" msgstr "" #. Default: Path to client certificate file for TLS communication #. (OPT_X_TLS_CERTFILE). Requires tls_clkeyfile -#: /home/plone/project/backend/sources/pas.plugins.ldap/src/pas/plugins/ldap/properties.yaml:81 +#: /home/plone/project/backend/sources/pas.plugins.ldap/src/pas/plugins/ldap/properties.yaml:83 msgid "lbl_tls_clcertfile" msgstr "" #. Default: If set, the client certificate is sent to the server. -#: /home/plone/project/backend/sources/pas.plugins.ldap/src/pas/plugins/ldap/properties.yaml:82 +#: /home/plone/project/backend/sources/pas.plugins.ldap/src/pas/plugins/ldap/properties.yaml:84 msgid "help_tls_clcertfile" msgstr "" #. Default: Path to client certificate key for TLS communication #. (OPT_X_TLS_KEYFILE). Requires tls_clcertfile -#: /home/plone/project/backend/sources/pas.plugins.ldap/src/pas/plugins/ldap/properties.yaml:87 +#: /home/plone/project/backend/sources/pas.plugins.ldap/src/pas/plugins/ldap/properties.yaml:89 msgid "lbl_tls_clkeyfile" msgstr "" #. Default: If set, the client certificate is sent to the server. -#: /home/plone/project/backend/sources/pas.plugins.ldap/src/pas/plugins/ldap/properties.yaml:88 +#: /home/plone/project/backend/sources/pas.plugins.ldap/src/pas/plugins/ldap/properties.yaml:90 msgid "help_tls_clkeyfile" msgstr "" #. Default: Page Size -#: /home/plone/project/backend/sources/pas.plugins.ldap/src/pas/plugins/ldap/properties.yaml:93 +#: /home/plone/project/backend/sources/pas.plugins.ldap/src/pas/plugins/ldap/properties.yaml:95 msgid "lbl_page_size" msgstr "" #. Default: Maximum page size, number of results to query the server at once #. for. -#: /home/plone/project/backend/sources/pas.plugins.ldap/src/pas/plugins/ldap/properties.yaml:94 +#: /home/plone/project/backend/sources/pas.plugins.ldap/src/pas/plugins/ldap/properties.yaml:96 msgid "help_page_size" msgstr "" #. Default: 'Page size must be given.' -#: /home/plone/project/backend/sources/pas.plugins.ldap/src/pas/plugins/ldap/properties.yaml:97 +#: /home/plone/project/backend/sources/pas.plugins.ldap/src/pas/plugins/ldap/properties.yaml:99 msgid "msg_page_size" msgstr "" #. Default: Users Settings -#: /home/plone/project/backend/sources/pas.plugins.ldap/src/pas/plugins/ldap/properties.yaml:101 +#: /home/plone/project/backend/sources/pas.plugins.ldap/src/pas/plugins/ldap/properties.yaml:103 msgid "lbl_users_settings" msgstr "" #. Default: Users container DN -#: /home/plone/project/backend/sources/pas.plugins.ldap/src/pas/plugins/ldap/properties.yaml:108 +#: /home/plone/project/backend/sources/pas.plugins.ldap/src/pas/plugins/ldap/properties.yaml:110 msgid "lbl_users_container_dn" msgstr "" #. Default: No Users DN defined -#: /home/plone/project/backend/sources/pas.plugins.ldap/src/pas/plugins/ldap/properties.yaml:109 +#: /home/plone/project/backend/sources/pas.plugins.ldap/src/pas/plugins/ldap/properties.yaml:111 msgid "msg_users_container_dn" msgstr "" #. Default: Users search scope -#: /home/plone/project/backend/sources/pas.plugins.ldap/src/pas/plugins/ldap/properties.yaml:114 +#: /home/plone/project/backend/sources/pas.plugins.ldap/src/pas/plugins/ldap/properties.yaml:116 msgid "lbl_users_search_scope" msgstr "" #. Default: Users search query filter -#: /home/plone/project/backend/sources/pas.plugins.ldap/src/pas/plugins/ldap/properties.yaml:120 +#: /home/plone/project/backend/sources/pas.plugins.ldap/src/pas/plugins/ldap/properties.yaml:122 msgid "lbl_users_search_query_filter" msgstr "" #. Default: Object classes for User creation -#: /home/plone/project/backend/sources/pas.plugins.ldap/src/pas/plugins/ldap/properties.yaml:125 +#: /home/plone/project/backend/sources/pas.plugins.ldap/src/pas/plugins/ldap/properties.yaml:127 msgid "lbl_object_classes_for_user_creation" msgstr "" #. Default: Object class -#: /home/plone/project/backend/sources/pas.plugins.ldap/src/pas/plugins/ldap/properties.yaml:126 -#: /home/plone/project/backend/sources/pas.plugins.ldap/src/pas/plugins/ldap/properties.yaml:225 +#: /home/plone/project/backend/sources/pas.plugins.ldap/src/pas/plugins/ldap/properties.yaml:128 +#: /home/plone/project/backend/sources/pas.plugins.ldap/src/pas/plugins/ldap/properties.yaml:226 msgid "lbl_object_class" msgstr "" #. Default: memberOf attribute supported? -#: /home/plone/project/backend/sources/pas.plugins.ldap/src/pas/plugins/ldap/properties.yaml:135 -#: /home/plone/project/backend/sources/pas.plugins.ldap/src/pas/plugins/ldap/properties.yaml:233 +#: /home/plone/project/backend/sources/pas.plugins.ldap/src/pas/plugins/ldap/properties.yaml:137 +#: /home/plone/project/backend/sources/pas.plugins.ldap/src/pas/plugins/ldap/properties.yaml:234 msgid "lbl_memberOf_attribute_supported" msgstr "" #. Default: Roles acquired -#: /home/plone/project/backend/sources/pas.plugins.ldap/src/pas/plugins/ldap/properties.yaml:140 +#: /home/plone/project/backend/sources/pas.plugins.ldap/src/pas/plugins/ldap/properties.yaml:142 msgid "lbl_roles_acquired" msgstr "" #. Default: Role -#: /home/plone/project/backend/sources/pas.plugins.ldap/src/pas/plugins/ldap/properties.yaml:141 +#: /home/plone/project/backend/sources/pas.plugins.ldap/src/pas/plugins/ldap/properties.yaml:143 msgid "lbl_role" msgstr "" #. Default: Support recursive/nested groups? -#: /home/plone/project/backend/sources/pas.plugins.ldap/src/pas/plugins/ldap/properties.yaml:150 +#: /home/plone/project/backend/sources/pas.plugins.ldap/src/pas/plugins/ldap/properties.yaml:152 msgid "lbl_support_recursive_nested_groups" msgstr "" #. Default: If your LDAP/AD supports it this will use #. LDAP_MATCHING_RULE_IN_CHAIN. By default only AD supports this. -#: /home/plone/project/backend/sources/pas.plugins.ldap/src/pas/plugins/ldap/properties.yaml:151 +#: /home/plone/project/backend/sources/pas.plugins.ldap/src/pas/plugins/ldap/properties.yaml:153 msgid "help_support_recursive_nested_groups" msgstr "" #. Default: "Group DNs outside of the groups base DN are ignored, except if #. listed here" -#: /home/plone/project/backend/sources/pas.plugins.ldap/src/pas/plugins/ldap/properties.yaml:156 +#: /home/plone/project/backend/sources/pas.plugins.ldap/src/pas/plugins/ldap/properties.yaml:158 msgid "help_memberOf_external_allowed_group_dns" msgstr "" #. Default: memberOf external allowed Group DNs -#: /home/plone/project/backend/sources/pas.plugins.ldap/src/pas/plugins/ldap/properties.yaml:157 +#: /home/plone/project/backend/sources/pas.plugins.ldap/src/pas/plugins/ldap/properties.yaml:159 msgid "lbl_memberOf_external_allowed_group_dns" msgstr "" #. Default: User Accounts expires? -#: /home/plone/project/backend/sources/pas.plugins.ldap/src/pas/plugins/ldap/properties.yaml:165 +#: /home/plone/project/backend/sources/pas.plugins.ldap/src/pas/plugins/ldap/properties.yaml:167 msgid "lbl_user_accounts_expires" msgstr "" #. Default: Attribute containing expiration Time -#: /home/plone/project/backend/sources/pas.plugins.ldap/src/pas/plugins/ldap/properties.yaml:170 +#: /home/plone/project/backend/sources/pas.plugins.ldap/src/pas/plugins/ldap/properties.yaml:172 msgid "lbl_attribute_containing_expiration_time" msgstr "" #. Default: Account expiration unit -#: /home/plone/project/backend/sources/pas.plugins.ldap/src/pas/plugins/ldap/properties.yaml:175 +#: /home/plone/project/backend/sources/pas.plugins.ldap/src/pas/plugins/ldap/properties.yaml:177 msgid "lbl_account_expiration_unit" msgstr "" #. Default: User attribute aliases -#: /home/plone/project/backend/sources/pas.plugins.ldap/src/pas/plugins/ldap/properties.yaml:182 +#: /home/plone/project/backend/sources/pas.plugins.ldap/src/pas/plugins/ldap/properties.yaml:183 msgid "lbl_user_attribute_aliases" msgstr "" #. Default: User attribute aliases values are mandatory -#: /home/plone/project/backend/sources/pas.plugins.ldap/src/pas/plugins/ldap/properties.yaml:183 +#: /home/plone/project/backend/sources/pas.plugins.ldap/src/pas/plugins/ldap/properties.yaml:184 msgid "msg_user_attribute_aliases" msgstr "" #. Default: Reserved Key -#. Default: Reserved key -#: /home/plone/project/backend/sources/pas.plugins.ldap/src/pas/plugins/ldap/properties.yaml:186 -#: /home/plone/project/backend/sources/pas.plugins.ldap/src/pas/plugins/ldap/properties.yaml:242 +#: /home/plone/project/backend/sources/pas.plugins.ldap/src/pas/plugins/ldap/properties.yaml:187 +#: /home/plone/project/backend/sources/pas.plugins.ldap/src/pas/plugins/ldap/properties.yaml:243 msgid "head_reserved_key" msgstr "" #. Default: LDAP Attribute -#: /home/plone/project/backend/sources/pas.plugins.ldap/src/pas/plugins/ldap/properties.yaml:187 -#: /home/plone/project/backend/sources/pas.plugins.ldap/src/pas/plugins/ldap/properties.yaml:195 -#: /home/plone/project/backend/sources/pas.plugins.ldap/src/pas/plugins/ldap/properties.yaml:251 +#: /home/plone/project/backend/sources/pas.plugins.ldap/src/pas/plugins/ldap/properties.yaml:188 +#: /home/plone/project/backend/sources/pas.plugins.ldap/src/pas/plugins/ldap/properties.yaml:196 +#: /home/plone/project/backend/sources/pas.plugins.ldap/src/pas/plugins/ldap/properties.yaml:252 msgid "head_ldap_attribute" msgstr "" #. Default: User Property-Sheet Attributes -#: /home/plone/project/backend/sources/pas.plugins.ldap/src/pas/plugins/ldap/properties.yaml:192 +#: /home/plone/project/backend/sources/pas.plugins.ldap/src/pas/plugins/ldap/properties.yaml:193 msgid "lbl_user_property-sheet_attributes" msgstr "" #. Default: Name on Sheet -#: /home/plone/project/backend/sources/pas.plugins.ldap/src/pas/plugins/ldap/properties.yaml:194 -#: /home/plone/project/backend/sources/pas.plugins.ldap/src/pas/plugins/ldap/properties.yaml:250 +#: /home/plone/project/backend/sources/pas.plugins.ldap/src/pas/plugins/ldap/properties.yaml:195 +#: /home/plone/project/backend/sources/pas.plugins.ldap/src/pas/plugins/ldap/properties.yaml:251 msgid "head_name_on_sheet" msgstr "" #. Default: Groups Settings -#: /home/plone/project/backend/sources/pas.plugins.ldap/src/pas/plugins/ldap/properties.yaml:199 +#: /home/plone/project/backend/sources/pas.plugins.ldap/src/pas/plugins/ldap/properties.yaml:200 msgid "lbl_groups_settings" msgstr "" #. Default: Groups container DN -#: /home/plone/project/backend/sources/pas.plugins.ldap/src/pas/plugins/ldap/properties.yaml:206 +#: /home/plone/project/backend/sources/pas.plugins.ldap/src/pas/plugins/ldap/properties.yaml:207 msgid "lbl_groups_container_dn" msgstr "" #. Default: No Groups DN defined -#: /home/plone/project/backend/sources/pas.plugins.ldap/src/pas/plugins/ldap/properties.yaml:207 +#: /home/plone/project/backend/sources/pas.plugins.ldap/src/pas/plugins/ldap/properties.yaml:208 msgid "msg_groups_container_dn" msgstr "" #. Default: Groups search scope -#: /home/plone/project/backend/sources/pas.plugins.ldap/src/pas/plugins/ldap/properties.yaml:212 +#: /home/plone/project/backend/sources/pas.plugins.ldap/src/pas/plugins/ldap/properties.yaml:213 msgid "lbl_groups_search_scope" msgstr "" #. Default: Groups search query filter -#: /home/plone/project/backend/sources/pas.plugins.ldap/src/pas/plugins/ldap/properties.yaml:218 +#: /home/plone/project/backend/sources/pas.plugins.ldap/src/pas/plugins/ldap/properties.yaml:219 msgid "lbl_groups_search_query_filter" msgstr "" #. Default: Object classes for Groups -#: /home/plone/project/backend/sources/pas.plugins.ldap/src/pas/plugins/ldap/properties.yaml:223 +#: /home/plone/project/backend/sources/pas.plugins.ldap/src/pas/plugins/ldap/properties.yaml:224 msgid "lbl_object_classes_for_groups" msgstr "" #. Default: "One of those is mandatory like as one the following options #. groupOfNames, groupOfUniqueNames, posixGroup, group" -#: /home/plone/project/backend/sources/pas.plugins.ldap/src/pas/plugins/ldap/properties.yaml:224 +#: /home/plone/project/backend/sources/pas.plugins.ldap/src/pas/plugins/ldap/properties.yaml:225 msgid "help_object_classes_for_groups" msgstr "" #. Default: Group attribute aliases -#: /home/plone/project/backend/sources/pas.plugins.ldap/src/pas/plugins/ldap/properties.yaml:238 +#: /home/plone/project/backend/sources/pas.plugins.ldap/src/pas/plugins/ldap/properties.yaml:239 msgid "lbl_group_attribute_aliases" msgstr "" #. Default: Group attribute aliases values are mandatory -#: /home/plone/project/backend/sources/pas.plugins.ldap/src/pas/plugins/ldap/properties.yaml:239 +#: /home/plone/project/backend/sources/pas.plugins.ldap/src/pas/plugins/ldap/properties.yaml:240 msgid "msg_group_attribute_aliases" msgstr "" #. Default: LDAP attr name -#: /home/plone/project/backend/sources/pas.plugins.ldap/src/pas/plugins/ldap/properties.yaml:243 +#: /home/plone/project/backend/sources/pas.plugins.ldap/src/pas/plugins/ldap/properties.yaml:244 msgid "head_ldap_attr_name" msgstr "" #. Default: Group Property-Sheet Attributes -#: /home/plone/project/backend/sources/pas.plugins.ldap/src/pas/plugins/ldap/properties.yaml:248 +#: /home/plone/project/backend/sources/pas.plugins.ldap/src/pas/plugins/ldap/properties.yaml:249 msgid "lbl_group_property-sheet_attributes" msgstr "" #. Default: Cache Settings -#: /home/plone/project/backend/sources/pas.plugins.ldap/src/pas/plugins/ldap/properties.yaml:255 +#: /home/plone/project/backend/sources/pas.plugins.ldap/src/pas/plugins/ldap/properties.yaml:256 msgid "lbl_cache_settings" msgstr "" #. Default: Cache LDAP queries -#: /home/plone/project/backend/sources/pas.plugins.ldap/src/pas/plugins/ldap/properties.yaml:262 +#: /home/plone/project/backend/sources/pas.plugins.ldap/src/pas/plugins/ldap/properties.yaml:263 msgid "lbl_cache_ldap_queries" msgstr "" #. Default: Memcached Server to use -#: /home/plone/project/backend/sources/pas.plugins.ldap/src/pas/plugins/ldap/properties.yaml:267 +#: /home/plone/project/backend/sources/pas.plugins.ldap/src/pas/plugins/ldap/properties.yaml:268 msgid "lbl_memcached_server_to_use" msgstr "" #. Default: global - same server for all ldap plugins -#: /home/plone/project/backend/sources/pas.plugins.ldap/src/pas/plugins/ldap/properties.yaml:268 +#: /home/plone/project/backend/sources/pas.plugins.ldap/src/pas/plugins/ldap/properties.yaml:269 msgid "help_memcached_server_to_use" msgstr "" #. Default: Cache timeout in seconds -#: /home/plone/project/backend/sources/pas.plugins.ldap/src/pas/plugins/ldap/properties.yaml:275 +#: /home/plone/project/backend/sources/pas.plugins.ldap/src/pas/plugins/ldap/properties.yaml:276 msgid "lbl_cache_timeout_in_seconds" msgstr "" #. Default: Save -#: /home/plone/project/backend/sources/pas.plugins.ldap/src/pas/plugins/ldap/properties.yaml:284 +#: /home/plone/project/backend/sources/pas.plugins.ldap/src/pas/plugins/ldap/properties.yaml:285 msgid "lbl_save" msgstr "" diff --git a/src/pas/plugins/ldap/locales/pas.plugins.ldap.pot b/src/pas/plugins/ldap/locales/pas.plugins.ldap.pot index 9de723e..f3c17f2 100644 --- a/src/pas/plugins/ldap/locales/pas.plugins.ldap.pot +++ b/src/pas/plugins/ldap/locales/pas.plugins.ldap.pot @@ -4,7 +4,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" -"POT-Creation-Date: 2026-05-01 23:50+0000\n" +"POT-Creation-Date: 2026-05-02 16:13+0000\n" "PO-Revision-Date: YEAR-MO-DA HO:MI +ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -17,28 +17,27 @@ msgstr "" "Preferred-Encodings: utf-8 latin1\n" "Domain: pas.plugins.ldap\n" -#: pas/plugins/ldap/properties.py:37 +#: pas/plugins/ldap/properties.py:41 msgid "BASE" msgstr "" -#: pas/plugins/ldap/plonecontrolpanel/controlpanel.pt:16 #: pas/plugins/ldap/zmi/manage_plugin.pt:28 msgid "Connection Test" msgstr "" -#: pas/plugins/ldap/properties.py:254 +#: pas/plugins/ldap/properties.py:279 msgid "Connection, users- and groups-access tested successfully." msgstr "" -#: pas/plugins/ldap/properties.py:43 +#: pas/plugins/ldap/properties.py:49 msgid "Days since Epoch" msgstr "" -#: pas/plugins/ldap/properties.py:253 +#: pas/plugins/ldap/properties.py:278 msgid "Exception in Groups; " msgstr "" -#: pas/plugins/ldap/properties.py:246 +#: pas/plugins/ldap/properties.py:271 msgid "Exception in Users; " msgstr "" @@ -66,7 +65,7 @@ msgstr "" msgid "LDAP Child Inspector" msgstr "" -#: pas/plugins/ldap/plonecontrolpanel/controlpanel.pt:25 +#: pas/plugins/ldap/plonecontrolpanel/controlpanel.pt:30 msgid "LDAP Inspector" msgstr "" @@ -74,15 +73,15 @@ msgstr "" msgid "LDAP Plugin for PAS - Zope 2 Base Installation" msgstr "" -#: pas/plugins/ldap/plonecontrolpanel/controlpanel.py:43 +#: pas/plugins/ldap/plonecontrolpanel/controlpanel.py:51 msgid "LDAP Settings saved." msgstr "" -#: pas/plugins/ldap/properties.py:250 +#: pas/plugins/ldap/properties.py:275 msgid "LDAP Users ok, but groups not; " msgstr "" -#: pas/plugins/ldap/properties.py:243 +#: pas/plugins/ldap/properties.py:268 msgid "LDAP users; " msgstr "" @@ -94,31 +93,31 @@ msgstr "" msgid "No search base selected" msgstr "" -#: pas/plugins/ldap/properties.py:234 +#: pas/plugins/ldap/properties.py:259 msgid "Non-LDAP error while getting ILDAPGroupsConfig!" msgstr "" -#: pas/plugins/ldap/properties.py:222 +#: pas/plugins/ldap/properties.py:247 msgid "Non-LDAP error while getting ILDAPProps!" msgstr "" -#: pas/plugins/ldap/properties.py:228 +#: pas/plugins/ldap/properties.py:253 msgid "Non-LDAP error while getting ILDAPUsersConfig!" msgstr "" -#: pas/plugins/ldap/properties.py:38 +#: pas/plugins/ldap/properties.py:42 msgid "ONELEVEL" msgstr "" -#: pas/plugins/ldap/properties.py:204 +#: pas/plugins/ldap/properties.py:229 msgid "Password is required for non-anonymous connections." msgstr "" -#: pas/plugins/ldap/properties.py:39 +#: pas/plugins/ldap/properties.py:43 msgid "SUBTREE" msgstr "" -#: pas/plugins/ldap/properties.py:44 +#: pas/plugins/ldap/properties.py:50 msgid "Seconds since epoch" msgstr "" @@ -126,7 +125,7 @@ msgstr "" msgid "Select search base" msgstr "" -#: pas/plugins/ldap/properties.py:241 +#: pas/plugins/ldap/properties.py:266 msgid "Server Down" msgstr "" @@ -134,10 +133,6 @@ msgstr "" msgid "Set properties for users and groups from LDAP/ActiveDirectory using the ${plugin_name} plugin." msgstr "" -#: pas/plugins/ldap/plonecontrolpanel/controlpanel.pt:34 -msgid "Settings" -msgstr "" - #: pas/plugins/ldap/plonecontrolpanel/configure.zcml:27 msgid "Uninstall LDAP Plugin for PAS" msgstr "" @@ -146,11 +141,11 @@ msgstr "" msgid "Uninstalls the pas.plugins.ldap add-on." msgstr "" -#: pas/plugins/ldap/properties.py:209 +#: pas/plugins/ldap/properties.py:234 msgid "User/Password are required if not anonymous." msgstr "" -#: pas/plugins/ldap/properties.py:198 +#: pas/plugins/ldap/properties.py:223 msgid "Username is required for non-anonymous connections." msgstr "" @@ -158,348 +153,355 @@ msgstr "" msgid "Users" msgstr "" +#: pas/plugins/ldap/plonecontrolpanel/controlpanel.pt:17 +msgid "You can configure an LDAP connection in this control panel. You can use a standard LDAP server or a Microsoft Active Directory server." +msgstr "" + +#: pas/plugins/ldap/properties.py:354 +msgid "feature not available" +msgstr "" + #. Default: LDAP attr name -#: pas/plugins/ldap/properties.yaml:243 +#: pas/plugins/ldap/properties.yaml:244 msgid "head_ldap_attr_name" msgstr "" #. Default: LDAP Attribute -#: pas/plugins/ldap/properties.yaml:187 +#: pas/plugins/ldap/properties.yaml:188 msgid "head_ldap_attribute" msgstr "" #. Default: Name on Sheet -#: pas/plugins/ldap/properties.yaml:194 +#: pas/plugins/ldap/properties.yaml:195 msgid "head_name_on_sheet" msgstr "" #. Default: Reserved Key -#. Default: Reserved key -#: pas/plugins/ldap/properties.yaml:186 +#: pas/plugins/ldap/properties.yaml:187 msgid "head_reserved_key" msgstr "" #. Default: Example, the protocol is ldap, the IP address 127.0.0.1 and the #. port 12345 -#: pas/plugins/ldap/properties.yaml:21 +#: pas/plugins/ldap/properties.yaml:23 msgid "help_connection_uri" msgstr "" #. Default: If set on authenticate a failing certificate chain check including #. CA is ignored. -#: pas/plugins/ldap/properties.yaml:58 +#: pas/plugins/ldap/properties.yaml:60 msgid "help_ignore_certificate_check" msgstr "" #. Default: The timeout period, in seconds, for waiting for a connection to be #. established with the LDAP server. -#: pas/plugins/ldap/properties.yaml:34 +#: pas/plugins/ldap/properties.yaml:36 msgid "help_ldap_connection_timeout_in_seconds" msgstr "" #. Default: The timeout in seconds for an operation such as a search or update #. to complete. If no timeout is required, use -1 as the value. -#: pas/plugins/ldap/properties.yaml:41 +#: pas/plugins/ldap/properties.yaml:43 msgid "help_ldap_operation_timeout_in_seconds" msgstr "" #. Default: "Group DNs outside of the groups base DN are ignored, except if #. listed here" -#: pas/plugins/ldap/properties.yaml:156 +#: pas/plugins/ldap/properties.yaml:158 msgid "help_memberOf_external_allowed_group_dns" msgstr "" #. Default: global - same server for all ldap plugins -#: pas/plugins/ldap/properties.yaml:268 +#: pas/plugins/ldap/properties.yaml:269 msgid "help_memcached_server_to_use" msgstr "" #. Default: "One of those is mandatory like as one the following options #. groupOfNames, groupOfUniqueNames, posixGroup, group" -#: pas/plugins/ldap/properties.yaml:224 +#: pas/plugins/ldap/properties.yaml:225 msgid "help_object_classes_for_groups" msgstr "" #. Default: Maximum page size, number of results to query the server at once #. for. -#: pas/plugins/ldap/properties.yaml:94 +#: pas/plugins/ldap/properties.yaml:96 msgid "help_page_size" msgstr "" #. Default: If your LDAP/AD supports it this will use #. LDAP_MATCHING_RULE_IN_CHAIN. By default only AD supports this. -#: pas/plugins/ldap/properties.yaml:151 +#: pas/plugins/ldap/properties.yaml:153 msgid "help_support_recursive_nested_groups" msgstr "" #. Default: If set, the LDAP server certificate is checked against the CA #. certificates in folder. -#: pas/plugins/ldap/properties.yaml:76 +#: pas/plugins/ldap/properties.yaml:78 msgid "help_tls_cacertdir" msgstr "" #. Default: If set, the LDAP server certificate is checked against the CA #. certificate file. -#: pas/plugins/ldap/properties.yaml:70 +#: pas/plugins/ldap/properties.yaml:72 msgid "help_tls_cacertfile" msgstr "" #. Default: If set, the client certificate is sent to the server. -#: pas/plugins/ldap/properties.yaml:82 +#: pas/plugins/ldap/properties.yaml:84 msgid "help_tls_clcertfile" msgstr "" #. Default: If set, the client certificate is sent to the server. -#: pas/plugins/ldap/properties.yaml:88 +#: pas/plugins/ldap/properties.yaml:90 msgid "help_tls_clkeyfile" msgstr "" #. Default: If set, the connection is upgraded to TLS. -#: pas/plugins/ldap/properties.yaml:64 +#: pas/plugins/ldap/properties.yaml:66 msgid "help_use_tls_connection" msgstr "" #. Default: Account expiration unit -#: pas/plugins/ldap/properties.yaml:175 +#: pas/plugins/ldap/properties.yaml:177 msgid "lbl_account_expiration_unit" msgstr "" #. Default: Anonymous Connection? -#: pas/plugins/ldap/properties.yaml:28 +#: pas/plugins/ldap/properties.yaml:30 msgid "lbl_anonymous_connection" msgstr "" #. Default: Attribute containing expiration Time -#: pas/plugins/ldap/properties.yaml:170 +#: pas/plugins/ldap/properties.yaml:172 msgid "lbl_attribute_containing_expiration_time" msgstr "" #. Default: Cache LDAP queries -#: pas/plugins/ldap/properties.yaml:262 +#: pas/plugins/ldap/properties.yaml:263 msgid "lbl_cache_ldap_queries" msgstr "" #. Default: Cache Settings -#: pas/plugins/ldap/properties.yaml:255 +#: pas/plugins/ldap/properties.yaml:256 msgid "lbl_cache_settings" msgstr "" #. Default: Cache timeout in seconds -#: pas/plugins/ldap/properties.yaml:275 +#: pas/plugins/ldap/properties.yaml:276 msgid "lbl_cache_timeout_in_seconds" msgstr "" #. Default: Connection URI -#: pas/plugins/ldap/properties.yaml:20 +#: pas/plugins/ldap/properties.yaml:22 msgid "lbl_connection_uri" msgstr "" #. Default: Group attribute aliases -#: pas/plugins/ldap/properties.yaml:238 +#: pas/plugins/ldap/properties.yaml:239 msgid "lbl_group_attribute_aliases" msgstr "" #. Default: Group Property-Sheet Attributes -#: pas/plugins/ldap/properties.yaml:248 +#: pas/plugins/ldap/properties.yaml:249 msgid "lbl_group_property-sheet_attributes" msgstr "" #. Default: Groups container DN -#: pas/plugins/ldap/properties.yaml:206 +#: pas/plugins/ldap/properties.yaml:207 msgid "lbl_groups_container_dn" msgstr "" #. Default: Groups search query filter -#: pas/plugins/ldap/properties.yaml:218 +#: pas/plugins/ldap/properties.yaml:219 msgid "lbl_groups_search_query_filter" msgstr "" #. Default: Groups search scope -#: pas/plugins/ldap/properties.yaml:212 +#: pas/plugins/ldap/properties.yaml:213 msgid "lbl_groups_search_scope" msgstr "" #. Default: Groups Settings -#: pas/plugins/ldap/properties.yaml:199 +#: pas/plugins/ldap/properties.yaml:200 msgid "lbl_groups_settings" msgstr "" #. Default: Ignore certificate check? -#: pas/plugins/ldap/properties.yaml:57 +#: pas/plugins/ldap/properties.yaml:59 msgid "lbl_ignore_certificate_check" msgstr "" #. Default: LDAP connection timeout in seconds. -#: pas/plugins/ldap/properties.yaml:33 +#: pas/plugins/ldap/properties.yaml:35 msgid "lbl_ldap_connection_timeout_in_seconds" msgstr "" #. Default: LDAP operation timeout in seconds. -#: pas/plugins/ldap/properties.yaml:40 +#: pas/plugins/ldap/properties.yaml:42 msgid "lbl_ldap_operation_timeout_in_seconds" msgstr "" #. Default: LDAP Server Settings -#: pas/plugins/ldap/properties.yaml:10 +#: pas/plugins/ldap/properties.yaml:12 msgid "lbl_ldap_server_settings" msgstr "" #. Default: Manager Password -#: pas/plugins/ldap/properties.yaml:52 +#: pas/plugins/ldap/properties.yaml:54 msgid "lbl_manager_password" msgstr "" #. Default: Manager User -#: pas/plugins/ldap/properties.yaml:47 +#: pas/plugins/ldap/properties.yaml:49 msgid "lbl_manager_user" msgstr "" #. Default: memberOf attribute supported? -#: pas/plugins/ldap/properties.yaml:135 +#: pas/plugins/ldap/properties.yaml:137 msgid "lbl_memberOf_attribute_supported" msgstr "" #. Default: memberOf external allowed Group DNs -#: pas/plugins/ldap/properties.yaml:157 +#: pas/plugins/ldap/properties.yaml:159 msgid "lbl_memberOf_external_allowed_group_dns" msgstr "" #. Default: Memcached Server to use -#: pas/plugins/ldap/properties.yaml:267 +#: pas/plugins/ldap/properties.yaml:268 msgid "lbl_memcached_server_to_use" msgstr "" #. Default: Object class -#: pas/plugins/ldap/properties.yaml:126 +#: pas/plugins/ldap/properties.yaml:128 msgid "lbl_object_class" msgstr "" #. Default: Object classes for Groups -#: pas/plugins/ldap/properties.yaml:223 +#: pas/plugins/ldap/properties.yaml:224 msgid "lbl_object_classes_for_groups" msgstr "" #. Default: Object classes for User creation -#: pas/plugins/ldap/properties.yaml:125 +#: pas/plugins/ldap/properties.yaml:127 msgid "lbl_object_classes_for_user_creation" msgstr "" #. Default: Page Size -#: pas/plugins/ldap/properties.yaml:93 +#: pas/plugins/ldap/properties.yaml:95 msgid "lbl_page_size" msgstr "" #. Default: Role -#: pas/plugins/ldap/properties.yaml:141 +#: pas/plugins/ldap/properties.yaml:143 msgid "lbl_role" msgstr "" #. Default: Roles acquired -#: pas/plugins/ldap/properties.yaml:140 +#: pas/plugins/ldap/properties.yaml:142 msgid "lbl_roles_acquired" msgstr "" #. Default: Save -#: pas/plugins/ldap/properties.yaml:284 +#: pas/plugins/ldap/properties.yaml:285 msgid "lbl_save" msgstr "" #. Default: Support recursive/nested groups? -#: pas/plugins/ldap/properties.yaml:150 +#: pas/plugins/ldap/properties.yaml:152 msgid "lbl_support_recursive_nested_groups" msgstr "" #. Default: Path to folder with CA certificate files for TLS communication #. (OPT_X_TLS_CACERTDIR) -#: pas/plugins/ldap/properties.yaml:75 +#: pas/plugins/ldap/properties.yaml:77 msgid "lbl_tls_cacertdir" msgstr "" #. Default: Path to CA certificate file for TLS communication #. (OPT_X_TLS_CACERTFILE) -#: pas/plugins/ldap/properties.yaml:69 +#: pas/plugins/ldap/properties.yaml:71 msgid "lbl_tls_cacertfile" msgstr "" #. Default: Path to client certificate file for TLS communication #. (OPT_X_TLS_CERTFILE). Requires tls_clkeyfile -#: pas/plugins/ldap/properties.yaml:81 +#: pas/plugins/ldap/properties.yaml:83 msgid "lbl_tls_clcertfile" msgstr "" #. Default: Path to client certificate key for TLS communication #. (OPT_X_TLS_KEYFILE). Requires tls_clcertfile -#: pas/plugins/ldap/properties.yaml:87 +#: pas/plugins/ldap/properties.yaml:89 msgid "lbl_tls_clkeyfile" msgstr "" #. Default: Use TLS connection -#: pas/plugins/ldap/properties.yaml:63 +#: pas/plugins/ldap/properties.yaml:65 msgid "lbl_use_tls_connection" msgstr "" #. Default: User Accounts expires? -#: pas/plugins/ldap/properties.yaml:165 +#: pas/plugins/ldap/properties.yaml:167 msgid "lbl_user_accounts_expires" msgstr "" #. Default: User attribute aliases -#: pas/plugins/ldap/properties.yaml:182 +#: pas/plugins/ldap/properties.yaml:183 msgid "lbl_user_attribute_aliases" msgstr "" #. Default: User Property-Sheet Attributes -#: pas/plugins/ldap/properties.yaml:192 +#: pas/plugins/ldap/properties.yaml:193 msgid "lbl_user_property-sheet_attributes" msgstr "" #. Default: Users container DN -#: pas/plugins/ldap/properties.yaml:108 +#: pas/plugins/ldap/properties.yaml:110 msgid "lbl_users_container_dn" msgstr "" #. Default: Users search query filter -#: pas/plugins/ldap/properties.yaml:120 +#: pas/plugins/ldap/properties.yaml:122 msgid "lbl_users_search_query_filter" msgstr "" #. Default: Users search scope -#: pas/plugins/ldap/properties.yaml:114 +#: pas/plugins/ldap/properties.yaml:116 msgid "lbl_users_search_scope" msgstr "" #. Default: Users Settings -#: pas/plugins/ldap/properties.yaml:101 +#: pas/plugins/ldap/properties.yaml:103 msgid "lbl_users_settings" msgstr "" #. Default: No URI defined -#: pas/plugins/ldap/properties.yaml:22 +#: pas/plugins/ldap/properties.yaml:24 msgid "msg_connection_uri" msgstr "" #. Default: Group attribute aliases values are mandatory -#: pas/plugins/ldap/properties.yaml:239 +#: pas/plugins/ldap/properties.yaml:240 msgid "msg_group_attribute_aliases" msgstr "" #. Default: No Groups DN defined -#: pas/plugins/ldap/properties.yaml:207 +#: pas/plugins/ldap/properties.yaml:208 msgid "msg_groups_container_dn" msgstr "" #. Default: 'Page size must be given.' -#: pas/plugins/ldap/properties.yaml:97 +#: pas/plugins/ldap/properties.yaml:99 msgid "msg_page_size" msgstr "" #. Default: User attribute aliases values are mandatory -#: pas/plugins/ldap/properties.yaml:183 +#: pas/plugins/ldap/properties.yaml:184 msgid "msg_user_attribute_aliases" msgstr "" #. Default: No Users DN defined -#: pas/plugins/ldap/properties.yaml:109 +#: pas/plugins/ldap/properties.yaml:111 msgid "msg_users_container_dn" msgstr "" @@ -507,7 +509,7 @@ msgstr "" msgid "pas.plugins.ldap support for users and groups from ldap/active directory." msgstr "" -#: pas/plugins/ldap/plonecontrolpanel/cache.py:31 +#: pas/plugins/ldap/plonecontrolpanel/cache.py:33 #: pas/plugins/ldap/plonecontrolpanel/profiles/default/registry.xml msgid "servers, delimited by space" msgstr "" diff --git a/src/pas/plugins/ldap/monkey.py b/src/pas/plugins/ldap/monkey.py index c0b75b6..9a25cef 100644 --- a/src/pas/plugins/ldap/monkey.py +++ b/src/pas/plugins/ldap/monkey.py @@ -20,7 +20,7 @@ class PortraitImage(Image): def getPhysicalPath(self): """Get the physical path for the portrait image.""" parent = aq_parent(aq_inner(self)) - trav = "++portrait++%s" % self.id() + trav = f"++portrait++{self.id()}" if not hasattr(parent, "getPhysicalPath"): return ("", trav) return tuple(list(parent.getPhysicalPath()) + [trav]) diff --git a/src/pas/plugins/ldap/plonecontrolpanel/__init__.py b/src/pas/plugins/ldap/plonecontrolpanel/__init__.py index 28d4f7f..b2a1516 100644 --- a/src/pas/plugins/ldap/plonecontrolpanel/__init__.py +++ b/src/pas/plugins/ldap/plonecontrolpanel/__init__.py @@ -4,7 +4,7 @@ @implementer(INonInstallable) class HiddenProfiles: - """This hides zope2 profile from the quick installer tool and plone cpanel""" + """This hides zope2 profile from the quick installer tool and plone control panel""" _hidden = [ "pas.plugins.ldap:default", @@ -12,7 +12,9 @@ class HiddenProfiles: ] def getNonInstallableProducts(self): + """Hide the whole product from the quick installer tool.""" return self._hidden def getNonInstallableProfiles(self): + """Hide specific profiles from the quick installer tool and plone control panel.""" return self._hidden diff --git a/src/pas/plugins/ldap/plonecontrolpanel/controlpanel.pt b/src/pas/plugins/ldap/plonecontrolpanel/controlpanel.pt index 5a1bf6f..00d5261 100644 --- a/src/pas/plugins/ldap/plonecontrolpanel/controlpanel.pt +++ b/src/pas/plugins/ldap/plonecontrolpanel/controlpanel.pt @@ -3,38 +3,42 @@ xmlns:metal="http://xml.zope.org/namespaces/metal" xmlns:i18n="http://xml.zope.org/namespaces/i18n" lang="en" - metal:use-macro="context/prefs_main_template/macros/master" + metal:use-macro="context/@@prefs_main_template/macros/master" i18n:domain="pas.plugins.ldap"> - - -

LDAP/ Active Directory Configuration

+ -

Connection Test

+

+ LDAP / Active Directory Configuration +

- + You can configure an LDAP connection in this control panel. You can use + a standard LDAP server or a Microsoft Active Directory server. +

+ + - state: msg -
- - LDAP Inspector - +
+ state: + msg +
+

+ + LDAP Inspector + +

-

Settings

+ - - +
diff --git a/src/pas/plugins/ldap/plonecontrolpanel/controlpanel.py b/src/pas/plugins/ldap/plonecontrolpanel/controlpanel.py index a9ab22c..1f05804 100644 --- a/src/pas/plugins/ldap/plonecontrolpanel/controlpanel.py +++ b/src/pas/plugins/ldap/plonecontrolpanel/controlpanel.py @@ -2,24 +2,16 @@ from ..properties import BasePropertiesForm from pas.plugins.ldap import _ -from Products.CMFCore.interfaces import ISiteRoot -from Products.CMFPlone.resources import add_bundle_on_request +from plumber import plumbing from Products.statusmessages.interfaces import IStatusMessage -from zope.component import getUtility - - -def getPortal(): - """Get the Plone portal object.""" - return getUtility(ISiteRoot) +from yafowil.plone.form import CSRFProtectionBehavior +from zope.component.hooks import getSite +@plumbing(CSRFProtectionBehavior) class LDAPControlPanel(BasePropertiesForm): """Control panel for managing LDAP settings.""" - def __init__(self, context, request): - super().__init__(context, request) - add_bundle_on_request(request, "yafowil") - def next(self, request): """Next @@ -34,9 +26,18 @@ def next(self, request): @property def plugin(self): """ControlPanel config is only for GS installed 'pasldap' plugin""" - portal = getPortal() + portal = getSite() aclu = portal.acl_users - return aclu.pasldap + # Use direct item access to avoid Zope acquisition walking up the + # chain when 'pasldap' is not installed, which produces a misleading + # "'RequestContainer' object has no attribute 'pasldap'" error. + try: + return aclu["pasldap"] + except KeyError: + raise AttributeError( + "Plugin 'pasldap' not found in acl_users. " + "Install pas.plugins.ldap via Plone Add-ons first." + ) def save(self, widget, data): """Save the LDAP setting. diff --git a/src/pas/plugins/ldap/plonecontrolpanel/inspector.py b/src/pas/plugins/ldap/plonecontrolpanel/inspector.py index 10ed6a8..e120f82 100644 --- a/src/pas/plugins/ldap/plonecontrolpanel/inspector.py +++ b/src/pas/plugins/ldap/plonecontrolpanel/inspector.py @@ -41,6 +41,7 @@ def users_children(self): return self.children(users.baseDN) def groups_children(self): + """Get the children of the LDAP groups container.""" groups = ILDAPGroupsConfig(self.plugin) return self.children(groups.baseDN) @@ -72,6 +73,7 @@ def node_attributes(self): return json.dumps(ret) def children(self, baseDN): + """Get the children of the LDAP node with the given base DN.""" node = LDAPNode(baseDN, self.props) ret = list() # XXX: related search filters for users and groups container? diff --git a/src/pas/plugins/ldap/plonecontrolpanel/setuphandlers.py b/src/pas/plugins/ldap/plonecontrolpanel/setuphandlers.py index b461bba..d403859 100644 --- a/src/pas/plugins/ldap/plonecontrolpanel/setuphandlers.py +++ b/src/pas/plugins/ldap/plonecontrolpanel/setuphandlers.py @@ -17,6 +17,7 @@ def _removePlugin(pas, PLUGIN_ID="pasldap"): installed = pas.objectIds() # check if plugin is installed if PLUGIN_ID not in installed: + logger.info("LDAPPlugin '%s' is not installed in acl_users.", PLUGIN_ID) return TITLE + " already uninstalled." plugin = getattr(pas, PLUGIN_ID) if not isinstance(plugin, LDAPPlugin): diff --git a/src/pas/plugins/ldap/plugin.py b/src/pas/plugins/ldap/plugin.py index 5691c54..30ea0b4 100644 --- a/src/pas/plugins/ldap/plugin.py +++ b/src/pas/plugins/ldap/plugin.py @@ -637,10 +637,10 @@ def doChangeUser(self, user_id, password, **kw): if self.users: try: self.users.passwd(user_id, None, password) - except KeyError: + except KeyError as exc: msg = f"{user_id:s} is not an LDAP user." - logger.warn(msg) - raise RuntimeError(msg) + logger.warning(msg) + raise RuntimeError(msg) from exc @security.private def doDeleteUser(self, login): diff --git a/src/pas/plugins/ldap/properties.py b/src/pas/plugins/ldap/properties.py index 218c55c..255ef6b 100644 --- a/src/pas/plugins/ldap/properties.py +++ b/src/pas/plugins/ldap/properties.py @@ -15,12 +15,11 @@ from odict import odict from pas.plugins.ldap import _ from pas.plugins.ldap import logger -from Products.Five import BrowserView from yafowil import loader # noqa: F401 from yafowil.base import ExtractionError from yafowil.base import UNSET from yafowil.controller import Controller -from yafowil.yaml import parse_from_YAML +from yafowil.plone.form import YAMLBaseForm from zope.component import adapter from zope.component import queryUtility from zope.interface import implementer @@ -30,9 +29,12 @@ _marker = dict() -class BasePropertiesForm(BrowserView): +class BasePropertiesForm(YAMLBaseForm): """Base class for LDAP properties forms.""" + form_template = "pas.plugins.ldap:properties.yaml" + message_factory = _ + # scope vocabulary, used in the form to provide options for the LDAP search # scope. The values represent the respective LDAP search scope constants. scope_vocab = [ @@ -64,21 +66,21 @@ def action(self): """Get the form action URL.""" return self.next({}) - def form(self): - """Render the LDAP properties form. - - Returns: - str: Rendered HTML of the form - """ + def prepare(self): + """Set up LDAP config objects on context and parse the YAML form.""" # make configuration data available on form context try: self.props = ILDAPProps(self.plugin) self.users = ILDAPUsersConfig(self.plugin) self.groups = ILDAPGroupsConfig(self.plugin) except Exception: - msg = "Problems getting the configuration adapters, re-initialize!" - logger.exception(msg) + logger.exception( + "Problems getting the configuration adapters, re-initialize!" + ) self.plugin.init_settings() + self.props = ILDAPProps(self.plugin) + self.users = ILDAPUsersConfig(self.plugin) + self.groups = ILDAPGroupsConfig(self.plugin) self.anonymous = not self.props.user # prepare users data on form context self.users_attrmap = odict() @@ -98,9 +100,17 @@ def form(self): if key in self.static_attrs_groups: continue self.groups_propsheet_attrmap[key] = value - # handle form - form = parse_from_YAML("pas.plugins.ldap:properties.yaml", self, _) - controller = Controller(form, self.request) + # parse YAML into self.form (YAMLBaseForm.prepare) + super().prepare() + + def render_form(self): + """Process and render the LDAP properties form. + + Returns: + str: Rendered HTML of the form, or empty string after redirect. + """ + self.prepare() + controller = Controller(self.form, self.request) if not controller.next: return controller.rendered self.request.RESPONSE.redirect(controller.next) @@ -316,7 +326,6 @@ def __init__(self, plugin): user = propproxy("server.user") roles = propproxy("server.roles") password = propproxy("server.password") - start_tls = propproxy("server.start_tls") ignore_cert = propproxy("server.ignore_cert") start_tls = propproxy("server.start_tls") tls_cacertfile = propproxy("server.tls_cacertfile") diff --git a/src/pas/plugins/ldap/properties.yaml b/src/pas/plugins/ldap/properties.yaml index ece0ef6..af73cbe 100644 --- a/src/pas/plugins/ldap/properties.yaml +++ b/src/pas/plugins/ldap/properties.yaml @@ -2,7 +2,9 @@ factory: form name: ldapsettings props: action: expr:context.action - class: edit-form enableFormTabbing enableUnloadProtection + class: edit-form enableFormTabbing pat-autotoc enableUnloadProtection + data: + pat-autotoc: "levels: legend; section: fieldset; className: autotabs" widgets: - server: factory: "*userpassanon:fieldset" diff --git a/src/pas/plugins/ldap/zmi/manage_plugin.pt b/src/pas/plugins/ldap/zmi/manage_plugin.pt index 3da8a79..9c47099 100644 --- a/src/pas/plugins/ldap/zmi/manage_plugin.pt +++ b/src/pas/plugins/ldap/zmi/manage_plugin.pt @@ -40,7 +40,7 @@

Set properties for users and groups from LDAP/ActiveDirectory using the pas.plugins.ldap plugin.

-
+
form
From 1260b7e624366739d3c1051cc1dcf323594a05d9 Mon Sep 17 00:00:00 2001 From: "Leonardo J. Caballero G" Date: Sat, 2 May 2026 19:52:07 +0200 Subject: [PATCH 70/74] Added full support for all unit test #113 --- tests/test_cache.py | 71 ++ tests/test_doctests.py | 5 + tests/test_monkey_unit.py | 379 ++++++++++ tests/test_plonecontrolpanel.py | 48 ++ tests/test_plugin.py | 49 +- tests/test_plugin_unit.py | 1164 ++++++++++++++++++++++++++++++ tests/test_properties_unit.py | 863 ++++++++++++++++++++++ tests/test_setuphandlers_unit.py | 169 +++++ tests/test_sheet_unit.py | 368 ++++++++++ tests/testing.py | 9 + 10 files changed, 3123 insertions(+), 2 deletions(-) create mode 100644 tests/test_cache.py create mode 100644 tests/test_monkey_unit.py create mode 100644 tests/test_plonecontrolpanel.py create mode 100644 tests/test_plugin_unit.py create mode 100644 tests/test_properties_unit.py create mode 100644 tests/test_setuphandlers_unit.py create mode 100644 tests/test_sheet_unit.py diff --git a/tests/test_cache.py b/tests/test_cache.py new file mode 100644 index 0000000..336338f --- /dev/null +++ b/tests/test_cache.py @@ -0,0 +1,71 @@ +"""Unit tests for pas.plugins.ldap.cache module.""" + +import time +import unittest +from unittest.mock import MagicMock, patch + + +class TestCacheProviderFactoryServers(unittest.TestCase): + """Tests for cacheProviderFactory.servers — uncovered branch (line 67).""" + + def test_servers_no_record_provider(self): + """servers returns '' when no ICacheSettingsRecordProvider is registered.""" + from pas.plugins.ldap.cache import cacheProviderFactory + + factory = cacheProviderFactory() + with patch("pas.plugins.ldap.cache.queryUtility", return_value=None): + result = factory.servers + self.assertEqual(result, "") + + +class TestRequestPluginCacheParentRequest(unittest.TestCase): + """Tests for RequestPluginCache.getRootRequest — uncovered branch (line 149).""" + + def test_get_root_request_follows_parent_chain(self): + """getRootRequest() recurses through PARENT_REQUEST to reach root.""" + from pas.plugins.ldap.cache import RequestPluginCache + + context = MagicMock() + context.getId.return_value = "testplugin" + cache = RequestPluginCache(context) + + inner_request = {"data": "inner"} + outer_request = {"PARENT_REQUEST": inner_request} + + with patch("pas.plugins.ldap.cache.getRequest", return_value=outer_request): + root = cache.getRootRequest() + + self.assertIs(root, inner_request) + + +class TestVolatilePluginCache(unittest.TestCase): + """Tests for VolatilePluginCache — uncovered branches (lines 192, 205-206).""" + + def test_get_returns_not_cached_when_expired(self): + """get() returns VALUE_NOT_CACHED when the cached entry has expired.""" + from pas.plugins.ldap.cache import VOLATILE_CACHE_MAXAGE, VolatilePluginCache + from pas.plugins.ldap.interfaces import VALUE_NOT_CACHED + + context = MagicMock() + context.getId.return_value = "testplugin" + cache = VolatilePluginCache(context) + + expired_time = time.time() - VOLATILE_CACHE_MAXAGE - 1 + setattr(context, cache._key, (expired_time, "stale_value")) + + result = cache.get() + self.assertIs(result, VALUE_NOT_CACHED) + + def test_invalidate_handles_missing_attribute(self): + """invalidate() silently ignores AttributeError when key is not set.""" + from pas.plugins.ldap.cache import VolatilePluginCache + + class SimpleContext: + def getId(self): + return "testplugin" + + context = SimpleContext() + cache = VolatilePluginCache(context) + # The cache attribute is not set on context, so delattr raises + # AttributeError — the except branch on lines 205-206 must absorb it. + cache.invalidate() # Must not raise diff --git a/tests/test_doctests.py b/tests/test_doctests.py index 9f433a3..65fa19d 100644 --- a/tests/test_doctests.py +++ b/tests/test_doctests.py @@ -1,3 +1,5 @@ +"""Test suite for pas.plugins.ldap doctests.""" + from testing import PASLDAPLayer from plone.testing import layered from plone.testing import zope @@ -16,7 +18,10 @@ def test_suite(): + """Test suite for pas.plugins.ldap doctests.""" + # We use layered doctests to have access to the test layer and its fixtures. suite = unittest.TestSuite() + # We use the zope testing layer to have access to the Zope application and its components. suite.addTests( [ layered( diff --git a/tests/test_monkey_unit.py b/tests/test_monkey_unit.py new file mode 100644 index 0000000..f7c274d --- /dev/null +++ b/tests/test_monkey_unit.py @@ -0,0 +1,379 @@ +"""Pure unit tests for pas.plugins.ldap.monkey module. + +100% coverage without Zope/LDAP layer. +""" + +import unittest +from unittest.mock import MagicMock, call, patch + +from pas.plugins.ldap.monkey import ( + PortraitImage, + PortraitTraverser, + getPortraitFromSheet, + patched_getPersonalPortrait, +) + + +# --------------------------------------------------------------------------- +# PortraitImage.getPhysicalPath (lines 22-26) +# --------------------------------------------------------------------------- + + +class TestPortraitImageGetPhysicalPath(unittest.TestCase): + """Tests for PortraitImage.getPhysicalPath.""" + + def _make_portrait(self, img_id="uid0"): + """Create PortraitImage bypassing OFS.Image.__init__.""" + portrait = PortraitImage.__new__(PortraitImage) + # In Zope, self.id() is called as a method in getPhysicalPath. + # Use a callable mock so the f-string works. + portrait.id = MagicMock(return_value=img_id) + return portrait + + def test_parent_with_getphysicalpath_appends_traversal(self): + """When parent has getPhysicalPath, returns combined path (line 26).""" + portrait = self._make_portrait("uid0") + mock_parent = MagicMock() + mock_parent.getPhysicalPath.return_value = ("", "plone", "acl_users") + with patch("pas.plugins.ldap.monkey.aq_inner", return_value=portrait), \ + patch("pas.plugins.ldap.monkey.aq_parent", return_value=mock_parent): + result = portrait.getPhysicalPath() + self.assertEqual(result, ("", "plone", "acl_users", "++portrait++uid0")) + + def test_parent_without_getphysicalpath_returns_root_trav(self): + """When parent has no getPhysicalPath, returns ('', trav) (line 25).""" + portrait = self._make_portrait("uid0") + mock_parent = object() # plain object, no getPhysicalPath attr + with patch("pas.plugins.ldap.monkey.aq_inner", return_value=portrait), \ + patch("pas.plugins.ldap.monkey.aq_parent", return_value=mock_parent): + result = portrait.getPhysicalPath() + self.assertEqual(result, ("", "++portrait++uid0")) + + +# --------------------------------------------------------------------------- +# getPortraitFromSheet (lines 31-50) +# --------------------------------------------------------------------------- + + +class TestGetPortraitFromSheet(unittest.TestCase): + """Tests for getPortraitFromSheet.""" + + def _member_not_found(self): + """Return mock mtool that resolves no member.""" + mtool = MagicMock() + mtool.getMemberById.return_value = None + return mtool + + def _member_with_sheet(self, property_ids, portrait_data=None, fullname="Test User"): + """Return (mtool, mock_user) for a member with one property sheet.""" + mock_sheet = MagicMock() + mock_sheet.propertyIds.return_value = property_ids + if portrait_data is not None: + mock_sheet.getProperty.return_value = portrait_data + + mock_user = MagicMock() + mock_user.listPropertysheets.return_value = ["ldap"] + mock_user.getPropertysheet.return_value = mock_sheet + mock_user.getProperty.return_value = fullname + + mock_member = MagicMock() + mock_member.getUser.return_value = mock_user + + mtool = MagicMock() + mtool.getMemberById.return_value = mock_member + return mtool, mock_user + + # --- branch: member not found --- + + def test_returns_none_when_member_not_found(self): + """getMemberById returns falsy → return None (line 34).""" + context = MagicMock() + with patch("pas.plugins.ldap.monkey.getToolByName", + return_value=self._member_not_found()): + result = getPortraitFromSheet(context, "uid0") + self.assertIsNone(result) + + # --- branch: no portrait in sheets --- + + def test_returns_none_when_no_sheets(self): + """User has no property sheets → return None (line 44).""" + context = MagicMock() + mock_user = MagicMock() + mock_user.listPropertysheets.return_value = [] + mock_member = MagicMock() + mock_member.getUser.return_value = mock_user + mtool = MagicMock() + mtool.getMemberById.return_value = mock_member + with patch("pas.plugins.ldap.monkey.getToolByName", return_value=mtool): + result = getPortraitFromSheet(context, "uid0") + self.assertIsNone(result) + + def test_returns_none_when_sheet_has_no_portrait_property(self): + """Sheet exists but no 'portrait' in propertyIds → return None (line 44).""" + context = MagicMock() + mtool, _ = self._member_with_sheet(["email", "fullname"]) + with patch("pas.plugins.ldap.monkey.getToolByName", return_value=mtool): + result = getPortraitFromSheet(context, "uid0") + self.assertIsNone(result) + + # --- branch: portrait found --- + + def test_returns_portrait_image_when_portrait_in_sheet(self): + """Portrait found in sheet → returns PortraitImage (line 50).""" + context = MagicMock() + mtool, _ = self._member_with_sheet( + property_ids=["portrait"], + portrait_data=b"JPEG_DATA", + fullname="Full Name", + ) + mock_portrait_img = MagicMock() + with patch("pas.plugins.ldap.monkey.getToolByName", return_value=mtool), \ + patch("pas.plugins.ldap.monkey.PortraitImage", + return_value=mock_portrait_img), \ + patch("pas.plugins.ldap.monkey.StringIO"): + result = getPortraitFromSheet(context, "uid0") + self.assertIs(result, mock_portrait_img) + + def test_portrait_image_created_with_correct_args(self): + """PortraitImage is constructed with userid, fullname, sio, content_type.""" + context = MagicMock() + mtool, mock_user = self._member_with_sheet( + property_ids=["portrait"], + portrait_data=b"DATA", + fullname="Jane Doe", + ) + mock_sio = MagicMock() + with patch("pas.plugins.ldap.monkey.getToolByName", return_value=mtool), \ + patch("pas.plugins.ldap.monkey.PortraitImage") as MockPI, \ + patch("pas.plugins.ldap.monkey.StringIO", return_value=mock_sio): + getPortraitFromSheet(context, "testuser") + + MockPI.assert_called_once_with("testuser", "Jane Doe", mock_sio, "image/jpeg") + mock_sio.write.assert_called_once_with(b"DATA") + + +# --------------------------------------------------------------------------- +# PortraitTraverser (lines 58-59, 63) +# --------------------------------------------------------------------------- + + +class TestPortraitTraverser(unittest.TestCase): + """Tests for PortraitTraverser.__init__ and traverse.""" + + def test_init_stores_context_and_request(self): + """__init__ stores context and request (lines 58-59).""" + ctx = MagicMock() + req = MagicMock() + traverser = PortraitTraverser(ctx, req) + self.assertIs(traverser.context, ctx) + self.assertIs(traverser.request, req) + + def test_init_request_defaults_to_none(self): + """request parameter defaults to None when omitted.""" + ctx = MagicMock() + traverser = PortraitTraverser(ctx) + self.assertIsNone(traverser.request) + + def test_traverse_calls_getPortraitFromSheet_and_of(self): + """traverse calls getPortraitFromSheet(context, userid).__of__(context) (line 63).""" + ctx = MagicMock() + traverser = PortraitTraverser(ctx) + + # __of__ is an Acquisition method — MagicMock raises AttributeError for it + # because it matches the "dunder" pattern. Use a simple stub instead. + mock_wrapped = MagicMock() + of_calls = [] + + class _PortraitStub: + def __of__(self, context): + of_calls.append(context) + return mock_wrapped + + mock_portrait = _PortraitStub() + + with patch( + "pas.plugins.ldap.monkey.getPortraitFromSheet", + return_value=mock_portrait, + ) as mock_gp: + result = traverser.traverse("uid0", []) + + mock_gp.assert_called_once_with(ctx, "uid0") + self.assertEqual(of_calls, [ctx]) + self.assertIs(result, mock_wrapped) + + +# --------------------------------------------------------------------------- +# patched_getPersonalPortrait (lines 72-94) +# --------------------------------------------------------------------------- + + +class TestPatchedGetPersonalPortrait(unittest.TestCase): + """Tests for patched_getPersonalPortrait.""" + + def _make_self(self): + """Return a MagicMock that acts as MembershipTool instance.""" + return MagicMock() + + # --- userid resolution (lines 72-74) --- + + def test_uses_authenticated_member_id_when_id_is_none(self): + """When id=None, userid comes from getAuthenticatedMember().getId() (line 74).""" + mock_self = self._make_self() + mock_self.getAuthenticatedMember.return_value.getId.return_value = "uid0" + mock_portrait = MagicMock() + with patch( + "pas.plugins.ldap.monkey.getPortraitFromSheet", + return_value=mock_portrait, + ) as mock_gp: + result = patched_getPersonalPortrait(mock_self, id=None) + + mock_self.getAuthenticatedMember.assert_called() + mock_gp.assert_called_once_with(mock_self, "uid0") + self.assertIs(result, mock_portrait) + + def test_uses_provided_id_directly(self): + """When id is given, userid is set to it without calling getAuthenticatedMember.""" + mock_self = self._make_self() + mock_portrait = MagicMock() + with patch( + "pas.plugins.ldap.monkey.getPortraitFromSheet", + return_value=mock_portrait, + ): + patched_getPersonalPortrait(mock_self, id="uid1") + + mock_self.getAuthenticatedMember.assert_not_called() + + # --- portrait from sheet (lines 76-78) --- + + def test_returns_portrait_from_sheet_when_found(self): + """If getPortraitFromSheet returns a portrait, return it immediately (line 78).""" + mock_self = self._make_self() + mock_portrait = MagicMock() + with patch( + "pas.plugins.ldap.monkey.getPortraitFromSheet", + return_value=mock_portrait, + ): + result = patched_getPersonalPortrait(mock_self, id="uid0") + + self.assertIs(result, mock_portrait) + + # --- fallback to memberdata (lines 80-83) --- + + def test_falls_back_to_memberdata_when_no_sheet_portrait(self): + """When sheet has no portrait, falls back to membertool._getPortrait (lines 81-83).""" + mock_self = self._make_self() + mock_portrait_obj = MagicMock() + mock_membertool = MagicMock() + mock_membertool._getPortrait.return_value = mock_portrait_obj + + with patch("pas.plugins.ldap.monkey.getPortraitFromSheet", return_value=None), \ + patch("pas.plugins.ldap.monkey.getToolByName", + return_value=mock_membertool): + result = patched_getPersonalPortrait(mock_self, id="uid0") + + self.assertIs(result, mock_portrait_obj) + + # --- portrait is str → None (lines 84-85) --- + + def test_string_portrait_becomes_none_and_falls_back_to_portal(self): + """_getPortrait returning a str → portrait=None → get default from portal (line 85).""" + mock_self = self._make_self() + mock_membertool = MagicMock() + mock_membertool._getPortrait.return_value = "some_string" + mock_portal = MagicMock() + mock_portal_url_tool = MagicMock() + mock_portal_url_tool.getPortalObject.return_value = mock_portal + + with patch("pas.plugins.ldap.monkey.getPortraitFromSheet", return_value=None), \ + patch( + "pas.plugins.ldap.monkey.getToolByName", + side_effect=[mock_membertool, mock_portal_url_tool], + ), \ + patch("pas.plugins.ldap.monkey.default_portrait", "defaultUser.png"): + result = patched_getPersonalPortrait(mock_self, id="uid0") + + mock_portal_url_tool.getPortalObject.assert_called_once() + + # --- verifyPermission branch (lines 86-89) --- + + def test_verify_permission_zero_skips_permission_check(self): + """With verifyPermission=0 (default), _checkPermission is never called (line 87 skipped).""" + mock_self = self._make_self() + mock_portrait_obj = MagicMock() + mock_membertool = MagicMock() + mock_membertool._getPortrait.return_value = mock_portrait_obj + + with patch("pas.plugins.ldap.monkey.getPortraitFromSheet", return_value=None), \ + patch("pas.plugins.ldap.monkey.getToolByName", + return_value=mock_membertool), \ + patch("pas.plugins.ldap.monkey._checkPermission") as mock_check: + result = patched_getPersonalPortrait(mock_self, id="uid0", + verifyPermission=0) + + mock_check.assert_not_called() + self.assertIs(result, mock_portrait_obj) + + def test_verify_permission_denied_hides_portrait(self): + """With verifyPermission=1, permission denied → portrait=None → default (lines 87-89).""" + mock_self = self._make_self() + mock_portrait_obj = MagicMock() + mock_membertool = MagicMock() + mock_membertool._getPortrait.return_value = mock_portrait_obj + mock_portal = MagicMock() + mock_portal_url_tool = MagicMock() + mock_portal_url_tool.getPortalObject.return_value = mock_portal + + with patch("pas.plugins.ldap.monkey.getPortraitFromSheet", return_value=None), \ + patch( + "pas.plugins.ldap.monkey.getToolByName", + side_effect=[mock_membertool, mock_portal_url_tool], + ), \ + patch("pas.plugins.ldap.monkey._checkPermission", return_value=False): + result = patched_getPersonalPortrait(mock_self, id="uid0", + verifyPermission=1) + + mock_portal_url_tool.getPortalObject.assert_called_once() + + def test_verify_permission_granted_returns_portrait(self): + """With verifyPermission=1, permission granted → portrait returned (line 94).""" + mock_self = self._make_self() + mock_portrait_obj = MagicMock() + mock_membertool = MagicMock() + mock_membertool._getPortrait.return_value = mock_portrait_obj + + with patch("pas.plugins.ldap.monkey.getPortraitFromSheet", return_value=None), \ + patch("pas.plugins.ldap.monkey.getToolByName", + return_value=mock_membertool), \ + patch("pas.plugins.ldap.monkey._checkPermission", return_value=True): + result = patched_getPersonalPortrait(mock_self, id="uid0", + verifyPermission=1) + + self.assertIs(result, mock_portrait_obj) + + # --- fallback to portal default portrait (lines 90-92) --- + + def test_fallback_to_portal_default_when_portrait_is_none(self): + """When portrait is None, gets default portrait from portal (lines 91-92).""" + mock_self = self._make_self() + mock_membertool = MagicMock() + mock_membertool._getPortrait.return_value = None + mock_default_portrait = MagicMock() + mock_portal = MagicMock() + setattr(mock_portal, "defaultUser.png", mock_default_portrait) + mock_portal_url_tool = MagicMock() + mock_portal_url_tool.getPortalObject.return_value = mock_portal + + with patch("pas.plugins.ldap.monkey.getPortraitFromSheet", return_value=None), \ + patch( + "pas.plugins.ldap.monkey.getToolByName", + side_effect=[mock_membertool, mock_portal_url_tool], + ), \ + patch("pas.plugins.ldap.monkey.default_portrait", "defaultUser.png"): + result = patched_getPersonalPortrait(mock_self, id="uid0") + + mock_portal_url_tool.getPortalObject.assert_called_once() + self.assertIs(result, mock_default_portrait) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_plonecontrolpanel.py b/tests/test_plonecontrolpanel.py new file mode 100644 index 0000000..9ee432f --- /dev/null +++ b/tests/test_plonecontrolpanel.py @@ -0,0 +1,48 @@ +"""Unit tests for pas.plugins.ldap.plonecontrolpanel modules.""" + +import unittest +from unittest.mock import patch + + +class TestHiddenProfiles(unittest.TestCase): + """Tests for HiddenProfiles — uncovered branches (return statements).""" + + def _makeOne(self): + from pas.plugins.ldap.plonecontrolpanel import HiddenProfiles + + return HiddenProfiles() + + def test_get_non_installable_products_returns_hidden_list(self): + """getNonInstallableProducts() returns the list of hidden profiles.""" + obj = self._makeOne() + result = obj.getNonInstallableProducts() + self.assertIn("pas.plugins.ldap:default", result) + self.assertIn("pas.plugins.ldap.plonecontrolpanel:install-base", result) + + def test_get_non_installable_profiles_returns_hidden_list(self): + """getNonInstallableProfiles() returns the list of hidden profiles.""" + obj = self._makeOne() + result = obj.getNonInstallableProfiles() + self.assertIn("pas.plugins.ldap:default", result) + self.assertIn("pas.plugins.ldap.plonecontrolpanel:install-base", result) + + +class TestCacheSettingsRecordProvider(unittest.TestCase): + """Tests for CacheSettingsRecordProvider — uncovered branch (line 29).""" + + def _makeOne(self): + from pas.plugins.ldap.plonecontrolpanel.cache import CacheSettingsRecordProvider + + return CacheSettingsRecordProvider() + + def test_call_returns_null_record_when_no_registry(self): + """__call__() returns NullRecord when IRegistry utility is not registered.""" + from pas.plugins.ldap.plonecontrolpanel.cache import NullRecord + + provider = self._makeOne() + with patch( + "pas.plugins.ldap.plonecontrolpanel.cache.queryUtility", return_value=None + ): + result = provider() + self.assertIsInstance(result, NullRecord) + self.assertEqual(result.value, "") diff --git a/tests/test_plugin.py b/tests/test_plugin.py index 9ff094b..a674800 100644 --- a/tests/test_plugin.py +++ b/tests/test_plugin.py @@ -1,18 +1,39 @@ +"""Unit tests for pas.plugins.ldap.""" + from testing import PASLDAP_FIXTURE from Products.PlonePAS.plugins.ufactory import PloneUser +from unittest.mock import MagicMock import unittest -class TestPluginInit(unittest.TestCase): +class TestInitialize(unittest.TestCase): + """Tests for the initialize() function in __init__.py.""" + + def test_initialize_registers_plugin(self): + """initialize() calls registerMultiPlugin and registerClass.""" + from pas.plugins.ldap import initialize + from pas.plugins.ldap.plugin import LDAPPlugin + + context = MagicMock() + initialize(context) + context.registerClass.assert_called_once() + call_kwargs = context.registerClass.call_args + self.assertIs(call_kwargs[0][0], LDAPPlugin) + + +class TestPluginInit(unittest.TestCase): + """Tests for the initialization of the LDAPPlugin.""" layer = PASLDAP_FIXTURE @property def pas(self): + """Helper to get the PAS from the test layer.""" return self.layer["app"].acl_users def test_pas_installed(self): + """Test that the PAS is installed.""" from Products.PluggableAuthService.PluggableAuthService import ( PluggableAuthService, ) @@ -20,6 +41,7 @@ def test_pas_installed(self): self.assertIsInstance(self.pas, PluggableAuthService) def test_create(self): + """Creating the plugin adds it to the PAS.""" from pas.plugins.ldap.setuphandlers import _addPlugin _addPlugin(self.pas) @@ -27,13 +49,16 @@ def test_create(self): class TestPluginFeatures(unittest.TestCase): + """Tests for the features of the LDAPPlugin.""" layer = PASLDAP_FIXTURE @property def pas(self): + """Helper to get the PAS from the test layer.""" return self.layer["app"].acl_users def setUp(self): + """Set up the test by adding the LDAP plugin to the PAS and configuring it.""" # add plugin from pas.plugins.ldap.setuphandlers import _addPlugin @@ -48,6 +73,7 @@ def setUp(self): self.ldap.groups.principal_attrmap["title"] = "cn" def test_IAuthenticationPlugin(self): + """Test the IAuthenticationPlugin implementation.""" self.assertEqual( self.ldap.authenticateCredentials({"login": "cn0", "password": "secret0"}), ("uid0", "cn0"), @@ -64,6 +90,7 @@ def test_IAuthenticationPlugin(self): ) def test_IGroupEnumerationPlugin_id(self): + """Test the IGroupEnumerationPlugin implementation with id criteria.""" self.assertEqual( [sorted(group.items()) for group in self.ldap.enumerateGroups(id="group2")], [[("id", "group2"), ("pluginid", "pasldap")]], @@ -109,6 +136,7 @@ def test_IGroupEnumerationPlugin_id(self): self.assertEqual(len(self.ldap.enumerateGroups(id="group*", max_results=3)), 3) def test_IGroupEnumerationPlugin_title(self): + """Test the IGroupEnumerationPlugin implementation with title criteria.""" self.assertEqual( [ sorted(group.items()) @@ -121,6 +149,7 @@ def test_IGroupEnumerationPlugin_title(self): ) def test_IGroupsPlugin_without_memberOf_support(self): + """Test the IGroupsPlugin implementation without memberOf support.""" user = self.pas.getUserById("uid9") self.assertEqual(self.ldap.getGroupsForPrincipal(user), ["group9"]) @@ -144,6 +173,7 @@ def test_IGroupsPlugin_without_memberOf_support(self): self.assertEqual(self.ldap.getGroupsForPrincipal(user), []) def test_IGroupsPlugin_with_memberOf_support(self): + """Test the IGroupsPlugin implementation with memberOf support.""" self.ldap.users.parent.ucfg.memberOfSupport = True user = self.pas.getUserById("uid9") self.assertEqual(self.ldap.getGroupsForPrincipal(user), ["group9"]) @@ -169,6 +199,7 @@ def test_IGroupsPlugin_with_memberOf_support(self): self.ldap.users.parent.ucfg.memberOfSupport = False def test_IUserEnumerationPlugin_with_id(self): + """Test the IUserEnumerationPlugin implementation with id criteria.""" self.assertEqual( [sorted(user.items()) for user in self.ldap.enumerateUsers(id="uid1")], [[("id", "uid1"), ("login", "cn1"), ("pluginid", "pasldap")]], @@ -215,6 +246,7 @@ def test_IUserEnumerationPlugin_with_id(self): self.assertEqual(len(self.ldap.enumerateUsers(id="uid*", max_results=3)), 3) def test_IUserEnumerationPlugin_with_login(self): + """Test the IUserEnumerationPlugin implementation with login criteria.""" users = self.ldap.enumerateUsers(login="cn1") self.assertEqual( [sorted(user.items()) for user in users], @@ -222,6 +254,7 @@ def test_IUserEnumerationPlugin_with_login(self): ) def test_IUserEnumerationPlugin_with_fullname(self): + """Test the IUserEnumerationPlugin implementation with fullname criteria.""" users = self.ldap.enumerateUsers(fullname="cn1") self.assertEqual( [sorted(user.items()) for user in users], @@ -263,6 +296,7 @@ def test_IGroupIntrospection_getGroupById(self): self.assertIsNone(self.ldap.getGroupById("non-existent")) def test_IGroupIntrospection_getGroupIds(self): + """getGroupIds returns the list of all group ids.""" self.assertEqual( self.ldap.getGroupIds(), [ @@ -280,6 +314,7 @@ def test_IGroupIntrospection_getGroupIds(self): ) def test_IGroupIntrospection_getGroups(self): + """getGroups returns the list of all group objects.""" groups = self.ldap.getGroups() self.assertEqual(len(groups), 10) from Products.PlonePAS.plugins.group import PloneGroup @@ -287,9 +322,11 @@ def test_IGroupIntrospection_getGroups(self): self.assertIsInstance(groups[0], PloneGroup) def test_IGroupIntrospection_getGroupMembers(self): + """getGroupMembers returns the list of all group members.""" self.assertEqual(self.ldap.getGroupMembers("group3"), ("uid1", "uid2", "uid3")) def test_IPasswordSetCapability(self): + """Test the IPasswordSetCapability implementation.""" # users may set passwords self.assertTrue(self.ldap.allowPasswordSet("uid0")) # groups not @@ -298,6 +335,7 @@ def test_IPasswordSetCapability(self): self.assertFalse(self.ldap.allowPasswordSet("ghost")) def test_IGroupManagement(self): + """Test the IGroupManagement implementation.""" self.assertFalse(self.ldap.addGroup(id)) self.assertFalse(self.ldap.addPrincipalToGroup("uid0", "group0")) self.assertFalse(self.ldap.updateGroup("group9", **{})) @@ -306,6 +344,7 @@ def test_IGroupManagement(self): self.assertFalse(self.ldap.removePrincipalFromGroup("uid1", "group1")) def test_IMutablePropertiesPlugin_get(self): + """Test the IMutablePropertiesPlugin implementation.""" user = self.pas.getUserById("uid0") sheet = self.ldap.getPropertiesForUser(user, request=None) from pas.plugins.ldap.sheet import LDAPUserPropertySheet @@ -314,7 +353,8 @@ def test_IMutablePropertiesPlugin_get(self): self.assertEqual(sheet.getProperty("mail"), "uid0@groupOfNames_10_10.com") def test_IMutablePropertiesPlugin_set(self): - """Set does nothing, but the sheet itselfs set immediatly""" + """Test the IMutablePropertiesPlugin implementation. + Set does nothing, but the sheet itselfs set immediatly.""" user = self.pas.getUserById("uid0") from pas.plugins.ldap.sheet import LDAPUserPropertySheet @@ -326,6 +366,7 @@ def test_IMutablePropertiesPlugin_set(self): self.assertEqual(sheet2.getProperty("mail"), "foobar@example.com") def test_IMutablePropertiesPlugin_pickle(self): + """Test that the property sheet is picklable, which is required for caching.""" user = self.pas.getUserById("uid0") from pas.plugins.ldap.sheet import LDAPUserPropertySheet @@ -335,6 +376,7 @@ def test_IMutablePropertiesPlugin_pickle(self): self.assertGreater(len(pickle.dumps(sheet)), 600) def test_IUserManagement(self): + """Test the IUserManagement implementation.""" self.ldap.doChangeUser("uid9", "geheim") self.assertEqual( self.ldap.authenticateCredentials({"login": "cn9", "password": "geheim"}), @@ -344,6 +386,7 @@ def test_IUserManagement(self): self.assertFalse(self.ldap.doDeleteUser("uid0")) def test_IRolesPlugin(self): + """Test the IRolesPlugin implementation.""" ldap_user = PloneUser("uid0", login="cn0") self.assertEqual( self.ldap.getRolesForPrincipal(ldap_user), @@ -357,10 +400,12 @@ def test_IRolesPlugin(self): # other tests higher pas level def test_user_for_group_without_memberOfSupport(self): + """Test user group retrieval without memberOf support.""" user = self.pas.getUserById("uid9") self.assertEqual(user.getGroups(), ["group9"]) def test_user_for_group_with_memberOfSupport(self): + """Test user group retrieval with memberOf support.""" self.ldap.users.parent.ucfg.memberOfSupport = True user = self.pas.getUserById("uid9") self.assertEqual(user.getGroups(), ["group9"]) diff --git a/tests/test_plugin_unit.py b/tests/test_plugin_unit.py new file mode 100644 index 0000000..8da8b11 --- /dev/null +++ b/tests/test_plugin_unit.py @@ -0,0 +1,1164 @@ +"""Pure unit tests for pas.plugins.ldap.plugin module. + +These tests exercise the branches not reached by the LDAP integration +tests, using unittest.mock so no real LDAP server or Zope layer is needed. +""" + +import time +import unittest +from unittest.mock import MagicMock, patch, PropertyMock + +import ldap + +from pas.plugins.ldap.plugin import ( + LDAP_ERROR_LOG_TIMEOUT, + LDAP_LONG_RUNNING_LOG_THRESHOLD, + LDAPPlugin, + ldap_error_handler, + manage_addLDAPPlugin, +) +from pas.plugins.ldap.interfaces import VALUE_NOT_CACHED +from Products.PluggableAuthService.interfaces import plugins as pas_interfaces +from Products.PlonePAS import interfaces as plonepas_interfaces + + +# --------------------------------------------------------------------------- +# Helpers +# --------------------------------------------------------------------------- + +def _make_plugin(plugin_id="testplugin", title="Test Plugin"): + """Return a minimal LDAPPlugin bypassing Zope/LDAP initialization.""" + from BTrees import OOBTree + + plugin = LDAPPlugin.__new__(LDAPPlugin) + plugin.id = plugin_id + plugin.title = title + plugin.plugin_caching = True + plugin.settings = OOBTree.OOBTree() + return plugin + + +class _Dummy: + """Simple object without any LDAP/Zope attributes.""" + + +# --------------------------------------------------------------------------- +# manage_addLDAPPlugin (lines 52-55) +# --------------------------------------------------------------------------- + +class TestManageAddLDAPPlugin(unittest.TestCase): + """Tests for the module-level manage_addLDAPPlugin factory function.""" + + def test_adds_plugin_to_dispatcher(self): + """Creates a plugin and calls dispatcher._setObject (lines 52-53).""" + dispatcher = MagicMock() + with patch("pas.plugins.ldap.plugin.LDAPPlugin") as MockPlugin: + instance = MockPlugin.return_value + instance.getId.return_value = "myldap" + manage_addLDAPPlugin(dispatcher, "myldap", "My LDAP") + dispatcher._setObject.assert_called_once_with("myldap", instance) + + def test_redirects_when_response_given(self): + """Calls RESPONSE.redirect when RESPONSE is not None (lines 54-55).""" + dispatcher = MagicMock() + response = MagicMock() + with patch("pas.plugins.ldap.plugin.LDAPPlugin") as MockPlugin: + MockPlugin.return_value.getId.return_value = "myldap" + manage_addLDAPPlugin(dispatcher, "myldap", RESPONSE=response) + response.redirect.assert_called_once_with("manage_workspace") + + def test_no_redirect_when_response_is_none(self): + """No redirect call when RESPONSE is None (line 54 branch).""" + dispatcher = MagicMock() + with patch("pas.plugins.ldap.plugin.LDAPPlugin") as MockPlugin: + MockPlugin.return_value.getId.return_value = "myldap" + manage_addLDAPPlugin(dispatcher, "myldap", RESPONSE=None) + dispatcher._setObject.assert_called_once() + + +# --------------------------------------------------------------------------- +# ldap_error_handler (lines 93, 102-106) +# --------------------------------------------------------------------------- + +class TestLdapErrorHandler(unittest.TestCase): + """Tests for the ldap_error_handler decorator factory.""" + + def test_long_running_call_logs_error(self): + """When elapsed > threshold, logger.error is called (line 93).""" + + @ldap_error_handler("test_prefix", default=None) + def slow_op(self): + return "done" + + obj = _Dummy() # no _v_ldaperror_timeout attribute + with patch("pas.plugins.ldap.plugin.process_time") as mock_pt: + # second call returns a value beyond the threshold + mock_pt.side_effect = [0.0, LDAP_LONG_RUNNING_LOG_THRESHOLD + 1.0] + with patch("pas.plugins.ldap.plugin.logger") as mock_logger: + result = slow_op(obj) + mock_logger.error.assert_called() + self.assertEqual(result, "done") + + def test_generic_exception_sets_error_state_and_returns_default(self): + """Non-LDAPError exception: sets timeout attrs, returns default (102-106).""" + + @ldap_error_handler("test_prefix", default="FALLBACK") + def bad_op(self): + raise RuntimeError("something bad") + + obj = _Dummy() + with patch("pas.plugins.ldap.plugin.logger"): + result = bad_op(obj) + self.assertEqual(result, "FALLBACK") + self.assertEqual(obj._v_ldaperror_msg, "something bad") + self.assertTrue(hasattr(obj, "_v_ldaperror_timeout")) + + def test_ldap_error_sets_error_state_and_returns_default(self): + """LDAPError: sets timeout attrs, returns default.""" + + @ldap_error_handler("test_prefix", default=None) + def ldap_op(self): + raise ldap.LDAPError("LDAP error") + + obj = _Dummy() + with patch("pas.plugins.ldap.plugin.logger"): + result = ldap_op(obj) + self.assertIsNone(result) + self.assertTrue(hasattr(obj, "_v_ldaperror_msg")) + + def test_timeout_phase_returns_default_immediately(self): + """When a recent error exists and timeout not expired, returns default.""" + + @ldap_error_handler("test_prefix", default="TIMEOUT_FALLBACK") + def good_op(self): + return "result" + + obj = _Dummy() + obj._v_ldaperror_timeout = time.time() # very recent error + obj._v_ldaperror_msg = "previous failure" + with patch("pas.plugins.ldap.plugin.logger"): + result = good_op(obj) + self.assertEqual(result, "TIMEOUT_FALLBACK") + + +# --------------------------------------------------------------------------- +# groups_enabled / users_enabled (lines 161, 167) +# --------------------------------------------------------------------------- + +class TestGroupsUsersEnabled(unittest.TestCase): + def setUp(self): + self.plugin = _make_plugin() + + def test_groups_enabled_true_when_groups_not_none(self): + """groups_enabled returns True when groups property is not None (161).""" + with patch.object(type(self.plugin), "groups", new_callable=PropertyMock) as pg: + pg.return_value = MagicMock() + self.assertTrue(self.plugin.groups_enabled) + + def test_groups_enabled_false_when_groups_is_none(self): + """groups_enabled returns False when groups property is None (161).""" + with patch.object(type(self.plugin), "groups", new_callable=PropertyMock) as pg: + pg.return_value = None + self.assertFalse(self.plugin.groups_enabled) + + def test_users_enabled_true_when_users_not_none(self): + """users_enabled returns True when users property is not None (167).""" + with patch.object(type(self.plugin), "users", new_callable=PropertyMock) as pu: + pu.return_value = MagicMock() + self.assertTrue(self.plugin.users_enabled) + + def test_users_enabled_false_when_users_is_none(self): + """users_enabled returns False when users property is None (167).""" + with patch.object(type(self.plugin), "users", new_callable=PropertyMock) as pu: + pu.return_value = None + self.assertFalse(self.plugin.users_enabled) + + +# --------------------------------------------------------------------------- +# ldaperror property (lines 204-208) +# --------------------------------------------------------------------------- + +class TestLdaperror(unittest.TestCase): + def setUp(self): + self.plugin = _make_plugin() + + def test_ldaperror_returns_message_when_recent_error(self): + """Returns formatted message string when error is within timeout (204-207).""" + self.plugin._v_ldaperror_msg = "connection refused" + self.plugin._v_ldaperror_timeout = time.time() # very recent + result = self.plugin.ldaperror + self.assertIn("connection refused", result) + self.assertIn("for", result) + + def test_ldaperror_returns_false_when_no_error_attribute(self): + """Returns False when _v_ldaperror_msg is not set (line 208).""" + result = self.plugin.ldaperror + self.assertFalse(result) + + def test_ldaperror_returns_false_when_error_expired(self): + """Returns False when error timeout has expired (line 208 branch).""" + self.plugin._v_ldaperror_msg = "old error" + self.plugin._v_ldaperror_timeout = time.time() - LDAP_ERROR_LOG_TIMEOUT - 1 + result = self.plugin.ldaperror + self.assertFalse(result) + + +# --------------------------------------------------------------------------- +# reset (line 214) +# --------------------------------------------------------------------------- + +class TestReset(unittest.TestCase): + def test_reset_executes_without_error(self): + """reset() passes silently (line 214).""" + plugin = _make_plugin() + plugin.reset() # should not raise + + +# --------------------------------------------------------------------------- +# authenticateCredentials (lines 235, 239, 243, 245-248) +# --------------------------------------------------------------------------- + +class TestAuthenticateCredentials(unittest.TestCase): + def setUp(self): + self.plugin = _make_plugin() + self.plugin.is_plugin_active = MagicMock(return_value=True) + + def _call(self, credentials): + return self.plugin.authenticateCredentials(credentials) + + def test_returns_none_when_plugin_not_active(self): + """Returns None immediately when plugin is not active (line 235).""" + self.plugin.is_plugin_active = MagicMock(return_value=False) + with patch.object(type(self.plugin), "users", new_callable=PropertyMock): + result = self._call({"login": "u", "password": "p"}) + self.assertIsNone(result) + + def test_returns_none_when_no_login(self): + """Returns None when login is empty/missing (line 239).""" + with patch.object(type(self.plugin), "users", new_callable=PropertyMock): + self.assertIsNone(self._call({"login": "", "password": "p"})) + + def test_returns_none_when_no_password(self): + """Returns None when password is empty/missing (line 239).""" + with patch.object(type(self.plugin), "users", new_callable=PropertyMock): + self.assertIsNone(self._call({"login": "u", "password": ""})) + + def test_returns_none_when_users_is_none(self): + """Returns None when self.users is None (line 243).""" + with patch.object(type(self.plugin), "users", new_callable=PropertyMock) as pu: + pu.return_value = None + result = self._call({"login": "u", "password": "p"}) + self.assertIsNone(result) + + def test_returns_userid_login_tuple_on_success(self): + """Returns (userid, login) when authentication succeeds (lines 245-247).""" + mock_users = MagicMock() + mock_users.authenticate.return_value = "uid42" + with patch.object(type(self.plugin), "users", new_callable=PropertyMock) as pu: + pu.return_value = mock_users + result = self._call({"login": "testuser", "password": "secret"}) + self.assertEqual(result, ("uid42", "testuser")) + + def test_returns_none_on_failed_authentication(self): + """Returns None when authenticate() returns falsy (line 248).""" + mock_users = MagicMock() + mock_users.authenticate.return_value = None + with patch.object(type(self.plugin), "users", new_callable=PropertyMock) as pu: + pu.return_value = mock_users + result = self._call({"login": "u", "password": "wrong"}) + self.assertIsNone(result) + + +# --------------------------------------------------------------------------- +# enumerateGroups (lines 300, 303, 307, 313-320) +# --------------------------------------------------------------------------- + +class TestEnumerateGroups(unittest.TestCase): + def setUp(self): + self.plugin = _make_plugin() + self.plugin.is_plugin_active = MagicMock(return_value=True) + self.plugin.getId = MagicMock(return_value="testplugin") + + def _call(self, **kw): + return self.plugin.enumerateGroups(**kw) + + def test_returns_empty_when_not_active(self): + """Returns () when plugin is not active for group enumeration (line 300).""" + self.plugin.is_plugin_active = MagicMock(return_value=False) + with patch.object(type(self.plugin), "groups", new_callable=PropertyMock): + result = self._call() + self.assertEqual(result, ()) + + def test_returns_empty_when_no_groups(self): + """Returns () when self.groups is None/falsy (line 303).""" + with patch.object(type(self.plugin), "groups", new_callable=PropertyMock) as pg: + pg.return_value = None + result = self._call() + self.assertEqual(result, ()) + + def test_returns_all_groups_when_no_criteria(self): + """Uses groups.ids to list all groups when no kw criteria (line 307).""" + mock_groups = MagicMock() + mock_groups.ids = ["grp1", "grp2"] + with patch.object(type(self.plugin), "groups", new_callable=PropertyMock) as pg: + pg.return_value = mock_groups + result = self._call() + self.assertEqual(len(result), 2) + self.assertEqual(result[0]["id"], "grp1") + + def test_returns_empty_on_value_error(self): + """Returns () when groups.search raises ValueError (line 313).""" + mock_groups = MagicMock() + mock_groups.search.side_effect = ValueError("not unique") + with patch.object(type(self.plugin), "groups", new_callable=PropertyMock) as pg: + pg.return_value = mock_groups + result = self._call(id="g1", exact_match=True) + self.assertEqual(result, ()) + + def test_sorts_by_id_when_sort_by_is_id(self): + """Sorted results when sort_by='id' (line 315).""" + mock_groups = MagicMock() + mock_groups.ids = ["zzz", "aaa", "mmm"] + with patch.object(type(self.plugin), "groups", new_callable=PropertyMock) as pg: + pg.return_value = mock_groups + result = self._call(sort_by="id") + ids = [r["id"] for r in result] + self.assertEqual(ids, sorted(ids)) + + def test_limits_results_by_max_results(self): + """Truncates results when len > max_results (lines 318-319).""" + mock_groups = MagicMock() + mock_groups.ids = ["g1", "g2", "g3", "g4"] + with patch.object(type(self.plugin), "groups", new_callable=PropertyMock) as pg: + pg.return_value = mock_groups + result = self._call(max_results=2) + self.assertEqual(len(result), 2) + + def test_returns_pluginid_in_each_item(self): + """Each result dict has pluginid from self.getId() (lines 316-317).""" + mock_groups = MagicMock() + mock_groups.ids = ["grp1"] + with patch.object(type(self.plugin), "groups", new_callable=PropertyMock) as pg: + pg.return_value = mock_groups + result = self._call() + self.assertEqual(result[0]["pluginid"], "testplugin") + + +# --------------------------------------------------------------------------- +# getGroupsForPrincipal (lines 337, 341-352) +# --------------------------------------------------------------------------- + +class TestGetGroupsForPrincipal(unittest.TestCase): + def setUp(self): + self.plugin = _make_plugin() + self.plugin.is_plugin_active = MagicMock(return_value=True) + + def _call(self, principal_id="u1"): + principal = MagicMock() + principal.getId.return_value = principal_id + return self.plugin.getGroupsForPrincipal(principal) + + def test_returns_empty_tuple_when_not_active(self): + """Returns () when plugin is not active for groups (line 337).""" + self.plugin.is_plugin_active = MagicMock(return_value=False) + with patch.object(type(self.plugin), "users", new_callable=PropertyMock): + result = self._call() + self.assertEqual(result, ()) + + def test_returns_empty_tuple_when_no_users(self): + """Returns () when self.users is None (line ~340).""" + with patch.object(type(self.plugin), "users", new_callable=PropertyMock) as pu: + pu.return_value = None + result = self._call() + self.assertEqual(result, ()) + + def test_returns_empty_tuple_on_key_error(self): + """Returns () when users[id] raises KeyError (lines 341-347).""" + mock_users = MagicMock() + mock_users.__getitem__.side_effect = KeyError("u1") + with patch.object(type(self.plugin), "users", new_callable=PropertyMock) as pu: + pu.return_value = mock_users + result = self._call("u1") + self.assertEqual(result, ()) + + def test_returns_empty_tuple_on_exception_getting_group_ids(self): + """Returns () when ugm_principal.group_ids raises Exception (lines 348-352).""" + mock_users = MagicMock() + ugm_principal = MagicMock() + type(ugm_principal).group_ids = PropertyMock(side_effect=Exception("error")) + mock_users.__getitem__.return_value = ugm_principal + with patch.object(type(self.plugin), "users", new_callable=PropertyMock) as pu: + pu.return_value = mock_users + with patch("pas.plugins.ldap.plugin.logger"): + result = self._call("u1") + self.assertEqual(result, ()) + + def test_returns_group_ids_on_success(self): + """Returns group_ids when everything succeeds.""" + mock_users = MagicMock() + ugm_principal = MagicMock() + ugm_principal.group_ids = ["group1", "group2"] + mock_users.__getitem__.return_value = ugm_principal + with patch.object(type(self.plugin), "users", new_callable=PropertyMock) as pu: + pu.return_value = mock_users + result = self._call("u1") + self.assertEqual(result, ["group1", "group2"]) + + +# --------------------------------------------------------------------------- +# enumerateUsers (lines 412, 417, 421, 425, 429, 441-448) +# --------------------------------------------------------------------------- + +class TestEnumerateUsers(unittest.TestCase): + def setUp(self): + self.plugin = _make_plugin() + self.plugin.is_plugin_active = MagicMock(return_value=True) + self.plugin.getId = MagicMock(return_value="testplugin") + + def _call(self, **kw): + return self.plugin.enumerateUsers(**kw) + + def test_returns_empty_when_not_active(self): + """Returns () when plugin is not active (line 412).""" + self.plugin.is_plugin_active = MagicMock(return_value=False) + with patch.object(type(self.plugin), "users", new_callable=PropertyMock): + result = self._call() + self.assertEqual(result, ()) + + def test_returns_empty_when_login_is_not_str(self): + """Returns () when login is a non-string sequence (line 417 executed, caught by error handler).""" + with patch.object(type(self.plugin), "users", new_callable=PropertyMock): + with patch("pas.plugins.ldap.plugin.logger"): + result = self._call(login=["user1", "user2"]) + self.assertEqual(result, ()) + + def test_removes_name_when_login_and_name_both_in_kw(self): + """Deletes 'name' from criteria when both 'login' and 'name' given (line 421).""" + mock_users = MagicMock() + mock_users.search.return_value = [] + with patch.object(type(self.plugin), "users", new_callable=PropertyMock) as pu: + pu.return_value = mock_users + self._call(login="bob", name="bob") + # 'name' should have been deleted; search called without 'name' + call_kw = mock_users.search.call_args[1] + self.assertNotIn("name", call_kw.get("criteria", {})) + + def test_returns_empty_when_id_is_not_str(self): + """Returns () when id is a non-string sequence (line 425 executed, caught by error handler).""" + with patch.object(type(self.plugin), "users", new_callable=PropertyMock): + with patch("pas.plugins.ldap.plugin.logger"): + result = self._call(id=["u1", "u2"]) + self.assertEqual(result, ()) + + def test_returns_empty_when_no_users(self): + """Returns () when self.users is None (line 429).""" + with patch.object(type(self.plugin), "users", new_callable=PropertyMock) as pu: + pu.return_value = None + result = self._call(id="u1") + self.assertEqual(result, ()) + + def test_returns_empty_on_value_error(self): + """Returns () when users.search raises ValueError (line 441).""" + mock_users = MagicMock() + mock_users.search.side_effect = ValueError("not unique") + with patch.object(type(self.plugin), "users", new_callable=PropertyMock) as pu: + pu.return_value = mock_users + result = self._call(id="u1", exact_match=True) + self.assertEqual(result, ()) + + def test_builds_result_list_from_search(self): + """Builds list of dicts with id/login/pluginid (lines 442-445).""" + mock_users = MagicMock() + mock_users.search.return_value = [ + ("uid0", {"login": ["cn0"]}), + ("uid1", {"login": ["cn1"]}), + ] + with patch.object(type(self.plugin), "users", new_callable=PropertyMock) as pu: + pu.return_value = mock_users + result = self._call(id="uid*") + self.assertEqual(len(result), 2) + self.assertEqual(result[0]["id"], "uid0") + self.assertEqual(result[0]["login"], "cn0") + self.assertEqual(result[0]["pluginid"], "testplugin") + + def test_limits_results_by_max_results(self): + """Truncates results when len > max_results (lines 446-447).""" + mock_users = MagicMock() + mock_users.search.return_value = [ + ("uid0", {"login": ["cn0"]}), + ("uid1", {"login": ["cn1"]}), + ("uid2", {"login": ["cn2"]}), + ] + with patch.object(type(self.plugin), "users", new_callable=PropertyMock) as pu: + pu.return_value = mock_users + result = self._call(id="uid*", max_results=2) + self.assertEqual(len(result), 2) + + +# --------------------------------------------------------------------------- +# getRolesForPrincipal (lines 466, 468) +# --------------------------------------------------------------------------- + +class TestGetRolesForPrincipal(unittest.TestCase): + def setUp(self): + self.plugin = _make_plugin() + + def test_returns_empty_when_no_users(self): + """Returns () when self.users is None (line 466).""" + with patch.object(type(self.plugin), "users", new_callable=PropertyMock) as pu: + pu.return_value = None + principal = MagicMock() + result = self.plugin.getRolesForPrincipal(principal) + self.assertEqual(result, ()) + + def test_returns_roles_when_user_exists(self): + """Returns roles tuple when user is found via enumerateUsers (line 468).""" + mock_users = MagicMock() + principal = MagicMock() + principal.getId.return_value = "uid0" + self.plugin.is_plugin_active = MagicMock(return_value=True) + self.plugin.getId = MagicMock(return_value="testplugin") + + mock_users.search.return_value = [("uid0", {"login": ["cn0"]})] + mock_ldap_props = MagicMock() + mock_ldap_props.roles = ["Member", "Reviewer"] + + with patch.object(type(self.plugin), "users", new_callable=PropertyMock) as pu: + pu.return_value = mock_users + with patch.object( + type(self.plugin), "_ldap_props", new_callable=PropertyMock + ) as pp: + pp.return_value = mock_ldap_props + result = self.plugin.getRolesForPrincipal(principal) + self.assertIn("Member", result) + self.assertIn("Reviewer", result) + + +# --------------------------------------------------------------------------- +# updateUser / updateEveryLoginName (lines 485, 501) +# --------------------------------------------------------------------------- + +class TestUpdateMethods(unittest.TestCase): + def setUp(self): + self.plugin = _make_plugin() + + def test_update_user_returns_false(self): + """updateUser always returns False (line 485).""" + result = self.plugin.updateUser("uid0", "new_login") + self.assertFalse(result) + + def test_update_every_login_name_returns_none(self): + """updateEveryLoginName returns None (line 501).""" + result = self.plugin.updateEveryLoginName() + self.assertIsNone(result) + + +# --------------------------------------------------------------------------- +# getPropertiesForUser (lines 584, 586-595) +# setPropertiesForUser (line 606) +# deleteUser (line 615) +# --------------------------------------------------------------------------- + +class TestPropertiesMethods(unittest.TestCase): + def setUp(self): + self.plugin = _make_plugin() + self.plugin.is_plugin_active = MagicMock(return_value=True) + self.plugin.getId = MagicMock(return_value="testplugin") + + def test_getPropertiesForUser_returns_empty_when_not_active(self): + """Returns {} when plugin is not active (line 584).""" + self.plugin.is_plugin_active = MagicMock(return_value=False) + user = MagicMock() + result = self.plugin.getPropertiesForUser(user) + self.assertEqual(result, {}) + + def test_getPropertiesForUser_decodes_bytes_ugid(self): + """Decodes bytes user id to str (line 587).""" + user = MagicMock() + user.getId.return_value = b"uid0" + self.plugin.enumerateUsers = MagicMock(return_value=[]) + self.plugin.enumerateGroups = MagicMock(return_value=[]) + result = self.plugin.getPropertiesForUser(user) + self.assertEqual(result, {}) + + def test_getPropertiesForUser_returns_sheet_when_user_found(self): + """Returns LDAPUserPropertySheet when user is enumerated (line 592).""" + user = MagicMock() + user.getId.return_value = "uid0" + self.plugin.enumerateUsers = MagicMock( + return_value=[{"id": "uid0", "login": "cn0", "pluginid": "testplugin"}] + ) + self.plugin.enumerateGroups = MagicMock(return_value=[]) + with patch("pas.plugins.ldap.plugin.LDAPUserPropertySheet") as MockSheet: + result = self.plugin.getPropertiesForUser(user) + MockSheet.assert_called_once_with(user, self.plugin) + self.assertEqual(result, MockSheet.return_value) + + def test_getPropertiesForUser_returns_empty_on_key_error(self): + """Returns {} when enumerateUsers raises KeyError (lines 593-595).""" + user = MagicMock() + user.getId.return_value = "uid0" + self.plugin.enumerateUsers = MagicMock(side_effect=KeyError("missing")) + result = self.plugin.getPropertiesForUser(user) + self.assertEqual(result, {}) + + def test_getPropertiesForUser_returns_empty_when_not_found(self): + """Returns {} when user/group not found via enumeration (line 595).""" + user = MagicMock() + user.getId.return_value = "uid_unknown" + self.plugin.enumerateUsers = MagicMock(return_value=[]) + self.plugin.enumerateGroups = MagicMock(return_value=[]) + result = self.plugin.getPropertiesForUser(user) + self.assertEqual(result, {}) + + def test_setPropertiesForUser_does_nothing(self): + """setPropertiesForUser is a no-op (line 606).""" + user = MagicMock() + sheet = MagicMock() + result = self.plugin.setPropertiesForUser(user, sheet) + self.assertIsNone(result) + + def test_deleteUser_does_nothing(self): + """deleteUser is a no-op (line 615).""" + result = self.plugin.deleteUser("uid0") + self.assertIsNone(result) + + +# --------------------------------------------------------------------------- +# doAddUser / doChangeUser / doDeleteUser (lines 629, 641-643, 654) +# --------------------------------------------------------------------------- + +class TestUserManagementMethods(unittest.TestCase): + def setUp(self): + self.plugin = _make_plugin() + + def test_doAddUser_returns_false(self): + """doAddUser always returns False (line 629).""" + result = self.plugin.doAddUser("login", "password") + self.assertFalse(result) + + def test_doChangeUser_raises_runtime_error_when_user_not_found(self): + """doChangeUser raises RuntimeError when passwd raises KeyError (641-643).""" + mock_users = MagicMock() + mock_users.passwd.side_effect = KeyError("uid_unknown") + with patch.object(type(self.plugin), "users", new_callable=PropertyMock) as pu: + pu.return_value = mock_users + with patch("pas.plugins.ldap.plugin.logger"): + with self.assertRaises(RuntimeError) as ctx: + self.plugin.doChangeUser("uid_unknown", "newpass") + self.assertIn("uid_unknown", str(ctx.exception)) + + def test_doDeleteUser_returns_false(self): + """doDeleteUser always returns False (line 654).""" + result = self.plugin.doDeleteUser("login") + self.assertFalse(result) + + +# --------------------------------------------------------------------------- +# getGroupById (lines 700, 702, 704, 707-729) +# --------------------------------------------------------------------------- + +class TestGetGroupById(unittest.TestCase): + def setUp(self): + self.plugin = _make_plugin() + self.plugin.is_plugin_active = MagicMock(return_value=True) + + def test_returns_none_when_not_active(self): + """Returns None when plugin is not active (line 700).""" + self.plugin.is_plugin_active = MagicMock(return_value=False) + with patch.object(type(self.plugin), "groups", new_callable=PropertyMock): + result = self.plugin.getGroupById("grp1") + self.assertIsNone(result) + + def test_returns_none_when_group_id_is_none(self): + """Returns None when group_id is None (line 702).""" + with patch.object(type(self.plugin), "groups", new_callable=PropertyMock): + result = self.plugin.getGroupById(None) + self.assertIsNone(result) + + def test_decodes_bytes_group_id(self): + """Decodes bytes group_id to str (line 704); returns None when not in keys.""" + with patch.object(type(self.plugin), "groups", new_callable=PropertyMock) as pg: + mock_groups = MagicMock() + # After decoding b"grp1" → "grp1", group is NOT in keys → returns default + mock_groups.keys.return_value = [] + pg.return_value = mock_groups + result = self.plugin.getGroupById(b"grp1") + self.assertIsNone(result) + + def test_returns_none_when_group_not_found(self): + """Returns None when group_id not in groups.keys() (line 706 guard).""" + with patch.object(type(self.plugin), "groups", new_callable=PropertyMock) as pg: + mock_groups = MagicMock() + mock_groups.keys.return_value = ["other"] + pg.return_value = mock_groups + result = self.plugin.getGroupById("grp_missing") + self.assertIsNone(result) + + def test_returns_plone_group_when_found(self): + """Builds and returns a PloneGroup when group exists (lines 707-729).""" + # Set up mock groups + mock_ugmgroup = MagicMock() + mock_ugmgroup.id = "grp1" + mock_ugmgroup.attrs.get.return_value = "Group One" + + mock_groups = MagicMock() + mock_groups.keys.return_value = ["grp1"] + mock_groups.__getitem__.return_value = mock_ugmgroup + + # Set up mock PAS + mock_pas = MagicMock() + mock_plugins = MagicMock() + mock_plugins.listPlugins.return_value = [] # no property/role finders + mock_pas.plugins = mock_plugins + mock_pas._getGroupsForPrincipal.return_value = [] + + self.plugin._getPAS = MagicMock(return_value=mock_pas) + + with patch.object(type(self.plugin), "groups", new_callable=PropertyMock) as pg: + pg.return_value = mock_groups + with patch("pas.plugins.ldap.plugin.PloneGroup") as MockPloneGroup: + mock_group = MagicMock() + MockPloneGroup.return_value.__of__ = MagicMock(return_value=mock_group) + result = self.plugin.getGroupById("grp1") + # Should have tried to create a PloneGroup + MockPloneGroup.assert_called_once_with("grp1", "Group One") + + def test_adds_properties_to_group(self): + """Iterates property finders and adds non-empty property sheets (line 720).""" + mock_ugmgroup = MagicMock() + mock_ugmgroup.id = "grp1" + mock_ugmgroup.attrs.get.return_value = None + + mock_groups = MagicMock() + mock_groups.keys.return_value = ["grp1"] + mock_groups.__getitem__.return_value = mock_ugmgroup + + mock_propfinder = MagicMock() + mock_propdata = MagicMock() + mock_propfinder.getPropertiesForUser.return_value = mock_propdata + + mock_pas = MagicMock() + mock_plugins = MagicMock() + # Return one propfinder and no role finder + mock_plugins.listPlugins.side_effect = [ + [("propfinder1", mock_propfinder)], # IPropertiesPlugin + [], # IRolesPlugin + ] + mock_pas.plugins = mock_plugins + mock_pas._getGroupsForPrincipal.return_value = [] + self.plugin._getPAS = MagicMock(return_value=mock_pas) + + with patch.object(type(self.plugin), "groups", new_callable=PropertyMock) as pg: + pg.return_value = mock_groups + with patch("pas.plugins.ldap.plugin.PloneGroup") as MockPloneGroup: + mock_group = MagicMock() + MockPloneGroup.return_value.__of__ = MagicMock(return_value=mock_group) + self.plugin.getGroupById("grp1") + mock_group.addPropertysheet.assert_called_once_with("propfinder1", mock_propdata) + + def test_adds_roles_to_group(self): + """Iterates role makers and adds non-empty roles (line 728).""" + mock_ugmgroup = MagicMock() + mock_ugmgroup.id = "grp1" + mock_ugmgroup.attrs.get.return_value = None + + mock_groups = MagicMock() + mock_groups.keys.return_value = ["grp1"] + mock_groups.__getitem__.return_value = mock_ugmgroup + + mock_rolemaker = MagicMock() + mock_rolemaker.getRolesForPrincipal.return_value = ["Member"] + + mock_pas = MagicMock() + mock_plugins = MagicMock() + mock_plugins.listPlugins.side_effect = [ + [], # IPropertiesPlugin + [("rolemaker1", mock_rolemaker)], # IRolesPlugin + ] + mock_pas.plugins = mock_plugins + mock_pas._getGroupsForPrincipal.return_value = [] + self.plugin._getPAS = MagicMock(return_value=mock_pas) + + with patch.object(type(self.plugin), "groups", new_callable=PropertyMock) as pg: + pg.return_value = mock_groups + with patch("pas.plugins.ldap.plugin.PloneGroup") as MockPloneGroup: + mock_group = MagicMock() + MockPloneGroup.return_value.__of__ = MagicMock(return_value=mock_group) + self.plugin.getGroupById("grp1") + mock_group._addRoles.assert_called_once_with(["Member"]) + + +# --------------------------------------------------------------------------- +# getGroupIds (line 748) +# --------------------------------------------------------------------------- + +class TestGetGroupIds(unittest.TestCase): + def setUp(self): + self.plugin = _make_plugin() + self.plugin.is_plugin_active = MagicMock(return_value=True) + + def test_returns_empty_when_not_active(self): + """Returns [] when plugin is not active (line 748).""" + self.plugin.is_plugin_active = MagicMock(return_value=False) + with patch.object(type(self.plugin), "groups", new_callable=PropertyMock): + result = self.plugin.getGroupIds() + self.assertEqual(result, []) + + def test_returns_group_ids_when_active(self): + """Returns groups.ids when plugin is active.""" + mock_groups = MagicMock() + mock_groups.ids = ["g1", "g2"] + with patch.object(type(self.plugin), "groups", new_callable=PropertyMock) as pg: + pg.return_value = mock_groups + result = self.plugin.getGroupIds() + self.assertEqual(result, ["g1", "g2"]) + + +# --------------------------------------------------------------------------- +# getGroupMembers (lines 758, 762-763) +# --------------------------------------------------------------------------- + +class TestGetGroupMembers(unittest.TestCase): + def setUp(self): + self.plugin = _make_plugin() + self.plugin.is_plugin_active = MagicMock(return_value=True) + + def test_returns_empty_when_not_active(self): + """Returns () when plugin is not active (line 758).""" + self.plugin.is_plugin_active = MagicMock(return_value=False) + with patch.object(type(self.plugin), "groups", new_callable=PropertyMock): + result = self.plugin.getGroupMembers("grp1") + self.assertEqual(result, ()) + + def test_returns_empty_on_key_error(self): + """Returns () when groups[group_id] raises KeyError (lines 762-763).""" + mock_groups = MagicMock() + mock_groups.__getitem__.side_effect = KeyError("grp_missing") + with patch.object(type(self.plugin), "groups", new_callable=PropertyMock) as pg: + pg.return_value = mock_groups + result = self.plugin.getGroupMembers("grp_missing") + self.assertEqual(result, ()) + + def test_returns_member_ids_tuple(self): + """Returns tuple of member_ids when group found.""" + mock_group = MagicMock() + mock_group.member_ids = ["u1", "u2"] + mock_groups = MagicMock() + mock_groups.__getitem__.return_value = mock_group + with patch.object(type(self.plugin), "groups", new_callable=PropertyMock) as pg: + pg.return_value = mock_groups + result = self.plugin.getGroupMembers("grp1") + self.assertEqual(result, ("u1", "u2")) + + +# --------------------------------------------------------------------------- +# allowPasswordSet (lines 774, 777, 779) +# --------------------------------------------------------------------------- + +class TestAllowPasswordSet(unittest.TestCase): + def setUp(self): + self.plugin = _make_plugin() + + def test_returns_false_when_no_users(self): + """Returns False when self.users is None (line 774).""" + with patch.object(type(self.plugin), "users", new_callable=PropertyMock) as pu: + pu.return_value = None + result = self.plugin.allowPasswordSet("uid0") + self.assertFalse(result) + + def test_returns_true_when_user_found(self): + """Returns True when search finds at least one result (line 777).""" + mock_users = MagicMock() + mock_users.search.return_value = [("uid0", {})] + with patch.object(type(self.plugin), "users", new_callable=PropertyMock) as pu: + pu.return_value = mock_users + result = self.plugin.allowPasswordSet("uid0") + self.assertTrue(result) + + def test_returns_false_when_user_not_found(self): + """Returns False when search finds no results (line 777 len==0).""" + mock_users = MagicMock() + mock_users.search.return_value = [] + with patch.object(type(self.plugin), "users", new_callable=PropertyMock) as pu: + pu.return_value = mock_users + result = self.plugin.allowPasswordSet("uid_unknown") + self.assertFalse(result) + + def test_returns_false_on_value_error(self): + """Returns False when users.search raises ValueError (line 779).""" + mock_users = MagicMock() + mock_users.search.side_effect = ValueError("not unique") + with patch.object(type(self.plugin), "users", new_callable=PropertyMock) as pu: + pu.return_value = mock_users + result = self.plugin.allowPasswordSet("uid0") + self.assertFalse(result) + + +# --------------------------------------------------------------------------- +# is_plugin_active real body (lines 153-155) +# --------------------------------------------------------------------------- + +class TestIsPluginActiveRealBody(unittest.TestCase): + def test_returns_true_when_plugin_id_in_list(self): + """Real method: returns True when getId() is in listPluginIds (153-155).""" + plugin = _make_plugin() + plugin.getId = MagicMock(return_value="testplugin") + mock_pas = MagicMock() + mock_pas.plugins.listPluginIds.return_value = ["testplugin", "other"] + plugin._getPAS = MagicMock(return_value=mock_pas) + result = plugin.is_plugin_active(MagicMock()) + self.assertTrue(result) + + def test_returns_false_when_plugin_id_not_in_list(self): + """Real method: returns False when getId() is NOT in listPluginIds (155).""" + plugin = _make_plugin() + plugin.getId = MagicMock(return_value="testplugin") + mock_pas = MagicMock() + mock_pas.plugins.listPluginIds.return_value = ["other_plugin"] + plugin._getPAS = MagicMock(return_value=mock_pas) + result = plugin.is_plugin_active(MagicMock()) + self.assertFalse(result) + + +# --------------------------------------------------------------------------- +# _ldap_props property (line 172) +# --------------------------------------------------------------------------- + +class TestLdapPropsProperty(unittest.TestCase): + def test_returns_ildapprops_adapter(self): + """_ldap_props calls ILDAPProps(self) and returns the result (line 172).""" + plugin = _make_plugin() + with patch("pas.plugins.ldap.plugin.ILDAPProps") as MockProps: + result = plugin._ldap_props + MockProps.assert_called_once_with(plugin) + self.assertEqual(result, MockProps.return_value) + + +# --------------------------------------------------------------------------- +# _ugm() method (lines 176-184) +# --------------------------------------------------------------------------- + +class TestUgmMethod(unittest.TestCase): + def test_ugm_builds_and_caches_ugm_on_cache_miss(self): + """Cache miss: builds Ugm, stores in cache, returns it (lines 176-184).""" + plugin = _make_plugin() + mock_cache = MagicMock() + mock_cache.get.return_value = VALUE_NOT_CACHED # cache miss sentinel + mock_ugm = MagicMock() + + with patch("pas.plugins.ldap.plugin.get_plugin_cache", return_value=mock_cache): + with patch("pas.plugins.ldap.plugin.ILDAPUsersConfig"): + with patch("pas.plugins.ldap.plugin.ILDAPGroupsConfig"): + with patch("pas.plugins.ldap.plugin.Ugm", return_value=mock_ugm): + with patch("pas.plugins.ldap.plugin.ILDAPProps"): + result = plugin._ugm() + + self.assertEqual(result, mock_ugm) + mock_cache.set.assert_called_once_with(mock_ugm) + + def test_ugm_returns_cached_value_on_cache_hit(self): + """Cache hit: returns cached ugm immediately without rebuilding (line 179).""" + plugin = _make_plugin() + cached_ugm = MagicMock() + mock_cache = MagicMock() + mock_cache.get.return_value = cached_ugm # something other than VALUE_NOT_CACHED + + with patch("pas.plugins.ldap.plugin.get_plugin_cache", return_value=mock_cache): + result = plugin._ugm() + + self.assertEqual(result, cached_ugm) + mock_cache.set.assert_not_called() + + +# --------------------------------------------------------------------------- +# groups / users real property bodies (lines 191, 198) +# --------------------------------------------------------------------------- + +class TestGroupsUsersPropertyBodies(unittest.TestCase): + def test_groups_property_calls_ugm_groups(self): + """Real groups property body returns self._ugm().groups (line 191).""" + plugin = _make_plugin() + mock_ugm = MagicMock() + mock_ugm.groups = MagicMock(name="ugm_groups") + plugin._ugm = MagicMock(return_value=mock_ugm) + result = plugin.groups + self.assertEqual(result, mock_ugm.groups) + + def test_users_property_calls_ugm_users(self): + """Real users property body returns self._ugm().users (line 198).""" + plugin = _make_plugin() + mock_ugm = MagicMock() + mock_ugm.users = MagicMock(name="ugm_users") + plugin._ugm = MagicMock(return_value=mock_ugm) + result = plugin.users + self.assertEqual(result, mock_ugm.users) + + +# --------------------------------------------------------------------------- +# getRolesForPrincipal "user not found" branch (line 469) +# --------------------------------------------------------------------------- + +class TestGetRolesForPrincipalNotFound(unittest.TestCase): + def test_returns_empty_when_enumerateusers_finds_nothing(self): + """Returns () when users is truthy but enumerateUsers returns () (line 469).""" + plugin = _make_plugin() + mock_users = MagicMock() + mock_users.search.return_value = [] # enumerateUsers will return () + principal = MagicMock() + principal.getId.return_value = "uid_ghost" + + with patch.object(type(plugin), "users", new_callable=PropertyMock) as pu: + pu.return_value = mock_users + result = plugin.getRolesForPrincipal(principal) + self.assertEqual(result, ()) + + +# --------------------------------------------------------------------------- +# Group management stubs (lines 513, 522, 531, 542, 551, 560) +# --------------------------------------------------------------------------- + +class TestGroupManagementStubs(unittest.TestCase): + def setUp(self): + self.plugin = _make_plugin() + + def test_addGroup_returns_false(self): + """addGroup always returns False (line 513).""" + self.assertFalse(self.plugin.addGroup("grp1")) + + def test_addPrincipalToGroup_returns_false(self): + """addPrincipalToGroup always returns False (line 522).""" + self.assertFalse(self.plugin.addPrincipalToGroup("u1", "grp1")) + + def test_updateGroup_returns_false(self): + """updateGroup always returns False (line 531).""" + self.assertFalse(self.plugin.updateGroup("grp1")) + + def test_setRolesForGroup_returns_false(self): + """setRolesForGroup always returns False (line 542).""" + self.assertFalse(self.plugin.setRolesForGroup("grp1", ["Member"])) + + def test_removeGroup_returns_false(self): + """removeGroup always returns False (line 551).""" + self.assertFalse(self.plugin.removeGroup("grp1")) + + def test_removePrincipalFromGroup_returns_false(self): + """removePrincipalFromGroup always returns False (line 560).""" + self.assertFalse(self.plugin.removePrincipalFromGroup("u1", "grp1")) + + +# --------------------------------------------------------------------------- +# Capability allow methods (lines 664, 677, 686) +# --------------------------------------------------------------------------- + +class TestCapabilityAllowMethods(unittest.TestCase): + def setUp(self): + self.plugin = _make_plugin() + + def test_allowDeletePrincipal_returns_false(self): + """allowDeletePrincipal always returns False (line 664).""" + self.assertFalse(self.plugin.allowDeletePrincipal("u1")) + + def test_allowGroupAdd_returns_false(self): + """allowGroupAdd always returns False (line 677).""" + self.assertFalse(self.plugin.allowGroupAdd("u1", "grp1")) + + def test_allowGroupRemove_returns_false(self): + """allowGroupRemove always returns False (line 686).""" + self.assertFalse(self.plugin.allowGroupRemove("u1", "grp1")) + + +# --------------------------------------------------------------------------- +# getGroupById continue branches (lines 719, 727) +# --------------------------------------------------------------------------- + +class TestGetGroupByIdContinueBranches(unittest.TestCase): + def setUp(self): + self.plugin = _make_plugin() + self.plugin.is_plugin_active = MagicMock(return_value=True) + + def _make_groups_mock(self, group_id="grp1"): + mock_ugmgroup = MagicMock() + mock_ugmgroup.id = group_id + mock_ugmgroup.attrs.get.return_value = "A Group" + mock_groups = MagicMock() + mock_groups.keys.return_value = [group_id] + mock_groups.__getitem__.return_value = mock_ugmgroup + return mock_groups + + def test_skips_propfinder_with_empty_data(self): + """Propfinder returning falsy data hits 'continue' at line 719.""" + mock_groups = self._make_groups_mock() + mock_propfinder = MagicMock() + mock_propfinder.getPropertiesForUser.return_value = {} # falsy + + mock_pas = MagicMock() + mock_plugins = MagicMock() + mock_plugins.listPlugins.side_effect = [ + [("propfinder1", mock_propfinder)], # IPropertiesPlugin + [], # IRolesPlugin + ] + mock_pas.plugins = mock_plugins + mock_pas._getGroupsForPrincipal.return_value = [] + self.plugin._getPAS = MagicMock(return_value=mock_pas) + + with patch.object(type(self.plugin), "groups", new_callable=PropertyMock) as pg: + pg.return_value = mock_groups + with patch("pas.plugins.ldap.plugin.PloneGroup") as MockPloneGroup: + mock_group = MagicMock() + MockPloneGroup.return_value.__of__ = MagicMock(return_value=mock_group) + self.plugin.getGroupById("grp1") + # addPropertysheet should NOT have been called because data was falsy + mock_group.addPropertysheet.assert_not_called() + + def test_skips_rolemaker_with_empty_roles(self): + """Rolemaker returning falsy roles hits 'continue' at line 727.""" + mock_groups = self._make_groups_mock() + mock_rolemaker = MagicMock() + mock_rolemaker.getRolesForPrincipal.return_value = () # falsy + + mock_pas = MagicMock() + mock_plugins = MagicMock() + mock_plugins.listPlugins.side_effect = [ + [], # IPropertiesPlugin + [("rolemaker1", mock_rolemaker)], # IRolesPlugin + ] + mock_pas.plugins = mock_plugins + mock_pas._getGroupsForPrincipal.return_value = [] + self.plugin._getPAS = MagicMock(return_value=mock_pas) + + with patch.object(type(self.plugin), "groups", new_callable=PropertyMock) as pg: + pg.return_value = mock_groups + with patch("pas.plugins.ldap.plugin.PloneGroup") as MockPloneGroup: + mock_group = MagicMock() + MockPloneGroup.return_value.__of__ = MagicMock(return_value=mock_group) + self.plugin.getGroupById("grp1") + # _addRoles should NOT have been called because roles was falsy + mock_group._addRoles.assert_not_called() + + +# --------------------------------------------------------------------------- +# getGroups (line 739) +# --------------------------------------------------------------------------- + +class TestGetGroups(unittest.TestCase): + def test_getGroups_returns_list_via_getGroupById(self): + """getGroups maps getGroupById over getGroupIds (line 739).""" + plugin = _make_plugin() + plugin.getGroupIds = MagicMock(return_value=["g1", "g2"]) + mock_g1 = MagicMock(name="group_g1") + mock_g2 = MagicMock(name="group_g2") + plugin.getGroupById = MagicMock(side_effect=[mock_g1, mock_g2]) + result = plugin.getGroups() + self.assertEqual(result, [mock_g1, mock_g2]) + plugin.getGroupById.assert_any_call("g1") + plugin.getGroupById.assert_any_call("g2") + + def test_getGroups_returns_empty_list_when_no_groups(self): + """getGroups returns [] when getGroupIds returns empty list.""" + plugin = _make_plugin() + plugin.getGroupIds = MagicMock(return_value=[]) + plugin.getGroupById = MagicMock() + result = plugin.getGroups() + self.assertEqual(result, []) + plugin.getGroupById.assert_not_called() diff --git a/tests/test_properties_unit.py b/tests/test_properties_unit.py new file mode 100644 index 0000000..78f66de --- /dev/null +++ b/tests/test_properties_unit.py @@ -0,0 +1,863 @@ +"""Pure unit tests for pas.plugins.ldap.properties module. + +These tests exercise all branches of properties.py using unittest.mock so +no real LDAP server, Zope layer, or Plone infrastructure is needed. +""" + +import unittest +from unittest.mock import MagicMock, patch + +import ldap +from yafowil.base import ExtractionError +from yafowil.base import UNSET +from yafowil.plone.form import YAMLBaseForm + +from pas.plugins.ldap.defaults import DEFAULTS +from pas.plugins.ldap.properties import ( + BasePropertiesForm, + GroupsConfig, + LDAPProps, + UsersConfig, + propproxy, +) + + +# --------------------------------------------------------------------------- +# Helpers +# --------------------------------------------------------------------------- + + +class ConcreteForm(BasePropertiesForm): + """Concrete subclass of BasePropertiesForm for testing. + + Provides minimal implementations of the two abstract members so that + the rest of the class can be exercised without a real Plone context. + """ + + _plugin = None + _next_url = "/test-next" + + @property + def plugin(self): + return self._plugin + + def next(self, request): + return self._next_url + + +def _make_form(plugin=None, next_url="/test-next"): + """Return a ConcreteForm instance, bypassing YAMLBaseForm.__init__.""" + form = ConcreteForm.__new__(ConcreteForm) + form._plugin = plugin if plugin is not None else MagicMock() + form._next_url = next_url + form.request = MagicMock() + form.form = MagicMock() + return form + + +def _make_plugin_with_settings(extra=None): + """Return a simple object whose .settings dict supports propproxy.""" + + class _FakePlugin: + settings = {} + + p = _FakePlugin() + if extra: + p.settings.update(extra) + return p + + +def _make_data(field_values): + """Build a mock *data* object whose fetch() simulates the form data layer. + + ``field_values`` maps short keys like ``"server.uri"`` to extracted + values. Keys not present default to ``UNSET``. + """ + data = MagicMock() + + def _fetch_side_effect(name): + key = name[len("ldapsettings."):] # strip "ldapsettings." prefix + result = MagicMock() + result.extracted = field_values.get(key, UNSET) + return result + + data.fetch.side_effect = _fetch_side_effect + return data + + +def _full_save_fields( + anonymous=False, + password="s3cret", + users_scope='"2"', + groups_scope='"2"', + users_propsheet=None, + groups_propsheet=None, + aliases_attrmap=None, +): + """Return a complete field_values dict for save() tests.""" + if aliases_attrmap is None: + # "uid" is a value but NOT a key → triggers self-referential branch + aliases_attrmap = {"rdn": "uid", "id": "uid", "login": "uid"} + if users_propsheet is None: + users_propsheet = {"email": "mail"} + if groups_propsheet is None: + groups_propsheet = {"title": "o"} + + values = { + "server.uri": "ldap://localhost:389", + "server.anonymous": anonymous, + "server.ignore_cert": False, + "server.start_tls": False, + "server.tls_cacertfile": "", + "server.tls_cacertdir": "", + "server.tls_clcertfile": "", + "server.tls_clkeyfile": "", + "server.page_size": 1000, + "server.conn_timeout": 5, + "server.op_timeout": 600, + "cache.cache": False, + "cache.memcached": "127.0.0.1:11211", + "cache.timeout": 300, + "users.roles": ["Member"], + "users.dn": "ou=users,dc=example,dc=com", + "users.aliases_attrmap": aliases_attrmap, + "users.propsheet_attrmap": users_propsheet, + "users.scope": users_scope, + "users.query": "(objectClass=inetOrgPerson)", + "users.object_classes": ["inetOrgPerson"], + "users.memberOfSupport": False, + "users.recursiveGroups": False, + "users.memberOfExternalGroupDNs": [], + "users.account_expiration": False, + "users.expires_attr": "shadowExpire", + "users.expires_unit": "0", + "groups.dn": "ou=groups,dc=example,dc=com", + "groups.aliases_attrmap": {"rdn": "cn", "id": "cn"}, + "groups.propsheet_attrmap": groups_propsheet, + "groups.scope": groups_scope, + "groups.query": "(objectClass=groupOfNames)", + "groups.object_classes": ["groupOfNames"], + "groups.memberOfSupport": False, + } + if not anonymous: + values["server.user"] = "cn=Manager,dc=example,dc=com" + values["server.password"] = password + return values + + +class _FakeLDAPGroupsError(ldap.LDAPError): + """ldap.LDAPError subclass with a .message dict (as expected by the code).""" + + def __init__(self, desc): + self.message = {"desc": desc} + super().__init__(desc) + + +# --------------------------------------------------------------------------- +# BasePropertiesForm — abstract methods (lines 58, 62) +# --------------------------------------------------------------------------- + + +class TestBasePropertiesFormAbstractMethods(unittest.TestCase): + """Tests for the abstract interface of BasePropertiesForm.""" + + def test_plugin_raises_not_implemented(self): + """plugin property raises NotImplementedError (line 58).""" + form = BasePropertiesForm.__new__(BasePropertiesForm) + with self.assertRaises(NotImplementedError): + _ = form.plugin + + def test_next_raises_not_implemented(self): + """next() raises NotImplementedError (line 62).""" + form = BasePropertiesForm.__new__(BasePropertiesForm) + with self.assertRaises(NotImplementedError): + form.next({}) + + +# --------------------------------------------------------------------------- +# action property (line 67) +# --------------------------------------------------------------------------- + + +class TestActionProperty(unittest.TestCase): + """Tests for the action property.""" + + def test_action_delegates_to_next(self): + """action property returns next({}) (line 67).""" + form = _make_form(next_url="/my-action-url") + self.assertEqual(form.action, "/my-action-url") + + +# --------------------------------------------------------------------------- +# prepare() (lines 72-104) +# --------------------------------------------------------------------------- + + +class TestPrepare(unittest.TestCase): + """Tests for BasePropertiesForm.prepare().""" + + def _mock_adapters(self, user="admin"): + mock_props = MagicMock() + mock_props.user = user + mock_users = MagicMock() + # dict so .get() and .items() work naturally + mock_users.attrmap = { + "rdn": "uid", + "id": "uid", + "login": "uid", + "email": "mail", + "fullname": "cn", + } + mock_groups = MagicMock() + mock_groups.attrmap = { + "rdn": "cn", + "id": "cn", + "title": "o", + "description": "description", + } + return mock_props, mock_users, mock_groups + + def test_prepare_happy_path_sets_all_attrs(self): + """Happy-path prepare() populates all expected attributes (lines 72-104).""" + form = _make_form() + mock_props, mock_users, mock_groups = self._mock_adapters(user="admin") + + with patch("pas.plugins.ldap.properties.ILDAPProps", return_value=mock_props), \ + patch("pas.plugins.ldap.properties.ILDAPUsersConfig", return_value=mock_users), \ + patch("pas.plugins.ldap.properties.ILDAPGroupsConfig", return_value=mock_groups), \ + patch.object(YAMLBaseForm, "prepare"): + form.prepare() + + self.assertIs(form.props, mock_props) + self.assertIs(form.users, mock_users) + self.assertIs(form.groups, mock_groups) + # user = "admin" (truthy) → not anonymous + self.assertFalse(form.anonymous) + # static user attrs in users_attrmap + for key in ("rdn", "id", "login"): + self.assertIn(key, form.users_attrmap) + # non-static attrs in users_propsheet_attrmap + self.assertIn("email", form.users_propsheet_attrmap) + self.assertIn("fullname", form.users_propsheet_attrmap) + # static attrs NOT in users_propsheet_attrmap + self.assertNotIn("rdn", form.users_propsheet_attrmap) + # static group attrs in groups_attrmap + for key in ("rdn", "id"): + self.assertIn(key, form.groups_attrmap) + # non-static group attrs in groups_propsheet_attrmap + self.assertIn("title", form.groups_propsheet_attrmap) + self.assertIn("description", form.groups_propsheet_attrmap) + + def test_prepare_sets_anonymous_when_user_empty(self): + """prepare() sets anonymous=True when props.user is falsy.""" + form = _make_form() + mock_props, mock_users, mock_groups = self._mock_adapters(user="") + + with patch("pas.plugins.ldap.properties.ILDAPProps", return_value=mock_props), \ + patch("pas.plugins.ldap.properties.ILDAPUsersConfig", return_value=mock_users), \ + patch("pas.plugins.ldap.properties.ILDAPGroupsConfig", return_value=mock_groups), \ + patch.object(YAMLBaseForm, "prepare"): + form.prepare() + + self.assertTrue(form.anonymous) + + def test_prepare_exception_reinitializes_and_retries(self): + """Exception branch: calls init_settings() and retries (lines 76-83).""" + form = _make_form() + mock_props, mock_users, mock_groups = self._mock_adapters(user="admin") + + call_count = [0] + + def _ildapprops(plugin): + call_count[0] += 1 + if call_count[0] == 1: + raise TypeError("adapter failure on first call") + return mock_props + + with patch("pas.plugins.ldap.properties.ILDAPProps", side_effect=_ildapprops), \ + patch("pas.plugins.ldap.properties.ILDAPUsersConfig", return_value=mock_users), \ + patch("pas.plugins.ldap.properties.ILDAPGroupsConfig", return_value=mock_groups), \ + patch.object(YAMLBaseForm, "prepare"), \ + patch("pas.plugins.ldap.properties.logger"): + form.prepare() + + # init_settings() should have been called once on the plugin + form._plugin.init_settings.assert_called_once() + # ILDAPProps was invoked twice (once failing, once succeeding) + self.assertEqual(call_count[0], 2) + self.assertIs(form.props, mock_props) + + +# --------------------------------------------------------------------------- +# render_form() (lines 112-117) +# --------------------------------------------------------------------------- + + +class TestRenderForm(unittest.TestCase): + """Tests for BasePropertiesForm.render_form().""" + + def test_render_form_returns_rendered_when_no_next(self): + """Returns controller.rendered when controller.next is falsy (line 115).""" + form = _make_form() + mock_controller = MagicMock() + mock_controller.next = None + mock_controller.rendered = "form html" + + with patch.object(form, "prepare"), \ + patch("pas.plugins.ldap.properties.Controller", return_value=mock_controller): + result = form.render_form() + + self.assertEqual(result, "form html") + + def test_render_form_redirects_and_returns_empty_on_next(self): + """Redirects and returns '' when controller.next is truthy (lines 116-117).""" + form = _make_form() + mock_controller = MagicMock() + mock_controller.next = "/redirect-target" + + with patch.object(form, "prepare"), \ + patch("pas.plugins.ldap.properties.Controller", return_value=mock_controller): + result = form.render_form() + + form.request.RESPONSE.redirect.assert_called_once_with("/redirect-target") + self.assertEqual(result, "") + + +# --------------------------------------------------------------------------- +# save() (lines 126-208) +# --------------------------------------------------------------------------- + + +class TestSave(unittest.TestCase): + """Tests for BasePropertiesForm.save().""" + + def _run_save(self, field_values, mock_props=None, mock_users=None, mock_groups=None): + """Helper: run save() with patched adapters and the given field values.""" + form = _make_form() + if mock_props is None: + mock_props = MagicMock() + if mock_users is None: + mock_users = MagicMock() + if mock_groups is None: + mock_groups = MagicMock() + data = _make_data(field_values) + + with patch("pas.plugins.ldap.properties.ILDAPProps", return_value=mock_props), \ + patch("pas.plugins.ldap.properties.ILDAPUsersConfig", return_value=mock_users), \ + patch("pas.plugins.ldap.properties.ILDAPGroupsConfig", return_value=mock_groups): + form.save(MagicMock(), data) + + return mock_props, mock_users, mock_groups + + def test_non_anonymous_sets_user_and_password(self): + """Non-anonymous path: sets user and password (lines 141-143).""" + values = _full_save_fields(anonymous=False, password="s3cret123") + mock_props, _, _ = self._run_save(values) + self.assertEqual(mock_props.user, "cn=Manager,dc=example,dc=com") + self.assertEqual(mock_props.password, "s3cret123") + + def test_non_anonymous_password_unset_skips_password_assignment(self): + """Non-anonymous path: password UNSET → password NOT overwritten (branch line 143).""" + values = _full_save_fields(anonymous=False, password=UNSET) + mock_props = MagicMock() + self._run_save(values, mock_props=mock_props) + # props.user is set; no exception means the UNSET branch ran without crash + self.assertEqual(mock_props.user, "cn=Manager,dc=example,dc=com") + + def test_anonymous_clears_user_and_password(self): + """Anonymous path: clears user and password (lines 145-147).""" + values = _full_save_fields(anonymous=True) + mock_props, _, _ = self._run_save(values) + self.assertEqual(mock_props.user, "") + self.assertEqual(mock_props.password, "") + + def test_propsheet_attrmap_updates_users_attrmap(self): + """When users propsheet_attrmap is not UNSET, it updates users.attrmap (lines 175-176).""" + values = _full_save_fields(users_propsheet={"email": "mail"}) + _, mock_users, _ = self._run_save(values) + # users.attrmap was replaced with an odict; verify "email" key was added + self.assertIn("email", mock_users.attrmap) + + def test_propsheet_attrmap_unset_skips_update(self): + """When propsheet_attrmap is UNSET, users.attrmap is NOT updated from it.""" + values = _full_save_fields( + users_propsheet=UNSET, + groups_propsheet=UNSET, + ) + _, mock_users, _ = self._run_save(values) + # Only the aliases_attrmap keys should be present + for key in ("email", "fullname"): + self.assertNotIn(key, mock_users.attrmap) + + def test_id_value_not_in_attrmap_adds_self_referential_entry(self): + """If attrmap['id'] value is not a key, adds self-referential entry (line 181).""" + # aliases: {"rdn": "uid", "id": "uid", "login": "uid"} + # After update: attrmap["id"] = "uid", but "uid" is NOT a key → line 181 runs + values = _full_save_fields( + aliases_attrmap={"rdn": "uid", "id": "uid", "login": "uid"}, + users_propsheet=UNSET, + ) + _, mock_users, _ = self._run_save(values) + self.assertIn("uid", mock_users.attrmap) + self.assertEqual(mock_users.attrmap["uid"], "uid") + + def test_id_value_already_in_attrmap_skips_self_referential(self): + """If attrmap['id'] value IS a key, the self-referential branch is skipped.""" + # Include "uid" as an explicit key so "uid" IS in attrmap + values = _full_save_fields( + aliases_attrmap={"rdn": "cn", "id": "cn", "login": "cn", "cn": "cn"}, + users_propsheet=UNSET, + ) + _, mock_users, _ = self._run_save(values) + # "cn" is both the id-value and an existing key → no extra entry added + self.assertIn("cn", mock_users.attrmap) + + def test_users_scope_converted_when_not_unset(self): + """users.scope is int-converted when not UNSET (line 184).""" + values = _full_save_fields(users_scope='"2"') + _, mock_users, _ = self._run_save(values) + self.assertEqual(mock_users.scope, 2) + + def test_users_scope_stays_unset_when_unset(self): + """users.scope is left as-is when fetch returns UNSET (line 183 branch).""" + values = _full_save_fields(users_scope=UNSET) + _, mock_users, _ = self._run_save(values) + self.assertIs(mock_users.scope, UNSET) + + def test_groups_propsheet_attrmap_updates_groups_attrmap(self): + """When groups propsheet_attrmap is not UNSET, groups.attrmap is updated (lines 198-199).""" + values = _full_save_fields(groups_propsheet={"title": "o"}) + _, _, mock_groups = self._run_save(values) + self.assertIn("title", mock_groups.attrmap) + + def test_groups_scope_converted_when_not_unset(self): + """groups.scope is int-converted when not UNSET (line 202).""" + values = _full_save_fields(groups_scope='"1"') + _, _, mock_groups = self._run_save(values) + self.assertEqual(mock_groups.scope, 1) + + def test_groups_scope_stays_unset_when_unset(self): + """groups.scope is left as-is when fetch returns UNSET.""" + values = _full_save_fields(groups_scope=UNSET) + _, _, mock_groups = self._run_save(values) + self.assertIs(mock_groups.scope, UNSET) + + def test_expires_unit_default_used_when_unset(self): + """fetch('users.expires_unit', 0) returns 0 when value is UNSET (line 137).""" + values = _full_save_fields(anonymous=False) + values["users.expires_unit"] = UNSET # override to UNSET → hits return default + _, mock_users, _ = self._run_save(values) + self.assertEqual(mock_users._expiresUnit, 0) + + def test_save_sets_all_server_props(self): + """All server-level props are forwarded to props object (lines 149-160).""" + values = _full_save_fields(anonymous=False, password="pw") + mock_props, _, _ = self._run_save(values) + self.assertEqual(mock_props.uri, "ldap://localhost:389") + self.assertFalse(mock_props.ignore_cert) + self.assertFalse(mock_props.start_tls) + + def test_save_sets_cache_props(self): + """Cache-related props are forwarded (lines 161-164).""" + values = _full_save_fields(anonymous=True) + mock_props, _, _ = self._run_save(values) + self.assertFalse(mock_props.cache) + self.assertEqual(mock_props.timeout, 300) + + +# --------------------------------------------------------------------------- +# userpassanon_extractor() (lines 218-235) +# --------------------------------------------------------------------------- + + +class TestUserPassAnonExtractor(unittest.TestCase): + """Tests for BasePropertiesForm.userpassanon_extractor().""" + + def _make_data_mock(self, extracted, anonymous, user="", password="", pw_value=""): + """Build a mock data object for userpassanon_extractor() tests.""" + data = MagicMock() + data.extracted = extracted + anon_mock = MagicMock(extracted=anonymous) + user_mock = MagicMock(extracted=user, errors=[]) + pw_mock = MagicMock(extracted=password, value=pw_value, errors=[]) + data.__getitem__ = MagicMock( + side_effect=lambda key: { + "anonymous": anon_mock, + "user": user_mock, + "password": pw_mock, + }[key] + ) + return data, user_mock, pw_mock + + def test_returns_early_when_not_extracted(self): + """Returns data.extracted when data.extracted is falsy (line 219 first branch).""" + form = _make_form() + data = MagicMock() + data.extracted = None + result = form.userpassanon_extractor(MagicMock(), data) + self.assertIsNone(result) + + def test_returns_early_when_anonymous(self): + """Returns data.extracted when anonymous=True (line 219 second branch).""" + form = _make_form() + data, _, _ = self._make_data_mock( + extracted="some-data", anonymous=True + ) + result = form.userpassanon_extractor(MagicMock(), data) + self.assertEqual(result, "some-data") + + def test_user_empty_appends_error_and_raises(self): + """Empty user → error appended and ExtractionError raised (lines 222-225).""" + form = _make_form() + data, user_mock, _ = self._make_data_mock( + extracted="data", anonymous=False, user="", password="secret", pw_value="secret" + ) + with self.assertRaises(ExtractionError): + form.userpassanon_extractor(MagicMock(), data) + self.assertEqual(len(user_mock.errors), 1) + + def test_password_empty_appends_error_and_raises(self): + """Empty password → error appended and ExtractionError raised (lines 226-230).""" + form = _make_form() + data, _, pw_mock = self._make_data_mock( + extracted="data", anonymous=False, user="admin", password="", pw_value="" + ) + with self.assertRaises(ExtractionError): + form.userpassanon_extractor(MagicMock(), data) + self.assertEqual(len(pw_mock.errors), 1) + + def test_both_empty_appends_both_errors_and_raises(self): + """Both user and password empty → two errors, ExtractionError raised.""" + form = _make_form() + data, user_mock, pw_mock = self._make_data_mock( + extracted="data", anonymous=False, user="", password="", pw_value="" + ) + with self.assertRaises(ExtractionError): + form.userpassanon_extractor(MagicMock(), data) + self.assertEqual(len(user_mock.errors), 1) + self.assertEqual(len(pw_mock.errors), 1) + + def test_valid_credentials_returns_extracted(self): + """Valid user and password → returns data.extracted (line 235).""" + form = _make_form() + data, _, _ = self._make_data_mock( + extracted="result-data", + anonymous=False, + user="admin", + password="secret", + pw_value="secret", + ) + result = form.userpassanon_extractor(MagicMock(), data) + self.assertEqual(result, "result-data") + + +# --------------------------------------------------------------------------- +# connection_test() (lines 244-279) +# --------------------------------------------------------------------------- + + +class TestConnectionTest(unittest.TestCase): + """Tests for BasePropertiesForm.connection_test().""" + + def _patch_adapters(self, props=None, users=None, groups=None): + """Context-manager helper that patches the three LDAP config adapters.""" + return ( + patch( + "pas.plugins.ldap.properties.ILDAPProps", + return_value=props if props is not None else MagicMock(), + ), + patch( + "pas.plugins.ldap.properties.ILDAPUsersConfig", + return_value=users if users is not None else MagicMock(), + ), + patch( + "pas.plugins.ldap.properties.ILDAPGroupsConfig", + return_value=groups if groups is not None else MagicMock(), + ), + ) + + def test_ildapprops_exception_returns_false(self): + """Returns (False, msg) when ILDAPProps raises (lines 246-249).""" + form = _make_form() + with patch( + "pas.plugins.ldap.properties.ILDAPProps", + side_effect=RuntimeError("props-fail"), + ), patch("pas.plugins.ldap.properties.logger"): + ok, msg = form.connection_test() + + self.assertFalse(ok) + self.assertIn("props-fail", str(msg)) + + def test_ildapusersconfig_exception_returns_false(self): + """Returns (False, msg) when ILDAPUsersConfig raises (lines 251-254).""" + form = _make_form() + with patch("pas.plugins.ldap.properties.ILDAPProps", return_value=MagicMock()), \ + patch( + "pas.plugins.ldap.properties.ILDAPUsersConfig", + side_effect=RuntimeError("users-fail"), + ), \ + patch("pas.plugins.ldap.properties.logger"): + ok, msg = form.connection_test() + + self.assertFalse(ok) + self.assertIn("users-fail", str(msg)) + + def test_ildapgroupsconfig_exception_returns_false(self): + """Returns (False, msg) when ILDAPGroupsConfig raises (lines 256-259).""" + form = _make_form() + with patch("pas.plugins.ldap.properties.ILDAPProps", return_value=MagicMock()), \ + patch("pas.plugins.ldap.properties.ILDAPUsersConfig", return_value=MagicMock()), \ + patch( + "pas.plugins.ldap.properties.ILDAPGroupsConfig", + side_effect=RuntimeError("groups-fail"), + ), \ + patch("pas.plugins.ldap.properties.logger"): + ok, msg = form.connection_test() + + self.assertFalse(ok) + self.assertIn("groups-fail", str(msg)) + + def test_server_down_returns_false(self): + """Returns (False, 'Server Down') on ldap.SERVER_DOWN (line 262).""" + form = _make_form() + mock_ugm = MagicMock() + mock_ugm.users.authenticate.side_effect = ldap.SERVER_DOWN + + p1, p2, p3 = self._patch_adapters() + with p1, p2, p3, \ + patch("pas.plugins.ldap.properties.Ugm", return_value=mock_ugm): + ok, msg = form.connection_test() + + self.assertFalse(ok) + self.assertIn("Down", str(msg)) + + def test_ldap_error_in_users_returns_false(self): + """Returns (False, msg) for ldap.LDAPError in users authenticate (line 263).""" + form = _make_form() + mock_ugm = MagicMock() + mock_ugm.users.authenticate.side_effect = ldap.LDAPError("users ldap error") + + p1, p2, p3 = self._patch_adapters() + with p1, p2, p3, \ + patch("pas.plugins.ldap.properties.Ugm", return_value=mock_ugm): + ok, msg = form.connection_test() + + self.assertFalse(ok) + self.assertIn("LDAP users", str(msg)) + + def test_generic_exception_in_users_returns_false(self): + """Returns (False, msg) for non-LDAP exception in users (lines 265-267).""" + form = _make_form() + mock_ugm = MagicMock() + mock_ugm.users.authenticate.side_effect = RuntimeError("generic users error") + + p1, p2, p3 = self._patch_adapters() + with p1, p2, p3, \ + patch("pas.plugins.ldap.properties.Ugm", return_value=mock_ugm), \ + patch("pas.plugins.ldap.properties.logger"): + ok, msg = form.connection_test() + + self.assertFalse(ok) + self.assertIn("generic users error", str(msg)) + + def test_ldap_error_in_groups_returns_false(self): + """Returns (False, msg) for ldap.LDAPError in groups.keys() (lines 269-270).""" + form = _make_form() + mock_ugm = MagicMock() + mock_ugm.users.authenticate.return_value = None # users OK + mock_ugm.groups.keys.side_effect = _FakeLDAPGroupsError("groups-ldap-error") + + p1, p2, p3 = self._patch_adapters() + with p1, p2, p3, \ + patch("pas.plugins.ldap.properties.Ugm", return_value=mock_ugm): + ok, msg = form.connection_test() + + self.assertFalse(ok) + self.assertIn("groups-ldap-error", str(msg)) + + def test_generic_exception_in_groups_returns_false(self): + """Returns (False, msg) for non-LDAP exception in groups.keys() (lines 271-273).""" + form = _make_form() + mock_ugm = MagicMock() + mock_ugm.users.authenticate.return_value = None + mock_ugm.groups.keys.side_effect = RuntimeError("generic groups error") + + p1, p2, p3 = self._patch_adapters() + with p1, p2, p3, \ + patch("pas.plugins.ldap.properties.Ugm", return_value=mock_ugm), \ + patch("pas.plugins.ldap.properties.logger"): + ok, msg = form.connection_test() + + self.assertFalse(ok) + self.assertIn("generic groups error", str(msg)) + + def test_connection_success_returns_true(self): + """Returns (True, success msg) when all checks pass (line 279).""" + form = _make_form() + mock_ugm = MagicMock() + mock_ugm.users.authenticate.return_value = None + mock_ugm.groups.keys.return_value = [] + + p1, p2, p3 = self._patch_adapters() + with p1, p2, p3, \ + patch("pas.plugins.ldap.properties.Ugm", return_value=mock_ugm): + ok, msg = form.connection_test() + + self.assertTrue(ok) + self.assertIn("successfully", str(msg)) + + +# --------------------------------------------------------------------------- +# propproxy (lines 293-310) +# --------------------------------------------------------------------------- + + +class TestPropProxy(unittest.TestCase): + """Tests for the propproxy() factory.""" + + def _make_adapter_class(self, ckey): + """Return a class with a propproxy-backed attribute.""" + + class _Adapter: + def __init__(self, plugin): + self.plugin = plugin + + prop = propproxy(ckey) + + return _Adapter + + def test_getter_returns_default_when_key_absent(self): + """_getter returns DEFAULTS[ckey] when key not in plugin.settings.""" + plugin = _make_plugin_with_settings() + cls = self._make_adapter_class("server.uri") + adapter = cls(plugin) + self.assertEqual(adapter.prop, DEFAULTS["server.uri"]) + + def test_getter_returns_stored_value(self): + """_getter returns the value stored in plugin.settings.""" + plugin = _make_plugin_with_settings({"server.uri": "ldap://custom:389"}) + cls = self._make_adapter_class("server.uri") + adapter = cls(plugin) + self.assertEqual(adapter.prop, "ldap://custom:389") + + def test_setter_stores_value_in_settings(self): + """_setter stores value in plugin.settings[ckey].""" + plugin = _make_plugin_with_settings() + cls = self._make_adapter_class("server.uri") + adapter = cls(plugin) + adapter.prop = "ldap://newhost:1389" + self.assertEqual(plugin.settings["server.uri"], "ldap://newhost:1389") + + +# --------------------------------------------------------------------------- +# LDAPProps.memcached getter/setter (lines 350-354, 371) +# --------------------------------------------------------------------------- + + +class TestLDAPPropsMemcached(unittest.TestCase): + """Tests for LDAPProps.memcached getter and setter.""" + + def _make_props(self): + plugin = _make_plugin_with_settings() + return LDAPProps(plugin) + + def test_memcached_getter_with_record_provider_returns_value(self): + """Returns record.value when a record provider is registered (lines 350-353).""" + props = self._make_props() + mock_record = MagicMock() + mock_record.value = "memcache-host:11211" + mock_provider = MagicMock(return_value=mock_record) + + with patch("pas.plugins.ldap.properties.queryUtility", return_value=mock_provider): + result = props.memcached + + self.assertEqual(result, "memcache-host:11211") + + def test_memcached_getter_without_record_provider_returns_unavailable(self): + """Returns 'feature not available' when no provider is registered (line 354).""" + props = self._make_props() + + with patch("pas.plugins.ldap.properties.queryUtility", return_value=None): + result = props.memcached + + self.assertIn("not available", str(result)) + + def test_memcached_setter_with_record_provider_sets_value(self): + """Sets record.value when a provider is registered (lines 366-369).""" + props = self._make_props() + mock_record = MagicMock() + mock_provider = MagicMock(return_value=mock_record) + + with patch("pas.plugins.ldap.properties.queryUtility", return_value=mock_provider): + props.memcached = "new-host:11211" + + self.assertEqual(mock_record.value, "new-host:11211") + + def test_memcached_setter_without_record_provider_returns_unavailable(self): + """Returns 'feature not available' in setter else branch (line 371).""" + props = self._make_props() + + # When queryUtility returns None, the else-branch (line 371) executes. + # The return value of a setter is normally discarded; we just verify + # that no exception is raised and that no record is written. + with patch("pas.plugins.ldap.properties.queryUtility", return_value=None): + props.memcached = "some-value" # must not raise + + +# --------------------------------------------------------------------------- +# UsersConfig.expiresAttr / expiresUnit (lines 407, 416) +# --------------------------------------------------------------------------- + + +class TestUsersConfigExpiry(unittest.TestCase): + """Tests for UsersConfig.expiresAttr and expiresUnit properties.""" + + def _make_users_config( + self, account_expiration=True, expires_attr="shadowExpire", expires_unit=1 + ): + plugin = _make_plugin_with_settings( + { + "users.account_expiration": account_expiration, + "users.expires_attr": expires_attr, + "users.expires_unit": expires_unit, + } + ) + return UsersConfig(plugin) + + # --- expiresAttr (line 407) --- + + def test_expires_attr_returns_attr_when_expiration_enabled(self): + """expiresAttr returns the attribute name when account_expiration is True (line 407).""" + config = self._make_users_config(account_expiration=True, expires_attr="shadowExpire") + self.assertEqual(config.expiresAttr, "shadowExpire") + + def test_expires_attr_returns_none_when_expiration_disabled(self): + """expiresAttr returns None when account_expiration is False.""" + config = self._make_users_config(account_expiration=False) + self.assertIsNone(config.expiresAttr) + + # --- expiresUnit (line 416) --- + + def test_expires_unit_returns_unit_when_expiration_enabled(self): + """expiresUnit returns the unit value when account_expiration is True (line 416).""" + config = self._make_users_config(account_expiration=True, expires_unit=1) + self.assertEqual(config.expiresUnit, 1) + + def test_expires_unit_returns_zero_when_expiration_disabled(self): + """expiresUnit returns 0 when account_expiration is False.""" + config = self._make_users_config(account_expiration=False, expires_unit=99) + self.assertEqual(config.expiresUnit, 0) + + +class TestGroupsConfigInit(unittest.TestCase): + """Tests to cover GroupsConfig.__init__ (line 425).""" + + def test_init_stores_plugin(self): + """GroupsConfig.__init__ stores plugin on self.plugin (line 425).""" + plugin = MagicMock() + config = GroupsConfig(plugin) + self.assertIs(config.plugin, plugin) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_setuphandlers_unit.py b/tests/test_setuphandlers_unit.py new file mode 100644 index 0000000..5d4e7a9 --- /dev/null +++ b/tests/test_setuphandlers_unit.py @@ -0,0 +1,169 @@ +"""Pure unit tests for pas.plugins.ldap.setuphandlers module. + +These tests exercise all branches of setuphandlers.py using unittest.mock +so no real LDAP server, Zope layer, or GenericSetup context is needed. +""" + +import unittest +from unittest.mock import MagicMock, patch, call + +from pas.plugins.ldap.setuphandlers import ( + TITLE, + _addPlugin, + post_install, + remove_persistent_import_step, +) + + +# --------------------------------------------------------------------------- +# remove_persistent_import_step (lines 28-31) +# --------------------------------------------------------------------------- + +class TestRemovePersistentImportStep(unittest.TestCase): + """Tests for remove_persistent_import_step.""" + + def _make_context(self, has_step=False): + """Return a mock GenericSetup context.""" + registry = MagicMock() + if has_step: + registry._registered = {"pas.plugins.ldap.setup": object()} + else: + registry._registered = {} + context = MagicMock() + context.getImportStepRegistry.return_value = registry + return context, registry + + def test_unregisters_step_when_present(self): + """Calls unregisterStep when the import step is in _registered (line 31).""" + context, registry = self._make_context(has_step=True) + remove_persistent_import_step(context) + registry.unregisterStep.assert_called_once_with("pas.plugins.ldap.setup") + + def test_does_not_unregister_when_step_absent(self): + """Does NOT call unregisterStep when the import step is not present (line 30 branch).""" + context, registry = self._make_context(has_step=False) + remove_persistent_import_step(context) + registry.unregisterStep.assert_not_called() + + def test_calls_getImportStepRegistry(self): + """Always calls context.getImportStepRegistry() (line 28).""" + context, _ = self._make_context() + remove_persistent_import_step(context) + context.getImportStepRegistry.assert_called_once() + + +# --------------------------------------------------------------------------- +# _addPlugin (lines 34-50) +# --------------------------------------------------------------------------- + +class TestAddPlugin(unittest.TestCase): + """Tests for _addPlugin helper.""" + + def _make_pas(self, installed=None): + """Return a mock PAS instance.""" + pas = MagicMock() + pas.objectIds.return_value = installed or [] + return pas + + def test_returns_already_installed_message_when_plugin_exists(self): + """Returns 'already installed' message when pluginid is in objectIds (lines 38-39).""" + pas = self._make_pas(installed=["pasldap"]) + result = _addPlugin(pas) + self.assertIn("already installed", result) + self.assertEqual(result, TITLE + " already installed.") + + def test_logs_info_when_plugin_already_installed(self): + """Logs info message when plugin already present (line 38).""" + pas = self._make_pas(installed=["pasldap"]) + with patch("pas.plugins.ldap.setuphandlers.logger") as mock_logger: + _addPlugin(pas) + mock_logger.info.assert_called_once() + args = mock_logger.info.call_args[0] + self.assertIn("pasldap", args[1]) + + def test_already_installed_with_custom_pluginid(self): + """Works with a custom pluginid (line 37 branch).""" + pas = self._make_pas(installed=["myplugin"]) + result = _addPlugin(pas, pluginid="myplugin") + self.assertIn("already installed", result) + + def test_installs_plugin_when_not_present(self): + """Creates plugin and calls _setObject when pluginid is not installed (lines 40-50).""" + pas = self._make_pas(installed=[]) + # No interfaces provide the plugin, so the loop body is skipped + pas.plugins.listPluginTypeInfo.return_value = [] + + with patch("pas.plugins.ldap.setuphandlers.LDAPPlugin") as MockPlugin: + mock_plugin_instance = MagicMock() + mock_plugin_instance.getId.return_value = "pasldap" + MockPlugin.return_value = mock_plugin_instance + pas.__getitem__ = MagicMock(return_value=mock_plugin_instance) + _addPlugin(pas) + + pas._setObject.assert_called_once() + MockPlugin.assert_called_once_with("pasldap", title=TITLE) + + def test_activates_interfaces_provided_by_plugin(self): + """Activates and reorders plugin for each matching interface (lines 45-50).""" + pas = self._make_pas(installed=[]) + mock_iface = MagicMock() + mock_iface.providedBy.return_value = True + pas.plugins.listPluginTypeInfo.return_value = [{"interface": mock_iface}] + pas.plugins.listPlugins.return_value = [("pasldap", MagicMock())] + + with patch("pas.plugins.ldap.setuphandlers.LDAPPlugin") as MockPlugin: + mock_plugin_instance = MagicMock() + mock_plugin_instance.getId.return_value = "pasldap" + MockPlugin.return_value = mock_plugin_instance + pas.__getitem__ = MagicMock(return_value=mock_plugin_instance) + _addPlugin(pas) + + pas.plugins.activatePlugin.assert_called_once_with(mock_iface, "pasldap") + pas.plugins.movePluginsDown.assert_called_once() + + def test_skips_interface_not_provided_by_plugin(self): + """Skips interface when plugin does not provide it (line 46 continue branch).""" + pas = self._make_pas(installed=[]) + mock_iface = MagicMock() + mock_iface.providedBy.return_value = False # not provided → continue + pas.plugins.listPluginTypeInfo.return_value = [{"interface": mock_iface}] + + with patch("pas.plugins.ldap.setuphandlers.LDAPPlugin") as MockPlugin: + mock_plugin_instance = MagicMock() + mock_plugin_instance.getId.return_value = "pasldap" + MockPlugin.return_value = mock_plugin_instance + pas.__getitem__ = MagicMock(return_value=mock_plugin_instance) + _addPlugin(pas) + + pas.plugins.activatePlugin.assert_not_called() + pas.plugins.movePluginsDown.assert_not_called() + + +# --------------------------------------------------------------------------- +# post_install (lines 53-57) +# --------------------------------------------------------------------------- + +class TestPostInstall(unittest.TestCase): + """Tests for post_install.""" + + def test_calls_addPlugin_with_acl_users(self): + """post_install gets the site, accesses acl_users and calls _addPlugin (lines 55-57).""" + mock_pas = MagicMock() + mock_site = MagicMock() + mock_site.acl_users = mock_pas + + with patch("pas.plugins.ldap.setuphandlers.getSite", return_value=mock_site): + with patch("pas.plugins.ldap.setuphandlers._addPlugin") as mock_add: + post_install(MagicMock()) + + mock_add.assert_called_once_with(mock_pas) + + def test_uses_getSite_to_get_site(self): + """post_install calls getSite() (line 55).""" + mock_site = MagicMock() + + with patch("pas.plugins.ldap.setuphandlers.getSite", return_value=mock_site) as mock_get_site: + with patch("pas.plugins.ldap.setuphandlers._addPlugin"): + post_install(MagicMock()) + + mock_get_site.assert_called_once() diff --git a/tests/test_sheet_unit.py b/tests/test_sheet_unit.py new file mode 100644 index 0000000..dd33346 --- /dev/null +++ b/tests/test_sheet_unit.py @@ -0,0 +1,368 @@ +"""Pure unit tests for pas.plugins.ldap.sheet module. + +These tests exercise all branches of sheet.py using unittest.mock so no +real LDAP server, Zope layer, or acquisition context is needed. +""" + +import unittest +from unittest.mock import MagicMock, patch, call + + +# --------------------------------------------------------------------------- +# Helper builders +# --------------------------------------------------------------------------- + +def _make_bare_sheet(properties=None, principal_type="users", principal_id="uid0", context_raises=False): + """Create a LDAPUserPropertySheet bypassing __init__, with full state set.""" + from pas.plugins.ldap.sheet import LDAPUserPropertySheet + + sheet = LDAPUserPropertySheet.__new__(LDAPUserPropertySheet) + sheet._properties = dict(properties or {"mail": "old@example.com"}) + sheet._attrmap = dict(properties or {"mail": "mail"}) + sheet._ldapprincipal_type = principal_type + sheet._ldapprincipal_id = principal_id + + mock_plugin = MagicMock() + mock_ldap_principal = MagicMock() + if context_raises: + mock_ldap_principal.context.side_effect = Exception("ctx_error") + + mock_plugin.users.__getitem__.return_value = mock_ldap_principal + mock_plugin.groups.__getitem__.return_value = mock_ldap_principal + sheet._plugin = mock_plugin + + return sheet, mock_ldap_principal + + +def _build_init_sheet(user_in_users=True, attrmap=None, request=None): + """ + Fully instantiate LDAPUserPropertySheet with all external dependencies + mocked so that no Zope/LDAP infrastructure is required. + """ + from pas.plugins.ldap.sheet import LDAPUserPropertySheet + + if attrmap is None: + attrmap = {"mail": "mail", "cn": "cn"} + + principal = MagicMock() + principal.getId.return_value = "uid0" + + plugin = MagicMock() + plugin.getId.return_value = "ldapplugin" + # `"uid0" in plugin.users` → user_in_users + plugin.users.__contains__.return_value = user_in_users + + mock_pcfg = MagicMock() + mock_pcfg.attrmap.items.return_value = list(attrmap.items()) + + mock_ldap_principal = MagicMock() + mock_ldap_principal.attrs.get.side_effect = lambda k, default="": f"v_{k}" + plugin.users.__getitem__.return_value = mock_ldap_principal + plugin.groups.__getitem__.return_value = mock_ldap_principal + + with patch("pas.plugins.ldap.sheet.aq_base", side_effect=lambda x: x), \ + patch("pas.plugins.ldap.sheet.ILDAPUsersConfig", return_value=mock_pcfg), \ + patch("pas.plugins.ldap.sheet.ILDAPGroupsConfig", return_value=mock_pcfg), \ + patch("pas.plugins.ldap.sheet.getRequest", return_value=request), \ + patch("pas.plugins.ldap.sheet.UserPropertySheet.__init__", return_value=None): + sheet = LDAPUserPropertySheet(principal, plugin) + + return sheet, plugin, mock_ldap_principal, mock_pcfg + + +# --------------------------------------------------------------------------- +# __init__ (lines 32-58) +# --------------------------------------------------------------------------- + +class TestLDAPUserPropertySheetInit(unittest.TestCase): + """Tests for LDAPUserPropertySheet.__init__.""" + + # --- user / group branch (lines 36-41) --- + + def test_init_user_branch_sets_type_to_users(self): + """When id is in plugin.users, _ldapprincipal_type = 'users' (lines 36-38).""" + sheet, _, _, _ = _build_init_sheet(user_in_users=True) + self.assertEqual(sheet._ldapprincipal_type, "users") + + def test_init_group_branch_sets_type_to_groups(self): + """When id NOT in plugin.users, _ldapprincipal_type = 'groups' (lines 40-41).""" + sheet, _, _, _ = _build_init_sheet(user_in_users=False) + self.assertEqual(sheet._ldapprincipal_type, "groups") + + def test_init_uses_ILDAPUsersConfig_for_user(self): + """ILDAPUsersConfig(plugin) is called for user principals (line 37).""" + with patch("pas.plugins.ldap.sheet.aq_base", side_effect=lambda x: x), \ + patch("pas.plugins.ldap.sheet.ILDAPUsersConfig") as mock_cfg, \ + patch("pas.plugins.ldap.sheet.ILDAPGroupsConfig"), \ + patch("pas.plugins.ldap.sheet.getRequest", return_value=None), \ + patch("pas.plugins.ldap.sheet.UserPropertySheet.__init__", return_value=None): + from pas.plugins.ldap.sheet import LDAPUserPropertySheet + principal = MagicMock() + principal.getId.return_value = "uid0" + plugin = MagicMock() + plugin.getId.return_value = "ldapplugin" + plugin.users.__contains__.return_value = True + mock_pcfg = MagicMock() + mock_pcfg.attrmap.items.return_value = [] + mock_cfg.return_value = mock_pcfg + plugin.users.__getitem__.return_value = MagicMock() + LDAPUserPropertySheet(principal, plugin) + mock_cfg.assert_called_once_with(plugin) + + def test_init_uses_ILDAPGroupsConfig_for_group(self): + """ILDAPGroupsConfig(plugin) is called for group principals (line 40).""" + with patch("pas.plugins.ldap.sheet.aq_base", side_effect=lambda x: x), \ + patch("pas.plugins.ldap.sheet.ILDAPUsersConfig"), \ + patch("pas.plugins.ldap.sheet.ILDAPGroupsConfig") as mock_cfg, \ + patch("pas.plugins.ldap.sheet.getRequest", return_value=None), \ + patch("pas.plugins.ldap.sheet.UserPropertySheet.__init__", return_value=None): + from pas.plugins.ldap.sheet import LDAPUserPropertySheet + principal = MagicMock() + principal.getId.return_value = "uid0" + plugin = MagicMock() + plugin.getId.return_value = "ldapplugin" + plugin.users.__contains__.return_value = False + mock_pcfg = MagicMock() + mock_pcfg.attrmap.items.return_value = [] + mock_cfg.return_value = mock_pcfg + plugin.groups.__getitem__.return_value = MagicMock() + LDAPUserPropertySheet(principal, plugin) + mock_cfg.assert_called_once_with(plugin) + + # --- attrmap loop (lines 42-46) --- + + def test_init_attrmap_skips_rdn_key(self): + """'rdn' key is skipped in attrmap (lines 42-45).""" + sheet, _, _, _ = _build_init_sheet(attrmap={"rdn": "uid", "mail": "mail"}) + self.assertNotIn("rdn", sheet._attrmap) + + def test_init_attrmap_skips_id_key(self): + """'id' key is skipped in attrmap (lines 43-45).""" + sheet, _, _, _ = _build_init_sheet(attrmap={"id": "uid", "mail": "mail"}) + self.assertNotIn("id", sheet._attrmap) + + def test_init_attrmap_includes_other_keys(self): + """Non-rdn/id keys are added to _attrmap (line 46).""" + sheet, _, _, _ = _build_init_sheet(attrmap={"mail": "mail", "cn": "cn"}) + self.assertIn("mail", sheet._attrmap) + self.assertIn("cn", sheet._attrmap) + + # --- request handling (lines 48-53) --- + + def test_init_no_request_calls_context_load(self): + """With request=None, attrs.context.load() is called (lines 50-51).""" + sheet, _, mock_ldap_principal, _ = _build_init_sheet( + request=None, attrmap={"mail": "mail"} + ) + mock_ldap_principal.attrs.context.load.assert_called_once() + + def test_init_no_request_does_not_set_reload_flag(self): + """With request=None, _ldap_props_reloaded is NOT set (line 52 False branch).""" + request = None + sheet, _, _, _ = _build_init_sheet(request=request, attrmap={"mail": "mail"}) + # No exception means the code didn't try to set the item on None + + def test_init_request_not_reloaded_sets_flag(self): + """With a fresh request, loads attrs and sets _ldap_props_reloaded (lines 50-53).""" + request = MagicMock() + request.get.return_value = None # not yet reloaded → falsy + sheet, _, mock_ldap_principal, _ = _build_init_sheet( + request=request, attrmap={"mail": "mail"} + ) + mock_ldap_principal.attrs.context.load.assert_called_once() + request.__setitem__.assert_called_with("_ldap_props_reloaded", 1) + + def test_init_request_already_reloaded_skips_load(self): + """With a reloaded request, skips attrs.context.load (line 50 False branch).""" + request = MagicMock() + request.get.return_value = 1 # already reloaded → truthy + sheet, _, mock_ldap_principal, _ = _build_init_sheet( + request=request, attrmap={"mail": "mail"} + ) + mock_ldap_principal.attrs.context.load.assert_not_called() + + # --- property loading (lines 54-58) --- + + def test_init_loads_properties_from_ldap_attrs(self): + """Properties are loaded from ldapprincipal.attrs for each attrmap key (lines 54-55).""" + sheet, _, mock_ldap_principal, _ = _build_init_sheet( + attrmap={"mail": "mail"}, request=None + ) + mock_ldap_principal.attrs.get.assert_called_with("mail", "") + + def test_init_calls_userpropertysheet_init(self): + """UserPropertySheet.__init__ is called at the end of __init__ (line 56).""" + with patch("pas.plugins.ldap.sheet.aq_base", side_effect=lambda x: x), \ + patch("pas.plugins.ldap.sheet.ILDAPUsersConfig") as mock_cfg, \ + patch("pas.plugins.ldap.sheet.ILDAPGroupsConfig"), \ + patch("pas.plugins.ldap.sheet.getRequest", return_value=None), \ + patch("pas.plugins.ldap.sheet.UserPropertySheet.__init__", return_value=None) as mock_up_init: + from pas.plugins.ldap.sheet import LDAPUserPropertySheet + principal = MagicMock() + principal.getId.return_value = "uid0" + plugin = MagicMock() + plugin.getId.return_value = "ldapplugin" + plugin.users.__contains__.return_value = True + mock_pcfg = MagicMock() + mock_pcfg.attrmap.items.return_value = [] + mock_cfg.return_value = mock_pcfg + plugin.users.__getitem__.return_value = MagicMock() + LDAPUserPropertySheet(principal, plugin) + mock_up_init.assert_called_once() + + def test_init_stores_principal_id(self): + """_ldapprincipal_id is set from principal.getId() (line 35).""" + sheet, _, _, _ = _build_init_sheet() + self.assertEqual(sheet._ldapprincipal_id, "uid0") + + def test_init_combined_rdn_and_other_keys(self): + """Mixed attrmap: skips rdn/id, keeps others; covers lines 43-46.""" + sheet, _, _, _ = _build_init_sheet( + attrmap={"rdn": "uid", "id": "uid", "mail": "mail", "cn": "cn"}, + request=None, + ) + self.assertEqual(sheet._attrmap, {"mail": "mail", "cn": "cn"}) + + +# --------------------------------------------------------------------------- +# _get_ldap_principal (lines 66-67) +# --------------------------------------------------------------------------- + +class TestGetLDAPPrincipal(unittest.TestCase): + """Tests for _get_ldap_principal.""" + + def test_returns_users_entry_for_user_type(self): + """getattr(plugin, 'users')[id] returned for type='users' (lines 66-67).""" + sheet, mock_ldap_principal = _make_bare_sheet(principal_type="users") + result = sheet._get_ldap_principal() + sheet._plugin.users.__getitem__.assert_called_with("uid0") + self.assertIs(result, mock_ldap_principal) + + def test_returns_groups_entry_for_group_type(self): + """getattr(plugin, 'groups')[id] returned for type='groups' (lines 66-67).""" + sheet, mock_ldap_principal = _make_bare_sheet(principal_type="groups") + result = sheet._get_ldap_principal() + sheet._plugin.groups.__getitem__.assert_called_with("uid0") + self.assertIs(result, mock_ldap_principal) + + +# --------------------------------------------------------------------------- +# canWriteProperty (line 71) +# --------------------------------------------------------------------------- + +class TestCanWriteProperty(unittest.TestCase): + """Tests for canWriteProperty.""" + + def test_returns_true_for_existing_property(self): + """Returns True when id is in _properties (line 71).""" + sheet, _ = _make_bare_sheet(properties={"mail": "test@example.com"}) + self.assertTrue(sheet.canWriteProperty(None, "mail")) + + def test_returns_false_for_nonexistent_property(self): + """Returns False when id is NOT in _properties (line 71).""" + sheet, _ = _make_bare_sheet(properties={"mail": "test@example.com"}) + self.assertFalse(sheet.canWriteProperty(None, "cn")) + + +# --------------------------------------------------------------------------- +# setProperty (lines 73-82) +# --------------------------------------------------------------------------- + +class TestSetProperty(unittest.TestCase): + """Tests for setProperty.""" + + def test_updates_properties_dict(self): + """_properties[id] is updated with the new value (line 77).""" + sheet, _ = _make_bare_sheet(properties={"mail": "old@example.com"}) + sheet.setProperty(None, "mail", "new@example.com") + self.assertEqual(sheet._properties["mail"], "new@example.com") + + def test_updates_ldap_attrs(self): + """ldapprincipal.attrs[id] is updated (line 77).""" + sheet, mock_ldap_principal = _make_bare_sheet(properties={"mail": "old@example.com"}) + sheet.setProperty(None, "mail", "new@example.com") + mock_ldap_principal.attrs.__setitem__.assert_called_with("mail", "new@example.com") + + def test_calls_ldapprincipal_context(self): + """ldapprincipal.context() is called to persist the change (line 79).""" + sheet, mock_ldap_principal = _make_bare_sheet(properties={"mail": "old@example.com"}) + sheet.setProperty(None, "mail", "new@example.com") + mock_ldap_principal.context.assert_called_once() + + def test_logs_error_on_context_exception(self): + """Exception from context() is caught and logged (lines 80-82).""" + sheet, _ = _make_bare_sheet( + properties={"mail": "old@example.com"}, context_raises=True + ) + with patch("pas.plugins.ldap.sheet.logger") as mock_logger: + sheet.setProperty(None, "mail", "new@example.com") + mock_logger.error.assert_called_once() + msg = mock_logger.error.call_args[0][0] + self.assertIn("ctx_error", msg) + + def test_asserts_id_must_be_in_properties(self): + """setProperty raises AssertionError for an unknown property (line 75).""" + sheet, _ = _make_bare_sheet(properties={"mail": "old@example.com"}) + with self.assertRaises(AssertionError): + sheet.setProperty(None, "nonexistent", "value") + + +# --------------------------------------------------------------------------- +# setProperties (lines 84-95) +# --------------------------------------------------------------------------- + +class TestSetProperties(unittest.TestCase): + """Tests for setProperties.""" + + def test_updates_all_properties(self): + """All properties in mapping are updated (lines 89-90).""" + sheet, _ = _make_bare_sheet(properties={"mail": "old@example.com", "cn": "Old"}) + sheet.setProperties(None, {"mail": "new@example.com", "cn": "New"}) + self.assertEqual(sheet._properties["mail"], "new@example.com") + self.assertEqual(sheet._properties["cn"], "New") + + def test_updates_ldap_attrs_for_all_keys(self): + """ldapprincipal.attrs[id] is set for each key in mapping (line 90).""" + sheet, mock_ldap_principal = _make_bare_sheet( + properties={"mail": "old@example.com", "cn": "Old"} + ) + sheet.setProperties(None, {"mail": "new@example.com", "cn": "New"}) + calls = mock_ldap_principal.attrs.__setitem__.call_args_list + call_keys = {c[0][0] for c in calls} + self.assertIn("mail", call_keys) + self.assertIn("cn", call_keys) + + def test_calls_ldapprincipal_context(self): + """ldapprincipal.context() is called once to persist all changes (line 92).""" + sheet, mock_ldap_principal = _make_bare_sheet( + properties={"mail": "old@example.com"} + ) + sheet.setProperties(None, {"mail": "new@example.com"}) + mock_ldap_principal.context.assert_called_once() + + def test_logs_error_on_context_exception(self): + """Exception from context() is caught and logged (lines 93-95).""" + sheet, _ = _make_bare_sheet( + properties={"mail": "old@example.com"}, context_raises=True + ) + with patch("pas.plugins.ldap.sheet.logger") as mock_logger: + sheet.setProperties(None, {"mail": "new@example.com"}) + mock_logger.error.assert_called_once() + msg = mock_logger.error.call_args[0][0] + self.assertIn("ctx_error", msg) + + def test_asserts_all_ids_must_be_in_properties(self): + """setProperties raises AssertionError for an unknown property (line 87).""" + sheet, _ = _make_bare_sheet(properties={"mail": "old@example.com"}) + with self.assertRaises(AssertionError): + sheet.setProperties(None, {"nonexistent": "value"}) + + def test_asserts_checks_all_keys_before_updating(self): + """Validation loop (lines 86-87) runs before update loop (lines 89-90).""" + sheet, mock_ldap_principal = _make_bare_sheet( + properties={"mail": "old@example.com"} + ) + with self.assertRaises(AssertionError): + sheet.setProperties(None, {"mail": "new@example.com", "bad_key": "value"}) + # ldapprincipal.context() must NOT have been called (failed before update) + mock_ldap_principal.context.assert_not_called() diff --git a/tests/testing.py b/tests/testing.py index bf92924..763b328 100644 --- a/tests/testing.py +++ b/tests/testing.py @@ -1,3 +1,5 @@ +"""Testing layer for pas.plugins.ldap.""" + from pas.plugins.ldap.cache import cacheProviderFactory from pas.plugins.ldap.cache import cacheProviderFactory from pas.plugins.ldap.interfaces import ICacheSettingsRecordProvider @@ -30,6 +32,8 @@ @implementer(ILDAPProps) @adapter(Interface) def ldapprops(context): + """Adapter to provide LDAPProps for the test layer, using + the properties defined in the ldaptesting fixture.""" props = LDAPProps(context) props.uri = ldaptesting.props.uri @@ -45,16 +49,21 @@ def ldapprops(context): @implementer(ILDAPUsersConfig) @adapter(Interface) def usersconfig(context): + """Adapter to provide LDAPUsersConfig for the test layer, using + the configuration defined in the ldaptesting fixture.""" return ldaptesting.LDIF_groupOfNames_10_10.ucfg @implementer(ILDAPGroupsConfig) @adapter(Interface) def groupsconfig(context): + """Adapter to provide LDAPGroupsConfig for the test layer, using + the configuration defined in the ldaptesting fixture.""" return ldaptesting.LDIF_groupOfNames_10_10.gcfg class PASLDAPLayer(Layer): + """Testing layer for pas.plugins.ldap add-on.""" defaultBases = (ldaptesting.LDIF_groupOfNames_10_10, zope.INTEGRATION_TESTING) From c6850ad4e1ca9e42e1d26bf896cb2b7ceac1fa95 Mon Sep 17 00:00:00 2001 From: "Leonardo J. Caballero G" Date: Sat, 2 May 2026 20:07:10 +0200 Subject: [PATCH 71/74] Reusing the PACKAGE_NAME variable in the logger #113 --- src/pas/plugins/ldap/locales/__main__.py | 3 ++- src/pas/plugins/ldap/plonecontrolpanel/setuphandlers.py | 3 ++- src/pas/plugins/ldap/setuphandlers.py | 8 +++++++- 3 files changed, 11 insertions(+), 3 deletions(-) diff --git a/src/pas/plugins/ldap/locales/__main__.py b/src/pas/plugins/ldap/locales/__main__.py index ad5ca75..b644f73 100644 --- a/src/pas/plugins/ldap/locales/__main__.py +++ b/src/pas/plugins/ldap/locales/__main__.py @@ -1,5 +1,6 @@ """Update locales.""" +from pas.plugins.ldap import PACKAGE_NAME from pathlib import Path import logging @@ -14,7 +15,7 @@ locale_path = Path(__file__).parent.resolve() target_path = locale_path.parent.resolve() -domain = "pas.plugins.ldap" +domain = PACKAGE_NAME i18ndude = "uvx i18ndude" lingua = "pot-create" diff --git a/src/pas/plugins/ldap/plonecontrolpanel/setuphandlers.py b/src/pas/plugins/ldap/plonecontrolpanel/setuphandlers.py index d403859..117491f 100644 --- a/src/pas/plugins/ldap/plonecontrolpanel/setuphandlers.py +++ b/src/pas/plugins/ldap/plonecontrolpanel/setuphandlers.py @@ -1,11 +1,12 @@ """Setup handlers for the LDAP plugin.""" +from pas.plugins.ldap import PACKAGE_NAME from pas.plugins.ldap.plugin import LDAPPlugin from zope.component.hooks import getSite import logging -logger = logging.getLogger(__name__) +logger = logging.getLogger(PACKAGE_NAME) TITLE = "LDAP plugin (pas.plugins.ldap)" diff --git a/src/pas/plugins/ldap/setuphandlers.py b/src/pas/plugins/ldap/setuphandlers.py index 3e9822c..2a855a0 100644 --- a/src/pas/plugins/ldap/setuphandlers.py +++ b/src/pas/plugins/ldap/setuphandlers.py @@ -1,9 +1,14 @@ """Setup handlers for the LDAP plugin.""" from .plugin import LDAPPlugin +from pas.plugins.ldap import PACKAGE_NAME from zope.component.hooks import getSite -TITLE = "LDAP plugin (pas.plugins.ldap)" +import logging + +logger = logging.getLogger(PACKAGE_NAME) + +TITLE = f"LDAP plugin ({PACKAGE_NAME})" def remove_persistent_import_step(context): @@ -30,6 +35,7 @@ def _addPlugin(pas, pluginid="pasldap"): """Add the LDAP plugin to the given PAS instance.""" installed = pas.objectIds() if pluginid in installed: + logger.info("LDAPPlugin '%s' is installed in acl_users.", pluginid) return TITLE + " already installed." plugin = LDAPPlugin(pluginid, title=TITLE) pas._setObject(pluginid, plugin) From 1a9095b16db9472c38b46b723c0510b437507dfb Mon Sep 17 00:00:00 2001 From: "Leonardo J. Caballero G" Date: Mon, 4 May 2026 06:07:44 +0200 Subject: [PATCH 72/74] Updated the README file --- README.rst | 52 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/README.rst b/README.rst index 4084cf1..9aa97d3 100644 --- a/README.rst +++ b/README.rst @@ -220,6 +220,58 @@ This package works fine for several 10000 users or groups, **unless you list use This is not that much a problem for small amount of users. There is room for future optimization in the underlying `node.ext.ldap `_. +---- + +Development Workflow +==================== + +1. Install requirements: + +:: + make install + +2. Start backend (Docker): + +:: + make backend-docker-start + +3. In another terminal, start frontend: + +:: + make start + +4. Black format and lint code: + +:: + make black-check && make black-format && make black-check + +5. Isort format and lint code: + +:: + make isort-check && make isort-format && make isort-check + +6. Zpretty format and lint code: + +:: + make zpretty-check && make zpretty-format && make zpretty-check + +1. Extract i18n messages: + +:: + make i18n + +7. Run unit tests: + +:: + make test + +8. Run coverage unit tests: + +:: + make coverage + +---- + Source Code =========== From 428d86386d999099396b313e8d673c34ba2507a6 Mon Sep 17 00:00:00 2001 From: "Leonardo J. Caballero G" Date: Mon, 4 May 2026 06:11:22 +0200 Subject: [PATCH 73/74] Updated the README file --- README.rst | 37 ++++++++++++++++++++----------------- 1 file changed, 20 insertions(+), 17 deletions(-) diff --git a/README.rst b/README.rst index 9aa97d3..495d899 100644 --- a/README.rst +++ b/README.rst @@ -227,47 +227,50 @@ Development Workflow 1. Install requirements: -:: - make install +.. code-block:: shell -2. Start backend (Docker): + make install -:: - make backend-docker-start +2. In another terminal, start frontend: -3. In another terminal, start frontend: +.. code-block:: shell -:: make start -4. Black format and lint code: +3. Black format and lint code: + +.. code-block:: shell -:: make black-check && make black-format && make black-check -5. Isort format and lint code: +4. Isort format and lint code: + +.. code-block:: shell -:: make isort-check && make isort-format && make isort-check -6. Zpretty format and lint code: +5. Zpretty format and lint code: + +.. code-block:: shell -:: make zpretty-check && make zpretty-format && make zpretty-check -1. Extract i18n messages: +6. Extract i18n messages: + +.. code-block:: shell -:: make i18n 7. Run unit tests: -:: +.. code-block:: shell + make test 8. Run coverage unit tests: -:: +.. code-block:: shell + make coverage ---- From 622cfae3bbbc227f46d25b112a07ddbe5ff4cbd7 Mon Sep 17 00:00:00 2001 From: "Leonardo J. Caballero G" Date: Mon, 4 May 2026 06:14:09 +0200 Subject: [PATCH 74/74] Updated the README file --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rst b/README.rst index 495d899..2c0acc0 100644 --- a/README.rst +++ b/README.rst @@ -259,7 +259,7 @@ Development Workflow .. code-block:: shell - make i18n + make gettext-create && make gettext-update && make gettext-compile 7. Run unit tests:
Id Id
Title Title
- +