From 4de41f40ace00e502dcecf8249dca629fcc902b5 Mon Sep 17 00:00:00 2001 From: Marc 'risson' Schmitt Date: Sat, 2 Nov 2024 01:44:50 +0100 Subject: [PATCH] Initial commit Signed-off-by: Marc 'risson' Schmitt --- .cargo/config.toml | 2 + .clippy.toml | 3 + .envrc | 19 + .gitignore | 7 + .rustfmt.toml | 16 + Cargo.lock | 545 ++++++++++++++++++ Cargo.toml | 41 ++ LICENSE | 22 + README.md | 87 +++ flake.lock | 210 +++++++ flake.nix | 62 ++ justfile | 83 +++ kadmin-sys/Cargo.toml | 26 + kadmin-sys/build.rs | 56 ++ kadmin-sys/src/lib.rs | 11 + kadmin-sys/src/wrapper.h | 2 + kadmin/Cargo.toml | 32 + kadmin/src/context.rs | 134 +++++ kadmin/src/db_args.rs | 96 +++ kadmin/src/error.rs | 132 +++++ kadmin/src/kadmin.rs | 396 +++++++++++++ kadmin/src/lib.rs | 27 + kadmin/src/params.rs | 209 +++++++ kadmin/src/principal.rs | 26 + kadmin/src/strconv.rs | 14 + kadmin/src/sync.rs | 172 ++++++ kadmin/tests/k5test.rs | 114 ++++ kadmin/tests/kadmin_builder.rs | 117 ++++ kadmin/tests/principals.rs | 73 +++ kadmin/tests/valgrind.supp | 10 + poetry.lock | 397 +++++++++++++ pyproject.toml | 81 +++ python-kadmin-rs/Cargo.toml | 18 + python-kadmin-rs/python/kadmin/__init__.py | 8 + python-kadmin-rs/python/kadmin/__init__.pyi | 52 ++ .../python/kadmin/exceptions/__init__.py | 19 + .../python/kadmin/exceptions/__init__.pyi | 7 + python-kadmin-rs/python/kadmin/py.typed | 0 .../python/kadmin_local/__init__.py | 8 + .../python/kadmin_local/__init__.pyi | 28 + .../kadmin_local/exceptions/__init__.py | 19 + .../kadmin_local/exceptions/__init__.pyi | 7 + python-kadmin-rs/python/kadmin_local/py.typed | 0 python-kadmin-rs/src/lib.rs | 294 ++++++++++ python-kadmin-rs/tests/__init__.py | 0 python-kadmin-rs/tests/test_init.py | 36 ++ python-kadmin-rs/tests/test_principal.py | 23 + python-kadmin-rs/tests/utils.py | 31 + rust-toolchain.toml | 3 + 49 files changed, 3775 insertions(+) create mode 100644 .cargo/config.toml create mode 100644 .clippy.toml create mode 100644 .envrc create mode 100644 .gitignore create mode 100644 .rustfmt.toml create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 LICENSE create mode 100644 README.md create mode 100644 flake.lock create mode 100644 flake.nix create mode 100644 justfile create mode 100644 kadmin-sys/Cargo.toml create mode 100644 kadmin-sys/build.rs create mode 100644 kadmin-sys/src/lib.rs create mode 100644 kadmin-sys/src/wrapper.h create mode 100644 kadmin/Cargo.toml create mode 100644 kadmin/src/context.rs create mode 100644 kadmin/src/db_args.rs create mode 100644 kadmin/src/error.rs create mode 100644 kadmin/src/kadmin.rs create mode 100644 kadmin/src/lib.rs create mode 100644 kadmin/src/params.rs create mode 100644 kadmin/src/principal.rs create mode 100644 kadmin/src/strconv.rs create mode 100644 kadmin/src/sync.rs create mode 100644 kadmin/tests/k5test.rs create mode 100644 kadmin/tests/kadmin_builder.rs create mode 100644 kadmin/tests/principals.rs create mode 100644 kadmin/tests/valgrind.supp create mode 100644 poetry.lock create mode 100644 pyproject.toml create mode 100644 python-kadmin-rs/Cargo.toml create mode 100644 python-kadmin-rs/python/kadmin/__init__.py create mode 100644 python-kadmin-rs/python/kadmin/__init__.pyi create mode 100644 python-kadmin-rs/python/kadmin/exceptions/__init__.py create mode 100644 python-kadmin-rs/python/kadmin/exceptions/__init__.pyi create mode 100644 python-kadmin-rs/python/kadmin/py.typed create mode 100644 python-kadmin-rs/python/kadmin_local/__init__.py create mode 100644 python-kadmin-rs/python/kadmin_local/__init__.pyi create mode 100644 python-kadmin-rs/python/kadmin_local/exceptions/__init__.py create mode 100644 python-kadmin-rs/python/kadmin_local/exceptions/__init__.pyi create mode 100644 python-kadmin-rs/python/kadmin_local/py.typed create mode 100644 python-kadmin-rs/src/lib.rs create mode 100644 python-kadmin-rs/tests/__init__.py create mode 100644 python-kadmin-rs/tests/test_init.py create mode 100644 python-kadmin-rs/tests/test_principal.py create mode 100644 python-kadmin-rs/tests/utils.py create mode 100644 rust-toolchain.toml 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/.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..b8f261c --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,545 @@ +# 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-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 = "glob" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[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", + "pkg-config", +] + +[[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 = "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 = "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 = "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 = "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" 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..d3a3bd5 --- /dev/null +++ b/README.md @@ -0,0 +1,87 @@ +# 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. + +## 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, none are enabled and the crate will not compile. + +- `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. 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..29610be --- /dev/null +++ b/justfile @@ -0,0 +1,83 @@ +# List available commands +default: + just --list + +# Auto format code +lint-fix: + cargo fmt + black python-kadmin-rs + ruff check --fix python-kadmin-rs + +# Lint code +lint: + cargo clippy + +# Lint and auto format +lint-all: lint-fix lint +alias l := lint-all + +# Build all rust crates +build-rust: + cargo build +alias b := build-rust + +# Build python wheel +build-python: + python -m build + +# Build rust crates and python wheel +build: build-rust build-python + +# Build Python wheels for all supported platforms +build-all: + cibuildwheel + +# 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 + +# Test all rust crates +test-rust: test-kadmin-sys test-kadmin test-python-kadmin-rs +alias t := test-rust + +# Test kadmin with valgrind for memory leaks +test-mem: + cd kadmin && \ + CARGO_TARGET_X86_64_UNKNOWN_LINUX_GNU_RUNNER="valgrind --error-exitcode=1 --suppressions=tests/valgrind.supp -s --leak-check=full" \ + cargo test +alias tm := test-mem + +# Test python bindings +test-python: install-python + python -m unittest python-kadmin-rs/tests/test_*.py + stubtest kadmin kadmin_local + +# Test rust crates and python bindings +test-all: test-rust test-mem test-python +alias ta := test-all + +# Build and install wheel +install-python: clean-python build + pip install --force-reinstall dist/python_kadmin_rs-*.whl + +# 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..115b25e --- /dev/null +++ b/kadmin-sys/Cargo.toml @@ -0,0 +1,26 @@ +[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" + +[features] +default = [] +client = [] +server = [] + +[dependencies] + +[build-dependencies] +bindgen = "0.70" +pkg-config = "0.3" diff --git a/kadmin-sys/build.rs b/kadmin-sys/build.rs new file mode 100644 index 0000000..e2e2abb --- /dev/null +++ b/kadmin-sys/build.rs @@ -0,0 +1,56 @@ +use std::{env, path::PathBuf}; + +use pkg_config::probe_library; + +fn main() { + let mut libraries = vec![ + probe_library("krb5").expect("Unable to find library 'krb5'."), + probe_library("kdb").expect("Unable to find library 'kdb5'."), + ]; + if cfg!(feature = "client") { + libraries.push(probe_library("kadm-client").expect("Unable to find library 'kadm5clnt'.")); + } + if cfg!(feature = "server") { + libraries.push(probe_library("kadm-server").expect("Unable to find library 'kadm5srv'.")); + } + + for lib in libraries { + for lib in lib.libs { + println!("cargo:rustc-lib-lib={}", lib); + } + } + + let bindings = 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())) + .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..1b276cf --- /dev/null +++ b/kadmin-sys/src/lib.rs @@ -0,0 +1,11 @@ +#![allow(non_upper_case_globals)] +#![allow(non_camel_case_types)] +#![allow(non_snake_case)] + +#[cfg(all(feature = "client", feature = "server"))] +compile_error!("Feature \"client\" and feature \"server\" cannot be enabled at the same time."); + +#[cfg(all(not(feature = "client"), not(feature = "server")))] +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..0a24d87 --- /dev/null +++ b/kadmin/Cargo.toml @@ -0,0 +1,32 @@ +[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 +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" } +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..391ca9a --- /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(feature = "local")] + 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..de220f0 --- /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(feature = "local")] + 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..65f3186 --- /dev/null +++ b/kadmin/tests/principals.rs @@ -0,0 +1,73 @@ +#[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, + vec![ + "HTTP/testserver@KRBTEST.COM", + "K/M@KRBTEST.COM", + "host/localhost@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, + vec![ + "HTTP/testserver@KRBTEST.COM", + "K/M@KRBTEST.COM", + "host/localhost@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..6f92315 --- /dev/null +++ b/python-kadmin-rs/Cargo.toml @@ -0,0 +1,18 @@ +[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 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..62d4dc1 --- /dev/null +++ b/python-kadmin-rs/tests/test_principal.py @@ -0,0 +1,23 @@ +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( + kadm.list_principals("*"), + [ + "HTTP/testserver@KRBTEST.COM", + "K/M@KRBTEST.COM", + "host/localhost@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"]