commit 4ebb2a21d29fd5a725c4684908342675cdcd1c21 Author: Hlars Date: Sun Jan 5 13:57:23 2025 +0100 initial diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7f75b83 --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +/target +.env +config.toml +*.log +*.db +/bindings +.DS_Store \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..3858614 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,18 @@ +{ + "cSpell.words": [ + "apikey", + "axum", + "chrono", + "color", + "Conn", + "dotenv", + "hmac", + "oneshot", + "openapi", + "recv", + "repr", + "Servable", + "sqlx", + "utoipa" + ] +} \ No newline at end of file diff --git a/ActiveDirecotry/Dockerfile b/ActiveDirecotry/Dockerfile new file mode 100644 index 0000000..b7a9eee --- /dev/null +++ b/ActiveDirecotry/Dockerfile @@ -0,0 +1,9 @@ +FROM ubuntu:22.04 +ENV DEBIAN_FRONTEND noninteractive +# Install Required packages +RUN apt-get update && apt-get -y install samba krb5-config winbind smbclient iproute2 openssl \ + && rm /etc/krb5.conf \ + && mkdir -p /opt/ad-scripts +WORKDIR /opt/ad-scripts +COPY *.sh /opt/ad-scripts +CMD chmod +x /opt/ad-scripts/*.sh && ./samba-ad-setup.sh && ./samba-ad-run.sh \ No newline at end of file diff --git a/ActiveDirecotry/docker-compose.yaml b/ActiveDirecotry/docker-compose.yaml new file mode 100644 index 0000000..d998a93 --- /dev/null +++ b/ActiveDirecotry/docker-compose.yaml @@ -0,0 +1,17 @@ +# https://github.com/amardeep2006/ldap-activedirectory-samba/ +version: "3.4" +services: + ldap.example.org: + restart: "no" + hostname: "ldap.example.org" + build: + context: . + dockerfile: Dockerfile + privileged: true + expose: + - "636" + ports: + - "636:636" + environment: + SMB_ADMIN_PASSWORD: "admin123!" + AD_DOMAIN: "DEV-AD" \ No newline at end of file diff --git a/ActiveDirecotry/samba-ad-run.sh b/ActiveDirecotry/samba-ad-run.sh new file mode 100644 index 0000000..4b370a5 --- /dev/null +++ b/ActiveDirecotry/samba-ad-run.sh @@ -0,0 +1,10 @@ +#!/bin/bash + +set -e + +[ -f /var/lib/samba/.setup ] || { + >&2 echo "[ERROR] Samba is not setup yet, which should happen automatically. Look for errors!" + exit 127 +} + +samba -i -s /var/lib/samba/private/smb.conf \ No newline at end of file diff --git a/ActiveDirecotry/samba-ad-setup.sh b/ActiveDirecotry/samba-ad-setup.sh new file mode 100644 index 0000000..22457bf --- /dev/null +++ b/ActiveDirecotry/samba-ad-setup.sh @@ -0,0 +1,31 @@ +#!/bin/bash + +set -e + +info () { + echo "[INFO] $@" +} + +info "Running setup" + +# Check if samba is setup +[ -f /var/lib/samba/.setup ] && info "Already setup..." && exit 0 + +info "Provisioning domain controller..." + +info "Given admin password: ${SMB_ADMIN_PASSWORD}" + +rm /etc/samba/smb.conf + +# Setting up Samba AD server +samba-tool domain provision\ + --server-role=dc\ + --use-rfc2307\ + --dns-backend=SAMBA_INTERNAL\ + --realm=$(hostname | cut -d '.' -f 2-)\ + --domain=${AD_DOMAIN}\ + --adminpass=${SMB_ADMIN_PASSWORD} + +mv /etc/samba/smb.conf /var/lib/samba/private/smb.conf + +touch /var/lib/samba/.setup \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..afaeb8a --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,3600 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "addr2line" +version = "0.24.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler2" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" + +[[package]] +name = "ahash" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" +dependencies = [ + "cfg-if", + "once_cell", + "version_check", + "zerocopy", +] + +[[package]] +name = "aho-corasick" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +dependencies = [ + "memchr", +] + +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "anstream" +version = "0.6.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" + +[[package]] +name = "anstyle-parse" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c" +dependencies = [ + "windows-sys 0.59.0", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2109dbce0e72be3ec00bed26e6a7479ca384ad226efdd66db8fa2e3a38c83125" +dependencies = [ + "anstyle", + "windows-sys 0.59.0", +] + +[[package]] +name = "anyhow" +version = "1.0.95" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34ac096ce696dc2fcabef30516bb13c0a68a11d30131d3df6f04711467681b04" + +[[package]] +name = "arbitrary" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dde20b3d026af13f561bdd0f15edf01fc734f0dafcedbaf42bba506a9517f223" +dependencies = [ + "derive_arbitrary", +] + +[[package]] +name = "arraydeque" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d902e3d592a523def97af8f317b08ce16b7ab854c1985a0c671e6f15cebc236" + +[[package]] +name = "arrayref" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb" + +[[package]] +name = "arrayvec" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" + +[[package]] +name = "async-trait" +version = "0.1.84" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b1244b10dcd56c92219da4e14caa97e312079e185f04ba3eea25061561dc0a0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "atoi" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f28d99ec8bfea296261ca1af174f24225171fea9664ba9003cbebee704810528" +dependencies = [ + "num-traits", +] + +[[package]] +name = "autocfg" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" + +[[package]] +name = "axum" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d6fd624c75e18b3b4c6b9caf42b1afe24437daaee904069137d8bab077be8b8" +dependencies = [ + "axum-core", + "axum-macros", + "bytes", + "form_urlencoded", + "futures-util", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-util", + "itoa", + "matchit", + "memchr", + "mime", + "percent-encoding", + "pin-project-lite", + "rustversion", + "serde", + "serde_json", + "serde_path_to_error", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tower", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "axum-api-test" +version = "0.1.0" +dependencies = [ + "axum", + "axum-jwt-login", + "chrono", + "clap", + "config", + "dotenv", + "error-stack", + "hmac", + "ldap3", + "once_cell", + "rand", + "rust-argon2", + "serde", + "sha2", + "sqlx", + "strum", + "tokio", + "tokio-util", + "tracing", + "tracing-appender", + "tracing-rolling-file", + "tracing-subscriber", + "ts-rs", + "utoipa", + "utoipa-axum", + "utoipa-redoc", + "utoipa-scalar", + "utoipa-swagger-ui", + "uuid", + "windows-service", +] + +[[package]] +name = "axum-core" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df1362f362fd16024ae199c1970ce98f9661bf5ef94b9808fee734bc3698b733" +dependencies = [ + "bytes", + "futures-util", + "http", + "http-body", + "http-body-util", + "mime", + "pin-project-lite", + "rustversion", + "sync_wrapper", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "axum-jwt-login" +version = "0.1.0" +source = "sparse+https://crates.esteil.dedyn.io/api/v1/crates/" +checksum = "6be705e80dad333a9b9426e718b685aa5d379fe555b40ba3609822c1f351f896" +dependencies = [ + "axum", + "chrono", + "jsonwebtoken", + "serde", + "thiserror 2.0.9", + "tokio", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "axum-macros" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "604fde5e028fea851ce1d8570bbdc034bec850d157f7569d10f347d06808c05c" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "backtrace" +version = "0.3.74" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d82cb332cdfaed17ae235a638438ac4d4839913cc2af585c3c6746e8f8bee1a" +dependencies = [ + "addr2line", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", + "windows-targets 0.52.6", +] + +[[package]] +name = "base64" +version = "0.21.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "base64ct" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" + +[[package]] +name = "bitflags" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" +dependencies = [ + "serde", +] + +[[package]] +name = "blake2b_simd" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23285ad32269793932e830392f2fe2f83e26488fd3ec778883a93c8323735780" +dependencies = [ + "arrayref", + "arrayvec", + "constant_time_eq", +] + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "bumpalo" +version = "3.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "325918d6fe32f23b19878fe4b34794ae41fc19ddbe53b10571a4874d44ffd39b" + +[[package]] +name = "cc" +version = "1.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a012a0df96dd6d06ba9a1b29d6402d1a5d77c6befd2566afdc26e10603dc93d7" +dependencies = [ + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "chrono" +version = "0.4.39" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e36cc9d416881d2e24f9a963be5fb1cd90966419ac844274161d10488b3e825" +dependencies = [ + "android-tzdata", + "iana-time-zone", + "js-sys", + "num-traits", + "serde", + "wasm-bindgen", + "windows-targets 0.52.6", +] + +[[package]] +name = "clap" +version = "4.5.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3135e7ec2ef7b10c6ed8950f0f792ed96ee093fa088608f1c76e569722700c84" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.5.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30582fc632330df2bd26877bde0c1f4470d57c582bbc070376afcd04d8cb4838" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ac6a0c7b1a9e9a5186361f67dfa1b88213572f427fb9ab038efb2bd8c582dab" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "clap_lex" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" + +[[package]] +name = "colorchoice" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" + +[[package]] +name = "concurrent-queue" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "config" +version = "0.15.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d84f8d224ac58107d53d3ec2b9ad39fd8c8c4e285d3c9cb35485ffd2ca88cb3" +dependencies = [ + "async-trait", + "convert_case", + "json5", + "pathdiff", + "ron", + "rust-ini", + "serde", + "serde_json", + "toml", + "winnow", + "yaml-rust2", +] + +[[package]] +name = "const-oid" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" + +[[package]] +name = "const-random" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87e00182fe74b066627d63b85fd550ac2998d4b0bd86bfed477a0ae4c7c71359" +dependencies = [ + "const-random-macro", +] + +[[package]] +name = "const-random-macro" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9d839f2a20b0aee515dc581a6172f2321f96cab76c1a38a4c584a194955390e" +dependencies = [ + "getrandom", + "once_cell", + "tiny-keccak", +] + +[[package]] +name = "constant_time_eq" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c74b8349d32d297c9134b8c88677813a227df8f779daa29bfc29c183fe3dca6" + +[[package]] +name = "convert_case" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec182b0ca2f35d8fc196cf3404988fd8b8c739a4d270ff118a398feb0cbec1ca" +dependencies = [ + "unicode-segmentation", +] + +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "cpufeatures" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16b80225097f2e5ae4e7179dd2266824648f3e2f49d9134d584b76389d31c4c3" +dependencies = [ + "libc", +] + +[[package]] +name = "crc" +version = "3.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69e6e4d7b33a94f0991c26729976b10ebde1d34c3ee82408fb536164fa10d636" +dependencies = [ + "crc-catalog", +] + +[[package]] +name = "crc-catalog" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" + +[[package]] +name = "crc32fast" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crossbeam-channel" +version = "0.5.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06ba6d68e24814cb8de6bb986db8222d3a027d15872cabc0d18817bc3c0e4471" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-queue" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f58bbc28f91df819d0aa2a2c00cd19754769c2fad90579b3592b1c9ba7a3115" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + +[[package]] +name = "crunchy" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" + +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "der" +version = "0.7.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f55bf8e7b65898637379c1b74eb1551107c8294ed26d855ceb9fd1a09cfc9bc0" +dependencies = [ + "const-oid", + "pem-rfc7468", + "zeroize", +] + +[[package]] +name = "deranged" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4" +dependencies = [ + "powerfmt", +] + +[[package]] +name = "derive_arbitrary" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30542c1ad912e0e3d22a1935c290e12e8a29d704a420177a31faad4a601a0800" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "const-oid", + "crypto-common", + "subtle", +] + +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "dlv-list" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "442039f5147480ba31067cb00ada1adae6892028e40e45fc5de7b7df6dcc1b5f" +dependencies = [ + "const-random", +] + +[[package]] +name = "dotenv" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77c90badedccf4105eca100756a0b1289e191f6fcbdadd3cee1d2f614f97da8f" + +[[package]] +name = "dotenvy" +version = "0.15.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" + +[[package]] +name = "either" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" +dependencies = [ + "serde", +] + +[[package]] +name = "encoding_rs" +version = "0.8.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "equivalent" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" + +[[package]] +name = "errno" +version = "0.3.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d" +dependencies = [ + "libc", + "windows-sys 0.59.0", +] + +[[package]] +name = "error-stack" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe413319145d1063f080f27556fd30b1d70b01e2ba10c2a6e40d4be982ffc5d1" +dependencies = [ + "anyhow", + "rustc_version", +] + +[[package]] +name = "etcetera" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "136d1b5283a1ab77bd9257427ffd09d8667ced0570b6f938942bc7568ed5b943" +dependencies = [ + "cfg-if", + "home", + "windows-sys 0.48.0", +] + +[[package]] +name = "event-listener" +version = "5.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6032be9bd27023a771701cc49f9f053c751055f71efb2e0ae5c15809093675ba" +dependencies = [ + "concurrent-queue", + "parking", + "pin-project-lite", +] + +[[package]] +name = "fastrand" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" + +[[package]] +name = "flate2" +version = "1.0.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c936bfdafb507ebbf50b8074c54fa31c5be9a1e7e5f467dd659697041407d07c" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "flume" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da0e4dd2a88388a1f4ccc7c9ce104604dab68d9f408dc34cd45823d5a9069095" +dependencies = [ + "futures-core", + "futures-sink", + "spin", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foldhash" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0d2fde1f7b3d48b8395d5f2de76c18a528bd6a9cdde438df747bfcba3e05d6f" + +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + +[[package]] +name = "form_urlencoded" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "futures" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" + +[[package]] +name = "futures-executor" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-intrusive" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d930c203dd0b6ff06e0201a4a2fe9149b43c684fd4420555b26d21b1a02956f" +dependencies = [ + "futures-core", + "lock_api", + "parking_lot", +] + +[[package]] +name = "futures-io" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" + +[[package]] +name = "futures-macro" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "futures-sink" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" + +[[package]] +name = "futures-task" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" + +[[package]] +name = "futures-util" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "pin-utils", + "slab", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "wasi", + "wasm-bindgen", +] + +[[package]] +name = "gimli" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" + +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" +dependencies = [ + "ahash", +] + +[[package]] +name = "hashbrown" +version = "0.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash", +] + +[[package]] +name = "hashlink" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ba4ff7128dee98c7dc9794b6a411377e1404dba1c97deb8d1a55297bd25d8af" +dependencies = [ + "hashbrown 0.14.5", +] + +[[package]] +name = "hashlink" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1" +dependencies = [ + "hashbrown 0.15.2", +] + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "hkdf" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" +dependencies = [ + "hmac", +] + +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest", +] + +[[package]] +name = "home" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589533453244b0995c858700322199b2becb13b627df2851f64a2775d024abcf" +dependencies = [ + "windows-sys 0.59.0", +] + +[[package]] +name = "http" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f16ca2af56261c99fba8bac40a10251ce8188205a4c448fbb745a2e4daa76fea" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "793429d76616a256bcb62c2a2ec2bed781c8307e797e2598c50010f2bee2544f" +dependencies = [ + "bytes", + "futures-util", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d71d3574edd2771538b901e6549113b4006ece66150fb69c0fb6d9a2adae946" + +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + +[[package]] +name = "hyper" +version = "1.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "256fb8d4bd6413123cc9d91832d78325c48ff41677595be797d90f42969beae0" +dependencies = [ + "bytes", + "futures-channel", + "futures-util", + "http", + "http-body", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "smallvec", + "tokio", +] + +[[package]] +name = "hyper-util" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df2dcfbe0677734ab2f3ffa7fa7bfd4706bfdc1ef393f2ee30184aed67e631b4" +dependencies = [ + "bytes", + "futures-util", + "http", + "http-body", + "hyper", + "pin-project-lite", + "tokio", + "tower-service", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.61" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "235e081f3925a06703c2d0117ea8b91f042756fd6e7a6e5d901e8ca1a996b220" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "icu_collections" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db2fa452206ebee18c4b5c2274dbf1de17008e874b4dc4f0aea9d01ca79e4526" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locid" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13acbb8371917fc971be86fc8057c41a64b521c184808a698c02acc242dbf637" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_locid_transform" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01d11ac35de8e40fdeda00d9e1e9d92525f3f9d887cdd7aa81d727596788b54e" +dependencies = [ + "displaydoc", + "icu_locid", + "icu_locid_transform_data", + "icu_provider", + "tinystr", + "zerovec", +] + +[[package]] +name = "icu_locid_transform_data" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdc8ff3388f852bede6b579ad4e978ab004f139284d7b28715f773507b946f6e" + +[[package]] +name = "icu_normalizer" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19ce3e0da2ec68599d193c93d088142efd7f9c5d6fc9b803774855747dc6a84f" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "utf16_iter", + "utf8_iter", + "write16", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8cafbf7aa791e9b22bec55a167906f9e1215fd475cd22adfcf660e03e989516" + +[[package]] +name = "icu_properties" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93d6020766cfc6302c15dbbc9c8778c37e62c14427cb7f6e601d849e092aeef5" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_locid_transform", + "icu_properties_data", + "icu_provider", + "tinystr", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67a8effbc3dd3e4ba1afa8ad918d5684b8868b3b26500753effea8d2eed19569" + +[[package]] +name = "icu_provider" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ed421c8a8ef78d3e2dbc98a973be2f3770cb42b606e3ab18d6237c4dfde68d9" +dependencies = [ + "displaydoc", + "icu_locid", + "icu_provider_macros", + "stable_deref_trait", + "tinystr", + "writeable", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_provider_macros" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "idna" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daca1df1c957320b2cf139ac61e7bd64fed304c5040df000a745aa1de3b4ef71" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "indexmap" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62f822373a4fe84d4bb149bf54e584a7f4abec90e072ed49cda0edea5b95471f" +dependencies = [ + "equivalent", + "hashbrown 0.15.2", + "serde", +] + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" + +[[package]] +name = "itoa" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674" + +[[package]] +name = "js-sys" +version = "0.3.76" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6717b6b5b077764fb5966237269cb3c64edddde4b14ce42647430a78ced9e7b7" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "json5" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96b0db21af676c1ce64250b5f40f3ce2cf27e4e47cb91ed91eb6fe9350b430c1" +dependencies = [ + "pest", + "pest_derive", + "serde", +] + +[[package]] +name = "jsonwebtoken" +version = "9.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9ae10193d25051e74945f1ea2d0b42e03cc3b890f7e4cc5faa44997d808193f" +dependencies = [ + "base64 0.21.7", + "js-sys", + "pem", + "ring", + "serde", + "serde_json", + "simple_asn1", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" +dependencies = [ + "spin", +] + +[[package]] +name = "lber" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2df7f9fd9f64cf8f59e1a4a0753fe7d575a5b38d3d7ac5758dcee9357d83ef0a" +dependencies = [ + "bytes", + "nom", +] + +[[package]] +name = "ldap3" +version = "0.11.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "166199a8207874a275144c8a94ff6eed5fcbf5c52303e4d9b4d53a0c7ac76554" +dependencies = [ + "async-trait", + "bytes", + "futures", + "futures-util", + "lazy_static", + "lber", + "log", + "native-tls", + "nom", + "percent-encoding", + "thiserror 1.0.69", + "tokio", + "tokio-native-tls", + "tokio-stream", + "tokio-util", + "url", +] + +[[package]] +name = "libc" +version = "0.2.169" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a" + +[[package]] +name = "libm" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8355be11b20d696c8f18f6cc018c4e372165b1fa8126cef092399c9951984ffa" + +[[package]] +name = "libsqlite3-sys" +version = "0.30.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e99fb7a497b1e3339bc746195567ed8d3e24945ecd636e3619d20b9de9e9149" +dependencies = [ + "pkg-config", + "vcpkg", +] + +[[package]] +name = "linux-raw-sys" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" + +[[package]] +name = "litemap" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ee93343901ab17bd981295f2cf0026d4ad018c7c31ba84549a4ddbb47a45104" + +[[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 = "lockfree-object-pool" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9374ef4228402d4b7e403e5838cb880d9ee663314b0a900d5a6aabf0c213552e" + +[[package]] +name = "log" +version = "0.4.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" + +[[package]] +name = "matchers" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558" +dependencies = [ + "regex-automata 0.1.10", +] + +[[package]] +name = "matchit" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3" + +[[package]] +name = "md-5" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf" +dependencies = [ + "cfg-if", + "digest", +] + +[[package]] +name = "memchr" +version = "2.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "mime_guess" +version = "2.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e" +dependencies = [ + "mime", + "unicase", +] + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "miniz_oxide" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ffbe83022cedc1d264172192511ae958937694cd57ce297164951b8b3568394" +dependencies = [ + "adler2", +] + +[[package]] +name = "mio" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.52.0", +] + +[[package]] +name = "native-tls" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8614eb2c83d59d1c8cc974dd3f920198647674a0a035e1af1fa58707e317466" +dependencies = [ + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework", + "security-framework-sys", + "tempfile", +] + +[[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 = "nu-ansi-term" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" +dependencies = [ + "overload", + "winapi", +] + +[[package]] +name = "num-bigint" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-bigint-dig" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc84195820f291c7697304f3cbdadd1cb7199c0efc917ff5eafd71225c136151" +dependencies = [ + "byteorder", + "lazy_static", + "libm", + "num-integer", + "num-iter", + "num-traits", + "rand", + "smallvec", + "zeroize", +] + +[[package]] +name = "num-conv" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-iter" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", + "libm", +] + +[[package]] +name = "num_threads" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c7398b9c8b70908f6371f47ed36737907c87c52af34c268fed0bf0ceb92ead9" +dependencies = [ + "libc", +] + +[[package]] +name = "object" +version = "0.36.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" +dependencies = [ + "memchr", +] + +[[package]] +name = "once_cell" +version = "1.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" + +[[package]] +name = "openssl" +version = "0.10.68" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6174bc48f102d208783c2c84bf931bb75927a617866870de8a4ea85597f871f5" +dependencies = [ + "bitflags", + "cfg-if", + "foreign-types", + "libc", + "once_cell", + "openssl-macros", + "openssl-sys", +] + +[[package]] +name = "openssl-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "openssl-probe" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" + +[[package]] +name = "openssl-sys" +version = "0.9.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45abf306cbf99debc8195b66b7346498d7b10c210de50418b5ccd7ceba08c741" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "ordered-multimap" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49203cdcae0030493bad186b28da2fa25645fa276a51b6fec8010d281e02ef79" +dependencies = [ + "dlv-list", + "hashbrown 0.14.5", +] + +[[package]] +name = "overload" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" + +[[package]] +name = "parking" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" + +[[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 0.52.6", +] + +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + +[[package]] +name = "pathdiff" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3" + +[[package]] +name = "pem" +version = "3.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e459365e590736a54c3fa561947c84837534b8e9af6fc5bf781307e82658fae" +dependencies = [ + "base64 0.22.1", + "serde", +] + +[[package]] +name = "pem-rfc7468" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" +dependencies = [ + "base64ct", +] + +[[package]] +name = "percent-encoding" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" + +[[package]] +name = "pest" +version = "2.7.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b7cafe60d6cf8e62e1b9b2ea516a089c008945bb5a275416789e7db0bc199dc" +dependencies = [ + "memchr", + "thiserror 2.0.9", + "ucd-trie", +] + +[[package]] +name = "pest_derive" +version = "2.7.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "816518421cfc6887a0d62bf441b6ffb4536fcc926395a69e1a85852d4363f57e" +dependencies = [ + "pest", + "pest_generator", +] + +[[package]] +name = "pest_generator" +version = "2.7.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d1396fd3a870fc7838768d171b4616d5c91f6cc25e377b673d714567d99377b" +dependencies = [ + "pest", + "pest_meta", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "pest_meta" +version = "2.7.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1e58089ea25d717bfd31fb534e4f3afcc2cc569c70de3e239778991ea3b7dea" +dependencies = [ + "once_cell", + "pest", + "sha2", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "915a1e146535de9163f3987b8944ed8cf49a18bb0056bcebcdcece385cece4ff" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "pkcs1" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f" +dependencies = [ + "der", + "pkcs8", + "spki", +] + +[[package]] +name = "pkcs8" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" +dependencies = [ + "der", + "spki", +] + +[[package]] +name = "pkg-config" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2" + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "ppv-lite86" +version = "0.2.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "proc-macro2" +version = "1.0.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37d3544b3f2748c54e147655edb5025752e2303145b5aefb3c3ea2c78b973bb0" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e4dccaaaf89514f546c693ddc140f729f958c247918a13380cccc6078391acc" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom", +] + +[[package]] +name = "redox_syscall" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03a862b389f93e68874fbf580b9de08dd02facb9a788ebadaf4a3fd33cf58834" +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 0.4.9", + "regex-syntax 0.8.5", +] + +[[package]] +name = "regex-automata" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" +dependencies = [ + "regex-syntax 0.6.29", +] + +[[package]] +name = "regex-automata" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax 0.8.5", +] + +[[package]] +name = "regex-syntax" +version = "0.6.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" + +[[package]] +name = "regex-syntax" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" + +[[package]] +name = "ring" +version = "0.17.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c17fa4cb658e3583423e915b9f3acc01cceaee1860e33d59ebae66adc3a2dc0d" +dependencies = [ + "cc", + "cfg-if", + "getrandom", + "libc", + "spin", + "untrusted", + "windows-sys 0.52.0", +] + +[[package]] +name = "ron" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b91f7eff05f748767f183df4320a63d6936e9c6107d97c9e6bdd9784f4289c94" +dependencies = [ + "base64 0.21.7", + "bitflags", + "serde", + "serde_derive", +] + +[[package]] +name = "rsa" +version = "0.9.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47c75d7c5c6b673e58bf54d8544a9f432e3a925b0e80f7cd3602ab5c50c55519" +dependencies = [ + "const-oid", + "digest", + "num-bigint-dig", + "num-integer", + "num-traits", + "pkcs1", + "pkcs8", + "rand_core", + "signature", + "spki", + "subtle", + "zeroize", +] + +[[package]] +name = "rust-argon2" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d9848531d60c9cbbcf9d166c885316c24bc0e2a9d3eba0956bb6cbbd79bc6e8" +dependencies = [ + "base64 0.21.7", + "blake2b_simd", + "constant_time_eq", +] + +[[package]] +name = "rust-embed" +version = "8.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa66af4a4fdd5e7ebc276f115e895611a34739a9c1c01028383d612d550953c0" +dependencies = [ + "rust-embed-impl", + "rust-embed-utils", + "walkdir", +] + +[[package]] +name = "rust-embed-impl" +version = "8.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6125dbc8867951125eec87294137f4e9c2c96566e61bf72c45095a7c77761478" +dependencies = [ + "proc-macro2", + "quote", + "rust-embed-utils", + "syn", + "walkdir", +] + +[[package]] +name = "rust-embed-utils" +version = "8.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e5347777e9aacb56039b0e1f28785929a8a3b709e87482e7442c72e7c12529d" +dependencies = [ + "sha2", + "walkdir", +] + +[[package]] +name = "rust-ini" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e310ef0e1b6eeb79169a1171daf9abcb87a2e17c03bee2c4bb100b55c75409f" +dependencies = [ + "cfg-if", + "ordered-multimap", + "trim-in-place", +] + +[[package]] +name = "rustc-demangle" +version = "0.1.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "rustix" +version = "0.38.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f93dc38ecbab2eb790ff964bb77fa94faf256fd3e73285fd7ba0903b76bedb85" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.59.0", +] + +[[package]] +name = "rustversion" +version = "1.0.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7c45b9784283f1b2e7fb61b42047c2fd678ef0960d4f6f1eba131594cc369d4" + +[[package]] +name = "ryu" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "schannel" +version = "0.1.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f29ebaa345f945cec9fbbc532eb307f0fdad8161f281b6369539c8d84876b3d" +dependencies = [ + "windows-sys 0.59.0", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "security-framework" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" +dependencies = [ + "bitflags", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1863fd3768cd83c56a7f60faa4dc0d403f1b6df0a38c3c25f44b7894e45370d5" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "semver" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3cb6eb87a131f756572d7fb904f6e7b68633f09cca868c5df1c4b8d1a694bbba" + +[[package]] +name = "serde" +version = "1.0.217" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02fc4265df13d6fa1d00ecff087228cc0a2b5f3c0e87e258d8b94a156e984c70" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.217" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a9bf7cf98d04a2b28aead066b7496853d4779c9cc183c440dbac457641e19a0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.134" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d00f4175c42ee48b15416f6193a959ba3a0d67fc699a0db9ad12df9f83991c7d" +dependencies = [ + "itoa", + "memchr", + "ryu", + "serde", +] + +[[package]] +name = "serde_path_to_error" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af99884400da37c88f5e9146b7f1fd0fbcae8f6eec4e9da38b67d05486f814a6" +dependencies = [ + "itoa", + "serde", +] + +[[package]] +name = "serde_spanned" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87607cb1398ed59d48732e575a4c28a7a8ebf2454b964fe3f224f2afc07909e1" +dependencies = [ + "serde", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "sha1" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "sha2" +version = "0.10.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "signal-hook-registry" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1" +dependencies = [ + "libc", +] + +[[package]] +name = "signature" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" +dependencies = [ + "digest", + "rand_core", +] + +[[package]] +name = "simd-adler32" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" + +[[package]] +name = "simple_asn1" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adc4e5204eb1910f40f9cfa375f6f05b68c3abac4b6fd879c8ff5e7ae8a0a085" +dependencies = [ + "num-bigint", + "num-traits", + "thiserror 1.0.69", + "time", +] + +[[package]] +name = "slab" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" +dependencies = [ + "autocfg", +] + +[[package]] +name = "smallvec" +version = "1.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" +dependencies = [ + "serde", +] + +[[package]] +name = "socket2" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c970269d99b64e60ec3bd6ad27270092a5394c4e309314b18ae3fe575695fbe8" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" +dependencies = [ + "lock_api", +] + +[[package]] +name = "spki" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" +dependencies = [ + "base64ct", + "der", +] + +[[package]] +name = "sqlx" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4410e73b3c0d8442c5f99b425d7a435b5ee0ae4167b3196771dd3f7a01be745f" +dependencies = [ + "sqlx-core", + "sqlx-macros", + "sqlx-mysql", + "sqlx-postgres", + "sqlx-sqlite", +] + +[[package]] +name = "sqlx-core" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a007b6936676aa9ab40207cde35daab0a04b823be8ae004368c0793b96a61e0" +dependencies = [ + "bytes", + "chrono", + "crc", + "crossbeam-queue", + "either", + "event-listener", + "futures-core", + "futures-intrusive", + "futures-io", + "futures-util", + "hashbrown 0.15.2", + "hashlink 0.10.0", + "indexmap", + "log", + "memchr", + "once_cell", + "percent-encoding", + "serde", + "serde_json", + "sha2", + "smallvec", + "thiserror 2.0.9", + "tokio", + "tokio-stream", + "tracing", + "url", +] + +[[package]] +name = "sqlx-macros" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3112e2ad78643fef903618d78cf0aec1cb3134b019730edb039b69eaf531f310" +dependencies = [ + "proc-macro2", + "quote", + "sqlx-core", + "sqlx-macros-core", + "syn", +] + +[[package]] +name = "sqlx-macros-core" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e9f90acc5ab146a99bf5061a7eb4976b573f560bc898ef3bf8435448dd5e7ad" +dependencies = [ + "dotenvy", + "either", + "heck", + "hex", + "once_cell", + "proc-macro2", + "quote", + "serde", + "serde_json", + "sha2", + "sqlx-core", + "sqlx-mysql", + "sqlx-postgres", + "sqlx-sqlite", + "syn", + "tempfile", + "tokio", + "url", +] + +[[package]] +name = "sqlx-mysql" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4560278f0e00ce64938540546f59f590d60beee33fffbd3b9cd47851e5fff233" +dependencies = [ + "atoi", + "base64 0.22.1", + "bitflags", + "byteorder", + "bytes", + "chrono", + "crc", + "digest", + "dotenvy", + "either", + "futures-channel", + "futures-core", + "futures-io", + "futures-util", + "generic-array", + "hex", + "hkdf", + "hmac", + "itoa", + "log", + "md-5", + "memchr", + "once_cell", + "percent-encoding", + "rand", + "rsa", + "serde", + "sha1", + "sha2", + "smallvec", + "sqlx-core", + "stringprep", + "thiserror 2.0.9", + "tracing", + "whoami", +] + +[[package]] +name = "sqlx-postgres" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5b98a57f363ed6764d5b3a12bfedf62f07aa16e1856a7ddc2a0bb190a959613" +dependencies = [ + "atoi", + "base64 0.22.1", + "bitflags", + "byteorder", + "chrono", + "crc", + "dotenvy", + "etcetera", + "futures-channel", + "futures-core", + "futures-util", + "hex", + "hkdf", + "hmac", + "home", + "itoa", + "log", + "md-5", + "memchr", + "once_cell", + "rand", + "serde", + "serde_json", + "sha2", + "smallvec", + "sqlx-core", + "stringprep", + "thiserror 2.0.9", + "tracing", + "whoami", +] + +[[package]] +name = "sqlx-sqlite" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f85ca71d3a5b24e64e1d08dd8fe36c6c95c339a896cc33068148906784620540" +dependencies = [ + "atoi", + "chrono", + "flume", + "futures-channel", + "futures-core", + "futures-executor", + "futures-intrusive", + "futures-util", + "libsqlite3-sys", + "log", + "percent-encoding", + "serde", + "serde_urlencoded", + "sqlx-core", + "tracing", + "url", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" + +[[package]] +name = "stringprep" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b4df3d392d81bd458a8a621b8bffbd2302a12ffe288a9d931670948749463b1" +dependencies = [ + "unicode-bidi", + "unicode-normalization", + "unicode-properties", +] + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "strum" +version = "0.26.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06" +dependencies = [ + "strum_macros", +] + +[[package]] +name = "strum_macros" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "rustversion", + "syn", +] + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "2.0.95" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46f71c0377baf4ef1cc3e3402ded576dccc315800fbc62dfc7fe04b009773b4a" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" + +[[package]] +name = "synstructure" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tempfile" +version = "3.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8a559c81686f576e8cd0290cd2a24a2a9ad80c98b3478856500fcbd7acd704" +dependencies = [ + "cfg-if", + "fastrand", + "getrandom", + "once_cell", + "rustix", + "windows-sys 0.59.0", +] + +[[package]] +name = "termcolor" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f072643fd0190df67a8bab670c20ef5d8737177d6ac6b2e9a236cb096206b2cc" +dependencies = [ + "thiserror-impl 2.0.9", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b50fa271071aae2e6ee85f842e2e28ba8cd2c5fb67f11fcb1fd70b276f9e7d4" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "thread_local" +version = "1.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b9ef9bad013ada3808854ceac7b46812a6465ba368859a37e2100283d2d719c" +dependencies = [ + "cfg-if", + "once_cell", +] + +[[package]] +name = "time" +version = "0.3.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35e7868883861bd0e56d9ac6efcaaca0d6d5d82a2a7ec8209ff492c07cf37b21" +dependencies = [ + "deranged", + "itoa", + "libc", + "num-conv", + "num_threads", + "powerfmt", + "serde", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" + +[[package]] +name = "time-macros" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2834e6017e3e5e4b9834939793b282bc03b37a3336245fa820e35e233e2a85de" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "tiny-keccak" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237" +dependencies = [ + "crunchy", +] + +[[package]] +name = "tinystr" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9117f5d4db391c1cf6927e7bea3db74b9a1c1add8f7eda9ffd5364f40f57b82f" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tinyvec" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "022db8904dfa342efe721985167e9fcd16c29b226db4397ed752a761cfce81e8" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "tokio" +version = "1.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5cec9b21b0450273377fc97bd4c33a8acffc8c996c987a7c5b319a0083707551" +dependencies = [ + "backtrace", + "bytes", + "libc", + "mio", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "windows-sys 0.52.0", +] + +[[package]] +name = "tokio-macros" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tokio-native-tls" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" +dependencies = [ + "native-tls", + "tokio", +] + +[[package]] +name = "tokio-stream" +version = "0.1.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eca58d7bba4a75707817a2c44174253f9236b2d5fbd055602e9d5c07c139a047" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tokio-util" +version = "0.7.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7fcaa8d55a2bdd6b83ace262b016eca0d79ee02818c5c1bcdf0305114081078" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "futures-util", + "hashbrown 0.14.5", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "toml" +version = "0.8.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1ed1f98e3fdc28d6d910e6737ae6ab1a93bf1985935a1193e68f93eeb68d24e" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime", + "toml_edit", +] + +[[package]] +name = "toml_datetime" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_edit" +version = "0.22.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ae48d6208a266e853d946088ed816055e556cc6028c5e8e2b84d9fa5dd7c7f5" +dependencies = [ + "indexmap", + "serde", + "serde_spanned", + "toml_datetime", + "winnow", +] + +[[package]] +name = "tower" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" +dependencies = [ + "log", + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-appender" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3566e8ce28cc0a3fe42519fc80e6b4c943cc4c8cef275620eb8dac2d3d4e06cf" +dependencies = [ + "crossbeam-channel", + "thiserror 1.0.69", + "time", + "tracing-subscriber", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "395ae124c09f9e6918a2310af6038fba074bcf474ac352496d5910dd59a2226d" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tracing-core" +version = "0.1.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-rolling-file" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9cdf73ffe536cc623d6a101a3acb6ea7b5db28af8fca9709e3a8f8bce722cd16" +dependencies = [ + "chrono", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8189decb5ac0fa7bc8b96b7cb9b2701d60d48805aca84a238004d665fcc4008" +dependencies = [ + "matchers", + "nu-ansi-term", + "once_cell", + "regex", + "sharded-slab", + "smallvec", + "thread_local", + "time", + "tracing", + "tracing-core", + "tracing-log", +] + +[[package]] +name = "trim-in-place" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "343e926fc669bc8cde4fa3129ab681c63671bae288b1f1081ceee6d9d37904fc" + +[[package]] +name = "ts-rs" +version = "10.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e640d9b0964e9d39df633548591090ab92f7a4567bc31d3891af23471a3365c6" +dependencies = [ + "chrono", + "lazy_static", + "thiserror 2.0.9", + "ts-rs-macros", +] + +[[package]] +name = "ts-rs-macros" +version = "10.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e9d8656589772eeec2cf7a8264d9cda40fb28b9bc53118ceb9e8c07f8f38730" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "termcolor", +] + +[[package]] +name = "typenum" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" + +[[package]] +name = "ucd-trie" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971" + +[[package]] +name = "unicase" +version = "2.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75b844d17643ee918803943289730bec8aac480150456169e647ed0b576ba539" + +[[package]] +name = "unicode-bidi" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5" + +[[package]] +name = "unicode-ident" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adb9e6ca4f869e1180728b7950e35922a7fc6397f7b641499e8f3ef06e50dc83" + +[[package]] +name = "unicode-normalization" +version = "0.1.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5033c97c4262335cded6d6fc3e5c18ab755e1a3dc96376350f3d8e9f009ad956" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "unicode-properties" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e70f2a8b45122e719eb623c01822704c4e0907e7e426a05927e1a1cfff5b75d0" + +[[package]] +name = "unicode-segmentation" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "url" +version = "2.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", +] + +[[package]] +name = "utf16_iter" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8232dd3cdaed5356e0f716d285e4b40b932ac434100fe9b7e0e8e935b9e6246" + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "utoipa" +version = "5.3.0" +source = "git+https://github.com/juhaku/utoipa#3ffad4bed73e5caeddc311bab70f810d1d772079" +dependencies = [ + "indexmap", + "serde", + "serde_json", + "utoipa-gen", +] + +[[package]] +name = "utoipa-axum" +version = "0.1.3" +source = "git+https://github.com/juhaku/utoipa#3ffad4bed73e5caeddc311bab70f810d1d772079" +dependencies = [ + "axum", + "paste", + "tower-layer", + "tower-service", + "utoipa", +] + +[[package]] +name = "utoipa-gen" +version = "5.3.0" +source = "git+https://github.com/juhaku/utoipa#3ffad4bed73e5caeddc311bab70f810d1d772079" +dependencies = [ + "proc-macro2", + "quote", + "regex", + "syn", +] + +[[package]] +name = "utoipa-redoc" +version = "5.0.0" +source = "git+https://github.com/juhaku/utoipa#3ffad4bed73e5caeddc311bab70f810d1d772079" +dependencies = [ + "axum", + "serde", + "serde_json", + "utoipa", +] + +[[package]] +name = "utoipa-scalar" +version = "0.2.0" +source = "git+https://github.com/juhaku/utoipa#3ffad4bed73e5caeddc311bab70f810d1d772079" +dependencies = [ + "axum", + "serde", + "serde_json", + "utoipa", +] + +[[package]] +name = "utoipa-swagger-ui" +version = "8.1.0" +source = "git+https://github.com/juhaku/utoipa#3ffad4bed73e5caeddc311bab70f810d1d772079" +dependencies = [ + "axum", + "base64 0.22.1", + "mime_guess", + "regex", + "rust-embed", + "serde", + "serde_json", + "url", + "utoipa", + "zip", +] + +[[package]] +name = "uuid" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8c5f0a0af699448548ad1a2fbf920fb4bee257eae39953ba95cb84891a0446a" +dependencies = [ + "getrandom", +] + +[[package]] +name = "valuable" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "wasite" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b" + +[[package]] +name = "wasm-bindgen" +version = "0.2.99" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a474f6281d1d70c17ae7aa6a613c87fce69a127e2624002df63dcb39d6cf6396" +dependencies = [ + "cfg-if", + "once_cell", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.99" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f89bb38646b4f81674e8f5c3fb81b562be1fd936d84320f3264486418519c79" +dependencies = [ + "bumpalo", + "log", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.99" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2cc6181fd9a7492eef6fef1f33961e3695e4579b9872a6f7c83aee556666d4fe" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.99" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30d7a95b763d3c45903ed6c81f156801839e5ee968bb07e534c44df0fcd330c2" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.99" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "943aab3fdaaa029a6e0271b35ea10b72b943135afe9bffca82384098ad0e06a6" + +[[package]] +name = "whoami" +version = "1.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "372d5b87f58ec45c384ba03563b03544dc5fadc3983e434b286913f5b4a9bb6d" +dependencies = [ + "redox_syscall", + "wasite", +] + +[[package]] +name = "widestring" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7219d36b6eac893fa81e84ebe06485e7dcbb616177469b142df14f1f4deb1311" + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" +dependencies = [ + "windows-sys 0.59.0", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-core" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-service" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d24d6bcc7f734a4091ecf8d7a64c5f7d7066f45585c1861eba06449909609c8a" +dependencies = [ + "bitflags", + "widestring", + "windows-sys 0.52.0", +] + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.5", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + +[[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.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + +[[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.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + +[[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.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + +[[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.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + +[[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.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + +[[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.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "winnow" +version = "0.6.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39281189af81c07ec09db316b302a3e67bf9bd7cbf6c820b50e35fee9c2fa980" +dependencies = [ + "memchr", +] + +[[package]] +name = "write16" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1890f4022759daae28ed4fe62859b1236caebfc61ede2f63ed4e695f3f6d936" + +[[package]] +name = "writeable" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51" + +[[package]] +name = "yaml-rust2" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a1a1c0bc9823338a3bdf8c61f994f23ac004c6fa32c08cd152984499b445e8d" +dependencies = [ + "arraydeque", + "encoding_rs", + "hashlink 0.9.1", +] + +[[package]] +name = "yoke" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "120e6aef9aa629e3d4f52dc8cc43a015c7724194c97dfaf45180d2daf2b77f40" +dependencies = [ + "serde", + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2380878cad4ac9aac1e2435f3eb4020e8374b5f13c296cb75b4620ff8e229154" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zerocopy" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" +dependencies = [ + "byteorder", + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zerofrom" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cff3ee08c995dee1859d998dea82f7374f2826091dd9cd47def953cae446cd2e" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "595eed982f7d355beb85837f651fa22e90b3c044842dc7f2c2842c086f295808" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zeroize" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" + +[[package]] +name = "zerovec" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa2b893d79df23bfb12d5461018d408ea19dfafe76c2c7ef6d4eba614f8ff079" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zip" +version = "2.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae9c1ea7b3a5e1f4b922ff856a129881167511563dc219869afe3787fc0c1a45" +dependencies = [ + "arbitrary", + "crc32fast", + "crossbeam-utils", + "displaydoc", + "flate2", + "indexmap", + "memchr", + "thiserror 2.0.9", + "zopfli", +] + +[[package]] +name = "zopfli" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5019f391bac5cf252e93bbcc53d039ffd62c7bfb7c150414d61369afe57e946" +dependencies = [ + "bumpalo", + "crc32fast", + "lockfree-object-pool", + "log", + "once_cell", + "simd-adler32", +] diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..a493c73 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,66 @@ +[package] +name = "axum-api-test" +version = "0.1.0" +edition = "2021" + +[dependencies] +# Logging +# ======================================== +tracing = "0.1.41" +tracing-subscriber = { version = "0.3.19", features = [ + "local-time", + "env-filter", +] } +tracing-appender = "0.2.3" +tracing-rolling-file = "0.1.2" +error-stack = "0.5.0" + +# CLI +# ======================================== +dotenv = "0.15" +clap = { version = "4.5.23", features = ["derive"] } +config = "0.15.4" + +# User Authentication +# ======================================== +uuid = { version = "1.11.0", features = ["v4"] } +sha2 = "0.10.8" +hmac = "0.12.1" +# axum-jwt-login = { path = "../axum-login-jwt" } +axum-jwt-login = { version = "0.1.0", registry = "kellnr" } +rust-argon2 = "2.1.0" +rand = "0.8.5" +ldap3 = "0.11.5" + +# Service +# ======================================== +windows-service = "0.7.0" + +axum = { version = "0.8.1", features = ["macros"] } +strum = { version = "0.26", features = ["derive"] } +# utoipa = { version = "5.3.0", features = ["axum_extras"] } +utoipa = { git = "https://github.com/juhaku/utoipa", features = [ + "axum_extras", +] } +# utoipa-axum = "0.1.3" +utoipa-axum = { git = "https://github.com/juhaku/utoipa" } +# utoipa-swagger-ui = { version = "8.1.0", features = ["axum"] } +# utoipa-redoc = { version = "*", features = ["axum"] } +# utoipa-scalar = { version = "*", features = ["axum"] } +utoipa-swagger-ui = { git = "https://github.com/juhaku/utoipa", features = [ + "axum", +] } +utoipa-redoc = { git = "https://github.com/juhaku/utoipa", features = ["axum"] } +utoipa-scalar = { git = "https://github.com/juhaku/utoipa", features = [ + "axum", +] } +ts-rs = { version = "10.1.0", features = ["chrono-impl"] } + +# Utilities +# ======================================== +serde = { version = "1.0.216", features = ["derive"] } +tokio = { version = "1.42.0", features = ["full"] } +tokio-util = { version = "0.7.13", features = ["rt"] } +once_cell = "1.20.2" +sqlx = { version = "0.8.3", features = ["runtime-tokio", "postgres", "chrono"] } +chrono = { version = "0.4.39", features = ["serde"] } diff --git a/src/api/backend/ldap.rs b/src/api/backend/ldap.rs new file mode 100644 index 0000000..2eb7c89 --- /dev/null +++ b/src/api/backend/ldap.rs @@ -0,0 +1,206 @@ +use std::collections::HashMap; + +use ldap3::{Ldap, LdapConnAsync, LdapConnSettings, Scope, SearchEntry}; +use serde::{Deserialize, Serialize}; +use ts_rs::TS; +use utoipa::ToSchema; + +use crate::{ + config::{Configuration, LDAP}, + errors::ApiError, +}; + +#[derive(Debug, Clone, Serialize, Deserialize, TS, ToSchema)] +#[ts(export)] +pub struct ActiveDirectoryUser { + pub name: String, + pub surname: String, + pub email: String, + pub id: String, +} + +pub struct LDAPBackend { + connection: Ldap, + config: LDAP, + bound: bool, +} + +impl LDAPBackend { + pub async fn from_config(config: &Configuration) -> Result { + // create connection to LDAP server + let ldap_settings = LdapConnSettings::new().set_no_tls_verify(config.ldap.skip_tls_verify); + let (connection, ldap) = LdapConnAsync::with_settings(ldap_settings, &config.ldap.server) + .await + .map_err(|e| ApiError::InternalError(format!("LDAP Server connection: {e}")))?; + ldap3::drive!(connection); + + Ok(Self { + config: config.ldap.clone(), + connection: ldap, + bound: false, + }) + } + + pub async fn unbind(&mut self) { + let _ = self.connection.unbind().await; + self.bound = false; + } + + pub async fn ad_bind(&mut self, user: &str, password: &str) -> Result<(), ApiError> { + self.connection + .simple_bind(&format!("{}\\{user}", self.config.ad_domain), &password) + .await + .map_err(|_e| ApiError::InvalidCredentials)?; + + self.bound = true; + + Ok(()) + } + + pub async fn get_user_detail( + &mut self, + user_id: &str, + ) -> Result>, ApiError> { + // check if elevated user has to be used + if self.config.elevated_search { + let id = self.config.elevated_user_id.clone(); + let password = self.config.elevated_user_pw.clone(); + + self.ad_bind(&id, &password).await?; + } + let (search_result, _) = self + .connection + .search( + &self.config.user_search_base, + Scope::Subtree, + // "(objectClass=*)", + &format!("(&(ObjectCategory=Person)(sAMAccountName={user_id}))"), + vec!["givenName", "sn", "mail"], + // vec!["*"], + ) + .await + .map_err(|e| ApiError::InternalError(format!("LDAP search error: {e}")))? + .success() + .map_err(|e| ApiError::InternalError(format!("LDAP search error: {e}")))?; + + let attributes = if let Some(entry) = search_result.first().cloned() { + SearchEntry::construct(entry).attrs + } else { + HashMap::new() + }; + + Ok(attributes) + } + + pub async fn get_ad_user_list(&mut self) -> Result, ApiError> { + // check if elevated user has to be used + if self.config.elevated_search { + let id = self.config.elevated_user_id.clone(); + let password = self.config.elevated_user_pw.clone(); + + self.ad_bind(&id, &password).await?; + } + + let (search_result, _) = self + .connection + .search( + &self.config.user_search_base, + Scope::Subtree, + &format!("(&(ObjectCategory=Person))"), + vec!["sAMAccountName", "givenName", "sn", "mail"], + ) + .await + .map_err(|e| ApiError::InternalError(format!("LDAP search error: {e}")))? + .success() + .map_err(|e| ApiError::InternalError(format!("LDAP search error: {e}")))?; + + let result = search_result + .into_iter() + .map(|entry| { + let user_info = SearchEntry::construct(entry).attrs; + + ActiveDirectoryUser { + name: user_info + .get("givenName") + .map(|v| v.first().cloned()) + .flatten() + .unwrap_or_default(), + surname: user_info + .get("sn") + .map(|v| v.first().cloned()) + .flatten() + .unwrap_or_default(), + email: user_info + .get("mail") + .map(|v| v.first().cloned()) + .flatten() + .unwrap_or_default(), + id: user_info + .get("sAMAccountName") + .map(|v| v.first().cloned()) + .flatten() + .unwrap_or_default(), + } + }) + .collect(); + + Ok(result) + } +} + +#[cfg(test)] +mod test { + use ldap3::{exop::WhoAmI, LdapConnAsync, LdapConnSettings, Scope, SearchEntry}; + + #[tokio::test] + async fn test_ldap() { + let (connection, mut ldap) = LdapConnAsync::with_settings( + LdapConnSettings::new().set_no_tls_verify(true), + "ldaps://localhost:636", + ) + .await + .unwrap(); + ldap3::drive!(connection); + + // bind to server + let res = ldap + // .simple_bind("cn=read-only-admin,dc=example,dc=com", "password") + .simple_bind( + // "CN=Abel Austin,OU=Accounting,OU=Mylab Users,DC=mylab,DC=local", + // "MYLAB\\A0H67123", + // "cn=read-only-admin,dc=example,dc=com", + // "DEV-AD\\Administrator", + // "admin123!", + "DEV-AD\\einstein", + "einstein", + ) + .await; + + println!("{res:?}"); + + let (res, re) = ldap + .search( + // "CN=Abel Austin,OU=Accounting,OU=Mylab Users,DC=mylab,DC=local", + "OU=DevUsers,DC=example,DC=org", + Scope::Subtree, + "(objectClass=*)", + // "(&(ObjectCategory=Person)(sAMAccountName=A0H67123))", + vec!["givenName", "sn"], + ) + .await + .unwrap() + .success() + .unwrap(); + + for entry in res { + println!("{:?}", SearchEntry::construct(entry)); + } + + let test = ldap.extended(WhoAmI).await.unwrap().0; + println!("{test:?}"); + // let whoami: ldap3::exop::WhoAmIResp = test.parse(); + // println!("{whoami:?}"); + + let _ = ldap.unbind().await; + } +} diff --git a/src/api/backend/mod.rs b/src/api/backend/mod.rs new file mode 100644 index 0000000..8cff223 --- /dev/null +++ b/src/api/backend/mod.rs @@ -0,0 +1,70 @@ +use axum_jwt_login::{AuthBackend, UserPermissions}; +use ldap::LDAPBackend; +use sqlx::PgPool; + +use crate::{ + api::routes::users::{models::UserStatus, sql::get_users}, + config::Configuration, + errors::ApiError, +}; + +use super::routes::{auth::models::Credentials, users::models::User}; + +pub mod ldap; + +#[derive(Debug, Clone)] +pub struct ApiBackend { + pool: PgPool, + config: Configuration, +} + +impl ApiBackend { + pub fn new(pool: PgPool, config: &Configuration) -> Self { + Self { + pool, + config: config.clone(), + } + } + + pub fn pool(&self) -> &PgPool { + &self.pool + } + + pub fn config(&self) -> &Configuration { + &self.config + } +} + +impl AuthBackend for ApiBackend { + type Credentials = Credentials; + type Error = ApiError; + + async fn authenticate( + &self, + Credentials { id, password }: Self::Credentials, + ) -> Result, Self::Error> { + // get user from Database + let user = get_users(&self.pool, Some(UserStatus::Active), Some(id)).await?; + let user = user.first().ok_or(ApiError::InvalidCredentials)?; + + // authenticate user + if user.active_directory_auth { + // authenticate against LDAP (AD) server + // -- + let mut ldap = LDAPBackend::from_config(&self.config).await?; + ldap.ad_bind(&user.id(), &password).await?; + + // update user information + let details = ldap.get_user_detail(&user.id()).await?; + user.update_with_ad_details(&self.pool, details).await?; + + // terminate connection + ldap.unbind().await; + } else { + argon2::verify_encoded(&user.password, password.as_bytes()) + .map_err(|_| ApiError::InvalidCredentials)?; + } + + Ok(Some(user.clone())) + } +} diff --git a/src/api/description.rs b/src/api/description.rs new file mode 100644 index 0000000..28412e7 --- /dev/null +++ b/src/api/description.rs @@ -0,0 +1,41 @@ +use utoipa::{ + openapi::security::{ApiKey, ApiKeyValue, HttpAuthScheme, HttpBuilder, SecurityScheme}, + Modify, OpenApi, +}; + +pub const AUTH_TAG: &str = "Authentication"; +pub const USERS_TAG: &str = "Users"; +pub const ORDER_TAG: &str = "order"; +pub const API_KEY_TAG: &str = "API Keys"; + +#[derive(OpenApi)] +#[openapi( + modifiers(&SecurityAddon), + tags( + (name = AUTH_TAG, description = "API Authentication endpoints"), + (name = ORDER_TAG, description = "Order API endpoints") + ), +)] +pub struct ApiDocumentation; + +struct SecurityAddon; + +impl Modify for SecurityAddon { + fn modify(&self, openapi: &mut utoipa::openapi::OpenApi) { + if let Some(components) = openapi.components.as_mut() { + components.add_security_scheme( + "api_key", + SecurityScheme::ApiKey(ApiKey::Header(ApiKeyValue::new("x-api-key"))), + ); + components.add_security_scheme( + "user_auth", + SecurityScheme::Http( + HttpBuilder::new() + .scheme(HttpAuthScheme::Bearer) + .bearer_format("JWT") + .build(), + ), + ); + } + } +} diff --git a/src/api/mod.rs b/src/api/mod.rs new file mode 100644 index 0000000..128d597 --- /dev/null +++ b/src/api/mod.rs @@ -0,0 +1,202 @@ +use std::{future::IntoFuture, net::Ipv4Addr, time::Duration}; + +use axum::http::HeaderName; +use axum_jwt_login::{AuthSessionWithApiKeyBuilder, JWTKeyPair}; +use backend::ApiBackend; +use error_stack::{Report, ResultExt}; +use routes::api_keys::models::ApiKey; +use sqlx::PgPool; +use tokio::net::TcpListener; +use tokio_util::sync::CancellationToken; +use tracing::{info, level_filters::LevelFilter, Level}; +use tracing_rolling_file::{RollingConditionBase, RollingFileAppenderBase}; +use tracing_subscriber::{fmt, layer::SubscriberExt, util::SubscriberInitExt}; + +use crate::APP_NAME; +use crate::{config::Configuration, errors::AppError, ROOT_PATH}; + +pub mod backend; +mod description; +pub mod routes; + +pub async fn start( + config: &Configuration, + cancellation_token: CancellationToken, +) -> Result<(), Report> { + // Set Report Colour Mode to NONE + Report::set_color_mode(error_stack::fmt::ColorMode::None); + + let level_filter = LevelFilter::from_level(match config.debug { + true => Level::DEBUG, + false => Level::INFO, + }); + + // Prepare logging to file + let file_appender = RollingFileAppenderBase::new( + ROOT_PATH.with_file_name(format!("{}.log", env!("CARGO_PKG_NAME"))), + RollingConditionBase::new().max_size(1024 * 1024 * 2), + 5, + ) + .change_context(AppError)?; + let (non_blocking, _guard) = tracing_appender::non_blocking(file_appender); + + // prepare initialization of logging + let log_layers = tracing_subscriber::registry().with(level_filter).with( + fmt::Layer::default() + .with_target(false) + .with_ansi(false) + .with_writer(non_blocking), + ); + + // also log to console in debug mode + #[cfg(debug_assertions)] + let stdout_log = tracing_subscriber::fmt::layer().pretty(); + #[cfg(debug_assertions)] + let log_layers = log_layers.with(stdout_log); + + // Initialize logging + log_layers.init(); + + // Initialize database pool + let pool = PgPool::connect(&config.database_url) + .await + .change_context(AppError)?; + // Get API Keys from database + let api_keys = ApiKey::get_all_with_secret_attached(&pool, config) + .await + .change_context(AppError)?; + + // Initialize API Backend + let backend = ApiBackend::new(pool.clone(), config); + // Create Session storage for API + let session = AuthSessionWithApiKeyBuilder::new() + .backend(backend) + .api_header_name(HeaderName::from_static("x-api-key")) + .initial_api_keys(api_keys) + .jwt_key_pair(JWTKeyPair::from_secret(&config.token_secret)) + .session_length(Duration::from_secs(config.session_length)) + .build() + .change_context(AppError)?; + + // Create routes + let router = routes::create_routes(session); + + // Define listener + let port = config.port; + let listener = TcpListener::bind((Ipv4Addr::LOCALHOST, port)) + .await + .change_context(AppError)?; + + // Serve API + let server = axum::serve(listener, router.into_make_service()) + .with_graceful_shutdown(shutdown_signal(cancellation_token)); + + tokio::spawn(server.into_future()); + + info!("{APP_NAME} up and running on port '{port}'."); + + Ok(()) +} + +async fn shutdown_signal(stop_signal: CancellationToken) { + // Wait for shutdown event. + stop_signal.cancelled().await; + + info!("Shutting down {APP_NAME}..."); +} + +// // Set Report Colour Mode to NONE +// Report::set_color_mode(error_stack::fmt::ColorMode::None); + +// // Enable the `INFO` level for anything in `darl` as default +// let level_filter = +// filter::Targets::new().with_target(env!("CARGO_PKG_NAME"), DEFAULT_LOG_LEVEL_FILTER); +// let (level_filter, tracing_target_reload_handle) = reload::Layer::new(level_filter); + +// // Prepare logging to file +// let file_appender = RollingFileAppenderBase::new( +// ROOT_PATH.with_file_name(format!("{}.log", env!("CARGO_PKG_NAME"))), +// RollingConditionBase::new().max_size(1024 * 1024 * 2), +// 5, +// ) +// .change_context(ServiceError::Starting)?; +// let (non_blocking, _guard) = tracing_appender::non_blocking(file_appender); + +// // Prepare live logging to config webserver +// let (log_receiver, live_log_layer) = LiveLogLayer::new(); + +// // prepare initialization of logging +// let log_layers = tracing_subscriber::registry() +// .with(level_filter) +// // .with(filter::LevelFilter::DEBUG) +// .with(live_log_layer) +// .with( +// fmt::Layer::default() +// .with_target(false) +// .with_ansi(false) +// .with_writer(non_blocking), +// ); + +// // also log to console in debug mode +// #[cfg(debug_assertions)] +// let stdout_log = tracing_subscriber::fmt::layer().pretty(); +// #[cfg(debug_assertions)] +// let log_layers = log_layers.with(stdout_log); + +// // Initialize logging +// log_layers.init(); + +// // Initialize local database +// let local_database = LocalDatabase::init() +// .await +// .change_context(ServiceError::Starting)?; + +// // Load configuration from config files +// let (config, external_database) = Configuration::initialize(&local_database) +// .await +// .change_context(ServiceError::Starting)?; +// let standalone_external_db = StandaloneExternalDatabase::from(&external_database); + +// // change log level to configured value +// if let Err(error) = tracing_target_reload_handle.modify(|filter| { +// *filter = filter::Targets::new().with_target(env!("CARGO_PKG_NAME"), &config.log_level) +// }) { +// error!("{error}"); +// } + +// // prepare and start connections +// let connections = MachineConnections::init(&local_database, &standalone_external_db, &config) +// .await +// .change_context(ServiceError::Starting)?; + +// // start config server +// ConfigServer::start( +// config.extended_config.webserver.config_server_port, +// config.opc_configuration.clone(), +// &local_database, +// standalone_external_db, +// connections, +// tracing_target_reload_handle, +// log_receiver, +// ) +// .await; + +// // start webserver +// WebServer::start(&config, &local_database).await; + +// // initialize Logging to external database +// external_database.start_writer().await; + +// info!("{APP_NAME} service is now running..."); + +// // block thread +// loop { +// // Poll shutdown event. +// if (stop_signal.recv().await).is_some() { +// // Break the loop either upon stop or channel disconnect +// info!("Shutting down {APP_NAME} service"); +// break; +// }; +// } + +// Ok(()) diff --git a/src/api/routes/api_keys/mod.rs b/src/api/routes/api_keys/mod.rs new file mode 100644 index 0000000..1e1bb3e --- /dev/null +++ b/src/api/routes/api_keys/mod.rs @@ -0,0 +1,145 @@ +use axum::{debug_handler, extract::Query, Extension, Json}; +use models::ApiKey; +use serde::Deserialize; +use utoipa::IntoParams; +use utoipa_axum::{router::OpenApiRouter, routes}; + +use crate::{ + api::{ + backend::ApiBackend, + description::API_KEY_TAG, + routes::{ + users::permissions::{Permission, PermissionDetail}, + User, + }, + }, + errors::ApiError, + permission_required, +}; + +use super::AuthBackendType; + +pub mod models; +pub mod sql; + +// expose the OpenAPI to parent module +pub fn router() -> OpenApiRouter { + let read = OpenApiRouter::new() + .routes(routes!(get_api_keys)) + .route_layer(permission_required!(Permission::Read( + PermissionDetail::APIKeys + ))); + let write = OpenApiRouter::new() + .routes(routes!(create_api_key, update_api_key, delete_api_key)) + .route_layer(permission_required!(Permission::Write( + PermissionDetail::APIKeys + ))); + + OpenApiRouter::new().merge(read).merge(write) +} + +#[debug_handler] +#[utoipa::path( + get, + path = "/apikeys", + summary = "Get all API Keys", + description = "Get a list of all configured API Keys.", + responses( + (status = OK, body = Vec, description = "List of API Keys"), + ), + security( + ("user_auth" = ["read:apikeys",]), + ), + tag = API_KEY_TAG)] +pub async fn get_api_keys( + Extension(backend): Extension, +) -> Result>, ApiError> { + Ok(Json(sql::get_api_keys(backend.pool()).await?)) +} + +#[debug_handler] +#[utoipa::path( + post, + path = "/apikeys", + summary = "Create new API Key", + description = "Create a new API Key.", + request_body(content = ApiKey, description = "API Key details", content_type = "application/json"), + responses( + (status = OK, description = "API Key successfully created (API Key Secret in Body)", body = String), + ), + security( + ("user_auth" = ["write:apikeys",]), + ), + tag = API_KEY_TAG)] +pub async fn create_api_key( + auth_session: AuthBackendType, + Json(api_key): Json, +) -> Result { + let backend = auth_session.backend(); + + // create new API key + let (key_secret, key) = ApiKey::create( + &api_key.name, + api_key.auth_required, + api_key.permissions.0, + backend.config(), + )?; + + // insert API Key into database + sql::create_api_key(backend.pool(), &key).await?; + + // add API Key to session + auth_session.add_api_key(key).await; + + // Return key secret in response + Ok(key_secret) +} + +#[debug_handler] +#[utoipa::path( + put, + path = "/apikeys", + summary = "Update API Key", + description = "Update an API Key.", + request_body(content = ApiKey, description = "API Key details", content_type = "application/json"), + responses( + (status = OK, description = "API Key successfully updated"), + ), + security( + ("user_auth" = ["write:apikeys",]), + ), + tag = API_KEY_TAG)] +pub async fn update_api_key( + Extension(backend): Extension, + Json(api_key): Json, +) -> Result<(), ApiError> { + sql::update_api_key(backend.pool(), &api_key).await?; + Ok(()) +} + +#[derive(Debug, Deserialize, IntoParams)] +pub struct DeleteQueryParameters { + key_id: String, +} + +#[debug_handler] +#[utoipa::path( + delete, + path = "/apikeys", + summary = "Delete API Key", + description = "Delete an API Key.", + params(DeleteQueryParameters), + responses( + (status = OK, description = "API Key successfully deleted"), + ), + security( + ("user_auth" = ["write:apikeys",]), + ), + tag = API_KEY_TAG)] +pub async fn delete_api_key( + Extension(backend): Extension, + Query(params): Query, +) -> Result<(), ApiError> { + sql::delete_api_key(backend.pool(), ¶ms.key_id).await?; + Ok(()) +} diff --git a/src/api/routes/api_keys/models.rs b/src/api/routes/api_keys/models.rs new file mode 100644 index 0000000..545458c --- /dev/null +++ b/src/api/routes/api_keys/models.rs @@ -0,0 +1,143 @@ +use std::collections::HashSet; + +use axum_jwt_login::ApiKey as ApiKeyTrait; +use chrono::NaiveDateTime; +use hmac::{Hmac, Mac}; +use serde::{Deserialize, Serialize}; +use sha2::Sha512; +use sqlx::PgPool; +use ts_rs::TS; +use utoipa::ToSchema; +use uuid::Uuid; + +use crate::{ + api::routes::users::permissions::{Permission, PermissionContainer}, + config::Configuration, + errors::ApiError, + utils::create_random, +}; + +const KEY_LENGTH: usize = 40; + +#[derive(Debug, Clone, Serialize, Deserialize, TS, ToSchema)] +#[ts(export)] +pub struct ApiKey { + pub id: String, + #[serde(skip)] // Don't leak Hash + pub hash: Vec, + pub name: String, + pub auth_required: bool, + #[schema(inline)] + pub permissions: PermissionContainer, + #[serde(skip)] // Don't leak secret + pub api_config_secret: Option, + #[serde(default)] + #[schema(value_type = String, read_only)] + pub creation_date: Option, + #[serde(default)] + #[schema(value_type = String, read_only)] + pub last_change: Option, +} + +impl ApiKey { + pub async fn get_all_with_secret_attached( + pool: &PgPool, + config: &Configuration, + ) -> Result, ApiError> { + let mut keys = super::sql::get_api_keys(pool).await?; + + for key in keys.iter_mut() { + key.api_config_secret = Some(config.token_secret.clone()); + } + + Ok(keys) + } + + pub fn create( + name: &str, + requires_auth: bool, + permissions: HashSet, + config: &Configuration, + ) -> Result<(String, Self), ApiError> { + // create uuid + let uuid = Uuid::new_v4().simple(); + // create API Key secret part + let key: String = create_random(KEY_LENGTH); + // calculate API Key signature + let mut mac = Hmac::::new_from_slice(config.token_secret.as_bytes())?; + mac.update(key.as_bytes()); + let signature = mac.finalize(); + + // Return Api Key Secret and Api Key + Ok(( + key, + Self { + name: name.to_string(), + auth_required: requires_auth, + id: uuid.to_string(), + hash: signature.into_bytes().as_slice().to_vec(), + permissions: PermissionContainer(permissions), + api_config_secret: Some(config.token_secret.clone()), + creation_date: None, + last_change: None, + }, + )) + } + + pub fn validate(&self, key_secret_part: &str) -> Result<(), ApiError> { + // calculate API Key signature + let mut mac = Hmac::::new_from_slice( + self.api_config_secret + .clone() + .ok_or(ApiError::InternalError( + "Missing API Config Secret".to_string(), + ))? + .as_bytes(), + )?; + mac.update(key_secret_part.as_bytes()); + + match mac.verify_slice(&self.hash) { + Ok(_) => Ok(()), + Err(_e) => Err(ApiError::AccessDenied), + } + } +} + +impl ApiKeyTrait for ApiKey { + type Permission = Permission; + + fn id(&self) -> String { + self.id.clone() + } + fn validate(&self, key_secret_part: &str) -> bool { + self.validate(key_secret_part).is_ok() + } + fn requires_user_auth(&self) -> bool { + self.auth_required + } + fn get_permissions(&self) -> HashSet { + self.permissions.0.clone() + } +} + +#[cfg(test)] +mod test { + use std::collections::HashSet; + + use crate::config::Configuration; + + use super::ApiKey; + + #[tokio::test] + async fn test_api_key_creation_and_validation() { + let config = Configuration { + token_secret: "abcdefghijk".to_string(), + ..Default::default() + }; + let (key, api_key) = + ApiKey::create("name", true, HashSet::new(), &config).expect("Error creating API Key"); + println!("{key}, {api_key:?}"); + assert_eq!(Ok(()), api_key.validate(&key)); + assert_ne!(Ok(()), api_key.validate("124")); + } +} diff --git a/src/api/routes/api_keys/sql.rs b/src/api/routes/api_keys/sql.rs new file mode 100644 index 0000000..9c37f2e --- /dev/null +++ b/src/api/routes/api_keys/sql.rs @@ -0,0 +1,135 @@ +use sqlx::{PgPool, Postgres, Transaction}; + +use crate::errors::ApiError; + +use super::models::ApiKey; + +pub async fn get_api_keys(pool: &PgPool) -> Result, ApiError> { + Ok(sqlx::query_as!( + ApiKey, + r#"SELECT + APIKEYS."KeyID" as id, + APIKEYS."Name" as name, + APIKEYS."Hash" as hash, + APIKEYS."UserAuthRequired" as auth_required, + APIKEYS."CreationDate" as "creation_date?", + APIKEYS."LastChanged" as "last_change?", + NULL as api_config_secret, + array_remove(ARRAY_AGG(APIKEY_PERMISSIONS."Permission"), NULL) AS permissions + FROM + apikeys + LEFT JOIN APIKEY_PERMISSIONS ON APIKEY_PERMISSIONS."KeyID" = APIKEYS."KeyID" + GROUP BY APIKEYS."KeyID""# + ) + .fetch_all(pool) + .await?) +} + +pub async fn create_api_key(pool: &PgPool, api_key: &ApiKey) -> Result<(), ApiError> { + // start transaction + let mut transaction = pool.begin().await?; + + // create api_key + sqlx::query!( + r#"INSERT INTO apikeys + ("KeyID", "Name", "UserAuthRequired", "Hash") + VALUES ($1, $2, $3, $4)"#, + api_key.id, + api_key.name, + api_key.auth_required, + api_key.hash, + ) + .execute(&mut *transaction) + .await?; + + // update permissions + update_permissions(&mut transaction, &api_key).await?; + + // commit transaction + transaction.commit().await?; + + Ok(()) +} + +pub async fn update_api_key(pool: &PgPool, api_key: &ApiKey) -> Result<(), ApiError> { + // start transaction + let mut transaction = pool.begin().await?; + + // create api_key + sqlx::query!( + r#"UPDATE apikeys SET + "Name" = $2, + "UserAuthRequired" = $3 + WHERE "KeyID" = $1"#, + api_key.id, + api_key.name, + api_key.auth_required, + ) + .execute(&mut *transaction) + .await?; + + // update permissions + update_permissions(&mut transaction, &api_key).await?; + + // commit transaction + transaction.commit().await?; + + Ok(()) +} + +pub async fn update_permissions( + transaction: &mut Transaction<'static, Postgres>, + api_key: &ApiKey, +) -> Result<(), ApiError> { + // delete all permissions for API Key + sqlx::query!( + r#"DELETE FROM apikey_permissions + WHERE "KeyID" = $1"#, + api_key.id, + ) + .execute(&mut **transaction) + .await?; + + // insert permissions + for permission in &api_key.permissions.0 { + sqlx::query!( + r#"INSERT INTO apikey_permissions + ("KeyID", "Permission") + VALUES ($1, $2)"#, + api_key.id, + permission.to_string() + ) + .execute(&mut **transaction) + .await?; + } + + Ok(()) +} + +pub async fn delete_api_key(pool: &PgPool, key_id: &str) -> Result<(), ApiError> { + // start transaction + let mut transaction = pool.begin().await?; + + // Delete permissions + sqlx::query!( + r#"DELETE FROM apikey_permissions + WHERE "KeyID" = $1"#, + key_id, + ) + .execute(&mut *transaction) + .await?; + + // Delete API Key + sqlx::query!( + r#"DELETE FROM apikeys + WHERE "KeyID" = $1"#, + key_id, + ) + .execute(&mut *transaction) + .await?; + + // commit transaction + transaction.commit().await?; + + Ok(()) +} diff --git a/src/api/routes/auth/mod.rs b/src/api/routes/auth/mod.rs new file mode 100644 index 0000000..b6f82e7 --- /dev/null +++ b/src/api/routes/auth/mod.rs @@ -0,0 +1,62 @@ +use axum::{debug_handler, Json}; +use models::Credentials; +use utoipa_axum::{router::OpenApiRouter, routes}; + +use crate::{api::description::AUTH_TAG, errors::ApiError}; + +use super::AuthBackendType; + +pub mod models; + +// expose the OpenAPI to parent module +pub fn router() -> OpenApiRouter { + OpenApiRouter::new() + .routes(routes!(authorize)) + .routes(routes!(logout)) +} + +#[debug_handler] +#[utoipa::path( + post, + path = "/login", + summary = "Authenticate as user", + description = "Authenticate as user and receive a JWT auth token.", + request_body(content = Credentials, description = "User credentials", content_type = "application/json"), + responses( + (status = OK, body = String, description = "Successfully logged in (JWT Token in body)"), + (status = UNAUTHORIZED, description = "Invalid credentials or unauthorized user") + ), + tag = AUTH_TAG)] +pub async fn authorize( + mut auth_session: AuthBackendType, + Json(credentials): Json, +) -> Result { + let token = match auth_session.authenticate(credentials).await { + Ok(Some(_user)) => { + if let Some(token) = auth_session.get_auth_token() { + token + } else { + return Err(ApiError::InvalidCredentials); + } + } + Ok(None) => return Err(ApiError::InvalidCredentials), + Err(_) => return Err(ApiError::InvalidCredentials), + }; + + Ok(token) +} + +#[debug_handler] +#[utoipa::path( + post, + path = "/logout", + summary = "Logout", + description = "Log the currently logged in user out.", + responses( + (status = OK, description = "Logout successful") + ), + tag = AUTH_TAG)] +pub async fn logout(mut auth_session: AuthBackendType) -> Result<(), ApiError> { + auth_session.logout().await?; + Ok(()) +} diff --git a/src/api/routes/auth/models.rs b/src/api/routes/auth/models.rs new file mode 100644 index 0000000..62ee609 --- /dev/null +++ b/src/api/routes/auth/models.rs @@ -0,0 +1,8 @@ +use serde::Deserialize; +use utoipa::ToSchema; + +#[derive(Debug, Deserialize, ToSchema)] +pub struct Credentials { + pub id: String, + pub password: String, +} diff --git a/src/api/routes/mod.rs b/src/api/routes/mod.rs new file mode 100644 index 0000000..e1675e8 --- /dev/null +++ b/src/api/routes/mod.rs @@ -0,0 +1,64 @@ +pub mod api_keys; +pub mod auth; +pub mod users; + +use api_keys::models::ApiKey as APIKey; +use axum::{Extension, Router}; +use axum_jwt_login::AuthSessionWithApiKey; +use users::models::User; +use utoipa::OpenApi; +use utoipa_axum::router::OpenApiRouter; +use utoipa_redoc::{Redoc, Servable}; +use utoipa_scalar::{Scalar, Servable as ScalarServable}; +use utoipa_swagger_ui::SwaggerUi; + +use super::{backend::ApiBackend, description::ApiDocumentation}; + +const API_BASE: &str = "/api"; + +type AuthBackendType = AuthSessionWithApiKey; +#[macro_export] +macro_rules! login_required { + () => { + axum_jwt_login::login_required!(User, ApiBackend, APIKey) + }; +} +#[macro_export] +macro_rules! permission_required { + ($($perm:expr),+ $(,)?) => { + axum_jwt_login::permission_required!( + User, + ApiBackend, + crate::api::routes::APIKey, + $($perm),+ + ) + }; +} + +pub fn create_routes(session: AuthBackendType) -> Router { + let backend = session.backend(); + let (router, api) = OpenApiRouter::with_openapi(ApiDocumentation::openapi()) + // .routes(routes!(health)) + .nest(API_BASE, auth::router()) + .nest(API_BASE, users::router()) + .nest(API_BASE, api_keys::router()) + // .nest( + // "/api/order", + // // order::router().route_layer(crate::login_required!(AuthenticationBackend)), + // order::router().route_layer(login_required!(User, auth_backend::AuthenticationBackend)), + // ) + // .routes(routes!( + // inner::secret_handlers::get_secret, + // inner::secret_handlers::post_secret + // )) + // .nest("/api", users::router()) + // // .layer(auth_layer) + .layer(session.into_layer()) + .layer(Extension(backend)) + .split_for_parts(); + + router + .merge(SwaggerUi::new("/swagger-ui").url("/apidoc/openapi.json", api.clone())) + .merge(Redoc::with_url("/redoc", api.clone())) + .merge(Scalar::with_url("/scalar", api)) +} diff --git a/src/api/routes/users/mod.rs b/src/api/routes/users/mod.rs new file mode 100644 index 0000000..18ad969 --- /dev/null +++ b/src/api/routes/users/mod.rs @@ -0,0 +1,159 @@ +use axum::{debug_handler, Extension, Json}; +use axum_jwt_login::UserPermissions; +use models::User; +use permissions::{Permission, PermissionDetail}; +use utoipa_axum::{router::OpenApiRouter, routes}; + +use crate::{ + api::{ + backend::{ + ldap::{ActiveDirectoryUser, LDAPBackend}, + ApiBackend, + }, + description::USERS_TAG, + routes::auth::models::Credentials, + }, + errors::ApiError, + permission_required, + utils::create_random, +}; + +pub mod models; +pub mod permissions; +pub mod sql; + +// expose the OpenAPI to parent module +pub fn router() -> OpenApiRouter { + let read = OpenApiRouter::new() + .routes(routes!(get_users)) + .route_layer(permission_required!(Permission::Read( + PermissionDetail::Users + ))); + let write = OpenApiRouter::new() + .routes(routes!(create_user, update_user)) + .routes(routes!(get_ad_users)) + .route_layer(permission_required!(Permission::Write( + PermissionDetail::Users + ))); + + OpenApiRouter::new().merge(read).merge(write) +} + +#[debug_handler] +#[utoipa::path( + get, + path = "/users", + summary = "Get all Users", + description = "Get a list of all users.", + responses( + (status = OK, body = Vec, description = "List of users"), + ), + security( + ("user_auth" = ["read:users",]), + ), + tag = USERS_TAG)] +pub async fn get_users( + Extension(backend): Extension, +) -> Result>, ApiError> { + Ok(Json(sql::get_users(backend.pool(), None, None).await?)) +} + +#[debug_handler] +#[utoipa::path( + put, + path = "/users", + summary = "Change User details", + description = "Update user information / permissions / groups ", + request_body(content = User, description = "User details", content_type = "application/json"), + responses( + (status = OK, description = "User successfully updated"), + ), + security( + ("user_auth" = ["write:users"]), + ), + tag = USERS_TAG)] +pub async fn update_user( + Extension(backend): Extension, + Json(user): Json, +) -> Result<(), ApiError> { + sql::update_user(backend.pool(), &user).await?; + + Ok(()) +} + +#[debug_handler] +#[utoipa::path( + post, + path = "/users", + summary = "Create a new User", + description = "Creates a new user with the given information ", + request_body(content = User, description = "User details", content_type = "application/json"), + responses( + (status = OK, description = "User successfully created (Assigned Password in body)", body = String), + ), + security( + ("user_auth" = ["write:users",]), + ), + tag = USERS_TAG)] +pub async fn create_user( + Extension(backend): Extension, + Json(user): Json, +) -> Result { + // create password if not Active Directory user + let (password, hash) = match user.active_directory_auth { + true => (String::new(), None), + false => { + let salt = create_random(20); + let argon_config = argon2::Config::default(); + + let password = create_random(10); + let password_hash = + argon2::hash_encoded(password.as_bytes(), salt.as_bytes(), &argon_config)?; + + (password, Some(password_hash)) + } + }; + // create user + sql::create_new_user(backend.pool(), &user, hash).await?; + // send created password back to frontend + Ok(password) +} + +#[debug_handler] +#[utoipa::path( + get, + path = "/users/available_ad_users", + summary = "Get Active Directory Users", + description = "Get all Available Users from the Active Directory that are not already registered with this API.", + responses( + (status = OK, body = Vec, description = "List of AD users"), + ), + security( + ("user_auth" = ["write:users",]), + ), + tag = USERS_TAG)] +pub async fn get_ad_users( + Extension(backend): Extension, + Json(credentials): Json>, +) -> Result>, ApiError> { + let api_user_ids: Vec = sql::get_users(backend.pool(), None, None) + .await? + .iter() + .map(|user| user.id()) + .collect(); + let mut ldap = LDAPBackend::from_config(backend.config()).await?; + // bind to AD user if credentials are given + if let Some(credentials) = credentials { + ldap.ad_bind(&credentials.id, &credentials.password).await?; + } + let ad_users = ldap + .get_ad_user_list() + .await? + .into_iter() + .filter(|entry| !api_user_ids.contains(&entry.id)) + .collect(); + // disconnect from AD server + ldap.unbind().await; + + Ok(Json(ad_users)) +} diff --git a/src/api/routes/users/models.rs b/src/api/routes/users/models.rs new file mode 100644 index 0000000..167131d --- /dev/null +++ b/src/api/routes/users/models.rs @@ -0,0 +1,118 @@ +use std::collections::{HashMap, HashSet}; + +use axum_jwt_login::UserPermissions; +use chrono::NaiveDateTime; +use serde::{Deserialize, Serialize}; +use sqlx::PgPool; +use ts_rs::TS; +use utoipa::ToSchema; + +use crate::errors::ApiError; + +use super::{ + permissions::{Permission, PermissionContainer}, + sql, +}; + +#[derive(Debug, Clone, Copy, Serialize, Deserialize, sqlx::Type, TS, ToSchema)] +#[repr(i16)] +pub enum UserStatus { + Deleted = -1, + Deactivated = 0, + Active = 1, +} + +#[derive(Debug, Clone, Serialize, Deserialize, TS, ToSchema)] +pub struct GroupContainer(pub HashSet); + +impl From>> for GroupContainer { + fn from(value: Option>) -> Self { + let set = match value { + Some(values) => HashSet::from_iter(values), + None => HashSet::new(), + }; + + Self(set) + } +} + +#[derive(Debug, Clone, Serialize, Deserialize, TS, ToSchema)] +#[ts(export)] +pub struct User { + pub user_id: String, + pub active_directory_auth: bool, + #[serde(skip)] // do not leak password hashes + pub password: String, + pub name: String, + pub surname: String, + pub email: String, + #[schema(inline)] + pub groups: GroupContainer, + #[schema(inline)] + pub group_permissions: PermissionContainer, + #[schema(inline)] + pub permissions: PermissionContainer, + pub status_flag: UserStatus, + #[serde(default)] + #[schema(value_type = String, read_only)] + pub creation_date: NaiveDateTime, + #[serde(default)] + #[schema(value_type = String, read_only)] + pub last_change: NaiveDateTime, +} + +impl UserPermissions for User { + type Error = ApiError; + type Permission = Permission; + type Id = String; + + fn id(&self) -> Self::Id { + self.user_id.clone() + } + + fn get_user_permissions(&self) -> Result, Self::Error> { + Ok(self.permissions.0.iter().cloned().collect()) + } + fn get_group_permissions(&self) -> Result, Self::Error> { + Ok(self.group_permissions.0.iter().cloned().collect()) + } +} + +impl User { + pub async fn update_with_ad_details( + &self, + pool: &PgPool, + details: HashMap>, + ) -> Result<(), ApiError> { + let create_error = |key: &str| { + ApiError::InternalError(format!( + "Update User Information: Information '{key}' is not present" + )) + }; + + // get field values + let name = details + .get("givenName") + .ok_or(create_error("Name"))? + .first() + .ok_or(create_error("Name"))?; + let surname = details + .get("sn") + .ok_or(create_error("Surname"))? + .first() + .ok_or(create_error("Surname"))?; + let email = details + .get("mail") + .ok_or(create_error("Email"))? + .first() + .ok_or(create_error("Email"))?; + + if name != &self.name || surname != &self.surname || email != &self.email { + let mut transaction = pool.begin().await?; + sql::update_user_details(&mut transaction, &self.id(), name, surname, email).await?; + transaction.commit().await?; + } + + Ok(()) + } +} diff --git a/src/api/routes/users/permissions.rs b/src/api/routes/users/permissions.rs new file mode 100644 index 0000000..dcaf5da --- /dev/null +++ b/src/api/routes/users/permissions.rs @@ -0,0 +1,102 @@ +use std::{collections::HashSet, convert::Infallible, fmt::Display, str::FromStr}; + +use serde::{Deserialize, Serialize}; +use strum::EnumString; +use ts_rs::TS; +use utoipa::ToSchema; + +#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Hash, TS, ToSchema)] +pub enum Permission { + Read(PermissionDetail), + Write(PermissionDetail), + None, +} + +impl FromStr for Permission { + type Err = Infallible; + + fn from_str(s: &str) -> Result { + if let Some((permission, detail)) = s.split_once(':') { + match permission { + "Read" => Ok(Self::Read( + PermissionDetail::from_str(detail).unwrap_or_default(), + )), + "Write" => Ok(Self::Write( + PermissionDetail::from_str(detail).unwrap_or_default(), + )), + _ => Ok(Self::None), + } + } else { + Ok(Self::None) + } + } +} + +impl Display for Permission { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let string = match self { + Permission::Read(detail) => format!("Read:{detail}"), + Permission::Write(detail) => format!("Write:{detail}"), + Permission::None => "None".to_string(), + }; + f.write_str(&string) + } +} + +#[derive( + Debug, + Default, + Clone, + Copy, + Serialize, + Deserialize, + PartialEq, + Eq, + Hash, + EnumString, + strum::Display, + TS, + ToSchema, +)] +pub enum PermissionDetail { + Users, + APIKeys, + #[default] + None, +} + +#[derive(Debug, Clone, Serialize, Deserialize, TS, ToSchema)] +pub struct PermissionContainer(pub HashSet); + +impl From>> for PermissionContainer { + fn from(value: Option>) -> Self { + let set = match value { + Some(values) => HashSet::from_iter( + values + .iter() + .map(|s| Permission::from_str(s).unwrap_or(Permission::None)), + ), + None => HashSet::new(), + }; + + Self(set) + } +} + +#[cfg(test)] +mod test { + use std::str::FromStr; + + use crate::api::routes::users::permissions::PermissionDetail; + + use super::Permission; + + #[tokio::test] + async fn test_permissions() { + let permission = Permission::Read(PermissionDetail::Users); + assert_eq!("Read:Users", permission.to_string()); + + let parsed = Permission::from_str("Write:Users").unwrap(); + assert_eq!(parsed, Permission::Write(PermissionDetail::Users)); + } +} diff --git a/src/api/routes/users/sql.rs b/src/api/routes/users/sql.rs new file mode 100644 index 0000000..832dee2 --- /dev/null +++ b/src/api/routes/users/sql.rs @@ -0,0 +1,184 @@ +use sqlx::{PgPool, Postgres, Transaction}; + +use crate::errors::ApiError; + +use super::models::{User, UserStatus}; + +pub async fn get_users( + pool: &PgPool, + filter_status: Option, + filter_id: Option, +) -> Result, ApiError> { + Ok(sqlx::query_as!( + User, + r#"SELECT + USERS."UserID" as user_id, + USERS."ActiveDirectoryAuth" as active_directory_auth, + USERS."Password" as password, + USERS."Name" as name, + USERS."Surname" as surname, + USERS."Email" as email, + USERS."StatusFlag" as "status_flag: UserStatus", + USERS."CreationDate" as "creation_date", + USERS."LastChanged" as "last_change", + array_remove(ARRAY_AGG(USERS_GROUPS."GroupID"), NULL) AS groups, + array_remove(ARRAY_AGG(USER_PERMISSIONS."Permission"), NULL) AS permissions, + array_remove(ARRAY_AGG(GROUP_PERMISSIONS."Permission"), NULL) AS group_permissions + FROM + users + LEFT JOIN PUBLIC.USER_PERMISSIONS ON USER_PERMISSIONS."UserID" = USERS."UserID" + LEFT JOIN USERS_GROUPS ON USERS."UserID" = USERS_GROUPS."UserID" + LEFT JOIN GROUP_PERMISSIONS ON GROUP_PERMISSIONS."GroupID" = USERS_GROUPS."GroupID" + WHERE + ($1::smallint IS NULL OR USERS."StatusFlag" = $1) + AND ($2::varchar IS NULL OR USERS."UserID" = $2) + GROUP BY USERS."UserID""#, + filter_status.map(|s| s as i16), + filter_id + ) + .fetch_all(pool) + .await?) +} + +pub async fn update_user(pool: &PgPool, user: &User) -> Result<(), ApiError> { + let mut transaction = pool.begin().await?; + + // update general stuff + sqlx::query!( + r#"UPDATE users SET + "ActiveDirectoryAuth" = $2, + "StatusFlag" = $3, + "LastChanged" = NOW() + WHERE "UserID" = $1"#, + user.user_id, + user.active_directory_auth, + user.status_flag as i16, + ) + .execute(&mut *transaction) + .await?; + + // update user information + update_user_details( + &mut transaction, + &user.user_id, + &user.name, + &user.surname, + &user.email, + ) + .await?; + // update permissions + update_user_permissions(&mut transaction, &user).await?; + // update groups + update_user_groups(&mut transaction, user).await?; + + // commit transaction + transaction.commit().await?; + + Ok(()) +} + +pub async fn update_user_details<'a>( + pool: &'a mut Transaction<'static, Postgres>, + user_id: &String, + name: &String, + surname: &String, + email: &String, +) -> Result<(), ApiError> { + // let test = **pool; + sqlx::query!( + r#"UPDATE users SET + "Name" = $2, + "Surname" = $3, + "Email" = $4 + WHERE "UserID" = $1"#, + user_id, + name, + surname, + email + ) + .execute(&mut **pool) + .await?; + + Ok(()) +} + +pub async fn update_user_permissions<'a>( + pool: &'a mut Transaction<'static, Postgres>, + user: &User, +) -> Result<(), ApiError> { + // delete all permissions for user + sqlx::query!( + r#"DELETE FROM user_permissions + WHERE "UserID" = $1"#, + user.user_id, + ) + .execute(&mut **pool) + .await?; + + // insert permissions + for permission in &user.permissions.0 { + sqlx::query!( + r#"INSERT INTO user_permissions + ("UserID", "Permission") + VALUES ($1, $2)"#, + user.user_id, + permission.to_string() + ) + .execute(&mut **pool) + .await?; + } + + Ok(()) +} + +pub async fn update_user_groups<'a>( + pool: &'a mut Transaction<'static, Postgres>, + user: &User, +) -> Result<(), ApiError> { + // delete all permissions for user + sqlx::query!( + r#"DELETE FROM users_groups + WHERE "UserID" = $1"#, + user.user_id, + ) + .execute(&mut **pool) + .await?; + + // insert permissions + for group_id in &user.groups.0 { + sqlx::query!( + r#"INSERT INTO users_groups + ("UserID", "GroupID") + VALUES ($1, $2)"#, + user.user_id, + group_id + ) + .execute(&mut **pool) + .await?; + } + + Ok(()) +} + +pub async fn create_new_user( + pool: &PgPool, + user: &User, + password: Option, +) -> Result<(), ApiError> { + let mut transaction = pool.begin().await?; + + sqlx::query!( + r#"INSERT INTO users + ("UserID", "Password") + VALUES ($1, $2)"#, + user.user_id, + password + ) + .execute(&mut *transaction) + .await?; + + // commit transaction + transaction.commit().await?; + + Ok(()) +} diff --git a/src/authentication/api_key.rs b/src/authentication/api_key.rs new file mode 100644 index 0000000..531206d --- /dev/null +++ b/src/authentication/api_key.rs @@ -0,0 +1,69 @@ +use rand::{distributions::Alphanumeric, Rng}; +use sha2::{Digest, Sha512}; +use uuid::Uuid; + +pub(crate) const KEY_LENGTH: usize = 40; + +pub trait ApiKeyInfo { + fn id(&self) -> String; + fn validate(&self, hash: &str) -> bool; + fn requires_auth(&self) -> bool; +} + +impl ApiKeyInfo for ApiKey { + fn id(&self) -> String { + self.id.to_string() + } + + fn validate(&self, hash: &str) -> bool { + let mut sha512 = Sha512::new(); + sha512.update(hash); + + format!("{:X}", sha512.finalize()) == self.hash + } + + fn requires_auth(&self) -> bool { + self.auth_required + } +} + +#[derive(Debug, Clone, Hash, PartialEq, Eq)] +pub(crate) struct ApiKey { + pub(crate) id: String, + pub(crate) key: Option, + pub(crate) hash: String, + pub(crate) name: String, + pub(crate) auth_required: bool, +} + +impl ApiKey { + pub(crate) fn create(name: &str, requires_auth: bool) -> Self { + let uuid = Uuid::new_v4().simple(); + let key: String = create_random(KEY_LENGTH); + let mut sha512 = Sha512::new(); + sha512.update(key.clone()); + + ApiKey { + id: uuid.to_string(), + key: Some(key.to_owned()), + hash: format!("{:X}", sha512.finalize()), + auth_required: requires_auth, + name: name.to_string(), + } + } + + pub(crate) fn validate(&self, password: &str) -> bool { + let mut sha512 = Sha512::new(); + sha512.update(password); + + format!("{:X}", sha512.finalize()) == self.hash + } +} + +pub(crate) fn create_random(len: usize) -> String { + rand::thread_rng() + .sample_iter(&Alphanumeric) + .take(len) + .map(char::from) + .collect() +} diff --git a/src/authentication/extract.rs b/src/authentication/extract.rs new file mode 100644 index 0000000..09f20ae --- /dev/null +++ b/src/authentication/extract.rs @@ -0,0 +1,27 @@ +use axum::{ + async_trait, + extract::FromRequestParts, + http::{request::Parts, StatusCode}, +}; + +use super::AuthenticationBackend; + +#[async_trait] +impl FromRequestParts for AuthenticationBackend +where + S: Send + Sync, + T: Send + Sync + Clone + 'static, +{ + type Rejection = (StatusCode, &'static str); + + async fn from_request_parts(parts: &mut Parts, _state: &S) -> Result { + parts + .extensions + .get::>() + .cloned() + .ok_or(( + StatusCode::INTERNAL_SERVER_ERROR, + "Can't extract auth session. Is `AuthenticationLayer` enabled?", + )) + } +} diff --git a/src/authentication/jwt.rs b/src/authentication/jwt.rs new file mode 100644 index 0000000..df0ffeb --- /dev/null +++ b/src/authentication/jwt.rs @@ -0,0 +1,112 @@ +use std::time::Duration; + +use chrono::{DateTime, TimeZone, Utc}; +use jsonwebtoken::{decode, Algorithm, DecodingKey, EncodingKey, Header, Validation}; +use serde::{Deserialize, Serialize}; + +use super::user::User; + +// Session length in seconds -> 12 hours +pub(crate) const DEFAULT_SESSION_LENGTH: std::time::Duration = Duration::from_secs(12 * 3600); + +#[derive(Clone)] +pub struct JwtAuthorizer { + encoding_key: EncodingKey, + decoding_key: DecodingKey, + algorithm: Algorithm, +} + +// Here we've implemented `Debug` manually to avoid accidentally logging the keys. +impl std::fmt::Debug for JwtAuthorizer { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("JwtAuthorizer") + .field("encoding_key", &"[redacted]") + .field("decoding_key", &"[redacted]") + .field("algorithm", &self.algorithm) + .finish() + } +} + +impl JwtAuthorizer { + pub fn new(encoding_key: EncodingKey, decoding_key: DecodingKey, algorithm: Algorithm) -> Self { + Self { + encoding_key, + decoding_key, + algorithm, + } + } + + pub fn encode(&self, user: User) -> Result { + let claims = RegisteredClaims::new(user); + jsonwebtoken::encode(&Header::default(), &claims, &self.encoding_key) + } + + pub fn decode(&self, token: &str) -> Result { + decode::>( + &token, + &self.decoding_key, + &Validation::new(self.algorithm), + ) + .map(|data| data.claims.data) + } +} + +/// Claims mentioned in the JWT specifications. +/// +/// +#[derive(Deserialize, Serialize, Clone, Debug)] +pub struct RegisteredClaims { + #[serde(skip_serializing_if = "Option::is_none")] + pub iss: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub sub: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub exp: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub nbf: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub iat: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub jti: Option, + pub data: T, +} + +impl RegisteredClaims { + pub fn new(data: T) -> Self { + let now = Utc::now(); + let exp = now + DEFAULT_SESSION_LENGTH; + Self { + iss: Some(env!("CARGO_PKG_NAME").to_string()), + sub: Some("User token".to_string()), + exp: Some(exp.into()), + nbf: Some(now.into()), + iat: Some(now.into()), + jti: None, + data, + } + } +} + +/// The number of seconds from 1970-01-01T00:00:00Z UTC until the specified UTC date/time ignoring leap seconds. +/// () +#[derive(Deserialize, Serialize, Clone, PartialEq, Eq, Debug)] +pub struct NumericDate(pub i64); + +/// accesses the underlying value +impl From for i64 { + fn from(t: NumericDate) -> Self { + t.0 + } +} + +impl From> for NumericDate { + fn from(t: DateTime) -> Self { + Self(t.timestamp()) + } +} + +impl From for DateTime { + fn from(t: NumericDate) -> Self { + Utc.timestamp_opt(t.0, 0).unwrap() + } +} diff --git a/src/authentication/layer.rs b/src/authentication/layer.rs new file mode 100644 index 0000000..153bc57 --- /dev/null +++ b/src/authentication/layer.rs @@ -0,0 +1,191 @@ +use std::{ + future::Future, + pin::Pin, + task::{Context, Poll}, +}; + +use axum::{ + extract::Request, + http::{self, header::AUTHORIZATION, HeaderMap, HeaderName, Response}, +}; +use tower_layer::Layer; +use tower_service::Service; +use tracing::Instrument; + +use super::{api_key::ApiKeyInfo, AuthenticationBackend, AuthenticationData, UserAuthData}; + +#[derive(Debug, Clone)] +pub struct AuthenticationLayer { + backend: AuthenticationBackend, +} + +impl AuthenticationLayer { + pub fn new(backend: AuthenticationBackend) -> Self { + Self { backend } + } +} + +impl Layer for AuthenticationLayer +where + T: Clone, +{ + type Service = AuthenticationService; + + fn layer(&self, service: S) -> Self::Service { + AuthenticationService { + backend: self.backend.clone(), + service, + } + } +} + +// This service implements the Log behavior +#[derive(Debug, Clone)] +pub struct AuthenticationService { + backend: AuthenticationBackend, + service: S, +} + +impl Service> for AuthenticationService +where + S: Service, Response = Response> + Clone + Send + 'static, + S::Future: Send + 'static, + ReqBody: Send + 'static, + ResBody: Default + Send, + T: ApiKeyInfo + Send + Sync + Clone + 'static, +{ + type Response = S::Response; + type Error = S::Error; + type Future = Pin> + Send>>; + + #[inline] + fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll> { + self.service.poll_ready(cx) + } + + fn call(&mut self, mut req: Request) -> Self::Future { + let span = tracing::info_span!("call", user.id = tracing::field::Empty); + + let mut backend = self.backend.clone(); + + // Because the inner service can panic until ready, we need to ensure we only + // use the ready service. + // + // See: https://docs.rs/tower/latest/tower/trait.Service.html#be-careful-when-cloning-inner-services + let clone = self.service.clone(); + let mut inner = std::mem::replace(&mut self.service, clone); + + Box::pin( + async move { + // check for api key if api key requirement is enabled + let bearer_required = if let Some(ref header) = backend.api_key_header { + match extract_api_key(req.headers(), header) { + Some((id, hash)) => { + // get api key information + if let Some(key) = backend.session_store.get_api_key(&id).await { + // validate api key + if !key.validate(&hash) { + tracing::error!("API Key invalid"); + let mut res = Response::default(); + *res.status_mut() = http::StatusCode::UNAUTHORIZED; + return Ok(res); + } + // check if further authentication is required + if key.requires_auth() { + true + } else { + // add API Key to authenticated information + backend.authenticated_user = + Some(AuthenticationData::ApiKey(key)); + false + } + } else { + let mut res = Response::default(); + *res.status_mut() = http::StatusCode::UNAUTHORIZED; + return Ok(res); + } + } + None => { + let mut res = Response::default(); + *res.status_mut() = http::StatusCode::UNAUTHORIZED; + return Ok(res); + } + } + } else { + true + }; + + // Authenticate with bearer if required + if bearer_required { + // try to extract Authentication Bearer from headers + let bearer = extract_bearer(req.headers()); + // check if bearer is present + if let Some(bearer) = bearer { + // check if bearer belongs to active session + if backend.session_store.includes(&bearer).await { + // try to decode bearer + match backend.jwt_authorizer.decode(&bearer) { + Ok(user) => { + backend.authenticated_user = + Some(AuthenticationData::User(UserAuthData { + user, + token: bearer, + })); + + // tracing::Span::current().record("user.id", user.id().to_string()); + } + Err(err) => { + // remove token from session storage + backend.session_store.remove(&bearer).await; + // log error + tracing::error!("Error decoding JWT token: {err}"); + // return internal server error + let mut res = Response::default(); + *res.status_mut() = http::StatusCode::INTERNAL_SERVER_ERROR; + return Ok(res); + } + } + } + } + } + + req.extensions_mut().insert(backend); + + inner.call(req).await + } + .instrument(span), + ) + } +} + +fn extract_bearer(headers: &HeaderMap) -> Option { + // Check that its a well-formed bearer and return + headers + .get(AUTHORIZATION) + .map(|header| { + let split = header.to_str().unwrap_or("").split_once(' '); + let bearer = match split { + // Found proper bearer + Some((name, contents)) if name == "Bearer" => Some(contents.to_string()), + // Found nothing + _ => None, + }; + bearer + }) + .flatten() +} + +fn extract_api_key(headers: &HeaderMap, name: &HeaderName) -> Option<(String, String)> { + // Check that its a well-formed bearer and return + headers + .get(name) + .map(|header| { + let split = header.to_str().unwrap_or("").split_once("."); + let api_key = match split { + Some((id, hash)) => Some((id.to_string(), hash.to_string())), + _ => None, + }; + api_key + }) + .flatten() +} diff --git a/src/authentication/middleware.rs b/src/authentication/middleware.rs new file mode 100644 index 0000000..d9e5c3f --- /dev/null +++ b/src/authentication/middleware.rs @@ -0,0 +1,76 @@ +/// Login predicate middleware. +/// +/// Requires that the user is authenticated. +#[macro_export] +macro_rules! login_required { + ($backend_type:ty) => {{ + async fn is_authenticated( + auth_session: $crate::axum_test::AuthenticationBackend<$backend_type>, + ) -> bool { + auth_session.is_authenticated() + } + + $crate::predicate_required!(is_authenticated, axum::http::StatusCode::UNAUTHORIZED) + }}; +} + +/// Permission predicate middleware. +/// +/// Requires that the specified permissions, either user or group or both, are +/// all assigned to the user. +#[macro_export] +macro_rules! permission_required { + ($backend_type:ty, login_url = $login_url:expr, redirect_field = $redirect_field:expr, $($perm:expr),+ $(,)?) => {{ + use $crate::AuthzBackend; + + async fn is_authorized(auth_session: $crate::axum_test::AuthSession<$backend_type>) -> bool { + if let Some(ref user) = auth_session.user { + let mut has_all_permissions = true; + $( + has_all_permissions = has_all_permissions && + auth_session.backend.has_perm(user, $perm.into()).await.unwrap_or(false); + )+ + has_all_permissions + } else { + false + } + } + + $crate::predicate_required!( + is_authorized, + login_url = $login_url, + redirect_field = $redirect_field + ) + }}; +} + +/// Predicate middleware. +/// +/// Can be specified with a login URL and next redirect field or an alternative +/// which implements [`IntoResponse`](axum::response::IntoResponse). +/// +/// When the predicate passes, the request processes normally. On failure, +/// either a redirect to the specified login URL is issued or the alternative is +/// used as the response. +#[macro_export] +macro_rules! predicate_required { + ($predicate:expr, $alternative:expr) => {{ + use axum::{ + extract::Request, + middleware::{from_fn, Next}, + response::IntoResponse, + }; + + from_fn( + |auth_session: $crate::axum_test::AuthenticationBackend<_>, + req: Request, + next: Next| async move { + if $predicate(auth_session).await { + next.run(req).await + } else { + $alternative.into_response() + } + }, + ) + }}; +} diff --git a/src/authentication/mod.rs b/src/authentication/mod.rs new file mode 100644 index 0000000..1bd0b91 --- /dev/null +++ b/src/authentication/mod.rs @@ -0,0 +1,123 @@ +pub mod api_key; +mod extract; +mod jwt; +pub mod layer; +mod middleware; +mod sessions; +mod user; + +// use api_key::ApiKey; +use axum::http::HeaderName; +use jsonwebtoken::{Algorithm, DecodingKey, EncodingKey}; +use jwt::JwtAuthorizer; +use sessions::SessionStore; +use user::User; + +#[derive(Debug, Clone)] +pub struct AuthenticationBackend { + sql_pool: (), + session_store: SessionStore, + jwt_authorizer: JwtAuthorizer, + api_key_header: Option, + authenticated_user: Option>, +} + +impl AuthenticationBackend { + pub fn is_authenticated(&self) -> bool { + self.authenticated_user.is_some() + } +} + +#[derive(Debug, Clone)] +pub enum AuthenticationData { + User(UserAuthData), + ApiKey(T), +} + +#[derive(Debug, Clone)] +pub struct UserAuthData { + user: User, + token: String, +} + +#[derive(Clone, Default)] +pub struct AuthenticationBackendBuilder { + pool: Option<()>, + api_header_key: Option, + loaded_api_keys: Option>, + jwt_decode_key: Option, + jwt_encoding_key: Option, + jwt_algorithm: Option, +} + +impl AuthenticationBackendBuilder { + pub fn new() -> Self { + Self { + pool: None, + api_header_key: None, + loaded_api_keys: None, + jwt_decode_key: None, + jwt_encoding_key: None, + jwt_algorithm: None, + } + } + pub fn pool(self, pool: ()) -> Self { + let mut me = self; + me.pool = Some(pool); + me + } + + pub fn use_api_key(self, key_header_name: HeaderName) -> Self { + let mut me = self; + me.api_header_key = Some(key_header_name); + me + } + + pub fn api_keys(self, keys: Vec) -> Self { + let mut me = self; + me.loaded_api_keys = Some(keys); + me + } + + pub fn jwt_decoding_key(self, key: DecodingKey) -> Self { + let mut me = self; + me.jwt_decode_key = Some(key); + me + } + + pub fn jwt_encoding_key(self, key: EncodingKey) -> Self { + let mut me = self; + me.jwt_encoding_key = Some(key); + me + } + + pub fn jwt_algorithm(self, algorithm: Algorithm) -> Self { + let mut me = self; + me.jwt_algorithm = Some(algorithm); + me + } + + pub fn build(self) -> Result, String> { + if self.pool.is_none() { + return Err("Missing Pool".to_string()); + } + if self.jwt_decode_key.is_none() { + return Err("Missing JWT Decoding key".to_string()); + } + if self.jwt_encoding_key.is_none() { + return Err("Missing JWT Encoding key".to_string()); + } + + Ok(AuthenticationBackend { + sql_pool: self.pool.unwrap(), + session_store: SessionStore::new(self.loaded_api_keys), + jwt_authorizer: JwtAuthorizer::new( + self.jwt_encoding_key.unwrap(), + self.jwt_decode_key.unwrap(), + self.jwt_algorithm.unwrap_or(Algorithm::RS256), + ), + api_key_header: self.api_header_key, + authenticated_user: None, + }) + } +} diff --git a/src/authentication/sessions.rs b/src/authentication/sessions.rs new file mode 100644 index 0000000..1647a79 --- /dev/null +++ b/src/authentication/sessions.rs @@ -0,0 +1,54 @@ +use std::{ + collections::{HashMap, HashSet}, + sync::Arc, +}; + +use tokio::sync::Mutex; + +// use super::ApiKey; + +#[derive(Debug, Clone)] +pub struct SessionStore { + active_sessions: Arc>>, + active_api_keys: Option>>>, +} + +impl SessionStore { + pub fn new(initial_api_keys: Option>) -> Self { + Self { + active_sessions: Arc::new(Mutex::new(HashSet::new())), + active_api_keys: None, //initial_api_keys.map(|keys| { + // Arc::new(Mutex::new(HashMap::from_iter( + // keys.into_iter().map(|key| (key.id.clone(), key)), + // ))) + // }), + } + } + + pub async fn insert(&self, token: impl Into) { + let mut sessions = self.active_sessions.lock().await; + sessions.insert(token.into()); + } + + pub async fn includes(&self, token: &str) -> bool { + let sessions = self.active_sessions.lock().await; + sessions.contains(token) + } + + pub async fn remove(&self, token: &str) -> bool { + let mut sessions = self.active_sessions.lock().await; + sessions.remove(token) + } + + pub async fn get_api_key(&self, key: &str) -> Option + where + T: Clone, + { + if let Some(ref key_store) = self.active_api_keys { + let lock = key_store.lock().await; + lock.get(key).cloned() + } else { + None + } + } +} diff --git a/src/authentication/user.rs b/src/authentication/user.rs new file mode 100644 index 0000000..1bf8c2c --- /dev/null +++ b/src/authentication/user.rs @@ -0,0 +1,4 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct User {} diff --git a/src/cli.rs b/src/cli.rs new file mode 100644 index 0000000..3a77cab --- /dev/null +++ b/src/cli.rs @@ -0,0 +1,310 @@ +use std::{collections::HashSet, path::PathBuf}; + +use clap::{command, Parser, Subcommand}; +use error_stack::{Report, ResultExt}; +use sqlx::PgPool; +use tokio::sync::mpsc::UnboundedReceiver; +use tokio_util::sync::CancellationToken; + +use crate::{ + api::{ + self, + routes::api_keys::{models::ApiKey, sql::create_api_key}, + }, + config::Configuration, + errors::AppError, + APP_NAME, ROOT_PATH, +}; + +#[derive(Parser, Debug)] +#[command(name = APP_NAME, version = "1.0", about = "A tool with subcommands")] +pub struct Cli { + #[arg(short, long, value_name = "FILE", help = "Configuration file location")] + config: Option, + + /// Subcommands for specific actions + #[command(subcommand)] + command: Option, +} + +#[derive(Debug, Subcommand)] +enum Commands { + Serve { + /// lists test values + #[arg(short, long)] + port: Option, + }, + Migrate, + #[command(subcommand)] + Service(ServiceCommands), + #[command(subcommand)] + Create(CreateCommands), +} + +#[derive(Debug, Subcommand)] +enum ServiceCommands { + Install, + Uninstall, + Run, +} + +#[derive(Debug, Subcommand)] +enum CreateCommands { + ApiKey { + /// name of API Key + #[arg(short, long)] + name: String, + /// name of API Key + #[arg(short, long, default_value = "true")] + requires_auth: bool, + }, +} + +impl Cli { + pub fn config(&self) -> PathBuf { + match self.config.as_ref() { + Some(path) => path.clone(), + None => ROOT_PATH.with_file_name("config.toml"), + } + } + + pub async fn handle( + &self, + config: &mut Configuration, + ) -> Result> { + match &self.command { + Some(command) => match command { + Commands::Serve { port } => { + if let Some(port) = port { + config.port = *port; + } + start_service(config, None).await + } + Commands::Migrate => { + let pool = PgPool::connect(&config.database_url) + .await + .change_context(AppError)?; + sqlx::migrate!("src/migrations") + .run(&pool) + .await + .change_context(AppError)?; + + Ok(DaemonStatus::NotRunning) + } + Commands::Service(service_commands) => match service_commands { + ServiceCommands::Install => { + // TODO do things + Ok(DaemonStatus::NotRunning) + } + ServiceCommands::Uninstall => { + // TODO do things + Ok(DaemonStatus::NotRunning) + } + ServiceCommands::Run => { + // TODO do things + // create shutdown signals + // let (shutdown_send, mut shutdown_recv) = mpsc::unbounded_channel(); + // Register generated `ffi_service_main` with the system and start the service, blocking + // this thread until the service is stopped. + #[cfg(windows)] + let _ = service_dispatcher::start(env!("CARGO_PKG_NAME"), ffi_service_main); + Ok(DaemonStatus::NotRunning) + } + }, + Commands::Create(create_commands) => match create_commands { + CreateCommands::ApiKey { + name, + requires_auth, + } => { + // create API Key + let (key_secret, key) = + ApiKey::create(name, *requires_auth, HashSet::new(), config) + .change_context(AppError)?; + + // Add API Key to Database + let pool = PgPool::connect(&config.database_url) + .await + .change_context(AppError)?; + create_api_key(&pool, &key).await.change_context(AppError)?; + + // print API key secret to console + println!("Created API Key: {}.{key_secret}", key.id); + + Ok(DaemonStatus::NotRunning) + } + }, + }, + None => start_service(config, None).await, + } + } +} + +pub enum DaemonStatus { + NotRunning, + Running((CancellationToken, Option>)), +} + +async fn start_service( + config: &Configuration, + shutdown_signal: Option>, +) -> Result> { + // create cancellation token + let cancellation_token = CancellationToken::new(); + + api::start(config, cancellation_token.clone()).await?; + + Ok(DaemonStatus::Running((cancellation_token, shutdown_signal))) +} + +#[cfg(windows)] +mod windows { + use error_stack::{Report, ResultExt}; + use std::{ffi::OsString, thread, time::Duration}; + use windows_service::{ + service::{ + ServiceAccess, ServiceErrorControl, ServiceInfo, ServiceStartType, ServiceState, + ServiceType, + }, + service_manager::{ServiceManager, ServiceManagerAccess}, + }; + + use crate::errors::AppError; + use crate::APP_NAME; + + pub fn install() -> Result<(), Report> { + let manager_access = ServiceManagerAccess::CONNECT | ServiceManagerAccess::CREATE_SERVICE; + let service_manager = ServiceManager::local_computer(None::<&str>, manager_access) + .change_context(AppError)?; + + // This example installs the service defined in `examples/ping_service.rs`. + // In the real world code you would set the executable path to point to your own binary + // that implements windows service. + let service_binary_path = ::std::env::current_exe() + .change_context(AppError)? + .with_file_name(format!("{}.exe", env!("CARGO_PKG_NAME"))); + + let service_info = ServiceInfo { + name: OsString::from(env!("CARGO_PKG_NAME")), + display_name: OsString::from(&format!("{APP_NAME}")), + service_type: ServiceType::OWN_PROCESS, + start_type: ServiceStartType::AutoStart, + error_control: ServiceErrorControl::Normal, + executable_path: service_binary_path, + launch_arguments: vec!["service run".into()], + dependencies: vec![], + account_name: None, // run as System + account_password: None, + }; + let service = service_manager + .create_service(&service_info, ServiceAccess::CHANGE_CONFIG) + .into_report() + .change_context(AppError)?; + + service + .set_description(format!("{APP_NAME}")) + .into_report() + .change_context(AppError)?; + Ok(()) + } + + pub fn remove() -> Result<(), Report> { + let manager_access = ServiceManagerAccess::CONNECT; + let service_manager = ServiceManager::local_computer(None::<&str>, manager_access) + .change_context(AppError)?; + + let service_access = + ServiceAccess::QUERY_STATUS | ServiceAccess::STOP | ServiceAccess::DELETE; + let service = service_manager + .open_service(env!("CARGO_PKG_NAME"), service_access) + .change_context(AppError)?; + + let service_status = service.query_status().change_context(AppError)?; + if service_status.current_state != ServiceState::Stopped { + service.stop().change_context(AppError)?; + // Wait for service to stop + thread::sleep(Duration::from_secs(1)); + } + + service.delete().into_report().change_context(AppError)?; + + Ok(()) + } + + // Service entry function which is called on background thread by the system with service + // parameters. There is no stdout or stderr at this point so make sure to configure the log + // output to file if needed. + pub fn service_main(_arguments: Vec) { + if let Err(_e) = run_service() { + // Handle the error, by logging or something. + } + } + + fn run_service() -> Result<(), Report> { + // Create a channel to be able to poll a stop event from the service worker loop. + let (shutdown_tx, shutdown_rx) = tokio::sync::mpsc::channel(1); + + // Define system service event handler that will be receiving service events. + let event_handler = move |control_event| -> ServiceControlHandlerResult { + match control_event { + // Notifies a service to report its current status information to the service + // control manager. Always return NoError even if not implemented. + ServiceControl::Interrogate => ServiceControlHandlerResult::NoError, + + // Handle stop + ServiceControl::Stop => { + shutdown_tx.try_send(()).unwrap(); + ServiceControlHandlerResult::NoError + } + + _ => ServiceControlHandlerResult::NotImplemented, + } + }; + + // Register system service event handler. + // The returned status handle should be used to report service status changes to the system. + let status_handle = + service_control_handler::register(env!("CARGO_PKG_NAME"), event_handler) + .change_context(AppError)?; + + // Tell the system that service is running + status_handle + .set_service_status(ServiceStatus { + service_type: ServiceType::OWN_PROCESS, + current_state: ServiceState::Running, + controls_accepted: ServiceControlAccept::STOP, + exit_code: ServiceExitCode::Win32(0), + checkpoint: 0, + wait_hint: Duration::default(), + process_id: None, + }) + .change_context(AppError)?; + + // run service - blocking thread + let rt = tokio::runtime::Builder::new_current_thread() + .enable_all() + .build() + .unwrap(); + + rt.block_on(async move { + if let Err(e) = execute(shutdown_rx).await { + error!("{e:?}"); + } + }); + + // Tell the system that service has stopped. + status_handle + .set_service_status(ServiceStatus { + service_type: ServiceType::OWN_PROCESS, + current_state: ServiceState::Stopped, + controls_accepted: ServiceControlAccept::empty(), + exit_code: ServiceExitCode::Win32(0), + checkpoint: 0, + wait_hint: Duration::default(), + process_id: None, + }) + .into_report() + .change_context(ServiceError::Starting)?; + + Ok(()) + } +} diff --git a/src/config.rs b/src/config.rs new file mode 100644 index 0000000..ea76841 --- /dev/null +++ b/src/config.rs @@ -0,0 +1,88 @@ +use std::path::PathBuf; + +use config::{Config, Environment, File}; +use error_stack::{Report, ResultExt}; +use serde::Deserialize; + +use crate::errors::AppError; + +fn default_port() -> u16 { + 8080 +} + +#[derive(Debug, Deserialize, Default, Clone)] +#[allow(unused)] +pub struct ConfigInfo { + pub location: Option, +} + +#[derive(Debug, Deserialize, Default, Clone)] +pub struct LDAP { + pub server: String, + #[serde(default)] + pub skip_tls_verify: bool, + #[serde(default)] + pub ad_domain: String, + #[serde(default)] + pub user_search_base: String, + #[serde(default)] + pub elevated_search: bool, + #[serde(default)] + pub elevated_user_id: String, + #[serde(default)] + pub elevated_user_pw: String, +} + +#[derive(Debug, Deserialize, Default, Clone)] +#[allow(unused)] +pub struct Configuration { + #[serde(default)] + pub config: ConfigInfo, + #[serde(default)] + pub debug: bool, + #[serde(default)] + pub token_secret: String, + #[serde(default)] + pub session_length: u64, + #[serde(default = "default_port")] + pub port: u16, + #[serde(default)] + #[serde(alias = "DATABASE_URL")] + pub database_url: String, + #[serde(default)] + pub ldap: LDAP, +} + +impl Configuration { + pub fn new(location: PathBuf) -> Result> { + let s = Config::builder() + .add_source(File::from(location.clone())) + .add_source(Environment::default()) + .set_override("config.location", location.to_str().unwrap_or("default")) + .change_context(AppError)? + .build() + .change_context(AppError)?; + + let settings = s.try_deserialize().change_context(AppError)?; + + Ok(settings) + } + + pub fn check(&self) -> Result<(), Report> { + // check if elevated LDAP user is set if activated + if self.ldap.elevated_search { + if self.ldap.elevated_user_id == "" { + return Err(Report::new(AppError).attach_printable( + "LDAP: Search with elevated user is activated but no user is configured", + )); + } + + if self.ldap.elevated_user_pw == "" { + return Err(Report::new(AppError).attach_printable( + "LDAP: Search with elevated user is activated but no password is configured", + )); + } + } + Ok(()) + } +} diff --git a/src/errors.rs b/src/errors.rs new file mode 100644 index 0000000..cc8397b --- /dev/null +++ b/src/errors.rs @@ -0,0 +1,124 @@ +use std::fmt::{self, Display}; + +use axum::{ + http::StatusCode, + response::{IntoResponse, Response}, + Json, +}; +use error_stack::Context; +use serde::Serialize; +use sha2::digest::InvalidLength; +use tracing::error; + +use crate::api::{backend::ApiBackend, routes::users::models::User}; + +#[derive(Debug)] +pub struct AppError; + +impl fmt::Display for AppError { + fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result { + fmt.write_str("Error on executing Application") + } +} + +impl Context for AppError {} + +#[derive(Debug, Serialize)] +pub struct ErrorInfo<'a> { + error_message: &'a str, + debug_info: Option<&'a String>, +} + +#[derive(Debug, PartialEq)] +pub enum ApiError { + SQLQueryError(String), + InvalidCredentials, + InternalError(String), + AccessDenied, +} + +impl ApiError { + fn as_error_info(&self) -> (StatusCode, ErrorInfo) { + let (status, error_message, debug_info) = match self { + Self::InvalidCredentials => (StatusCode::UNAUTHORIZED, "Invalid credentials", None), + Self::SQLQueryError(error) => ( + StatusCode::INTERNAL_SERVER_ERROR, + "Invalid credentials", + Some(error), + ), + Self::InternalError(error) => ( + StatusCode::INTERNAL_SERVER_ERROR, + "Internal Server Error", + Some(error), + ), + Self::AccessDenied => (StatusCode::FORBIDDEN, "Access Denied", None), // ApiError::WrongCredentials => (StatusCode::UNAUTHORIZED, "Wrong credentials"), + // ApiError::MissingCredentials => (StatusCode::BAD_REQUEST, "Missing credentials"), + // ApiError::TokenCreation => (StatusCode::INTERNAL_SERVER_ERROR, "Token creation error"), + // ApiError::InvalidToken => (StatusCode::BAD_REQUEST, "Invalid token"), + // ApiError::InvalidApiKey => (StatusCode::BAD_REQUEST, "Invalid api key"), + // ApiError::InternalServerError => { + // (StatusCode::INTERNAL_SERVER_ERROR, "Unspecified error") + // } + // ApiError::AccessDenied => (StatusCode::UNAUTHORIZED, "Access denied"), + // ApiError::InvalidPermissions => (StatusCode::BAD_REQUEST, "Invalid permissions"), + }; + + ( + status, + ErrorInfo { + error_message, + debug_info, + }, + ) + } +} + +impl IntoResponse for ApiError { + fn into_response(self) -> Response { + let (status, error_info) = self.as_error_info(); + let body = Json(error_info); + (status, body).into_response() + } +} + +impl From for ApiError { + fn from(error: sqlx::Error) -> Self { + error!("{error}"); + Self::SQLQueryError(error.to_string()) + } +} + +impl From> for ApiError { + fn from(value: axum_jwt_login::Error) -> Self { + match value { + axum_jwt_login::Error::Jwt(error) => ApiError::InternalError(error.to_string()), + axum_jwt_login::Error::Backend(error) => error, + _ => unreachable!(), + } + } +} + +impl From for ApiError { + fn from(_: argon2::Error) -> Self { + Self::InvalidCredentials + } +} + +impl From for ApiError { + fn from(value: InvalidLength) -> Self { + Self::InternalError(format!("Invalid HMac Key length: {value}")) + } +} + +impl Display for ApiError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let error_info = self.as_error_info().1; + f.write_str(&format!( + "{}: {}", + error_info.error_message, + error_info.debug_info.unwrap_or(&"".to_string()) + )) + } +} + +impl std::error::Error for ApiError {} diff --git a/src/ldap_test.rs b/src/ldap_test.rs new file mode 100644 index 0000000..469bedd --- /dev/null +++ b/src/ldap_test.rs @@ -0,0 +1,55 @@ +use ldap3::exop::WhoAmI; +use ldap3::{LdapConnAsync, Scope, SearchEntry}; + +pub async fn try_authentication() -> ldap3::result::Result<()> { + println!("Hello, world!"); + + // let (conn, mut ldap) = LdapConnAsync::new("ldap://ldap.forumsys.com:389").await?; + let (conn, mut ldap) = LdapConnAsync::new("ldap://192.168.10.200:389").await?; + ldap3::drive!(conn); + + let res = ldap + // .simple_bind("cn=read-only-admin,dc=example,dc=com", "password") + .simple_bind( + // "CN=Abel Austin,OU=Accounting,OU=Mylab Users,DC=mylab,DC=local", + "MYLAB\\A0H67123", + "Passwort123", + ) + .await?; + println!("{res:?}"); + // let (res, re) = ldap + // .search( + // "ou=mathematicians,dc=example,dc=com", + // Scope::Subtree, + // "(objectClass=*)", + // vec!["*"], + // ) + // .await? + // .success()?; + + // for entry in res { + // println!("{:?}", SearchEntry::construct(entry)); + // } + + let (res, re) = ldap + .search( + // "CN=Abel Austin,OU=Accounting,OU=Mylab Users,DC=mylab,DC=local", + "OU=Mylab Users,DC=mylab,DC=local", + Scope::Subtree, + // "(objectClass=*)", + "(&(ObjectCategory=Person)(sAMAccountName=A0H67123))", + vec!["givenName", "sn"], + ) + .await? + .success()?; + + for entry in res { + println!("{:?}", SearchEntry::construct(entry)); + } + + let test = ldap.extended(WhoAmI).await?.0; + let whoami: ldap3::exop::WhoAmIResp = test.parse(); + println!("{whoami:?}"); + + Ok(ldap.unbind().await?) +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..bc6bb7d --- /dev/null +++ b/src/main.rs @@ -0,0 +1,96 @@ +use std::{path::PathBuf, time::Duration}; + +use clap::Parser; +use error_stack::Report; +use errors::AppError; +use once_cell::sync::Lazy; +use tokio::signal; +use tokio_util::task::TaskTracker; +use tracing::{error, info}; + +pub mod api; +mod cli; +mod config; +mod errors; +mod utils; + +pub static ROOT_PATH: Lazy = Lazy::new(|| match std::env::current_exe() { + #[cfg(not(debug_assertions))] + Ok(path) => path, + #[cfg(debug_assertions)] + Ok(_) => PathBuf::from("./"), + Err(e) => { + error!("Exiting on error for getting root path: {e:?}"); + panic!("Exiting on error for getting root path: {e:?}"); + } +}); + +pub const APP_NAME: &str = "Generic API Service"; + +#[tokio::main] +async fn main() -> Result<(), Report> { + dotenv::dotenv().ok(); + + // prepare CLI + let cli = cli::Cli::parse(); + + // load config file + let mut config = config::Configuration::new(cli.config())?; + config.check()?; + + println!("{config:?}"); + + // handle CLI input + let daemon = cli.handle(&mut config).await?; + + match daemon { + cli::DaemonStatus::NotRunning => {} + cli::DaemonStatus::Running((cancellation_token, shutdown_signal)) => { + // create task tracker + let tracker = TaskTracker::new(); + + // terminate signal + #[cfg(unix)] + let terminate = async { + signal::unix::signal(signal::unix::SignalKind::terminate()) + .expect("Failed to install termination signal handler") + .recv() + .await; + }; + #[cfg(not(unix))] + let terminate = std::future::pending::<()>(); + + let shutdown_signal = async { + match shutdown_signal { + Some(mut signal) => signal.recv().await, + None => std::future::pending::>().await, + }; + }; + + // wait for shutdown signals + tokio::select! { + _ = signal::ctrl_c() => {}, + _ = terminate => {}, + _ = shutdown_signal => {}, + } + + // send shutdown signal to application and wait + cancellation_token.cancel(); + + // close task tracker + tracker.close(); + + // Wait for everything to finish. + tracker.wait().await; + tokio::time::sleep(Duration::from_millis(200)).await; + + info!("{APP_NAME} gracefully shut down."); + } + }; + + // let _ = axum_test::start_axum().await.unwrap(); + + // let _ = ldap_test::try_authentication().await.unwrap(); + + Ok(()) +} diff --git a/src/migrations/01_api_keys.sql b/src/migrations/01_api_keys.sql new file mode 100644 index 0000000..bade7ad --- /dev/null +++ b/src/migrations/01_api_keys.sql @@ -0,0 +1,37 @@ +-- Table: public.apikeys + +-- DROP TABLE IF EXISTS public.apikeys; + +CREATE TABLE IF NOT EXISTS public.apikeys +( + "KeyID" character varying(32) COLLATE pg_catalog."default" NOT NULL, + "Name" character varying(255) COLLATE pg_catalog."default" NOT NULL, + "UserAuthRequired" boolean NOT NULL DEFAULT true, + "Hash" bytea NOT NULL, + "CreationDate" timestamp without time zone NOT NULL DEFAULT now(), + "LastChanged" timestamp without time zone NOT NULL DEFAULT now(), + CONSTRAINT apikeys_pkey PRIMARY KEY ("KeyID") +) + +TABLESPACE pg_default; + +ALTER TABLE IF EXISTS public.apikeys + OWNER to postgres; + +COMMENT ON COLUMN public.apikeys."KeyID" + IS 'UUID of API Key'; + +COMMENT ON COLUMN public.apikeys."Name" + IS 'Name/Description of API Key'; + +COMMENT ON COLUMN public.apikeys."Hash" + IS 'Hashed value of API Key'; + +COMMENT ON COLUMN public.apikeys."UserAuthRequired" + IS 'Indication if this api key requires additional user authentication'; + +COMMENT ON COLUMN public.apikeys."CreationDate" + IS 'Time of creation'; + +COMMENT ON COLUMN public.apikeys."LastChanged" + IS 'Time of last modification'; \ No newline at end of file diff --git a/src/migrations/02_users.sql b/src/migrations/02_users.sql new file mode 100644 index 0000000..cc01b3d --- /dev/null +++ b/src/migrations/02_users.sql @@ -0,0 +1,19 @@ +CREATE TABLE public.users +( + "UserID" character varying(10)[] NOT NULL, + "ActiveDirectoryAuth" boolean NOT NULL DEFAULT false, + "Name" character varying(250) NOT NULL DEFAULT '', + "Surname" character varying(250) NOT NULL DEFAULT '', + "Email" character varying(500) NOT NULL DEFAULT '', + "Password" character varying(255) NOT NULL DEFAULT '', + "CreationDate" timestamp without time zone NOT NULL DEFAULT NOW(), + "LastChanged" timestamp without time zone NOT NULL DEFAULT NOW(), + "StatusFlag" smallint NOT NULL, + PRIMARY KEY ("UserID") +); + +ALTER TABLE IF EXISTS public.users + OWNER to postgres; + +COMMENT ON TABLE public.users + IS 'Table containing user information'; \ No newline at end of file diff --git a/src/migrations/03_groups.sql b/src/migrations/03_groups.sql new file mode 100644 index 0000000..424db67 --- /dev/null +++ b/src/migrations/03_groups.sql @@ -0,0 +1,12 @@ +CREATE TABLE public.groups +( + "GroupID" serial NOT NULL, + "GroupName" character varying(255) NOT NULL DEFAULT '', + PRIMARY KEY ("GroupID") +); + +ALTER TABLE IF EXISTS public.groups + OWNER to postgres; + +COMMENT ON TABLE public.groups + IS 'Group information'; \ No newline at end of file diff --git a/src/migrations/04_user_permissions.sql b/src/migrations/04_user_permissions.sql new file mode 100644 index 0000000..66a67c3 --- /dev/null +++ b/src/migrations/04_user_permissions.sql @@ -0,0 +1,18 @@ +CREATE TABLE IF NOT EXISTS public.user_permissions +( + "UserID" character varying(10) COLLATE pg_catalog."default" NOT NULL, + "Permission" character varying(250) COLLATE pg_catalog."default" NOT NULL, + CONSTRAINT "DistinctUserPermission" UNIQUE ("UserID", "Permission"), + CONSTRAINT "UserID" FOREIGN KEY ("UserID") + REFERENCES public.users ("UserID") MATCH SIMPLE + ON UPDATE NO ACTION + ON DELETE NO ACTION +) + +TABLESPACE pg_default; + +ALTER TABLE IF EXISTS public.user_permissions + OWNER to postgres; + +COMMENT ON TABLE public.user_permissions + IS 'Contains the permissions for every user'; \ No newline at end of file diff --git a/src/migrations/05_group_permssions.sql b/src/migrations/05_group_permssions.sql new file mode 100644 index 0000000..634dc17 --- /dev/null +++ b/src/migrations/05_group_permssions.sql @@ -0,0 +1,18 @@ +CREATE TABLE IF NOT EXISTS public.group_permissions +( + "GroupID" integer NOT NULL, + "Permission" character varying(250) COLLATE pg_catalog."default" NOT NULL, + CONSTRAINT "DistinctGroupPermission" UNIQUE ("GroupID", "Permission"), + CONSTRAINT "GroupID" FOREIGN KEY ("GroupID") + REFERENCES public.groups ("GroupID") MATCH SIMPLE + ON UPDATE NO ACTION + ON DELETE NO ACTION +) + +TABLESPACE pg_default; + +ALTER TABLE IF EXISTS public.group_permissions + OWNER to postgres; + +COMMENT ON TABLE public.group_permissions + IS 'Group -> Permission relation'; \ No newline at end of file diff --git a/src/utils.rs b/src/utils.rs new file mode 100644 index 0000000..70d058c --- /dev/null +++ b/src/utils.rs @@ -0,0 +1,6 @@ +pub(crate) fn create_random(len: usize) -> String { + rand::Rng::sample_iter(rand::thread_rng(), &rand::distributions::Alphanumeric) + .take(len) + .map(char::from) + .collect() +}