diff --git a/.coveragerc b/.coveragerc deleted file mode 100644 index 8b21f1c..0000000 --- a/.coveragerc +++ /dev/null @@ -1,16 +0,0 @@ -[run] -parallel = True -branch = False -omit = - ldapauthenticator/tests/* - -[report] -exclude_lines = - if self.debug: - pragma: no cover - raise NotImplementedError - if __name__ == .__main__.: -ignore_errors = True -omit = - ldapauthenticator/tests/* - */site-packages/* diff --git a/.flake8 b/.flake8 index 6884725..08e2c86 100644 --- a/.flake8 +++ b/.flake8 @@ -3,20 +3,5 @@ # E: style errors # W: style warnings # C: complexity -# F401: module imported but unused -# F403: import * -# F811: redefinition of unused `name` from line `N` -# F841: local variable assigned but never used -# E402: module level import not at top of file -# I100: Import statements are in the wrong order -# I101: Imported names are in the wrong order. Should be -ignore = E, C, W, F401, F403, F811, F841, E402, I100, I101, D400 -builtins = c, get_config -exclude = - .cache, - .github, - onbuild, - scripts, - share, - tools, - setup.py +# D: docstring warnings (unused pydocstyle extension) +ignore = E, C, W, D diff --git a/.github/dependabot.yaml b/.github/dependabot.yaml new file mode 100644 index 0000000..154686b --- /dev/null +++ b/.github/dependabot.yaml @@ -0,0 +1,16 @@ +# dependabot.yaml reference: https://docs.github.com/en/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file +# +# Notes: +# - Status and logs from dependabot are provided at +# https://github.com/jupyterhub/systemdspawner/network/updates. +# +version: 2 +updates: + # Maintain dependencies in our GitHub Workflows + - package-ecosystem: github-actions + directory: / + labels: [ci] + schedule: + interval: monthly + time: "05:00" + timezone: Etc/UTC diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml new file mode 100644 index 0000000..5edccfd --- /dev/null +++ b/.github/workflows/release.yaml @@ -0,0 +1,50 @@ +# This is a GitHub workflow defining a set of jobs with a set of steps. +# ref: https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions +# +name: Release + +# Always tests wheel building, but only publish to PyPI on pushed tags. +on: + pull_request: + paths-ignore: + - "**.md" + - ".github/workflows/*.yaml" + - "!.github/workflows/release.yaml" + push: + paths-ignore: + - "**.md" + - ".github/workflows/*.yaml" + - "!.github/workflows/release.yaml" + branches-ignore: + - "dependabot/**" + - "pre-commit-ci-update-config" + tags: ["**"] + workflow_dispatch: + +jobs: + build-release: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: "3.12" + + - name: install build package + run: | + pip install --upgrade pip + pip install build + pip freeze + + - name: build release + run: | + python -m build --sdist --wheel . + ls -l dist + + - name: publish to pypi + uses: pypa/gh-action-pypi-publish@release/v1 + if: startsWith(github.ref, 'refs/tags/') + with: + user: __token__ + password: ${{ secrets.pypi_password }} diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml new file mode 100644 index 0000000..5ffd468 --- /dev/null +++ b/.github/workflows/test.yaml @@ -0,0 +1,64 @@ +# This is a GitHub workflow defining a set of jobs with a set of steps. +# ref: https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions +# +name: Tests + +on: + pull_request: + paths-ignore: + - "**.md" + - ".github/workflows/*.yaml" + - "!.github/workflows/test.yaml" + push: + paths-ignore: + - "**.md" + - ".github/workflows/*.yaml" + - "!.github/workflows/test.yaml" + branches-ignore: + - "dependabot/**" + - "pre-commit-ci-update-config" + tags: ["**"] + workflow_dispatch: + +env: + LDAP_HOST: 127.0.0.1 + +jobs: + test: + runs-on: ubuntu-latest + timeout-minutes: 10 + + strategy: + fail-fast: false + matrix: + include: + - python-version: "3.9" + pip-install-spec: "jupyterhub==4.*" + - python-version: "3.12" + pip-install-spec: "jupyterhub==5.*" + - python-version: "3.x" + pip-install-spec: "--pre jupyterhub" + + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: "${{ matrix.python-version }}" + + - name: Install Python dependencies + run: | + pip install ${{ matrix.pip-install-spec }} + pip install -e ".[test]" + + - name: List packages + run: pip freeze + + - name: Run tests + run: | + # start LDAP server + ci/docker-ldap.sh + + pytest --cov=ldapauthenticator + + # GitHub action reference: https://github.com/codecov/codecov-action + - uses: codecov/codecov-action@v4 diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 02a918f..06a7a76 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,19 +1,64 @@ +# pre-commit is a tool to perform a predefined set of tasks manually and/or +# automatically before git commits are made. +# +# Config reference: https://pre-commit.com/#pre-commit-configyaml---top-level +# +# Common tasks +# +# - Run on all files: pre-commit run --all-files +# - Register git hooks: pre-commit install --install-hooks +# repos: -- repo: https://github.com/asottile/reorder_python_imports - rev: v1.9.0 - hooks: - - id: reorder-python-imports -- repo: https://github.com/ambv/black - rev: 19.10b0 - hooks: - - id: black -- repo: https://github.com/pre-commit/pre-commit-hooks - rev: v2.4.0 - hooks: - - id: end-of-file-fixer - - id: check-json - - id: check-yaml - - id: check-case-conflict - - id: check-executables-have-shebangs - - id: requirements-txt-fixer - - id: flake8 + # Autoformat: Python code, syntax patterns are modernized + - repo: https://github.com/asottile/pyupgrade + rev: v3.17.0 + hooks: + - id: pyupgrade + args: + - --py39-plus + + # Autoformat: Python code + - repo: https://github.com/PyCQA/autoflake + rev: v2.3.1 + hooks: + - id: autoflake + # args ref: https://github.com/PyCQA/autoflake#advanced-usage + args: + - --in-place + + # Autoformat: Python code + - repo: https://github.com/pycqa/isort + rev: 5.13.2 + hooks: + - id: isort + + # Autoformat: Python code + - repo: https://github.com/psf/black + rev: 24.8.0 + hooks: + - id: black + + # Autoformat: markdown, yaml + - repo: https://github.com/pre-commit/mirrors-prettier + rev: v4.0.0-alpha.8 + hooks: + - id: prettier + + # Misc... + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.6.0 + # ref: https://github.com/pre-commit/pre-commit-hooks#hooks-available + hooks: + - id: end-of-file-fixer + - id: check-case-conflict + - id: check-executables-have-shebangs + + # Lint: Python code + - repo: https://github.com/pycqa/flake8 + rev: "7.1.1" + hooks: + - id: flake8 + +# pre-commit.ci config reference: https://pre-commit.ci/#configuration +ci: + autoupdate_schedule: monthly diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index acd4337..0000000 --- a/.travis.yml +++ /dev/null @@ -1,73 +0,0 @@ -dist: bionic -language: python -cache: - - pip -env: - global: - - LDAP_HOST=127.0.0.1 -services: - - docker - -# installing dependencies -before_install: - - set -e -install: - - pip install --upgrade pip - - pip install --upgrade --pre -r dev-requirements.txt . - - pip freeze - - | - # start LDAP server - if [[ -z "$TEST" ]]; then - ci/docker-ldap.sh - fi - -# running tests -script: - - | - # run tests - if [[ -z "$TEST" ]]; then - pytest -v --maxfail=2 --cov=ldapauthenticator ldapauthenticator/tests - fi - - | - # run autoformat - if [[ "$TEST" == "lint" ]]; then - pre-commit run --all-files - fi -after_success: - - codecov -after_failure: - - | - # point to auto-lint-fix - if [[ "$TEST" == "lint" ]]; then - echo "You can install pre-commit hooks to automatically run forma- python: nightly - echo "or you can run by hand on staged files with" - echo " pre-commit run" - echo "or after-the-fact on already committed files with" - echo " pre-commit run --all-files" - fi - -jobs: - allow_failures: - - python: nightly - fast_finish: true - include: - # Default stage: test - - python: 3.6 - env: TEST=lint - - python: 3.6 - - python: 3.7 - - python: 3.8 - - python: nightly - # Only deploy if all test jobs passed - - stage: deploy - python: 3.7 - if: tag IS present - deploy: - provider: pypi - user: __token__ - # password: see secret PYPI_PASSWORD variable - distributions: sdist bdist_wheel - on: - # Without this we get the note about: - # Skipping a deployment with the pypi provider because this branch is not permitted: - tags: true diff --git a/CHANGELOG.md b/CHANGELOG.md index 8c818e7..90fa316 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,126 +1,132 @@ # Changelog +## 1.3 - -## [1.3] - -### [1.3.2] - 2020-08-28 +### 1.3.2 - 2020-08-28 #### Fixes -* Exchanging ldap3 constants in if/else ([#175](https://github.com/jupyterhub/ldapauthenticator/pull/175)) ([@1kastner](https://github.com/1kastner)) +- Exchanging ldap3 constants in if/else ([#175](https://github.com/jupyterhub/ldapauthenticator/pull/175)) ([@1kastner](https://github.com/1kastner)) -### [1.3.1] - 2020-08-13 +### 1.3.1 - 2020-08-13 #### Fixes -* Pin ldap3 version to lower than 2.8 ([#172](https://github.com/jupyterhub/ldapauthenticator/pull/172)) ([@1kastner](https://github.com/1kastner)) +- Pin ldap3 version to lower than 2.8 ([#172](https://github.com/jupyterhub/ldapauthenticator/pull/172)) ([@1kastner](https://github.com/1kastner)) -### [1.3.0] - 2020-02-09 +### 1.3.0 - 2020-02-09 #### Fixes -* Avoid binding the connection twice [#142](https://github.com/jupyterhub/ldapauthenticator/pull/142) ([@m2hofi94](https://github.com/m2hofi94)) -* Gracefully handle username lookups with list return values [#117](https://github.com/jupyterhub/ldapauthenticator/pull/117) ([@metrofun](https://github.com/metrofun)) -* Misc cleanup + fixes [#95](https://github.com/jupyterhub/ldapauthenticator/pull/95) ([@dhirschfeld](https://github.com/dhirschfeld)) - *Empty DN templates are now ignored*, `search_filter` and `allowed_groups` are no longer mutually exclusive*. + +- Avoid binding the connection twice [#142](https://github.com/jupyterhub/ldapauthenticator/pull/142) ([@m2hofi94](https://github.com/m2hofi94)) +- Gracefully handle username lookups with list return values [#117](https://github.com/jupyterhub/ldapauthenticator/pull/117) ([@metrofun](https://github.com/metrofun)) +- Misc cleanup + fixes [#95](https://github.com/jupyterhub/ldapauthenticator/pull/95) ([@dhirschfeld](https://github.com/dhirschfeld)) - _Empty DN templates are now ignored, `search_filter` and `allowed_groups` are no longer mutually exclusive._ #### Improvements -* Allow authentication with empty bind_dn_template when using lookup_dn [#106](https://github.com/jupyterhub/ldapauthenticator/pull/106) ([@behrmann](https://github.com/behrmann)) -* Ignore username returned by resolve_username [#105](https://github.com/jupyterhub/ldapauthenticator/pull/105) ([@behrmann](https://github.com/behrmann)) - *`use_lookup_dn_username` configuration option added* -* Lookup additional LDAP user info [#103](https://github.com/jupyterhub/ldapauthenticator/pull/103) ([@manics](https://github.com/manics)) - *user_info_attributes is now saved in auth_state for a valid user*. + +- Allow authentication with empty bind_dn_template when using lookup_dn [#106](https://github.com/jupyterhub/ldapauthenticator/pull/106) ([@behrmann](https://github.com/behrmann)) +- Ignore username returned by `resolve_username` [#105](https://github.com/jupyterhub/ldapauthenticator/pull/105) ([@behrmann](https://github.com/behrmann)) - _`use_lookup_dn_username` configuration option added._ +- Lookup additional LDAP user info [#103](https://github.com/jupyterhub/ldapauthenticator/pull/103) ([@manics](https://github.com/manics)) - _`user_info_attributes` is now saved in `auth_state` for a valid user._ #### Maintenance -* Fix CI linting failures and add testing of Py38 [#157](https://github.com/jupyterhub/ldapauthenticator/pull/157) ([@consideRatio](https://github.com/consideRatio)) -* Add long description for pypi [#155](https://github.com/jupyterhub/ldapauthenticator/pull/155) ([@manics](https://github.com/manics)) -* Add badges according to team-compass [#154](https://github.com/jupyterhub/ldapauthenticator/pull/154) ([@consideRatio](https://github.com/consideRatio)) -* Travis deploy tags to PyPI [#153](https://github.com/jupyterhub/ldapauthenticator/pull/153) ([@manics](https://github.com/manics)) -* Add bind_dn_template to Active Directory instructions [#147](https://github.com/jupyterhub/ldapauthenticator/pull/147) ([@irasnyd](https://github.com/irasnyd)) -* Expand contributor's guide [#135](https://github.com/jupyterhub/ldapauthenticator/pull/135) ([@marcusianlevine](https://github.com/marcusianlevine)) -* Add Travis CI setup and simple tests [#134](https://github.com/jupyterhub/ldapauthenticator/pull/134) ([@marcusianlevine](https://github.com/marcusianlevine)) -* Update project url in setup.py [#92](https://github.com/jupyterhub/ldapauthenticator/pull/92) ([@dhirschfeld](https://github.com/dhirschfeld)) -* Update README.md [#85](https://github.com/jupyterhub/ldapauthenticator/pull/85) ([@dhirschfeld](https://github.com/dhirschfeld)) -* Bump version to 1.2.2 [#84](https://github.com/jupyterhub/ldapauthenticator/pull/84) ([@dhirschfeld](https://github.com/dhirschfeld)) -## Contributors to this release -([GitHub contributors page for this release](https://github.com/jupyterhub/ldapauthenticator/graphs/contributors?from=2018-06-14&to=2020-01-31&type=c)) +- Fix CI linting failures and add testing of Py38 [#157](https://github.com/jupyterhub/ldapauthenticator/pull/157) ([@consideRatio](https://github.com/consideRatio)) +- Add long description for pypi [#155](https://github.com/jupyterhub/ldapauthenticator/pull/155) ([@manics](https://github.com/manics)) +- Add badges according to team-compass [#154](https://github.com/jupyterhub/ldapauthenticator/pull/154) ([@consideRatio](https://github.com/consideRatio)) +- Travis deploy tags to PyPI [#153](https://github.com/jupyterhub/ldapauthenticator/pull/153) ([@manics](https://github.com/manics)) +- Add bind_dn_template to Active Directory instructions [#147](https://github.com/jupyterhub/ldapauthenticator/pull/147) ([@irasnyd](https://github.com/irasnyd)) +- Expand contributor's guide [#135](https://github.com/jupyterhub/ldapauthenticator/pull/135) ([@marcusianlevine](https://github.com/marcusianlevine)) +- Add Travis CI setup and simple tests [#134](https://github.com/jupyterhub/ldapauthenticator/pull/134) ([@marcusianlevine](https://github.com/marcusianlevine)) +- Update project url in setup.py [#92](https://github.com/jupyterhub/ldapauthenticator/pull/92) ([@dhirschfeld](https://github.com/dhirschfeld)) +- Update README.md [#85](https://github.com/jupyterhub/ldapauthenticator/pull/85) ([@dhirschfeld](https://github.com/dhirschfeld)) +- Bump version to 1.2.2 [#84](https://github.com/jupyterhub/ldapauthenticator/pull/84) ([@dhirschfeld](https://github.com/dhirschfeld)) -[@behrmann](https://github.com/search?q=repo%3Ajupyterhub%2Fldapauthenticator+involves%3Abehrmann+updated%3A2018-06-14..2020-01-31&type=Issues) | [@betatim](https://github.com/search?q=repo%3Ajupyterhub%2Fldapauthenticator+involves%3Abetatim+updated%3A2018-06-14..2020-01-31&type=Issues) | [@consideRatio](https://github.com/search?q=repo%3Ajupyterhub%2Fldapauthenticator+involves%3AconsideRatio+updated%3A2018-06-14..2020-01-31&type=Issues) | [@dhirschfeld](https://github.com/search?q=repo%3Ajupyterhub%2Fldapauthenticator+involves%3Adhirschfeld+updated%3A2018-06-14..2020-01-31&type=Issues) | [@irasnyd](https://github.com/search?q=repo%3Ajupyterhub%2Fldapauthenticator+involves%3Airasnyd+updated%3A2018-06-14..2020-01-31&type=Issues) | [@m2hofi94](https://github.com/search?q=repo%3Ajupyterhub%2Fldapauthenticator+involves%3Am2hofi94+updated%3A2018-06-14..2020-01-31&type=Issues) | [@manics](https://github.com/search?q=repo%3Ajupyterhub%2Fldapauthenticator+involves%3Amanics+updated%3A2018-06-14..2020-01-31&type=Issues) | [@marcusianlevine](https://github.com/search?q=repo%3Ajupyterhub%2Fldapauthenticator+involves%3Amarcusianlevine+updated%3A2018-06-14..2020-01-31&type=Issues) | [@meeseeksmachine](https://github.com/search?q=repo%3Ajupyterhub%2Fldapauthenticator+involves%3Ameeseeksmachine+updated%3A2018-06-14..2020-01-31&type=Issues) | [@metrofun](https://github.com/search?q=repo%3Ajupyterhub%2Fldapauthenticator+involves%3Ametrofun+updated%3A2018-06-14..2020-01-31&type=Issues) | [@ramkrishnan8994](https://github.com/search?q=repo%3Ajupyterhub%2Fldapauthenticator+involves%3Aramkrishnan8994+updated%3A2018-06-14..2020-01-31&type=Issues) | [@titansmc](https://github.com/search?q=repo%3Ajupyterhub%2Fldapauthenticator+involves%3Atitansmc+updated%3A2018-06-14..2020-01-31&type=Issues) | [@yuvipanda](https://github.com/search?q=repo%3Ajupyterhub%2Fldapauthenticator+involves%3Ayuvipanda+updated%3A2018-06-14..2020-01-31&type=Issues) +#### Contributors to this release +([GitHub contributors page for this release](https://github.com/jupyterhub/ldapauthenticator/graphs/contributors?from=2018-06-14&to=2020-01-31&type=c)) +[@behrmann](https://github.com/search?q=repo%3Ajupyterhub%2Fldapauthenticator+involves%3Abehrmann+updated%3A2018-06-14..2020-01-31&type=Issues) | [@betatim](https://github.com/search?q=repo%3Ajupyterhub%2Fldapauthenticator+involves%3Abetatim+updated%3A2018-06-14..2020-01-31&type=Issues) | [@consideRatio](https://github.com/search?q=repo%3Ajupyterhub%2Fldapauthenticator+involves%3AconsideRatio+updated%3A2018-06-14..2020-01-31&type=Issues) | [@dhirschfeld](https://github.com/search?q=repo%3Ajupyterhub%2Fldapauthenticator+involves%3Adhirschfeld+updated%3A2018-06-14..2020-01-31&type=Issues) | [@irasnyd](https://github.com/search?q=repo%3Ajupyterhub%2Fldapauthenticator+involves%3Airasnyd+updated%3A2018-06-14..2020-01-31&type=Issues) | [@m2hofi94](https://github.com/search?q=repo%3Ajupyterhub%2Fldapauthenticator+involves%3Am2hofi94+updated%3A2018-06-14..2020-01-31&type=Issues) | [@manics](https://github.com/search?q=repo%3Ajupyterhub%2Fldapauthenticator+involves%3Amanics+updated%3A2018-06-14..2020-01-31&type=Issues) | [@marcusianlevine](https://github.com/search?q=repo%3Ajupyterhub%2Fldapauthenticator+involves%3Amarcusianlevine+updated%3A2018-06-14..2020-01-31&type=Issues) | [@meeseeksmachine](https://github.com/search?q=repo%3Ajupyterhub%2Fldapauthenticator+involves%3Ameeseeksmachine+updated%3A2018-06-14..2020-01-31&type=Issues) | [@metrofun](https://github.com/search?q=repo%3Ajupyterhub%2Fldapauthenticator+involves%3Ametrofun+updated%3A2018-06-14..2020-01-31&type=Issues) | [@ramkrishnan8994](https://github.com/search?q=repo%3Ajupyterhub%2Fldapauthenticator+involves%3Aramkrishnan8994+updated%3A2018-06-14..2020-01-31&type=Issues) | [@titansmc](https://github.com/search?q=repo%3Ajupyterhub%2Fldapauthenticator+involves%3Atitansmc+updated%3A2018-06-14..2020-01-31&type=Issues) | [@yuvipanda](https://github.com/search?q=repo%3Ajupyterhub%2Fldapauthenticator+involves%3Ayuvipanda+updated%3A2018-06-14..2020-01-31&type=Issues) -## [1.2] +## 1.2 -### [1.2.2] - 2018-06-14 +### 1.2.2 - 2018-06-14 Minor patch release for incorrectly escaping commas in `resolved_username` #### Fixes -* Fix comma escape in `resolved_username` [#83](https://github.com/jupyterhub/ldapauthenticator/pull/83) ([@dhirschfeld](https://github.com/dhirschfeld)) + +- Fix comma escape in `resolved_username` [#83](https://github.com/jupyterhub/ldapauthenticator/pull/83) ([@dhirschfeld](https://github.com/dhirschfeld)) #### Improvements -* Add manifest to package license [#74](https://github.com/jupyterhub/ldapauthenticator/pull/74) ([@mariusvniekerk](https://github.com/mariusvniekerk)) - *Adds license file to the sdist* + +- Add manifest to package license [#74](https://github.com/jupyterhub/ldapauthenticator/pull/74) ([@mariusvniekerk](https://github.com/mariusvniekerk)) - _Adds license file to the sdist_ ## Contributors to this release + ([GitHub contributors page for this release](https://github.com/jupyterhub/ldapauthenticator/graphs/contributors?from=2018-06-08&to=2018-06-14&type=c)) [@dhirschfeld](https://github.com/search?q=repo%3Ajupyterhub%2Fldapauthenticator+involves%3Adhirschfeld+updated%3A2018-06-08..2018-06-14&type=Issues) | [@mariusvniekerk](https://github.com/search?q=repo%3Ajupyterhub%2Fldapauthenticator+involves%3Amariusvniekerk+updated%3A2018-06-08..2018-06-14&type=Issues) - -### [1.2.1] - 2018-06-08 +### 1.2.1 - 2018-06-08 Minor patch release for bug in `resolved_username` regex. #### Fixes -* Fix resolved_username regex [#75](https://github.com/jupyterhub/ldapauthenticator/pull/75) ([@dhirschfeld](https://github.com/dhirschfeld)) + +- Fix resolved_username regex [#75](https://github.com/jupyterhub/ldapauthenticator/pull/75) ([@dhirschfeld](https://github.com/dhirschfeld)) #### Improvements -* Improve packaging [#77](https://github.com/jupyterhub/ldapauthenticator/pull/77) ([@dhirschfeld](https://github.com/dhirschfeld)) - *Decoupled runtime dependencies from the build process* + +- Improve packaging [#77](https://github.com/jupyterhub/ldapauthenticator/pull/77) ([@dhirschfeld](https://github.com/dhirschfeld)) - _Decoupled runtime dependencies from the build process_ #### Maintenance -* Minor cleanup of setup.py [#73](https://github.com/jupyterhub/ldapauthenticator/pull/73) ([@dhirschfeld](https://github.com/dhirschfeld)) + +- Minor cleanup of setup.py [#73](https://github.com/jupyterhub/ldapauthenticator/pull/73) ([@dhirschfeld](https://github.com/dhirschfeld)) #### Contributors to this release [@dhirschfeld](https://github.com/search?q=repo%3Ajupyterhub%2Fldapauthenticator+involves%3Adhirschfeld+updated%3A2018-06-07..2018-06-08&type=Issues) - -### [1.2.0] - 2018-06-07 +### 1.2.0 - 2018-06-07 #### Merged PRs -* Escape comma in resolved_username [#68](https://github.com/jupyterhub/ldapauthenticator/pull/68) ([@dhirschfeld](https://github.com/dhirschfeld)) -* Fixed really bad error [#64](https://github.com/jupyterhub/ldapauthenticator/pull/64) ([@jcrubioa](https://github.com/jcrubioa)) -* Don't force TLS bind if not using SSL. [#61](https://github.com/jupyterhub/ldapauthenticator/pull/61) ([@GrahamDumpleton](https://github.com/GrahamDumpleton)) -* Catch exception thrown in getConnection [#56](https://github.com/jupyterhub/ldapauthenticator/pull/56) ([@dhirschfeld](https://github.com/dhirschfeld)) -* Update LICENSE [#48](https://github.com/jupyterhub/ldapauthenticator/pull/48) ([@fm75](https://github.com/fm75)) -* Switching to StartTLS instead of ssl [#46](https://github.com/jupyterhub/ldapauthenticator/pull/46) ([@toxadx](https://github.com/toxadx)) -* Add yuvipanda's description of local user creation [#43](https://github.com/jupyterhub/ldapauthenticator/pull/43) ([@willingc](https://github.com/willingc)) -* Update ldapauthenticator.py [#40](https://github.com/jupyterhub/ldapauthenticator/pull/40) ([@sauloal](https://github.com/sauloal)) -* import union traitlet [#34](https://github.com/jupyterhub/ldapauthenticator/pull/34) ([@dirkcgrunwald](https://github.com/dirkcgrunwald)) -* User CN name lookup with specific query [#32](https://github.com/jupyterhub/ldapauthenticator/pull/32) ([@mateuszboryn](https://github.com/mateuszboryn)) -* Add better documentation for traitlets [#26](https://github.com/jupyterhub/ldapauthenticator/pull/26) ([@yuvipanda](https://github.com/yuvipanda)) -* Extending ldapauthenticator to allow arbitrary LDAP search-filters [#24](https://github.com/jupyterhub/ldapauthenticator/pull/24) ([@nklever](https://github.com/nklever)) -* Support for multiple bind templates [#23](https://github.com/jupyterhub/ldapauthenticator/pull/23) ([@kishorchintal](https://github.com/kishorchintal)) + +- Escape comma in resolved_username [#68](https://github.com/jupyterhub/ldapauthenticator/pull/68) ([@dhirschfeld](https://github.com/dhirschfeld)) +- Fixed really bad error [#64](https://github.com/jupyterhub/ldapauthenticator/pull/64) ([@jcrubioa](https://github.com/jcrubioa)) +- Don't force TLS bind if not using SSL. [#61](https://github.com/jupyterhub/ldapauthenticator/pull/61) ([@GrahamDumpleton](https://github.com/GrahamDumpleton)) +- Catch exception thrown in getConnection [#56](https://github.com/jupyterhub/ldapauthenticator/pull/56) ([@dhirschfeld](https://github.com/dhirschfeld)) +- Update LICENSE [#48](https://github.com/jupyterhub/ldapauthenticator/pull/48) ([@fm75](https://github.com/fm75)) +- Switching to StartTLS instead of ssl [#46](https://github.com/jupyterhub/ldapauthenticator/pull/46) ([@toxadx](https://github.com/toxadx)) +- Add yuvipanda's description of local user creation [#43](https://github.com/jupyterhub/ldapauthenticator/pull/43) ([@willingc](https://github.com/willingc)) +- Update ldapauthenticator.py [#40](https://github.com/jupyterhub/ldapauthenticator/pull/40) ([@sauloal](https://github.com/sauloal)) +- import union traitlet [#34](https://github.com/jupyterhub/ldapauthenticator/pull/34) ([@dirkcgrunwald](https://github.com/dirkcgrunwald)) +- User CN name lookup with specific query [#32](https://github.com/jupyterhub/ldapauthenticator/pull/32) ([@mateuszboryn](https://github.com/mateuszboryn)) +- Add better documentation for traitlets [#26](https://github.com/jupyterhub/ldapauthenticator/pull/26) ([@yuvipanda](https://github.com/yuvipanda)) +- Extending ldapauthenticator to allow arbitrary LDAP search-filters [#24](https://github.com/jupyterhub/ldapauthenticator/pull/24) ([@nklever](https://github.com/nklever)) +- Support for multiple bind templates [#23](https://github.com/jupyterhub/ldapauthenticator/pull/23) ([@kishorchintal](https://github.com/kishorchintal)) #### Contributors to this release [@beenje](https://github.com/search?q=repo%3Ajupyterhub%2Fldapauthenticator+involves%3Abeenje+updated%3A2016-11-21..2018-06-07&type=Issues) | [@deebuls](https://github.com/search?q=repo%3Ajupyterhub%2Fldapauthenticator+involves%3Adeebuls+updated%3A2016-11-21..2018-06-07&type=Issues) | [@dhirschfeld](https://github.com/search?q=repo%3Ajupyterhub%2Fldapauthenticator+involves%3Adhirschfeld+updated%3A2016-11-21..2018-06-07&type=Issues) | [@dirkcgrunwald](https://github.com/search?q=repo%3Ajupyterhub%2Fldapauthenticator+involves%3Adirkcgrunwald+updated%3A2016-11-21..2018-06-07&type=Issues) | [@fm75](https://github.com/search?q=repo%3Ajupyterhub%2Fldapauthenticator+involves%3Afm75+updated%3A2016-11-21..2018-06-07&type=Issues) | [@GrahamDumpleton](https://github.com/search?q=repo%3Ajupyterhub%2Fldapauthenticator+involves%3AGrahamDumpleton+updated%3A2016-11-21..2018-06-07&type=Issues) | [@jcrubioa](https://github.com/search?q=repo%3Ajupyterhub%2Fldapauthenticator+involves%3Ajcrubioa+updated%3A2016-11-21..2018-06-07&type=Issues) | [@kishorchintal](https://github.com/search?q=repo%3Ajupyterhub%2Fldapauthenticator+involves%3Akishorchintal+updated%3A2016-11-21..2018-06-07&type=Issues) | [@mateuszboryn](https://github.com/search?q=repo%3Ajupyterhub%2Fldapauthenticator+involves%3Amateuszboryn+updated%3A2016-11-21..2018-06-07&type=Issues) | [@minrk](https://github.com/search?q=repo%3Ajupyterhub%2Fldapauthenticator+involves%3Aminrk+updated%3A2016-11-21..2018-06-07&type=Issues) | [@nklever](https://github.com/search?q=repo%3Ajupyterhub%2Fldapauthenticator+involves%3Anklever+updated%3A2016-11-21..2018-06-07&type=Issues) | [@pratik705](https://github.com/search?q=repo%3Ajupyterhub%2Fldapauthenticator+involves%3Apratik705+updated%3A2016-11-21..2018-06-07&type=Issues) | [@sauloal](https://github.com/search?q=repo%3Ajupyterhub%2Fldapauthenticator+involves%3Asauloal+updated%3A2016-11-21..2018-06-07&type=Issues) | [@toxadx](https://github.com/search?q=repo%3Ajupyterhub%2Fldapauthenticator+involves%3Atoxadx+updated%3A2016-11-21..2018-06-07&type=Issues) | [@willingc](https://github.com/search?q=repo%3Ajupyterhub%2Fldapauthenticator+involves%3Awillingc+updated%3A2016-11-21..2018-06-07&type=Issues) | [@yuvipanda](https://github.com/search?q=repo%3Ajupyterhub%2Fldapauthenticator+involves%3Ayuvipanda+updated%3A2016-11-21..2018-06-07&type=Issues) +## 1.1 - -## [1.1] - 2016-11-21 +### 1.1.0 - 2016-11-21 #### Merged PRs -* More options for ldap group membership [#22](https://github.com/jupyterhub/ldapauthenticator/pull/22) ([@m0zes](https://github.com/m0zes)) -* Add info on invalidating existing logins [#18](https://github.com/jupyterhub/ldapauthenticator/pull/18) ([@yuvipanda](https://github.com/yuvipanda)) -* Add more verbose logging for login failures [#17](https://github.com/jupyterhub/ldapauthenticator/pull/17) ([@yuvipanda](https://github.com/yuvipanda)) -* Clarify usage of 'c.' [#16](https://github.com/jupyterhub/ldapauthenticator/pull/16) ([@yuvipanda](https://github.com/yuvipanda)) -* Add support for looking up the account DN post-bind [#12](https://github.com/jupyterhub/ldapauthenticator/pull/12) ([@skemper](https://github.com/skemper)) + +- More options for ldap group membership [#22](https://github.com/jupyterhub/ldapauthenticator/pull/22) ([@m0zes](https://github.com/m0zes)) +- Add info on invalidating existing logins [#18](https://github.com/jupyterhub/ldapauthenticator/pull/18) ([@yuvipanda](https://github.com/yuvipanda)) +- Add more verbose logging for login failures [#17](https://github.com/jupyterhub/ldapauthenticator/pull/17) ([@yuvipanda](https://github.com/yuvipanda)) +- Clarify usage of 'c.' [#16](https://github.com/jupyterhub/ldapauthenticator/pull/16) ([@yuvipanda](https://github.com/yuvipanda)) +- Add support for looking up the account DN post-bind [#12](https://github.com/jupyterhub/ldapauthenticator/pull/12) ([@skemper](https://github.com/skemper)) #### Contributors to this release [@m0zes](https://github.com/search?q=repo%3Ajupyterhub%2Fldapauthenticator+involves%3Am0zes+updated%3A2016-03-28..2016-11-21&type=Issues) | [@minrk](https://github.com/search?q=repo%3Ajupyterhub%2Fldapauthenticator+involves%3Aminrk+updated%3A2016-03-28..2016-11-21&type=Issues) | [@skemper](https://github.com/search?q=repo%3Ajupyterhub%2Fldapauthenticator+involves%3Askemper+updated%3A2016-03-28..2016-11-21&type=Issues) | [@yuvipanda](https://github.com/search?q=repo%3Ajupyterhub%2Fldapauthenticator+involves%3Ayuvipanda+updated%3A2016-03-28..2016-11-21&type=Issues) +## 1.0 - -## [v1.0] - 2016-03-28 +### 1.0.0 - 2016-03-28 Initial release. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 01086fc..676ce8c 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,76 +1,51 @@ # Contributing -Welcome! As a [Jupyter](https://jupyter.org) project, we follow the [Jupyter contributor guide](https://jupyter.readthedocs.io/en/latest/contributor/content-contributor.html). +Welcome! As a [Jupyter](https://jupyter.org) project, you can follow the [Jupyter contributor guide](https://docs.jupyter.org/en/latest/contributing/content-contributor.html). -Make sure to also follow [Project Jupyter's Code of Conduct](https://github.com/jupyter/governance/blob/master/conduct/code_of_conduct.md) +Make sure to also follow [Project Jupyter's Code of Conduct](https://github.com/jupyter/governance/blob/main/conduct/code_of_conduct.md) for a friendly and welcoming collaborative environment. -This guide was adapted from the [contributing guide in the main `jupyterhub` repo.](https://github.com/jupyterhub/jupyterhub/blob/master/CONTRIBUTING.md) +This guide was adapted from the [contributing guide in the main `jupyterhub` repo.](https://github.com/jupyterhub/jupyterhub/blob/main/CONTRIBUTING.md) ## Setting up a development environment -JupyterHub requires Python >= 3.5. - As a Python project, a development install of JupyterHub follows standard practices for installation and testing. Note: if you have Docker installed locally, you can run all of the subsequent commands inside of a container after you run the following initial commands: -``` +```shell +# starts an openldap server inside a docker container ./ci/docker-ldap.sh -docker run -v $PWD:/usr/local/src --workdir /usr/local/src --net=host --rm -it python:3.6 bash + +# starts a python docker image +docker run --rm -it -v $PWD:/usr/local/src --workdir=/usr/local/src --net=host python:3.11 bash ``` 1. Do a development install with pip - ```bash - cd ldapauthenticator - python3 -m pip install --editable . - ``` - -1. Install the development requirements, - which include things like testing tools + ```bash + pip install --editable ".[test]" + ``` - ```bash - python3 -m pip install -r dev-requirements.txt - ``` 1. Set up pre-commit hooks for automatic code formatting, etc. - ```bash - pre-commit install - ``` + ```bash + pip install pre-commit - You can also invoke the pre-commit hook manually at any time with + pre-commit install --install-hooks + ``` - ```bash - pre-commit run - ``` - -To clean up your development LDAP deployment, run: -``` -docker rm -f ldap -``` + You can also invoke the pre-commit hook manually at any time with -## Contributing + ```bash + pre-commit run + ``` -JupyterHub has adopted automatic code formatting so you shouldn't -need to worry too much about your code style. -As long as your code is valid, -the pre-commit hook should take care of how it should look. -You can invoke the pre-commit hook by hand at any time with: +To clean up your development LDAP deployment, run: -```bash -pre-commit run ``` - -which should run any autoformatting on your code -and tell you about any errors it couldn't fix automatically. -You may also install [black integration](https://github.com/ambv/black#editor-integration) -into your text editor to format code automatically. - -If you have already committed files before setting up the pre-commit -hook with `pre-commit install`, you can fix everything up using -`pre-commit run --all-files`. You need to make the fixing commit -yourself after that. +docker rm -f test-openldap +``` ## Testing @@ -80,7 +55,11 @@ or that trigger any bugs that you have fixed to catch regressions. You can run the tests with: ```bash -pytest -v +# starts an openldap server inside a docker container +./ci/docker-ldap.sh + +# run tests +pytest ``` The tests live in `ldapauthenticator/tests`. diff --git a/README.md b/README.md index aaeb748..91c28a8 100644 --- a/README.md +++ b/README.md @@ -1,28 +1,29 @@ # ldapauthenticator -[![TravisCI (.com) build status](https://img.shields.io/travis/com/jupyterhub/ldapauthenticator/master?logo=travis)](https://travis-ci.com/jupyterhub/ldapauthenticator) [![Latest PyPI version](https://img.shields.io/pypi/v/jupyterhub-ldapauthenticator?logo=pypi)](https://pypi.python.org/pypi/jupyterhub-ldapauthenticator) [![Latest conda-forge version](https://img.shields.io/conda/vn/conda-forge/jupyterhub-ldapauthenticator?logo=conda-forge)](https://anaconda.org/conda-forge/jupyterhub-ldapauthenticator) -[![GitHub](https://img.shields.io/badge/issue_tracking-github-blue?logo=github)](https://github.com/jupyterhub/ldapauthenticator/issues) -[![Discourse](https://img.shields.io/badge/help_forum-discourse-blue?logo=discourse)](https://discourse.jupyter.org/c/jupyterhub) -[![Gitter](https://img.shields.io/badge/social_chat-gitter-blue?logo=gitter)](https://gitter.im/jupyterhub/jupyterhub) +[![GitHub Workflow Status - Test](https://img.shields.io/github/actions/workflow/status/jupyterhub/ldapauthenticator/test.yaml?logo=github&label=tests)](https://github.com/jupyterhub/ldapauthenticator/actions) +[![Test coverage of code](https://codecov.io/gh/jupyterhub/ldapauthenticator/branch/main/graph/badge.svg)](https://codecov.io/gh/jupyterhub/ldapauthenticator) +[![Issue tracking - GitHub](https://img.shields.io/badge/issue_tracking-github-blue?logo=github)](https://github.com/jupyterhub/ldapauthenticator/issues) +[![Help forum - Discourse](https://img.shields.io/badge/help_forum-discourse-blue?logo=discourse)](https://discourse.jupyter.org/c/jupyterhub) Simple LDAP Authenticator Plugin for JupyterHub -## Installation ## +## Installation You can install it from pip with: ``` pip install jupyterhub-ldapauthenticator ``` + ...or using conda with: + ``` conda install -c conda-forge jupyterhub-ldapauthenticator ``` - -## Logging people out ## +## Logging people out If you make any changes to JupyterHub's authentication setup that changes which group of users is allowed to login (such as changing `allowed_groups` @@ -31,41 +32,41 @@ jupyterhub cookie secret, or users who were previously logged in and did not log out would continue to be able to log in! You can do this by deleting the `jupyterhub_cookie_secret` file. Note -that this will log out *all* users who are currently logged in. +that this will log out _all_ users who are currently logged in. +## Usage -## Usage ## +You can enable this authenticator by adding lines to your `jupyterhub_config.py`. -You can enable this authenticator with the following lines in your -`jupyter_config.py`: +**Note: This file may not exist in your current installation! In TLJH, it +is located in /opt/tljh/config/jupyterhub_config.d. Create it there if you +don't already have one.** ```python -c.JupyterHub.authenticator_class = 'ldapauthenticator.LDAPAuthenticator' +c.JupyterHub.authenticator_class = 'ldap' ``` -### Required configuration ### +### Required configuration At minimum, the following two configuration options must be set before the LDAP Authenticator can be used: - -#### `LDAPAuthenticator.server_address` #### +#### `LDAPAuthenticator.server_address` Address of the LDAP Server to contact. Just use a bare hostname or IP, without a port name or protocol prefix. - -#### `LDAPAuthenticator.lookup_dn` or `LDAPAuthenticator.bind_dn_template` #### +#### `LDAPAuthenticator.lookup_dn` or `LDAPAuthenticator.bind_dn_template` To authenticate a user we need the corresponding DN to bind against the LDAP server. The DN can be acquired by either: 1. setting `bind_dn_template`, which is a list of string template used to generate the full DN for a user from the human readable username, or 2. setting `lookup_dn` to `True`, which does a reverse lookup to obtain the - user's DN. This is because ome LDAP servers, such as Active Directory, don't + user's DN. This is because some LDAP servers, such as Active Directory, don't always bind with the true DN. -##### `lookup_dn = False` ##### +##### `lookup_dn = False` If `lookup_dn = False`, then `bind_dn_template` is required to be a non-empty list of templates the users belong to. For example, if some of the users in your @@ -87,7 +88,7 @@ uses [traitlets](https://traitlets.readthedocs.io) for configuration, and the The `{username}` is expanded into the username the user provides. -##### `lookup_dn = True` ##### +##### `lookup_dn = True` ```python c.LDAPAuthenticator.lookup_dn = True @@ -107,9 +108,9 @@ Also, when using `lookup_dn = True` the options `user_search_base`, `user_attribute`, `lookup_dn_user_dn_attribute` and `lookup_dn_search_filter` are required, although their defaults might be sufficient for your use case. -### Optional configuration ### +### Optional configuration -#### `LDAPAuthenticator.allowed_groups` #### +#### `LDAPAuthenticator.allowed_groups` LDAP groups whose members are allowed to log in. This must be set to either empty `[]` (the default, to disable) or to a list of @@ -126,7 +127,7 @@ c.LDAPAuthenticator.allowed_groups = [ ] ``` -#### `LDAPAuthenticator.group_filter` #### +#### `LDAPAuthenticator.group_filter` The LDAP group search filter. @@ -147,7 +148,7 @@ Here is an example that should work with OpenLDAP servers. (member={userdn}) ``` -#### `LDAPAuthenticator.group_attributes` #### +#### `LDAPAuthenticator.group_attributes` A list of attributes used when searching for LDAP groups. @@ -155,7 +156,7 @@ By default uses `member`, `uniqueMember`, and `memberUid`. Certain servers may reject invalid values causing exceptions during authentication. -#### `LDAPAuthenticator.valid_username_regex` #### +#### `LDAPAuthenticator.valid_username_regex` All usernames will be checked against this before being sent to LDAP. This acts as both an easy way to filter out invalid @@ -164,30 +165,30 @@ usernames as well as protection against LDAP injection attacks. By default it looks for the regex `^[a-z][.a-z0-9_-]*$` which is what most shell username validators do. -#### `LDAPAuthenticator.use_ssl` #### +#### `LDAPAuthenticator.use_ssl` Boolean to specify whether to use SSL encryption when contacting the LDAP server. If it is left to `False` (the default) `LDAPAuthenticator` will try to upgrade connection with StartTLS. Set this to be `True` to start SSL connection. -#### `LDAPAuthenticator.server_port` #### +#### `LDAPAuthenticator.server_port` Port to use to contact the LDAP server. Defaults to 389 if no SSL is being used, and 636 is SSL is being used. -#### `LDAPAuthenticator.user_search_base` #### +#### `LDAPAuthenticator.user_search_base` -Only used with `lookup_dn=True`. Defines the search base for looking up users +Only used with `lookup_dn=True`. Defines the search base for looking up users in the directory. ```python c.LDAPAuthenticator.user_search_base = 'ou=People,dc=example,dc=com' ``` -#### `LDAPAuthenticator.user_attribute` #### +#### `LDAPAuthenticator.user_attribute` -Only used with `lookup_dn=True`. Defines the attribute that stores a user's +Only used with `lookup_dn=True`. Defines the attribute that stores a user's username in your directory. ```python @@ -198,54 +199,52 @@ c.LDAPAuthenticator.user_attribute = 'sAMAccountName' c.LDAPAuthenticator.user_attribute = 'uid' ``` -#### `LDAPAuthenticator.lookup_dn_search_filter` #### +#### `LDAPAuthenticator.lookup_dn_search_filter` How to query LDAP for user name lookup, if `lookup_dn` is set to True. Default value `'({login_attr}={login})'` should be good enough for most use cases. - -#### `LDAPAuthenticator.lookup_dn_search_user`, `LDAPAuthenticator.lookup_dn_search_password` #### +#### `LDAPAuthenticator.lookup_dn_search_user`, `LDAPAuthenticator.lookup_dn_search_password` Technical account for user lookup, if `lookup_dn` is set to True. If both lookup_dn_search_user and lookup_dn_search_password are None, then anonymous LDAP query will be done. +#### `LDAPAuthenticator.lookup_dn_user_dn_attribute` -#### `LDAPAuthenticator.lookup_dn_user_dn_attribute` #### - -Attribute containing user's name needed for building DN string, if `lookup_dn` is set to True. +Attribute containing user's name needed for building DN string, if `lookup_dn` is set to True. See `user_search_base` for info on how this attribute is used. -For most LDAP servers, this is username. For Active Directory, it is cn. +For most LDAP servers, this is username. For Active Directory, it is cn. -#### `LDAPAuthenticator.escape_userdn` #### +#### `LDAPAuthenticator.escape_userdn` If set to True, escape special chars in userdn when authenticating in LDAP. On some LDAP servers, when userdn contains chars like '(', ')', '\' authentication may fail when those chars are not escaped. -#### `LDAPAuthenticator.auth_state_attributes` #### +#### `LDAPAuthenticator.auth_state_attributes` An optional list of attributes to be fetched for a user after login. If found these will be returned as `auth_state`. -#### `LDAPAuthenticator.use_lookup_dn_username` #### +#### `LDAPAuthenticator.use_lookup_dn_username` If set to True (the default) the username used to build the DN string is returned as the username when `lookup_dn` is True. When authenticating on a Linux machine against an AD server this might return something different from the supplied UNIX username. In this case setting this option to False might be a solution. -## Compatibility ## +## Compatibility This has been tested against an OpenLDAP server, with the client running Python 3.4. Verifications of this code working well with other LDAP setups are welcome, as are bug reports and patches to make it work with other LDAP setups! - -## Active Directory integration ## +## Active Directory integration Please use following options for AD integration. This is useful especially in two cases: -* LDAP Search requires valid user account in order to query user database -* DN does not contain login but some other field, like CN (actual login is present in sAMAccountName, and we need to lookup CN) + +- LDAP Search requires valid user account in order to query user database +- DN does not contain login but some other field, like CN (actual login is present in sAMAccountName, and we need to lookup CN) ```python c.LDAPAuthenticator.lookup_dn = True @@ -256,13 +255,11 @@ c.LDAPAuthenticator.user_search_base = 'ou=people,dc=wikimedia,dc=org' c.LDAPAuthenticator.user_attribute = 'sAMAccountName' c.LDAPAuthenticator.lookup_dn_user_dn_attribute = 'cn' c.LDAPAuthenticator.escape_userdn = False -c.LDAPAuthenticator.bind_dn_template = '{username}' ``` In setup above, first LDAP will be searched (with account ldap_search_user_technical_account) for users that have sAMAccountName=login Then DN will be constructed using found CN value. - ## Configuration note on local user creation Currently, local user creation by the LDAPAuthenticator is unsupported as @@ -280,3 +277,42 @@ JupyterHub create local accounts using the LDAPAuthenticator. Issue [#19](https://github.com/jupyterhub/ldapauthenticator/issues/19) provides additional discussion on local user creation. + +## Testing LDAPAuthenticator without JupyterHub + +This script can be written to a file such as `test_ldap_auth.py`, and run with +`python test_ldap_auth.py`, to test use of LDAPAuthenticator with a given config +without involving JupyterHub. + +If the authenticator works, this script should print either None or a username +depending if the user was considered allowed access. + +```python +import asyncio +import getpass + +from traitlets.config import Config +from ldapauthenticator import LDAPAuthenticator + +# Configure LDAPAuthenticator below to work against your ldap server +c = Config() +c.LDAPAuthenticator.server_address = "ldap.organisation.org" +c.LDAPAuthenticator.server_port = 636 +c.LDAPAuthenticator.bind_dn_template = "uid={username},ou=people,dc=organisation,dc=org" +c.LDAPAuthenticator.user_attribute = "uid" +c.LDAPAuthenticator.user_search_base = "ou=people,dc=organisation,dc=org" +c.LDAPAuthenticator.attributes = ["uid", "cn", "mail", "ou", "o"] +# The following is an example of a search_filter which is build on LDAP AND and OR operations +# here in this example as a combination of the LDAP attributes 'ou', 'mail' and 'uid' +sf = "(&(o={o})(ou={ou}))".format(o="yourOrganisation", ou="yourOrganisationalUnit") +sf += "(&(o={o})(mail={mail}))".format(o="yourOrganisation", mail="yourMailAddress") +c.LDAPAuthenticator.search_filter = f"(&({{userattr}}={{username}})(|{sf}))" + +# Run test +authenticator = LDAPAuthenticator(config=c) +username = input("Username: ") +password = getpass.getpass() +data = dict(username=username, password=password) +return_value = asyncio.run(authenticator.authenticate(None, data)) +print(return_value) +``` diff --git a/RELEASE.md b/RELEASE.md index 5157119..e1eb364 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -1,68 +1,61 @@ # How to make a release -`ldapauthenticator` is a package [available on PyPI](https://pypi.org/project/jupyterhub-ldapauthenticator/) and -[conda-forge](https://anaconda.org/conda-forge/jupyterhub-ldapauthenticator). -These are instructions on how to make a release on PyPI. -The PyPI release is done automatically by TravisCI when a tag is pushed. +`jupyterhub-ldapauthenticator` is a package available on [PyPI] and on +[conda-forge]. -For you to follow along according to these instructions, you need: -- To have push rights to the [ldapauthenticator GitHub - repository](https://github.com/jupyterhub/ldapauthenticator). +These are the instructions on how to make a release. -## Steps to make a release +## Pre-requisites -1. Checkout master and make sure it is up to date. +- Push rights to this GitHub repository - ```shell - ORIGIN=${ORIGIN:-origin} # set to the canonical remote, e.g. 'upstream' if 'origin' is not the official repo - git checkout master - git fetch $ORIGIN master - git reset --hard $ORIGIN/master - # WARNING! This next command deletes any untracked files in the repo - git clean -xfd - ``` +## Steps to make a release -1. Set the `version` variable in [setup.py](setup.py) - appropriately and make a commit. +1. Create a PR updating `CHANGELOG.md` with [github-activity] and continue when + its merged. - ``` - git add setup.py - VERSION=... # e.g. 1.2.3 - git commit -m "release $VERSION" - ``` + Advice on this procedure can be found in [this team compass + issue](https://github.com/jupyterhub/team-compass/issues/563). -1. Reset the `version` variable in - [setup.py](setup.py) appropriately with an incremented - patch version and a `dev` element, then make a commit. - ``` - git add setup.py - git commit -m "back to dev" +2. Checkout main and make sure it is up to date. + + ```shell + git checkout main + git fetch origin main + git reset --hard origin/main ``` -1. Push your two commits to master. +3. Update the version, make commits, and push a git tag with `tbump`. ```shell - # first push commits without a tags to ensure the - # commits comes through, because a tag can otherwise - # be pushed all alone without company of rejected - # commits, and we want have our tagged release coupled - # with a specific commit in master - git push $ORIGIN master + pip install tbump ``` -1. Create a git tag for the pushed release commit and push it. + `tbump` will ask for confirmation before doing anything. ```shell - git tag -a $VERSION -m $VERSION HEAD~1 + # Example versions to set: 1.0.0, 1.0.0b1 + VERSION= + tbump ${VERSION} + ``` - # then verify you tagged the right commit - git log + Following this, the [CI system] will build and publish a release. - # then push it - git push $ORIGIN refs/tags/$VERSION +4. Reset the version back to dev, e.g. `1.0.1.dev` after releasing `1.0.0`. + + ```shell + # Example version to set: 1.0.1.dev + NEXT_VERSION= + tbump --no-tag ${NEXT_VERSION}.dev ``` -1. Following the release to PyPI, an automated PR should arrive to - [conda-forge/ldapauthenticator-feedstock](https://github.com/conda-forge/jupyterhub-ldapauthenticator-feedstock), - check for the tests to succeed on this PR and then merge it to successfully - update the package for `conda` on the conda-forge channel. +5. Following the release to PyPI, an automated PR should arrive within 24 hours + to [conda-forge/jupyterhub-ldapauthenticator-feedstock] with instructions on + releasing to conda-forge. You are welcome to volunteer doing this, but aren't + required as part of making this release to PyPI. + +[github-activity]: https://github.com/executablebooks/github-activity +[pypi]: https://pypi.org/project/jupyterhub-ldapauthenticator/ +[ci system]: https://github.com/jupyterhub/jupyterhub-ldapauthenticator/actions/workflows/release.yaml +[conda-forge]: https://anaconda.org/conda-forge/jupyterhub-ldapauthenticator +[conda-forge/jupyterhub-ldapauthenticator-feedstock]: https://github.com/conda-forge/jupyterhub-ldapauthenticator-feedstock diff --git a/ci/docker-ldap.sh b/ci/docker-ldap.sh index 7c140f4..976f969 100755 --- a/ci/docker-ldap.sh +++ b/ci/docker-ldap.sh @@ -1,13 +1,25 @@ #!/usr/bin/env bash -# source this file to setup LDAP -# for local testing (as similar as possible to docker) - set -e -NAME="hub-test-ldap" -DOCKER_RUN="docker run -d --name $NAME" -RUN_ARGS="-p 389:389 -p 636:636 rroemhild/test-openldap" - -docker rm -f "$NAME" 2>/dev/null || true +# This file (re-)starts an openldap server to test against within a docker +# container based on the image rroemhild/test-openldap. +# +# ref: https://github.com/rroemhild/docker-test-openldap +# ref: https://github.com/rroemhild/docker-test-openldap/pkgs/container/docker-test-openldap +# +# Stop any existing test-openldap container +docker rm --force test-openldap 2>/dev/null || true +# Start a container, and expose some ports, where 389 and 636 are the local +# system's ports that are redirected to the started container. +# +# - 389:10389 (ldap) +# - 636:10636 (ldaps) +# +# Image updated 2024-09-12 to the latest commit's build +# https://github.com/rroemhild/docker-test-openldap/commit/2645f2164ffb51ec4b5b4a9af0065ad7f2ffc1cf +# +IMAGE=ghcr.io/rroemhild/docker-test-openldap@sha256:107ecba713dd233f6f84047701d1b4dda03307d972814f2ae1db69b0d250544f +docker run --detach --name=test-openldap -p 389:10389 -p 636:10636 $IMAGE -$DOCKER_RUN $RUN_ARGS +# It takes a bit more than one second for the container to become ready +sleep 3 diff --git a/dev-requirements.txt b/dev-requirements.txt deleted file mode 100644 index f225f7a..0000000 --- a/dev-requirements.txt +++ /dev/null @@ -1,13 +0,0 @@ -beautifulsoup4 -codecov -coverage -cryptography -html5lib # needed for beautifulsoup -mock -notebook -pre-commit -pytest-asyncio -pytest-cov -pytest>=3.3 -requests-mock -virtualenv diff --git a/ldapauthenticator/__init__.py b/ldapauthenticator/__init__.py index 6687943..cf2752e 100644 --- a/ldapauthenticator/__init__.py +++ b/ldapauthenticator/__init__.py @@ -1 +1,6 @@ -from ldapauthenticator.ldapauthenticator import LDAPAuthenticator +from ldapauthenticator.ldapauthenticator import LDAPAuthenticator # noqa + +# __version__ should be updated using tbump, based on configuration in +# pyproject.toml, according to instructions in RELEASE.md. +# +__version__ = "2.0.0.dev" diff --git a/ldapauthenticator/ldapauthenticator.py b/ldapauthenticator/ldapauthenticator.py index c0c9570..c17f17c 100644 --- a/ldapauthenticator/ldapauthenticator.py +++ b/ldapauthenticator/ldapauthenticator.py @@ -3,12 +3,7 @@ import ldap3 from jupyterhub.auth import Authenticator from ldap3.utils.conv import escape_filter_chars -from tornado import gen -from traitlets import Bool -from traitlets import Int -from traitlets import List -from traitlets import Unicode -from traitlets import Union +from traitlets import Bool, Int, List, Unicode, Union, validate class LDAPAuthenticator(Authenticator): @@ -69,10 +64,23 @@ def _server_port_default(self): """, ) + @validate("bind_dn_template") + def _validate_bind_dn_template(self, proposal): + """ + Ensure a List[str] is set, filtered from empty string elements. + """ + rv = [] + if isinstance(proposal.value, str): + rv = [proposal.value] + if "" in rv: + self.log.warning("Ignoring blank 'bind_dn_template' entry!") + rv = [e for e in rv if e] + return rv + allowed_groups = List( config=True, allow_none=True, - default=None, + default_value=None, help=""" List of LDAP group DNs that users could be members of to be granted access. @@ -135,7 +143,7 @@ def _server_port_default(self): user_search_base = Unicode( config=True, - default=None, + default_value=None, allow_none=True, help=""" Base for looking up user accounts in the directory, if `lookup_dn` is set to True. @@ -152,7 +160,7 @@ def _server_port_default(self): c.LDAPAuthenticator.lookup_dn_search_user = 'ldap_search_user_technical_account' c.LDAPAuthenticator.lookup_dn_search_password = 'secret' c.LDAPAuthenticator.user_search_base = 'ou=people,dc=wikimedia,dc=org' - c.LDAPAuthenticator.user_attribute = 'sAMAccountName' + c.LDAPAuthenticator.user_attribute = 'uid' c.LDAPAuthenticator.lookup_dn_user_dn_attribute = 'cn' c.LDAPAuthenticator.bind_dn_template = '{username}' ``` @@ -161,7 +169,7 @@ def _server_port_default(self): user_attribute = Unicode( config=True, - default=None, + default_value=None, allow_none=True, help=""" Attribute containing user's name, if `lookup_dn` is set to True. @@ -247,35 +255,32 @@ def _server_port_default(self): ) def resolve_username(self, username_supplied_by_user): + """ + Resolves a username supplied by a user to the a user DN when lookup_dn + is True. + """ search_dn = self.lookup_dn_search_user if self.escape_userdn: search_dn = escape_filter_chars(search_dn) conn = self.get_connection( - userdn=search_dn, password=self.lookup_dn_search_password + userdn=search_dn, + password=self.lookup_dn_search_password, ) - is_bound = conn.bind() - if not is_bound: - msg = "Failed to connect to LDAP server with search user '{search_dn}'" - self.log.warning(msg.format(search_dn=search_dn)) + if not conn.bind(): + self.log.warning( + f"Failed to connect to LDAP server with search user '{search_dn}'" + ) return (None, None) search_filter = self.lookup_dn_search_filter.format( - login_attr=self.user_attribute, login=username_supplied_by_user - ) - msg = "\n".join( - [ - "Looking up user with:", - " search_base = '{search_base}'", - " search_filter = '{search_filter}'", - " attributes = '{attributes}'", - ] + login_attr=self.user_attribute, + login=escape_filter_chars(username_supplied_by_user), ) self.log.debug( - msg.format( - search_base=self.user_search_base, - search_filter=search_filter, - attributes=self.user_attribute, - ) + "Looking up user with:\n", + f" search_base = '{self.user_search_base}'\n", + f" search_filter = '{search_filter}'\n", + f" attributes = '[{self.lookup_dn_user_dn_attribute}]'", ) conn.search( search_base=self.user_search_base, @@ -285,14 +290,9 @@ def resolve_username(self, username_supplied_by_user): ) response = conn.response if len(response) == 0 or "attributes" not in response[0].keys(): - msg = ( - "No entry found for user '{username}' " - "when looking up attribute '{attribute}'" - ) self.log.warning( - msg.format( - username=username_supplied_by_user, attribute=self.user_attribute - ) + f"No entry found for user '{username_supplied_by_user}' " + f"when looking up attribute '{self.user_attribute}'" ) return (None, None) @@ -303,19 +303,11 @@ def resolve_username(self, username_supplied_by_user): elif len(user_dn) == 1: user_dn = user_dn[0] else: - msg = ( - "A lookup of the username '{username}' returned a list " - "of entries for the attribute '{attribute}'. Only the " - "first among these ('{first_entry}') was used. The other " - "entries ({other_entries}) were ignored." - ) self.log.warn( - msg.format( - username=username_supplied_by_user, - attribute=self.lookup_dn_user_dn_attribute, - first_entry=user_dn[0], - other_entries=", ".join(user_dn[1:]), - ) + f"A lookup of the username '{username_supplied_by_user}' returned a list " + f"of entries for the attribute '{self.lookup_dn_user_dn_attribute}'. Only " + f"the first among these ('{user_dn[0]}') was used. The other entries " + f"({', '.join(user_dn[1:])}) were ignored." ) user_dn = user_dn[0] @@ -343,8 +335,15 @@ def get_user_attributes(self, conn, userdn): attrs = conn.entries[0].entry_attributes_as_dict return attrs - @gen.coroutine - def authenticate(self, handler, data): + async def authenticate(self, handler, data): + """ + Note: This function is really meant to identify a user, and + check_allowed and check_blocked are meant to determine if its an + authorized user. Authorization is currently handled by returning + None here instead. + + ref: https://jupyterhub.readthedocs.io/en/latest/reference/authenticators.html#authenticator-authenticate + """ username = data["username"] password = data["password"] @@ -362,18 +361,14 @@ def authenticate(self, handler, data): self.log.warning("username:%s Login denied for blank password", username) return None - # bind_dn_template should be of type List[str] - bind_dn_template = self.bind_dn_template - if isinstance(bind_dn_template, str): - bind_dn_template = [bind_dn_template] - # sanity check - if not self.lookup_dn and not bind_dn_template: + if not self.lookup_dn and not self.bind_dn_template: self.log.warning( "Login not allowed, please configure 'lookup_dn' or 'bind_dn_template'." ) return None + bind_dn_template = self.bind_dn_template if self.lookup_dn: username, resolved_dn = self.resolve_username(username) if not username: @@ -386,14 +381,10 @@ def authenticate(self, handler, data): is_bound = False for dn in bind_dn_template: - if not dn: - self.log.warning("Ignoring blank 'bind_dn_template' entry!") - continue userdn = dn.format(username=username) if self.escape_userdn: userdn = escape_filter_chars(userdn) - msg = "Attempting to bind {username} with {userdn}" - self.log.debug(msg.format(username=username, userdn=userdn)) + self.log.debug(f"Attempting to bind {username} with {userdn}") msg = "Status of user bind {username} with {userdn} : {is_bound}" try: conn = self.get_connection(userdn, password) @@ -411,13 +402,12 @@ def authenticate(self, handler, data): break if not is_bound: - msg = "Invalid password for user '{username}'" - self.log.warning(msg.format(username=username)) + self.log.warning(f"Invalid password for user '{username}'") return None if self.search_filter: search_filter = self.search_filter.format( - userattr=self.user_attribute, username=username + userattr=self.user_attribute, username=escape_filter_chars(username) ) conn.search( search_base=self.user_search_base, @@ -427,20 +417,14 @@ def authenticate(self, handler, data): ) n_users = len(conn.response) if n_users == 0: - msg = "User with '{userattr}={username}' not found in directory" self.log.warning( - msg.format(userattr=self.user_attribute, username=username) + f"User with '{self.user_attribute}={username}' not found in directory" ) return None if n_users > 1: - msg = ( - "Duplicate users found! " - "{n_users} users found with '{userattr}={username}'" - ) self.log.warning( - msg.format( - userattr=self.user_attribute, username=username, n_users=n_users - ) + "Duplicate users found! {n_users} users found " + f"with '{self.user_attribute}={username}'" ) return None @@ -454,7 +438,10 @@ def authenticate(self, handler, data): found = False for group in self.allowed_groups: group_filter = self.group_filter - group_filter = group_filter.format(userdn=userdn, uid=username) + group_filter = group_filter.format( + userdn=escape_filter_chars(userdn), + uid=escape_filter_chars(username), + ) group_attributes = self.group_attributes found = conn.search( group, @@ -466,8 +453,9 @@ def authenticate(self, handler, data): break if not found: # If we reach here, then none of the groups matched - msg = "username:{username} User not in any of the allowed groups" - self.log.warning(msg.format(username=username)) + self.log.warning( + f"username:{username} User not in any of the allowed groups" + ) return None if not self.use_lookup_dn_username: @@ -478,25 +466,3 @@ def authenticate(self, handler, data): self.log.debug("username:%s attributes:%s", username, user_info) return {"name": username, "auth_state": user_info} return username - - -if __name__ == "__main__": - import getpass - - c = LDAPAuthenticator() - c.server_address = "ldap.organisation.org" - c.server_port = 636 - c.bind_dn_template = "uid={username},ou=people,dc=organisation,dc=org" - c.user_attribute = "uid" - c.user_search_base = "ou=people,dc=organisation,dc=org" - c.attributes = ["uid", "cn", "mail", "ou", "o"] - # The following is an example of a search_filter which is build on LDAP AND and OR operations - # here in this example as a combination of the LDAP attributes 'ou', 'mail' and 'uid' - sf = "(&(o={o})(ou={ou}))".format(o="yourOrganisation", ou="yourOrganisationalUnit") - sf += "(&(o={o})(mail={mail}))".format(o="yourOrganisation", mail="yourMailAddress") - c.search_filter = "(&({{userattr}}={{username}})(|{}))".format(sf) - username = input("Username: ") - passwd = getpass.getpass() - data = dict(username=username, password=passwd) - rs = c.authenticate(None, data) - print(rs.result()) diff --git a/ldapauthenticator/tests/conftest.py b/ldapauthenticator/tests/conftest.py index 545744f..5204a35 100644 --- a/ldapauthenticator/tests/conftest.py +++ b/ldapauthenticator/tests/conftest.py @@ -1,4 +1,3 @@ -import inspect import os import pytest @@ -6,17 +5,7 @@ from ..ldapauthenticator import LDAPAuthenticator -def pytest_collection_modifyitems(items): - """add asyncio marker to all async tests""" - for item in items: - if inspect.iscoroutinefunction(item.obj): - item.add_marker("asyncio") - if hasattr(inspect, "isasyncgenfunction"): - # double-check that we aren't mixing yield and async def - assert not inspect.isasyncgenfunction(item.obj) - - -@pytest.fixture(scope="session") +@pytest.fixture() def authenticator(): authenticator = LDAPAuthenticator() authenticator.server_address = os.environ.get("LDAP_HOST", "localhost") diff --git a/ldapauthenticator/tests/test_ldapauthenticator.py b/ldapauthenticator/tests/test_ldapauthenticator.py index 8e52163..35a0371 100644 --- a/ldapauthenticator/tests/test_ldapauthenticator.py +++ b/ldapauthenticator/tests/test_ldapauthenticator.py @@ -1,4 +1,9 @@ -# Inspired by https://github.com/jupyterhub/jupyterhub/blob/master/jupyterhub/tests/test_auth.py +""" +Inspired by https://github.com/jupyterhub/jupyterhub/blob/main/jupyterhub/tests/test_auth.py + +Testing data is hardcoded in docker-test-openldap, described at +https://github.com/rroemhild/docker-test-openldap?tab=readme-ov-file#ldap-structure +""" async def test_ldap_auth_allowed(authenticator): @@ -70,7 +75,6 @@ async def test_ldap_auth_use_lookup_dn(authenticator): None, {"username": "fry", "password": "fry"} ) assert authorized["name"] == "philip j. fry" - authenticator.use_lookup_dn_username = False async def test_ldap_auth_search_filter(authenticator): diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..a8605bc --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,89 @@ +# autoflake is used for autoformatting Python code +# +# ref: https://github.com/PyCQA/autoflake#readme +# +[tool.autoflake] +ignore-init-module-imports = true +remove-all-unused-imports = true +remove-duplicate-keys = true +remove-unused-variables = true + + +# isort is used for autoformatting Python code +# +# ref: https://pycqa.github.io/isort/ +# +[tool.isort] +profile = "black" + + +# black is used for autoformatting Python code +# +# ref: https://black.readthedocs.io/en/stable/ +# +[tool.black] +# target-version should be all supported versions, see +# https://github.com/psf/black/issues/751#issuecomment-473066811 +target_version = [ + "py39", + "py310", + "py311", + "py312", +] + + +# pytest is used for running Python based tests +# +# ref: https://docs.pytest.org/en/stable/ +# +[tool.pytest.ini_options] +addopts = "--verbose --color=yes --durations=10" +asyncio_mode = "auto" +testpaths = ["ldapauthenticator/tests"] +# warnings we can safely ignore stemming from jupyterhub 3 + sqlalchemy 2 +filterwarnings = [ + 'ignore:.*The new signature is "def engine_connect\(conn\)"*:sqlalchemy.exc.SADeprecationWarning', + 'ignore:.*The new signature is "def engine_connect\(conn\)"*:sqlalchemy.exc.SAWarning', +] + + +# pytest-cov / coverage is used to measure code coverage of tests +# +# ref: https://coverage.readthedocs.io/en/stable/config.html +# +[tool.coverage.run] +omit = [ + "ldapauthenticator/tests/**", +] + + +# tbump is used to simplify and standardize the release process when updating +# the version, making a git commit and tag, and pushing changes. +# +# ref: https://github.com/your-tools/tbump#readme +# +[tool.tbump] +github_url = "https://github.com/jupyterhub/systemdspawner" + +[tool.tbump.version] +current = "2.0.0.dev" +regex = ''' + (?P\d+) + \. + (?P\d+) + \. + (?P\d+) + (?P
((a|b|rc)\d+)|)
+    \.?
+    (?P(?<=\.)dev\d*|)
+'''
+
+[tool.tbump.git]
+message_template = "Bump to {new_version}"
+tag_template = "{new_version}"
+
+[[tool.tbump.file]]
+src = "setup.py"
+
+[[tool.tbump.file]]
+src = "ldapauthenticator/__init__.py"
diff --git a/pytest.ini b/pytest.ini
deleted file mode 100644
index 9dffb00..0000000
--- a/pytest.ini
+++ /dev/null
@@ -1,11 +0,0 @@
-[pytest]
-# pytest 3.10 has broken minversion checks,
-# so we have to disable this until pytest 3.11
-# minversion = 3.3
-
-python_files = test_*.py
-markers =
-    group: mark as a test for groups
-    services: mark as a services test
-    user: mark as a test for a user
-    slow: mark a test as slow
diff --git a/scripts b/scripts
new file mode 100644
index 0000000..e69de29
diff --git a/setup.py b/setup.py
index 6e1d805..4bb2099 100644
--- a/setup.py
+++ b/setup.py
@@ -1,16 +1,8 @@
 from setuptools import setup
 
-
-version = "1.3.3.dev"
-
-
-with open("./ldapauthenticator/__init__.py", "a") as f:
-    f.write("\n__version__ = '{}'\n".format(version))
-
-
 setup(
     name="jupyterhub-ldapauthenticator",
-    version=version,
+    version="2.0.0.dev",
     description="LDAP Authenticator for JupyterHub",
     long_description=open("README.md").read(),
     long_description_content_type="text/markdown",
@@ -19,5 +11,23 @@
     author_email="yuvipanda@riseup.net",
     license="3 Clause BSD",
     packages=["ldapauthenticator"],
-    install_requires=["jupyterhub", "ldap3", "tornado", "traitlets"],
+    python_requires=">=3.9",
+    install_requires=[
+        "jupyterhub>=4.1.6",
+        "ldap3>=2.9.1",
+        "traitlets",
+    ],
+    extras_require={
+        "test": [
+            "pytest",
+            "pytest-asyncio",
+            "pytest-cov",
+        ],
+    },
+    entry_points={
+        "jupyterhub.authenticators": [
+            "ldap = ldapauthenticator:LDAPAuthenticator",
+            "ldapauthenticator = ldapauthenticator:LDAPAuthenticator",
+        ],
+    },
 )