diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml new file mode 100644 index 0000000..c80eee4 --- /dev/null +++ b/.github/workflows/tests.yaml @@ -0,0 +1,46 @@ +name: Test the pas.plugins.ldap code +on: + push + +jobs: + build: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + python: + - "3.10" + - "3.11" + - "3.12" + - "3.13" + 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 build-essential libsasl2-dev libssl-dev libdb-dev libldap2-dev + + - name: Setup Plone ${{ matrix.plone }} with Python ${{ matrix.python }} + id: setup + 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 + run: | + make VENV=off test diff --git a/.gitignore b/.gitignore index b40829b..78c3c45 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 +/.mxmake/ /.project /.pydevproject /.Python /.settings/ +/.venv/ /.vscode/ -/bin +/*eggs/ /coverage/ -/devsrc/ /dist/ /htmlcov/ -/include/ -/lib/ -/local/ -/parts/ -/share/ -/src/*.egg-info -/var/ -/buildout.cfg +/instance/ +/openldap/ /pip-selfcheck.json +/sources/ +/src/*.egg-info 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..715e307 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -2,10 +2,41 @@ History ======= -1.8.3 (unreleased) +2.0.0 (unreleased) ------------------ -- Nothing changed yet. +- 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) +------------------ + +- Added Spanish translation #132 + [macagua] + +- Added the initial i18n support #131 + [macagua] + +- Drop support for Plone 5/ Python < 3.9 + [jensens] 1.8.2 (2022-10-31) diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..730d85d --- /dev/null +++ b/Makefile @@ -0,0 +1,845 @@ +############################################################################## +# THIS FILE IS GENERATED BY MXMAKE +# +# DOMAINS: +#: applications.cookiecutter +#: applications.zope +#: core.base +#: core.mxenv +#: core.mxfiles +#: core.packages +#: core.sources +#: i18n.gettext +#: ldap.openldap +#: qa.black +#: qa.coverage +#: qa.isort +#: qa.test +#: qa.zpretty +# +# SETTINGS (ALL CHANGES MADE BELOW SETTINGS WILL BE LOST) +############################################################################## + +## core.base + +# `deploy` target dependencies. +# No default value. +DEPLOY_TARGETS?= + +# target to be executed when calling `make run` +# No default value. +RUN_TARGET?=zope-start + +# Additional files and folders to remove when running clean target +# No default value. +CLEAN_FS?= + +# Optional makefile to include before default targets. This can +# be used to provide custom targets or hook up to existing targets. +# Default: include.mk +INCLUDE_MAKEFILE?=include.mk + +# 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?= + +## ldap.openldap + +# OpenLDAP version to download +# Default: 2.4.59 +OPENLDAP_VERSION?=2.6.9 + +# 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 +# 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 + +# Minimum required Python version. +# 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 `PRIMARY_PYTHON` found in `PATH` is used. +# Default: true +VENV_ENABLED?=true + +# Flag whether to create a virtual environment. If set to `false` +# and `VENV_ENABLED` is `true`, `VENV_FOLDER` is expected to point to an +# existing virtual environment. +# Default: true +VENV_CREATE?=true + +# The folder of the virtual environment. +# If `VENV_ENABLED` is `true` and `VENV_CREATE` is true it is used as the +# 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 +VENV_FOLDER?=.venv + +# mxdev to install in virtual environment. +# Default: mxdev +MXDEV?=mxdev + +# mxmake to install in virtual environment. +# Default: mxmake +MXMAKE?=mxmake + +## qa.zpretty + +# Source folder to scan for XML and ZCML files. +# Default: src +ZPRETTY_SRC?=src + +## qa.isort + +# Source folder to scan for Python files to run isort on. +# Default: src +ISORT_SRC?=src + +## qa.black + +# Source folder to scan for Python files to run black on. +# Default: src +BLACK_SRC?=src + +## core.mxfiles + +# The config file to use. +# 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 +# :ref:`run-tests` template gets rendered to if configured. +# Default: .mxmake/files/run-tests.sh +TEST_COMMAND?=.mxmake/files/run-tests.sh pas.plugins.ldap + +# Additional Python requirements for running tests to be +# installed (via pip). +# Default: pytest +TEST_REQUIREMENTS?=zope.testrunner + +# Additional make targets the test target depends on. +# No default value. +TEST_DEPENDENCY_TARGETS?=openldap + +## qa.coverage + +# The command which gets executed. Defaults to the location the +# :ref:`run-coverage` template gets rendered to if configured. +# Default: .mxmake/files/run-coverage.sh +COVERAGE_COMMAND?=.mxmake/files/run-coverage.sh + +## applications.zope + +# cookiecutter configuration file to use +# Default: instance.yaml +ZOPE_CONFIGURATION_FILE?=instance.yaml + +# cookiecutter configuration file to use +# 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?=. + +# script to run +# 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. +# Default: locale +GETTEXT_LOCALES_PATH?=locale + +# Translation domain to use. +# No default value. +GETTEXT_DOMAIN?= + +# Space separated list of language identifiers. +# No default value. +GETTEXT_LANGUAGES?= + +############################################################################## +# END SETTINGS - DO NOT EDIT BELOW THIS LINE +############################################################################## + +INSTALL_TARGETS?= +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: +# 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 + +# mxmake folder +MXMAKE_FOLDER?=.mxmake + +# Sentinel files +SENTINEL_FOLDER?=$(MXMAKE_FOLDER)/sentinels +SENTINEL?=$(SENTINEL_FOLDER)/about.txt +$(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) || 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 \ + && 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 +############################################################################## + +OS?= + +# 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 +export PATH:=$(VENV_EXECUTABLE_FOLDER):$(PATH) +MXENV_PYTHON=python +else +MXENV_PYTHON=$(PRIMARY_PYTHON) +endif + +# Determine the package installer +ifeq ("$(PYTHON_PACKAGE_INSTALLER)","uv") +PYTHON_PACKAGE_COMMAND=uv pip +else +PYTHON_PACKAGE_COMMAND=$(MXENV_PYTHON) -m pip +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" ]] \ + && echo "Package installer uv does not work with a global Python interpreter." && exit 1 || : +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) +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 +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 +mxenv: $(MXENV_TARGET) + +.PHONY: mxenv-dirty +mxenv-dirty: + @rm -f $(MXENV_TARGET) + +.PHONY: mxenv-clean +mxenv-clean: mxenv-dirty +ifeq ("$(VENV_ENABLED)", "true") +ifeq ("$(VENV_CREATE)", "true") + @rm -rf $(VENV_FOLDER) +endif +else + @$(PYTHON_PACKAGE_COMMAND) uninstall -y $(MXDEV) + @$(PYTHON_PACKAGE_COMMAND) uninstall -y $(MXMAKE) +endif + +INSTALL_TARGETS+=mxenv +DIRTY_TARGETS+=mxenv-dirty +CLEAN_TARGETS+=mxenv-clean + +############################################################################## +# zpretty +############################################################################## + +ZPRETTY_TARGET:=$(SENTINEL_FOLDER)/zpretty.sentinel +$(ZPRETTY_TARGET): $(MXENV_TARGET) + @echo "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 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 zpretty -i {} + + +.PHONY: zpretty-dirty +zpretty-dirty: + @rm -f $(ZPRETTY_TARGET) + +.PHONY: zpretty-clean +zpretty-clean: zpretty-dirty + @test -e $(MXENV_PYTHON) && $(MXENV_PYTHON) -m pip uninstall -y zpretty || : + +INSTALL_TARGETS+=$(ZPRETTY_TARGET) +CHECK_TARGETS+=zpretty-check +FORMAT_TARGETS+=zpretty-format +DIRTY_TARGETS+=zpretty-dirty +CLEAN_TARGETS+=zpretty-clean + +############################################################################## +# isort +############################################################################## + +ISORT_TARGET:=$(SENTINEL_FOLDER)/isort.sentinel +$(ISORT_TARGET): $(MXENV_TARGET) + @echo "Install isort" + @$(PYTHON_PACKAGE_COMMAND) install isort + @touch $(ISORT_TARGET) + +.PHONY: isort-check +isort-check: $(ISORT_TARGET) + @echo "Run isort check" + @isort --check $(ISORT_SRC) + +.PHONY: isort-format +isort-format: $(ISORT_TARGET) + @echo "Run isort format" + @isort $(ISORT_SRC) + +.PHONY: isort-dirty +isort-dirty: + @rm -f $(ISORT_TARGET) + +.PHONY: isort-clean +isort-clean: isort-dirty + @test -e $(MXENV_PYTHON) && $(MXENV_PYTHON) -m pip uninstall -y isort || : + +INSTALL_TARGETS+=$(ISORT_TARGET) +CHECK_TARGETS+=isort-check +FORMAT_TARGETS+=isort-format +DIRTY_TARGETS+=isort-dirty +CLEAN_TARGETS+=isort-clean + +############################################################################## +# black +############################################################################## + +BLACK_TARGET:=$(SENTINEL_FOLDER)/black.sentinel +$(BLACK_TARGET): $(MXENV_TARGET) + @echo "Install Black" + @$(PYTHON_PACKAGE_COMMAND) install black + @touch $(BLACK_TARGET) + +.PHONY: black-check +black-check: $(BLACK_TARGET) + @echo "Run black checks" + @black --check $(BLACK_SRC) + +.PHONY: black-format +black-format: $(BLACK_TARGET) + @echo "Run black format" + @black $(BLACK_SRC) + +.PHONY: black-dirty +black-dirty: + @rm -f $(BLACK_TARGET) + +.PHONY: black-clean +black-clean: black-dirty + @test -e $(MXENV_PYTHON) && $(MXENV_PYTHON) -m pip uninstall -y black || : + +INSTALL_TARGETS+=$(BLACK_TARGET) +CHECK_TARGETS+=black-check +FORMAT_TARGETS+=black-format +DIRTY_TARGETS+=black-dirty +CLEAN_TARGETS+=black-clean + +############################################################################## +# sources +############################################################################## + +SOURCES_TARGET:=$(SENTINEL_FOLDER)/sources.sentinel +$(SOURCES_TARGET): $(PROJECT_CONFIG) $(MXENV_TARGET) + @echo "Checkout project sources" + @mxdev -o -c $(PROJECT_CONFIG) + @touch $(SOURCES_TARGET) + +.PHONY: sources +sources: $(SOURCES_TARGET) + +.PHONY: sources-dirty +sources-dirty: + @rm -f $(SOURCES_TARGET) + +.PHONY: sources-purge +sources-purge: sources-dirty + @rm -rf sources + +INSTALL_TARGETS+=sources +DIRTY_TARGETS+=sources-dirty +PURGE_TARGETS+=sources-purge + +############################################################################## +# mxfiles +############################################################################## + +# case `core.sources` domain not included +SOURCES_TARGET?= + +# File generation target +MXMAKE_FILES?=$(MXMAKE_FOLDER)/files + +# set environment variables for mxmake +define set_mxfiles_env + @export MXMAKE_FILES=$(1) +endef + +# unset environment variables for mxmake +define unset_mxfiles_env + @unset MXMAKE_FILES +endef + +$(PROJECT_CONFIG): +ifneq ("$(wildcard $(PROJECT_CONFIG))","") + @touch $(PROJECT_CONFIG) +else + @echo "[settings]" > $(PROJECT_CONFIG) +endif + +LOCAL_PACKAGE_FILES:=$(wildcard pyproject.toml setup.cfg setup.py requirements.txt constraints.txt) + +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,$(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 +mxfiles: $(FILES_TARGET) + +.PHONY: mxfiles-dirty +mxfiles-dirty: + @touch $(PROJECT_CONFIG) + +.PHONY: mxfiles-clean +mxfiles-clean: mxfiles-dirty + @rm -rf constraints-mxdev.txt requirements-mxdev.txt $(MXMAKE_FILES) + +INSTALL_TARGETS+=mxfiles +DIRTY_TARGETS+=mxfiles-dirty +CLEAN_TARGETS+=mxfiles-clean + +############################################################################## +# packages +############################################################################## + +# additional sources targets which requires package re-install on change +-include $(MXMAKE_FILES)/additional_sources_targets.mk +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" + @$(PYTHON_PACKAGE_COMMAND) install $(PACKAGES_PRERELEASES) -r $(FILES_TARGET) + @$(PYTHON_PACKAGE_COMMAND) freeze > $(INSTALLED_PACKAGES) + @touch $(PACKAGES_TARGET) + +.PHONY: packages +packages: $(PACKAGES_TARGET) + +.PHONY: packages-dirty +packages-dirty: + @rm -f $(PACKAGES_TARGET) + +.PHONY: packages-clean +packages-clean: + @test -e $(FILES_TARGET) \ + && test -e $(MXENV_PYTHON) \ + && $(MXENV_PYTHON) -m pip uninstall -y -r $(FILES_TARGET) \ + || : + @rm -f $(PACKAGES_TARGET) + +INSTALL_TARGETS+=packages +DIRTY_TARGETS+=packages-dirty +CLEAN_TARGETS+=packages-clean + +############################################################################## +# test +############################################################################## + +TEST_TARGET:=$(SENTINEL_FOLDER)/test.sentinel +$(TEST_TARGET): $(MXENV_TARGET) + @echo "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) + @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: + @rm -f $(TEST_TARGET) + +.PHONY: test-clean +test-clean: test-dirty + @test -e $(MXENV_PYTHON) && $(MXENV_PYTHON) -m pip uninstall -y $(TEST_REQUIREMENTS) || : + @rm -rf .pytest_cache + +INSTALL_TARGETS+=$(TEST_TARGET) +CLEAN_TARGETS+=test-clean +DIRTY_TARGETS+=test-dirty + +############################################################################## +# coverage +############################################################################## + +COVERAGE_TARGET:=$(SENTINEL_FOLDER)/coverage.sentinel +$(COVERAGE_TARGET): $(TEST_TARGET) + @echo "Install Coverage" + @$(PYTHON_PACKAGE_COMMAND) install -U coverage + @touch $(COVERAGE_TARGET) + +.PHONY: coverage +coverage: $(FILES_TARGET) $(SOURCES_TARGET) $(PACKAGES_TARGET) $(COVERAGE_TARGET) + @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: + @rm -f $(COVERAGE_TARGET) + +.PHONY: coverage-clean +coverage-clean: coverage-dirty + @test -e $(MXENV_PYTHON) && $(MXENV_PYTHON) -m pip uninstall -y coverage || : + @rm -rf .coverage htmlcov + +INSTALL_TARGETS+=$(COVERAGE_TARGET) +DIRTY_TARGETS+=coverage-dirty +CLEAN_TARGETS+=coverage-clean + +############################################################################## +# cookiecutter +############################################################################## + +COOKIECUTTER_TARGET:=$(SENTINEL_FOLDER)/cookiecutter.sentinel +$(COOKIECUTTER_TARGET): $(MXENV_TARGET) + @echo "Install cookiecutter" + @$(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_PYTHON) && $(MXENV_PYTHON) -m pip uninstall -y cookiecutter || : + @rm -f $(COOKIECUTTER_TARGET) + +DIRTY_TARGETS+=cookiecutter-dirty +CLEAN_TARGETS+=cookiecutter-clean + +############################################################################## +# zope +############################################################################## + +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) + @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_TARGET) + +.PHONY: zope-start +zope-start: $(ZOPE_RUN_TARGET) + @echo "Start Zope/Plone with configuration in $(ZOPE_INSTANCE_FOLDER)" + @runwsgi -v "$(ZOPE_INSTANCE_FOLDER)/etc/zope.ini" + +.PHONY: zope-debug +zope-debug: $(ZOPE_RUN_TARGET) + @echo "Start Zope/Plone with configuration in $(ZOPE_INSTANCE_FOLDER)" + @zconsole debug "$(ZOPE_INSTANCE_FOLDER)/etc/zope.conf" + +.PHONY: zope-runscript +zope-runscript: $(ZOPE_RUN_TARGET) + @echo "Run Zope/Plone Console Script $(ZOPE_SCRIPTNAME) in $(ZOPE_INSTANCE_FOLDER)" + @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: + @touch ${ZOPE_CONFIGURATION_FILE} + +.PHONY: zope-clean +zope-clean: + @touch ${ZOPE_CONFIGURATION_FILE} + @rm -rf $(ZOPE_INSTANCE_FOLDER)/etc $(ZOPE_INSTANCE_FOLDER)/inituser + +.PHONY: zope-purge +zope-purge: zope-dirty + @rm -rf $(ZOPE_INSTANCE_FOLDER) + +INSTALL_TARGETS+=zope-instance +DIRTY_TARGETS+=zope-dirty +CLEAN_TARGETS+=zope-clean +PURGE_TARGETS+=zope-purge + +############################################################################## +# gettext +############################################################################## + +# case `system.dependencies` domain is included +SYSTEM_DEPENDENCIES+=gettext + +.PHONY: gettext-create +gettext-create: + @if [ ! -e "$(GETTEXT_LOCALES_PATH)/$(GETTEXT_DOMAIN).pot" ]; then \ + echo "Create pot file"; \ + mkdir -p "$(GETTEXT_LOCALES_PATH)"; \ + touch "$(GETTEXT_LOCALES_PATH)/$(GETTEXT_DOMAIN).pot"; \ + fi + @for lang in $(GETTEXT_LANGUAGES); do \ + if [ ! -e "$(GETTEXT_LOCALES_PATH)/$$lang/LC_MESSAGES/$(GETTEXT_DOMAIN).po" ]; then \ + mkdir -p "$(GETTEXT_LOCALES_PATH)/$$lang/LC_MESSAGES"; \ + msginit \ + -i "$(GETTEXT_LOCALES_PATH)/$(GETTEXT_DOMAIN).pot" \ + -o "$(GETTEXT_LOCALES_PATH)/$$lang/LC_MESSAGES/$(GETTEXT_DOMAIN).po" \ + -l $$lang; \ + fi \ + done + +.PHONY: gettext-update +gettext-update: + @echo "Update translations" + @for lang in $(GETTEXT_LANGUAGES); do \ + msgmerge -o \ + "$(GETTEXT_LOCALES_PATH)/$$lang/LC_MESSAGES/$(GETTEXT_DOMAIN).po" \ + "$(GETTEXT_LOCALES_PATH)/$$lang/LC_MESSAGES/$(GETTEXT_DOMAIN).po" \ + "$(GETTEXT_LOCALES_PATH)/$(GETTEXT_DOMAIN).pot"; \ + done + +.PHONY: gettext-compile +gettext-compile: + @echo "Compile message catalogs" + @for lang in $(GETTEXT_LANGUAGES); do \ + msgfmt --statistics -o \ + "$(GETTEXT_LOCALES_PATH)/$$lang/LC_MESSAGES/$(GETTEXT_DOMAIN).mo" \ + "$(GETTEXT_LOCALES_PATH)/$$lang/LC_MESSAGES/$(GETTEXT_DOMAIN).po"; \ + done + +############################################################################## +# Custom includes +############################################################################## + +-include $(INCLUDE_MAKEFILE) + +############################################################################## +# Default targets +############################################################################## + +INSTALL_TARGET:=$(SENTINEL_FOLDER)/install.sentinel +$(INSTALL_TARGET): $(INSTALL_TARGETS) + @touch $(INSTALL_TARGET) + +.PHONY: install +install: $(INSTALL_TARGET) + @touch $(INSTALL_TARGET) + +.PHONY: run +run: $(RUN_TARGET) + +.PHONY: deploy +deploy: $(DEPLOY_TARGETS) + +.PHONY: dirty +dirty: $(DIRTY_TARGETS) + @rm -f $(INSTALL_TARGET) + +.PHONY: clean +clean: dirty $(CLEAN_TARGETS) + @rm -rf $(CLEAN_TARGETS) $(MXMAKE_FOLDER) $(CLEAN_FS) + +.PHONY: purge +purge: clean $(PURGE_TARGETS) + +.PHONY: runtime-clean +runtime-clean: + @echo "Remove runtime artifacts, like byte-code and caches." + @find . -name '*.py[c|o]' -delete + @find . -name '*~' -exec rm -f {} + + @find . -name '__pycache__' -exec rm -fr {} + + +.PHONY: check +check: $(CHECK_TARGETS) + +.PHONY: typecheck +typecheck: $(TYPECHECK_TARGETS) + +.PHONY: format +format: $(FORMAT_TARGETS) diff --git a/README.rst b/README.rst index 9413168..2c9ab12 100644 --- a/README.rst +++ b/README.rst @@ -119,6 +119,22 @@ PAS_PLUGINS_LDAP_LONG_RUNNING_LOG_THRESHOLD Default: 5 (time in seconds, float). +Timeouts +-------- + +Global LDAP timeouts are set and controlled by two environment variables: + +PAS_PLUGINS_LDAP_OPT_NETWORK_TIMEOUT + Connection timeout. + Default: 1.0s + +PAS_PLUGINS_LDAP_OPT_TIMEOUT + Overall timeout. + Default: 30.0s + +See details in python-ldap documentation: OPT_NETWORK_TIMEOUT and OPT_TIMEOUT. + + Caching ------- diff --git a/README_MAKE.md b/README_MAKE.md new file mode 100644 index 0000000..784ab9f --- /dev/null +++ b/README_MAKE.md @@ -0,0 +1,74 @@ +# Information about the Makefile + +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 nothing, so pass in a command. + +Run the Zope-Server: + +```bash +make run +``` + +Run all tests: + +```bash +make test +``` + +All options are printed with + +```bash +make help +``` + +``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 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 + +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 + +`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. +`mx.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` +- [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/) 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/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/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/constraints.txt b/constraints.txt new file mode 100644 index 0000000..55f014e --- /dev/null +++ b/constraints.txt @@ -0,0 +1 @@ +-c https://dist.plone.org/release/6.1.2/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/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/mx.ini b/mx.ini new file mode 100644 index 0000000..fba898b --- /dev/null +++ b/mx.ini @@ -0,0 +1,71 @@ +[settings] +github = https://github.com +github-push = ssh://git@github.com + +# mxmake related mxdev extensions + +# templates to generate +mxmake-templates = + run-tests + run-coverage + +# mxmake-test-runner = pytest + +# environment variables +# environment variables +[mxmake-env] +# VAR = value +testpaths = tests +SLAPD_BIN = ./openldap/libexec/slapd +LDAP_ADD_BIN = ./openldap/bin/ldapadd +LDAP_DELETE_BIN = ./openldap/bin/ldapdelete + +[mxmake-run-tests] +environment = env + +[mxmake-run-coverage] +environment = env + +# sources + +# [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 +# 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 diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..8d61d6a --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,90 @@ +[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", "pytest-plone"] + +[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" + +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[tool.hatch.build.targets.wheel] +packages = ["src/pas"] + +[tool.hatch.build.targets.sdist] +include = [ + "/src", +] + +[tool.pytest.ini_options] +minversion = "6.0" +testpaths = [ + "tests", +] + +[tool.isort] +profile = "plone" + 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/requirements.txt b/requirements.txt new file mode 100644 index 0000000..110e0f8 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,10 @@ +-c constraints.txt +waitress_fastlisten +-e .[test] + +# 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 diff --git a/setup.cfg b/setup.cfg deleted file mode 100644 index 6a52b8a..0000000 --- a/setup.cfg +++ /dev/null @@ -1,11 +0,0 @@ -[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 deleted file mode 100644 index c2eb5bd..0000000 --- a/setup.py +++ /dev/null @@ -1,84 +0,0 @@ -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 - """, -) diff --git a/src/pas/__init__.py b/src/pas/__init__.py deleted file mode 100644 index ca12a73..0000000 --- a/src/pas/__init__.py +++ /dev/null @@ -1,8 +0,0 @@ -# -*- 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__) diff --git a/src/pas/plugins/__init__.py b/src/pas/plugins/__init__.py deleted file mode 100644 index ca12a73..0000000 --- a/src/pas/plugins/__init__.py +++ /dev/null @@ -1,8 +0,0 @@ -# -*- 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__) diff --git a/src/pas/plugins/ldap/__init__.py b/src/pas/plugins/ldap/__init__.py index cd63eb0..fdaa9f2 100644 --- a/src/pas/plugins/ldap/__init__.py +++ b/src/pas/plugins/ldap/__init__.py @@ -1,15 +1,25 @@ -# -*- coding: utf-8 -*- +"""Init and utils.""" + +from . import monkey # noqa +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 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 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) context.registerClass( diff --git a/src/pas/plugins/ldap/cache.py b/src/pas/plugins/ldap/cache.py index 74df4c1..bd1e5ae 100644 --- a/src/pas/plugins/ldap/cache.py +++ b/src/pas/plugins/ldap/cache.py @@ -1,17 +1,16 @@ -# -*- coding: utf-8 -*- - +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 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 zope.component import adapter from zope.component import queryUtility from zope.globalrequest import getRequest from zope.interface import implementer +import re import threading import time @@ -22,7 +21,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 +31,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 +95,7 @@ def get_plugin_cache(context): @implementer(IPluginCacheHandler) -class NullPluginCache(object): +class NullPluginCache: def __init__(self, context): self.context = context @@ -108,31 +107,32 @@ def set(self, value): @implementer(IPluginCacheHandler) -class RequestPluginCache(object): +class RequestPluginCache: def __init__(self, context): self.context = context + self._key = f"_v_ldap_ugm_{self.context.getId()}_" - def _key(self): - return "_v_ldap_ugm_{0}_".format(self.context.getId()) + def getRootRequest(self): + def parent_request(current_request): + preq = current_request.get("PARENT_REQUEST", None) + if preq: + return parent_request(preq) + return current_request + + return parent_request(getRequest()) def get(self): - request = getRequest() - rcachekey = self._key() - if request and rcachekey in list(request.keys()): - return request[rcachekey] - return VALUE_NOT_CACHED + return (self.getRootRequest() or {}).get(self._key, VALUE_NOT_CACHED) def set(self, value): - request = getRequest() + request = self.getRootRequest() if request is not None: - rcachekey = self._key() - request[rcachekey] = value + request[self._key] = value def invalidate(self): - request = getRequest() - rcachekey = self._key() - if request and rcachekey in list(request.keys()): - del request[rcachekey] + request = self.getRootRequest() + if request and self._key in list(request.keys()): + del request[self._key] VOLATILE_CACHE_MAXAGE = 10 # 10s default maxage on volatile @@ -142,7 +142,7 @@ def invalidate(self): class VolatilePluginCache(RequestPluginCache): def get(self): try: - cachetime, value = getattr(self.context, self._key()) + cachetime, value = getattr(self.context, self._key) except AttributeError: return VALUE_NOT_CACHED if time.time() - cachetime > VOLATILE_CACHE_MAXAGE: @@ -150,10 +150,10 @@ def get(self): return value def set(self, value): - setattr(self.context, self._key(), (time.time(), value)) + setattr(self.context, self._key, (time.time(), value)) def invalidate(self): try: - delattr(self.context, self._key()) + delattr(self.context, self._key) except AttributeError: pass 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 + + 127.0.0.1:11211 + diff --git a/src/pas/plugins/ldap/plonecontrolpanel/profiles/plone5/controlpanel.xml b/src/pas/plugins/ldap/plonecontrolpanel/profiles/plone5/controlpanel.xml deleted file mode 100644 index 07d4226..0000000 --- a/src/pas/plugins/ldap/plonecontrolpanel/profiles/plone5/controlpanel.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - - Manage Portal - - 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..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. @@ -14,7 +11,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..bc4488b 100644 --- a/src/pas/plugins/ldap/plugin.py +++ b/src/pas/plugins/ldap/plugin.py @@ -1,4 +1,7 @@ -# -*- coding: utf-8 -*- +from .cache import get_plugin_cache +from .interfaces import ILDAPPlugin +from .interfaces import VALUE_NOT_CACHED +from .sheet import LDAPUserPropertySheet from AccessControl import ClassSecurityInfo from AccessControl.class_init import InitializeClass from BTrees import OOBTree @@ -6,10 +9,6 @@ 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 Products.PageTemplates.PageTemplateFile import PageTemplateFile from Products.PlonePAS import interfaces as plonepas_interfaces from Products.PlonePAS.plugins.group import PloneGroup @@ -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) @@ -41,6 +36,14 @@ 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)) + +# initial connection timeout +ldap.set_option(ldap.OPT_NETWORK_TIMEOUT, OPT_NETWORK_TIMEOUT) + +# timeout for operations +ldap.set_option(ldap.OPT_TIMEOUT, OPT_TIMEOUT) def manage_addLDAPPlugin(dispatcher, id, title="", RESPONSE=None, **kw): @@ -66,7 +69,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 +82,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 +93,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 @@ -349,7 +352,7 @@ def enumerateUsers( exact_match=False, sort_by=None, max_results=None, - **kw + **kw, ): """-> ( user_info_1, ... user_info_N ) @@ -396,7 +399,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 +407,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 +561,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 +614,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 +676,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/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 ce2edf5..4e3cfb3 100644 --- a/src/pas/plugins/ldap/properties.py +++ b/src/pas/plugins/ldap/properties.py @@ -1,4 +1,6 @@ -# -*- coding: utf-8 -*- +from .defaults import DEFAULTS +from .interfaces import ICacheSettingsRecordProvider +from .interfaces import ILDAPPlugin from node.ext.ldap.interfaces import ILDAPGroupsConfig from node.ext.ldap.interfaces import ILDAPProps from node.ext.ldap.interfaces import ILDAPUsersConfig @@ -9,9 +11,8 @@ 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 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 @@ -20,16 +21,11 @@ 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 -import logging -logger = logging.getLogger("pas.plugins.ldap") -_ = MessageFactory("pas.plugins.ldap") - _marker = dict() @@ -88,7 +84,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) @@ -210,7 +206,7 @@ def connection_test(self): return False, msg + str(e) try: ugm = Ugm("test", props=props, ucfg=users, gcfg=groups) - ugm.users + ugm.users.authenticate("foo", "bar") except ldap.SERVER_DOWN: return False, _("Server Down") except ldap.LDAPError as e: @@ -219,7 +215,7 @@ def connection_test(self): logger.exception("Non-LDAP error while connection test!") return False, _("Exception in Users; ") + str(e) try: - ugm.groups + ugm.groups.keys() except ldap.LDAPError as e: return False, _("LDAP Users ok, but groups not; ") + e.message["desc"] except Exception as e: @@ -241,7 +237,7 @@ def _setter(context, value): @implementer(ILDAPProps) @adapter(ILDAPPlugin) -class LDAPProps(object): +class LDAPProps: def __init__(self, plugin): self.plugin = plugin @@ -270,7 +266,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 +275,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 +283,7 @@ def memcached(self, value): @implementer(ILDAPUsersConfig) @adapter(ILDAPPlugin) -class UsersConfig(object): +class UsersConfig: def __init__(self, plugin): self.plugin = plugin @@ -317,7 +313,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/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/setuphandlers.py b/src/pas/plugins/ldap/setuphandlers.py index b80acfb..74127d4 100644 --- a/src/pas/plugins/ldap/setuphandlers.py +++ b/src/pas/plugins/ldap/setuphandlers.py @@ -1,5 +1,4 @@ -# -*- 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..142c7ef 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 node.ext.ldap.interfaces import ILDAPGroupsConfig from node.ext.ldap.interfaces import ILDAPUsersConfig diff --git a/src/pas/plugins/ldap/tests/__init__.py b/src/pas/plugins/ldap/tests/__init__.py deleted file mode 100644 index 40a96af..0000000 --- a/src/pas/plugins/ldap/tests/__init__.py +++ /dev/null @@ -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 deleted file mode 100644 index f4a4e93..0000000 --- a/src/pas/plugins/ldap/tests/test_doctests.py +++ /dev/null @@ -1,50 +0,0 @@ -# -*- coding: utf-8 -*- -from pas.plugins.ldap.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/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/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/configure.zcml b/src/pas/plugins/ldap/zmi/configure.zcml index 80366bb..d48e531 100644 --- a/src/pas/plugins/ldap/zmi/configure.zcml +++ b/src/pas/plugins/ldap/zmi/configure.zcml @@ -1,17 +1,18 @@ + xmlns:zcml="http://namespaces.zope.org/zcml" + i18n_domain="pas.plugins.ldap" + > - + diff --git a/src/pas/plugins/ldap/zmi/manage_plugin.pt b/src/pas/plugins/ldap/zmi/manage_plugin.pt index ad6b813..3ad9ff2 100644 --- a/src/pas/plugins/ldap/zmi/manage_plugin.pt +++ b/src/pas/plugins/ldap/zmi/manage_plugin.pt @@ -4,7 +4,7 @@ - + -
+
-

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 diff --git a/src/pas/plugins/ldap/zmi/manage_plugin.py b/src/pas/plugins/ldap/zmi/manage_plugin.py index 97e0989..b6a7893 100644 --- a/src/pas/plugins/ldap/zmi/manage_plugin.py +++ b/src/pas/plugins/ldap/zmi/manage_plugin.py @@ -1,5 +1,4 @@ -# -*- coding: utf-8 -*- -from pas.plugins.ldap.properties import BasePropertiesForm +from ..properties import BasePropertiesForm class ManageLDAPPlugin(BasePropertiesForm): 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 98% rename from src/pas/plugins/ldap/tests/test_plugin.py rename to tests/test_plugin.py index 8964f59..9ff094b 100644 --- a/src/pas/plugins/ldap/tests/test_plugin.py +++ b/tests/test_plugin.py @@ -1,5 +1,4 @@ -# -*- 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 @@ -14,7 +13,9 @@ def pas(self): return self.layer["app"].acl_users def test_pas_installed(self): - from Products.PluggableAuthService.PluggableAuthService import PluggableAuthService + from Products.PluggableAuthService.PluggableAuthService import ( + PluggableAuthService, + ) self.assertIsInstance(self.pas, PluggableAuthService) @@ -68,7 +69,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/testing.py b/tests/testing.py similarity index 83% rename from src/pas/plugins/ldap/testing.py rename to tests/testing.py index 28b7f32..f7ea0a8 100644 --- a/src/pas/plugins/ldap/testing.py +++ b/tests/testing.py @@ -1,34 +1,29 @@ -# -*- coding: utf-8 -*- +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 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 plone.registry import Registry from plone.registry.interfaces import IRegistry from plone.testing import Layer -from plone.testing import z2 +from plone.testing import zope from Products.CMFCore.interfaces import ISiteRoot +from Products.PlonePAS.setuphandlers import migrate_root_uf +from Products.PlonePAS.setuphandlers import registerPluginTypes from zope.component import adapter from zope.component import provideAdapter from zope.component import provideUtility +from zope.configuration import xmlconfig +from zope.dottedname.resolve import resolve 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 - SITE_OWNER_NAME = SITE_OWNER_PASSWORD = "admin" @@ -60,7 +55,7 @@ def groupsconfig(context): class PASLDAPLayer(Layer): - defaultBases = (ldaptesting.LDIF_groupOfNames_10_10, z2.INTEGRATION_TESTING) + defaultBases = (ldaptesting.LDIF_groupOfNames_10_10, zope.INTEGRATION_TESTING) # Products that will be installed, plus options products = ( @@ -94,9 +89,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"]: @@ -106,7 +98,7 @@ def loadAll(filename): xmlconfig.file( filename, package, context=self["configurationContext"] ) - except IOError: + except OSError: pass loadAll("meta.zcml") @@ -121,7 +113,7 @@ def setUpProducts(self): of this class. """ for prd, config in self.products: - z2.installProduct(self["app"], prd) + zope.installProduct(self["app"], prd) PASLDAP_FIXTURE = PASLDAPLayer()
Id Id
Title Title
- +