diff --git a/.cargo/config.toml b/.cargo/config.toml new file mode 100644 index 0000000..b2c9bde --- /dev/null +++ b/.cargo/config.toml @@ -0,0 +1,2 @@ +[doc.extern-map.registries] +crates-io = "https://docs.rs/" diff --git a/.clippy.toml b/.clippy.toml new file mode 100644 index 0000000..ce833e6 --- /dev/null +++ b/.clippy.toml @@ -0,0 +1,3 @@ +avoid-breaking-exported-api = true +disallowed-types = ["std::collections::HashMap", "std::collections::HashSet"] +msrv = "1.77" diff --git a/.envrc b/.envrc new file mode 100644 index 0000000..7f7290a --- /dev/null +++ b/.envrc @@ -0,0 +1,19 @@ +layout_poetry() { + if [[ ! -f pyproject.toml ]]; then + log_error 'No pyproject.toml found. Use `poetry new` or `poetry init` to create one first.' + exit 2 + fi + + local VENV=$(poetry run poetry env info --path) + if [[ -z $VENV || ! -d $VENV/bin ]]; then + log_error 'No poetry virtual environment found. Use `poetry install` to create one first.' + exit 2 + fi + + export VIRTUAL_ENV=$VENV + export POETRY_ACTIVE=1 + PATH_add "$VENV/bin" +} + +use flake +layout_poetry diff --git a/.github/workflows/ci-python.yml b/.github/workflows/ci-python.yml new file mode 100644 index 0000000..38ebd91 --- /dev/null +++ b/.github/workflows/ci-python.yml @@ -0,0 +1,215 @@ +--- +name: Python CI + +"on": + push: + branches: + - main + tags: + - "*" + pull_request: + branches: + - main + +env: + DEBIAN_FRONTEND: noninteractive + +jobs: + lint: + strategy: + fail-fast: false + matrix: + job: + - black + - mypy + - ruff + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - run: pipx install poetry || true + - uses: actions/setup-python@v5 + with: + python-version-file: "pyproject.toml" + cache: "poetry" + - run: poetry install --only=dev + - uses: taiki-e/install-action@v2 + with: + tool: just + - run: poetry run just ci-lint-${{ matrix.job }} + build: + strategy: + fail-fast: false + matrix: + python-version: + - "3.8" + - "3.9" + - "3.10" + - "3.11" + - "3.12" + - "3.13" + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions-rust-lang/setup-rust-toolchain@v1 + - run: pipx install poetry || true + - uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + cache: "poetry" + - run: poetry install --only=dev + - uses: taiki-e/install-action@v2 + with: + tool: just + - run: poetry run just ci-build-python + - uses: actions/upload-artifact@v4 + with: + name: python-${{ matrix.python-version }}-build + path: dist + test: + needs: build + strategy: + fail-fast: false + matrix: + job: + - mit + # Several issues in k5test preventing us from running kadmind with it currently + # - h5l + python-version: + - "3.8" + - "3.9" + - "3.10" + - "3.11" + - "3.12" + - "3.13" + runs-on: ubuntu-latest + env: + KRB5_TRACE: /dev/stderr + steps: + - uses: actions/checkout@v4 + - shell: bash + run: | + pipx install poetry || true + - uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + cache: "poetry" + - run: poetry install --only=test + - uses: taiki-e/install-action@v2 + with: + tool: just + - uses: actions/download-artifact@v4 + with: + name: python-${{ matrix.python-version }}-build + path: dist + - run: | + PATH="/usr/lib/heimdal-servers:$PATH̀ˆ" poetry run just ci-test-python-${{ matrix.job }} + check: + if: always() + needs: + - lint + - build + - test + runs-on: ubuntu-latest + steps: + - uses: re-actors/alls-green@release/v1 + with: + jobs: ${{ toJSON(needs) }} + build-sdist: + needs: check + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + - run: pipx install poetry || true + - uses: actions/setup-python@v5 + with: + python-version-file: pyproject.toml + cache: "poetry" + - run: poetry install --only=dev + - uses: taiki-e/install-action@v2 + with: + tool: just + - run: poetry run just ci-build-python-sdist + - uses: actions/upload-artifact@v4 + with: + name: python-cibw-sdist + path: dist/*.tar.gz + build-wheels-matrix: + # needs: check + runs-on: ubuntu-latest + outputs: + matrix: ${{ steps.set-matrix.outputs.matrix }} + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + - run: pipx install poetry || true + - uses: actions/setup-python@v5 + with: + python-version-file: pyproject.toml + cache: "poetry" + - run: poetry install --only=dev + - id: set-matrix + name: compute matrix + run: | + MATRIX="$( + { + poetry run cibuildwheel --print-build-identifiers --platform linux --archs x86_64,aarch64,ppc64le,s390x \ + | sed 's/.*/{"cibw-only": "&", "os": "ubuntu-latest"}/' \ + && poetry run cibuildwheel --print-build-identifiers --platform macos --archs x86_64 \ + | sed 's/.*/{"cibw-only": "&", "os": "macos-13" }/' \ + && poetry run cibuildwheel --print-build-identifiers --platform macos --archs arm64 \ + | sed 's/.*/{"cibw-only": "&", "os": "macos-14" }/' + } | jq --slurp --compact-output '{"include": .}' + )" + echo matrix="$MATRIX" >> "$GITHUB_OUTPUT" + build-wheels: + needs: build-wheels-matrix + strategy: + fail-fast: false + matrix: ${{ fromJson(needs.build-wheels-matrix.outputs.matrix) }} + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + - uses: docker/setup-qemu-action@v3 + if: runner.os == 'Linux' + with: + platforms: all + - uses: pypa/cibuildwheel@v2.21.3 + with: + only: "${{ matrix.cibw-only }}" + env: + CIBW_BEFORE_ALL_LINUX: "curl -sSf https://sh.rustup.rs | sh -s -- -y && yum install -y krb5-devel clang-devel" + CIBW_BEFORE_ALL_MACOS: "curl -sSf https://sh.rustup.rs | sh -s -- -y && brew install llvm krb5" + CIBW_ENVIRONMENT_LINUX: "PATH=$HOME/.cargo/bin:$PATH" + CIBW_ENVIRONMENT_MACOS: "PKG_CONFIG_PATH=/opt/homebrew/opt/krb5/lib/pkgconfig:/usr/local/opt/krb5/lib/pkgconfig MACOSX_DEPLOYMENT_TARGET=14.0" + CIBW_MANYLINUX_X86_64_IMAGE: manylinux_2_28 + CIBW_MANYLINUX_PYPY_X86_64_IMAGE: manylinux_2_28 + CIBW_MANYLINUX_AARCH64_IMAGE: manylinux_2_28 + CIBW_MANYLINUX_PPC64LE_IMAGE: manylinux_2_28 + CIBW_MANYLINUX_S390X_IMAGE: manylinux_2_28 + CIBW_MANYLINUX_PYPY_AARCH64_IMAGE: manylinux_2_28 + - uses: actions/upload-artifact@v4 + with: + name: python-cibw-wheels-${{ matrix.cibw-only }} + path: ./wheelhouse/*.whl + release: + needs: + - check + - build-sdist + - build-wheels + runs-on: ubuntu-latest + environment: pypi + permissions: + id-token: write + if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/python-kadmin-rs/version/') + steps: + - uses: actions/download-artifact@v4 + with: + pattern: python-cibw-* + path: dist + merge-multiple: true + - uses: pypa/gh-action-pypi-publish@release/v1 diff --git a/.github/workflows/ci-rust.yml b/.github/workflows/ci-rust.yml new file mode 100644 index 0000000..b64cd20 --- /dev/null +++ b/.github/workflows/ci-rust.yml @@ -0,0 +1,120 @@ +--- +name: Rust CI + +"on": + push: + branches: + - main + pull_request: + branches: + - main + +env: + DEBIAN_FRONTEND: noninteractive + +jobs: + lint: + strategy: + fail-fast: false + matrix: + job: + - clippy + include: + - job: rustfmt + toolchain: nightly + components: rustfmt + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - if: ${{ matrix.toolchain }} + name: Setup rust + uses: actions-rust-lang/setup-rust-toolchain@v1 + with: + toolchain: ${{ matrix.toolchain }} + components: ${{ matrix.components }} + - if: ${{ ! matrix.toolchain }} + name: Setup rust + uses: actions-rust-lang/setup-rust-toolchain@v1 + - uses: taiki-e/install-action@v2 + with: + tool: just + - name: Lint + run: just ci-lint-${{ matrix.job }} + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Setup rust + uses: actions-rust-lang/setup-rust-toolchain@v1 + - uses: taiki-e/install-action@v2 + with: + tool: just + - name: Build + run: just ci-build-rust + test: + strategy: + fail-fast: false + matrix: + job: + - rust + - sanity + runs-on: ubuntu-latest + env: + KRB5_TRACE: /dev/stderr + steps: + - uses: actions/checkout@v4 + - name: Install poetry & deps + shell: bash + run: | + pipx install poetry || true + - name: Setup python and restore poetry + uses: actions/setup-python@v5 + with: + python-version-file: "pyproject.toml" + cache: "poetry" + - name: Install Python dependencies + run: poetry install --only=test + - name: Setup rust + uses: actions-rust-lang/setup-rust-toolchain@v1 + - uses: taiki-e/install-action@v2 + with: + tool: just + - name: Test + run: poetry run just ci-test-${{ matrix.job }} + check: + if: always() + needs: + - lint + - build + - test + runs-on: ubuntu-latest + steps: + - uses: re-actors/alls-green@release/v1 + with: + jobs: ${{ toJSON(needs) }} + release: + needs: + - check + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + include: + - crate: kadmin-sys + extra_args: "--no-verify" + - crate: kadmin + extra_args: "" + if: github.event_name == 'push' && startsWith(github.ref, format('refs/tags/{0}/version/', matrix.crate)) + env: + CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + - name: Setup rust + uses: actions-rust-lang/setup-rust-toolchain@v1 + - uses: taiki-e/install-action@v2 + with: + tool: just + - run: just ci-build-deps + - run: cargo publish --package ${{ matrix.crate }} ${{ matrix.extra_args }} diff --git a/.github/workflows/release-create.yml b/.github/workflows/release-create.yml new file mode 100644 index 0000000..9afda21 --- /dev/null +++ b/.github/workflows/release-create.yml @@ -0,0 +1,39 @@ +--- +name: Create release + +on: + pull_request: + types: + - closed + branches: + - main + +jobs: + release-info: + if: github.event.pull_request.merged + outputs: + is-release: ${{ steps.meta.outputs.is-release }} + package: ${{ steps.meta.outputs.crates-name }} + version: ${{ steps.meta.outputs.version-actual }} + runs-on: ubuntu-latest + steps: + - id: meta + uses: cargo-bins/release-meta@v1 + with: + event-data: ${{ toJSON(github.event) }} + + release-create: + needs: + - info + if: needs.info.outputs.is-release == 'true' + permissions: + contents: write + runs-on: ubuntu-latest + steps: + - uses: softprops/action-gh-release@v2 + with: + name: "${{ needs.info.outputs.package }} ${{ needs.info.outputs.version }}" + tag_name: "${{ needs.info.outputs.package }}/version/${{ needs.info.outputs.version }}" + target_commitish: "${{ github.sha }}" + draft: true + make_latest: true diff --git a/.github/workflows/release-pr.yml b/.github/workflows/release-pr.yml new file mode 100644 index 0000000..ef8a8c7 --- /dev/null +++ b/.github/workflows/release-pr.yml @@ -0,0 +1,40 @@ +--- +name: Create release PR + +on: + workflow_dispatch: + inputs: + package: + description: Package to release + required: true + type: choice + options: + - kadmin-sys + - kadmin + - python-kadmin-rs + version: + description: Version to release + required: true + type: string + +jobs: + release-pr: + permissions: + id-token: write + pull-requests: write + contents: write + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: chainguard-dev/actions/setup-gitsign@main + - uses: actions-rust-lang/setup-rust-toolchain@v1 + - uses: taiki-e/install-action@v2 + with: + tool: just,cargo-release,cargo-workspaces + - run: just ci-build-deps + - uses: cargo-bins/release-pr@v2 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + version: ${{ inputs.version }} + crate-name: ${{ inputs.package }} + pr-label: release diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..705b823 --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +build +dist +sdist +target +*.whl +*.egg-info +__pycache__ diff --git a/.rustfmt.toml b/.rustfmt.toml new file mode 100644 index 0000000..0e15701 --- /dev/null +++ b/.rustfmt.toml @@ -0,0 +1,16 @@ +comment_width = 120 +format_code_in_doc_comments = true +format_strings = true +group_imports = "StdExternalCrate" +hex_literal_case = "Lower" +imports_granularity = "Crate" +max_width = 120 +newline_style = "Unix" +normalize_comments = true +normalize_doc_attributes = true +reorder_impl_items = true +use_field_init_shorthand = true +use_try_shorthand = true +style_edition = "2024" +where_single_line = true +wrap_comments = true diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..40a5f43 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,668 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "aho-corasick" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +dependencies = [ + "memchr", +] + +[[package]] +name = "anyhow" +version = "1.0.91" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c042108f3ed77fd83760a5fd79b53be043192bb3b9dba91d8c574c0ada7850c8" + +[[package]] +name = "autocfg" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" + +[[package]] +name = "bindgen" +version = "0.70.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f49d8fed880d473ea71efb9bf597651e77201bdd4893efe54c9e5d65ae04ce6f" +dependencies = [ + "bitflags", + "cexpr", + "clang-sys", + "itertools", + "log", + "prettyplease", + "proc-macro2", + "quote", + "regex", + "rustc-hash", + "shlex", + "syn", +] + +[[package]] +name = "bitflags" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" + +[[package]] +name = "cexpr" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" +dependencies = [ + "nom", +] + +[[package]] +name = "cfg-expr" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0890061c4d3223e7267f3bad2ec40b997d64faac1c2815a4a9d95018e2b9e9c" +dependencies = [ + "smallvec", + "target-lexicon", +] + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "clang-sys" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4" +dependencies = [ + "glob", + "libc", + "libloading", +] + +[[package]] +name = "either" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" + +[[package]] +name = "equivalent" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" + +[[package]] +name = "glob" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" + +[[package]] +name = "hashbrown" +version = "0.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a9bfc1af68b1726ea47d3d5109de126281def866b33970e10fbab11b5dafab3" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "indexmap" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "707907fe3c25f5424cce2cb7e1cbcafee6bdbe735ca90ef77c29e84591e5b9da" +dependencies = [ + "equivalent", + "hashbrown", +] + +[[package]] +name = "indoc" +version = "2.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b248f5224d1d606005e02c97f5aa4e88eeb230488bcc03bc9ca4d7991399f2b5" + +[[package]] +name = "itertools" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +dependencies = [ + "either", +] + +[[package]] +name = "kadmin" +version = "0.0.0" +dependencies = [ + "anyhow", + "kadmin-sys", + "pyo3", + "serial_test", + "thiserror", +] + +[[package]] +name = "kadmin-sys" +version = "0.0.0" +dependencies = [ + "bindgen", + "system-deps", +] + +[[package]] +name = "libc" +version = "0.2.161" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9489c2807c139ffd9c1794f4af0ebe86a828db53ecdc7fea2111d0fed085d1" + +[[package]] +name = "libloading" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4979f22fdb869068da03c9f7528f8297c6fd2606bc3a4affe42e6a823fdb8da4" +dependencies = [ + "cfg-if", + "windows-targets", +] + +[[package]] +name = "lock_api" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" +dependencies = [ + "autocfg", + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" + +[[package]] +name = "memchr" +version = "2.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" + +[[package]] +name = "memoffset" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" +dependencies = [ + "autocfg", +] + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "once_cell" +version = "1.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" + +[[package]] +name = "parking_lot" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-targets", +] + +[[package]] +name = "pkg-config" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2" + +[[package]] +name = "portable-atomic" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc9c68a3f6da06753e9335d63e27f6b9754dd1920d941135b7ea8224f141adb2" + +[[package]] +name = "prettyplease" +version = "0.2.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64d1ec885c64d0457d564db4ec299b2dae3f9c02808b8ad9c3a089c591b18033" +dependencies = [ + "proc-macro2", + "syn", +] + +[[package]] +name = "proc-macro2" +version = "1.0.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f139b0662de085916d1fb67d2b4169d1addddda1919e696f3252b740b629986e" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "pyo3" +version = "0.22.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d922163ba1f79c04bc49073ba7b32fd5a8d3b76a87c955921234b8e77333c51" +dependencies = [ + "cfg-if", + "indoc", + "libc", + "memoffset", + "once_cell", + "portable-atomic", + "pyo3-build-config", + "pyo3-ffi", + "pyo3-macros", + "unindent", +] + +[[package]] +name = "pyo3-build-config" +version = "0.22.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc38c5feeb496c8321091edf3d63e9a6829eab4b863b4a6a65f26f3e9cc6b179" +dependencies = [ + "once_cell", + "target-lexicon", +] + +[[package]] +name = "pyo3-ffi" +version = "0.22.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94845622d88ae274d2729fcefc850e63d7a3ddff5e3ce11bd88486db9f1d357d" +dependencies = [ + "libc", + "pyo3-build-config", +] + +[[package]] +name = "pyo3-macros" +version = "0.22.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e655aad15e09b94ffdb3ce3d217acf652e26bbc37697ef012f5e5e348c716e5e" +dependencies = [ + "proc-macro2", + "pyo3-macros-backend", + "quote", + "syn", +] + +[[package]] +name = "pyo3-macros-backend" +version = "0.22.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae1e3f09eecd94618f60a455a23def79f79eba4dc561a97324bf9ac8c6df30ce" +dependencies = [ + "heck", + "proc-macro2", + "pyo3-build-config", + "quote", + "syn", +] + +[[package]] +name = "python-kadmin-rs" +version = "0.0.0" +dependencies = [ + "kadmin", + "pyo3", +] + +[[package]] +name = "quote" +version = "1.0.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "redox_syscall" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b6dfecf2c74bce2466cabf93f6664d6998a69eb21e39f4207930065b27b771f" +dependencies = [ + "bitflags", +] + +[[package]] +name = "regex" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "368758f23274712b504848e9d5a6f010445cc8b87a7cdb4d7cbee666c1288da3" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" + +[[package]] +name = "rustc-hash" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" + +[[package]] +name = "scc" +version = "2.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8d25269dd3a12467afe2e510f69fb0b46b698e5afb296b59f2145259deaf8e8" +dependencies = [ + "sdd", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "sdd" +version = "3.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49c1eeaf4b6a87c7479688c6d52b9f1153cedd3c489300564f932b065c6eab95" + +[[package]] +name = "serde" +version = "1.0.214" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f55c3193aca71c12ad7890f1785d2b73e1b9f63a0bbc353c08ef26fe03fc56b5" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.214" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de523f781f095e28fa605cdce0f8307e451cc0fd14e2eb4cd2e98a355b147766" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_spanned" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87607cb1398ed59d48732e575a4c28a7a8ebf2454b964fe3f224f2afc07909e1" +dependencies = [ + "serde", +] + +[[package]] +name = "serial_test" +version = "3.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b4b487fe2acf240a021cf57c6b2b4903b1e78ca0ecd862a71b71d2a51fed77d" +dependencies = [ + "log", + "once_cell", + "parking_lot", + "scc", + "serial_test_derive", +] + +[[package]] +name = "serial_test_derive" +version = "3.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82fe9db325bcef1fbcde82e078a5cc4efdf787e96b3b9cf45b50b529f2083d67" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "smallvec" +version = "1.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" + +[[package]] +name = "syn" +version = "2.0.85" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5023162dfcd14ef8f32034d8bcd4cc5ddc61ef7a247c024a33e24e1f24d21b56" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "system-deps" +version = "7.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66d23aaf9f331227789a99e8de4c91bf46703add012bdfd45fdecdfb2975a005" +dependencies = [ + "cfg-expr", + "heck", + "pkg-config", + "toml", + "version-compare", +] + +[[package]] +name = "target-lexicon" +version = "0.12.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" + +[[package]] +name = "thiserror" +version = "1.0.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d11abd9594d9b38965ef50805c5e469ca9cc6f197f883f717e0269a3057b3d5" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae71770322cbd277e69d762a16c444af02aa0575ac0d174f0b9562d3b37f8602" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "toml" +version = "0.8.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1ed1f98e3fdc28d6d910e6737ae6ab1a93bf1985935a1193e68f93eeb68d24e" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime", + "toml_edit", +] + +[[package]] +name = "toml_datetime" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_edit" +version = "0.22.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ae48d6208a266e853d946088ed816055e556cc6028c5e8e2b84d9fa5dd7c7f5" +dependencies = [ + "indexmap", + "serde", + "serde_spanned", + "toml_datetime", + "winnow", +] + +[[package]] +name = "unicode-ident" +version = "1.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e91b56cd4cadaeb79bbf1a5645f6b4f8dc5bde8834ad5894a8db35fda9efa1fe" + +[[package]] +name = "unindent" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7de7d73e1754487cb58364ee906a499937a0dfabd86bcb980fa99ec8c8fa2ce" + +[[package]] +name = "version-compare" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "852e951cb7832cb45cb1169900d19760cfa39b82bc0ea9c0e5a14ae88411c98b" + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "winnow" +version = "0.6.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36c1fec1a2bb5866f07c25f68c26e565c4c200aebb96d7e55710c19d3e8ac49b" +dependencies = [ + "memchr", +] diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..5692a88 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,41 @@ +[workspace] +members = ["kadmin-sys", "kadmin", "python-kadmin-rs"] +resolver = "2" + +[workspace.package] +authors = [ + "Marc 'risson' Schmitt ", + "authentik community ", +] +edition = "2021" +rust-version = "1.77" +readme = "README.md" +homepage = "https://github.com/authentik-community/kadmin-rs" +repository = "https://github.com/authentik-community/kadmin-rs.git" +license = "MIT" + +[workspace.lints.rust] +semicolon_in_expressions_from_macros = "warn" +unreachable_pub = "warn" +unused_import_braces = "warn" +unused_qualifications = "warn" + +[workspace.lints.clippy] +branches_sharing_code = "warn" +cloned_instead_of_copied = "warn" +dbg_macro = "warn" +disallowed_types = "warn" +empty_line_after_outer_attr = "warn" +exhaustive_enums = "warn" +exhaustive_structs = "warn" +inefficient_to_string = "warn" +macro_use_imports = "warn" +map_flatten = "warn" +missing_enforced_import_renames = "warn" +mut_mut = "warn" +nonstandard_macro_braces = "warn" +semicolon_if_nothing_returned = "warn" +str_to_string = "warn" +todo = "warn" +unreadable_literal = "warn" +unseparated_literal_suffix = "warn" diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..a49ca04 --- /dev/null +++ b/LICENSE @@ -0,0 +1,22 @@ +MIT License + +Copyright (c) 2024 Marc 'risson' Schmitt +Copyright (c) 2024 authentik community + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..ecbba32 --- /dev/null +++ b/README.md @@ -0,0 +1,102 @@ +# Rust and Python bindings for the Kerberos administration interface (kadm5) + +This repository contains both a work-in-progress safe, idiomatic Rust bindings for libkadm5, the library to administrate a Kerberos realm that supports the Kerberos administration interface (mainly Heimdal and MIT Kerberos 5), and the underlying "unsafe" bindings generated by bindgen in kadmin-sys. + +It also contains a Python API to those bindings. + +### Kerberos implementations compatibility + +These libraries will only compile against MIT krb5. However, they will allow you to communicate with an MIT krb5 KDC as well as a Heimdal KDC. In fact, these libraries are tested against both! + +## kadmin-sys + +![Crates.io Version](https://img.shields.io/crates/v/kadmin-sys) +![docs.rs](https://img.shields.io/docsrs/kadmin-sys) + +These are the raw bindings to libkadm5. This crate offers two features, `client` and `server`. You must choose one of them depending on how your application is going to interact with the KDC. By default, `client` is enabled. + +- `client`: links against `kadm5clnt`. Use this is you plan to remotely access the KDC, using kadmind's GSS-API RPC interface, like the CLI tool `kadmin` does. +- `server`: links against `kadm5srv`. Use this is you plan to directly edit the KDB from the machine where the KDC is running, like the CLI tool `kadmin.local` does. + +## kadmin + +![Crates.io Version](https://img.shields.io/crates/v/kadmin) +![docs.rs](https://img.shields.io/docsrs/kadmin) + +This is a safe, idiomatic Rust interface to libkadm5. This crate offers two features, `client` and `local`. They are similar to how kadmin-sys behaves. You should only enable one of them. + +With the `client` feature: + +```rust +use kadmin::KAdmin; + +let princ = "user/admin@EXAMPLE.ORG"; +let password = "vErYsEcUrE"; + +let kadmin = KAdmin::builder().with_password(&princ, &password).unwrap(); + +dbg!("{}", kadmin.list_principals("*").unwrap()); +``` + +With the `local` feature: + +```rust +use kadmin::KAdmin; + +let princ = "user/admin@EXAMPLE.ORG"; +let password = "vErYsEcUrE"; + +let kadmin = KAdmin::builder().local().unwrap(); + +dbg!("{}", kadmin.list_principals("*").unwrap()); +``` + +#### About thread safety + +As far as I can tell, libkadm5 APIs are **not** thread safe. As such, the types provided by this crate are neither `Send` nor `Sync`. You _must not_ use those with threads. You can either create a `KAdmin` instance per thread, or use the `kadmin::sync::KAdmin` interface that spawns a thread and sends the various commands to it. The API is not exactly the same as the non-thread-safe one, but should be close enough that switching between one or the other is easy enough. Read more about this in the documentation of the crate. + +## python-kadmin-rs + +![PyPI - Version](https://img.shields.io/pypi/v/python-kadmin-rs) +![Read the Docs](https://img.shields.io/readthedocs/python-kadmin-rs) + +These are Python bindings to the above Rust library, using the `kadmin::sync` interface to ensure thread safety. It provides two Python modules: `kadmin` for remote operations, and `kadmin_local` for local operations. + +With `kadmin`: + +```python +import kadmin + +princ = "user/admin@EXAMPLE.ORG" +password = "vErYsEcUrE" +kadm = kadmin.KAdmin.with_password(princ, password) +print(kadm.list_principals("*")) +``` + +With `kadmin_local`: + +```python +import kadmin + +kadm = kadmin.KAdmin.with_local() +print(kadm.list_principals("*")) +``` + +## License + +Licensed under the [MIT License](./LICENSE). + +## Contributing + +Just open a PR. + +### Releasing + +1. Go to [Actions > Create release PR](https://github.com/authentik-community/kadmin-rs/actions/workflows/release-pr.yml) +2. Click "Run workflow" and select what you need to release and input the new version. +3. Wait for the PR to be opened and the CI to pass +4. Merge the PR. +5. Go to [Releases](https://github.com/authentik-community/kadmin-rs/releases) +6. Edit the created release. +7. Click "Generate release notes" +8. Publish diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000..a670756 --- /dev/null +++ b/flake.lock @@ -0,0 +1,210 @@ +{ + "nodes": { + "flake-utils": { + "inputs": { + "systems": "systems_2" + }, + "locked": { + "lastModified": 1726560853, + "narHash": "sha256-X6rJYSESBVr3hBoH0WbKE5KvhPU5bloyZ2L4K60/fPQ=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "c1dfcf08411b08f6b8615f7d8971a2bfa81d5e8a", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, + "futils": { + "inputs": { + "systems": "systems" + }, + "locked": { + "lastModified": 1726560853, + "narHash": "sha256-X6rJYSESBVr3hBoH0WbKE5KvhPU5bloyZ2L4K60/fPQ=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "c1dfcf08411b08f6b8615f7d8971a2bfa81d5e8a", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, + "nix-github-actions": { + "inputs": { + "nixpkgs": [ + "poetry2nix", + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1729742964, + "narHash": "sha256-B4mzTcQ0FZHdpeWcpDYPERtyjJd/NIuaQ9+BV1h+MpA=", + "owner": "nix-community", + "repo": "nix-github-actions", + "rev": "e04df33f62cdcf93d73e9a04142464753a16db67", + "type": "github" + }, + "original": { + "owner": "nix-community", + "repo": "nix-github-actions", + "type": "github" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1730272153, + "narHash": "sha256-B5WRZYsRlJgwVHIV6DvidFN7VX7Fg9uuwkRW9Ha8z+w=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "2d2a9ddbe3f2c00747398f3dc9b05f7f2ebb0f53", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixpkgs-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "nixpkgs_2": { + "locked": { + "lastModified": 1728538411, + "narHash": "sha256-f0SBJz1eZ2yOuKUr5CA9BHULGXVSn6miBuUWdTyhUhU=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "b69de56fac8c2b6f8fd27f2eca01dcda8e0a4221", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixpkgs-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "poetry2nix": { + "inputs": { + "flake-utils": "flake-utils", + "nix-github-actions": "nix-github-actions", + "nixpkgs": [ + "nixpkgs" + ], + "systems": "systems_3", + "treefmt-nix": "treefmt-nix" + }, + "locked": { + "lastModified": 1730284601, + "narHash": "sha256-eHYcKVLIRRv3J1vjmxurS6HVdGphB53qxUeAkylYrZY=", + "owner": "nix-community", + "repo": "poetry2nix", + "rev": "43a898b4d76f7f3f70df77a2cc2d40096bc9d75e", + "type": "github" + }, + "original": { + "owner": "nix-community", + "repo": "poetry2nix", + "type": "github" + } + }, + "root": { + "inputs": { + "futils": "futils", + "nixpkgs": "nixpkgs", + "poetry2nix": "poetry2nix", + "rust-overlay": "rust-overlay" + } + }, + "rust-overlay": { + "inputs": { + "nixpkgs": "nixpkgs_2" + }, + "locked": { + "lastModified": 1730687492, + "narHash": "sha256-xQVadjquBA/tFxDt5A55LJ1D1AvkVWsnrKC2o+pr8F4=", + "owner": "oxalica", + "repo": "rust-overlay", + "rev": "41814763a2c597755b0755dbe3e721367a5e420f", + "type": "github" + }, + "original": { + "owner": "oxalica", + "repo": "rust-overlay", + "type": "github" + } + }, + "systems": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } + }, + "systems_2": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } + }, + "systems_3": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "id": "systems", + "type": "indirect" + } + }, + "treefmt-nix": { + "inputs": { + "nixpkgs": [ + "poetry2nix", + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1730120726, + "narHash": "sha256-LqHYIxMrl/1p3/kvm2ir925tZ8DkI0KA10djk8wecSk=", + "owner": "numtide", + "repo": "treefmt-nix", + "rev": "9ef337e492a5555d8e17a51c911ff1f02635be15", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "treefmt-nix", + "type": "github" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..c7516fd --- /dev/null +++ b/flake.nix @@ -0,0 +1,62 @@ +{ + inputs = { + nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable"; + rust-overlay.url = "github:oxalica/rust-overlay"; + poetry2nix = { + url = "github:nix-community/poetry2nix"; + inputs.nixpkgs.follows = "nixpkgs"; + }; + futils.url = "github:numtide/flake-utils"; + }; + + outputs = { + self, + nixpkgs, + rust-overlay, + poetry2nix, + futils, + } @ inputs: let + inherit (nixpkgs) lib; + inherit (futils.lib) eachDefaultSystem defaultSystems; + + nixpkgsFor = lib.genAttrs defaultSystems (system: + import nixpkgs { + inherit system; + overlays = [ + rust-overlay.overlays.default + poetry2nix.overlays.default + ]; + }); + in + eachDefaultSystem (system: let + pkgs = nixpkgsFor.${system}; + in { + devShell = pkgs.mkShell { + buildInputs = with pkgs; [ + (lib.hiPrio rust-bin.nightly.latest.rustfmt) + (rust-bin.fromRustupToolchainFile ./rust-toolchain.toml) + + poetry + python3Full + + clang + glibc + krb5.dev + krb5.out + libclang + openssl + pkg-config + + cargo-msrv + cargo-release + git + just + valgrind + ]; + + RUST_SRC_PATH = "${pkgs.rust.packages.stable.rustPlatform.rustLibSrc}"; + RUST_BACKTRACE = 1; + LIBCLANG_PATH = "${pkgs.libclang.lib}/lib"; + }; + }); +} diff --git a/justfile b/justfile new file mode 100644 index 0000000..386e501 --- /dev/null +++ b/justfile @@ -0,0 +1,145 @@ +# List available commands +default: + just --list + +# Auto format code +lint-fix: + cargo fmt + black python-kadmin-rs + ruff check --fix python-kadmin-rs +[private] +ci-lint-rustfmt: + cargo fmt --check +[private] +ci-lint-black: + black --check python-kadmin-rs +[private] +ci-lint-ruff: + ruff check python-kadmin-rs + +# Lint code +lint-rust: + cd kadmin-sys && cargo clippy --features client + cd kadmin-sys && cargo clippy --features server + cd kadmin && cargo clippy + cd kadmin && cargo clippy --no-default-features --features local + cd python-kadmin-rs && cargo clippy + cd python-kadmin-rs && cargo clippy --no-default-features --features local +[private] +ci-lint-clippy: ci-build-deps + RUSTFLAGS="-Dwarnings" just lint-rust + +# Mypy types checking +lint-mypy: install-python + stubtest kadmin kadmin_local +[private] +ci-lint-mypy: ci-build-deps lint-mypy + +alias l := lint +# Lint and auto format +lint: lint-fix lint-rust + +alias la := lint-all +# Common lint plus mypy types checking +lint-all: lint lint-mypy + +alias b := build-rust +# Build all rust crates +build-rust: + cd kadmin-sys && cargo build --features client + cd kadmin-sys && cargo build --features server + cd kadmin && cargo build + cd kadmin && cargo build --no-default-features --features local + cd python-kadmin-rs && cargo build + cd python-kadmin-rs && cargo build --no-default-features --features local +[private] +ci-build-deps: + sudo apt-get update + sudo apt-get install -y --no-install-recommends libkrb5-dev krb5-multidev +[private] +ci-build-rust: ci-build-deps + RUSTFLAGS="-Dwarnings" just build-rust + +# Build python wheel +build-python: + python -m build +[private] +ci-build-python: ci-build-deps build-python +[private] +ci-build-python-sdist: + python -m build --sdist + +# Build rust crates and python wheel +build: build-rust build-python + +# Test kadmin-sys crate +test-kadmin-sys: + cd kadmin-sys && cargo test --features client + cd kadmin-sys && cargo test --features server + +# Test kadmin crate +test-kadmin: + cd kadmin && cargo test + cd kadmin && cargo test --no-default-features --features local + +test-python-kadmin-rs: + cd python-kadmin-rs && cargo test + cd python-kadmin-rs && cargo test --no-default-features --features local + +alias t := test-rust +# Test all rust crates +test-rust: test-kadmin-sys test-kadmin test-python-kadmin-rs +[private] +ci-test-deps: + sudo apt-get install -y --no-install-recommends valgrind +[private] +ci-test-deps-mit: ci-build-deps ci-test-deps + sudo apt-get install -y --no-install-recommends krb5-kdc krb5-user krb5-admin-server +[private] +ci-test-rust: ci-test-deps-mit + RUSTFLAGS="-Dwarnings" just test-rust + +alias ts := test-sanity +# Test kadmin with valgrind for memory leaks +test-sanity: + cd kadmin && \ + CARGO_TARGET_X86_64_UNKNOWN_LINUX_GNU_RUNNER="valgrind --error-exitcode=1 --suppressions=tests/valgrind.supp -s --leak-check=full" \ + cargo test + cd kadmin && \ + CARGO_TARGET_X86_64_UNKNOWN_LINUX_GNU_RUNNER="valgrind --error-exitcode=1 --suppressions=tests/valgrind.supp -s --leak-check=full" \ + cargo test --no-default-features --features local +[private] +ci-test-sanity: ci-test-deps-mit + just test-sanity + +_test-python: + python -m unittest python-kadmin-rs/tests/test_*.py +# Test python bindings +test-python: install-python _test-python +[private] +ci-test-deps-h5l: ci-test-deps + sudo apt-get install -y --no-install-recommends libkrb5-3 libkadm5clnt-mit12 libkadm5srv-mit12 heimdal-dev heimdal-servers heimdal-kdc +[private] +ci-test-python-mit: ci-test-deps-mit _install-python _test-python +ci-test-python-h5l: ci-test-deps-h5l _install-python _test-python + +# Test rust crates and python bindings +test-all: test-rust test-sanity test-python +alias ta := test-all + +_install-python: + pip install --force-reinstall dist/python_kadmin_rs-*.whl +# Build and install wheel +install-python: clean-python build-python _install-python + +# Cleanup rust build directory +clean-rust: + rm -rf target + +# Cleanup python wheel builds +clean-python: + pip uninstall -y python-kadmin-rs + rm -rf dist wheelhouse + +# Cleanup all +clean: clean-rust clean-python diff --git a/kadmin-sys/Cargo.toml b/kadmin-sys/Cargo.toml new file mode 100644 index 0000000..e9453d7 --- /dev/null +++ b/kadmin-sys/Cargo.toml @@ -0,0 +1,37 @@ +[package] +name = "kadmin-sys" +version = "0.0.0" +authors.workspace = true +edition.workspace = true +rust-version.workspace = true +description = "FFI bindings for the Kerberos administration interface (kadm5)" +readme.workspace = true +homepage.workspace = true +repository.workspace = true +license.workspace = true +keywords = ["kerberos", "krb5", "kadm5", "kadmin"] +categories = ["external-ffi-bindings", "authentication"] + +links = "kadm5" + +[package.metadata.system-deps] +krb5 = "*" +kadm5clnt = { name = "kadm-client", version = "*", feature = "client", fallback-names = [ + "kadm5clnt", + "kadm5clnt_mit", +] } +kadm5srv = { name = "kadm-server", version = "*", feature = "server", fallback-names = [ + "kadm5srv", + "kadm5srv_mit", +] } + +[features] +default = ["client"] +client = [] +server = [] + +[dependencies] + +[build-dependencies] +bindgen = "0.70" +system-deps = "7.0" diff --git a/kadmin-sys/build.rs b/kadmin-sys/build.rs new file mode 100644 index 0000000..c010112 --- /dev/null +++ b/kadmin-sys/build.rs @@ -0,0 +1,43 @@ +use std::{env, path::PathBuf}; + +fn main() { + let deps = system_deps::Config::new().probe().unwrap(); + + let mut builder = bindgen::builder() + .header("src/wrapper.h") + .allowlist_type("(_|)kadm5.*") + .allowlist_function("kadm5.*") + .allowlist_var("KADM5_.*") + .allowlist_var("KRB5_NT_SRV_HST") + .allowlist_var("KRB5_OK") + .allowlist_function("krb5_init_context") + .allowlist_function("krb5_free_context") + .allowlist_function("krb5_get_error_message") + .allowlist_function("krb5_free_error_message") + .allowlist_function("krb5_parse_name") + .allowlist_function("krb5_sname_to_principal") + .allowlist_function("krb5_free_principal") + .allowlist_function("krb5_unparse_name") + .allowlist_function("krb5_free_unparsed_name") + .allowlist_function("krb5_cc_get_principal") + .allowlist_function("krb5_cc_default") + .allowlist_function("krb5_cc_resolve") + .allowlist_function("krb5_cc_close") + .allowlist_function("krb5_get_default_realm") + .allowlist_function("krb5_free_default_realm") + .clang_arg("-fparse-all-comments") + .derive_default(true) + .generate_cstr(true) + .parse_callbacks(Box::new(bindgen::CargoCallbacks::new())); + + for include_path in deps.all_include_paths() { + builder = builder.clang_arg(format!("-I{}", include_path.display())); + } + + let bindings = builder.generate().expect("Unable to generate bindings"); + + let out_path = PathBuf::from(env::var("OUT_DIR").unwrap()); + bindings + .write_to_file(out_path.join("bindings.rs")) + .expect("Couldn't write bindings!"); +} diff --git a/kadmin-sys/src/lib.rs b/kadmin-sys/src/lib.rs new file mode 100644 index 0000000..abec63a --- /dev/null +++ b/kadmin-sys/src/lib.rs @@ -0,0 +1,45 @@ +//! # Raw bindings to libkadm5 +//! +//! This crate providers raw bindings to libkadm5. +//! +//! These bindings are generated by [bindgen](https://docs.rs/bindgen) by including `kadm5/admin.h`. The types provided +//! are filtered to only import required symbols for kadm5. In the future, this crate may also allow for more symbols +//! that may be useful, such as error types. +//! +//! This crate links against libkrb5 plus the required kadm5 library depending on the feature +//! selected (see below). +//! +//! By default, those include headers and libraries are found using pkg-config. You can override this behavior with the +//! following environment variables (which must be paths to directories containing the required libraries and header +//! files): +//! +//! - `SYSTEM_DEPS_KRB5_SEARCH_NATIVE` +//! - `SYSTEM_DEPS_KRB5_INCLUDE` +//! - `SYSTEM_DEPS_KADM5CLNT_SEARCH_NATIVE` +//! - `SYSTEM_DEPS_KADM5CLNT_INCLUDE` +//! - `SYSTEM_DEPS_KADM5SRV_SEARCH_NATIVE` +//! - `SYSTEM_DEPS_KADM5SRV_INCLUDE` +//! +//! You can read more about this in the [system-deps documentation](https://docs.rs/system-deps). +//! +//! # Features +//! +//! This crate offers two features, client and server. You must choose one of them depending on how your application is +//! going to interact with the KDC. By default, `client` is enabled. +//! +//! - `client`: links against `kadm5clnt`. Use this is you plan to remotely access the KDC, using kadmind's GSS-API RPC +//! interface, like the CLI tool `kadmin` does. +//! - `server`: links against `kadm5srv`. Use this is you plan to directly edit the KDB from the machine where the KDC +//! is running, like the CLI tool `kadmin.local` does. + +#![allow(non_upper_case_globals)] +#![allow(non_camel_case_types)] +#![allow(non_snake_case)] + +#[cfg(all(feature = "client", feature = "server", not(docsrs)))] +compile_error!("Feature \"client\" and feature \"server\" cannot be enabled at the same time."); + +#[cfg(all(not(feature = "client"), not(feature = "server"), not(docsrs)))] +compile_error!("Exactly one of feature \"client\" or feature \"server\" must be selected."); + +include!(concat!(env!("OUT_DIR"), "/bindings.rs")); diff --git a/kadmin-sys/src/wrapper.h b/kadmin-sys/src/wrapper.h new file mode 100644 index 0000000..079c01d --- /dev/null +++ b/kadmin-sys/src/wrapper.h @@ -0,0 +1,2 @@ +#include +const krb5_error_code KRB5_OK = 0; diff --git a/kadmin/Cargo.toml b/kadmin/Cargo.toml new file mode 100644 index 0000000..6fb4bd8 --- /dev/null +++ b/kadmin/Cargo.toml @@ -0,0 +1,33 @@ +[package] +name = "kadmin" +version = "0.0.0" +authors.workspace = true +edition.workspace = true +rust-version.workspace = true +description = "Rust bindings for the Kerberos administration interface (kadm5)" +readme.workspace = true +homepage.workspace = true +repository.workspace = true +license.workspace = true +keywords = ["kerberos", "krb5", "kadm5", "kadmin"] +categories = ["api-bindings", "authentication"] + +[features] +default = ["client"] +client = ["kadmin-sys/client"] +local = ["kadmin-sys/server"] + +[dependencies] +kadmin-sys = { path = "../kadmin-sys", version = "0.0.0", default-features = false } +thiserror = "1" + +[dev-dependencies] +anyhow = "1" +pyo3 = { version = "0.22", features = ["auto-initialize"] } +serial_test = { version = "3.1", default-features = false, features = [ + "log", + "logging", +] } + +[lints] +workspace = true diff --git a/kadmin/src/context.rs b/kadmin/src/context.rs new file mode 100644 index 0000000..648ec9c --- /dev/null +++ b/kadmin/src/context.rs @@ -0,0 +1,134 @@ +use std::{ + ffi::{CStr, CString}, + mem::MaybeUninit, + os::raw::c_char, + ptr::null_mut, + sync::Mutex, +}; + +use kadmin_sys::*; + +use crate::{ + error::{Result, krb5_error_code_escape_hatch}, + strconv::c_string_to_string, +}; + +static CONTEXT_INIT_LOCK: Mutex<()> = Mutex::new(()); + +#[derive(Debug)] +pub struct KAdminContext { + pub(crate) context: krb5_context, + pub(crate) default_realm: Option, +} + +impl KAdminContext { + pub fn new() -> Result { + Self::builder().build() + } + + pub fn builder() -> KAdminContextBuilder { + KAdminContextBuilder::default() + } + + fn fill_default_realm(&mut self) { + self.default_realm = { + let mut raw_default_realm: *mut c_char = null_mut(); + let code = unsafe { krb5_get_default_realm(self.context, &mut raw_default_realm) }; + match code { + KRB5_OK => { + let default_realm = unsafe { CStr::from_ptr(raw_default_realm) }.to_owned(); + unsafe { + krb5_free_default_realm(self.context, raw_default_realm); + } + Some(default_realm) + } + _ => None, + } + }; + } + + pub(crate) fn error_code_to_message(&self, code: krb5_error_code) -> String { + let message: *const c_char = unsafe { krb5_get_error_message(self.context, code) }; + + match c_string_to_string(message) { + Ok(string) => { + unsafe { krb5_free_error_message(self.context, message) }; + string + } + Err(error) => error.to_string(), + } + } +} + +#[derive(Debug, Default)] +pub struct KAdminContextBuilder { + context: Option, +} + +impl KAdminContextBuilder { + pub unsafe fn context(mut self, context: krb5_context) -> Self { + self.context = Some(context); + self + } + + pub fn build(self) -> Result { + if let Some(ctx) = self.context { + let mut context = KAdminContext { + context: ctx, + default_realm: None, + }; + context.fill_default_realm(); + return Ok(context); + } + + let _guard = CONTEXT_INIT_LOCK + .lock() + .expect("Failed to lock context initialization."); + + let mut context_ptr: MaybeUninit = MaybeUninit::zeroed(); + + let code = unsafe { kadm5_init_krb5_context(context_ptr.as_mut_ptr()) }; + let mut context = KAdminContext { + context: unsafe { context_ptr.assume_init() }, + default_realm: None, + }; + krb5_error_code_escape_hatch(&context, code)?; + context.fill_default_realm(); + Ok(context) + } +} + +impl Drop for KAdminContext { + fn drop(&mut self) { + let _guard = CONTEXT_INIT_LOCK + .lock() + .expect("Failed to lock context for de-initialization."); + + unsafe { krb5_free_context(self.context) }; + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn new() { + let context = KAdminContext::new(); + assert!(context.is_ok()); + } + + #[test] + fn error_code_to_message() { + let context = KAdminContext::new().unwrap(); + let message = context.error_code_to_message(-1765328384); + assert_eq!(message, "No error".to_string()); + } + + #[test] + fn error_code_to_message_wrong_code() { + let context = KAdminContext::new().unwrap(); + let message = context.error_code_to_message(-1); + assert_eq!(message, "Unknown code ____ 255".to_string()); + } +} diff --git a/kadmin/src/db_args.rs b/kadmin/src/db_args.rs new file mode 100644 index 0000000..e111122 --- /dev/null +++ b/kadmin/src/db_args.rs @@ -0,0 +1,96 @@ +use std::{ffi::CString, os::raw::c_char, ptr::null_mut}; + +use crate::error::Result; + +#[derive(Debug)] +pub struct KAdminDbArgs { + pub(crate) db_args: *mut *mut c_char, + + // Additional fields to store transient strings so the pointer stored in db_args + // doesn't become invalid while this struct lives. + _origin_args: Vec, + _ptr_vec: Vec<*mut c_char>, +} + +impl KAdminDbArgs { + pub fn builder() -> KAdminDbArgsBuilder { + KAdminDbArgsBuilder::default() + } +} + +impl Default for KAdminDbArgs { + fn default() -> Self { + Self::builder().build().unwrap() + } +} + +#[derive(Clone, Debug, Default)] +pub struct KAdminDbArgsBuilder(Vec<(String, Option)>); + +impl KAdminDbArgsBuilder { + pub fn arg(mut self, name: &str, value: Option<&str>) -> Self { + self.0.push((name.to_owned(), value.map(|s| s.to_owned()))); + self + } + + pub fn build(&self) -> Result { + let formatted_args = self.0.clone().into_iter().map(|(name, value)| { + if let Some(value) = value { + format!("{name}={value}") + } else { + name + } + }); + let mut _origin_args = vec![]; + let mut _ptr_vec = vec![]; + for arg in formatted_args { + let c_arg = CString::new(arg)?; + _ptr_vec.push(c_arg.as_ptr().cast_mut()); + _origin_args.push(c_arg); + } + // Null terminated + _ptr_vec.push(null_mut()); + + let db_args = _ptr_vec.as_mut_ptr(); + + Ok(KAdminDbArgs { + db_args, + _origin_args, + _ptr_vec, + }) + } +} + +#[cfg(test)] +mod tests { + use std::ffi::CStr; + + use super::*; + + #[test] + fn build_empty() { + let db_args = KAdminDbArgs::builder().build().unwrap(); + + unsafe { + assert_eq!(*db_args.db_args, null_mut()); + } + } + + #[test] + fn build_no_value() { + let db_args = KAdminDbArgs::builder().arg("lockiter", None).build().unwrap(); + assert_eq!( + unsafe { CStr::from_ptr(*db_args.db_args).to_owned() }, + CString::new("lockiter").unwrap() + ); + } + + #[test] + fn build_with_value() { + let db_args = KAdminDbArgs::builder().arg("host", Some("ldap.test")).build().unwrap(); + assert_eq!( + unsafe { CStr::from_ptr(*db_args.db_args).to_owned() }, + CString::new("host=ldap.test").unwrap() + ); + } +} diff --git a/kadmin/src/error.rs b/kadmin/src/error.rs new file mode 100644 index 0000000..f9af117 --- /dev/null +++ b/kadmin/src/error.rs @@ -0,0 +1,132 @@ +use kadmin_sys::*; + +use crate::context::KAdminContext; + +#[derive(thiserror::Error, Debug)] +#[non_exhaustive] +pub enum Error { + #[error("Kerberos error: {message} (code: {code})")] + Kerberos { code: krb5_error_code, message: String }, + + #[error("KAdmin error: {message} (code: {code})")] + KAdmin { code: kadm5_ret_t, message: String }, + + #[error("NULL pointer dereference error")] + NullPointerDereference, + + #[error(transparent)] + CStringConversion(#[from] std::ffi::IntoStringError), + #[error(transparent)] + CStringImportFromVec(#[from] std::ffi::FromVecWithNulError), + #[error(transparent)] + StringConversion(#[from] std::ffi::NulError), + #[error("Failed to send operation to executor")] + ThreadSendError, + #[error("Failed to receive result from executor")] + ThreadRecvError(#[from] std::sync::mpsc::RecvError), +} + +impl From> for Error { + fn from(_error: std::sync::mpsc::SendError) -> Self { + Self::ThreadSendError + } +} + +pub type Result = std::result::Result; + +pub(crate) fn krb5_error_code_escape_hatch(context: &KAdminContext, code: krb5_error_code) -> Result<()> { + if code == 0 { + Ok(()) + } else { + Err(Error::Kerberos { + code, + message: context.error_code_to_message(code), + }) + } +} + +pub(crate) fn kadm5_ret_t_escape_hatch(code: kadm5_ret_t) -> Result<()> { + if code == KADM5_OK as kadm5_ret_t { + return Ok(()); + } + let message = match code as u32 { + KADM5_FAILURE => "Operation failed for unspecified reason", + KADM5_AUTH_GET => "Operation requires ``get'' privilege", + KADM5_AUTH_ADD => "Operation requires ``add'' privilege", + KADM5_AUTH_MODIFY => "Operation requires ``modify'' privilege", + KADM5_AUTH_DELETE => "Operation requires ``delete'' privilege", + KADM5_AUTH_INSUFFICIENT => "Insufficient authorization for operation", + KADM5_BAD_DB => "Database inconsistency detected", + KADM5_DUP => "Principal or policy already exists", + KADM5_RPC_ERROR => "Communication failure with server", + KADM5_NO_SRV => "No administration server found for realm", + KADM5_BAD_HIST_KEY => "Password history principal key version mismatch", + KADM5_NOT_INIT => "Connection to server not initialized", + KADM5_UNK_PRINC => "Principal does not exist", + KADM5_UNK_POLICY => "Policy does not exist", + KADM5_BAD_MASK => "Invalid field mask for operation", + KADM5_BAD_CLASS => "Invalid number of character classes", + KADM5_BAD_LENGTH => "Invalid password length", + KADM5_BAD_POLICY => "Illegal policy name", + KADM5_BAD_PRINCIPAL => "Illegal principal name", + KADM5_BAD_AUX_ATTR => "Invalid auxillary attributes", + KADM5_BAD_HISTORY => "Invalid password history count", + KADM5_BAD_MIN_PASS_LIFE => "Password minimum life is greater then password maximum life", + KADM5_PASS_Q_TOOSHORT => "Password is too short", + KADM5_PASS_Q_CLASS => "Password does not contain enough character classes", + KADM5_PASS_Q_DICT => "Password is in the password dictionary", + KADM5_PASS_REUSE => "Cannot reuse password", + KADM5_PASS_TOOSOON => "Current password's minimum life has not expired", + KADM5_POLICY_REF => "Policy is in use", + KADM5_INIT => "Connection to server already initialized", + KADM5_BAD_PASSWORD => "Incorrect password", + KADM5_PROTECT_PRINCIPAL => "Cannot change protected principal", + KADM5_BAD_SERVER_HANDLE => "Programmer error! Bad Admin server handle", + KADM5_BAD_STRUCT_VERSION => "Programmer error! Bad API structure version", + KADM5_OLD_STRUCT_VERSION => { + "API structure version specified by application is no longer supported (to fix, recompile application \ + against current Admin API header files and libraries)" + } + KADM5_NEW_STRUCT_VERSION => { + "API structure version specified by application is unknown to libraries (to fix, obtain current Admin API \ + header files and libraries and recompile application)" + } + KADM5_BAD_API_VERSION => "Programmer error! Bad API version", + KADM5_OLD_LIB_API_VERSION => { + "API version specified by application is no longer supported by libraries (to fix, update application to \ + adhere to current API version and recompile)" + } + KADM5_OLD_SERVER_API_VERSION => { + "API version specified by application is no longer supported by server (to fix, update application to \ + adhere to current API version and recompile)" + } + KADM5_NEW_LIB_API_VERSION => { + "API version specified by application is unknown to libraries (to fix, obtain current Admin API header \ + files and libraries and recompile application)" + } + KADM5_NEW_SERVER_API_VERSION => { + "API version specified by application is unknown to server (to fix, obtain and install newest Admin Server)" + } + KADM5_SECURE_PRINC_MISSING => "Database error! Required principal missing", + KADM5_NO_RENAME_SALT => "The salt type of the specified principal does not support renaming", + KADM5_BAD_CLIENT_PARAMS => "Illegal configuration parameter for remote KADM5 client", + KADM5_BAD_SERVER_PARAMS => "Illegal configuration parameter for local KADM5 client.", + KADM5_AUTH_LIST => "Operation requires ``list'' privilege", + KADM5_AUTH_CHANGEPW => "Operation requires ``change-password'' privilege", + KADM5_GSS_ERROR => "GSS-API (or Kerberos) error", + KADM5_BAD_TL_TYPE => "Programmer error! Illegal tagged data list element type", + KADM5_MISSING_CONF_PARAMS => "Required parameters in kdc.conf missing", + KADM5_BAD_SERVER_NAME => "Bad krb5 admin server hostname", + KADM5_AUTH_SETKEY => "Operation requires ``set-key'' privilege", + KADM5_SETKEY_DUP_ENCTYPES => "Multiple values for single or folded enctype", + KADM5_SETV4KEY_INVAL_ENCTYPE => "Invalid enctype for setv4key", + KADM5_SETKEY3_ETYPE_MISMATCH => "Mismatched enctypes for setkey3", + KADM5_MISSING_KRB5_CONF_PARAMS => "Missing parameters in krb5.conf required for kadmin client", + KADM5_XDR_FAILURE => "XDR encoding error", + KADM5_CANT_RESOLVE => "", + KADM5_PASS_Q_GENERIC => "Database synchronization failed", + _ => "Unknown error", + } + .to_owned(); + Err(Error::KAdmin { code, message }) +} diff --git a/kadmin/src/kadmin.rs b/kadmin/src/kadmin.rs new file mode 100644 index 0000000..a41b080 --- /dev/null +++ b/kadmin/src/kadmin.rs @@ -0,0 +1,396 @@ +#[cfg(feature = "client")] +use std::{ffi::CStr, mem::MaybeUninit}; +use std::{ + ffi::CString, + os::raw::{c_char, c_void}, + ptr::null_mut, + sync::Mutex, +}; + +use kadmin_sys::*; + +use crate::{ + context::KAdminContext, + db_args::KAdminDbArgs, + error::{Result, kadm5_ret_t_escape_hatch, krb5_error_code_escape_hatch}, + params::KAdminParams, + principal::Principal, + strconv::c_string_to_string, +}; + +static KADMIN_INIT_LOCK: Mutex<()> = Mutex::new(()); + +#[derive(Debug)] +pub struct KAdmin { + context: KAdminContext, + pub(crate) server_handle: *mut c_void, +} + +impl KAdmin { + pub fn builder() -> KAdminBuilder { + KAdminBuilder::default() + } + + // ank, addprinc, add_principal + pub fn add_principal() { + unimplemented!(); + } + + // delprinc, delete_principal + pub fn delete_principal() { + unimplemented!(); + } + + // modify_principal, modprinc + pub fn modify_principal() { + unimplemented!(); + } + + // rename_principal, renprinc + pub fn rename_principal() { + unimplemented!(); + } + + // get_principal, getprinc + #[allow(unreachable_code)] + #[allow(unused_variables)] + pub fn get_principal(&self, name: &str) -> Result>> { + unimplemented!(); + let mut temp_princ = null_mut(); + let name = CString::new(name)?; + let code = unsafe { krb5_parse_name(self.context.context, name.as_ptr().cast_mut(), &mut temp_princ) }; + krb5_error_code_escape_hatch(&self.context, code)?; + let mut principal_entry = Principal::new(self); + let code = unsafe { + kadm5_get_principal( + self.server_handle, + temp_princ, + &mut principal_entry.inner, + (KADM5_PRINCIPAL_NORMAL_MASK | KADM5_KEY_DATA) as i64, + ) + }; + unsafe { + krb5_free_principal(self.context.context, temp_princ); + } + if code == KADM5_UNK_PRINC as i64 { + return Ok(None); + } + kadm5_ret_t_escape_hatch(code)?; + Ok(Some(principal_entry)) + } + + // list_principals, listprincs, get_principals, getprincs + pub fn list_principals(&self, query: &str) -> Result> { + let query = CString::new(query)?; + let mut count = 0; + let mut princs: *mut *mut c_char = null_mut(); + let code = + unsafe { kadm5_get_principals(self.server_handle, query.as_ptr().cast_mut(), &mut princs, &mut count) }; + kadm5_ret_t_escape_hatch(code)?; + let mut result = Vec::with_capacity(count as usize); + for raw in unsafe { std::slice::from_raw_parts(princs, count as usize) }.iter() { + result.push(c_string_to_string(*raw)?); + } + unsafe { + kadm5_free_name_list(self.server_handle, princs, count); + } + Ok(result) + } + + // add_policy, addpol + pub fn add_policy() { + unimplemented!(); + } + + // modify_policy, modpol + pub fn modify_policy() { + unimplemented!(); + } + + // delete_policy, delpol + pub fn delete_policy() { + unimplemented!(); + } + + // get_policy, getpol + pub fn get_policy() { + unimplemented!(); + } + + // list_policies, listpols, get_policies, getpols + pub fn list_policies(&self, query: &str) -> Result> { + let query = CString::new(query)?; + let mut count = 0; + let mut policies: *mut *mut c_char = null_mut(); + let code = + unsafe { kadm5_get_policies(self.server_handle, query.as_ptr().cast_mut(), &mut policies, &mut count) }; + kadm5_ret_t_escape_hatch(code)?; + let mut result = Vec::with_capacity(count as usize); + for raw in unsafe { std::slice::from_raw_parts(policies, count as usize) }.iter() { + result.push(c_string_to_string(*raw)?); + } + unsafe { + kadm5_free_name_list(self.server_handle, policies, count); + } + Ok(result) + } + + // get_privs, getprivs + pub fn get_privs() { + unimplemented!(); + } +} + +impl Drop for KAdmin { + fn drop(&mut self) { + let _guard = KADMIN_INIT_LOCK + .lock() + .expect("Failed to lock kadmin for de-initialization."); + unsafe { + kadm5_flush(self.server_handle); + kadm5_destroy(self.server_handle); + } + } +} + +#[derive(Debug, Default)] +pub struct KAdminBuilder { + context: Option>, + params: Option, + db_args: Option, +} + +impl KAdminBuilder { + pub fn context(mut self, context: KAdminContext) -> Self { + self.context = Some(Ok(context)); + self + } + + pub fn params(mut self, params: KAdminParams) -> Self { + self.params = Some(params); + self + } + + pub fn db_args(mut self, db_args: KAdminDbArgs) -> Self { + self.db_args = Some(db_args); + self + } + + fn get_kadmin(self) -> Result<(KAdmin, KAdminParams, KAdminDbArgs)> { + let params = self.params.unwrap_or_default(); + let db_args = self.db_args.unwrap_or_default(); + let context = self.context.unwrap_or(KAdminContext::new())?; + let kadmin = KAdmin { + context, + server_handle: null_mut(), + }; + Ok((kadmin, params, db_args)) + } + + #[cfg(feature = "client")] + pub fn with_password(self, client_name: &str, password: &str) -> Result { + let _guard = KADMIN_INIT_LOCK.lock().expect("Failed to lock context initialization."); + + let (mut kadmin, params, db_args) = self.get_kadmin()?; + + let client_name = CString::new(client_name)?; + let password = CString::new(password)?; + let service_name = KADM5_ADMIN_SERVICE.to_owned(); + + let mut params = params; + + let code = unsafe { + kadm5_init_with_password( + kadmin.context.context, + client_name.as_ptr().cast_mut(), + password.as_ptr().cast_mut(), + service_name.as_ptr().cast_mut(), + &mut params.params, + KADM5_STRUCT_VERSION, + KADM5_API_VERSION_2, + db_args.db_args, + &mut kadmin.server_handle, + ) + }; + + kadm5_ret_t_escape_hatch(code)?; + + Ok(kadmin) + } + + #[cfg(feature = "client")] + pub fn with_keytab(self, client_name: Option<&str>, keytab: Option<&str>) -> Result { + let _guard = KADMIN_INIT_LOCK.lock().expect("Failed to lock context initialization."); + + let (mut kadmin, params, db_args) = self.get_kadmin()?; + + let client_name = if let Some(client_name) = client_name { + CString::new(client_name)? + } else { + let mut princ_ptr: MaybeUninit = MaybeUninit::zeroed(); + let code = unsafe { + krb5_sname_to_principal( + kadmin.context.context, + null_mut(), + CString::new("host")?.as_ptr().cast_mut(), + KRB5_NT_SRV_HST as i32, + princ_ptr.as_mut_ptr(), + ) + }; + krb5_error_code_escape_hatch(&kadmin.context, code)?; + let princ = unsafe { princ_ptr.assume_init() }; + let mut raw_client_name: *mut c_char = null_mut(); + let code = unsafe { krb5_unparse_name(kadmin.context.context, princ, &mut raw_client_name) }; + krb5_error_code_escape_hatch(&kadmin.context, code)?; + unsafe { + krb5_free_principal(kadmin.context.context, princ); + } + let client_name = unsafe { CStr::from_ptr(raw_client_name) }.to_owned(); + unsafe { + krb5_free_unparsed_name(kadmin.context.context, raw_client_name); + } + client_name + }; + let keytab = if let Some(keytab) = keytab { + CString::new(keytab)? + } else { + CString::new("/etc/krb5.keytab")? + }; + let service_name = KADM5_ADMIN_SERVICE.to_owned(); + + let mut params = params; + + let code = unsafe { + kadm5_init_with_skey( + kadmin.context.context, + client_name.as_ptr().cast_mut(), + keytab.as_ptr().cast_mut(), + service_name.as_ptr().cast_mut(), + &mut params.params, + KADM5_STRUCT_VERSION, + KADM5_API_VERSION_2, + db_args.db_args, + &mut kadmin.server_handle, + ) + }; + + kadm5_ret_t_escape_hatch(code)?; + + Ok(kadmin) + } + + #[cfg(feature = "client")] + pub fn with_ccache(self, client_name: Option<&str>, ccache_name: Option<&str>) -> Result { + let _guard = KADMIN_INIT_LOCK.lock().expect("Failed to lock context initialization."); + + let (mut kadmin, params, db_args) = self.get_kadmin()?; + + let ccache = { + let mut ccache: MaybeUninit = MaybeUninit::zeroed(); + let code = if let Some(ccache_name) = ccache_name { + let ccache_name = CString::new(ccache_name)?; + unsafe { + krb5_cc_resolve( + kadmin.context.context, + ccache_name.as_ptr().cast_mut(), + ccache.as_mut_ptr(), + ) + } + } else { + unsafe { krb5_cc_default(kadmin.context.context, ccache.as_mut_ptr()) } + }; + krb5_error_code_escape_hatch(&kadmin.context, code)?; + unsafe { ccache.assume_init() } + }; + + let client_name = if let Some(client_name) = client_name { + CString::new(client_name)? + } else { + let mut princ_ptr: MaybeUninit = MaybeUninit::zeroed(); + let code = unsafe { krb5_cc_get_principal(kadmin.context.context, ccache, princ_ptr.as_mut_ptr()) }; + krb5_error_code_escape_hatch(&kadmin.context, code)?; + let princ = unsafe { princ_ptr.assume_init() }; + let mut raw_client_name: *mut c_char = null_mut(); + let code = unsafe { krb5_unparse_name(kadmin.context.context, princ, &mut raw_client_name) }; + krb5_error_code_escape_hatch(&kadmin.context, code)?; + unsafe { + krb5_free_principal(kadmin.context.context, princ); + } + let client_name = unsafe { CStr::from_ptr(raw_client_name) }.to_owned(); + unsafe { + krb5_free_unparsed_name(kadmin.context.context, raw_client_name); + } + client_name + }; + let service_name = KADM5_ADMIN_SERVICE.to_owned(); + + let mut params = params; + + let code = unsafe { + kadm5_init_with_creds( + kadmin.context.context, + client_name.as_ptr().cast_mut(), + ccache, + service_name.as_ptr().cast_mut(), + &mut params.params, + KADM5_STRUCT_VERSION, + KADM5_API_VERSION_2, + db_args.db_args, + &mut kadmin.server_handle, + ) + }; + + unsafe { + krb5_cc_close(kadmin.context.context, ccache); + } + + kadm5_ret_t_escape_hatch(code)?; + + Ok(kadmin) + } + + #[cfg(feature = "client")] + pub fn with_anonymous(self, _client_name: &str) -> Result { + let _guard = KADMIN_INIT_LOCK.lock().expect("Failed to lock context initialization."); + + let (mut _kadmin, _params, _db_args) = self.get_kadmin()?; + + unimplemented!(); + } + + #[cfg(any(feature = "local", docsrs))] + pub fn with_local(self) -> Result { + let _guard = KADMIN_INIT_LOCK.lock().expect("Failed to lock context initialization."); + + let (mut kadmin, params, db_args) = self.get_kadmin()?; + + let client_name = if let Some(default_realm) = &kadmin.context.default_realm { + let mut concat = CString::new("root/admin@")?.into_bytes(); + concat.extend_from_slice(default_realm.to_bytes_with_nul()); + CString::from_vec_with_nul(concat)? + } else { + CString::new("root/admin")? + }; + let service_name = KADM5_ADMIN_SERVICE.to_owned(); + + let mut params = params; + + let code = unsafe { + kadm5_init_with_creds( + kadmin.context.context, + client_name.as_ptr().cast_mut(), + null_mut(), + service_name.as_ptr().cast_mut(), + &mut params.params, + KADM5_STRUCT_VERSION, + KADM5_API_VERSION_2, + db_args.db_args, + &mut kadmin.server_handle, + ) + }; + + kadm5_ret_t_escape_hatch(code)?; + + Ok(kadmin) + } +} diff --git a/kadmin/src/lib.rs b/kadmin/src/lib.rs new file mode 100644 index 0000000..1d49ba9 --- /dev/null +++ b/kadmin/src/lib.rs @@ -0,0 +1,27 @@ +#[cfg(all(feature = "client", feature = "local"))] +compile_error!("Feature \"client\" and feature \"local\" cannot be enabled at the same time."); + +#[cfg(all(not(feature = "client"), not(feature = "local")))] +compile_error!("Exactly one of feature \"client\" or feature \"local\" must be selected."); + +pub mod context; +pub use context::KAdminContext; + +pub mod db_args; +pub use db_args::KAdminDbArgs; + +pub mod error; +pub use error::Error; + +pub mod kadmin; +pub use kadmin::KAdmin; + +pub mod params; +pub use params::KAdminParams; + +pub mod principal; +pub use principal::Principal; + +mod strconv; + +pub mod sync; diff --git a/kadmin/src/params.rs b/kadmin/src/params.rs new file mode 100644 index 0000000..34b9b85 --- /dev/null +++ b/kadmin/src/params.rs @@ -0,0 +1,209 @@ +use std::{ffi::CString, ptr::null_mut}; + +use kadmin_sys::*; + +use crate::error::Result; + +#[derive(Debug)] +pub struct KAdminParams { + pub(crate) params: kadm5_config_params, + + // Additional fields to store transient strings so the pointer stored in kadm5_config_params + // doesn't become invalid while this struct lives. + _strings: Vec>, +} + +impl KAdminParams { + pub fn builder() -> KAdminParamsBuilder { + KAdminParamsBuilder::default() + } +} + +impl Default for KAdminParams { + fn default() -> Self { + Self::builder().build().unwrap() + } +} + +#[derive(Clone, Debug, Default)] +pub struct KAdminParamsBuilder { + mask: i64, + + realm: Option, + kadmind_port: i32, + kpasswd_port: i32, + admin_server: Option, + dbname: Option, + acl_file: Option, + dict_file: Option, + stash_file: Option, +} + +impl KAdminParamsBuilder { + pub fn realm(mut self, realm: &str) -> Self { + self.realm = Some(realm.to_owned()); + self.mask |= KADM5_CONFIG_REALM as i64; + self + } + + pub fn kadmind_port(mut self, port: i32) -> Self { + self.kadmind_port = port; + self.mask |= KADM5_CONFIG_KADMIND_PORT as i64; + self + } + + pub fn kpasswd_port(mut self, port: i32) -> Self { + self.kpasswd_port = port; + self.mask |= KADM5_CONFIG_KPASSWD_PORT as i64; + self + } + + pub fn admin_server(mut self, admin_server: &str) -> Self { + self.admin_server = Some(admin_server.to_owned()); + self.mask |= KADM5_CONFIG_ADMIN_SERVER as i64; + self + } + + pub fn dbname(mut self, dbname: &str) -> Self { + self.dbname = Some(dbname.to_owned()); + self.mask |= KADM5_CONFIG_DBNAME as i64; + self + } + + pub fn acl_file(mut self, acl_file: &str) -> Self { + self.acl_file = Some(acl_file.to_owned()); + self.mask |= KADM5_CONFIG_ACL_FILE as i64; + self + } + + pub fn dict_file(mut self, dict_file: &str) -> Self { + self.dict_file = Some(dict_file.to_owned()); + self.mask |= KADM5_CONFIG_DICT_FILE as i64; + self + } + + pub fn stash_file(mut self, stash_file: &str) -> Self { + self.stash_file = Some(stash_file.to_owned()); + self.mask |= KADM5_CONFIG_STASH_FILE as i64; + self + } + + pub fn build(self) -> Result { + let realm = self.realm.map(CString::new).transpose()?; + let admin_server = self.admin_server.map(CString::new).transpose()?; + let dbname = self.dbname.map(CString::new).transpose()?; + let acl_file = self.acl_file.map(CString::new).transpose()?; + let dict_file = self.dict_file.map(CString::new).transpose()?; + let stash_file = self.stash_file.map(CString::new).transpose()?; + + let params = kadm5_config_params { + mask: self.mask, + realm: if let Some(realm) = &realm { + realm.as_ptr().cast_mut() + } else { + null_mut() + }, + kadmind_port: self.kadmind_port, + kpasswd_port: self.kpasswd_port, + + admin_server: if let Some(admin_server) = &admin_server { + admin_server.as_ptr().cast_mut() + } else { + null_mut() + }, + + dbname: if let Some(dbname) = &dbname { + dbname.as_ptr().cast_mut() + } else { + null_mut() + }, + acl_file: if let Some(acl_file) = &acl_file { + acl_file.as_ptr().cast_mut() + } else { + null_mut() + }, + dict_file: if let Some(dict_file) = &dict_file { + dict_file.as_ptr().cast_mut() + } else { + null_mut() + }, + mkey_from_kbd: 0, + stash_file: if let Some(stash_file) = &stash_file { + stash_file.as_ptr().cast_mut() + } else { + null_mut() + }, + mkey_name: null_mut(), + enctype: 0, + max_life: 0, + max_rlife: 0, + expiration: 0, + flags: 0, + keysalts: null_mut(), + num_keysalts: 0, + kvno: 0, + iprop_enabled: 0, + iprop_ulogsize: 0, + iprop_poll_time: 0, + iprop_logfile: null_mut(), + iprop_port: 0, + iprop_resync_timeout: 0, + kadmind_listen: null_mut(), + kpasswd_listen: null_mut(), + iprop_listen: null_mut(), + }; + + Ok(KAdminParams { + params, + _strings: vec![realm, admin_server, dbname, acl_file, dict_file, stash_file], + }) + } +} + +#[cfg(test)] +mod tests { + use std::ffi::CStr; + + use super::*; + + #[test] + fn build_empty() { + let params = KAdminParams::builder().build().unwrap(); + + assert_eq!(params.params.mask, 0); + } + + #[test] + fn build_realm() { + let params = KAdminParams::builder().realm("EXAMPLE.ORG").build().unwrap(); + + assert_eq!(params.params.mask, 1); + assert_eq!( + unsafe { CStr::from_ptr(params.params.realm).to_owned() }, + CString::new("EXAMPLE.ORG").unwrap() + ); + } + + #[test] + fn build_all() { + let params = KAdminParams::builder() + .realm("EXAMPLE.ORG") + .admin_server("kdc.example.org") + .kadmind_port(750) + .kpasswd_port(465) + .build() + .unwrap(); + + assert_eq!(params.params.mask, 0x94001); + assert_eq!( + unsafe { CStr::from_ptr(params.params.realm).to_owned() }, + CString::new("EXAMPLE.ORG").unwrap() + ); + assert_eq!( + unsafe { CStr::from_ptr(params.params.realm).to_owned() }, + CString::new("EXAMPLE.ORG").unwrap() + ); + assert_eq!(params.params.kadmind_port, 750); + assert_eq!(params.params.kpasswd_port, 465); + } +} diff --git a/kadmin/src/principal.rs b/kadmin/src/principal.rs new file mode 100644 index 0000000..44313a1 --- /dev/null +++ b/kadmin/src/principal.rs @@ -0,0 +1,26 @@ +use kadmin_sys::*; + +use crate::kadmin::KAdmin; + +#[derive(Debug)] +pub struct Principal<'a> { + pub(crate) kadmin: &'a KAdmin, + pub(crate) inner: _kadm5_principal_ent_t, +} + +impl<'a> Principal<'a> { + pub(crate) fn new(kadmin: &'a KAdmin) -> Self { + Self { + kadmin, + inner: _kadm5_principal_ent_t::default(), + } + } +} + +impl Drop for Principal<'_> { + fn drop(&mut self) { + unsafe { + kadm5_free_principal_ent(self.kadmin.server_handle, &mut self.inner); + } + } +} diff --git a/kadmin/src/strconv.rs b/kadmin/src/strconv.rs new file mode 100644 index 0000000..975826a --- /dev/null +++ b/kadmin/src/strconv.rs @@ -0,0 +1,14 @@ +use std::{ffi::CStr, os::raw::c_char}; + +use crate::error::{Error, Result}; + +pub(crate) fn c_string_to_string(c_string: *const c_char) -> Result { + if c_string.is_null() { + return Err(Error::NullPointerDereference); + } + + match unsafe { CStr::from_ptr(c_string) }.to_owned().into_string() { + Ok(string) => Ok(string), + Err(error) => Err(error.into()), + } +} diff --git a/kadmin/src/sync.rs b/kadmin/src/sync.rs new file mode 100644 index 0000000..5a8c4c8 --- /dev/null +++ b/kadmin/src/sync.rs @@ -0,0 +1,172 @@ +use std::{ + panic::resume_unwind, + sync::mpsc::{Sender, channel}, + thread::{JoinHandle, spawn}, +}; + +use crate::{db_args::KAdminDbArgsBuilder, error::Result, params::KAdminParamsBuilder}; + +enum KAdminOperation { + ListPrincipals(String, Sender>>), + ListPolicies(String, Sender>>), + Exit, +} + +impl KAdminOperation { + fn handle(&self, kadmin: &crate::kadmin::KAdmin) { + match self { + Self::Exit => (), + Self::ListPrincipals(query, sender) => { + let _ = sender.send(kadmin.list_principals(query)); + } + Self::ListPolicies(query, sender) => { + let _ = sender.send(kadmin.list_policies(query)); + } + } + } +} + +pub struct KAdmin { + op_sender: Sender, + join_handle: Option>, +} + +impl KAdmin { + pub fn builder() -> KAdminBuilder { + KAdminBuilder::default() + } + + pub fn list_principals(&self, query: &str) -> Result> { + let (sender, receiver) = channel(); + self.op_sender + .send(KAdminOperation::ListPrincipals(query.to_owned(), sender))?; + receiver.recv()? + } + + pub fn list_policies(&self, query: &str) -> Result> { + let (sender, receiver) = channel(); + self.op_sender + .send(KAdminOperation::ListPolicies(query.to_owned(), sender))?; + receiver.recv()? + } +} + +impl Drop for KAdmin { + fn drop(&mut self) { + // Thread might have already exited, so we don't care about the result of this. + let _ = self.op_sender.send(KAdminOperation::Exit); + if let Some(join_handle) = self.join_handle.take() { + if let Err(e) = join_handle.join() { + resume_unwind(e); + } + } + } +} + +#[derive(Debug, Default)] +pub struct KAdminBuilder { + params_builder: Option, + db_args_builder: Option, +} + +impl KAdminBuilder { + pub fn params_builder(mut self, params_builder: KAdminParamsBuilder) -> Self { + self.params_builder = Some(params_builder); + self + } + + pub fn db_args_builder(mut self, db_args_builder: KAdminDbArgsBuilder) -> Self { + self.db_args_builder = Some(db_args_builder); + self + } + + fn get_builder(self) -> Result { + let mut builder = crate::kadmin::KAdmin::builder(); + if let Some(params_builder) = self.params_builder { + builder = builder.params(params_builder.build()?); + } + if let Some(db_args_builder) = self.db_args_builder { + builder = builder.db_args(db_args_builder.build()?); + } + Ok(builder) + } + + fn build(self, kadmin_build: F) -> Result + where F: FnOnce(crate::kadmin::KAdminBuilder) -> Result + Send + 'static { + let (op_sender, op_receiver) = channel(); + let (start_sender, start_receiver) = channel(); + + let join_handle = spawn(move || { + let builder = match self.get_builder() { + Ok(builder) => builder, + Err(e) => { + let _ = start_sender.send(Err(e)); + return; + } + }; + let kadmin = match kadmin_build(builder) { + Ok(kadmin) => { + let _ = start_sender.send(Ok(())); + kadmin + } + Err(e) => { + let _ = start_sender.send(Err(e)); + return; + } + }; + while let Ok(op) = op_receiver.recv() { + match op { + KAdminOperation::Exit => break, + _ => op.handle(&kadmin), + }; + } + }); + + match start_receiver.recv()? { + Ok(_) => Ok(KAdmin { + op_sender, + join_handle: Some(join_handle), + }), + Err(e) => match join_handle.join() { + Ok(_) => Err(e), + Err(e) => resume_unwind(e), + }, + } + } + + #[cfg(feature = "client")] + pub fn with_password(self, client_name: &str, password: &str) -> Result { + let client_name = client_name.to_owned(); + let password = password.to_owned(); + + self.build(move |builder| builder.with_password(&client_name, &password)) + } + + #[cfg(feature = "client")] + pub fn with_keytab(self, client_name: Option<&str>, keytab: Option<&str>) -> Result { + let client_name = client_name.map(String::from); + let keytab = keytab.map(String::from); + + self.build(move |builder| builder.with_keytab(client_name.as_deref(), keytab.as_deref())) + } + + #[cfg(feature = "client")] + pub fn with_ccache(self, client_name: Option<&str>, ccache_name: Option<&str>) -> Result { + let client_name = client_name.map(String::from); + let ccache_name = ccache_name.map(String::from); + + self.build(move |builder| builder.with_ccache(client_name.as_deref(), ccache_name.as_deref())) + } + + #[cfg(feature = "client")] + pub fn with_anonymous(self, client_name: &str) -> Result { + let client_name = client_name.to_owned(); + + self.build(move |builder| builder.with_anonymous(&client_name)) + } + + #[cfg(any(feature = "local", docsrs))] + pub fn with_local(self) -> Result { + self.build(move |builder| builder.with_local()) + } +} diff --git a/kadmin/tests/k5test.rs b/kadmin/tests/k5test.rs new file mode 100644 index 0000000..35ca806 --- /dev/null +++ b/kadmin/tests/k5test.rs @@ -0,0 +1,114 @@ +use anyhow::Result; +#[allow(unused_imports)] +use pyo3::{prelude::*, types::PyDict}; + +#[allow(dead_code)] +const K5REALM_INIT: &str = r#" +import os +from copy import deepcopy +from k5test import realm + +realm = realm.K5Realm(start_kadmind=True) +realm.http_princ = f"HTTP/testserver@{realm.realm}" +realm.http_keytab = os.path.join(realm.tmpdir, "http_keytab") +realm.addprinc(realm.http_princ) +realm.extract_keytab(realm.http_princ, realm.http_keytab) + +saved_env = deepcopy(os.environ) +for k, v in realm.env.items(): + os.environ[k] = v +"#; + +const RESTORE_ENV: &str = r#" +import os +from copy import deepcopy + +def restore_env(saved_env): + for k in deepcopy(os.environ): + if k in saved_env: + os.environ[k] = saved_env[k] + else: + del os.environ[k] +"#; + +pub(crate) struct K5Test { + realm: PyObject, + saved_env: PyObject, +} + +impl K5Test { + #[allow(dead_code)] + pub(crate) fn new() -> Result { + let (realm, saved_env) = Python::with_gil(|py| { + let module = PyModule::from_code_bound(py, K5REALM_INIT, "", "")?; + let realm = module.getattr("realm")?; + let saved_env = module.getattr("saved_env")?; + Ok::<(PyObject, PyObject), PyErr>((realm.into(), saved_env.into())) + })?; + + Ok(Self { realm, saved_env }) + } + + #[allow(dead_code)] + pub(crate) fn tmpdir(&self) -> Result { + Python::with_gil(|py| { + let realm = self.realm.bind(py); + let tmpdir: String = realm.getattr("tmpdir")?.extract()?; + Ok(tmpdir) + }) + } + + #[allow(dead_code)] + pub(crate) fn admin_princ(&self) -> Result { + Python::with_gil(|py| { + let realm = self.realm.bind(py); + let admin_princ: String = realm.getattr("admin_princ")?.extract()?; + Ok(admin_princ) + }) + } + + #[allow(dead_code)] + pub(crate) fn kadmin_ccache(&self) -> Result { + Python::with_gil(|py| { + let realm = self.realm.bind(py); + let kadmin_ccache: String = realm.getattr("kadmin_ccache")?.extract()?; + Ok(kadmin_ccache) + }) + } + + #[allow(dead_code)] + pub(crate) fn password(&self, name: &str) -> Result { + Python::with_gil(|py| { + let realm = self.realm.bind(py); + let password: String = realm.call_method1("password", (name,))?.extract()?; + Ok(password) + }) + } + + #[allow(dead_code)] + pub(crate) fn prep_kadmin(&self) -> Result<()> { + Python::with_gil(|py| { + let realm = self.realm.bind(py); + realm.call_method0("prep_kadmin")?; + Ok(()) + }) + } +} + +impl Drop for K5Test { + fn drop(&mut self) { + Python::with_gil(|py| { + let realm = self.realm.bind(py); + let saved_env = self.saved_env.bind(py); + + realm.call_method0("stop")?; + + let module = PyModule::from_code_bound(py, RESTORE_ENV, "", "")?; + let restore_env = module.getattr("restore_env")?; + restore_env.call1((saved_env,))?; + + Ok::<(), PyErr>(()) + }) + .unwrap(); + } +} diff --git a/kadmin/tests/kadmin_builder.rs b/kadmin/tests/kadmin_builder.rs new file mode 100644 index 0000000..2452af3 --- /dev/null +++ b/kadmin/tests/kadmin_builder.rs @@ -0,0 +1,117 @@ +use anyhow::Result; +use kadmin::KAdmin; +#[cfg(feature = "local")] +use kadmin::{KAdminDbArgs, KAdminParams}; +use serial_test::serial; +mod k5test; +use k5test::K5Test; + +#[cfg(feature = "client")] +#[test] +#[serial] +fn with_password() -> Result<()> { + let realm = K5Test::new()?; + let kadmin = KAdmin::builder().with_password(&realm.admin_princ()?, &realm.password("admin")?)?; + kadmin.list_principals("*")?; + Ok(()) +} + +#[cfg(feature = "client")] +#[test] +#[serial] +fn with_keytab() -> Result<()> { + let realm = K5Test::new()?; + let kadmin = KAdmin::builder().with_password(&realm.admin_princ()?, &realm.password("admin")?)?; + kadmin.list_principals("*")?; + Ok(()) +} + +#[cfg(feature = "client")] +#[test] +#[serial] +fn with_ccache() -> Result<()> { + let realm = K5Test::new()?; + realm.prep_kadmin()?; + let kadmin_ccache = realm.kadmin_ccache()?; + let kadmin = KAdmin::builder().with_ccache(Some(&realm.admin_princ()?), Some(&kadmin_ccache))?; + kadmin.list_principals("*")?; + Ok(()) +} + +#[cfg(feature = "local")] +#[test] +#[serial] +fn with_local() -> Result<()> { + let realm = K5Test::new()?; + let db_args = KAdminDbArgs::builder() + .arg("dbname", Some(&format!("{}/db", realm.tmpdir()?))) + .build()?; + let params = KAdminParams::builder() + .dbname(&format!("{}/db", realm.tmpdir()?)) + .acl_file(&format!("{}/acl", realm.tmpdir()?)) + .dict_file(&format!("{}/dict", realm.tmpdir()?)) + .stash_file(&format!("{}/stash", realm.tmpdir()?)) + .build()?; + let _kadmin = KAdmin::builder().db_args(db_args).params(params).with_local()?; + Ok(()) +} + +mod sync { + use anyhow::Result; + use kadmin::sync::KAdmin; + #[cfg(feature = "local")] + use kadmin::{KAdminDbArgs, KAdminParams}; + use serial_test::serial; + + use crate::K5Test; + + #[cfg(feature = "client")] + #[test] + #[serial] + fn with_password() -> Result<()> { + let realm = K5Test::new()?; + let kadmin = KAdmin::builder().with_password(&realm.admin_princ()?, &realm.password("admin")?)?; + kadmin.list_principals("*")?; + Ok(()) + } + + #[cfg(feature = "client")] + #[test] + #[serial] + fn with_keytab() -> Result<()> { + let realm = K5Test::new()?; + let kadmin = KAdmin::builder().with_password(&realm.admin_princ()?, &realm.password("admin")?)?; + kadmin.list_principals("*")?; + Ok(()) + } + + #[cfg(feature = "client")] + #[test] + #[serial] + fn with_ccache() -> Result<()> { + let realm = K5Test::new()?; + realm.prep_kadmin()?; + let kadmin_ccache = realm.kadmin_ccache()?; + let kadmin = KAdmin::builder().with_ccache(Some(&realm.admin_princ()?), Some(&kadmin_ccache))?; + kadmin.list_principals("*")?; + Ok(()) + } + + #[cfg(feature = "local")] + #[test] + #[serial] + fn with_local() -> Result<()> { + let realm = K5Test::new()?; + let db_args_builder = KAdminDbArgs::builder().arg("dbname", Some(&format!("{}/db", realm.tmpdir()?))); + let params_builder = KAdminParams::builder() + .dbname(&format!("{}/db", realm.tmpdir()?)) + .acl_file(&format!("{}/acl", realm.tmpdir()?)) + .dict_file(&format!("{}/dict", realm.tmpdir()?)) + .stash_file(&format!("{}/stash", realm.tmpdir()?)); + let _kadmin = KAdmin::builder() + .db_args_builder(db_args_builder) + .params_builder(params_builder) + .with_local()?; + Ok(()) + } +} diff --git a/kadmin/tests/principals.rs b/kadmin/tests/principals.rs new file mode 100644 index 0000000..e480f32 --- /dev/null +++ b/kadmin/tests/principals.rs @@ -0,0 +1,77 @@ +#[cfg(feature = "client")] +use anyhow::Result; +#[cfg(feature = "client")] +use kadmin::KAdmin; +#[cfg(feature = "client")] +use serial_test::serial; +mod k5test; +#[cfg(feature = "client")] +use k5test::K5Test; + +#[cfg(feature = "client")] +#[test] +#[serial] +fn list_principals() -> Result<()> { + let realm = K5Test::new()?; + let kadmin = KAdmin::builder().with_password(&realm.admin_princ()?, &realm.password("admin")?)?; + let principals = kadmin.list_principals("*")?; + assert_eq!( + principals + .into_iter() + .filter(|princ| !princ.starts_with("host/")) + .collect::>(), + vec![ + "HTTP/testserver@KRBTEST.COM", + "K/M@KRBTEST.COM", + "kadmin/admin@KRBTEST.COM", + "kadmin/changepw@KRBTEST.COM", + "krbtgt/KRBTEST.COM@KRBTEST.COM", + "user/admin@KRBTEST.COM", + "user@KRBTEST.COM", + ] + .into_iter() + .map(String::from) + .collect::>() + ); + Ok(()) +} + +mod sync { + #[cfg(feature = "client")] + use anyhow::Result; + #[cfg(feature = "client")] + use kadmin::sync::KAdmin; + #[cfg(feature = "client")] + use serial_test::serial; + + #[cfg(feature = "client")] + use crate::K5Test; + + #[cfg(feature = "client")] + #[test] + #[serial] + fn list_principals() -> Result<()> { + let realm = K5Test::new()?; + let kadmin = KAdmin::builder().with_password(&realm.admin_princ()?, &realm.password("admin")?)?; + let principals = kadmin.list_principals("*")?; + assert_eq!( + principals + .into_iter() + .filter(|princ| !princ.starts_with("host/")) + .collect::>(), + vec![ + "HTTP/testserver@KRBTEST.COM", + "K/M@KRBTEST.COM", + "kadmin/admin@KRBTEST.COM", + "kadmin/changepw@KRBTEST.COM", + "krbtgt/KRBTEST.COM@KRBTEST.COM", + "user/admin@KRBTEST.COM", + "user@KRBTEST.COM", + ] + .into_iter() + .map(String::from) + .collect::>() + ); + Ok(()) + } +} diff --git a/kadmin/tests/valgrind.supp b/kadmin/tests/valgrind.supp new file mode 100644 index 0000000..b8bfeae --- /dev/null +++ b/kadmin/tests/valgrind.supp @@ -0,0 +1,10 @@ +{ + ignore_python_conditional_jump_errors + Memcheck:Cond + obj:*/libpython* +} +{ + ignore_valgrind_memcheck + Memcheck:Leak + obj:*/vgpreload_memcheck-*.so +} diff --git a/poetry.lock b/poetry.lock new file mode 100644 index 0000000..63606a8 --- /dev/null +++ b/poetry.lock @@ -0,0 +1,397 @@ +# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand. + +[[package]] +name = "bashlex" +version = "0.18" +description = "Python parser for bash" +optional = false +python-versions = ">=2.7, !=3.0, !=3.1, !=3.2, !=3.3, !=3.4" +files = [ + {file = "bashlex-0.18-py2.py3-none-any.whl", hash = "sha256:91d73a23a3e51711919c1c899083890cdecffc91d8c088942725ac13e9dcfffa"}, + {file = "bashlex-0.18.tar.gz", hash = "sha256:5bb03a01c6d5676338c36fd1028009c8ad07e7d61d8a1ce3f513b7fff52796ee"}, +] + +[[package]] +name = "black" +version = "24.8.0" +description = "The uncompromising code formatter." +optional = false +python-versions = ">=3.8" +files = [ + {file = "black-24.8.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:09cdeb74d494ec023ded657f7092ba518e8cf78fa8386155e4a03fdcc44679e6"}, + {file = "black-24.8.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:81c6742da39f33b08e791da38410f32e27d632260e599df7245cccee2064afeb"}, + {file = "black-24.8.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:707a1ca89221bc8a1a64fb5e15ef39cd755633daa672a9db7498d1c19de66a42"}, + {file = "black-24.8.0-cp310-cp310-win_amd64.whl", hash = "sha256:d6417535d99c37cee4091a2f24eb2b6d5ec42b144d50f1f2e436d9fe1916fe1a"}, + {file = "black-24.8.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:fb6e2c0b86bbd43dee042e48059c9ad7830abd5c94b0bc518c0eeec57c3eddc1"}, + {file = "black-24.8.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:837fd281f1908d0076844bc2b801ad2d369c78c45cf800cad7b61686051041af"}, + {file = "black-24.8.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:62e8730977f0b77998029da7971fa896ceefa2c4c4933fcd593fa599ecbf97a4"}, + {file = "black-24.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:72901b4913cbac8972ad911dc4098d5753704d1f3c56e44ae8dce99eecb0e3af"}, + {file = "black-24.8.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:7c046c1d1eeb7aea9335da62472481d3bbf3fd986e093cffd35f4385c94ae368"}, + {file = "black-24.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:649f6d84ccbae73ab767e206772cc2d7a393a001070a4c814a546afd0d423aed"}, + {file = "black-24.8.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2b59b250fdba5f9a9cd9d0ece6e6d993d91ce877d121d161e4698af3eb9c1018"}, + {file = "black-24.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:6e55d30d44bed36593c3163b9bc63bf58b3b30e4611e4d88a0c3c239930ed5b2"}, + {file = "black-24.8.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:505289f17ceda596658ae81b61ebbe2d9b25aa78067035184ed0a9d855d18afd"}, + {file = "black-24.8.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:b19c9ad992c7883ad84c9b22aaa73562a16b819c1d8db7a1a1a49fb7ec13c7d2"}, + {file = "black-24.8.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1f13f7f386f86f8121d76599114bb8c17b69d962137fc70efe56137727c7047e"}, + {file = "black-24.8.0-cp38-cp38-win_amd64.whl", hash = "sha256:f490dbd59680d809ca31efdae20e634f3fae27fba3ce0ba3208333b713bc3920"}, + {file = "black-24.8.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:eab4dd44ce80dea27dc69db40dab62d4ca96112f87996bca68cd75639aeb2e4c"}, + {file = "black-24.8.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3c4285573d4897a7610054af5a890bde7c65cb466040c5f0c8b732812d7f0e5e"}, + {file = "black-24.8.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9e84e33b37be070ba135176c123ae52a51f82306def9f7d063ee302ecab2cf47"}, + {file = "black-24.8.0-cp39-cp39-win_amd64.whl", hash = "sha256:73bbf84ed136e45d451a260c6b73ed674652f90a2b3211d6a35e78054563a9bb"}, + {file = "black-24.8.0-py3-none-any.whl", hash = "sha256:972085c618ee94f402da1af548a4f218c754ea7e5dc70acb168bfaca4c2542ed"}, + {file = "black-24.8.0.tar.gz", hash = "sha256:2500945420b6784c38b9ee885af039f5e7471ef284ab03fa35ecdde4688cd83f"}, +] + +[package.dependencies] +click = ">=8.0.0" +mypy-extensions = ">=0.4.3" +packaging = ">=22.0" +pathspec = ">=0.9.0" +platformdirs = ">=2" +tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} +typing-extensions = {version = ">=4.0.1", markers = "python_version < \"3.11\""} + +[package.extras] +colorama = ["colorama (>=0.4.3)"] +d = ["aiohttp (>=3.7.4)", "aiohttp (>=3.7.4,!=3.9.0)"] +jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] +uvloop = ["uvloop (>=0.15.2)"] + +[[package]] +name = "bracex" +version = "2.5.post1" +description = "Bash style brace expander." +optional = false +python-versions = ">=3.8" +files = [ + {file = "bracex-2.5.post1-py3-none-any.whl", hash = "sha256:13e5732fec27828d6af308628285ad358047cec36801598368cb28bc631dbaf6"}, + {file = "bracex-2.5.post1.tar.gz", hash = "sha256:12c50952415bfa773d2d9ccb8e79651b8cdb1f31a42f6091b804f6ba2b4a66b6"}, +] + +[[package]] +name = "build" +version = "1.2.2.post1" +description = "A simple, correct Python build frontend" +optional = false +python-versions = ">=3.8" +files = [ + {file = "build-1.2.2.post1-py3-none-any.whl", hash = "sha256:1d61c0887fa860c01971625baae8bdd338e517b836a2f70dd1f7aa3a6b2fc5b5"}, + {file = "build-1.2.2.post1.tar.gz", hash = "sha256:b36993e92ca9375a219c99e606a122ff365a760a2d4bba0caa09bd5278b608b7"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "os_name == \"nt\""} +importlib-metadata = {version = ">=4.6", markers = "python_full_version < \"3.10.2\""} +packaging = ">=19.1" +pyproject_hooks = "*" +tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} + +[package.extras] +docs = ["furo (>=2023.08.17)", "sphinx (>=7.0,<8.0)", "sphinx-argparse-cli (>=1.5)", "sphinx-autodoc-typehints (>=1.10)", "sphinx-issues (>=3.0.0)"] +test = ["build[uv,virtualenv]", "filelock (>=3)", "pytest (>=6.2.4)", "pytest-cov (>=2.12)", "pytest-mock (>=2)", "pytest-rerunfailures (>=9.1)", "pytest-xdist (>=1.34)", "setuptools (>=42.0.0)", "setuptools (>=56.0.0)", "setuptools (>=56.0.0)", "setuptools (>=67.8.0)", "wheel (>=0.36.0)"] +typing = ["build[uv]", "importlib-metadata (>=5.1)", "mypy (>=1.9.0,<1.10.0)", "tomli", "typing-extensions (>=3.7.4.3)"] +uv = ["uv (>=0.1.18)"] +virtualenv = ["virtualenv (>=20.0.35)"] + +[[package]] +name = "certifi" +version = "2024.8.30" +description = "Python package for providing Mozilla's CA Bundle." +optional = false +python-versions = ">=3.6" +files = [ + {file = "certifi-2024.8.30-py3-none-any.whl", hash = "sha256:922820b53db7a7257ffbda3f597266d435245903d80737e34f8a45ff3e3230d8"}, + {file = "certifi-2024.8.30.tar.gz", hash = "sha256:bec941d2aa8195e248a60b31ff9f0558284cf01a52591ceda73ea9afffd69fd9"}, +] + +[[package]] +name = "cibuildwheel" +version = "2.21.3" +description = "Build Python wheels on CI with minimal configuration." +optional = false +python-versions = ">=3.8" +files = [ + {file = "cibuildwheel-2.21.3-py3-none-any.whl", hash = "sha256:f1d036a13603a6ce4019d8b1bd52c296cf32461a3b3be8441434b60b8b378b80"}, + {file = "cibuildwheel-2.21.3.tar.gz", hash = "sha256:3ce23a9e5406b3eeb80039d7a6fdb218a2450932a8037c0bf76511cd88dfb74e"}, +] + +[package.dependencies] +bashlex = "!=0.13" +bracex = "*" +certifi = "*" +filelock = "*" +packaging = ">=20.9" +platformdirs = "*" +tomli = {version = "*", markers = "python_version < \"3.11\""} +typing-extensions = {version = ">=4.1.0", markers = "python_version < \"3.11\""} + +[package.extras] +bin = ["click", "packaging (>=21.0)", "pip-tools", "pygithub", "pyyaml", "requests", "rich (>=9.6)"] +dev = ["build", "click", "jinja2", "packaging (>=21.0)", "pip-tools", "pygithub", "pytest (>=6)", "pytest-timeout", "pytest-xdist", "pyyaml", "requests", "rich (>=9.6)", "setuptools", "tomli-w", "validate-pyproject"] +docs = ["jinja2 (>=3.1.2)", "mkdocs (==1.6.1)", "mkdocs-include-markdown-plugin (==6.2.2)", "mkdocs-macros-plugin", "pymdown-extensions"] +test = ["build", "jinja2", "pytest (>=6)", "pytest-timeout", "pytest-xdist", "setuptools", "tomli-w", "validate-pyproject"] +uv = ["uv"] + +[[package]] +name = "click" +version = "8.1.7" +description = "Composable command line interface toolkit" +optional = false +python-versions = ">=3.7" +files = [ + {file = "click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28"}, + {file = "click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "platform_system == \"Windows\""} + +[[package]] +name = "colorama" +version = "0.4.6" +description = "Cross-platform colored terminal text." +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +files = [ + {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, + {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, +] + +[[package]] +name = "filelock" +version = "3.16.1" +description = "A platform independent file lock." +optional = false +python-versions = ">=3.8" +files = [ + {file = "filelock-3.16.1-py3-none-any.whl", hash = "sha256:2082e5703d51fbf98ea75855d9d5527e33d8ff23099bec374a134febee6946b0"}, + {file = "filelock-3.16.1.tar.gz", hash = "sha256:c249fbfcd5db47e5e2d6d62198e565475ee65e4831e2561c8e313fa7eb961435"}, +] + +[package.extras] +docs = ["furo (>=2024.8.6)", "sphinx (>=8.0.2)", "sphinx-autodoc-typehints (>=2.4.1)"] +testing = ["covdefaults (>=2.3)", "coverage (>=7.6.1)", "diff-cover (>=9.2)", "pytest (>=8.3.3)", "pytest-asyncio (>=0.24)", "pytest-cov (>=5)", "pytest-mock (>=3.14)", "pytest-timeout (>=2.3.1)", "virtualenv (>=20.26.4)"] +typing = ["typing-extensions (>=4.12.2)"] + +[[package]] +name = "importlib-metadata" +version = "8.5.0" +description = "Read metadata from Python packages" +optional = false +python-versions = ">=3.8" +files = [ + {file = "importlib_metadata-8.5.0-py3-none-any.whl", hash = "sha256:45e54197d28b7a7f1559e60b95e7c567032b602131fbd588f1497f47880aa68b"}, + {file = "importlib_metadata-8.5.0.tar.gz", hash = "sha256:71522656f0abace1d072b9e5481a48f07c138e00f079c38c8f883823f9c26bd7"}, +] + +[package.dependencies] +zipp = ">=3.20" + +[package.extras] +check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1)"] +cover = ["pytest-cov"] +doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] +enabler = ["pytest-enabler (>=2.2)"] +perf = ["ipython"] +test = ["flufl.flake8", "importlib-resources (>=1.3)", "jaraco.test (>=5.4)", "packaging", "pyfakefs", "pytest (>=6,!=8.1.*)", "pytest-perf (>=0.9.2)"] +type = ["pytest-mypy"] + +[[package]] +name = "k5test" +version = "0.10.4" +description = "A library for testing Python applications in self-contained Kerberos 5 environments" +optional = false +python-versions = ">=3.6" +files = [ + {file = "k5test-0.10.4-py2.py3-none-any.whl", hash = "sha256:33de7ff10bf99155fe8ee5d5976798ad1db6237214306dadf5a0ae9d6bb0ad03"}, + {file = "k5test-0.10.4.tar.gz", hash = "sha256:e152491e6602f6a93b3d533d387bd4590f2476093b6842170ff0b93de64bef30"}, +] + +[package.extras] +extension-test = ["gssapi"] + +[[package]] +name = "mypy" +version = "1.13.0" +description = "Optional static typing for Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "mypy-1.13.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6607e0f1dd1fb7f0aca14d936d13fd19eba5e17e1cd2a14f808fa5f8f6d8f60a"}, + {file = "mypy-1.13.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8a21be69bd26fa81b1f80a61ee7ab05b076c674d9b18fb56239d72e21d9f4c80"}, + {file = "mypy-1.13.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7b2353a44d2179846a096e25691d54d59904559f4232519d420d64da6828a3a7"}, + {file = "mypy-1.13.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:0730d1c6a2739d4511dc4253f8274cdd140c55c32dfb0a4cf8b7a43f40abfa6f"}, + {file = "mypy-1.13.0-cp310-cp310-win_amd64.whl", hash = "sha256:c5fc54dbb712ff5e5a0fca797e6e0aa25726c7e72c6a5850cfd2adbc1eb0a372"}, + {file = "mypy-1.13.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:581665e6f3a8a9078f28d5502f4c334c0c8d802ef55ea0e7276a6e409bc0d82d"}, + {file = "mypy-1.13.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3ddb5b9bf82e05cc9a627e84707b528e5c7caaa1c55c69e175abb15a761cec2d"}, + {file = "mypy-1.13.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:20c7ee0bc0d5a9595c46f38beb04201f2620065a93755704e141fcac9f59db2b"}, + {file = "mypy-1.13.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:3790ded76f0b34bc9c8ba4def8f919dd6a46db0f5a6610fb994fe8efdd447f73"}, + {file = "mypy-1.13.0-cp311-cp311-win_amd64.whl", hash = "sha256:51f869f4b6b538229c1d1bcc1dd7d119817206e2bc54e8e374b3dfa202defcca"}, + {file = "mypy-1.13.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:5c7051a3461ae84dfb5dd15eff5094640c61c5f22257c8b766794e6dd85e72d5"}, + {file = "mypy-1.13.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:39bb21c69a5d6342f4ce526e4584bc5c197fd20a60d14a8624d8743fffb9472e"}, + {file = "mypy-1.13.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:164f28cb9d6367439031f4c81e84d3ccaa1e19232d9d05d37cb0bd880d3f93c2"}, + {file = "mypy-1.13.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:a4c1bfcdbce96ff5d96fc9b08e3831acb30dc44ab02671eca5953eadad07d6d0"}, + {file = "mypy-1.13.0-cp312-cp312-win_amd64.whl", hash = "sha256:a0affb3a79a256b4183ba09811e3577c5163ed06685e4d4b46429a271ba174d2"}, + {file = "mypy-1.13.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a7b44178c9760ce1a43f544e595d35ed61ac2c3de306599fa59b38a6048e1aa7"}, + {file = "mypy-1.13.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5d5092efb8516d08440e36626f0153b5006d4088c1d663d88bf79625af3d1d62"}, + {file = "mypy-1.13.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:de2904956dac40ced10931ac967ae63c5089bd498542194b436eb097a9f77bc8"}, + {file = "mypy-1.13.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:7bfd8836970d33c2105562650656b6846149374dc8ed77d98424b40b09340ba7"}, + {file = "mypy-1.13.0-cp313-cp313-win_amd64.whl", hash = "sha256:9f73dba9ec77acb86457a8fc04b5239822df0c14a082564737833d2963677dbc"}, + {file = "mypy-1.13.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:100fac22ce82925f676a734af0db922ecfea991e1d7ec0ceb1e115ebe501301a"}, + {file = "mypy-1.13.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:7bcb0bb7f42a978bb323a7c88f1081d1b5dee77ca86f4100735a6f541299d8fb"}, + {file = "mypy-1.13.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bde31fc887c213e223bbfc34328070996061b0833b0a4cfec53745ed61f3519b"}, + {file = "mypy-1.13.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:07de989f89786f62b937851295ed62e51774722e5444a27cecca993fc3f9cd74"}, + {file = "mypy-1.13.0-cp38-cp38-win_amd64.whl", hash = "sha256:4bde84334fbe19bad704b3f5b78c4abd35ff1026f8ba72b29de70dda0916beb6"}, + {file = "mypy-1.13.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:0246bcb1b5de7f08f2826451abd947bf656945209b140d16ed317f65a17dc7dc"}, + {file = "mypy-1.13.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:7f5b7deae912cf8b77e990b9280f170381fdfbddf61b4ef80927edd813163732"}, + {file = "mypy-1.13.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7029881ec6ffb8bc233a4fa364736789582c738217b133f1b55967115288a2bc"}, + {file = "mypy-1.13.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:3e38b980e5681f28f033f3be86b099a247b13c491f14bb8b1e1e134d23bb599d"}, + {file = "mypy-1.13.0-cp39-cp39-win_amd64.whl", hash = "sha256:a6789be98a2017c912ae6ccb77ea553bbaf13d27605d2ca20a76dfbced631b24"}, + {file = "mypy-1.13.0-py3-none-any.whl", hash = "sha256:9c250883f9fd81d212e0952c92dbfcc96fc237f4b7c92f56ac81fd48460b3e5a"}, + {file = "mypy-1.13.0.tar.gz", hash = "sha256:0291a61b6fbf3e6673e3405cfcc0e7650bebc7939659fdca2702958038bd835e"}, +] + +[package.dependencies] +mypy-extensions = ">=1.0.0" +tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} +typing-extensions = ">=4.6.0" + +[package.extras] +dmypy = ["psutil (>=4.0)"] +faster-cache = ["orjson"] +install-types = ["pip"] +mypyc = ["setuptools (>=50)"] +reports = ["lxml"] + +[[package]] +name = "mypy-extensions" +version = "1.0.0" +description = "Type system extensions for programs checked with the mypy type checker." +optional = false +python-versions = ">=3.5" +files = [ + {file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"}, + {file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"}, +] + +[[package]] +name = "packaging" +version = "24.1" +description = "Core utilities for Python packages" +optional = false +python-versions = ">=3.8" +files = [ + {file = "packaging-24.1-py3-none-any.whl", hash = "sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124"}, + {file = "packaging-24.1.tar.gz", hash = "sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002"}, +] + +[[package]] +name = "pathspec" +version = "0.12.1" +description = "Utility library for gitignore style pattern matching of file paths." +optional = false +python-versions = ">=3.8" +files = [ + {file = "pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08"}, + {file = "pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712"}, +] + +[[package]] +name = "platformdirs" +version = "4.3.6" +description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." +optional = false +python-versions = ">=3.8" +files = [ + {file = "platformdirs-4.3.6-py3-none-any.whl", hash = "sha256:73e575e1408ab8103900836b97580d5307456908a03e92031bab39e4554cc3fb"}, + {file = "platformdirs-4.3.6.tar.gz", hash = "sha256:357fb2acbc885b0419afd3ce3ed34564c13c9b95c89360cd9563f73aa5e2b907"}, +] + +[package.extras] +docs = ["furo (>=2024.8.6)", "proselint (>=0.14)", "sphinx (>=8.0.2)", "sphinx-autodoc-typehints (>=2.4)"] +test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=8.3.2)", "pytest-cov (>=5)", "pytest-mock (>=3.14)"] +type = ["mypy (>=1.11.2)"] + +[[package]] +name = "pyproject-hooks" +version = "1.2.0" +description = "Wrappers to call pyproject.toml-based build backend hooks." +optional = false +python-versions = ">=3.7" +files = [ + {file = "pyproject_hooks-1.2.0-py3-none-any.whl", hash = "sha256:9e5c6bfa8dcc30091c74b0cf803c81fdd29d94f01992a7707bc97babb1141913"}, + {file = "pyproject_hooks-1.2.0.tar.gz", hash = "sha256:1e859bd5c40fae9448642dd871adf459e5e2084186e8d2c2a79a824c970da1f8"}, +] + +[[package]] +name = "ruff" +version = "0.7.2" +description = "An extremely fast Python linter and code formatter, written in Rust." +optional = false +python-versions = ">=3.7" +files = [ + {file = "ruff-0.7.2-py3-none-linux_armv6l.whl", hash = "sha256:b73f873b5f52092e63ed540adefc3c36f1f803790ecf2590e1df8bf0a9f72cb8"}, + {file = "ruff-0.7.2-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:5b813ef26db1015953daf476202585512afd6a6862a02cde63f3bafb53d0b2d4"}, + {file = "ruff-0.7.2-py3-none-macosx_11_0_arm64.whl", hash = "sha256:853277dbd9675810c6826dad7a428d52a11760744508340e66bf46f8be9701d9"}, + {file = "ruff-0.7.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:21aae53ab1490a52bf4e3bf520c10ce120987b047c494cacf4edad0ba0888da2"}, + {file = "ruff-0.7.2-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ccc7e0fc6e0cb3168443eeadb6445285abaae75142ee22b2b72c27d790ab60ba"}, + {file = "ruff-0.7.2-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fd77877a4e43b3a98e5ef4715ba3862105e299af0c48942cc6d51ba3d97dc859"}, + {file = "ruff-0.7.2-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:e00163fb897d35523c70d71a46fbaa43bf7bf9af0f4534c53ea5b96b2e03397b"}, + {file = "ruff-0.7.2-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f3c54b538633482dc342e9b634d91168fe8cc56b30a4b4f99287f4e339103e88"}, + {file = "ruff-0.7.2-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7b792468e9804a204be221b14257566669d1db5c00d6bb335996e5cd7004ba80"}, + {file = "ruff-0.7.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dba53ed84ac19ae4bfb4ea4bf0172550a2285fa27fbb13e3746f04c80f7fa088"}, + {file = "ruff-0.7.2-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:b19fafe261bf741bca2764c14cbb4ee1819b67adb63ebc2db6401dcd652e3748"}, + {file = "ruff-0.7.2-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:28bd8220f4d8f79d590db9e2f6a0674f75ddbc3847277dd44ac1f8d30684b828"}, + {file = "ruff-0.7.2-py3-none-musllinux_1_2_i686.whl", hash = "sha256:9fd67094e77efbea932e62b5d2483006154794040abb3a5072e659096415ae1e"}, + {file = "ruff-0.7.2-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:576305393998b7bd6c46018f8104ea3a9cb3fa7908c21d8580e3274a3b04b691"}, + {file = "ruff-0.7.2-py3-none-win32.whl", hash = "sha256:fa993cfc9f0ff11187e82de874dfc3611df80852540331bc85c75809c93253a8"}, + {file = "ruff-0.7.2-py3-none-win_amd64.whl", hash = "sha256:dd8800cbe0254e06b8fec585e97554047fb82c894973f7ff18558eee33d1cb88"}, + {file = "ruff-0.7.2-py3-none-win_arm64.whl", hash = "sha256:bb8368cd45bba3f57bb29cbb8d64b4a33f8415d0149d2655c5c8539452ce7760"}, + {file = "ruff-0.7.2.tar.gz", hash = "sha256:2b14e77293380e475b4e3a7a368e14549288ed2931fce259a6f99978669e844f"}, +] + +[[package]] +name = "tomli" +version = "2.0.2" +description = "A lil' TOML parser" +optional = false +python-versions = ">=3.8" +files = [ + {file = "tomli-2.0.2-py3-none-any.whl", hash = "sha256:2ebe24485c53d303f690b0ec092806a085f07af5a5aa1464f3931eec36caaa38"}, + {file = "tomli-2.0.2.tar.gz", hash = "sha256:d46d457a85337051c36524bc5349dd91b1877838e2979ac5ced3e710ed8a60ed"}, +] + +[[package]] +name = "typing-extensions" +version = "4.12.2" +description = "Backported and Experimental Type Hints for Python 3.8+" +optional = false +python-versions = ">=3.8" +files = [ + {file = "typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d"}, + {file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"}, +] + +[[package]] +name = "zipp" +version = "3.20.2" +description = "Backport of pathlib-compatible object wrapper for zip files" +optional = false +python-versions = ">=3.8" +files = [ + {file = "zipp-3.20.2-py3-none-any.whl", hash = "sha256:a817ac80d6cf4b23bf7f2828b7cabf326f15a001bea8b1f9b49631780ba28350"}, + {file = "zipp-3.20.2.tar.gz", hash = "sha256:bc9eb26f4506fda01b81bcde0ca78103b6e62f991b381fec825435c836edbc29"}, +] + +[package.extras] +check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1)"] +cover = ["pytest-cov"] +doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] +enabler = ["pytest-enabler (>=2.2)"] +test = ["big-O", "importlib-resources", "jaraco.functools", "jaraco.itertools", "jaraco.test", "more-itertools", "pytest (>=6,!=8.1.*)", "pytest-ignore-flaky"] +type = ["pytest-mypy"] + +[metadata] +lock-version = "2.0" +python-versions = ">=3.8" +content-hash = "eb9341bbfc45163dbc002f9dc777abb9d2d4d1609c8a224769d135fcaf7e4150" diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..64f0dc1 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,81 @@ +[project] +name = "python-kadmin-rs" +version = "0.0.0" +description = "Python interface to the Kerberos administration interface (kadm5)" +requires-python = ">=3.8,<=3.13" +readme = "README.md" +license = { file = "LICENSE" } +authors = [ + { name = "Marc 'risson' Schmitt", email = "marc.schmitt@risson.space" }, + { name = "authentik community", email = "hello@goauthentik.io" }, +] +keywords = ["krb5", "kadmin", "kadm5", "kerberos"] + +classifiers = [ + "Development Status :: 3 - Alpha", + "Intended Audience :: System Administrators", + "Intended Audience :: Developers", + "License :: OSI Approved :: MIT License", + "Operating System :: MacOS", + "Operating System :: POSIX", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: Implementation :: CPython", + "Programming Language :: Python", + "Programming Language :: Rust", + "Topic :: Software Development :: Libraries :: Python Modules", + "Topic :: System :: Systems Administration :: Authentication/Directory", + "Typing :: Typed", +] + +[project.urls] +Homepage = "https://github.com/authentik-community/kadmin-rs" +Documentation = "https://github.com/authentik-community/kadmin-rs" +Repository = "https://github.com/authentik-community/kadmin-rs.git" + +[build-system] +requires = ["setuptools", "setuptools-rust", "setuptools-scm"] +build-backend = "setuptools.build_meta" + +[tool.setuptools.packages] +find = { where = ["python-kadmin-rs/python"] } + +[[tool.setuptools-rust.ext-modules]] +target = "kadmin._lib" +path = "python-kadmin-rs/Cargo.toml" +strip = "All" +args = ["--no-default-features"] +features = ["client"] + +[[tool.setuptools-rust.ext-modules]] +target = "kadmin_local._lib" +path = "python-kadmin-rs/Cargo.toml" +strip = "All" +args = ["--no-default-features"] +features = ["local"] + +[tool.ruff] +line-length = 100 +target-version = "py312" + +[tool.poetry] +package-mode = false +name = "kadmin-rs" + +[tool.poetry.dependencies] +python = ">=3.8" + +[tool.poetry.group.dev.dependencies] +black = "*" +build = "*" +cibuildwheel = "*" +mypy = "*" +ruff = "*" + +[tool.poetry.group.test.dependencies] +k5test = "*" diff --git a/python-kadmin-rs/Cargo.toml b/python-kadmin-rs/Cargo.toml new file mode 100644 index 0000000..15dd497 --- /dev/null +++ b/python-kadmin-rs/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "python-kadmin-rs" +version = "0.0.0" +edition.workspace = true +rust-version.workspace = true +publish = false + +[features] +default = ["client"] +client = ["kadmin/client"] +local = ["kadmin/local"] + +[dependencies] +kadmin = { path = "../kadmin", version = "0.0.0", default-features = false } +pyo3 = { version = "0.22", features = ["extension-module"] } + +[lints] +workspace = true + +[package.metadata.release] +pre-release-replacements = [ + { file = "../pyproject.toml", search = "^version = \"[a-z0-9\\.-]+\"", replace = "version = \"{{ version }}\"", exactly = 1 }, +] diff --git a/python-kadmin-rs/python/kadmin/__init__.py b/python-kadmin-rs/python/kadmin/__init__.py new file mode 100644 index 0000000..0505610 --- /dev/null +++ b/python-kadmin-rs/python/kadmin/__init__.py @@ -0,0 +1,8 @@ +from kadmin._lib import KAdmin, Params, DbArgs, __version__ + +__all__ = ( + "__version__", + "KAdmin", + "Params", + "DbArgs", +) diff --git a/python-kadmin-rs/python/kadmin/__init__.pyi b/python-kadmin-rs/python/kadmin/__init__.pyi new file mode 100644 index 0000000..b8b64e3 --- /dev/null +++ b/python-kadmin-rs/python/kadmin/__init__.pyi @@ -0,0 +1,52 @@ +from typing import List, final + +__version__: str + +@final +class KAdmin: + def add_principal(self): ... + def delete_principal(self): ... + def modify_principal(self): ... + def rename_principal(self): ... + def get_principal(self): ... + def list_principals(self, query: str) -> List[str]: ... + def add_policy(self): ... + def modify_policy(self): ... + def delete_policy(self): ... + def get_policy(self): ... + def list_policies(self, query: str) -> List[str]: ... + def get_privs(self): ... + @staticmethod + def with_password( + client_name: str, + password: str, + params: Params | None = None, + db_args: DbArgs | None = None, + ) -> KAdmin: ... + @staticmethod + def with_keytab( + client_name: str | None = None, + keytab: str | None = None, + params: Params | None = None, + db_args: DbArgs | None = None, + ) -> KAdmin: ... + @staticmethod + def with_ccache( + client_name: str | None = None, + ccache_name: str | None = None, + params: Params | None = None, + db_args: DbArgs | None = None, + ) -> KAdmin: ... + @staticmethod + def with_anonymous( + client_name: str, params: Params | None = None, db_args: DbArgs | None = None + ) -> KAdmin: ... + + # @staticmethod + # def with_local(params: Params | None = None, db_args: DbArgs | None = None) -> KAdmin: ... + +@final +class Params: ... + +@final +class DbArgs: ... diff --git a/python-kadmin-rs/python/kadmin/exceptions/__init__.py b/python-kadmin-rs/python/kadmin/exceptions/__init__.py new file mode 100644 index 0000000..b5b1df0 --- /dev/null +++ b/python-kadmin-rs/python/kadmin/exceptions/__init__.py @@ -0,0 +1,19 @@ +from kadmin._lib import exceptions + +PyKAdminException = exceptions.PyKAdminException +KAdminException = exceptions.KAdminException +KerberosException = exceptions.KerberosException +NullPointerDereference = exceptions.NullPointerDereference +CStringConversion = exceptions.CStringConversion +CStringImportFromVec = exceptions.CStringImportFromVec +StringConversion = exceptions.StringConversion + +__all__ = ( + "PyKAdminException", + "KAdminException", + "KerberosException", + "NullPointerDereference", + "CStringConversion", + "CStringImportFromVec", + "StringConversion", +) diff --git a/python-kadmin-rs/python/kadmin/exceptions/__init__.pyi b/python-kadmin-rs/python/kadmin/exceptions/__init__.pyi new file mode 100644 index 0000000..b269670 --- /dev/null +++ b/python-kadmin-rs/python/kadmin/exceptions/__init__.pyi @@ -0,0 +1,7 @@ +class PyKAdminException(Exception): ... +class KAdminException(PyKAdminException): ... +class KerberosException(PyKAdminException): ... +class NullPointerDereference(PyKAdminException): ... +class CStringConversion(PyKAdminException): ... +class CStringImportFromVec(PyKAdminException): ... +class StringConversion(PyKAdminException): ... diff --git a/python-kadmin-rs/python/kadmin/py.typed b/python-kadmin-rs/python/kadmin/py.typed new file mode 100644 index 0000000..e69de29 diff --git a/python-kadmin-rs/python/kadmin_local/__init__.py b/python-kadmin-rs/python/kadmin_local/__init__.py new file mode 100644 index 0000000..f95be01 --- /dev/null +++ b/python-kadmin-rs/python/kadmin_local/__init__.py @@ -0,0 +1,8 @@ +from kadmin_local._lib import KAdmin, Params, DbArgs, __version__ + +__all__ = ( + "__version__", + "KAdmin", + "Params", + "DbArgs", +) diff --git a/python-kadmin-rs/python/kadmin_local/__init__.pyi b/python-kadmin-rs/python/kadmin_local/__init__.pyi new file mode 100644 index 0000000..c6fc801 --- /dev/null +++ b/python-kadmin-rs/python/kadmin_local/__init__.pyi @@ -0,0 +1,28 @@ +from typing import List, final + +__version__: str + +@final +class KAdmin: + def add_principal(self): ... + def delete_principal(self): ... + def modify_principal(self): ... + def rename_principal(self): ... + def get_principal(self): ... + def list_principals(self, query: str) -> List[str]: ... + def add_policy(self): ... + def modify_policy(self): ... + def delete_policy(self): ... + def get_policy(self): ... + def list_policies(self, query: str) -> List[str]: ... + def get_privs(self): ... + @staticmethod + def with_local( + params: Params | None = None, db_args: DbArgs | None = None + ) -> KAdmin: ... + +@final +class Params: ... + +@final +class DbArgs: ... diff --git a/python-kadmin-rs/python/kadmin_local/exceptions/__init__.py b/python-kadmin-rs/python/kadmin_local/exceptions/__init__.py new file mode 100644 index 0000000..a2c26fd --- /dev/null +++ b/python-kadmin-rs/python/kadmin_local/exceptions/__init__.py @@ -0,0 +1,19 @@ +from kadmin_local._lib import exceptions + +PyKAdminException = exceptions.PyKAdminException +KAdminException = exceptions.KAdminException +KerberosException = exceptions.KerberosException +NullPointerDereference = exceptions.NullPointerDereference +CStringConversion = exceptions.CStringConversion +CStringImportFromVec = exceptions.CStringImportFromVec +StringConversion = exceptions.StringConversion + +__all__ = ( + "PyKAdminException", + "KAdminException", + "KerberosException", + "NullPointerDereference", + "CStringConversion", + "CStringImportFromVec", + "StringConversion", +) diff --git a/python-kadmin-rs/python/kadmin_local/exceptions/__init__.pyi b/python-kadmin-rs/python/kadmin_local/exceptions/__init__.pyi new file mode 100644 index 0000000..b269670 --- /dev/null +++ b/python-kadmin-rs/python/kadmin_local/exceptions/__init__.pyi @@ -0,0 +1,7 @@ +class PyKAdminException(Exception): ... +class KAdminException(PyKAdminException): ... +class KerberosException(PyKAdminException): ... +class NullPointerDereference(PyKAdminException): ... +class CStringConversion(PyKAdminException): ... +class CStringImportFromVec(PyKAdminException): ... +class StringConversion(PyKAdminException): ... diff --git a/python-kadmin-rs/python/kadmin_local/py.typed b/python-kadmin-rs/python/kadmin_local/py.typed new file mode 100644 index 0000000..e69de29 diff --git a/python-kadmin-rs/src/lib.rs b/python-kadmin-rs/src/lib.rs new file mode 100644 index 0000000..96ee9d2 --- /dev/null +++ b/python-kadmin-rs/src/lib.rs @@ -0,0 +1,294 @@ +use pyo3::prelude::*; + +#[pymodule(name = "_lib")] +mod kadmin { + use kadmin::{ + db_args::KAdminDbArgsBuilder, + params::KAdminParamsBuilder, + sync::{KAdmin as SyncKAdmin, KAdminBuilder}, + }; + use pyo3::{ + prelude::*, + types::{PyDict, PyString}, + }; + + type Result = std::result::Result; + + #[pymodule_init] + fn init(m: &Bound<'_, PyModule>) -> PyResult<()> { + m.add("__version__", env!("CARGO_PKG_VERSION"))?; + Ok(()) + } + + #[pyclass] + #[derive(Clone)] + struct Params(KAdminParamsBuilder); + + #[pymethods] + impl Params { + #[new] + #[pyo3(signature = (realm=None, kadmind_port=None, kpasswd_port=None, admin_server=None, dbname=None, acl_file=None, dict_file=None, stash_file=None))] + #[allow(clippy::too_many_arguments)] + fn new( + realm: Option<&str>, + kadmind_port: Option, + kpasswd_port: Option, + admin_server: Option<&str>, + dbname: Option<&str>, + acl_file: Option<&str>, + dict_file: Option<&str>, + stash_file: Option<&str>, + ) -> Self { + let mut builder = KAdminParamsBuilder::default(); + if let Some(realm) = realm { + builder = builder.realm(realm); + } + if let Some(kadmind_port) = kadmind_port { + builder = builder.kadmind_port(kadmind_port); + } + if let Some(kpasswd_port) = kpasswd_port { + builder = builder.kpasswd_port(kpasswd_port); + } + if let Some(admin_server) = admin_server { + builder = builder.admin_server(admin_server); + } + if let Some(dbname) = dbname { + builder = builder.dbname(dbname); + } + if let Some(acl_file) = acl_file { + builder = builder.acl_file(acl_file); + } + if let Some(dict_file) = dict_file { + builder = builder.dict_file(dict_file); + } + if let Some(stash_file) = stash_file { + builder = builder.stash_file(stash_file); + } + Self(builder) + } + } + + #[pyclass] + #[derive(Clone)] + struct DbArgs(KAdminDbArgsBuilder); + + #[pymethods] + impl DbArgs { + #[new] + #[pyo3(signature = (**kwargs))] + fn new(kwargs: Option<&Bound<'_, PyDict>>) -> PyResult { + let mut builder = KAdminDbArgsBuilder::default(); + if let Some(kwargs) = kwargs { + for (name, value) in kwargs.iter() { + let name = if !name.is_instance_of::() { + name.str()? + } else { + name.extract()? + }; + builder = if !value.is_none() { + let value = value.str()?; + builder.arg(name.to_str()?, Some(value.to_str()?)) + } else { + builder.arg(name.to_str()?, None) + }; + } + } + Ok(Self(builder)) + } + } + + #[pyclass] + struct KAdmin(SyncKAdmin); + + impl KAdmin { + fn get_builder(params: Option, db_args: Option) -> KAdminBuilder { + let mut builder = KAdminBuilder::default(); + if let Some(params) = params { + builder = builder.params_builder(params.0); + } + if let Some(db_args) = db_args { + builder = builder.db_args_builder(db_args.0); + } + builder + } + } + + #[pymethods] + impl KAdmin { + fn add_principal(&self) { + unimplemented!(); + } + + fn delete_principal(&self) { + unimplemented!(); + } + + fn modify_principal(&self) { + unimplemented!(); + } + + fn rename_principal(&self) { + unimplemented!(); + } + + fn get_principal(&self) { + unimplemented!(); + } + + fn list_principals(&self, query: &str) -> Result> { + Ok(self.0.list_principals(query)?) + } + + fn add_policy(&self) { + unimplemented!(); + } + + fn modify_policy(&self) { + unimplemented!(); + } + + fn delete_policy(&self) { + unimplemented!(); + } + + fn get_policy(&self) { + unimplemented!(); + } + + fn list_policies(&self, query: &str) -> Result> { + Ok(self.0.list_policies(query)?) + } + + fn get_privs(&self) { + unimplemented!(); + } + + #[cfg(feature = "client")] + #[staticmethod] + #[pyo3(signature = (client_name, password, params=None, db_args=None))] + fn with_password( + client_name: &str, + password: &str, + params: Option, + db_args: Option, + ) -> Result { + Ok(Self( + Self::get_builder(params, db_args).with_password(client_name, password)?, + )) + } + + #[cfg(feature = "client")] + #[staticmethod] + #[pyo3(signature = (client_name=None, keytab=None, params=None, db_args=None))] + fn with_keytab( + client_name: Option<&str>, + keytab: Option<&str>, + params: Option, + db_args: Option, + ) -> Result { + Ok(Self( + Self::get_builder(params, db_args).with_keytab(client_name, keytab)?, + )) + } + + #[cfg(feature = "client")] + #[staticmethod] + #[pyo3(signature = (client_name=None, ccache_name=None, params=None, db_args=None))] + fn with_ccache( + client_name: Option<&str>, + ccache_name: Option<&str>, + params: Option, + db_args: Option, + ) -> Result { + Ok(Self( + Self::get_builder(params, db_args).with_ccache(client_name, ccache_name)?, + )) + } + + #[cfg(feature = "client")] + #[staticmethod] + #[pyo3(signature = (client_name, params=None, db_args=None))] + fn with_anonymous(client_name: &str, params: Option, db_args: Option) -> Result { + Ok(Self(Self::get_builder(params, db_args).with_anonymous(client_name)?)) + } + + #[cfg(feature = "local")] + #[staticmethod] + #[pyo3(signature = (params=None, db_args=None))] + fn with_local(params: Option, db_args: Option) -> Result { + Ok(Self(Self::get_builder(params, db_args).with_local()?)) + } + } + + #[pymodule] + mod exceptions { + use kadmin::Error; + use pyo3::{create_exception, exceptions::PyException, intern, prelude::*}; + + #[pymodule_init] + fn init(m: &Bound<'_, PyModule>) -> PyResult<()> { + m.add("PyKAdminException", m.py().get_type_bound::())?; + m.add("KAdminException", m.py().get_type_bound::())?; + m.add("KerberosException", m.py().get_type_bound::())?; + m.add("NullPointerDereference", m.py().get_type_bound::())?; + m.add("CStringConversion", m.py().get_type_bound::())?; + m.add("CStringImportFromVec", m.py().get_type_bound::())?; + m.add("StringConversion", m.py().get_type_bound::())?; + Ok(()) + } + + create_exception!(exceptions, PyKAdminException, PyException); + create_exception!(exceptions, KAdminException, PyKAdminException); + create_exception!(exceptions, KerberosException, PyKAdminException); + create_exception!(exceptions, NullPointerDereference, PyKAdminException); + create_exception!(exceptions, CStringConversion, PyKAdminException); + create_exception!(exceptions, CStringImportFromVec, PyKAdminException); + create_exception!(exceptions, StringConversion, PyKAdminException); + create_exception!(exceptions, ThreadSendError, PyKAdminException); + create_exception!(exceptions, ThreadRecvError, PyKAdminException); + + pub(crate) struct PyKAdminError(Error); + + impl From for PyKAdminError { + fn from(error: Error) -> Self { + Self(error) + } + } + + impl From for PyErr { + fn from(error: PyKAdminError) -> Self { + Python::with_gil(|py| { + let error = error.0; + let (exc, extras) = match &error { + Error::Kerberos { code, message } => ( + KerberosException::new_err(error.to_string()), + Some((*code as i64, message)), + ), + Error::KAdmin { code, message } => { + (KAdminException::new_err(error.to_string()), Some((*code, message))) + } + Error::NullPointerDereference => (NullPointerDereference::new_err(error.to_string()), None), + Error::CStringConversion(_) => (CStringConversion::new_err(error.to_string()), None), + Error::CStringImportFromVec(_) => (CStringImportFromVec::new_err(error.to_string()), None), + Error::StringConversion(_) => (StringConversion::new_err(error.to_string()), None), + Error::ThreadSendError => (ThreadSendError::new_err(error.to_string()), None), + Error::ThreadRecvError(_) => (ThreadRecvError::new_err(error.to_string()), None), + _ => (PyKAdminException::new_err("Unknown error: {}"), None), + }; + + if let Some((code, message)) = extras { + let bound_exc = exc.value_bound(py); + if let Err(err) = bound_exc.setattr(intern!(py, "code"), code) { + return err; + } + if let Err(err) = bound_exc.setattr(intern!(py, "origin_message"), message) { + return err; + } + } + + exc + }) + } + } + } +} diff --git a/python-kadmin-rs/tests/__init__.py b/python-kadmin-rs/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/python-kadmin-rs/tests/test_init.py b/python-kadmin-rs/tests/test_init.py new file mode 100644 index 0000000..44df309 --- /dev/null +++ b/python-kadmin-rs/tests/test_init.py @@ -0,0 +1,36 @@ +from .utils import KerberosTestCase + +import kadmin +import kadmin_local + + +class TestInit(KerberosTestCase): + def test_with_password(self): + kadm = kadmin.KAdmin.with_password( + self.realm.admin_princ, self.realm.password("admin") + ) + kadm.list_principals("*") + + def test_with_keytab(self): + kadm = kadmin.KAdmin.with_password( + self.realm.admin_princ, self.realm.password("admin") + ) + kadm.list_principals("*") + + def test_with_ccache(self): + self.realm.prep_kadmin() + kadm = kadmin.KAdmin.with_ccache( + self.realm.admin_princ, self.realm.kadmin_ccache + ) + kadm.list_principals("*") + + def test_with_local(self): + db_args = kadmin_local.DbArgs(dbname=f"{self.realm.tmpdir}/db") + params = kadmin_local.Params( + dbname=f"{self.realm.tmpdir}/db", + acl_file=f"{self.realm.tmpdir}/acl", + dict_file=f"{self.realm.tmpdir}/dict", + stash_file=f"{self.realm.tmpdir}/stash", + ) + kadm = kadmin_local.KAdmin.with_local(db_args=db_args, params=params) + kadm.list_principals("*") diff --git a/python-kadmin-rs/tests/test_principal.py b/python-kadmin-rs/tests/test_principal.py new file mode 100644 index 0000000..311ae6c --- /dev/null +++ b/python-kadmin-rs/tests/test_principal.py @@ -0,0 +1,26 @@ +from .utils import KerberosTestCase + +import kadmin + + +class TestInit(KerberosTestCase): + def test_list_principals(self): + kadm = kadmin.KAdmin.with_password( + self.realm.admin_princ, self.realm.password("admin") + ) + self.assertEqual( + [ + princ + for princ in kadm.list_principals("*") + if not princ.startswith("host/") + ], + [ + "HTTP/testserver@KRBTEST.COM", + "K/M@KRBTEST.COM", + "kadmin/admin@KRBTEST.COM", + "kadmin/changepw@KRBTEST.COM", + "krbtgt/KRBTEST.COM@KRBTEST.COM", + "user/admin@KRBTEST.COM", + "user@KRBTEST.COM", + ], + ) diff --git a/python-kadmin-rs/tests/utils.py b/python-kadmin-rs/tests/utils.py new file mode 100644 index 0000000..1d3b87e --- /dev/null +++ b/python-kadmin-rs/tests/utils.py @@ -0,0 +1,31 @@ +import os +from copy import deepcopy +from k5test import realm +from unittest import TestCase + + +class KerberosTestCase(TestCase): + @classmethod + def setUpClass(cls): + cls.realm = realm.K5Realm(start_kadmind=True) + + cls.realm.http_princ = f"HTTP/testserver@{cls.realm.realm}" + cls.realm.http_keytab = os.path.join(cls.realm.tmpdir, "http_keytab") + cls.realm.addprinc(cls.realm.http_princ) + cls.realm.extract_keytab(cls.realm.http_princ, cls.realm.http_keytab) + + cls._saved_env = deepcopy(os.environ) + for k, v in cls.realm.env.items(): + os.environ[k] = v + + @classmethod + def tearDownClass(cls): + cls.realm.stop() + del cls.realm + + for k in deepcopy(os.environ): + if k in cls._saved_env: + os.environ[k] = cls._saved_env[k] + else: + del os.environ[k] + cls._saved_env = None diff --git a/rust-toolchain.toml b/rust-toolchain.toml new file mode 100644 index 0000000..73cb934 --- /dev/null +++ b/rust-toolchain.toml @@ -0,0 +1,3 @@ +[toolchain] +channel = "stable" +components = ["rustfmt", "clippy"]